在组件化开发中,不管是使用Vue还是React,都会遇到组件间数据传递的问题,两个框架都有各自的解决方法,一般采用的方法无非是
- 将数据放到全局供任何地方调用
- 通过组件间层层传递来共享数据
- 通过发布订阅的方式
一般情况下,如果只是父组件传到子组件,我们可以直接采用props来传递数据,不管在Vue中还是在React中都可以,但是一旦有多层组件嵌套,那么层层传递数据就会显得非常麻烦,而且后期也难以维护,为了解决这个问题,在React开发中,我们可以使用redux(在Vue中可以使用vuex:Vue学习笔记 在模块化开发中使用Vuex)
Redux
简介
redux可以直接通过npm来安装
npm i redux --save
Redux和Vuex一样,都是状态管理器,Redux中通过将数据放到state仓库中,数据类型使用普通的对象来描述
{
todoList:[{
content:'todo 1',
completed:false
},{
content:'todo 2',
completed:true
}],
count:2
}
区别于一般对象的是,该对象没有setter,即不能直接修改这个对象,在Redux中,我们只允许通过发起action来修改state仓库,如果随意修改state,可能会造成难以修复的bug。通过发起action来修改Redux可以使数据流动更在清晰和可预测,且通过发起action来修改可以在redux-devtools中观察到,而直接修改state可能会无法触发props更新。
// action的发起举例
let action={
type:"changeTitle",
value:e.target.value
}
store.dispatch(action)
通过声明一个action,使用dispatch将其传递到reducer,reducer返回一个新的state供我们使用
Redux的三个原则
- 单一数据源
整个React应用的数据被保存在一个对象树中,并且该树只存在唯一一个store中
{
todoList:[{
content:'todo 1',
completed:false
},{
content:'todo 2',
completed:true
}],
count:2
}
- state是只读的
在Redux中,只能通过发起action来修改state,不能用其他方式来修改(这里的不能更多是一种规则/约束,实际上还是可以修改的,但是不建议这么做,会引起不必要的bug)
// action的发起举例
let action={
type:"changeTitle",
value:e.target.value
}
store.dispatch(action)
- 使用纯函数来执行修改
Redux使用reducer来接收state和action,然后将新的state返回给store仓库。
function reducerFn(state=defaultState,action){
if(action.type==='SHOW_DATA'){
return action.value;
}
return state;
}
纯函数:返回的值只受传入的参数的影响,不受其他因素的影响
如下面这个函数就不是纯函数
function showDate(){
return new Date();
}
这个函数受现在的时间的因素影响,所以并非纯函数
核心
Action
前面已经说了,发起action是唯一可以修改state的方法,一个简单的action就只是一个普通的对象,在Redux中,约定action内必须有一个type属性,使用该属性来判断接下来要执行的动作,多数情况下,这个属性会被定义为字符串常量,当使用规模变大时,建议采用单独的模块或者文件来存放action,通过在action发起的文件/模块和reducer文件/模块中引入这些常量,可以让我们在字符串写错的时候有明确的报错。如果不使用这种方式,可能导致我们在发起action时因为type写错而静默失败
// actionTypes中声明常量
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO'
// action发起的文件和reducer文件引入常量
import { ADD_TODO, TOGGLE_TODO } from '../actionTypes'
通过函数来创建action
每当我们要修改state时,我们就需要发起action,但是每次都编写这些代码会显得冗余,而且也容易出错,一旦我们要修改常量名对应得那个变量,还要对每个都进行修改,通过使用函数来创建action就可以解决这些问题,使代码更容易维护
function createAddAction(val){
return {
type:ADD_TODO,
val
}
}
通过这种写法,使得我们可以更好地复用,在使用时,我们只要dispatch这个函数就可以了
store.dispatch(createAddAction('newVal'))
Reducer
Reducer是一个接收state和action的纯函数,通过action的type,来决定对state执行什么修改,而接收的state作为默认值,在一开始没有action时,就返回默认的state
function reducerFn(state=defaultState,action){
if(action.type==='ADD_TODO'){
// ...
return newState;
}else if(action.type==='TOGGLE_TODO '){
// ...
return newState;
}
// ...
return state;
}
根据官方文档中的要求,不要在reducer中执行下面的操作
- 修改传入的参数,即action和state
- 执行有副作用的操作,如API请求和路由跳转
- 调用非纯函数,像Date.now()或Math.random()
因为不能修改state,但是有时我们只是修改state中的一部分内容,希望能在原来的state的基础上进行修改再return回去,此时我们就要使用深拷贝
官方文档中使用Object.assign()
let newState=Object.assign({}, state, {
visibilityFilter: action.filter
})
事实上,我们可以直接采用JSON方法来做到深拷贝
let newState=JSON.parse(JSON.stringify(state));
拆分Reducer
现在我们只是在做一个小的demo,那么当我们的应用变大时,由于state是单一的,我们不能像vuex里面拆分成多个文件,每个文件有自己的state
为了解决这个问题,我们可以拆分Reducer,每个拆分出来的Reducer去管理单一state中的不同部分的数据
function todos(state = [], action) {
// ...
}
function visibilityFilter(state = SHOW_ALL, action) {
// ...
}
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
上面的代码中,由todos和visibilityFilter分别管理state中的todos和visibilityFilter,通过这种方式,我们可以把管理不同部分的方法放到不同的文件中,再在总的reducer文件中引入,这样就可以达到拆分state的目的了
此外,在Redux中提供了combineReducers() 工具类来完成上面todoApp的工作
import { combineReducers } from 'redux'
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
// 上面的代码和下面的代码完全等价
export default function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
Store
Store实际上就是用来存储数据的,在一个应用中,Store有下列的职责
- 保存整个应用的数据
- 通过
getState()
来获取state中的数据
store.getState()
返回state对象树 - 通过
dispatch(action)
更新state - 通过
subscribe(listener)
注册监听器
这里可以使用下面的代码来在该组件中注册监听器
store.subscribe(()=>{
this.setState(store.getState())
})
- 通过
subscribe(listener)
返回的函数注销监听器
创建store
创建store非常简单,使用createStore方法,传入reducer即可
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
createState的第二个参数是可选的,用于设置state的初始状态,值得一提的是,如果要使用redux-devtools,那么就要在第二个参数中写上window.__REDUX_DEVTOOLS_EXTENSION__&&window.__REDUX_DEVTOOLS_EXTENSION__()
const store=createStore(
todoApp ,
window.__REDUX_DEVTOOLS_EXTENSION__&&window.__REDUX_DEVTOOLS_EXTENSION__()
);
如果不写上这行,就会出现下面的情况
Redux中的数据生命周期
- 发起action(调用store.dispatch(action))
- Redux调用reducer中的函数
通过store.dispatch,store将原来的state和action传给reducer,触发reducer中的函数 - 根reducer将多个子reducer的输出合并成一个单一的state树
如果没有子reducer,那么就直接输出一个新的state树,如果有子reducer,那么首先要让各个子reducer输出各自管理的值,然后在根state树合并为一个单一的state树 - 调用store.subscribe(listener) 的监听器来更新UI
在通过action发起修改state后,调用store.subscribe(listener)
的监听器,让与修改数据相关的UI更新
具体在demo中如何使用,详见React入门 制作简单的todolist