-
Notifications
You must be signed in to change notification settings - Fork 1
Description
作为 react 技术栈的同学来说,redux 可谓是老常客 —— 一个主要用于状态管理的框架,经常出现在各种大大小小的项目中,其重要性不言而喻。对于如此重要的状态管理库,如果能知其然,也知其所以然(了解其内部原理),想必能给我们的工作、学习带来很大的帮助。
笔者最近在解读 redux 源码时,发现其代码非常简练,利于理解,很适合阅读。如果你有解读各类源码的想法,不妨先从它入手。在本篇文章中,笔者将尝试用一张图描绘出 redux 最为本质的运作流程,并配合每个环节的文字 + 代码描述,帮助大家更好地了解 redux,同时如果你已经开始在学习其源码,相信也能给你带来些许帮助。
另外,本文所使用的版本为 4.0.5,感兴趣的同学可在这里下载源码。
在4.0.5中,其./src下的目录结构如下所示,后面的解读同样基于此结构进行:
一张图
首先我们先看看,当在外部发起一个 action,直到 state 完成更新并下发通知订阅者的这个过程中 redux 做了什么,可以用一张图来概括:
总得来说流程并不复杂:
- 我们使用
actionCreator发起action(当然actionCreator的使用只是一种规范约束,你可以直接dispatch(action)或者使用bindActionCreators,这取决于你及你的团队风格) action首先经过一系列中间件middlewareXX的处理(可以留意下这些中间件的图形结构,它比较类似 koa 的洋葱模型,它们本身是层层包裹的,后面会详细说明)- 纯函数
combination同时接受传入的action及当前的 state 树,即currentState,并分发给对应的reducer分别计算不同的subState - 完成计算后,生成新的 state 树,即
newState,然后赋值给currentState,并通知所有的listeners本次更新完成
需要注意的是,上文中的中间件不是必须的,它只是个可选项,如果没了它,整个流程会更简单:action 直接进入 combination。
大体流程就这样,我们再看看每个模块的具体实现
模块解读
我们将按照上图流程,依次介绍每个模块的实现。另外,为了确保文章的简明扼要,笔者对源码进行了部分删减,建议读者配合源码一起食用,效果更佳:)
createStore.js
createStore 方法可谓是 redux 的集大成者,绝大部分模块,最终都在此函数内发挥其作用,所以我们先以全局的视角看看它内部到底干了些什么。
首先我们先忽略 createStore 内部各具体函数的实现细节:
export default function createStore(reducer, preloadedState, enhancer) {
// 参数校验
if (typeof enhancer !== "undefined") {
if (typeof enhancer !== "function") {
throw new Error("Expected the enhancer to be a function.");
}
// 后面会具体讲解此实现
return enhancer(createStore)(reducer, preloadedState);
}
if (typeof reducer !== "function") {
throw new Error("Expected the reducer to be a function.");
}
let currentReducer = reducer;
let currentState = preloadedState;
let currentListeners = [];
let nextListeners = currentListeners;
let isDispatching = false;
function ensureCanMutateNextListeners() {
// some ignored code...
}
function getState() {
// some ignored code...
}
function subscribe(listener) {
// some ignored code...
}
function dispatch(action) {
// some ignored code...
}
function replaceReducer(nextReducer) {
// some ignored code...
}
function observable() {
// some ignored code...
}
dispatch({ type: ActionTypes.INIT });
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable,
};
}从函数的参数定义上,我们可知道它需要传入一个必须参数 reducer 及两个可选参数 preloadedState 和 enhancer,这与文档 API 保持一致。它具体做了以下几件事:
- 校验参数的合法性,其中的
enhancer需要注意下,后面关于applyMiddlewares的实现也与它相关; - 定义了一系列的内部变量,如
currentStatecurrentReducer; - 定义了内部私有方法
ensureCanMutateNextListeners,主要配合currentListeners与nextListeners维护发布-订阅的可靠性与有序性; - 定义了一系列对外输出的函数,如我们常用的
dispatch,subscribe,getState,及在代码分割场景下需动态 rootReducer 替换用到的replaceReducer和在响应式编程上(如 rxjs)可配合使用的observable - 调用内部行为
ActionTypes.INIT通知 store 被初始化 - 最终输出一个集成上述函数的对象
store
从输出的对象上看也印证了上面的观点 —— createStore 返回的 store 确实是集大成者,接下来我们看看这些内部方法的具体实现(考虑篇幅关系,笔者只挑选了比较常用的几种方法进行讲解)
dispatch
function dispatch(action) {
if (!isPlainObject(action)) {
throw new Error(
"Actions must be plain objects. " +
"Use custom middleware for async actions."
);
}
if (typeof action.type === "undefined") {
throw new Error(
'Actions may not have an undefined "type" property. ' +
"Have you misspelled a constant?"
);
}
if (isDispatching) {
throw new Error("Reducers may not dispatch actions.");
}
try {
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
return action;
}可以看到此函数的最核心之处在于调用纯函数 currentReducer 生成新的 currentState, 并在结束后遍历 listeners 触发订阅回调。这里需要主要下标识变量 isDispatching,在 createStore 内部各处都会出现它的身影,它充当“锁”的作用 —— 当在调用 reducer 重新计算 store 时,会锁住以下功能:
- 不允许再次触发
dispatch - 不允许新增或取消订阅
- 不允许
getState
以上行为当且仅当在非 dispatching 阶段才开放
subscribe
function subscribe(listener) {
if (typeof listener !== "function") {
throw new Error("Expected the listener to be a function.");
}
if (isDispatching) {
throw new Error(
"You may not call store.subscribe() while the reducer is executing. " +
"If you would like to be notified after the store has been updated, subscribe from a " +
"component and invoke store.getState() in the callback to access the latest state. " +
"See https://redux.js.org/api-reference/store#subscribelistener for more details."
);
}
let isSubscribed = true;
ensureCanMutateNextListeners();
nextListeners.push(listener);
return function unsubscribe() {
if (!isSubscribed) {
return;
}
if (isDispatching) {
throw new Error(
"You may not unsubscribe from a store listener while the reducer is executing. " +
"See https://redux.js.org/api-reference/store#subscribelistener for more details."
);
}
isSubscribed = false;
ensureCanMutateNextListeners();
const index = nextListeners.indexOf(listener);
nextListeners.splice(index, 1);
currentListeners = null;
};
}subscribe 主要用于新增订阅者,并返回一个取消订阅的函数 unsubscribe。大家需要注意下这里的两个订阅者列表 currentListeners 与 nextListeners,也许你会很好奇为什么 redux 需要在内部维护两个订阅者列表呢?因为 redux dispatch 完后、在遍历 listeners 并触发订阅回调时,它并不清楚订阅者中会不会存在新增或取消订阅的行为,为了保证发布-订阅的可靠性与有序性,它通过两个列表来实现“快照”功能 —— currentListeners 表示当前的订阅者列表,store 更新完毕后,redux 是遍历此订阅列表进行消息通知,而如果在此期间发生了新增或取消订阅,则会把这部分的变化更新到 nextListeners 中 —— 这意味着 nextListeners 始终存放着变动的、未来的订阅者,关于这一点的设计,在返回的 unsubscribe 及 dispatch 函数中都均有体现
PS: 在此期间 ensureCanMutateNextListeners 函数也发挥重要作用,用于确保两个订阅者列表是不同的引用,一旦两者引用相等时,则使用浅拷贝的方式再次分开两者
getState
function getState() {
if (isDispatching) {
throw new Error(
"You may not call store.getState() while the reducer is executing. " +
"The reducer has already received the state as an argument. " +
"Pass it down from the top reducer instead of reading it from the store."
);
}
return currentState;
}
getState 就非常简单了,直接返回内部的 currentState 即可
createStore 的介绍到这里基本就结束了,接下来我们再一一介绍其他相关模块。我们就从一个 action 的发起开始吧,与 action 发起相关的函数为 bindActionCreator,一起看看它的内部实现。
bindActionCreator.js
从 redux 官方文档可知,函数 bindActionCreator 的作用在于绑定 action 与
dispatch,这样就无须单独引入 dispatch 并手动调用,其在项目开发中的好处不言而喻:例如对于 react 展示型组件,内部则无须引入 dispatch,取而代之的是由父组件传递而来的经过 bindActionCreators 包装过后的函数,使其充分解耦。
我们来看看 redux 是怎么实现它的:
function bindActionCreator(actionCreator, dispatch) {
return function () {
return dispatch(actionCreator.apply(this, arguments));
};
}
export default function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === "function") {
return bindActionCreator(actionCreators, dispatch);
}
// some ignored code here...
const boundActionCreators = {};
for (const key in actionCreators) {
const actionCreator = actionCreators[key];
if (typeof actionCreator === "function") {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
}
}
return boundActionCreators;
}上述代码最为核心的是 bindActionCreator 函数,它借助闭包,使得返回的函数拥有访问 dispatch 的权限,以此实现绑定。由于代码过于简单,这里就不再赘述,我们继续往下看,当一个 action 发起后,首先它会经过哪个模块呢?是直接进入 reducer 重新计算 store 吗?当项目没有应用任何中间件时,确实是这样的,但一旦传入了中间件后,它首先需要经过一层层中间件的“洗礼”,因此在介绍 combineReducers 前,首先看下与中间件机制相关的函数 applyMiddleware 吧~
applyMiddleware.js
当 redux 引入中间件后,action 首先要经过的是层层包裹的中间件,关于 redux 对中间件的实现思路,其实官网上已经给出了自己的推演说明,讲解得非常精彩,有兴趣的同学可直接戳 中文文档 进行查看
import compose from "./compose";
export default function applyMiddleware(...middlewares) {
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));
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch,
};
};
}首先 redux 引入了工具函数 compose,它是用来干什么的呢?我们先看看其代码:
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)));
}从这段代码可以看出,compose 的作用是:传入一组任意数量的函数,比如 funcA, funcB,funcC,可生成一个新的函数 (...args) => funcA(funcB(funcC(...args))),**它的含义是每个函数均以上一个函数的返回值为参数传入,并将自己计算得到的返回值作为下一个函数的参数。**对于第一次接触 compose 概念的童鞋来说可能比较绕,可多花思考体会一下。
回到代码本身,当引入了 compose 之后,下面则为 applyMiddleware 函数的实现:当我们执行 applyMiddleware(...middlewares) 时,它返回了一个诸如 (createStore) => (...args) => { ... } 的柯里化函数,等等...这是什么鬼!?从代码实现上看,它确实有点绕,因此对于后面的讲解希望大家能打起十二分精神,结合源码,多看几次
化简后的 applyMiddleware
笔者将上述代码进行了化简,同时截取了部分 createStore 的代码,结合阅读更便于理解它的工作原理:
export default function applyMiddleware(...middlewares) {
return (createStore) => (...args) => {
const store = createStore(...args);
// some ignored code...
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch,
};
};
}截取的部分 createStore
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof enhancer !== "undefined") {
if (typeof enhancer !== "function") {
throw new Error("Expected the enhancer to be a function.");
}
return enhancer(createStore)(reducer, preloadedState);
}
1;
// some ignored code...
}createStore 函数的第三个参数 enhancer,其实就是 applyMiddleware() 执行后返回的函数,显而易见,当存在 enhancer 时,redux 会做一层递归 —— 把 createStore 及传入的 reducer 和 preloadedState 分别传递给 enhancer,由它的内部完成 store 的创建工作,并返回出去供业务使用;
由于 enhancer 获得了创建并返回 store 的权利,因此可以偷偷给 store “做手脚”:比如在 applyMiddlewares 内部它篡改了 dispatch 函数 —— 它借助 compose + middlewares 对最初始的 dispatch 函数进行层层包装,以实现各种丰富功能;
看到这里,我们已经对 applyMiddlewares 的大致功能有了了解,接下来我们再看看它是如何通过包装 dispatch 以实现中间件功能的
dispatch 的层层包装
光看 applyMiddleware 的内部实现可能还无法完全理解,需要加上一个简易的中间件配合阅读,这里笔者实现了个非常简单的类似 redux-thunk 的功能 —— 赋予 redux 处理异步函数的能力:
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));
dispatch = compose(...chain)(store.dispatch);一个简易 redux-thunk 的具体实现
const myThunk = ({ getState, dispatch }) => (next) => (action) => {
if (typeof action === "function") {
action(dispatch, getState);
} else {
next(action);
}
};它们的执行流程如下:
- 首先执行
middleware(middlewareAPI)返回格式为next => action => {...}的函数,函数通过闭包,可随时访问dispatch和getState; - 借助上述
compose机制,next => action => {...}函数中的next为上个函数的执行结果,而自身计算的结果将作为下一个函数的参数使用,因此效果类似如下代码:const finalDispatch = funcA(funcB(funcC(store.dispatch))); store.dispatch = finalDispatch;
- 由于
finalDispatch最终会触发原本的store.dispatch,不仅机制上来说结果不变,而且中间件们还能在这期间实现自己的功能:比如处理异步函数、promise、输出日志等等;
以上代码可以得出 redux 实现中间件的核心思想在于 dispatch 函数的包装与复写 —— 通过一系列中间件的层层包装,我们最终在外边拿到的,并非是最初在 createStore 内部定义的原汁原味的 dispatch,它早已被 middleware 们“动了手脚”,也正是因为改写了 dispatch,诸如 redux-thunk、redux-promise 等中间价赋予了 redux 处理异步的能力,actionCreator 的返回值得到了拓展,它不再必须为纯对象,返回函数或 promise 也是可以的
combineReducers
其实我们在使用 reducer 时,不一定非要使用 combineReducers 函数,它只是提供了组合多个子 reducer 的一种方式。关于 combineReducers 的实现,官方已经在文档上给出了大概的实现,但我们还是来看看真实的内部代码是怎样的吧:
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers);
const finalReducers = {};
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
if (process.env.NODE_ENV !== "production") {
if (typeof reducers[key] === "undefined") {
warning(`No reducer provided for key "${key}"`);
}
}
if (typeof reducers[key] === "function") {
finalReducers[key] = reducers[key];
}
}
const finalReducerKeys = Object.keys(finalReducers);
// This is used to make sure we don't warn about the same
// keys multiple times.
let unexpectedKeyCache;
if (process.env.NODE_ENV !== "production") {
unexpectedKeyCache = {};
}
let shapeAssertionError;
try {
assertReducerShape(finalReducers);
} catch (e) {
shapeAssertionError = e;
}
return function combination(state = {}, action) {
if (shapeAssertionError) {
throw shapeAssertionError;
}
if (process.env.NODE_ENV !== "production") {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
);
if (warningMessage) {
warning(warningMessage);
}
}
let hasChanged = false;
const nextState = {};
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 errorMessage = getUndefinedStateErrorMessage(key, action);
throw new Error(errorMessage);
}
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length;
return hasChanged ? nextState : state;
};
}笔者上面所贴的代码,其实删减了一些跟告警、校验相关的函数。combineReducers 做了以下几件事情:
- 校验 reducers 的合法性,包括:是否存在 undefined 的值、剔除假 reducer(即判定是否为函数),以及判定每个 reducer 能否正常处理未知 action、能否在初始化正常返回 initialState 等等;
- 返回了一个新函数
combination,也就是我们常说的rootReducer - 当执行
combination后,combination先进行参数校验,随后通过遍历finalReducerKeys获取每个 state 的 key 及对应的 reducer,并计算出新的 state 即nextStateForKey - 当存在
nextStateForKey !== previousStateForKey时,则返回新的 state, 否则返
回旧的 state (这里需要注意下,它是通过标志位hasChanged来判断 state 是否有更新的)
结尾
写到这里,redux 的代码就基本读完了,总的来说,如果要让笔者用一个词来形容它,那就是“精炼”——会有一种麻雀虽小五脏俱全的感觉,而且有些地方的实现也非常巧妙,还是很推荐大家有时间读一读源码,相信会非常有收获的:) 由于笔者能力有限,可能存在部分解读不当的情况,欢迎大家拍砖~