对任何项目来说,读懂源码应该都是最透彻的学习方式。只是很多项目的代码量实在太多,又或者是阅读的难度太高,所以一般不会采用这种方式学习。不过redux是一个例外,它本身的代码极少,去掉注释和空行的话只有100多行,而且实现的方式也很简洁,十分适合进行阅读。
然而,既然redux只有100多行——相信大家平时写100多行代码都是分分钟的事,那么,有什么理由不自己手动实现一次呢?何况这100多行代码里还有很大一部分用于类型校验,我们只是为了学习,所以类型校验也可以去掉,只保留最基本的核心功能,这样剩下的代码就更少了。
接下来,就请和我一起动手实现一个简单的redux吧,如果你之前没有做过类似的事,相信你会从中收获不少。
createStore
调用createStore方法是使用redux的第一步,它接收三个参数:reducers、initState、enhancer,并返回一个store对象。Store对象包含三个变量:
-
dispatch: 发出action,通知redux进行状态更新。
-
getState: 获得当前的state树。
-
subscribe: 监听store变化。
由此可见,createStore方法的返回值,即是包含了上述三个函数的store对象。
接下来再看看createStore中的成员变量。Redux中有三个核心的成员变量:
-
currentReducer:用于存放reducer;
-
currentState:用于保存当前的state树;
-
currentListener:一个数组,存放了当前所有的listener,redux状态更新的时候会对其进行通知;
根据以上信息,我们可以推断出一个简单的redux结构是这样的。
function createStore(reducer,initState,enhancer){
const currentReducer=reducer;
const currentState=initState;
const currentListener=[];
function dispatch(){}
function getState(){}
function subscribe(){}
return {
dispatch,
getState,
subscribe
}
}
接下来,我们挨个实现里面三个函数就好。
getState
getState用于获取当前的state树,由于我们已经有一个currentState变量用于保存当前state,所以直接将currentState这个变量直接返回就好(原本的redux也是这么简单粗暴)。
function getState(){
return currentState;
}
subscribe
在绝大多数项目里面,redux都和react-redux一起使用,订阅的操作被react-redux封装了,所以很多人可能对subscribe不太熟悉。这里首先让我们看看subscribe是如何使用的:
store.subscribe(()=>{
//利用getState获取state
const state = store.getState();
//利用state进行其他操作
})
可以看到,subscribe接收一个回调函数,当redux中的状态发生变化时,就会自动触发里面的回调函数。
所以subscribe的实现也很简单,它所做的就是把接收的回调函数存入currentListener数组里面。
当state产生变化的时候,就遍历该数组取出里面的回调函数并调用即可。
subscribe还有一个返回值unsubscribe,用于退订store监听。所谓退订,其实只要把对应的function在数组中移除就可以了。subscribe实现如下:
function subscribe(listener){
//将listener(即回调函数)存入currentListener数组里面
currentListener.push(listener);
//返回一个退订函数
return function unsubscribe(){
//从currentListener数组里面找到该函数,将其删除即可
const index=currentListener.indexOf(listener);
currentListener.splice(index,0);
}
}
dispatch
dispatch接受一个action(一般是一个字符串),通知redux进行状态更新。
所以dispatch主要做了两件事:
-
生成新的state树;
-
调用所有listener,通知他们状态已经更新了;
用过redux的都知道,状态更新的逻辑是由程序员自己编写的reducer函数负责处理的,而在createStore函数里面,有一个变量currentReducer用于保存当前的reducer。
所以直接把action传入currentReducer即可,它的返回值就是我们需要的状态(不太明白的同学建议复习下reducer的用法)。
至于调用listener,只要遍历currentListener数组并调用就可以了。具体实现如下:
function dispatch(action){
//调用currentReducer方法,生成新的state数
//并把新生成的state树赋值给currentState
currentState=currentReducer(currentState,action)
//遍历currentListener数组,依次调用保存在其中的listener
for(let i=0;i<currentListener.length;i++){
const listener=currentListener[i];
listener();
}
}
至此,一个简单的createStore就被我们实现了。虽然看似很简单,但是在核心功能的实现上,跟原本的redux是一模一样的,只是少了一些错误处理。测试代码已经放在github上,可以自行下载。
中间件
实现enhancer
首先稍微解释一下中间件是什么。抛开那些高大上的概念,中间件的作用其实很简单:在发出的action在到达reducer之前,利用一个函数做一些预处理操作。这些预处理包括但不限于:打印日志(redux-logger)、请求网络数据并将其传入redux(redux-thunk、redux-saga)……由于redux用法并不是本文重点,这里就不再赘述了。
中间件需要用到createStore的第三个参数enhancer(中文意思为增强器)。enhancer的用法如下:
/**
* 在控制台打印redux发出的action
* @param createStore 就是createStore本身
* @param reducer reducer
* @param initState store中的初始state
*/
function logEnhancer(createStore) {
return function(reducer, initState) {
const store = createStore(reducer, initState);
return {
getState: store.getState,
// 自定义的dispatch
dispatch: function(action) {
console.log('当前发出的action是:' + action);
store.dispatch(action);
},
subscribe: store.subscribe
};
};
}
// enhancer用法
import {createStore} from 'redux'
//...省略reducer和initState
const store=createStore(reducer,initState,enhancer)
可以看到在我们自定义的logEnhancer中,用一个自定义的dispatch替换了store原本的dispatch。然后将我们的logEnhancer传入即可createStore的第三参数即可。
从logEnhancer的结构可以看出,createStore内部对于enhancer的处理相当简单,只是单纯地将createStore、reducer、preloadedState这三个参数传入里面。所以在creteStore内部enhancer功能的实现如下:
function createStore(reducer,initState,enhancer){
// 如果参数里面传入了enhancer,简单地调用即可
if (typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer, initState)
}
//...省略其他代码
}
是不是特别简单?
实现compose
接下来让我们继续研究中间件该怎么实现。为了照顾某些对redux不太熟悉的小伙伴,首先让我们看看如何写一个中间件。
这里用了著名的异步处理中间件redux-thunk作为例子,其内部实现非常简单。
function createThunkMiddleware() {
return ({ dispatch, getState }) => next => action => {
// 判断发出的action是不是一个函数,如果是就调用它
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
export default thunk;
对比上面的enhancer,你会发现他跟中间件写法非常相似;除了参数不同外,它们的主要区别在于enhancer只能有一个,中间件却有多个。所以先让我们看看redux是怎么执行多个中间件的。
单纯地执行多个函数并不难,一个大家都能想到的方法,就是用一个数组保存中间件函数,在dispatch时遍历这个数组,依次将函数取出并调用即可。但redux中的方法却更为巧妙和优雅(虽然稍微有些复杂):
// 合并函数
function compose(...funcs) {
// 对函数进行累加处理
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
reduce方法可以在遍历的同时,把每一次的结果累加起来,如果我们往compose函数里面传入多个函数,他们就会形成一个嵌套结构。举一个例子:
// 向compose中传入三个函数
const result = compose(fn1,fn2,fn3);
result等同于这样一个函数:
const result = (...args) => fn1(fn2(fn3(...args)));
可以看到,经过累加后三个函数会彼此嵌套。当这个嵌套函数开始执行时,会按照fn3,fn2,fn1的顺序依次调用。
经过compose处理后,无论我们传入多少个函数,只要调用result这个高阶函数,即可将它们一并启动。执行多个函数的问题就这样解决了。
实现applyMiddleware,理解洋葱模型
applyMiddleware是创造中间件的核心函数,接收一个由中间件组成的数组,然后利用compose将它们组合到一起。但这种组合又不是单纯地将它们嵌套执行,而是采用了一种“洋葱模型”的设计思路。而这种设计思路也是我认为redux源码中最精华、最有价值的一部分。
由于信息量比较大,我就直接在代码里面写注释了。
import compose from './compose'
export default function applyMiddleware(...middlewares) {
// applyMiddleware本质上就是一个enhancer,所以前面的处理都和enhancer一样
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {}
const middlewareAPI = {
getState: store.getState,z
dispatch: (...args) => dispatch(...args)
}
// 遍历中间件数组,调用中间件,把getState和dispatch传入第一个参数
// 中间件的格式是 ({ dispatch, getState }) => next => action => {}
// 经过这次遍历调用后,中间件就只剩下 next => action => {} 这一部分,next变成了中间件第一个参数
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 将处理后的中间件传入compose里
// 现在的数据结构变成 const result = (...args) => fn1(fn2(fn3(...args)));
const result = compose(...chain);
// 由于next变成了中间件第一个参数,当调用中间件时,next便是你调用中间件时传入的参数
// 这里传入的参数是store.dispatch,所以store.dispatch就是fn3的next,同理,fn2的next就是fn3,fn1的next就是fn2
// 经过这次调用后,中间件结构由 next => action => {} 变为 action => {}
// 最终dispatch的结构会变为这样: action=>action=>action=>{}
dispatch = result(store.dispatch);
return {
...store,
dispatch
}
}
}
在最后一行注释里面,dispatch的结构变为action=>action=>action=>{},看着有些奇怪,那是因为我们没有进行任何操作的原因。让我们在next调用前和调用后分别打印一次日志试试:
const fn1 = (next) => (action) =>{
console.log('fn1 next调用前');
next();
console.log('fn1 next调用后');
}
//……fn2和fn3都是相同的写法,只是函数名不同
上面说过,fn1的next就是fn2,fn2的next就是fn3,fn3的next就是store.dispatch。所以最终会变成这样一个结构:
fn1 {
console.log('fn1 next调用前');
fn2 {
console.log('fn2 next调用前');
fn3 {
console.log('fn3 next调用前');
store.dispatch('action');
console.log('fn3 next调用后');
}
console.log('fn2 next调用后');
}
console.log('fn1 next调用后');
}
从这个结构图里也可以看出中间件里面代码的执行顺序。在store.dispatch执行之前,会把next之前的代码按照fn1->fn2->fn3的顺序执行;在store.dispatch执行之后,会把next之后的代码按照fn3->fn2->fn1的顺序执行。这就是所谓的洋葱模型。

得益于这种奇妙的设计,你既可以在dispatch发出之前做一些预处理,也可以在dispatch发送完、redux中的状态更新完毕后,做一些收尾工作,功能实在是非常强大。不得不佩服第一个想出这种设计的大佬。
到此redux的功能就全部实现了,相关代码都放到了文末的github地址上。希望你看到这里能有所收获。