『React』从零开始整个 redux 中间件
“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情”
碎碎念🤡
大家好,我是潘小安,一个永远在减肥路上的前端er🐷 !
虽说之前的公司一直使用的是 mobx
,但是对于 Redux
这种热门状态管理工具,我也有这着有丰富的 “云实战经验”。
都说 “师父领进门,修行在个人”,在跟着视频写过几个 demo 后,我在师父的门前驻足停留许久至今,Redux 的真正的内核始终不得其要领。借着这次新工作过渡期,我把 Redux 源码的所有注释结合自己的理解翻译了一遍,整理在了 这个仓库 中,有兴趣的小伙伴可以瞅瞅。
同时参考 Redux 的中文文档,借鉴社区其他小伙伴的行文思路(写在文末参考文章),以自己的视角出发,整了一篇从 0 开始写 redux 中间件的,既是为了把自己所学分享回馈给社区,也是为自己的新工作和新环境打个 tag
。
开整 😘
Redux 简介
首先我们要问两个问题:
- Redux 是什么?
- Redux 是必须的吗?
首先我们打开 Redux 的官网,可以看到一个高清大图下面的加粗小字。
A Predictable State Container for JS Apps.
翻译过来就是一个为 JS 应用打造的可预测的状态容器。
我理解的 可预测 特点是由 Redux
内的单向数据流提供的。当我们 store
中的数据发生变化的时候,那么一定是有对应的 action
被 dispatch
了,不可能存在其他的数据变更发起的地方。这样的设计对于我们定位问题,理解数据流动都大有裨益。
那么缺点是什么呢?不论多大的变动,都需要走一遍整个流程,就势必会造成大量的中间代码,创建 dispatch
,去 reducer
中返回最新的 state
等操作,中间可能还走了一些我们本文的主角-中间件。这其实也回答了我们的第二个问题- Redux 是必须的吗?
当然不是必须的,只有状态的管理复杂到需要单向数据流去维持的才需要用 Redux
,如果项目中本身只有简单的状态去使用 redux
,反而会使得代码量变大,看起来比较冗余。
那怎么判断项目的状态目前是否复杂?当你定位一个由于数据问题引起的 bug
,花了半天定位不到触发的数据修改的源头的时候,不妨考虑用一用吧~
Redux 中间件
什么是中间件?
首先我们要问,什么是中间件?redux 为什么需要中间件?
因为 redux 没有给我们提供处理复杂 action 的能力
dispatch
本身只负责接收一个是 plainobject
的 aciton
作为参数。
funciton dispatch(action){
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
...
}
那什么是 plainObject
呢?
- 由
Obect.create(null)
创建的对象 - 由
new Object()
创建的对象 - 对象字面量创建的对象
具体实现逻辑可以找找这个仓库 里面的 isPlainObject.js
文件。其中涉及到一些跨 iframe 对象的判断,蛮有意思。
reducer
接收后通过 type
和 data
去改变对应的 state
。但是当 action
是一个函数,或者需要处理异步,又或者需要复用一些逻辑的时候,我们就需要使用中间件给 dispatch
强化一下,加点 buff
。
从 0 开始整一个
既然是从 0 开始整一个中间件,那么我们假装我们从来都没有学习过redux
,直接清一下脑子里面和 redux
相关的缓存,开始来思考一个问题:如果让我们自己写一个状态管理库,我们要从何下手?
先整一个 redux
第一步:我们要创建一个状态,考虑到多个组件要用,就用对象初始化一下。
// 哎呀呀 我被创建了
let state = {}
第二步:这个状态每个组件都可以去取,还可以去改,但是改的时候不能想改就改,只有我这个”仓库“有权限进行修改,其他组件想改就要拿函数来改,想拿也得通过我的函数来拿。那这个就有点像是把 state
看做一个私有变量了,于是我们不妨把这个仓库名命名为 createStore
,取数据的函数命名为 getState
,改变数据的指令命名为dispatch
。
// 啦啦啦啦 我初始化了
function createStore() {
let state = {};
function getState() {
return state;
}
function dispatch(value) {
state = value;
}
}
第三步:这么多组件,要改的状态又不一样,总不能每次都全部替换,所以我们每次修改的时候需要指明一种类型,告诉 store
我们这次修改的是哪部分的 state
。
function dispatch({ type: type, value: value }) {
switch (type) {
case "add":
state = {
...state,
num: state.num + 1,
};
case "changeOperation":
state = {
...state,
operation: value,
};
...
}
}
如果项目中有 1000 个不同的 type,那就写他1000 个 case!这样可以吗?
当然是不行的,所以我们需要亿点点优化。比如说把所有的 switch case
里面的所有逻辑都抽出来,然后取名叫做 reducer
,在 store
初始化的时候再传进来就好了,顺便给 dispatch
的参数起个响亮的名字 - action
。
import reduce from './reducer'
// 啦啦啦啦 我初始化了
function createStore(reducer) {
let state = {};
function getState() {
return this.state;
}
function dispatch(action) {
state = reducer(state, action);
}
}
//reduce.js
function reducer(state = {}, action) {
const { type, value } = action
switch (type) {
case "add":
return {
...state,
num: state.num + 1,
};
case "changeOperation":
return {
...state,
operation: value,
};
default:
return state
}
}
对了,还漏了一个 state
的值的初始化的问题,如果 state
不进行初始化,之后很容出现 xxx is not undefined ,can’t find xxx from null/undefined
。那这个事情谁来做?
我们可以直接在 createStore
里面多传递一个参数,就叫 defaultState
。
像这样:
function createStore(reducer, defaultState) {
let state = defaultState || {};
...
}
另外我们可以在 reducer
传参的时候给一个默认值,确保执行的时候不会报错,且当接收到未知的 action
的时候,需要返回当前 state
作为默认值。
最后就是向外暴露这些方法,写到这里,我们这个状态管理方法长这个样子:
// 啦啦啦啦 我初始化了å
function createStore(reducer, defaultState) {
let state = defaultState || {};
function getState() {
return this.state;
}
function dispatch(action) {
state = reducer(state, action);
}
export { dispatch, getState }
}
//reduce.js
function reducer(state = {}, action) {
const { type, value } = action
switch (type) {
case "add":
return {
...state,
num: state.num + 1,
};
case "changeOperation":
return {
...state,
operation: value,
};
default:
return state
}
}
写到这里,我们基本完成了一个状态管理仓库的雏形,剩下的就是如何来整我们的中间件了。
再来一个中间件
在第一部分的状态管理库中,我们假定 action
是一个规规矩矩的对象
{
type:''
data:xxxx
}
但是当我们进行页面交互的时候,有很多种情况和需求,比如当我们点击触发了接口请求,当我们需要做记录 action
和 state
,当我们需要错误上报,我们能否把这些事情都丢给 dispatch
去做?
以记录为例,如果我们单独处理的话,需要在每次 dispatch
之前去做记录,比如说像这样:
console.log('你正在派发的 action 是', action.type)
store.dispatch(action)
那如果每个都想监听,到处去加肯定不现实。
或者我们把这个封装成方法,然后重新赋值 store
的 dispatch
,像这样:
// 使用变量临时保存原来的 dispatch 方法
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log("你正在派发的 action 是", action.type);
return next(action);
};
那之后所有的 dispatch 都会进入我们定义的这个 dispatchAndLog 方法,搞定!
那我如果现在还想要错误捕捉上报的功能呢?简单!
let next = store.dispatch;
store.dispatch = function xxxdispatch(action) {
// 日志记录
console.log("你正在派发的 action 是", action.type);
try {
return next(action);
} catch (err) {
// 错误上报
console.error("我是捕获异常操作!", err);
}
};
那如果再加功能呢?
本着一个函数只做一件事的原则,解耦是必要的,做成可插拔的插件形式才是最终的解决方案!
那么如何做呢?
首先,既然要做到可插拔,那每个插件肯定是要单独定义的,以日志功能和错误上报功能为例,我们需要让它们有单独作用域包裹,使其模块化。
- 错误上报
function dispatchErroReport(store){
let next1 = store.dispatch;
store.dispatch = function _dispatchErroReport(action) {
try {
return next1(action);
} catch (err) {
console.error("我是捕获异常操作!", err);
}
}}
- 日志记录
function dispatchLog(store){
let next2 = function _dispatchErroReport(action) {
try {
return next1(action);
} catch (err) {
console.error("我是捕获异常操作!", err);
}
}}
store.dispatch = function _dispatchLog(action) {
console.log("你正在派发的 action 是", action.type);
return function _dispatchErroReport(action) {
try {
return next1(action);
} catch (err) {
console.error("我是捕获异常操作!", err);
}
}}(action);
}
}
这时我们连续调用 dispatchLog
和 dispatchErroReport
后
//拿到的 next2 的值为原生的,执行后 store.dispatch的值为 _dispatchErroReport
dispatchErroReport(store)
//拿到的 next2 的值为_dispatchErroReport,store.dispatch的值为 _dispatchLog
dispatchLog(store)
------------------------------------------------
就相当于执行了以下代码:
function dispatchLog(store) {
let next1 = store.dispatch;
let next2 = function _dispatchErroReport(action) {
try {
return next1(action);
} catch (err) {
console.error("我是捕获异常操作!", err);
}
};
store.dispatch = function _dispatchLog(action) {
console.log("你正在派发的 action 是", action.type);
return next2(action);
};
}
当界面触发 dispatch
的时候,这个时候最新的 dispatch
是由最后一个执行的插件定义的,所以后初始化的先运行,也就是说 action
先进入了 日志记录部分,
这样所有的功能就以插件的形式形成了一个调用链,之后的每个 action
都会按照插件的调用顺序依次进入每个插件进行处理,最后才将 action
派发到 store
中。这种强化我们整个派发 action
流程的插件,我们给它取个名字就叫 middleware
有没有优化的地方?
如果我们不在 middleware
中直接去覆盖 store.dispatch
拿,而是用链式调用的方式从上一个 middleware
的结果中拿,上面的代码会变成什么样子?。
function dispatchErroReport(store){
let next1 = store.dispatch;
return function _dispatchErroReport(action) {
try {
return next1(action);
} catch (err) {
console.error("我是捕获异常操作!", err);
}
}}
function dispatchLog(store){
let next2 = store.dispatch
return function _dispatchLog(action) {
console.log("你正在派发的 action 是", action.type);
return next2(action);
}
}
dispatchErroReport()
dispatchLog()
=>
function dispatchLog(store) {
let next2 = dispatchErroReport(store)
store.dispatch = function _dispatchLog(action) {
console.log("你正在派发的 action 是", action.type);
return next2(action);
};
}
转成箭头函数瞅瞅
//转成箭头函数看看
const dispatchLog = () => {
let next2 = (store) => {
let next1 = store.dispatch;
return (action) => {
try {
return next1(action);
} catch (err) {
console.error("我是捕获异常操作!", err);
}
};
};
return (action) => {
console.log("你正在派发的 action 是", action.type);
return next2(action);
};
};
说到这里我们需要补充一个前置基础知识,就是函数柯里化
柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。柯里化,是一个逐步接收参数的过程。
除了最先初始化的 middleware
需要从 store
中获取 dispatch
之外,其他的 middleware
内部的 dispatch
都是上一个 middleware
的返回值。
以 dispatchLog
为例,我们可以把 next2
的获取转成部分计算函数
const dispatchLog = (store) => (next2) => (action) => {
console.log("你正在派发的 action 是", action.type);
return next2(action);
};
dispatchLog(store)(dispatchErroReport(store));
写到这里,大家可以看看 redex-thunk
的源码:
const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
({ dispatch, getState }) =>
next =>
action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument)
}
return next(action)
}
return middleware
巧了嘛这不是~ 巧了嘛这不是~ 和我们手写出来的 middleware
的样子不能说大差不差,只能说是一模一样🐶。
从 Redux 源码中学到了什么?
applyMiddleware
applyMiddleware
做的事情是给dispatch
加 Buff
。在该方法内直接传入
createStore
用于 store
的创建。从 store 中获取 dispatch
后使用 compose
函数链式调用所有的 middlewares
的第二层(第一层在 chain 的初始化的时候调用了)。
const store = createStore(reducer, preloadedState, enhancer)
...
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
其中值得学习的是这个 compose
的实现:
let compose = function (...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
使用数组的 reduce API
巧妙的实现 middleware
的链式调用。
subscribe
Redux
中的 subscribe
方法可以维护了一个 listener
列表,可以让一些回调函数在 dispatch
后执行。使用到的观察者模式这种设计模式可以在很多技术中可以看到它们的身影,同时 subscribe
还维护了两个 listener
队列来保证listener
的正确调用。
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
//浅拷贝所有listener 防止更新过程中listener更新
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
此外,redux
中还有较为细致的异常处理和相应的提示 console
,这些都值得我们在日常开发中学习。
参考文章
小声 BB
转眼九月中旬了,深圳的秋天好像还没来。
在新公司陆陆续续写了几个需求了,勉强可以完成任务,比刚过来的时候好很多。同时很 nice
,领导也 nice
,氛围也 nice
,就是周边生活成本有些高。
最近在看 《软技能》 这本书,书已经看的七七八八了,还输出了一些 读书笔记,书里面谈到一些职场小技巧,创业小技巧,还有学习小技巧,有兴趣的同学可以瞅瞅。
最近群里小伙伴还推了一本 《福格行为模型》,大概看了一下,觉得非常不错,列入了后期训练营计划的读书笔记里面去。
当然也遇到一些烦心事,但是因为都是和钱有关,所以也就不值一提。
海贼王越来越精彩了,天王出手,属实排面。
九月还有两三个很久没见的朋友需要一起吃个饭,还有一些需求需要收尾,还需要做一些心态调整,work life balance
始终还在探索。
emm。。。大概就是这些了,下次的更文不出意外的话,应该是在十月假期之后的事情了,十月见。
🎉 🎉 觉得文章对您有帮助的小伙伴,请不要吝啬您的点赞~🎉 🎉
🎉 🎉 对文章中的措辞表达、知识点、文章格式等方面有任何疑问或者建议,请留下您的评论~🎉 🎉
转载自:https://juejin.cn/post/7144552288076431374