redux数据流分析
在阅读redux源码之前,先整理一下redux的数据流,官网的数据流程图如下所示。该图十分清晰明了的展示了redux的数据流:
- 点击UI,发起一个存钱的点击事件。
- 在点击事件处理函数中,dispatch分发一个action到reducer中。
- reducer接收当前的state和dispatch发起的action,经过计算得到一个新的state。
- state状态被重新更新到UI上。
这样就形成了一个完整的数据闭环。当然还有一些小的细节,例如:很多小的reducer会被包含在一个root reducer中;reducer存在于store中;state更新UI的方式是通过subscribe里的监听函数等。
虽然图像很简单,但是绘制这样的图需要较深入的流程理解,值的好好看一下。参考链接: https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow
redux文件结构分析
在此基础上,在github redux 4.x 分支上,可以看到redux的相关代码。main分支代码已被全部更新为ts,介于我对ts掌握程度不够,所以阅读的是4.x分支的代码,该分支代码仍使用的js编写方式。
源码结构如下图所示,主要文件只有六个:
applyMiddleware.js - 用于中间件应用,也是生成createStore.js文件中enhancer的方法
bindActionCreators.js - 将dispatch和action绑定
combineReducers.js - 将reducer进行combine
compose.js - 封装了reduce函数,该文件用于applyMiddleware.js中逻辑的书写
createStore.js - store相关的一些主要逻辑
index.js - 导出文件
createStore.js代码结构分析
这里我重要阅读和梳理一个createStore.js的源码,剩余的代码下次再分析。之所以先梳理createStore.js,是因为它是相对比较重要的一个文件,文件内的一些函数的定义刚好可以对应上面展示的数据流程图。
首先还是从宏观上观察一下createStore.js内的结构,如下图所示:
ensureCanMutateNextListeners() - 将currentListeners数组浅复制给nextListeners
getState() - 获取当前的state
subscribe(listener) - 订阅函数,用于接收监听函数
dispatch(action) - 分发action
replaceReducer(nextReducer) - 更换reducer
observable() - 非相关业务代码,不重要,想研究可以根据官方注释跳转到该链接研究https://github.com/tc39/proposal-observable
createStore.js代码内容分析
宏观上对文件结构和代码结构进行分析之后,有个整体的概念,再深入阅读代码,会使代码阅读的逻辑更为清晰。
下面我通过在源码中添加中文注释的方式对代码进行解释和分析。另外源代码中的英文注释也写的很清楚,对于英文注释说明的内容我就不额外翻译了。
import $$observable from './utils/symbol-observable'
import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'
import { kindOf } from './utils/kindOf'
/**
* @deprecated
*
* **We recommend using the `configureStore` method
* of the `@reduxjs/toolkit` package**, which replaces `createStore`.
*
* Redux Toolkit is our recommended approach for writing Redux logic today,
* including store setup, reducers, data fetching, and more.
*
* **For more details, please read this Redux docs page:**
* **https://redux.js.org/introduction/why-rtk-is-redux-today**
*
* `configureStore` from Redux Toolkit is an improved version of `createStore` that
* simplifies setup and helps avoid common bugs.
*
* You should not be using the `redux` core package by itself today, except for learning purposes.
* The `createStore` method from the core `redux` package will not be removed, but we encourage
* all users to migrate to using Redux Toolkit for all Redux code.
*
* If you want to use `createStore` without this visual deprecation warning, use
* the `legacy_createStore` import instead:
*
* `import { legacy_createStore as createStore} from 'redux'`
*
*/
// 开头的文档注释反复强调了不推荐直接使用createStore,推荐使用@reduxjs/toolkit包
// 如果非要用createStore,也建议使用import { legacy_createStore as createStore} from 'redux'的方式
export function createStore(reducer, preloadedState, enhancer) {
// 只能传入一个enhancer函数
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function. See https://redux.js.org/tutorials/fundamentals/part-4-store#creating-a-store-with-enhancers for an example.'
)
}
// 只传reducer和一个函数的情况下,该函数是enhancer
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
// 传入的enhancer只能是函数
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error(
`Expected the enhancer to be a function. Instead, received: '${kindOf(
enhancer
)}'`
)
}
return enhancer(createStore)(reducer, preloadedState)
}
// reducer必须是函数
if (typeof reducer !== 'function') {
throw new Error(
`Expected the root reducer to be a function. Instead, received: '${kindOf(
reducer
)}'`
)
}
// 变量顾名思义都很容易理解
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners // 思考点:为什么定义两个监听数组?(后文和注释中有解释)
let isDispatching = false
/**
* This makes a shallow copy of currentListeners so we can use
* nextListeners as a temporary list while dispatching.
*
* This prevents any bugs around consumers calling
* subscribe/unsubscribe in the middle of a dispatch.
*/
// 这段代码很好理解,就是一个简单的复制过程
// 思考点:为什么要nextListeners === currentListeners判断后才赋值?(后文有解释)
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
/**
* Reads the state tree managed by the store.
*
* @returns {any} The current state tree of your application.
*/
// 注意这里是直接把currentState暴露了
function getState() {
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}
return currentState
}
/**
* Adds a change listener. It will be called any time an action is dispatched,
* and some part of the state tree may potentially have changed. You may then
* call `getState()` to read the current state tree inside the callback.
*
* You may call `dispatch()` from a change listener, with the following
* caveats:
*
* 1. The subscriptions are snapshotted just before every `dispatch()` call.
* If you subscribe or unsubscribe while the listeners are being invoked, this
* will not have any effect on the `dispatch()` that is currently in progress.
* However, the next `dispatch()` call, whether nested or not, will use a more
* recent snapshot of the subscription list.
*
* 2. The listener should not expect to see all state changes, as the state
* might have been updated multiple times during a nested `dispatch()` before
* the listener is called. It is, however, guaranteed that all subscribers
* registered before the `dispatch()` started will be called with the latest
* state by the time it exits.
*
* @param {Function} listener A callback to be invoked on every dispatch.
* @returns {Function} A function to remove this change listener.
*/
// 上面那段英文注释真的写的特别的好,建议仔细阅读,它告诉我们在dispatch时,我们应用的是当前的subscribe快照
// 如果调用dispatch期间改变了listener数组,其实是不会影响当前使用的快照的,而是应用到下一次dispatch中,因为下一次dispatch之前会产生一个新的快照,这个新快照就会包含改变的数组
// 这其实也是为什么应用两个listener数组的原因,即保持当前dispatch进行中时遍历的listener数组的不变性
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error(
`Expected the listener to be a function. Instead, received: '${kindOf(
listener
)}'`
)
}
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api/store#subscribelistener for more details.'
)
}
let isSubscribed = true
// 增加监听函数
ensureCanMutateNextListeners()
nextListeners.push(listener)
// 返回解除监听函数
return function unsubscribe() {
// 防止多次解除订阅
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://redux.js.org/api/store#subscribelistener for more details.'
)
}
isSubscribed = false
// 解除当前监听的函数
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
// 思考点:这里数组为什么置null?(后文有解释)
currentListeners = null
}
}
/**
* Dispatches an action. It is the only way to trigger a state change.
*
* The `reducer` function, used to create the store, will be called with the
* current state tree and the given `action`. Its return value will
* be considered the **next** state of the tree, and the change listeners
* will be notified.
*
* The base implementation only supports plain object actions. If you want to
* dispatch a Promise, an Observable, a thunk, or something else, you need to
* wrap your store creating function into the corresponding middleware. For
* example, see the documentation for the `redux-thunk` package. Even the
* middleware will eventually dispatch plain object actions using this method.
*
* @param {Object} action A plain object representing “what changed”. It is
* a good idea to keep actions serializable so you can record and replay user
* sessions, or use the time travelling `redux-devtools`. An action must have
* a `type` property which may not be `undefined`. It is a good idea to use
* string constants for action types.
*
* @returns {Object} For convenience, the same action object you dispatched.
*
* Note that, if you use a custom middleware, it may wrap `dispatch()` to
* return something else (for example, a Promise you can await).
*/
function dispatch(action) {
// 限制了action的类型
if (!isPlainObject(action)) {
throw new Error(
`Actions must be plain objects. Instead, the actual type was: '${kindOf(
action
)}'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.`
)
}
// action必须要有type
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.'
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
// 触发reducer
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// subscribe时我们操作的都是nextListeners,因此要先把最新的nextListeners赋值给currentListeners,然后再遍历,这就是获取最新快照的过程
// 由于我们遍历的是currentListeners,而subscribe时操作的是nextListeners,就这保证了dispatch时当前遍历数组的不变性,例如在遍历过程中如果又触发了额外的subscribe操作或者unsubscribe操作,也只会改变nextListeners,而这种改变不会影响当前的监听函数调用过程,只会影响下次dispatch时监听函数调用过程
// 使用for循环遍历性能会远高于forEach
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
/**
* Replaces the reducer currently used by the store to calculate the state.
*
* You might need this if your app implements code splitting and you want to
* load some of the reducers dynamically. You might also need this if you
* implement a hot reloading mechanism for Redux.
*
* @param {Function} nextReducer The reducer for the store to use instead.
* @returns {void}
*/
// 简单易懂的代码没什么好说的
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error(
`Expected the nextReducer to be a function. Instead, received: '${kindOf(
nextReducer
)}`
)
}
currentReducer = nextReducer
// This action has a similiar effect to ActionTypes.INIT.
// Any reducers that existed in both the new and old rootReducer
// will receive the previous state. This effectively populates
// the new state tree with any relevant data from the old one.
dispatch({ type: ActionTypes.REPLACE })
}
/**
* Interoperability point for observable/reactive libraries.
* @returns {observable} A minimal observable of state changes.
* For more information, see the observable proposal:
* https://github.com/tc39/proposal-observable
*/
// 非主业务代码,感兴趣的去看注释里的github链接,这里不深入研究
function observable() {
const outerSubscribe = subscribe
return {
/**
* The minimal observable subscription method.
* @param {Object} observer Any object that can be used as an observer.
* The observer object should have a `next` method.
* @returns {subscription} An object with an `unsubscribe` method that can
* be used to unsubscribe the observable from the store, and prevent further
* emission of values from the observable.
*/
subscribe(observer) {
if (typeof observer !== 'object' || observer === null) {
throw new TypeError(
`Expected the observer to be an object. Instead, received: '${kindOf(
observer
)}'`
)
}
function observeState() {
if (observer.next) {
observer.next(getState())
}
}
observeState()
const unsubscribe = outerSubscribe(observeState)
return { unsubscribe }
},
[$$observable]() {
return this
},
}
}
// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
// 初始化一个state树,否则的话拿到的是一个undefine
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable,
}
}
/**
* Creates a Redux store that holds the state tree.
*
* **We recommend using `configureStore` from the
* `@reduxjs/toolkit` package**, which replaces `createStore`:
* **https://redux.js.org/introduction/why-rtk-is-redux-today**
*
* The only way to change the data in the store is to call `dispatch()` on it.
*
* There should only be a single store in your app. To specify how different
* parts of the state tree respond to actions, you may combine several reducers
* into a single reducer function by using `combineReducers`.
*
* @param {Function} reducer A function that returns the next state tree, given
* the current state tree and the action to handle.
*
* @param {any} [preloadedState] The initial state. You may optionally specify it
* to hydrate the state from the server in universal apps, or to restore a
* previously serialized user session.
* If you use `combineReducers` to produce the root reducer function, this must be
* an object with the same shape as `combineReducers` keys.
*
* @param {Function} [enhancer] The store enhancer. You may optionally specify it
* to enhance the store with third-party capabilities such as middleware,
* time travel, persistence, etc. The only store enhancer that ships with Redux
* is `applyMiddleware()`.
*
* @returns {Store} A Redux store that lets you read the state, dispatch actions
* and subscribe to changes.
*/
// 这里的export呼应了文档开头推荐使用import { legacy_createStore as createStore} from 'redux'引入模块
export const legacy_createStore = createStore
在注释里我对代码进行了详细清晰的讲解,如有错误或者不理解的部分可以联系我一起探讨。
下面是几个我对代码的思考:
- gitState拿到的是什么?能否直接修改state?
- 为什么用到了两个listener数组?
- 为什么ensureCanMutateNextListeners函数中需要先进行nextListeners === currentListeners判断,然后才赋值nextListeners = currentListeners.slice()?
- 为什么dispatch触发reducer时不允许getState(),不允许subscribe(listener),不允许unsubscribe(),不允许dispatch(action)?
- 为什么unsubscribe时,currentListeners数组置null?
以下是我对上面几个问题思考的一些结果:
- getState可以直接拿到currentState,因此理论上来讲其实可以直接修改当前的state,但是如果直接修改了该值,就没有后续操作了。而redux之所以希望我们使用dispatch去修改state,是因为使用dispatch修改state之后,会依次调用订阅的listener函数,这样就能实时响应识别state的改变。
- 对于该问题我在注释中也进行了较详细的阐述。结合subscribe()和dispatch()注释中我的分析,总的来说就是保证在dispatch时,只会触发当前dispatch之前我们订阅的listener,而在dispatch过程中改变的监听数组会应用在下一次的dispatch中,保证当前监听数组的不变性,得到预期的结果。
- 该问题和上一个问题是相关的,因为ensureCanMutateNextListeners只在subscribe函数和unsubscribe函数中使用了,之所以要先判断相等才赋值是因为如果dispatch遍历currentListeners过程中又额外触发了subscribe或者unsubscribe操作,那么此时如果直接赋值就会导致更新的nextListeners又恢复到和原来的currentListeners一样了。举例说明:例如在dispatch过程中,遍历currentListeners时,又额外触发了两次subscribe函数,那么第一次进入subscribe时,在往nextListeners里push listener之前,调用ensureCanMutateNextListener,此时nextListeners和currentListeners是一样的,此时判不判断相等都不影响。而当我们把新的listener push到nextListeners之后,此时nextListeners就和currentListeners不同了,相当于比currentListeners多了一个listener。那么在这次subscribe结束后,第二次进入subscribe时,依旧是在往nextListeners里push listener之前,调用ensureCanMutateNextListeners函数时,此时两个数组不相等,如果在这种情况下进行赋值,就会将nextListeners恢复为第一次进入subscribe之前的状态。导致的结果就是,我们预期获得多两个listener的nextListeners数组,而没有了相等才赋值的判断之后,我们获得了只增加了最后一个listener的nextListeners数组,丢了一个想添加的listener。而unsubscribe甚至有currentListeners=null的操作,如果多次unsubscribe还强行赋值,nextListeners也跟着变成null了。
- reducer会造成currentState的改变,而getState()、subscribe(listener)、unsubscribe()和dispatch(action)会涉及到对状态的改变或者识别,我认为强行操作会导致结果无法预期。
- 这里之所以会有这个问题,是因为在unsubscribe的时候,redux会删除nextListeners数组中想要解除订阅的listener,而在dispatch时,会有种这样一个赋值操作currentListeners = nextListeners,全程currentListeners的改变都只来源于nextListeners的赋值,那么为什么还要多此一举在unsubscribe时将currentListeners=null呢?这里涉及到js的gc问题,由redux代码可知,currentListeners = nextListeners,即currentListeners永远来自nextListeners,而只有在调用subscribe或者unsubscribe,且满足nextListeners === currentListeners时,才有nextListeners = currentListeners.slice()。unsubscribe时执行currentListeners=null这会导致一种情况,就是当我们调用一个unsubscribe的后,就会产生nextListeners !== currentListeners的情况,而这种情况下,将绝对不会再用到currentListeners的值!无论之后我们是订阅、解除订阅还是执行dispatch(action),都不会再用到currentListeners的值,那么这种情况下对于currentListeners指向的那些存在堆里的值就完全没必要存储了,及时置null相当于手动让其进行垃圾回收。