最近一段时间公司比较闲下来,故抽空学了react
+ redux
。在react
方面,鉴于有Vue
的经验,很多东西概念上还是很统一的,例如Virtual Dom
props
JSX
等。区别在于react
没有像Vue
那样那么多的api,更多的是用纯粹的JavaScript去解决,在这一点我觉得我更喜欢react
多些。
当然,react
更加的函数式,对于state
的变更一定要通过setState
这一点可以看出,react
对于state
不允许有任何的副作用
。而相对与react
的简单,更困扰我的是redux
,它更加函数式,从代码中,随处可见的高阶函数
、科里化
以及让这个之前没有接触过函数式编程的我感到神奇的compose
等。因为如此,它的异步操作
更是与Vuex
有着很大的差异,各种各样基于redux
的异步处理工具如雨后春笋般冒出来,例如redux-saga
、redux-observable
以及官方的一个只有12行代码的redux-thunk
。
因为没有时间可以给我实战中去理解它,所以我选择通过阅读其源代码,来加深对redux
的理解
主要的api
redux
暴露出来的api并不多
- createStore:接受
reducers
生成状态树 - combineReducers:把多个
reducer
合成一个reducers
- bindActionCreators: 封装多一层函数嵌套,规避使用
dispatch
- applyMiddleware:初始化中间件
- compose:非常函数式的一个方法,从右到左把接收到的函数合成后的最终函数
在src/indexjs
里面能看到,redux
就只有暴露这五个api,其中经常用到的也就 3-4 个。
createStore
先看一下createStore
的参数
// src/createStore.js/** * @param {Function} reducer * @param {any} [preloadedState] 初始化的state * @param {Function} [enhancer] 这里是一个中间件 * @returns {Store} 返回的是状态树**/复制代码
参数很简单,只有三个,一个是reducer
,第二个是初始化state,在我们有定义reducer
的初始化state
的时候,这个参数是默认不传的,在这里,先把其忽略。第三个是applyMiddleware(...middlewares)
,是中间件
看下整体的代码 其中getState
很简单,就不单独拎出来说,obervable
用于观察者模式,平时几乎用不到,也不讲
// src/createStore.jsexport default function createStore(reducer, preloadedState, enhancer) { // 前面的代码是在做参数的初始化 if (typeof enhancer !== 'undefined') { // 这里是处理中间件的地方,先不看这里,后面看中间件的时候反过来看 return enhancer(createStore)(reducer, preloadedState) } // .. let currentReducer = reducer // reducer let currentState = preloadedState // state let currentListeners = [] // 初始化监听器 let nextListeners = currentListeners let isDispatching = false function ensureCanMutateNextListeners() { // .. } function getState() { // 获取state的方法 // getState极其简单,就不单独拎出来的,就返回了当前的state if (isDispatching) { // 抛出错误 } return currentState } function subscribe(listener) { // 监听器,dispatch action的时候就会执行 // .. } function dispatch(action) { // 分发action的唯一方法 // .. } function replaceReducer(nextReducer) { // 替换 store 当前用来计算 state 的 reducer // .. } function observable() { // 这个方法在文档里也没有体现,貌似是给redux-obervable专门提供的吗还是?不懂 // .. } // 这里dispatch一个init action进行初始化 // 在 src/utils/actionTypes.js 里 dispatch({ type: ActionTypes.INIT }) return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable }}复制代码
接下来一个个看它的方法
subscribe
为什么要先讲subscribe
呢,这涉及到react-redux
的连接
其实我并不了解react-redux
,只知道它调用了subscribe
开启了个监听器,并传入了一个listener
这段代码在react-redux
里的src/utils/Subscription.js
里
trySubscribe() { if (!this.unsubscribe) { this.unsubscribe = this.parentSub ? this.parentSub.addNestedSub(this.onStateChange) : this.store.subscribe(this.onStateChange) this.listeners = createListenerCollection() }}复制代码
在这里传入了一个onStateChange
方法作为listener
然后再看回subscribe
// src/createStore.jsfunction subscribe(listener) { // ... let isSubscribed = true // 判断当前的listener是否与下一个listener一样 ensureCanMutateNextListeners() // push到nextListeners nextListeners.push(listener) console.log(listener) // 返回一个方法,作为取消订阅 return function unsubscribe() { // ... isSubscribed = false ensureCanMutateNextListeners() const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } }复制代码
dispatch
dispatch
做的事情很简单,只是把reducers
传入state
和action
去执行然后返回来的新的state赋值到currentState
,接着再执行通过subscribe
的监听函数就完成了。
// src/createStore.jsfunction dispatch(action) {if (!isPlainObject(action)) { // 如果不是春函数,抛错}if (typeof action.type === 'undefined') { // 没有 type 抛错}if (isDispatching) { // 正在分发其他action,抛错}try { // 进行分发 isDispatching = true // currentReducer 就是我们传进来的reducers currentState = currentReducer(currentState, action)} finally { isDispatching = false}// 执行监听函数const listeners = (currentListeners = nextListeners)for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener()}// 返回actionreturn action}复制代码
对于如何执行的reducers
,我们到后面combineReducers
再看
replaceReducer
在文档里,replaceReducer
是这样描述的
替换 store 当前用来计算 state 的 reducer。这是一个高级 API。只有在你需要实现代码分隔,而且需要立即加载一些 reducer 的时候才可能会用到它。在实现 Redux 热加载机制的时候也可能会用到。
在这里我也没用过,不知道具体用途,可是代码简单,就贴出来看看
// src/createStore.jsfunction replaceReducer(nextReducer) { // 更替reducers再dispatch REPLACE type currentReducer = nextReducer dispatch({ type: ActionTypes.REPLACE }) }复制代码
combineReducers
对于reducers
,如果只有一个reducer
那好说,action
进去然后switch case
执行完就完事了。可是大部分情况我们需要combineReducers
组合我们的多个reducer
,接下来看下combineReducers
的代码实现
// src/combineReducers.jsexport default function combineReducers(reducers) { // 获取reducers的key const reducerKeys = Object.keys(reducers) // 最终生成的reducers const finalReducers = {} // 遍历生成reducers,剔除不是function的 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) let unexpectedKeyCache if (process.env.NODE_ENV !== 'production') { unexpectedKeyCache = {} } let shapeAssertionError try { // 判断是否有初始化state assertReducerShape(finalReducers) } catch (e) { shapeAssertionError = e } // 最后返回了一个combinenation // 在 dispatch 的时候,就执行这个函数 // currentState = currentReducer(currentState, action) return function combination(state = {}, action) { // ... }}复制代码
combineReducers
做的事情也很简单,通过高阶函数,利用闭包剔除不合格的reducer
返回一个combination
,接下来,来看看combination
做了些什么
combination
先回顾下dispatch
里,是怎么调用combination
的
// src/createStore.jscurrentState = currentReducer(currentState, action)复制代码
传入了state
还有action
// src/combineReducers.jsfunction combination(state = {}, action) { // ... let hasChanged = false // 全新的数据 const nextState = {} // 遍历reducers keys // 每一个reducer都会去执行一遍 for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i] const reducer = finalReducers[key] // 拿到变更前的state const previousStateForKey = state[key] // 变更后的state const nextStateForKey = reducer(previousStateForKey, action) // ... // 插入到nextState nextState[key] = nextStateForKey // 判断是否有变化 hasChanged = hasChanged || nextStateForKey !== previousStateForKey } return hasChanged ? nextState : state }复制代码
combination
也很简单,就是遍历reduces
并执行,再返回state。
到这里,整个dispatch的流程就可以清晰列出来了
dispatch => combination => iterator reducers => return new state
中间件的实现
在redux
里,可以通过applyMiddleware
添加中间件,redux
的代码非常简洁,需要复杂的功能,就需要通过中间件去加强。
### applyMiddleware
中间件的作用是,在dispatch
做分发的时候,会按照在applyMiddleware
时传入的中间件顺序,依次执行,到最后再做dispatch
先看看中间件的写法
// 回调给的getState, dispatch方法// 一般dispatch不会使用function someMiddleware ({ getState, dispatch}) { // next是下一个中间件 // action就是dispatch的action return next => action => { // before dispatch // 执行下一个中间件 let returnValue = next(action) // after dispatch // 一般会是 action 本身,除非后面的 middleware 修改了它。 return returnValue }}复制代码
在需要中间件去生成store
的方式有两种
import { createStore, combineReducers, applyMiddleware } from 'redux'// 这是第一种const store = createStore( reducers, applyMiddleware( thunkMiddleware, logger, loggerMiddleware ))// 这是第二种const store = applyMiddleware( thunkMiddleware, logger, loggerMiddleware )(createStore)(reducers)复制代码
两种方式并没有区别,还记不记得之前在createStore
里,说先不看的那句代码
return enhancer(createStore)(reducer, preloadedState)复制代码
这里的enhancer
就是applyMiddleware(...middlewares)
,看下applyMiddleware
的代码
// src/applyMiddleware.jsfunction applyMiddleware(...middlewares) { // 返回一个参数为createStore的匿名函数 // args就是需要传入的reduces还有init state return createStore => (...args) => { // 重新走一遍createStore,生成store const store = createStore(...args) let dispatch = function () { // 这里提供dispatch,可是只用来在中间件中dispatch报错 } let chain = [] // 定义中间件的chain // 在中间件需要的两个方法 const middlewareAPI = { getState: store.getState, dispatch: (...args) => { console.log('DISPATCH') return dispatch(...args) } } // 把需要的getState以及dispatch方法传入中间件 chain = middlewares.map(middleware => { return middleware(middlewareAPI)}) // 这里通过compose生成一个dispatch函数 // 这里还需要传入最初的dispatch,就是为了在中间件执行完毕只有,最后能成功dispatch action dispatch = compose(...chain)(store.dispatch) // 最后返回store的方法以及新的dispatch // 原来的dispatch被覆盖 return { ...store, dispatch } }}复制代码
compose
compose是干什么的呢?先看看一个在Ramda.js
下的compose
import R from 'ramda'let compse = R.compose( R.multiply(2), R.add(7), R.divide(3))compose(9)// 输出20复制代码
简单的说,compose
是一个柯里化函数,将多个函数合并成一个函数,从右到左执行
在例子中,compose
接受三个方法,从右到左就是 除以3
加7
以及乘以2
。添加一个参数9
,依次执行,最后输出的是20
我们先跳到applyMiddleware
看他的执行
// src/applyMiddleware.jschain = middlewares.map(middleware => { return middleware(middlewareAPI)})dispatch = compose(...chain)(store.dispatch)复制代码
看看redux
中的compose
// src/compose.jsexport default function compose(...funcs) { // 判断funcs的数量 if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } // 这里通过reduce,非常优雅地实现了compose return funcs.reduce((a, b) => { // 这里是compose(...chain) return (...args) => { // 这里传入store.dispatch,最终形成一个执行链 return a(b(...args)) } })}复制代码
reduce
reduce
方法是一个非常函数式的方法,在MDN
上我们可以看到reduce
的详情
reduce() 方法对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个值。
看看reduce
的参数,第一个是执行回调函数,直接贴上MDN
的参数说明
callback 执行数组中每个值的函数,包含四个参数: accumulator 累加器累加回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue(如下所示)。 currentValue 数组中正在处理的元素。 currentIndex 数组中正在处理的当前元素的索引。 如果提供了initialValue,则索引号为0,否则为索引为1。 array 调用reduce的数组
arr.reduce(callback[, initialValue])const total = [0, 1, 2, 3].reduce(function(sum, value) { return sum + value;}, 0);// log 6复制代码
到此,整个中间件就完成了,整理下中间件的流程,举例上面的例子
dispatch(action) => thunkMiddleware => logger => loggerMiddleware => store.dispatch
总结
从整体来说,redux
是一个非常巧妙的库,它代码不多,随处散发着函数式编程的魅力。也是因为那个compose
驱使着我去探索函数式编程。
作为一个浅入react
开发的程序员,redux
只是整个技术栈的一部分,希望接下来可以去探索一些异步流程库,例如redux-saga
redux-observable
等。