1. 概念
redux 是 react 的状态管理应用,状态管理主要是为了解决我们去保存一些全局的状态,否则在需要相互之间通信来拿到数据,嵌套层级过多,数据流无法较好地追踪。(可以运行在多端,客户端,服务端,原生应用等)。
redux 他可以帮助我们进行集中式的状态管理,我们不用关心状态是如何分布到每一个组件内的。
redux 视图让状态的变化变得可以追踪。
从他解决的方面其实我们也可以得到他的应用场景:
- 存在大量的状态需要共享,并且状态比较复杂,需要追踪。并且 Redux 本身也提供给我们一个可以追踪的工具【redux-devtools】(github.com/reduxjs/red…
2. 三大原则
1.单一数据源
整个应用的全局 state 被存储在一颗 object tree 中,并且 object tree 只存在一个唯一的 store 里面。
好处是:我们随时可以取出整个应用的状态,然后做持久化(比如针对整个应用的及时保存情况)。
追问:redux 如何跟应用 localStorage 或者 sessionStorage 做持久化?
- 通过使用 redux-persist 可以帮忙我们解决。
2.state 是只读的
只能通过触发事件,也就是触发 Action 来产生新的状态数据,Action 是描述一个变化事件的对象。
好处:能够使得视图或者网络请求都不能直接修改请求,而是通过 action 来告知需要修改请求的情况,这样的话,更改的顺序是固定的,严格按照一一排列执行,同时也不用担心竟态(race condition)的出现,同时方便对状态的追踪。
3. 使用纯函数来进行修改
概念: 什么是纯函数?
- 相同的输入始终输出相同的内容
- 不对外部进行操作,或者引用
然后举几个是纯函数的例子以及不是的情况 常见的数组操作函数,slice,和 splice, 比如一个数字是 const arr = [1,2,3,4,5] 我们会发现 slice(0,3) 始终都是[1,2,3] 而 splice(0,3) 第一次是[1,2,3],第二次就是[4,5]
// 不纯的
var minimum = 21;
var checkAge = function(age) {
return age >= minimum;
};
// 纯的
var checkAge = function(age) {
var minimum = 21;
return age >= minimum;
};
引用了外部的变量 minimum,checkAge 的结果就依赖于外部的状态,增加了系统的复杂度。
3. 源码部分:
- createStore 主要是有几个函数,createStore 里面返回了一个 store,里面返回了 getState,dispatch,subscribe 方法。在源码中,主要通过闭包返回相关的函数,保证里面的几个变量不会直接被外界改变,里面主要有几个参数分别是 currentState,currentReducer,currentListeners 和 nextListeners 以及 isDispatch 参数来完善 getState,dispatch,subscribe 的方法。 其中 getState 就是直接返回 currentState,而 dispatch 则是传入相关的 action,然后传入 reducer 中返回新的 state,在 dispatch 中有一个 isDispatch 参数是为了解决 reducer 里面如果有再次 dispatch 的情况,导致死循环。而对 listener 的订阅部分也有一个特殊的处理,有一个 ensureCanMutateNextListeners,主要将 currentListener 赋给 nextListener,主要解决的问题是,如果 listener 数组里面是比如三个,第一个是订阅 A,第二个是解绑 A,第三个是订阅 C。如果没有这样的处理的话,在遍历到第三个的时候,会发现 listener 就取值不到,因为数组已经被改变,然后报错了。
// 第一次订阅A listenerA
const unsubscribeA = store.subscribe(() => {
console.log('aaaa')
})
// 在第二次订阅时解绑A listenerB
store.subscribe(() => unsubscribeA())
// 触发第三次订阅 listenerC
store.subscribe(() => {
console.log('ccccc')
})
store.dispatch({ type: 'todos/todoAdded', payload: 'Try creating a store' })
// 源码部分
const listeners = currentListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
- bindActionCreators: 作用是将一个或多个 action 和 dispatch 组合起来生成 mapDispatchToProps 需要生成的内容 如果他是一个函数,他会直接包装为一个函数,如果是一个对象,会遍历里面所有的 key,最终生成 bindActionCreators 的对象。
用处主要是如果在不使用的情况下,调用 dispatch 的话需要
dispatch(XXAction.increase(1))
如果使用了以后就变成
XXAction.increase();不需要再写dispatch了。
包装的函数主要实现是传入 action,以及 dispatch,返回是
return function (){
dispatch(actionCreate.apply(this, arguments))
}
- combineReducers 主要是将多个 reducers 合成一个 reducer 函数,同时 combineReducer 要求默认返回 state。
出现的原因是:实际开发中所有 state 和 改变 state 的逻辑放在一个 reducer 中显然是不合适,combineReducers 的意义就在对各个业务模块的 reducer 进行组合,最后传递给 redux 的 createStore 方法,继而生成一个的状态树。
当发出一个 dispatch 以后,他会遍历所有的 reducer,找到里面名字对应的 action,然后更新值。
1. 提问:
- 如果我有两个 reducer,combine 以后,里面都有一个同名的 action,他触发的时候都会更新吗? 回答: 会的。
源码中做了几件事:
- 过滤不合法的 reducer
- 遍历所有合法的 reducer ,并得到新的 state 如果里面有任意一个 reducer 改变了,就使用新的 nextState,否则就使用旧的 state。
- 判断 state 是否更新,如果更新就返回新值,否则反之。
- 合并所有 state,并返回。
4. redux 的使用
可以参考官网进行学习。
5. 实现一个简单的 redux
源码实现
function Redux(reducer, state) {
let currentState = state;
let currentReducer = reducer;
let listenerList = [];
function getState() {
return currentState;
}
function dispatch(action) {
const newState = reducer(currentState, action);
currentState = newState;
debugger;
for (let i = 0; i < listenerList.length; i++) {
const element = listenerList[i];
element();
}
}
function subscribe(listener) {
listenerList.push(listener);
listener();
return () => {
const index = listenerList.indexOf(listener);
if (index !== -1) {
listenerList.splice(index, 1);
}
};
}
return {
getState,
dispatch,
subscribe,
};
}
6. redux 异步 以及中间件
- 问: 为什么需要特殊处理异步? 因为reducer里面需要返回纯函数,因此在里面不能处理异步的操作。因此需要通过中间件来进行加强。
这块我们先了解一下中间件,applyMiddleware 怎么实现的?
没有中间件之前,比如我们需要一个log的方法,记录每次dispatch的action Type。 我们都其实是先需要把这个store.dispatch 暂存起来,变成一个next,然后再后续使用的之前,先log 相应的type,再用前面的next dispatch action,然后再输出对应的state。
import store from './store'
import {userNameChange} from './store/user'
let next = store.dispatch //保存原来的dispatch完整功能
store.dispatch = (action) => { //重写dispatch,接收一个action
console.log('dispatch', action.type)
let result = next(action) //执行原来的dispatch功能
console.log('newState', store.getState())
return result
}
let action = userNameChange('xiaoming')
store.dispatch(action)
类似上面这样,然后就可以继续理解相应applyMiddleware的源码了。 他需要middlewares 数组传入,需要加强的中间件,然后把他的下一个参数需要传入createStore,以及剩余...args,就是我们的reducer,然后使用enhancer来进行增强。
enhancer(createStore)(reducer, preloadedState)
在处理store的时候,我们会把的dispatch先暂存起来,然后增强。 主要是先通过middlesware 传入相应的中间件可以访问的参数{getState, dispatch},然后后面再传入store.dispatch. 通过compose的方法,使得中间件从右到左执行。
function applyMiddleware(...middlewares) {
//createStore会判断如果执行applyMiddleware返回函数,创建store的工作就交由下面的代码来执行
//返回一个处理过dispatch的store,现在的...args为我们传入的reducer。
//createStore的代码为 enhancer(createStore)(reducer, preloadedState)
return (createStore) => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = { //中间件可访问的参数
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI)), //接收next,返回dispatch的函数组成的数组。
dispatch = compose(...chain)(store.dispatch) //原始dispatch传入compose生成的函数被链式处理。
return {
...store,
dispatch //被处理过的dispatch
}
}
}
相应的compose实现,通过reduce来实现从右到左的参数输出。
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
7. react 为什么能运行 redux,如何实现 react 和 redux 相关的关联。
通过react-redux 来进行实现,其实redux只是一个状态管理的库,除了react,vue中也可以使用。
订阅流程 整个订阅的流程是,如果被connect包裹,并且具有第一个参数。首先通过context获取最近的 subscription,然后创建一个新的subscription,并且和父级的subscription建立起关联。当第一次hoc容器组件挂在完成后,在useEffect里,进行订阅,将自己的订阅函数checkForUpdates,作为回调函数,通过trySubscribe 和this.parentSub.addNestedSub ,加入到父级subscription的listeners中。由此完成整个订阅流程。
更新流程 整个更新流程是,那state改变,会触发根订阅器的store.subscribe,然后会触发listeners.notify ,也就是checkForUpdates函数,然后checkForUpdates函数首先根据mapStoretoprops,mergeprops等操作,验证该组件是否发起订阅,props 是否改变,并更新,如果发生改变,那么触发useReducer的forceComponentUpdateDispatch函数,来更新业务组件,如果没有发生更新,那么通过调用notifyNestedSubs,来通知当前subscription的listeners检查是否更新,然后尽心层层checkForUpdates,逐级向下,借此完成整个更新流程。
8. redux 的缺点
- 如果使用了 reducer combine,他会遍历所有的 reducer,如果太大的会有可能性能不太好
参考:
- 浅谈前端的状态管理,以及 anguar 的状态管理库【segmentfault.com/a/119000004…
- redux-persist v6 持久化数据 & 页面刷新缓存初始化问题【juejin.cn/post/707302…
- redux 源码-详解 ensureCanMutateNextListeners 函数【juejin.cn/post/717443…
- Redux 源码分析(4) - combineReducers 和 bindActionCreators 【juejin.cn/post/684490…
- Redux的异步解决方案【juejin.cn/post/689525…
- 浅谈redux、applyMiddleware、redux-thunk【juejin.cn/post/690233…
- 一文吃透react-redux 8.0 源码【zhuanlan.zhihu.com/p/473671316…
- 「源码解析」一文吃透react-redux源码(useMemo经典源码级案例)【zhuanlan.zhihu.com/p/404025461…