这个星期看了一下 redux 的文档, 都说 redux 源码不多就对着文档看了一下源码。下面大部分的定义和例子都是官网和中文网上的。有疑问的可以直接去看一下官方文档或者例子。
Motivation 动机
随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。 如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等等。
这里的复杂性很大程度上来自于:我们总是将两个难以理清的概念混淆在一起:变化和异步。 跟随 Flux、CQRS 和 Event Sourcing 的脚步,通过限制更新发生的时间和方式,Redux 试图让 state 的变化变得可预测。这些限制条件反映在 Redux 的三大原则中。
三大原则
单一数据源
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
State 是只读的
唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
使用纯函数来执行修改
为了描述 action 如何改变 state tree ,你需要编写 reducers。
api 使用
action
action 是一个普通对象, 一般用于 store.dispatch(action) , reducer 函数也会接受 action 对象, 根据 action 的参数进行 state 的处理。 action 中一般会有一个 type 属性, 用来描述 dispatch 操作类型 , 其他属性一般用于 reducer 中更新state的参数。
reducer
reducer 是用来更新 state 的函数, redux 提倡使用纯函数更新 state 。
(prevState,action)=>newState reducer 接收一个初始state或者是之前状态的state, 接收到 action 时进行状态更新。一般情况下, 每个 reducer 只负责更新整个
state 中的部分字段,例如官方 todos 的示例中 state 整个状态为
{
todos:[{id,text,completed}],
visibilityFilter
}
其中含有两个 reducer, reducer todos 用来更新state中的 todos 字段的状态 , reducer visibilityFilter 更新 visibilityFilter 。
将 reducer 指定单一功能只更新指定的字段, 那么在书写方面就会带来一些冗余, 例如我更新的是 todos , 那么在 reducer 中我返回的 state 是
Object.assign({},prevState,{todos: newTodos}) 每次都需要返回一个合并的完整的 state 对象。
redux 中针对上面的情况提供了 combineReducers 的方法,用来合并所有reducer返回的字段成为一个完整的 state 对象, 当然复杂 state 中也可以使用
该方法来返回每个数据结构的对象,最后在合并成一个总的 state, 用法如下:
const todos = (state=[],action)=>newTodos;
const visibilityFilter = (state='SHOW_ALL',action)=>newFilter;
let state = combineReducers({
todos:todos,
visibilityFilter:visibilityFilter
})
从上面的用法中可以看出, 每个 reducer 接收的不再是一个完整的 state 了, 而是自己负责更新的字段的值。
在复杂的应用中, 我们对于 state 状态的拆分也需要慎重考虑。 redux 官方说可以将 state 看成一个数据库, 对于复杂的结构我们可以使用 id 类似的索引进行拆分, 但是对应的要考虑每个结构的状态更新。
Store
Store 就是用来维持应用所有的 state 树 的一个对象。创建 Store 的方法就是使用 createStore 方法。
Store 的职责:
- 维持应用的 state;
- 提供 getState() 方法获取 state;
- 提供 dispatch(action) 方法更新 state;
- 通过 subscribe(listener) 注册监听器;
- 通过 subscribe(listener) 返回的函数注销监听器。
createStore(reducer,[preloadedState],enhancer)
- reducer (Function): 接收两个参数,分别是当前的 state 树和要处理的 action,返回新的 state 树。
- [preloadedState] (any): 初始时的 state。 在同构应用中,你可以决定是否把服务端传来的 state 水合(hydrate)后传给它,或者从之前保存的用户会话中恢复一个传给它。如果你使用 combineReducers 创建 reducer,它必须是一个普通对象,与传入的 keys 保持同样的结构。否则,你可以自由传入任何 reducer 可理解的内容。
- enhancer (Function): Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。这与 middleware 相似,它也允许你通过复合函数改变 store 接口。
返回值 (Store): 保存了应用所有 state 的对象。
middleware
middleware 具体的演进过程可以参考(地址)[www.redux.org.cn/docs/advanc…]。 正常情况下 action 都是同步的, 对于异步的 action 就需要借助于 middleware 来实现。下面就来了解一下 middleware 以及 applyMiddleware 的实现。
官网中举了一个 logger 的如何演变成中间件, 其实可以发现中间件主要是对 dispatch 中的操作进行了扩展, 以前是直接 dispatch(action) , 现在只不过封装了一个函数里面除了 dispatch(action) 的操作, 还打印了 action 以及改变后的 state。
下面是官网中实现的中间件的列子。
/**
* 记录所有被发起的 action 以及产生的新的 state。
*/
const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd(action.type)
return result
}
/**
* 在 state 更新完成和 listener 被通知之后发送崩溃报告。
*/
const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
Raven.captureException(err, {
extra: {
action,
state: store.getState()
}
})
throw err
}
}
/**
* 用 { meta: { delay: N } } 来让 action 延迟 N 毫秒。
* 在这个案例中,让 `dispatch` 返回一个取消 timeout 的函数。
*/
const timeoutScheduler = store => next => action => {
if (!action.meta || !action.meta.delay) {
return next(action)
}
let timeoutId = setTimeout(
() => next(action),
action.meta.delay
)
return function cancel() {
clearTimeout(timeoutId)
}
}
/**
* 通过 { meta: { raf: true } } 让 action 在一个 rAF 循环帧中被发起。
* 在这个案例中,让 `dispatch` 返回一个从队列中移除该 action 的函数。
*/
const rafScheduler = store => next => {
let queuedActions = []
let frame = null
function loop() {
frame = null
try {
if (queuedActions.length) {
next(queuedActions.shift())
}
} finally {
maybeRaf()
}
}
function maybeRaf() {
if (queuedActions.length && !frame) {
frame = requestAnimationFrame(loop)
}
}
return action => {
if (!action.meta || !action.meta.raf) {
return next(action)
}
queuedActions.push(action)
maybeRaf()
return function cancel() {
queuedActions = queuedActions.filter(a => a !== action)
}
}
}
/**
* 使你除了 action 之外还可以发起 promise。
* 如果这个 promise 被 resolved,他的结果将被作为 action 发起。
* 这个 promise 会被 `dispatch` 返回,因此调用者可以处理 rejection。
*/
const vanillaPromise = store => next => action => {
if (typeof action.then !== 'function') {
return next(action)
}
return Promise.resolve(action).then(store.dispatch)
}
/**
* 让你可以发起带有一个 { promise } 属性的特殊 action。
*
* 这个 middleware 会在开始时发起一个 action,并在这个 `promise` resolve 时发起另一个成功(或失败)的 action。
*
* 为了方便起见,`dispatch` 会返回这个 promise 让调用者可以等待。
*/
const readyStatePromise = store => next => action => {
if (!action.promise) {
return next(action)
}
function makeAction(ready, data) {
let newAction = Object.assign({}, action, { ready }, data)
delete newAction.promise
return newAction
}
next(makeAction(false))
return action.promise.then(
result => next(makeAction(true, { result })),
error => next(makeAction(true, { error }))
)
}
/**
* 让你可以发起一个函数来替代 action。
* 这个函数接收 `dispatch` 和 `getState` 作为参数。
*
* 对于(根据 `getState()` 的情况)提前退出,或者异步控制流( `dispatch()` 一些其他东西)来说,这非常有用。
*
* `dispatch` 会返回被发起函数的返回值。
*/
const thunk = store => next => action =>
typeof action === 'function' ?
action(store.dispatch, store.getState) :
next(action)
// 你可以使用以上全部的 middleware!(当然,这不意味着你必须全都使用。)
let todoApp = combineReducers(reducers)
let store = createStore(
todoApp,
applyMiddleware(
rafScheduler,
timeoutScheduler,
thunk,
vanillaPromise,
readyStatePromise,
logger,
crashReporter
)
)
从上面的中间件的格式我们不难发现中间件接收的参数是 store , 返回一个函数, 返回的函数中接收一个 next 参数, next 其实就是经过封装之后的 dispatch 函数, dispatch 函数接收 action。 整个中间件的逻辑就是这样。 下面来看看 applyMiddleware 中的代码
// compose.js
/**
* Composes single-argument functions from right to left. The rightmost
* function can take multiple arguments as it provides the signature for
* the resulting composite function.
*
* @param {...Function} funcs The functions to compose.
* @returns {Function} A function obtained by composing the argument functions
* from right to left. For example, compose(f, g, h) is identical to doing
* (...args) => f(g(h(...args))).
*/
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)))
}
//applyMiddleware.js
import compose from './compose';
/**
* Creates a store enhancer that applies middleware to the dispatch method
* of the Redux store. This is handy for a variety of tasks, such as expressing
* asynchronous actions in a concise manner, or logging every action payload.
*
* See `redux-thunk` package as an example of the Redux middleware.
*
* Because middleware is potentially asynchronous, this should be the first
* store enhancer in the composition chain.
*
* Note that each middleware will be given the `dispatch` and `getState` functions
* as named arguments.
*
* @param {...Function} middlewares The middleware chain to be applied.
* @returns {Function} A store enhancer applying the middleware.
*/
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
我们先来看下 compose 函数做了些什么, 其中核心代码就是 funcs.reduce((a, b) => (...args) => a(b(...args))),
reduce函数中 a 为前面累计的函数, b 为当前函数 , 返回格式为 a(b(...args)) , 先执行当前函数,在执行a中的累计函数, 其实就是 funcs 中最后一个函数执行,函数的返回作为参数传递给前一个函数。 不理解看下面的例子:
let fn1 = name=>{
console.log('name',name)
}
let fn2 = (age,name)=>{
console.log('age',age)
return name
}
let composed = compose(fn1,fn2)// (...args)=>fn1(fn2(...args))
composed(18,'ming') //age 18 , name ming
了解了 compose 之后, 再来看看 applyMiddleware 内部实现, 首先该方法返回的是一个函数接收 createStore 方法, 该函数中创建了一个 store 对象, 返回 store 中的属性以及经过封装之后的 dispatch 。
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
middlewareAPI 中的 dispatch 被禁止使用, 使用即报错, 能使用的方法只有 getState。
const chain = middlewares.map(middleware => middleware(middlewareAPI))
所有 middleware 都被执行传入了 middlewareAPI 即中间件接收的 store , 现在 chain 中的函数应该是 next=>action=>{...; return next(action)}
dispatch = compose(...chain)(store.dispatch)
在此处 compose 改变了 middleware 的执行顺序并传入了redux初始的 dispatch 方法, 每个 middleware 最后都返回封装后的 dispatch 函数, 获取到最终的 dispatch 函数在最后将其返回。
最后还需要注意的一点就是==中间件的顺序==,因为中间件是从后往前执行的, 有可能某个中间件会依赖于另一个中间件, 那么被依赖的就需要放在后面。
createStore
该方法是用来创建 store 对象的, 该函数接收三个参数: reducer, preloadedState, enhancer 其中 reducer 为必要参数其余可选, preloadedState 是初始状态, enhancer 是 applyMiddlware 返回的函数。 对于 preloadedState 还特地标注了如果 reducer 是 combineReducer 返回的函数, 其数据结构需要和 reducer 中的保持一致。 createStore 具体构造如下:
import ?observable from 'symbol-observable'
import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'
/**
* Creates a Redux store that holds the state tree.
* 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 default function createStore(reducer, preloadedState, 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.'
)
}
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
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.
*/
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.
*/
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.
*/
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
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-reference/store#subscribe(listener) 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-reference/store#subscribe(listener) for more details.'
)
}
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
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) {
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
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.')
}
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
*/
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.')
}
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.
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer,
[?observable]: observable
}
}
对于参数的判断我们就忽略,在文章上面的 applyMiddleware 的内部中也使用到了该方法, 我们先来看看其在有 applyMiddleware 时是怎么处理的。
//常规使用方法
const store = createStore(rootReducers,applyMiddleware(['thunk','logger']))
在 createStore 的构造中对应代码如下
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
applyMiddleware 返回的函数格式为 createStore=>(...args)=>{...}, 可以看到此时为 enhancer 注入了 craeteStore 方法, ...args 也就对应了 reducer 和 preloadedState 。
此时 createStore 进入到了 applyMiddleware 中, 真正的 store 对象就是 applyMiddleware 中返回的 {..store,dispatch}。
下面我们来看看 store.dispatch 是什么样的。
/**
*
* @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) {
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
从上面的构造函数中我们可以看到 dispatch 中的 action 只能是一个普通对象, action.type 也是必须的,不能并行执行 dispatch, 还有就是 dispatch 操作时同步的。 state 是完全由 reducer 控制返回, 那么 combineReducer 返回的reducer函数怎么检测 preloadedState 结构一致,待会可以再找找。
dispatch 最终返回的是 action , 这也是为什么中间件中返回的都是 next(action) 后面的中间件也能拿到 action 。
唯一需要注意的就是 listeners, 在此处我们只知道是里面的元素都是函数,可以推测应该是监听某个 action 被dispatch 时所要执行的函数。我们继续看看 listener 相关的代码:
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.
*/
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
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-reference/store#subscribe(listener) 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-reference/store#subscribe(listener) for more details.'
)
}
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}
createStore 里定义了几个函数级作用域的变量, 其中就包含了 currentListeners 和 nextListeners 。 还定义了 ensureCanMutateNextListeners 函数用来浅拷贝 currentListeners 并赋值给 nextListeners, 注释中说是防止在 dispatching 状态下防止直接使用 currentListeners 出现bug, 想了一下可能因为 listener 没有要求为纯函数会造成其他影响吧。
再来看看 subscribe 的代码, 其检测 listener 必须为函数, 且在 dispatching 状态时也禁止订阅, 这就与上面 ensureCanMutateNextListeners 的函数作用类似了, 应该是开发遇到过在 dispatching 改变 listeners 发生过bug。 subscribe 函数并没对 action.type 进行监听, listeners 在 dispatch 执行时就会执行, 有需要的要自己在 listener 中判断。 subscribe 最终返回的是 unsubscribe 函数, unsubscribe 执行将当前 listener 从 nextListeners 队列中删除。
createStore 中还有一个 [?observable] 方法, 对应的就是 observable 函数。 observable 返回的是一个对象, 该对象包含 subscribe 方法, 该 subscribe 方法是基于 createStore.subscribe 的拓展。 subscribe 接收一个对象,且该对象中需要有一个 next 方法, 还用了 symbol-observable 的三方库, 具体实现就不深究了。 observable 是对 github.com/tc39/propos… 的实现, 该函数函数作用就是监听 state 的变化。
最后还有一个 bindActionCreator 函数, 该函数的作用中文文档中是这样解释的:
把一个 value 为不同 action creator 的对象,转成拥有同名 key 的对象。同时使用 dispatch 对每个 action creator 进行包装,以便可以直接调用它们。 字都认识, 就是不懂什么意思。没关系,我们看看官方的例子。
//actions
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}
export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
}
}
//component
import { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as TodoActionCreators from './TodoActionCreators'
console.log(TodoActionCreators)
// {
// addTodo: Function,
// removeTodo: Function
// }
class TodoListContainer extends Component {
constructor(props) {
super(props)
const { dispatch } = props
// 这是一个很好的 bindActionCreators 的使用示例:
// 你想让你的子组件完全不感知 Redux 的存在。
// 我们在这里对 action creator 绑定 dispatch 方法,
// 以便稍后将其传给子组件。
this.boundActionCreators = bindActionCreators(TodoActionCreators, dispatch)
console.log(this.boundActionCreators)
// {
// addTodo: Function,
// removeTodo: Function
// }
}
componentDidMount() {
let { dispatch } = this.props
let action = TodoActionCreators.addTodo('Use Redux')
dispatch(action)
}
render() {
// 由 react-redux 注入的 todos:
let { todos } = this.props
return <TodoList todos={todos} {...this.boundActionCreators} />
// 另一替代 bindActionCreators 的做法是
// 直接把 dispatch 函数当作 prop 传递给子组件,但这时你的子组件需要
// 引入 action creator 并且感知它们
// return <TodoList todos={todos} dispatch={dispatch} />;
}
}
export default connect(state => ({ todos: state.todos }))(TodoListContainer)
看了之后还是没明白 bindActionCreators 的意图,后来再看看网上的相关解释, 原来 bindActionCreators 只是为了少些一个 dispatch(action), 回头来再看看代码
function bindActionCreator(actionCreator, dispatch) {
return function() {
return dispatch(actionCreator.apply(this, arguments))
}
}
bindActionCreators 对 actionCreators 中的每个函数都调用了 bindActionCreator 方法 , 最终返回的对象就是下面的格式。
{
creator1:()=>dispatch(creator1.apply(this,arguments)),
creator2:()=>dispatch(creator2.apply(this,arguments))
}
后面我们 dispatch 直接调用 bindActionCreators 返回对象中的方法就行。前后对比如下:
//前
let action1 = creator1(msg);
let action2 = creator2(msg);
dispatch(action1);
dispatch(action2);
//后
import creators from 'actions';
let wrapCreators = bindActionCreators(creators);
wrapCreators.creator1(msg);
wrapCreators.creator2(msg);
combineReducers
我们先来解析一下 combineReducers 函数的功能, 其接收一个对象, 对象的key就是 state 的key, value 则是处理对应key的 reducer, 使用该方法的 reducer 接收的 state 不再是完整的, 而是所负责的字段的值。返回的是一个合并了所有 reducer 的函数。返回的结构跟文档中的 reducers 部分的 todoApp 是类似的。
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
下面我们来看看源码
import ActionTypes from './utils/actionTypes'
import warning from './utils/warning'
import isPlainObject from './utils/isPlainObject'
function getUndefinedStateErrorMessage(key, action) {
const actionType = action && action.type
const actionDescription =
(actionType && `action "${String(actionType)}"`) || 'an action'
return (
`Given ${actionDescription}, reducer "${key}" returned undefined. ` +
`To ignore an action, you must explicitly return the previous state. ` +
`If you want this reducer to hold no value, you can return null instead of undefined.`
)
}
function getUnexpectedStateShapeWarningMessage(
inputState,
reducers,
action,
unexpectedKeyCache
) {
const reducerKeys = Object.keys(reducers)
const argumentName =
action && action.type === ActionTypes.INIT
? 'preloadedState argument passed to createStore'
: 'previous state received by the reducer'
if (reducerKeys.length === 0) {
return (
'Store does not have a valid reducer. Make sure the argument passed ' +
'to combineReducers is an object whose values are reducers.'
)
}
if (!isPlainObject(inputState)) {
return (
`The ${argumentName} has unexpected type of "` +
{}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
`". Expected argument to be an object with the following ` +
`keys: "${reducerKeys.join('", "')}"`
)
}
const unexpectedKeys = Object.keys(inputState).filter(
key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
)
unexpectedKeys.forEach(key => {
unexpectedKeyCache[key] = true
})
if (action && action.type === ActionTypes.REPLACE) return
if (unexpectedKeys.length > 0) {
return (
`Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
`"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
`Expected to find one of the known reducer keys instead: ` +
`"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
)
}
}
function assertReducerShape(reducers) {
Object.keys(reducers).forEach(key => {
const reducer = reducers[key]
const initialState = reducer(undefined, { type: ActionTypes.INIT })
if (typeof initialState === 'undefined') {
throw new Error(
`Reducer "${key}" returned undefined during initialization. ` +
`If the state passed to the reducer is undefined, you must ` +
`explicitly return the initial state. The initial state may ` +
`not be undefined. If you don't want to set a value for this reducer, ` +
`you can use null instead of undefined.`
)
}
if (
typeof reducer(undefined, {
type: ActionTypes.PROBE_UNKNOWN_ACTION()
}) === 'undefined'
) {
throw new Error(
`Reducer "${key}" returned undefined when probed with a random type. ` +
`Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
`namespace. They are considered private. Instead, you must return the ` +
`current state for any unknown actions, unless it is undefined, ` +
`in which case you must return the initial state, regardless of the ` +
`action type. The initial state may not be undefined, but can be null.`
)
}
})
}
/**
* Turns an object whose values are different reducer functions, into a single
* reducer function. It will call every child reducer, and gather their results
* into a single state object, whose keys correspond to the keys of the passed
* reducer functions.
*
* @param {Object} reducers An object whose values correspond to different
* reducer functions that need to be combined into one. One handy way to obtain
* it is to use ES6 `import * as reducers` syntax. The reducers may never return
* undefined for any action. Instead, they should return their initial state
* if the state passed to them was undefined, and the current state for any
* unrecognized action.
*
* @returns {Function} A reducer function that invokes every reducer inside the
* passed object, and builds a state object with the same shape.
*/
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
// This is used to make sure we don't warn about the same
// keys multiple times.
let unexpectedKeyCache
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
let shapeAssertionError
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
return function combination(state = {}, action) {
if (shapeAssertionError) {
throw shapeAssertionError
}
if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
let hasChanged = false
const nextState = {}
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)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
官方文档中有提到过 reducer 中必须对未知的 action 返回旧的 state , 而对应的 state 不能为 undefined 。 如何检测你的 reducer 是不是返回了 undefined , 很简单直接执行你的 reducer 就行。 传入 state = undefined , action.type 也是你无法识别的, 查看你的返回结果。 下面来看看对应的代码。
//检测代码
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
//检测函数
function assertReducerShape(reducers) {
Object.keys(reducers).forEach(key => {
const reducer = reducers[key]
const initialState = reducer(undefined, { type: ActionTypes.INIT })
if (typeof initialState === 'undefined') {
throw new Error(
`Reducer "${key}" returned undefined during initialization. ` +
`If the state passed to the reducer is undefined, you must ` +
`explicitly return the initial state. The initial state may ` +
`not be undefined. If you don't want to set a value for this reducer, ` +
`you can use null instead of undefined.`
)
}
if (
typeof reducer(undefined, {
type: ActionTypes.PROBE_UNKNOWN_ACTION()
}) === 'undefined'
) {
throw new Error(
`Reducer "${key}" returned undefined when probed with a random type. ` +
`Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
`namespace. They are considered private. Instead, you must return the ` +
`current state for any unknown actions, unless it is undefined, ` +
`in which case you must return the initial state, regardless of the ` +
`action type. The initial state may not be undefined, but can be null.`
)
}
})
}
接下来看看返回合并reducer的代码
return function combination(state = {}, action) {
if (shapeAssertionError) {
throw shapeAssertionError
}
if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
let hasChanged = false
const nextState = {}
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)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
首先 shapeAssertionError 就是检测你的 reducer 是否规范的错误, 核心代码就是 for 循环里面的。
获取 state 的 key 以及对应的 reducer , reducer(previousStateForKey, action) 将旧 state 中的值传入, 使用 nextState 存储新的 state 。
上面有一个比较巧妙的代码 hasChanged = hasChanged || nextStateForKey !== previousStateForKey, 常规情况下循环里面判断是否存在一次符合条件时的经常会写成 if(condition){hasChanged = true} , 相对于上面的写法来说不够巧妙, 上面的写法只要 hasChanged 被赋值成 true 之后,后面的条件比较就不需要再次执行, 而普通写法每次都要对 condition 条件进行判断。
后记
对于 applyMiddleware 中还有点疑问, 在网上看了一下中间件中 dispatch 和 next 的区别, dispatch 会从头走一遍所有的 middleware , next 直接将 action 传递给下个中间件。 对于 _dispatch 报错的函数还是不知道什么用处。