先祭上本文的思维导图:
一、为什么讲 Redux
在项目中用 Redux 的时候,有时候就觉得会用,但是不明白为什么这样用。导致在 debug 的时候,无法快速的 debug 出原因。而且 Redux 的源码也不复杂,暴露出来的只有 5 个 API,可以作为很好的阅读源码的开端,所以在这里很开心可以和大家一起来探索 Redux。如果有些讲的不准确的地方,欢迎大家提出来;也特别希望大家积极的讨论,迸发出更多想法。
二、Redux 为什么会出现
要了解 Redux,就要从 Flux 说起。可以认为 Redux 是 Flux 思想的一种实现。那 Redux 是为什么被提出来呢?就要提一下 MVC 了。
1、MVC
说到 Flux,我们就不得不要提一下 MVC 框架。
MVC 框架将应用分为 3 个部分:
- View:视图,展示用户界面
- Controller:管理应用的行为和数据,响应用户的输入(经常来自 View)和更新状态的指令(经常来自 Model)
- Model:管理数据,大部分业务逻辑也在 Model 中
用户请求先到达 Controller,然后 Controller 调用 Model 获得数据,再把数据交给 View。这个想法是很理想的想法。在实际的框架应用中,大部分都是允许 View 和 Model 直接通信的。当项目变的越来越大的时候,这种不同模块之间的依赖关系就变得“不可预测”了,所以就变成了下面这样子。
虽然这张图有夸大的嫌疑,但是也说明了 MVC 在大型项目下,容易造成数据混乱的问题。
所以,Flux 诞生了。在写这篇文章之前,我查阅很多资料,有些说 Flux 思想替代了 MVC 框架,我则不这么认为。个人觉得,Flux 思想更严格的控制了 MVC 的数据流走向。下面咱们来看看 Flux 是如何严格控制数据流的。
2、Flux
一个 Flux 应用包含四个部分:
- Dispatcher,处理动作分发,维持 Store 之间的依赖关系
- Store,负责存储数据和处理数据相关逻辑
- Action,触发 Dispatcher
- View,视图,负责显示用户界面
通过上图可以看出来,Flux 的特点就是单向数据流:
- 用户在 View 层发起一个 Action 对象给 Dispatcher
- Dispatcher 接收到 Action 并要求 Store 做相应的更新
- Store 做出相对应更新,然后发出一个 change 事件
- View 接收到 change 事件后,更新页面
所以在 Flux 体系下,如果想要驱动界面,只能派发一个 Store,别无他法。在这种规矩下,如果想要追溯一个应用的逻辑就变得很轻松了。而且这种思想解决了 MVC 中无法杜绝 View 和 Model 之间的直接对话的问题。
这里就不具体讲关于 Flux 的例子了,如果想要更了解 Flux ,可以看一下阮一峰老师的 Flux 架构入门教程。
4、Redux 诞生
Redux 是 Flux 的一种实现,意思就是除了“单向数据流”之外,Redux 还强调三个基本原则:
- 唯一的 store(Single Source of Truth)
- 保持状态只读(State is read-only)
- 数据改变只能通过纯函数完成(Changes are made with pure functions)
a. 唯一的 store
在 Flux 中,应用可以拥有多个 Store,但是分成多个 Store 容易造成数据冗余,数据一致性不太好处理,而且 Store 之间可能还会有依赖,增加了应用的复杂度。所以 Redux 对这个问题的解决方法就是:整个应用只有一个 Store。
b. 保持状态只读
就是不能直接修改状态。如果想要修改状态,只能通过派发一个 Action 对象来完成。
c. 数据改变只能通过纯函数完成
这里说的纯函数就是 Reducer。按照 redux 作者 Dan 的说法:Redux = Reducer + Flux。
三、在 React 中应用 Redux
下面咱们根据例子来了解一下 Reudx 在 React 中的应用。
1、Redux 中的数据流动
创建一个 Redux 应用需要下面几部分:
- Actions
- Reducers
- Store
他们分别是什么意思呢?下面我们来举一个例子:
比如下面是商场某品牌鞋子的展示柜:
店长来视察,发现鞋子2
放的太高了,而且这款鞋还是店里的主推款,放在这个位置不适合宣传,就让店员把鞋子 2 往下挪两排
,放下去之后,店长看着舒服多了。
其实通过上面的例子,我们现在就很好解释 Redux 了:
- View: 鞋子摆放在鞋架上的整体效果
- Action: 店长给店员分配的任务(往下挪鞋子)
- Reducers: 具体任务的实施者(把鞋子往下挪两排)
- Store: 鞋子在鞋架上的具体位置
所以整个过程可以是下面这样:
Store 决定了 View,然后用户的交互产生了 Action,Reducer 根据接收到的 Action 执行任务,从而改变 Store 中的 state,最后展示到 View 上。那么,Reducer 如何接收到动作(Action)信号的呢?伴随着这个问题,咱们来看一个例子。
2、Redux 实践
了解了 Redux 中各个部分代表的意思,下面咱们来通过一个计数器的例子进一步了解一下 Redux 的原理(具体代码可以看 GitHub)。我们想要的最终效果如下:
根据上面的思路,可以分别把 Action 和 Reducer 定义为:
- 动作(Action): 加
- 执行者(Reducer): 加 1
那么我们来创建 Action 和 Reducer 这两个文件:
Actions
|
|
首先我们创建一个 ActionTypes.js 和 Actions.js 这两个文件。ActionType 代表的就是 Action 的类型,可以看到它是一个常量。在 Actions.js 中,我们定义了两个 Action 构造函数,他们返回的都是一个简单对象 (plain object),而且每个对象必须包含 type 属性。
可以看出来 Action 明确表达了我们想要做的事情(加和减)。
可能有些同学会问,在 Action 中,有时候也会 return 一个 function,不是简单对象。其实这个时候,是中间件拦截了 Action,如果是 function,就执行中间件中的方法。但是咱们这次不讲中间件,所以就先忽略这种情况。
Reducer
|
|
可以看到 Reducer 是一个纯函数。它接收两个参数 state 和 Action,根据接收到的 state 和 Action 来判断自己需要对当前的 state 做哪些操作,并且返回新的 state。
在 Reducer 中我们给了 state 一个默认的值,这就是我们的初始 state。关于 redux 是如何返回初始值的,继续往下看。
Action 和 Reducer 都有了,那怎么让他们两个联系起来呢?下面咱们看一下 redux 中的精华部分 - Store。
createStore
首先我们先创建 Store:
在 store.js 中,我们把 reducer 传给 createStore 方法并且执行了它,来创建 Store。这个方法是 Redux 的精髓所在。
下面看一下 createStore 的源码部分:
createStore 接收三个参数:
- reducer{Function}
- state{any}(可选参数)
- enhancer{Function}(可选参数)
返回一个对象,这个对象包含五个方法,咱们目前先只关注前三个方法:
- dispatch
- subscribe
- getState
在整个 createStore 中,只执行了 dispatch({ type: ActionTypes.INIT })
这一句代码。那 dispatch 做了什么呢?
|
|
我省略了一些代码,这是 dispatch 方法的核心代码。它接收一个 action 对象,并且把 createStore 接收到的 state 参数和通过 dispatch 方法传进来的 Action 参数,传给了 Reducer 并且执行,然后把 reducer 返回的 state 赋值给 currentState。最后执行订阅队列中的方法。
createStore 方法一上来就执行了 dispatch({ type: ActionTypes.INIT })
。这句话的意思咱们现在也清楚了,它的主要目的就是初始化 state。
现在咱们已经把 Action 和 Reducer 联系起来了。可以看到,在 createStore 方法中,它维护一个变量 currentState,通过 dispatch 方法来更新 currentState 变量。外部如果想要获取 currentState,只需要调用 createStore 暴露出来的 getState 方法即可:
|
|
getState 方法是获取当前的 currentState 变量,如果想要实时获取 state,那就需要注册监听事件,每次 dispatch 的时候,就都会执行一遍这个事件。
|
|
现在咱们来梳理一下思路:
- Action:此次动作的目的
- Reducer:根据接收到的 Action 命令来做具体的操作
- Store:把 Action 传给 Reducer,并且更新 state。然后执行订阅队列中的方法。
Redux 和 React 是两个独立的产品,但是如果两个结合使用,就不得不提 react-redux 这个库了,可以大大的简化代码的书写,但是咱们先不讲这个库,来自己实现一下。
2、store 和 context 结合
大家都知道,在 React 中我们都是使用 props 来传递数据的。整个 React 应用就是一个组件树,一层一层的往下传递数据。
但是如果在一个多层嵌套的组件结构中,只有最里层的组件才需要使用这个数据,导致中间的组件都需要帮忙传递这个数据,我们就要写很多次 props,这样就很麻烦。
好在 React 提供了一个叫做 context 的功能,可以很好的解决和这个问题。
所谓 context 就是“上下文环境”,让一个树状组件上所有组件都能访问一个共同的对象,为了完成这个任务,需要上下级组件的配合。
首先是上级组件宣称自己支持 context,并且提供给一个函数来返回代表 context 的对象。
然后,子组件只要宣称自己需要这个 context,就可以通过 this.context 来访问这个共同的对象。
所以我们可以利用 React 的 context,把 Store 挂在它上面,就可以实现全局共享 Store 了。
了解了如何在 React 中共享 Store,那咱们就动手来实现一下吧~
Provider
Provider,顾名思义,它是提供者,在这个例子中,它是 context 的提供者。
就像下面这样来使用:
|
|
Provider 提供了一个函数 getChildContext,这个函数返回的是就是代表 context 的对象。在调用 Store 的时候可以从 context 中获取:this.context.store。
Provider 为了声明自己是 context 的提供者,还需要指定 Provider 的 childContextTypes 属性(需要和 getChildContext 对其)。
只有具备上面两个特点,Provider 才有可能访问到 context。
好了,Provider 组件咱们已经完成了,下面咱们就可以把 context 挂到整个应用的顶层组件上了。
进入整个应用的入口文件 index.js:
|
|
我们把 Store 作为 props 传递给了 Provider 组件,Provider 组件把 Store 挂在了 context 上。所以下面我们就要从 context 中来获取 Store 了。
消费者
下面是我们整个计数器应用的骨架部分。
我们先把页面渲染出来:
|
|
在上面的组件中,我们做了两件事情:
- 第一件事情是:声称自己需要 context
- 第二件事情是:初始化 state。
如何声称自己需要 context 呢?
- 首先是需要给 App 组件的 contextType 赋值,值的类型和 Provider 中提供的 context 的类型一样。
- 然后在构造函数中加上 context,这样组件的其他部分就可以通过 this.context 来调用 context 了。
- 然后是初始化 state。看代码可以知道,我们调用了挂在 context 上的 Store 的 getState 方法。
上面我们了解过,getState 方法返回的就是 createStore 方法中维护的那个变量。在 createStore 执行的时候,就已经初始化过了这个变量。
接下来我们给“加号”加上具体动作。
我们想要把数字加一,所以就有一个“加”的动作,这个动作就是一个 Action,这个 Action 就是 addAction。如果想要触发这个动作,就需要执行 dispatch 方法。
|
|
通过 dispatch 方法,把 Action 对象传给了 Reducer,经过处理,Reducer 会返回一个加 1 的新 state。
其实现在 Store 中的数据已经是最新的了,可以我们看到页面上还没有更新。那我们如何能获取到最新的 state 呢?
订阅
就像关注公众号一样,我只需要在最开始的时候订阅一下,之后每次有更新,我都会收到推送。
这个时候就要使用 Store 的 subscribe 方法了。顾名思义,就是我要订阅 state 的变化。我们先看一下代码怎么写:
|
|
在组件的 componentDidMount 生命周期中,我们调用了 store 的 subscribe 方法,每次 state 更新的时候,都会去调用 onChange 方法;在 onChange 方法中,我们会取得最新的 state,并且赋值。在组件被卸载的时候,我们取消订阅。
上面这样就完成了订阅功能。这时候再运行程序,可以发现页面上就会显示最新的数字了。
react-redux
在这个例子中,可以看出来我们可以抽象出来很多逻辑,比如 Provider,还有订阅 store 变化的功能。其实这些 react-redux 都已经帮我们做好了。
- Provider: 提供包含 store 的 context
- connect: 把 state 转化为内层组件的 props,监听 state 的变化,组件性能优化
在咱们这个例子中,只是简单的实现了一下 react-redux 部分功能。具体的大家可以到官网上去看。
总结
下面咱们来总结一下 redux 和 react 结合使用的整个数据流:
good~ 我们已经全部完成了整个应用。现在大家了解 Redux 的运行原理 了吗?
具体代码可以到 GitHub 查看。
参考资料: