let hasChanged = false
const nextState: StateFromReducersMapObject<typeof reducers> = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const actionType = action && action.type
throw new Error(
`When called with an action of type ${
actionType ? `"${String(actionType)}"` : '(unknown type)'
}, the slice reducer for key "${key}" returned undefined. ` +
`To ignore an action, you must explicitly return the previous state. ` +
`If you want this reducer to hold no value, you can return null instead of undefined.`
)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state
redux你用对了吗?
redux 的三大原则
redux
的开发和使用必须要遵循三大原则,即:关于第一点很容易理解,整个应用应当只有一个
store
,全局唯一的store
有利于更好的管理全局的状态,方便开发调试,对实现“撤销”、“重做”这类的功能也更加方便。第二点,
state
是只读的,因此,我们在任何时候都不应该直接修改state
,唯一能改变state
的方法就是通过dispatch
一个action
,间接的来修改,以此来保证对大型应用的状态进行有效的管理。第三点,要想修改
state
,必要要编写reducer
来进行,reducer
必须是纯函数,reducer
接收先前的state
和action
,并且返回一个全新的state
。什么是纯函数?
前面我们介绍
redux
三大原则的时候提到过,修改state
要编写reducer
,且reducer
必须是一个纯函数,那么问题来了,什么是纯函数呢?维基百科里是这么定义纯函数的:
简单总结一下,如果一个函数的返回结果只依赖他的参数,并且在执行过程中没有副作用,我们就把这个函数定义为纯函数。
举个🌰:
函数
add
就不是一个纯函数,因为函数add
的返回值依赖外部变量x
,输入一定的情况下,输出结果不确定。我们把上面的例子稍微调整下:
现在的函数就是一个纯函数,因为函数
add
的返回值永远只依赖他的入参a
和b
,不管外部变量x
的值如何变化,也不会影响到函数add
的返回值。再看一个例子:
现在,我们给函数
add
传一个对象,并且,在函数add
内部对这个对象的某个属性进行修改,在执行函数add
的时候修改了外部传进来的temp
对象,即产生了副作用,因此这不是一个纯函数。除了上面说的在纯函数内部不能修改外部变量,在函数内部调用
Dom api
修改页面、发送ajax
请求,甚至调用console.log
打印日志都是副作用,在纯函数中都是禁止的,也就说,在纯函数内部我们一般只做计算数据的工作,计算的时候不能依赖函数参数以外的数据。为什么reducer需要返回一个全新的state
上面我们介绍了什么是纯函数,
redux
里面规定reducer
必须是一个纯函数,并且每个纯函数需要返回一个全新的state,那么这里大家肯定就有一个疑问,为什么reducer
必须要返回一个全新的state
,直接修改完了state
再返回不行吗?带着这个问题,我们来举个例子验证下,假如我们在一个
reducer
里面直接修改state
的值,再返回修改后的state
会发生什么。我们定义三个组件:
App
、Title
和Content
。App
作为Title
和Content
的父组件,有一个默认的state
状态树,结构如下:初始state:
Title组件:
Content组件:
App组件:
reducer:
demo非常简单,我们在
App
组件里面触发一个dispatch
,发送一个action
,调用reducer
来修改state
里面的title
,我们点击“修改title名称”按钮,发现组件并没有按照我们的预期发生变化,但是查看state里面的数据发现,state的值却变化了。 页面并没有如预期发生变化: 这个例子很好的验证了redux
的说法,我们不能直接修改state
,并返回。现在调整下
reducer
,通过...
运算符重新新建一个对象,然后把state
所有的属性都复制到新的对象中,我们禁止直接修改原来的对象,一旦你要修改某些属性,你就得把修改路径上的所有对象复制一遍,例如,我们不写下面的修改代码:取而代之的是,我们新建一个
state
,新建state.title
,新建state.title.tip
。这样做的好处是可以实现共享结构的对象。比如,
state
和newState
是两个不同的对象,这两个对象里面的content
属性在我们的场景中是不需要修改的,因此content
属性可以指向同一个对象,但是因为title
被一个新的对象覆盖了,所以它们的title
属性指向的对象是不同的,使用一个树状结构来表示对象结构的话,结构如下如所示:
现在的
reducer
:重新点击 “修改title名称” 按钮,我们想要的效果就可以实现了。 好了,知道结果之后我们来稍微探究下背后的原因。
查看
redux
的combineReducers
源代码我们发现,
combineReducers
内部通过hasChanged = hasChanged || nextStateForKey !== previousStateForKey
来比较新旧两个对象是否一致,来判断返回nextState
还是state
,出于性能考虑,redux 直接采用了浅比较,也就是说比较的是两个对象的引用地址,所以,当reducer
函数直接返回旧的state
对象时,这里的浅比较就会失败,redux
认为没有任何改变,从而导致页面更新出现某些意料之外的事情。immer
上面我们已经分析了
redux
里面的reducer
为什么要返回一个全新的state
,但是,如果按照上面reducer
的写法,要修改的state
树层级深了之后,修改起来无疑是非常麻烦的,那么有没有什么快捷的方式可以方便我们直接修改state
呢?答案是有的。
immer
是mobx
的作者写的一个immutable
库,核心实现是利用ES6
的proxy
,几乎以最小的成本实现了js
的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对js
不可变数据结构的需求。当然,除了immer
之外,还有别的库也同样能解决我们的问题,但是immer
应该是最简单也是最容易上手的一个库之一了。如果你的工程使用的是
dva
,那么可以直接开启dva-immer
,就可以简化state
的写法。上面的例子就可以这么写:或者直接使用
immer
库来改进我们的reducer
写法:安装:
使用:
总结
本篇文章重点介绍了
redux
的相关概念,什么是纯函数,以及为什么reducer
需要返回一个全新的 state ?从源码角度分析了需要返回全新state的原因,最后引入了immer
库,引入了immutable
概念,redux 配合immer
可以方便我们便捷高效的用好redux
。