Redux 源码解析
文章目录
为了方便理解,部分源码的异常抛出部分被精简
这次主要阅读了一下redux的源码实现,之前有用过多次redux但都是针对于个人小型项目使用,因此很少使用到关于reduce函数的拆分合并以及通过中间件函数来加强dispatch实现的。
1. 为什么需要redux?
Redux用于状态的管理,使用Redux可以帮助我们处理应用的共享状态。而应用的 共享状态这个问题,应该是随着组件化的开发而需要被解决的问题。当我们使用React或是Vue这样的组件化的开发框架时,组件之间的通信其实是在开发中要面对的问题。这时候redux就可以帮助你去实现全局组件之间数据的共享。当然这些框架都有各自全局状态管理的最佳实践,然而redux是比较早且比较经典的实现其中很多的设计思想都是值得学习借鉴的。
2.基本实现redux功能
我们先来通过上图来讲述redux的基本运作原理:
先通过createStore函数生成一个store对象,store是一个全局状态存储的容器。添加订阅函数,可以在state对象发生变化的时候通知你。若我们通过dispatch发起一个action将会触发reduce函数,该函数会修改原来的store中的数据并告诉所有的订阅者。
redux源码主要分为以下几个主要文件:
- createStore.js 文件: 基本的redux文件,生成全局状态机
- combineReducer.js文件:用于reducer的合并
- applyMiddleware.js文件:用于异步函数的实现
2.1 createStore.js
首先我们来讲以下createStore的源码,该文件主要包含了三个主要方法,而这三个方法就可以实现整个消息的订阅发布:
- getState: 获取store中存储的数据
- dispatch:通过收到的action,改变store中的state值
- subscribe:订阅state的更新事件,状态改变时触发订阅事件
/**
* 创建出store以及控制store中数据改变的dispatch和subscribe操作
* @param {*} reducer
* @param {*} preloadedState
* @param {*} enhancer
*/
export function CreateStore (
reducer, // 动作相应方式
preloadedState, // 初始的状态
enhancer, // 用于添加中间件来获得增强版store
) {
// 判断是否要获得添加中间件版的 store
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
// 抛出一个异常 (enhancer 必须是一个函数)
throw new Error('Expected the enhancer to be a function.')
}
// 调用 enhancer ,返回一个增强版的 store creator
return enhancer(CreateStore)(reducer, preloadedState)
}
let currentReducer = reducer;
let currentState = preloadedState;
let currentListeners = [];
let nextListeners = currentListeners;
let isDispatching = false;
/**
* 保证 nextListeners 的变化不影响当前 currentListeners
*/
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice();
}
};
/**
* 获得状态
* @returns
*/
function getState(){
if (isDispatching) {
throw new Error('要先等任务派送完才可以获得状态');
}
return currentState;
};
/**
* 消息的订阅
* @param {*} listener
*/
function subscribe(listener) {
if(typeof listener !== "function" ){
throw new Error("listener must be a function");
}
// 订阅状态的标志
let isSubscribed = true;
ensureCanMutateNextListeners();
// 将新的监听函数添加到监听链中
nextListeners.push(listener);
// 闭包的方式取消监听
return function unsubscribe() {
if(!isSubscribed) return;
isSubscribed = false;
ensureCanMutateNextListeners();
const index = nextListeners.indexOf(listener);
nextListeners.splice(index, 1);
currentListeners = null;
}
}
/**
* 消息的发布
* @param {*} action
*/
function dispatch(action) {
try{
isDispatching = true;
// 核心,跟新currentState中的数据
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
const listeners = ( currentListeners = nextListeners );
/**
* 循环的遍历去触发每一个监听者
*/
for(let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
// #### 为了配合使用中间件时compose使用
return action;
}
/**
* 替换事件处理中心
* @param {*} nextReducer
* @returns
*/
function replaceReducer(nextReducer){
currentReducer = nextReducer;
dispatch({ type: "Replace Reducer" })
return store;
}
dispatch({ type: "INIT" })
const store = {
dispatch,
subscribe,
getState,
replaceReducer,
}
};
createStore的实现主要通过闭包的思想来实现状态的管理, 整体的实现理解过程并不难,根据其中的注释可以清晰的理解。
2.2 combineReducer.js
reduce函数用于负责相应action修改新的state,整个应用只有一个state对象,其中包含所有的数据。当开发一个多人协作的应用时,不同的开发者需要根据自己的需求编写自己的reduce函数。但是createStore只能接收一个reducer函数,此时我们就需要用combineReducer将多个reducer函数获得一个汇总的reducer函数。
/**
* 合并多个 reducer
* @param {*} reducers
*/
function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers);
const finalReducers = {};
// 过滤掉传入的非法 reducer
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);
// 返回一个合并的 reducer
return function combination(
state,
action,
) {
let hasChanged = false // 记录state对象是否改变的变量
const nextState = {} // 下一个状态
// 遍历key值
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key] // 当前key值对应的状态
const nextStateForKey = reducer(previousStateForKey, action) //调用reducer后新生成的状态
nextState[key] = nextStateForKey //将新生成的状态赋值到新状态的对应key值上
// 通过对比previousStateForKey 和 nextStateForKey是否相等来表明状态是否改变
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 如果状态改变就返回新的状态,没有,就返回原来的state对象
return hasChanged ? nextState : state
}
}
combineReducers的整体代码并不复杂。“合并”reducer其实就是获得一个综合的action响应函数,其实质就是当合并的reducer接收一个action时它会触发最初传入的所有reduer。
2.3 applyMiddleware.js
applyMiddleware通过中间件的形式扩展了dispatch函数。例如可以让dispatch可以执行异步函数,让dispatch执行后立刻执行一些中间间函数。
import {compose} from "./compose.js"
/**
* 使用中间件
* @param {...any} middlewares
* @returns
*/
export default function applyMiddleware(
...middlewares
) {
// 返回一个加强版的 store
return (createStore) => {
const store = createStore(reducer, preloadedState)
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: (action, ...args) => dispatch(action, ...args)
}
// 将 store 中的一些信息传入中间件链中
const chain = middlewares.map(middleware => middleware(middlewareAPI));
// 获得一个新的加强版的 dispatch
// 这里比较难理解,下面通过一个例子来理解是如何获得这个dispatch的
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
compose
/**
* 将传入的函数从右往做的顺序依次执行
* @param {...any} funcs
* @returns
*/
function compose(...funcs) {
if(funcs.length === 1) {
return funcs[0];
}
return funcs.reduce(
(a, b) =>
(...args) =>
a(b(...args))
)
}
下面我们重点来理解
dispatch = compose(...chain)(store.dispatch)
首先我们来看一下官方给出的中间件的使用案例。我们重点关注标注的中间件函数(logger)也就是上述中chain中每一个函数的运行格式,必须是返回下图这种函数叠函数的形式(其中的next其实就是对应上面的store.dispatch)。
我们可以通过下述的案例更清晰的理解新的dispatch
是如何获得的。
案例
我们在applyMiddleware.js中测试下述的代码
const add = (next) => (action) => next(action);
const multiply = (next) => (action) => next(action);
const dispatch = (action) => {
console.log(action);
return action;
}
const myAction= {
type: "test"
}
console.log(compose( add, multiply )(dispatch));
console.log(compose( add, multiply )(dispatch)(myAction));
输出
(action) => next(action)
{type: 'test'}
{type: 'test'}
结合该案例我们可以清晰的理解新dispatch
的获取流程。
参考
小结
Redux的整体源码还是比较好理解的,其中最难理解的应该属applyMiddleware
函数。结合之前学习的设计模式,Reudx的使用其实还借助了单例模式保证了全局的store的唯一。此外,其中还包含了许多闭包的思想值得借鉴思考。