浅析Redux源码
@(Redux)[|用法|源码]
Redux 由Dan Abramov在2015年创建的科技术语。是受2014年Facebook的Flux架构以及函数式编程语言Elm启发。很快,Redux因其简单易学体积小短时间内成为最热门的前端架构。
@[三大原则]
- 单一数据源 - 整个应用的
state
被储存在一棵object tree
中,并且这个object tree
只存在于唯一一个store
中。所有数据会通过store.getState()
方法调用获取. - State‘只读’ - 根据
state
只读原则,数据变更会通过store,dispatch(action)
方法. - 使用纯函数修改 -
Reducer
只是一些纯函数1,它接收先前的state
和action
,并返回新的state
.
[TOC]
准备阶段
柯里化函数(curry)
//curry example const A = (a) => { return (b) => { return a + b } }
通俗的来讲,可以用一句话概括柯里化函数:返回函数的函数.
优点: 避免了给一个函数传入大量的参数,将参数的代入分离开,更有利于调试。降低耦合度和代码冗余,便于复用.
代码组合(compose)
举个例子
let init = (...args) => args.reduce((ele1, ele2) => ele1 + ele2, 0) let step2 = (val) => val + 2 let step3 = (val) => val + 3 let step4 = (val) => val + 4 let steps = [step4, step3, step2, init] let composeFunc = compose(...steps) console.log(composeFunc(1, 2, 3)) // 1+2+3+2+3+4 = 15
接下来看下FP思想的compose的源码
const compose = function (...args) { let length = args.length let count = length - 1 let result let this_ = this // 递归 return function f1(...arg1) { result = args[count].apply(this, arg1) if (count <= 0) { count = length - 1 return result } count-- return f1.call(null, result) } }
通俗的讲: 从右到左执行函数,最右函数以arguments为参数,其余函数以上个函数结果为入参数执行。
优点: 通过这样函数之间的组合,可以大大增加可读性,效果远大于嵌套一大堆的函数调用,并且我们可以随意更改函数的调用顺序
CombineReducers
作用
随着整个项目越来越大,state
状态树也会越来越庞大,state的层级也会越来越深,由于redux
只维护唯一的state
,当某个action.type
所对应的需要修改state.a.b.c.d.e.f
时,我的函数写起来就非常复杂,我必须在这个函数的头部验证state
对象有没有那个属性。这是让开发者非常头疼的一件事。于是有了CombineReducers
。我们除去源码校验函数部分,从最终返回的大的Reducers
来看。
Note:
- FinalReducers : 通过
=== 'function'
校验后的Reducers
.- FinalReducerKeys :
FinalReducers
的所有key
(与入参
Object
的key
区别:过滤了value
不为function
的值)
源码
// 返回一个function。该方法接收state和action作为参数 return function combination(state = {}, action) { var hasChanged = false var nextState = {} // 遍历所有的key和reducer,分别将reducer对应的key所代表的state,代入到reducer中进行函数调用 for (var i = 0; i < finalReducerKeys.length; i++) { var key = finalReducerKeys[i] var reducer = finalReducers[key] // CombineReducers入参Object中的Value为reducer function,从这可以看出reducer function的name就是返回给store中的state的key。 var previousStateForKey = state[key] // debugger var nextStateForKey = reducer(previousStateForKey, action) // 如果reducer返回undefined则抛出错误 if (typeof nextStateForKey === 'undefined') { var errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } // 将reducer返回的值填入nextState nextState[key] = nextStateForKey // 如果任一state有更新则hasChanged为true hasChanged = hasChanged || nextStateForKey !== previousStateForKey } return hasChanged ? nextState : state }
小结
CombineReducers
实现方法很简单,它遍历传入的Reducers
,返回一个新的Reducer
.该函数根据state
的key
去执行相应的子Reducer
,并将返回结果合并成一个大的state
对象。
CreateStore
作用
createStore
主要用于store
的生成,我们先整理看下createStore
具体做了哪些事儿。(这里我们看简化版代码)
源码(简化版)
const createStore = (reducer, initialState) => { // initialState一般设置为null,或者由服务端给默认值。 // internal variables const store = {}; store.state = initialState; store.listeners = []; // api-subscribe store.subscribe = (listener) => { store.listeners.push(listener); }; // api-dispatch store.dispatch = (action) => { store.state = reducer(store.state, action); store.listeners.forEach(listener => listener()); }; // api-getState store.getState = () => store.state; return store; }
小结
源码角度,一大堆类型判断先忽略,可以看到声明了一系列函数,然后执行了dispatch
方法,最后暴露了dispatch
、subscribe
……几个方法。这里dispatch
了一个init Action
是为了生成初始的state
树。
ThunkMiddleware
作用
首先,说ThunkMiddleware
之前,也许有人会问,到底middleware
有什么用?
这就要从action
说起。在redux
里,action
仅仅是携带了数据的普通js
对象。action creator
返回的值是这个action
类型的对象。然后通过store.dispatch()
进行分发……
action ---> dispatcher ---> reducers
同步的情况下一切都很完美……
如果遇到异步情况,比如点击一个按钮,希望1秒之后显示。我们可能这么写:
function (dispatch) { setTimeout(function () { dispatch({ type: 'show' }) }, 1000) }
这会报错,返回的不是一个action
,而是一个function
。这个返回值无法被Reducer
识别。
大家可能会想到,这时候需要在action
和Reducer
之间架起一座桥梁……
当然这座桥梁就是middleware
。接下来我们先看看最简单,最精髓的ThunkMiddleware
的源码
源码
const thunkMiddleware = ({ dispatch, getState }) => { return next => action => { typeof action === 'function' ? action(dispatch, getState) : next(action) } }
非常之精髓。。。我们先记住上述代码,引出下面的ApplyMiddleware
ApplyMiddleware
作用
介绍ApplyMiddleware
之前我们先看下项目中store
的使用方法如下:
let step = [ReduxThunk, middleware, ReduxLogger] let store = applyMiddleware(...step)(createStore)(reducer) return store
通过使用方法可以看到有3处柯里化函数的调用,ApplyMiddleware
函数redux
最精髓的地方,成功的让redux
有了极大的可拓展空间,在action
传递的过程中带来无数的“副作用”,虽然这往往也是麻烦所在。 这个middleware
的洋葱模型思想是从koa
的中间件拿过来的,用图来表示最直观。
洋葱模型
我们来看源码:
源码
const applyMiddleware = (...middlewares) => { return (createStore) => (reducer, initialState, enhancer) => { var store = createStore(reducer, initialState, enhancer) var dispatch var chain = [] var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } // 每个 middleware 都以 middlewareAPI 作为参数进行注入,返回一个新的链。 // 此时的返回值相当于调用 thunkMiddleware 返回的函数: (next) => (action) => {} ,接收一个next作为其参数 chain = middlewares.map(middleware => middleware(middlewareAPI)) // 并将链代入进 compose 组成一个函数的调用链 dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } }
ApplyMiddleware
函数第一次调用的时候,返回一个以createStore
为参数的匿名函数,这个函数返回另一个以Reducer
,initialState
,enhancer
为参数的匿名函数.我们在使用方法中,分别可以看到传入的值。
结合一个简单的实例来理解中间件以及洋葱模型
// 传入middlewareA const middlewareA = ({ dispatch, getState }) => { return next => action => { console.warn('A middleware start') next(action) console.warn('A middleware end') } } // 传入多个middlewareB const middlewareB = ({ dispatch, getState }) => { return next => action => { console.warn('B middleware start') next(action) console.warn('B middleware end') } } // 传入多个middlewareC const middlewareC = ({ dispatch, getState }) => { return next => action => { console.warn('C middleware start') next(action) console.warn('C middleware end') } }
当我们传入多个类似A,B,C的middleware
到ApplyMiddleware
后,调用
dispatch = compose(...chain)(store.dispatch)
结合场景并且执行compose
结果为:
dispatch = middlewareA(middlewareB(middlewareC(store.dispatch)))
从中我们可以清晰的看到middleware
函数中的next
函数相互连接,这里体现了compose
FP编程思想中代码组合的强大作用。再结合洋葱模型的图片,不难理解是怎么样的一个工作流程。
最后我们看结果,当我们触发一个store.dispath
的时候进行分发。则会先进入middlewareA
并且打印A start
然后进入next
函数,也就是middlewareB
同时打印B start
,然后触发next
函数,这里的next
函数就是middlewareC
,然后打印C start
,之后才处理dispath
,处理完成后先打印C end
,然后B end
,最后A end
。完成整体流程。
小结
Redux applyMiddleware
机制的核心在于,函数式编程(FP)
的compose
组合函数,需将所有的中间件串联起来。- 为了配合
compose
对单参函数的使用,对每个中间件采用currying
的设计。同时,利用闭包原理做到每个中间件共享store
。(middlewareAPI
的注入)
Feedback & Bug Report
- github: @同性交友网站
Thank you for reading this record.
- 纯函数,它不依赖于外部环境(例如:全局变量、环境变量)、不改变外部环境(例如:发送请求、改变DOM结构),函数的输出完全由函数的输入决定。比如 slice 和 splice,这两个函数的作用并无二致——但是注意,它们各自的方式却大不同,但不管怎么说作用还是一样的。我们说 slice 符合纯函数的定义是因为对相同的输入它保证能返回相同的输出。而 splice 却会嚼烂调用它的那个数组,然后再吐出来;这就会产生可观察到的副作用,即这个数组永久地改变了。可以看到,splice改变了原始数组,而slice没有。我们认为,slice不改变原来数组的方式更加“安全”。改变原始组数,是一种“副作用”。 ↩