写点什么

redux 源码学习

作者:yuanyxh
  • 2024-09-14
    中国香港
  • 本文字数:8895 字

    阅读完需:约 29 分钟

redux 源码学习

前言

redux,一个继承了 flux 架构思想的状态管理库,不与任何 UI 框架、库强绑定,因为其不与框架耦合的特性,所以在状态变更时无法自动触发视图更新,且配置较为繁琐,在使用体验上不如一些较新的状态管理库。


为什么学习它?在几个月前笔者学习 React 及其周边库时,对 Redux 的中间件实现颇感兴趣,所以翻看了对应的源码,以此进了函数式编程的门,这次想对 Redux 的完整源码进行学习。


Redux 版本 4.2.1,建议阅读文档熟悉相关 Api 后进行学习。

createStore

在使用 Redux 时,通常使用 createStore 函数进行状态(store)的创建,它在 /src/index.js 中被导出,代码省略如下:


// src/index.js
import { createStore } from './createStore';
export { createStore };
复制代码


可以看到是从同级的 createStore.js 文件导入进来的,该 js 文件中只定义了一个 createStore 函数,这个函数可以说是 redux 的核心。


下面我们就一步一步的学习 createStore 函数的代码,首先查看它的函数签名:


// src/createStore.js
export function createStore(reducer, preloadedState, enhancer) {}
复制代码


在函数签名中可以发现它定义了三个形参:


  • reducer:作用固定,即用户定义的用于更新状态的纯函数

  • preloadedState:作用不固定,可能是初始状态,也可能是中间件

  • enhancer:可能是中间件函数,也可能为 undefined


preloadedStateenhancer 的作用在后续代码的判断中可以知道:


// src/createStore.js
export function createStore(reducer, preloadedState, enhancer) { if ( (typeof preloadedState === 'function' && typeof enhancer === 'function') || (typeof enhancer === 'function' && typeof arguments[3] === 'function') ) { throw new Error( 'It looks like you are passing several store enhancers to ' + 'createStore(). This is not supported. Instead, compose them ' + 'together to a single function. See https://redux.js.org/tutorials/fundamentals/part-4-store#creating-a-store-with-enhancers for an example.' ); }
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState; preloadedState = undefined; }
if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error( `Expected the enhancer to be a function. Instead, received: '${kindOf( enhancer )}'` ); }
return enhancer(createStore)(reducer, preloadedState); }
if (typeof reducer !== 'function') { throw new Error( `Expected the root reducer to be a function. Instead, received: '${kindOf( reducer )}'` ); }}
复制代码


在第一个判断中判断了 preloadedStateenhancer 是否是函数,或者 enhancer 和可能传入的第四个参数是否是函数,如果这两个中的其中一个满足了条件即抛出错误,根据错误信息可以知道 redux 不允许直接传递多个中间件,而建议将多个中间件以组合的方式组合为一个函数。


在第二个判断中判断了 preloadedState 是否是一个函数且 enhancerundefined,当条件满足时, redux 会认为用户没有传入初始状态,只传入了中间件函数,此时会交换两者的值以明确它们的含义以完成后续的操作。


第三个判断限制了 enhancer 只能为 undefined 或是一个中间件,当 enhancer 是一个中间件时,会执行 enhancer(createStore)(reducer, preloadedState),这部分代码在后续关于中间件的实现中再讲解。


第四个判断限制了 reducer 必须是一个函数。


通过这四个判断我们可以发现几种常见的调用 createStore 的方式:


import { createStore, applyMiddleware } from 'redux';
// 传入 reducer 及初始状态createStore(reducer, initState);
// 中间件createStore(reducer, applyMiddleware(/** 一个或多个中间件 */));createStore(reducer, initState, applyMiddleware(/** 一个或多个中间件 */));
复制代码


当我们需要传入中间件时,可以使用 redux 提供的工具函数 applyMiddleware,这个函数会初始化 store 并将传入的中间件挂载。


在经过几个判断后,createStore 定义了几个变量,这些变量是 redux 用于维护内部行为的:


// src/createStore.js
export function createStore(reducer, preloadedState, enhancer) { /** ..... */
/** reducer 纯函数 */ let currentReducer = reducer; /** 当前状态 */ let currentState = preloadedState; /** 当前侦听器 */ let currentListeners = []; /** 替换后的侦听器 */ let nextListeners = currentListeners; /** 是否在更新状态 */ let isDispatching = false;}
复制代码


后续的代码因为代码顺序写的比较乱,所以从调用者的角度去讲解相关的代码,当我们调用 createStore 时,它是返回了一个 store 对象的,而我们则依靠这个对象提供的方法进行状态的更新维护:


// src/createStore.js
export function createStore(reducer, preloadedState, enhancer) { /** ..... */
return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable };}
复制代码

dispatch

我们使用 redux 来管理状态时,使用 dispatch 方法来进行状态的更新,它的实现如下:


