内容简介:Redux 的设计十分精妙,透过源码可以看到里面有很多代码技巧,发布订阅、组合函数、函数柯里化、函数式编程等方式的运用,给 Redux 又增加了一丝神秘的面纱,今天我们将这些面纱逐步的解开。Redux 官方文档对 Redux 的定义如下:一个面向 JavaScript 应用的可预测状态容器。
Redux 的设计十分精妙,透过源码可以看到里面有很多代码技巧,发布订阅、组合函数、函数柯里化、函数式编程等方式的运用,给 Redux 又增加了一丝神秘的面纱,今天我们将这些面纱逐步的解开。
Redux 的作用
Redux 官方文档对 Redux 的定义如下:
一个面向 JavaScript 应用的可预测状态容器。
- 状态容器是什么? 通俗点说它就是一个 JS 库,来管理前端开发中的数据,它将数据集中管理,用到什么只需从整个数据集合中取出。
- 可预测又是什么? 这个解释在下面再给出。
- 为什么会需要 Redux ? 说一个解释性比较强的,React 中两个子组件需要通信,一般是传递给父组件做中转,但是有多级子组件的时候就会状态提升很多层,比较麻烦,使用 Redux 就会很方便。
我们知道 Redux 有自己的工作流程,就是 store 会通过一个数据树存储整个应用的状态,然后组件会订阅数据 state,通过组件的派发(dispatch)行为(action)给 store 的方式来刷新 view,Redux 有三大原则(唯一数据源、保持只读状态、数据改变只能通过纯函数来执行),这里我们先不去解释这是什么,先一点一点实现一个 myRedux,之后再看它的工作流程会十分清晰。
Redux 简易实现
比如页面有个 HTML 结构:
<div id='cont'></div> 复制代码
然后我们创建一个 state 数据集合:
const stateAll = { people:{ eyes: '有点不舒服', color: 'red' } } 复制代码
假设我们现在渲染数据只从这个数据集合来取数据渲染,可以编写如下逻辑:
function render(data){ var ele = document.getElementById('cont') ele.innerHTML = data.people.eyes; ele.style.color = data.people.color; } stateAll.people.eyes = '还是有点不舒服'; stateAll.people.eyes = '感觉差不多了'; stateAll.people.eyes = '没问题了'; stateAll.people.color = 'green'; render(stateAll) 复制代码
现在假如多个人同时使用操作了这个公共变量,调用 render 时候只会显示最终的那个结果,其他修改的过程无法被记录检测,这个过程是可以被任意修改并且无法预测,现在我们把问题复杂化,让数据变化变成可预测的。
举个例子,假如 A 去医院看病,去个小医院可以直接找个医生瞧病拿药(stateAll.people.eyes='随意修改'),但是去大医院的话,是需要挂号预约的,比如 A 眼睛不舒服,需要先找到眼科,再挂对应眼科的号,挂成功了才可以进入科室瞧病,所以我们需要对上面的代码进行流程完善一下:
function dispatch(action){ switch (action.type){ case 'EYES_QUESTION_LOG': stateAll.people.eyes = action.data break; case 'EYES_COLOR_LOG': stateAll.people.color = action.data break; default: break; } } dispatch({ type: "EYES_QUESTION_LOG", data: "眼睛有点红" }) dispatch({ type: "EYES_COLOR_LOG", data: "lightcoral" }) render(stateAll) 复制代码
为什么需要这个东西呢?
比如 A 想知道眼睛的病情描述和眼睛的颜色状况,在小医院的话可能甲、乙、丙、丁四个医生都给他瞧过并分别记录的他的眼睛问题,等回过头来去找病情记录得分别去各个医生那里去要。医生很忙的,一找就是半天还不一定能找到(这里甲乙丙丁就是多个开发者或者多个函数,同时操作了同一个患者病情记录)。但是现在大医院规定所有的病情记录必须得通过同一个科室(dispatch)的系统记录,甲、乙、丙、丁医生需要通过 dispatch({type:"
", data:"
"}) 这种方式,这样的好处就是我查找问题时候不需要各个地方跑去看数据,只需要在 switch 中打断点看数据就可以了。
现在再仔细看下,我们会发现一个问题,我的 render 现在只能在 dispatch 之后才可以得到正确的结果,相当于医生修改完病历之后我看不到,需要跑去总科室处要一下数据,现在不想跑了,等医生发布完我就想立马知道情况。所以现在就用到“发布订阅”设计模式。
发布—订阅模式又叫观察者模式,它定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,一般用事件模型来替代传统的发布—订阅模式。
//创建一个仓库 function createStore () { //需要订阅的函数集合 let listeners = [] //外部每调用一次subscribe就往被订阅的集合中存入 listener参数必须为一个函数 const subscribe = (listener) => { listeners.push(listener) } //dispatch执行就是让被订阅的函数执行 const dispatch = (action) => { listeners.forEach((listener) => listener()) } //通过闭包的形式返回 外部可调用内部方法 return { dispatch, subscribe } } 复制代码
现在可以通过:
const store = createStore(); store.subscribe(()=>{ //把渲染函数订阅上 render(stateAll) }) store.dispatch(); //通知结果 复制代码
这样医生将来通知只需要 dispacth 之后患者就可以知道了结果,不需要等医生发布后主动跑去问情况。
但是现在是没有数据的,需要将数据和处理方式集成到 createStore 中,方便统一调用:
//dispatch函数继续改写为reducer 增加一个参数state function reducer(state, action){ switch (action.type){ case 'EYES_QUESTION_LOG': state.people.eyes = action.data break; ... } } //增加了两个参数state, stateChanger function createStore (state, stateChanger) { const listeners = [] //这样可以直接从store中取数据 const getState = () => state const subscribe = (listener) => { listeners.push(listener) } const dispatch = (action) => { //这里新数据处理的地方 就是之前的dispatch({type: "EYES_COLOR_LOG",data: "lightcoral"}) 只不过这里换成参数的方式 stateChanger(state, action) listeners.forEach((listener) => listener()) } return { getState, dispatch, subscribe } } //将现有的数据集合和处理方式传入进去 const store = createStore(stateAll, reducer); store.subscribe(()=>{ //通过store的方式获取数据 不用管全局变量的名称了 render(store.getState()) }) //派发数据 action store.dispatch({ type: "EYES_QUESTION_LOG", data: "好点了" }) 复制代码
createStore 传入两个参数,第一个就是整个初始数据集合 state,第二个就是最开始写的 dispatch 函数,它有个官方名字叫 reducer。通过 store.getState 获取共享状态,通过 store.dispatch 修改共享状态,通过 store.subscribe 监听数据数据状态被修改后进行重新渲染页面。
现在整个流程是可以跑通,但是还是一直在操作全局那个数据集合,只不过是换了一种全新的形式,这样假如医生记录的数据是“好点了”,但是总数据不小心被人直接 state.people.eyes = “问题有点严重” 做了这样的处理,达不到预期的结果,所以现在我们引入一个纯函数的概念,就是 reducer 内部不再进行 state 数据的直接操作,而是变成一个纯函数,这样避免了直接修改数据的不可预测性。
在进行修改 reducer 之前,先介绍下函数式编程中的一个概念——纯函数:
- 对于同一参数,返回同一结果
- 完全取决于传入的参数
- 不会产生副作用
对于同一参数返回同一结果:
let x = 1; const add = (y) => x + y console.log(add(2)) //3 console.log(add(2)) //3 复制代码
两次 add 函数调用传入的都是 2 ,可以看到返回结果同样是 3 ,满足第一个条件,继续看第二个条件完全取决于传入的参数:
let x = 1; const add = (y) => x + y console.log(add(2)) //3 x = 2; console.log(add(2)) // 4 复制代码
很明显,这个结果值没有完全依赖传入的参数,修改了外部变量 x 的值,返回值也跟着变化,同时第一个条件也不成立,所以这个函数不是纯函数。再继续看下第三个条件不会产生副作用,如下代码:
const add = (obj, y) => { obj.x = 3 return obj.x + y } const obj = { x:1 } console.log(add(obj,2)) //5 console.log(obj.x) // 2 复制代码
在执行 add 时修改了外部 obj.x 的值,所以相当于对外部的变量产生了副作用,修改为纯函数:
const add = (y) => { let obj = { x:1 } obj.x = 2 return obj.x + y } console.log(add(2)) //4 或者 const add = (x,y) => x + y //也是一个纯函数 复制代码
这个在 add 内部修改了 obj.x 的值,但是对外部是没有任何影响的,不管我传的参数是多少,不会受其他干扰,可预期到结果值,同时满足上述三个条件,所以它是一个纯函数。所谓的对外部有副作用,就是我们在函数中进行了:
- 发出 HTTP 调用
- 改变外部数据或者 Dom 状态
- console.log()
- Math.random()
- 获取的当前时间
所以判断是否是纯函数,只要同时满足上述三个条件即可。
在我们平时开发中并非所有函数都需要是纯的, 比如操作 DOM 的事件处理程序就不适合纯函数。使用纯函数的目的是为了更好的进行单元测试,函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同,因此,每一个函数都可以被看做独立单元。纯函数是很严格的,有的时候甚至只能进行数据的操作,在 Redux 中之所以要这么用纯函数编写 reducer 主要是为了记录数据前后变化,达到数据可预测的结果(开头预留的问题)。
了解了纯函数,先开始修改下 reducer 函数,使其变成一个纯函数,先引入一个浅拷贝知识点,浅拷贝就是只拷贝对象的第一层,里面的层都是引用拷贝,新数据的修改会触发原数据的变化。可以通过对象展开运算符或者 Object.assign() 实现浅拷贝:
const state = { a:1, b:{ c: 1 } } const newstate = Object.assign({}, state, { a:2 }) newstate.b.c=4 console.log(state.b.c) // =>4 因为Object.assign是浅拷贝,深层次的拷贝的是引用,所以受newstate的影响 //可以在每一层要修改的数据的地方进行一次浅拷贝 const newstate = Object.assign({}, state, { a:2, b:{ ...state.b, c: 5 } }) newstate.b.c=4 console.log(statestate.b.c) //=> 1 不受newstate 的影响,因为 b 又被浅拷贝了一份出来 复制代码
开始修改 reducer 使其内容都浅拷贝一份并返回出来(这里数据不多的情况下,深拷贝也是可以的,这里涉及到 redux 的数据优化问题,先不介绍):
function reducer(state, action){ switch (action.type){ case 'EYES_QUESTION_LOG': return Object.assign({}, state, { people: { ...state.people, eyes: action.data } }) break; case 'EYES_COLOR_LOG': return Object.assign({}, state, { people: { ...state.people, color: action.data } }) break; default: break; } } //ceateStore函数中的dispatch也需要修改下 const dispatch = (action) => { state = stateChanger(state, action) //将reducer中的值每次浅拷贝一份 返回出来 listeners.forEach((listener) => listener()) } 复制代码
这样修改之前,dispatch 之前修改了全局变量 dispatch 之后的结果会受全局变量的影响,比如:
stateAll.people.eyes = '111' store.dispatch({ type: "EYES_QUESTION_LOG", data: "好点了" }) 复制代码
此时页面中会打印出 111,因为数据对象指向同一个地址,修改后会直接变化,现在我们的 reducer 是纯函数,而且里面的数据都是浅拷贝一份出来的,这样修改之后无论全局变量如何变化,我们 dispatch 的结果永远是自己控制的数据,不受外界影响,所以界面会打印出“好点了”。现在的 stateAll 我们是放在全局的,再继续优化下,直接放在 reducer 中创建:
function reducer(state, action){ if(!state){ return { people:{ eyes: '有点疼', color: 'red' } } } ... } function createStore (stateChanger) { //修改为只接受一个参数,内部让state初始为null 这样可以在reducer中默认创建初始值 let state = null; ... dispatch({}) //这里主动执行以下,使默认值生效 } 复制代码
到这里我们已经完成了一个简易版的 Redux,使用了观察者 设计模式 来创建 store,使用函数式编程来编写 reducer,它不属于源码的部分,而是留给我们来手动编写的,Redux 的核心函数只有 createStore 这一个。
本小节的完整代码会在文末统一贴出。
JS 中间件介绍
中间件是一种独立的系统软件或服务程序,分布式应用软件借助这种软件在不同的技术之间共享资源。中间件位于客户机/ 服务器的操作系统之上,管理计算机资源和网络通讯。是连接两个独立应用程序或独立系统的软件。相连接的系统,即使它们具有不同的接口,但通过中间件相互之间仍能交换信息。执行中间件的一个关键途径是信息传递。
以上解释来自百度百科。其中最关键的一句是信息传递,在 JS 中我们引入中间件的概念就是为了传递信息,它在 Node.js 中被广泛使用,"它泛指一种特定的设计模式、一系列的处理单元、过滤器和处理程序,以函数的形式存在,连接在一起,形成一个异步队列,来完成对任何数据的预处理和后处理"。
为什么会需要中间件?
比如 A 在医生发布结果后,虽然知道了当前的病情状态,但是他还想知道此前的病情状态来做个对比,那么只能医生在每次发布的时候在传递一个之前的状态操作,假如说这个医生很负责,每天都要给患者发送报告,一个疗程下来就会增加双倍的操作,现在通过一个中间件,可以让之前的数据报告由中间件处理,医生还是只负责 dispatch 患者当前病情,不用操心之前的那个状态了。说白了就是我们开发中,想在 dispatch 前后打印出数据 log 记录,不可能去在 dispatch 前后都写上 console.log(),这样的话重复代码就太多了,所以可以借助中间件:
let looger = function({dispatch,getState}){ return function(next){ return function(action){ //可以优化处理 根据action操作 console.log('dispatch之前数据:', getState()) let result = next(action) console.log('dispatch之后数据:', getState()) return result; } } } 复制代码
再比如,医生想发布报告后,让患者延迟一会儿再接收,你会想加个定时器不就可以了?答案是可以的,但是不要墨守成规,再大胆一点,我让 dispatch 也可以执行一个异步的函数,所以 redux-thunk 这个中间件就出现了。我们看下如何实现:
let thunk = function({dispatch,getState}){ return function(next){ return function(action){ if (typeof action === 'function') { return action(dispatch, getState) } return next(action) } } } // es6写法 let thunk = ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState) } return next(action) } 复制代码
参数先忽略掉,怎么调用也先别管,直接看这个中间件的函数结构:
let thunk = function(){ return function(){ return function(){ return '处理结果' } } } 复制代码
先不要问为什么,先记住这样的结构,在 Redux 中编写中间件就是通过这种方式来创建。redux-thunk 源码也就短短的几行,所以创建一个中间件相对来说还是比较轻松的,但是 redux-saga 就不是,它是更复杂的一种处理情况,是一个管理 Redux 应用异步操作的中间件,用于代替 redux-thunk 的。
redux-thunk 采用的是扩展 action 的方式,使 store.dispatch 的内容从普通对象扩展到函数,而 saga 通过创建 Sagas 将所有异步操作逻辑存放在一个地方进行集中处理,以此将 React 中的同步操作与异步操作区分开来,以便于后期的管理与维护。在 saga 中,全局监听器和接收器都使用 Generator 函数和 saga 自身的一些辅助函数实现对整个流程的管控。
虽然它比 redux-thunk 复杂很多,但是本质上它也是一个中间件,只不过里面多了其他功能,而 redux-thunk、redux-promise、redux-logger 这几个常见的源码都十分短小清晰,直接拿来可以用,自己写也完全没问题。
JS 函数柯里化
了解函数的柯里化之前,先看一下高阶函数(high-order function),高阶函数是满足下面两个条件其中一个的函数:
- 函数可以作为参数
- 函数可以作为返回值
我们平时使用的 setTimeout,map,filter,reduce 等都属于高阶函数,函数的柯里化,也属于高阶函数的一种应用。柯里化函数的定义是:
它用于创建已经设置好了一个或多个参数的函数。函数的柯里化的基本使用方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数
注:以上解释来自《JavaScript高级程序设计(第3版)》。
柯里化是转换函数调用从 f(a,b,c) 至 f(a)(b)(c),比如有一个计算和的函数:
function add(num1, num2, num3){ return num1 + num2 + num3; } add(1, 2, 3) //=>6 //改为柯里化形式 function add(num1){ return (num2) => (num3) => { return num1 + num2 + num3 } } add(1)(2)(3) //=>6 复制代码
看到这里是不是就和我们上面中间件的写法一致了,当然这里的柯力化只是举个最简单的例子(这是因为参数个数已知并且固定)。现在假如我们有两个中间件函数:
function middle1(api){ return (num2) => (num3) => { return num2(num3) } } function middle2(api){ return (num2) => (num3) => { return num2+num3 } } 复制代码
现在我想做的是让数据过来的时候传入这两个中间件,数据从 middle1 流向 middle2,并最终执行 middle2 中的处理结果,我们可以这样做:
var b = middle2({})(2); var newDispatch = middle1({})(b); applyMiddleware(middle1,middle2).dispatch(6) // 6 + 2 =>8 复制代码
在执行 middle2({}) 之后返回:
(num2) => (num3) => { return num2+num3 } 复制代码
再次执行 middle2({})(2) ,上面这个函数的 num2 就是传入的参数 2,此时返回:
(num3) => { return num2+num3 //此时的num2 是2 num3还没有值 } 复制代码
接来下执行 middle1({}) 之后返回:
(num2) => (num3) => { return num2(num3) } 复制代码
我们可以看到和上面执行 middle2({}) 的结果是一致的,接着将 b 执行的结果再传入 newDispatch,现在变成了将 middle1 中的参数 num2 变成 b 的执行结果:
1号函数: (num3) => { //num2就是 2号函数: (num3) => { // return num2+num3 //} return num2(num3) } 复制代码
所以我们最后执行 newDispatch(6) 就是相当于执行最后的逻辑, 1号函数接受外界的6这个参数,num3 变成了6,再执行2号函数,将 num3(6)传入并执行 return num2+num3,这里打个断点,就会很清晰的明白执行流程了。通过这样的操作,我们就将两个中间件函数串联了起来。我们知道 JS 中间件就是负责信息传输的,可以理解为它不修改源数据,而是能够采集信息附带的资源或改变信息的原本运输方式但不修改源信息,比如将同步改为异步(这句话纯属个人解释,无任何参考源)。上面的代码还需要继续完善:
function middle1(api){ return (next) => (num3) => { return next(num3) } } //这里改写 不在中间件的地方求值 function middle2(api){ return (num2) => (num) => { return num2(num) } } //将处理函数封装 function applyMiddleware(arg1,arg2){ const api = {} let b = arg2(api)(function(num){ console.log(num+':我是外面传来的数据') }); let newDispatch = arg1(api)(b); return { dispatch:newDispatch } } applyMiddleware(middle1,middle2).dispatch(6) //=>6:我是外面传来的数据 复制代码
中间件中不再进行求值运算,b 函数之前是接受一个常量,现在改为一个函数,在这里可以接受到外部传入的值,从而可以进行其他计算,将 newDispatch 闭包的形式暴露出去,外部可以直接调用。
现在已经大致明白了中间件的如何进行数据串联并且通过一个 applyMiddleware 处理函数来取得结果,现在将这个功能和上面我们已经实现的一个简单 Redux 结合起来使用,就可以实现我们上面提到的中间件所说的一些优点。
Redux 集成中间件
上面提到执行逻辑放在了 applyMiddleware 这个函数中,现在和 Redux 结合起来,让 Redux 执行这个逻辑去,这样的话整个数据又变成了可预测的。之前的创建 store 的方式是 createStore(reducer),现在我们集成中间件,所以需要先修改下:
const store = applyMiddleware(thunk,looger,createStore,reducer); 复制代码
前两个参数就是我们所谓的中间件函数,如第四小节中的例子,为了让数据在两个中间件中串行,第三个参数就是 createStore 函数,目的就是为了让之前 b 函数中的数据处理逻辑让给 redux 执行,第四个参数就是我们的 reducer 管理数据的。先理解了这些,再看这个 applyMiddleware 函数如何改写才能让给 Redux 执行逻辑:
function applyMiddleware(arg1,arglog,arg2,arg3){ //创建 store var store = arg2(arg3) // 获取dispatch var dispatch = store.dispatch; //先暂时创建一个 新的dispatch 理解为增强之前的 dispatch var newDispatch = function(){}; //为了让中间件函数可以使用使用store的 getState 和 dispatch功能函数 var middlewareAPI = {} //这里就是上面例子所说的 让数据串流 var b = arglog(middlewareAPI)(dispatch); newDispatch = arg1(middlewareAPI)(b); // 这里多了一个...store,因为我们要让中间件处理函数处理中间件之后外部的store依然可以使用原来store的功能 return { ...store, dispatch:newDispatch } } 复制代码
先看第一句就是我们第二小节的创建 store 的方式,只不过这里是作为参数传入到 applyMiddleware 函数中去创建,这里的重点是我们将 dispatch 替换了例子中的那个函数形参,因为这样的话数据又回到了 Redux 中去处理了,这个 applyMiddleware 就完全成为了一个纯函数,只是起到了集成中间件的作用,对数据并不会产生修改等无法预测的影响。现在我们 dispatch 一个常量或者一个对象是没问题的,但是我们想 dispatch 一个函数是不行的,所以再继续修改下:
function applyMiddleware(arg1,arglog,arg2,arg3){ ... var middlewareAPI = { getState: store.getState, dispatch: (action) => newDispatch(action) } ... } let thunk = function({dispatch,getState}){ return function(next){ return function(action){ // dispatch的是函数 if (typeof action === 'function') { return action(dispatch, getState) } //普通调用 return next(action) } } } 复制代码
这样改装之后我们也就明白了中间件的第一层函数中的对象形参是什么了,就是在 middlewareAPI 这个对象,这样的话在中间件函数中我们可以转发参数到 dispatch(fun) 中,比如使用 thunk 这个中间件:
var common = function(dispatch){ console.log('2s之后会打印这个异步结果,请稍后...') setTimeout(function(){ dispatch({ type: 'UPDATAS', data: 'newdata' }) },2000) } store.dispatch(common) 复制代码
thunk 函数中第三层的形参 action 就是 common 函数,common 里面执行 dispatch 时候触发了 newDispatch,此时的 newDispatch 已经有了新值,它执行的时候会触发 next(action),此时的 next 就是在 applyMiddleware 中给第一个中间件传的最后一个参数 dispatch。
这个是有点太绕了,就是函数的各种回调,打个断点,慢慢的看执行流程,再回过头来肉眼多看几遍代码的处理流程,就会明白。现在我们参数都是写的固定的传入两个中间件,假如数我想传入多个现在的方式就不适用了,所以还是需要继续优化,利用参数截取,除去后两个固定的参数,剩下的就是我们的中间件的个数,但是这样不是很好,再把问题复杂化一下,现在我还是想直接调用 createStore 来创建 store,将 applyMiddleware 作为参数传给 createStore,看看如何改写:
function creatStore(reducer, preloadState, enhancer){ //creatStore(reducer,middelewareFun) 只有两个参数时(第二个参数为中间件函数,preloadState可选) 将第二个参数视为enhancer增强函数 if(typeof preloadState === 'function' && typeof enhancer === 'undefined'){ enhancer = preloadState; preloadState = undefined; } //如果传了中间件则处理 否则直接执行无中间的情况 if(typeof enhancer !== 'undefined'){ if(typeof enhancer !== 'function'){ throw new Error('中间件必须是一个函数!'); } /** * enhancer是上面applyMiddleware函数返回的匿名函数 接收了 enhancer 传来的 createStore * // 第一层匿名函数 * return function (createStore) { // 接收了 enhancer(createStore) 传来的 reducer, preloadedState return function (reducer, preloadedState, enhancer) { ... } }; */ return enhancer(creatStore)(reducer, preloadState); } //reducer为一个函数必须 if(typeof reducer !== 'function'){ throw new Error('reducer 必须为一个函数'); } } const store = creatStore(changeState, applyMiddleware(thunk,looger)); 复制代码
第一层判断就是防止第二个参数不传入默认值,做一个参数交换,剩下的无非都是做一些限制性的判断。这里最关键的一句话就是 return enhancer(creatStore)(reducer, preloadState); 说白了这个还是去主动调用的 applyMiddleware 换一身衣服而已。applyMiddleware 这个函数也得变变身:
function applyMiddleware(...middlewares){ //第一层匿名函数(createStore)接收一个参数 return (createStore) => (...args) => { // 第二层匿名函数...args代表(reducer, preloadedState)接收两个参数 /** * 在下面的函数creatStore的enhancer(creatStore)(reducer, preloadState) * 只传了reducer, preloadState两个参数 也就是在这个过程中就当做无中间件的情况处理 */ var store = createStore(...args) ... } } 复制代码
这样的好处就是我们所有的中间件函数只传给 applyMiddleware 就可以,用户调用的时候参数是分离的,没有参在一块,执行顺序是 applyMiddleware(thunk,looger) --> creatStore() --> enhancer(creatStore)(reducer, preloadState) ,具体的参数说明请看注释。改写完了函数的外表,applyMiddleware 中的中间件处理函数也可以接着改写:
function applyMiddleware(...middlewares){ ... var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } middlewares.forEach(middleware => dispatch = middleware(middlewareAPI)(dispatch) ) return { ...store, dispatch } } 复制代码
我们上面是创建一个 newDispatch 来存储新的 dispatch,没有必要这样处理,直接将 dispatch 覆盖掉就行,所以我们这样可以通过循环,处理传入进来的中间件函数,不需要手动一个个单独执行。到这里虽然已经可以了,但是还是想折腾一下,换个写法试试:
function middle1(api){ return (next) => (num3) => { return next(num3) } } function middle2(api){ return (num2) => (num) => { return num2(num) } } function applyMiddleware(arg1,arg2){ const api = {} var a = arg1(api); var b = arg2(api); let newDispatch = a(b(function(num){ console.log(num+':我是外面传来的数据') })); return { dispatch:newDispatch } } applyMiddleware(middle1,middle2).dispatch(6) // 6:我是外面传来的数据 复制代码
applyMiddleware 中采用 compose 的方式处理结果也是一样,执行流程也是一样的,只不过它又是一个变身的写法,同样要处理不固定参数,所以要实现 fun1(fun2(fun3('我是store.dispatch'))) 到 compose([fun1,fun2,fun3])('我是store.dispatch') 这个过程才是重点,可以通过多种方式来实现。
方式1 :最容易理解的一种方式,就是通过 for 循环,依次遍历执行并将结果返回给下一个函数执行,这个的循环是逆序的,我们可以将数组翻转一下,正序使用循环遍历也可以。
function compose(...fns) { return function (res) { for (var i = fns.length - 1; i > -1; i--) { res = fns[i](res) } return res } } 复制代码
方式2 :使用递归。
function compose(...args) { let count = args.length - 1 let result return function fun (...arg1) { result = args[count].apply(null, arg1) if (count <= 0) { return result } count-- return fun.call(null, result) } } 复制代码
方式3 :借助高阶函数 reduce 来实现,它上一次处理的返回值,无默认指定初始值参数时候参数1代表处理的数组第一个值,参数2从 1 开始计算下标, 参数2代表当前处理的数据。
function compose(...funcs){ return function(...args){ return funcs.reduce(function(a,b){ //a 上一次处理的返回值(无默认指定初始值参数时候a代表处理的数组第一个值,b从1开始计算下标) b当前处理的数据 return a(b(...args)) }) } } 复制代码
方式4 :借助高阶函数 reduceRight 来实现,它的执行逻辑和 reduce 相反。
function compose(...funcs){ return function(...args){ return funcs.reduceRight(function(a,b){ // 和reduce对比 这里的参数位置对换下 return b(a(...args)) }) } } } 复制代码
所以我们的 applyMiddleware 可以使用为 compose 的形式改写:
function applyMiddleware(...middlewares){ ... var chain = [] chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) ... } 复制代码
到这里中间处理函数我们已经完全实现了。
接下来再优化代码,比如,我们要保证 reducer 为一个纯函数,对它来做一些限制,比如我们在 reducer 函数中又执行了 store.dispatch({type:'UPDATA',data:2}) 不加锁就会死循环等等:
function creatStore(reducer, preloadState, enhancer){ ... let isDispatching = false; const subscribe = (listenerFun) => { //订阅不允许在reducer操作 if(isDispatching){ throw new Error('Reducer 中不可以subscribe'); } listeners.push(listenerFun); } const getState = () => { //执行到 state = reducer(state, action); isDispatching 变成 true 未执行到 finally if(isDispatching){ throw new Error('Reducer 中不可以读取state'); } return state }; const dispatch = (action) => { if (isDispatching) { throw new Error('Reducers中不允许执行dispatch') } try{ //到这里会出现死循环的情况 所以需要加锁 isDispatching = true; state = reducer(state, action); }finally{ isDispatching = false; } for(let i = 0, len = listeners.length; i < len; i++){ listeners[i](state); } } ... } 复制代码
增加了一个 isDispatching 锁来判断,在取值那里的 reducer 是不允许操作 state ,尽管可以取到,但是就是规定不允许这么做。到这里还得继续考虑一个比较严重的问题,我们现在只能处理一个 reducer,业务复杂的情况下,就这么一个中转站,代码会十分庞大,很难去维护,所以需要拆分为多个 reducer,所以还得需要一个功能,就是把这些小的中转站再汇集到一个大的中转站,这样数据还是由大的中转自动处理,我们只管理各个小的 就可以,首先我们要明确数据格式应该转换成什么样子:
var allState = combineReducers({ reducer, reducer1 ... }); 处理之后 allState 的数据格式如下: { reducer: {...reducer}, reducer1: {...reducer1} } 复制代码
其实就是把原来零散的数据统一集中到一个大的 obj 下面了,创建 combineReducers 功能函数:
const combineReducerss = reducers => { //这里得到的state在creatstore之后合并为一个{a:{..},b:{...},...}这种形式 return (state = {}, action) => { let all = {} for(let key in reducers){ //reducers[key](state[key], action);中state[key]拆开为对应各个reducer的state 最后返回整个集合 all[key] = reducers[key](state[key], action); } return all }; }; var reducer = combineReducers({ reducer1, reducer2 ... }); 复制代码
第一次调用 combineReducers 返回一个函数,是因为我们在 createStore 函数里面 state = reducer(state, action) 这样调用 reducer,相当于去执行了所有的 reducer,并最终将数据返回。我们也可以使用函数式编程的方式,使用高阶函数 reduce 将上面的方法再换个包装:
const combineReducers = reducers => { return (state = {}, action) => { return Object.keys(reducers).reduce( (nextState, key) => { nextState[key] = reducers[key](state[key], action); return nextState; }, {} ); }; }; 复制代码
和 for 循环是一个道理,只不过这里的 reduce 传入了一个默认的参数,因为让它默认是一个对象,才可以使用 Object[key] 的形式。现在每次都会返回一个新生成的对象,假如数据没有变化,也是返回一个重新生成的对象,现在再优化一下:
const combineReducers = reducers => { return (state = {}, action) => { let hasChanged = false const nextState = {} for(let key in reducers){ const reducer = reducers[key] const previousStateForKey = state[key] const nextStateForKey = reducer(previousStateForKey, action) nextState[key] = nextStateForKey hasChanged = hasChanged || nextStateForKey !== previousStateForKey } return hasChanged ? nextState : state }; }; 复制代码
这里重要的是 nextStateForKey !== previousStateForKey 做一个对象变化前后的对比,比较的是引用是否相同,而不是比较值,hasChanged = hasChanged || nextStateForKey !== previousStateForKey 默认是 false,只要前后数据对比发生了引用的变化,则 hasChanged 肯定就是 true,会返回 nextState 全新的状态,数据引用一致的情况就不对外公布新的状态,还是返回原来的 state。
总结
我们已经把 Redux 的实现过程做了一个比较详细的介绍,感谢您认真地看完了本篇文章,现在再回去翻看 Redux 的源码就会感觉比较有个清晰的认识了,相信里面有些比较绕的写法也会很清晰的读懂。
发布订阅、组合函数、函数柯里化、函数式编程等技巧在 Redux 中展现的淋漓尽致,尤其是 compose 功能的实现,还可以使用 Promise、Generator 等方式实现(本文没有列举),所以在以后的代码编写过程中还是要多创新,把问题去复杂化一下有时也是好事!
第二小节代码:https://github.com/wineSu/redux/blob/master/demo.js 完整代码:https://github.com/wineSu/redux/blob/master/redux4.js
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- ReactNative源码解析-初识源码
- Spring源码系列:BeanDefinition源码解析
- Spring源码分析:AOP源码解析(下篇)
- Spring源码分析:AOP源码解析(上篇)
- 注册中心 Eureka 源码解析 —— EndPoint 与 解析器
- 新一代Json解析库Moshi源码解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。