Redux源码阅读笔记(2) - Redux的原理与适配

Posted by Lxxyx on 2017-05-22

范式

与其说Redux是一个工具类库,我更想说它是一套处理状态与数据变更的范式。官方有明确的一些规则,社区也累积了很多最佳实践。

从谷歌搜索来看,这是个很有趣的现象。
至于个人看法则是根据项目和团队实际情况来选用Redux方案,而非强制上马,避免未来可能会背的技术债。

Redux Flow

Redux是单向数据流,其工作流如下图所示(看不懂或者没用过的小伙伴可以先看看Redux文档,用一用):

在Redux中,Store中state的改变只能由action触发,经过reducer,从而改变state,因为在Redux中,是不允许直接改变state的。至于这一点,下一篇博客会谈到为什么。

这样的特性也就决定了,我们需要编写大量的Action和Reducer代码,从而给人一种很繁琐的感觉。从而带来一些不必要的开销与麻烦。因此是不怎么适合小项目的。

取舍

至于这一点,社区则有一篇文章讲述了这个:You Might Not Need Redux

而Redux与Flux的作者也表达过:

我想修正一个观点:当你在使用 React 遇到问题时,才使用 Redux。
你应当清楚何时需要 Flux。如果你不确定是否需要它,那么其实你并不需要它。

在Redux的文档中,则有:

在打算使用 Redux 的时候进行权衡是非常重要的。它从设计之初就不是为了编写最短、最快的代码,他是为了解决 “当有确定的状态发生改变时,数据从哪里来” 这种可预测行为的问题的。它要求你在应用程序中遵循特定的约定:应用的状态需要存储为纯数据的格式、用普通的对象描述状态的改变、用不可更新的纯函数式方式来处理状态变化。这也成了抱怨是“样板代码”的来源。这些约束需要开发人员一起来努力维护,但也打开了一扇扇可能的大门(比如:数据持久性、同步)。

原理

至于Redux实现的原理,就得看它的源代码了,就像上一篇博客所说的,Redux的源代码简洁且有力。
今天则从Redux最重要的方法createStore()说起,来看看Redux的内部实现。

下面是简化后的代码,提取了Redux的createStore()的主干部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function createStore (reducer, preloadedState, enhancer) {

let currentReducer = reducer
// Store state
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false

function getState () {}
function ensureCanMutateNextListeners () {}
function subscribe (listener) {}
function dispatch (action) {}
function replaceReducer () {}

return {
dispatch,
subscribe,
getState,
replaceReducer
}
}

下面则针对各个问题,来解读这份源代码。

1. 为什么不能直接修改state,怎么做到的

createStore函数中,初始化state为preloadedState,如果preloadedState不存在则为undefined。
而我们要拿到store的state,只能通过store的getState函数做到。函数内容如下:

1
2
3
4
5
6
7
8
/**
* Reads the state tree managed by the store.
*
* @returns {any} The current state tree of your application.
*/
function getState () {
return currentState
}

至于不可直接更改的原因则是currentState作为私有变量,只能被内部访问,没有暴露在公共接口中。
这也算是闭包的一种应用吧。

这儿也解决了一个我的小疑惑,就是频繁调用getState()开销如何?
现在看来,也只是和直接读取 state 的开销差不多,微乎其微。

2. 怎么通过subscribe来Redux state的更新

这点的话,当时看了看源代码,便了然了。
Redux内部实现了个订阅者模式,subscribe则是添加订阅。
而在dispatch函数中,底部有这么一段:

1
2
3
4
5
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}

3. state是怎么更新的

这个问题则涉及到了 dispatch函数,在 dispatch 函数内部有这么一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function dispatch(action) {
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时,currentReducer计算并返回新的state,完成这次更新。

令我感到注目的是,这儿用了 try..catch..finally这种方式,但是并没有catch部分,于是自己实践了一下:

1
2
3
4
5
try {
throw new Error('Error')
} finally {
console.log('Finally')
}

对此控制台的结果如下:

也就是说在reducer计算新的state时,错误会抛出,但是函数却能继续运行,不至于直接崩溃。
感觉这个方法对于某些特定场景还是很有用的。

4. ensureCanMutateNextListeners是什么

createStore的源代码中,有一个函数是 ensureCanMutateNextListeners,内容是:

1
2
3
4
5
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}

很多函数在执行时都调用了它,开始还不是很理解为什么,然后找了找别人的解释:

同时有一个辅助方法ensureCanMutateNextListeners()。这是考虑到,在执行某个监听函数的时候,可能会添加新的监听函数,或者取消某个监听函数。为了让这些改变不影响当前的监听函数列表的执行,因此在改变之前,先拷贝一份副本(即nextListeners),然后对该副本进行操作,从而所有的改变会在下一次dispatch(action)的时候生效。 – Redux入门

Redux的适配

Redux从来就不想只做React的工具,他的目标是和各大框架配合使用。而事实上各大框架也有Redux的适配库。
那么自己的问题就在于Redux怎么和别的工具去做一个适配。

一开始不理解,后面看了源代码,想了想这种观察者模式,与 createStore 返回的store对象所具有的内容,心里便了然了。

框架通过Redux的subscribe方法,订阅state更新,并通过dispatch方法,触发state更新。
那么具体框架如何应用数据,就是框架内部业务层面的事情了。每个框架根据自身特点和需求处理的方式都不一样。有兴趣的可以看看 react-reduxrevue