// src/createStore.js
function dispatch(action) { if (!isPlainObject(action)) { throw new Error( `Actions must be plain objects. Instead, the actual type was: '${kindOf( action )}'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.` ); }
if (typeof action.type === 'undefined') { throw new Error( 'Actions may not have an undefined "type" property. You may have misspelled an action type string 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;}
复制代码


开头进行了三个判断,前面两个判断限制了 dispatch 必须传入一个参数 action,且必须是一个带有 type 属性的普通对象;随后判断当 isDispatching 的值为真时抛出一个错误,这是一个常见的开关操作,可以避免一些不合理操作导致的错误,如在 reducer 函数中进行 dispatch 的调用。


之后尝试进行状态更新的操作,先将 isDispatching 设置为真,将通道关闭,防止在本次状态更新完毕前又触发一次更新操作,然后调用传入的 reducer 函数并传入当前状态和 action 来进行状态的更新,然后将通道打开。


函数最后就是遍历监听器数组,将更新操作告诉每个侦听器,这里个人感觉进行一层比较会更好,有时候我们会将状态原样返回表示我们不需要此次更新,加上一层对比可以阻止侦听器的触发,redux 一般是在 UI 框架中使用的,状态变更通常意味着 UI 框架会重新渲染组件,加上一层对比也能有效防止组件重渲染的情况。

subscribe

当我们进行状态的更新后,我们是希望得知状态的变化从而去做一些事情的,为此 createStore 返回了一个 subscribe 方法,这个方法允许我们注册若干个侦听器,当状态变更时侦听器就会被触发,subscribe 代码如下:


// src/createStore.js
function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice(); }}
function subscribe(listener) { if (typeof listener !== 'function') { throw new Error( `Expected the listener to be a function. Instead, received: '${kindOf( listener )}'` ); }
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/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/store#subscribelistener for more details.' ); }
isSubscribed = false;
ensureCanMutateNextListeners(); const index = nextListeners.indexOf(listener); nextListeners.splice(index, 1); currentListeners = null; };}
复制代码


开始还是经典的进行判断,主要是限制 subscribe 方法的入参,以及避免在状态更新中进行添加侦听器的操作,然后声明了一个 isSubscribed 变量,这也是一个开关变量,用于避免重复卸载侦听器;随后调用了 ensureCanMutateNextListeners 函数,这个函数的作用就是区分开 currentListenersnextListeners 这两个变量的引用,最后将传入的参数 listener 添加进 nextListeners 数组中。


subscribe 的返回值是一个 unsubscribe 函数,用于将当前添加的侦听器移除。

getState

获取状态的方法,单纯的返回当前状态,代码如下:


// src/createStore.js
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;}
复制代码

replaceReducer

这是一个替换 reducer 状态更新函数的方法,代码如下:


// src/createStore.js
function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error( `Expected the nextReducer to be a function. Instead, received: '${kindOf( nextReducer )}` ); }
currentReducer = nextReducer;
// This action has a similiar effect to ActionTypes.INIT. // Any reducers that existed in both the new and old rootReducer // will receive the previous state. This effectively populates // the new state tree with any relevant data from the old one. dispatch({ type: ActionTypes.REPLACE });}
复制代码


判断参数的有效性后会替换 currentReducer 变量的值,需要注意的是在替换后还默认调用了一次 dispatch,这是为了更新状态,代码中的 ActionTypes 定义在 /src/utils/actionTypes.js 里,里面存放了一些 redux 内部使用的 type


