这次阅读的redux的版本是4.x
的版本,为啥不是最新的呢?因为最新的redux使用typescript来重写了,变化不是特别大,而typescript会有很多的函数类型定义;另一方面,对js的熟练度肯定比ts要好,理解起来也会相对容易一点,还有这次阅读源码的目的是为了了解整个redux的工作流程。redux的源码非常精简,很适合源码的阅读与学习。在阅读之前,对redux有一定的使用会带来更好的效果,一边看一边反思平常所写的内容。下面就开始吧:
目录
下面的内容是项目的src目录结构
1 2 3 4 5 6 7 8 9 10 11 |
# src -- utils/ -- actionTypes.js -- isPlainObject.js -- warning.js -- applyMiddleware.js -- bindActionCreators.js -- combineReducers.js -- compose.js -- createStore.js -- index.js |
项目的文件非常的少,主要逻辑也是在src
直接目录下的文件,先做个热身,对简单的utils
文件夹入手,这是一些通用的工具方法:
utils
在看源码的过程中,对一些工具方法的使用效果保持一定的记忆,对流程的理解上挺有帮助
actionTypes.js
1 2 3 4 5 |
const ActionTypes = {
INIT: `@@redux/INIT${randomString()}`,
REPLACE: `@@redux/REPLACE${randomString()}`,
PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
} |
actionTypes.js
主要定义一些redux内部使用的action
,randomString
函数是生成一些随机字符串,保证内部使用的action
不会冲突
isPlaginObject.js
1 2 3 4 5 6 7 8 9 10 |
export default function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false
let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto
} |
该方法用于判断参数是否是一个“纯对象”。什么是“纯对象”,就是直接继承Object.prototype
的对象,例如直接声明的对象:const obj = {}
;如果const objSub = Object.create(obj)
,那么objSub
就不是这里说的“纯对象”。
warning.js
1 2 3 4 5 6 7 8 |
export default function warning(message) {
if (typeof console !== 'undefined' && typeof console.error === 'function') {
console.error(message)
}
try {
throw new Error(message)
} catch (e) {} // eslint-disable-line no-empty
} |
warning.js逻辑比较简单:先把错误的详细信息打印出来,再抛出错误。
核心
热身完之后,我们来看一下redux的核心,入口在:src/index.js
:
1 2 3 4 5 6 7 8 |
export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose,
__DO_NOT_USE__ActionTypes
} |
这个是index.js
的暴露对象,都是从外部引入;除此之外,还有一个叫空函数isCrushed
:
1
|
function isCrushed() {} |
这个空函数的作用是啥?因为在代码压缩的时候,会对该函数进行重命名,变成function a(){}
,这样子的函数;这个函数的作用就是,判断如果redux代码被压缩了,而且redux不是运行在production
环境,就会报错,提示使用开发版本。
redux 的核心是createStore
,这个核心我们先放一下,后面再处理,先了解一些辅助该核心的方法:
bindActionCreators
这个方法出场率有时候不是很高,那么它的作用是啥?
首先我们知道一个词汇actionCreator
,这个actionCreator
就如命名那样,是用于创建action
类型的函数。那bindActionCreators
的目的又是什么?这里可能要结合react-redux
的connect
方法与“容器组件”、“展示组件”(容器组件 vs 展示组件)来说明会更好。
通常情况下,如果要让当前组件是用redux,我们会使用react-redux
的connect
方法,把我们的组件通过connect
包裹为一个高级组件,而包裹的过程拿到dispatch
与我们指定的store
数据:
1 2 3 4 5 6 7 8 9 10 11 |
// Container.jsx
import { connect } from 'react-redux'
class Container extends React.Component {
// ...
render () {
return <SimpleComponent />
}
}
export default connect(state => ({ todo: state.todo }))(Container) |
而这个Container
组件我们可以称之为“容器组件”,因为里面包含了一些复杂的处理逻辑,例如与redux的连接;而如果SimpleComponent
组件也有一些操作,这些操作需要更改到redux
的内容,这样子的话,处理方法有两个:
SimpleComponent
也使用connect
处理为高级组件Container
把redux
的dispatch
方法显示传递到SimpleComponent
这两个方法都不是很好,第一方法会让组件更加复杂,可能与我们的容器组件-展示组件的姿势有点不同;第二种方法也可以,但是会让组件变得耦合度高。那能不能让SimpleComponent
组件给Container
的反馈也通过平常props-event
的形式来处理呢,让SimpleComponent
感知不到redux
的存在?
这个时候就可以使用bindActionCreators
了;例如有一个action
为:{ type: 'increment', value: 1}
,通常如果Container
组件触发可以通过:
1 2 |
// actions.js
const Add = (value) => { type: 'increment', value } |
1 2 3 4 5 6 7 8 9 |
// Container.jsx
import Add from './actions.js'
// Container.jsx 某个事件触发触发更新
class Container extends React.Component {
onClick() {
dispatch(Add(1))
}
} |
利用bindActionCreators
处理后,给到SimpleComponent
使用则可以这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Container.jsx
import Add from './actions.js'
class Container extends React.Component {
render () {
const { dispatch } = this.props // 通过 react-redux 的 connect 方法组件可以获取到
const actions = bindActionCreator({
add: Add
}, dispatch)
return <SimpleComponent {...actions} />
}
}
// SimpleComponent.jsx
function SimpleComponent({ add }) {
return <button onClick={() => add(1)}>click</button>
} |
通过bindActionCreators
处理后的函数,add
,直接调用,就可以触发dispatch
来更新,而这个时候SimpleComponent
并没有感知到有redux
,只是当是一个事件函数那样子调用。
了解到bindActionCreators
的作用之后,我们再来看一下源码就很好理解了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function bindActionCreator(actionCreator, dispatch) {
// 使用闭包来屏蔽 dispatch 与 actionCreator
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
export default function bindActionCreators(actionCreators, dispatch) {
// 当 actionCreators 只有一个的时候,直接返回该函数的打包结果
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
// 省略参数类型判断
// ...
const boundActionCreators = {}
for (const key in actionCreators) {
const actionCreator = actionCreators[key]
// 只对 { key: actionCreator } 中的函数处理;actionCreators 中的其他数据类型被忽略
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
} |
combineReducers
接下来说一下combineReducers
,这个方法理解起来比较简单,就是把多个reducer
合并到一起,因为在开发过程中,大多数的数据不会只有一个reducer这么简单,需要多个联合起来,组成复杂的数据。
1 2 3 4 5 6 |
// () => {} 为每个对应的reducer
const state = {
count: () => {},
userData: () => {},
oeherData: () => {}
} |
通常使用compineReducer
可以让我们规避一些问题,例如对reducer的传入参数的判断等,保证reduce
流程的运转,简化核心代码如下,去掉一部分开发代码,但是会注释作用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
// 检查所有reducer的key值是否合法,reducer是一个函数,则加入到finalReducers
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
// 这里有个判断,如果reducers对象的某个key值的value为undefined,则报错
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
// 这里有个判断(assertReducerShape),判断是否有 reducer 返回是 undefined
// 如果有,则先保留这个错误,我们定义为错误 A
// 这个 combination 函数,每次dispatch都会执行一次
return function combination(state = {}, action) {
// 这里有个判断,如果错误A存在,则抛出异常
// 这里有个判断(getUnexpectedStateShapeWarningMessage),会对数据进行多重判断,
// 判断有错,则抛出异常,判断的规则有:
// 1. reducers的数量是否为0
// 2. 对每次执行reducer传入的state(state的来源后面讲到)是否是“纯对象”(上面有提到)
// 3. 对每次执行reducer传入的state对象判断,是否该对象所有的字段都是“自己的”(hasOwnProperty),
// 也就是没有一些从父对象继承,toString ?
// 第三点其实有点不太了解,因为第二步纯对象已经过滤了?
// 下面这个就是 combineReducers 的核心代码
let hasChanged = false
const nextState = {}
// 遍历所有的函数reducer,获取返回值,通过判断前后值的不同,判断是否发生了变化,有变化,则返回新的state
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
// 不允许 reducer 返回的值为 undefined,否则报错
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
// 这里判断是否改变,是通过判断 reducer 返回的值与之前的值是否一致
// 所以就突出了“不可变对象”的重要性
// 如果reducer每次返回的对象是在旧对象上面更改数据
// 而对象地址没改变,那么 redux 就认为,这次改变是无效的
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
} |
createStore
讲完上面两个辅助方法之后,来讲一下创建store的核心createStore
的方法;因为createStore
方法比较长,下面先看一下概览:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
export default function createStore(reducer, preloadState, enhancer) {
// 判断是否传入各种参数是否符合要求
// 对于增强器(enhancer)的调用会提前返回
// 创建store的过程被延后到增强器中
// ...
// 当前最新的 reducer 与 state
// listeners 是通过 store实例subscribe的函数数组
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
// 当前的reducer是否在执行当中
let isDispatching = false
// 用于防止 listeners 数组出错,后面讲到
function ensureCanMutateNextListeners() {}
// 返回当前最新的 state,给外部函数调用获取内部数据
function getState() {
return currentState
}
// store.subscribe的方法,用于添加 listener,后面有详细讲解
// 监听 state 的变化
function subscribe(listener) {}
// 触发 reducer 执行,返回新的 state
function dispatch(action) {}
// 使用新的 reducer 替换当前的 currentReducer
// 通常在两种情况下使用:
// 1. 部分 reducer 异步加载,加载完毕后添加
// 2. 用于开发时候用,热更新
function replaceReducer(nextReducer) {
currentReducer = nextReducer
}
// TODO 这个了解不多
function observable () {}
// 触发第一个更新。拿到第一次初始化的 state
dispatch({ type: ActionTypes.INIT })
} |
在没有enhancer处理的过程,createStore
的过程,都是一些声明的函数与变量,唯一开始执行的是dispatch
,现在就从这个dispatch
开始讲解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
function dispatch (action) {
// 判断 action 是否是“纯”函数
// 判断 action.type 是否存在
// ...
// 判断当前的dispatch是否在执行中,多次触发,则报错
if (isDispatching) { throw new Error() }
try {
isDispatching = true
// 尝试去执行 reducer,把返回的 state 作为最新的 state
// 如果 我们的 reducer 是使用 combineReducers 方法包裹的话
// 这里的 currentReducer 为 comineReducer的combination方法
// 这里回答了之前所说的 combination 方法拿到的第一个参数 state
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 更新完 state 之后,就会把监听的函数全都执行一遍
// 注意这里的 currentListeners 被赋值为 nextListeners
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
} |
整个 dispatch 就结束了,很简单,就是把所有reducer都执行一遍,返回最新的 reducer;如果使用combineReducer
来联合所有的reducer的话,相当于执行combination
方法,该方法会把被联合的所有reducer都执行一遍,所以这里能解释说,为什么在reducer方法的时候,在switch...case
要保留一个default
选项,因为有可能执行当前reducer的action,是用于触发其他reducer的;这种情况就把当前reducer对应的state返回即可
1 2 3 4 5 6 7 8 9 10 11 12 |
function reducer(state, action) {
switch (action.type) {
case '':
// ...
break
case '':
// ...
break
default:
return state
}
} |
当state通过reducer更新之后,就会把加入监听的listener
逐个执行;循环的listeners
是currentListeners
,这里要圆一下之前说的ensureCanMutateNextListeners
函数与subscribe
的行为,函数代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
function subscribe(listener) {
// 省略部分参数与进程判断
let isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
if (!isSubscribed) {
return
}
// 省略部分进程可行性判断
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
} |
我们看到subscribe
与unsubscribe
的过程,只是一个很简单的数组处理添加与删除listeners
的过程,但是这两个过程都有执行ensureCanMutateNextListeners
的函数。这个函数的作用是:
保证当次触发listeners
的过程不受影响
这句话怎么理解呢?可以看到触发listeners
也只是把listeners
的函数循环执行一遍。但如果listeners
由此至终都只是一个数组,那么如果某个listeners
执行的内容,再次添加/删除listener
,那么这个循环过程就有可能出现问题:
1 2 3 4 5 6 7 8 9 10 11 |
const { dispatch, subscribe } = createStore(/* ... */) subscribe(() => { log('state change') // 在 listeners 添加监听方法 subscribe(() => { }) // 或者 移除之前监听的部分方法 unsubscribe(/* ... */) }) |
所以ensureCanMutateNextListeners
把listeners
区分为两个数组,一个是当前循环的数组,另一个是下次循环的数组。每次触发dispatch
都是最近更新的一次listeners
快照。
compose 与 applyMiddleware
了解完核心createStore
之后,我们再了解一下增强核心功能的函数:applyMiddleware
,因为applyMiddleware
与compose
关联很密切,applyMiddleware
的实现依赖compose
。
compose
compose
是一个函数,先看一下compose
的代码:
1 2 3 4 5 6 7 8 9 10 11 |
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
} |
非常的精简;compose
的代码的作用是简化代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
(...args) => f(g(h(...args)))
// 等同于
compose(f, g, h)(...args)
// compose使用例子
function foo (str) {
return str + '_foo'
}
function bar (str) {
return str + '_bar'
}
function baz (str) {
return str + '_baz'
}
compose(baz, bar, foo)('base') // "base_foo_bar_baz" |
compose
方法就是把上一个函数执行的结果作为下一个函数执行的参数,执行顺序从后往前,传入参数的最后一个函数先被执行。
applyMiddleware
middleware就是一个中间件的概念,简化如下:
数据经过每个中间件的处理,会对数据,或者保留一些数据的痕迹,例如写入日志等
applyMiddleware
的用法也是类似:
1
|
const store = createStore(rootReducer, {}, applyMiddleware(middleware1, middleware2)) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// applyMiddleware 源码
export default function applyMiddleware(...middlewares) {
// createStore方法作为参数传入
// 相当于延迟一步初始化 store
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {/* ... */}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 把传入的 middleware 先执行了一遍
// 把 getState 与 dispatch 方法传入
// 让 middleware 能够获取到当前 store 的 state与有触发新的 dispatch 能力
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 这个时候的 dispatch 不是原有的 createStore 函数中的方法
// 而是一个经过 middleware 集成的新方法
// 而原有的 dispatch 方法作为参数,传入到不同的middleware
dispatch = compose(...chain)(store.dispatch)
return {
...store,
// 使用当前的 dispatch 覆盖 createStore 的 dispatch 方法
dispatch
}
}
} |
redux-thunk
是一个对于了解middleware
很好的例子,下面参照redux-thunk
弄一个自定义的middleware, 源码如下:
1 2 3 4 5 6 7 8 9 10 11 |
function customeMiddleware({ dispatch, getState }) {
return next => {
return action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
}
} |
为什么会函数嵌套那么多层呢?其实每一层都是有原因的;第一层:
1 2 3 |
function customeMiddleware ({dispatch, getState}) {
// ...
} |
dispatch
是能够触发一个完整流程更改state的方法,getState
方法用于获取整个reducer的state
数据;这两个方法都是给到middleware
需要获取完整state
的方法。从上面applyMiddleware
的方法可以知道,applyMiddleware
执行的时候,就先把middleware
函数都执行了一遍,返回chains
数组:
1 2 |
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch) |
看到这里,会有一个疑问compose
执行的顺序是从后面往前执行,但是我们定义middleware
是从前往后的。
chains
数组的方法相当于middleware
的next
方法(接收参数为next
函数,暂时这样子命名),compose执行的时候,相当于next
已经执行,并且返回一个新的函数,这个函数是接收一个叫action
的函数(暂命名为action函数);因为每个middleware
的接收next
函数执行后都是action
函数;next
函数的next
参数就是上一个函数的返回值。执行到最后,dispatch = compose(...)(store.dispatch)
,dispatch
函数其实是第一个middleware
的action
函数
1 2 3 4 5 |
// chain0 表示 chain 数组中的第一个函数,chain1表示第二个,以此类推
// compose 执行顺序为倒序
const action2 = chain2(store.dispatch) // store.dispatch的值是compose()(store.dispatch)传入的
const action1 = chain1(action2)
const action0 = chain0(action1) |
action0
就是最终返回到dispatch
函数;当我们在组件中执行dispatch()
的时候,实际上是调用action0
函数,action0
函数可以通过next
调用下一个middleware
1 2 3 4 5 6 7 8 9 |
// action0
action0(action)
const result = next(action) // 这个 next 的函数为 action1
// action1
action1(action)
const result = next(action) // 这个 next 的函数为action2
// action2
action2(action)
const result = next(action) // 这个 next 的函数为 store.dispatch |
就这样子层层嵌套,把每个middleware
都执行完,最终去到store.dispatch
,最终更改好reducer,返回一个全新的state;而这个state也层层冒泡传到最顶层的middleware
;middleware
执行顺序的疑问由此解开。
小结
redux
的源码不多,使用起来也很简单,但是里面运用的知识不少,特别是在middleware
的时候,需要很细心的看且有较好的基础,不然看起来还是有点吃力的。另外一些用闭包来缓存变量、保存函数执行状态等,用得很精妙。Get.~