// src/utils/actionTypes.js
const randomString = () => Math.random().toString(36).substring(7).split('').join('.');
const ActionTypes = { INIT: `@@redux/INIT${randomString()}`, REPLACE: `@@redux/REPLACE${randomString()}`, PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`};
export default ActionTypes;
复制代码


这些 type 也是 redux 不建议我们在 reducer 函数中使用的。

$$observable

这个方法其实就是另一个版本的 subscribe 实现,根据代码以及注释可以知道这是当时 redux 作者为了兼容以后可能成为规范的 observable 协议而做的一层封装,实现还是依赖于 subscribe,方法代码如下:


// src/utils/symbol-observable.js
export default (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();
// src/createStore.jsimport $$observable from './utils/symbol-observable';
function observable() { const outerSubscribe = subscribe;
return { subscribe(observer) { if (typeof observer !== 'object' || observer === null) { throw new TypeError( `Expected the observer to be an object. Instead, received: '${kindOf( observer )}'` ); }
function observeState() { if (observer.next) { observer.next(getState()); } }
observeState(); const unsubscribe = outerSubscribe(observeState); return { unsubscribe }; },
[$$observable]() { return this; } };}
复制代码


关于 observable 提案可以查看 tc39/proposal-observable

中间件实现

redux 除了封装了常规的状态操作外,还提供了非常高的可扩展、可定制能力,这都是中间件带来的,下面我们就来看看 redux 中的中间件是如何设计的。


当我们使用 redux 时并需要传入中间件时,一般会进行这样的操作:


import { createStore, applyMiddleware } from 'redux';
createStore(reducer, applyMiddleware(/** 一个或多个中间件 */));
复制代码


applyMiddleware 的代码如下:


// src/applyMiddleware.js
export default function applyMiddleware(...middlewares) { return (createStore) => (...args) => { /** do something */ };}
复制代码


它直接返回了一个函数,而在 createStore 中会执行 enhancer(createStore)(reducer, preloadedState) 代码块,这个 enhancer 也就是 applyMiddleware 函数的返回值,也就是说相当于执行了以下代码:


createStore(reducer, applyMiddleware(/** 一个或多个中间件 */));
// be equal
applyMiddleware(/** 一个或多个中间件 */)(createStore)(reducer, preloadedState);
复制代码


这时我们就知道实际执行的是最内层的函数,再看这个函数里面都干了些什么,代码如下:


// src/applyMiddleware.js
(...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 };};
复制代码


首先初始化一个 store 对象,并声明一个 dispatch 函数,这个函数什么也不干,单纯抛出一个错误,随后声明了一个对象,这个对象对 storegetStatedispatch 方法进行包装;随后执行了两句代码,第一句代码是初始化中间件函数并将之前的对象传递过去,这时中间件函数内部是能够访问到 dispatch 函数的,只是访问到的是一个抛出错误的 dispatch,而第二句代码将那个抛出错误的 dispatch 函数重新赋值,这里我们就能看出作者的用意:不允许在中间件初始化时调用 dispatch 进行状态更新。


关于中间件,它应该是如下格式的一个函数:


const middleware = (store) => (next) => (action) => next(action);
复制代码


我们知道 store 就是一个含有 getStatedispatch 方法的对象,同时经过初始化后,得到以下函数:


(next) => (action) => next(action);
复制代码


这些函数包含在 chain 数组中,然后通过 compose(...chain)(store.dispatch) 语句整合为了一个函数,这里我们看看 compose 的实现,代码如下:


// src/compose.js
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)) );}
复制代码


我们主要看最后一句代码,这里使用了函数式编程中的组合概念,将多个函数组合为了一个函数, (...args) => a(b(c(...args))),而当调用这个函数时,看似先执行 a,但为了拿到入参其实会执行 b,而对于 b 同样如此。


我们先以一个中间件的情况举例,当只传入一个函数时 compose 是将它直接返回的,那么此时就是:


const temp = (next) => (action) => next(action);
dispatch = temp(store.dispatch);
复制代码


此时的 dispatch 就变成了:


(action) => next(action);
复制代码


这里的 next 就是传入的 store.dispatch 方法,也就是真正的状态更新函数,而对于多个中间件而言,因为会执行 a(b(c(...args))),除了最开始执行的 c 接收到的是真正的 dispatch 外,后续的中间件接收到的都是上一个中间件返回的伪 dispatch,这样就保证了中间件以一个层层递进的关系被调用,而不是同时调用。

combineReducers

项目中,可能有不同逻辑的状态需要维护,为此我们需要定义多个 reducer,而 redux 提供了 combineReducers 函数能够将多个 reducer 组合为一个 rootReducer,它的实现精简代码如下:


// src/combineReducers.js
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 (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key]; } }
const finalReducerKeys = Object.keys(finalReducers);
let shapeAssertionError; try { assertReducerShape(finalReducers); } catch (e) { shapeAssertionError = e; }
return function combination(state = {}, action) { if (shapeAssertionError) { throw shapeAssertionError; }
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);
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey; }
hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length;
return hasChanged ? nextState : state; };}
复制代码


主要是遍历传入的 reducers 对象,获取其中的 key 与实际的 value,代码中 assertReducerShape 函数用于初始化所有的 reducer 并判断返回值是否有效,然后返回了一个 combination 函数。


返回的 combination 函数遍历 reducers 对象并执行所有的 reducer,传入对应的状态和 action,同时对比前后状态来判断是否需要返回新的状态。

bindActionCreators

在项目中,如果需要维护多个状态,以正常方式来说是比较麻烦且难以维护,其中对于如何维护不同的 action 也是一个比较繁琐的事情,对于此 redux 提供了一个工具函数 bindActionCreators,我们可以使用它来管理维护不同状态的 action,例如:


import { createStore, bindActionCreators } from 'redux';
const add = (payload) => { return { type: 'add', payload };};
const remove = (payload) => { return { type: 'remove', payload };};
const reducer = (state = [], action) => { if (action.type === 'add') { return [...state].push(action.payload); } else if (action.type === 'remove') { const index = state.indexOf(action.payload);
const newState = state.slice().splice(index, 1);
return newState; }};
const store = createStore(reducer);
const boundActionCreators = bindActionCreators({ add, remove }, store.dispatch);
复制代码


然后我们就可以直接通过 boundActionCreators.add(1) 进行状态的更新了,而关于 bindActionCreators 的实现,代码如下:


// src/bindActionCreators.js
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); }
const boundActionCreators = {}; for (const key in actionCreators) { const actionCreator = actionCreators[key]; if (typeof actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch); } } return boundActionCreators;}
复制代码


可以看到实际的逻辑就是将 actionCreator 函数替换为:


function () {  return dispatch(actionCreator.apply(this, arguments));};
复制代码


---end


发布于: 刚刚阅读数: 2
用户头像

yuanyxh

关注

站在巨人的肩膀上 2023-08-19 加入

web development

评论

发布
暂无评论
redux 源码学习_js_yuanyxh_InfoQ写作社区