阅读 3078

通过了解 Redux 简单源码,掌握 Redux 数据流原理

先祭上本文的思维导图:

一、为什么讲 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,然后用户的交互产生了 ActionReducer 根据接收到的 Action 执行任务,从而改变 Store 中的 state,最后展示到 View 上。那么,Reducer 如何接收到动作(Action)信号的呢?伴随着这个问题,咱们来看一个例子。

2、Redux 实践

了解了 Redux 中各个部分代表的意思,下面咱们来通过一个计数器的例子进一步了解一下 Redux 的原理(具体代码可以看 GitHub)。我们想要的最终效果如下:

根据上面的思路,可以分别把 Action 和 Reducer 定义为:

  • 动作(Action): 加
  • 执行者(Reducer): 加 1

那么我们来创建 Action 和 Reducer 这两个文件:

Actions

首先我们创建一个 ActionTypes.jsActions.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 的提供者,还需要指定 ProviderchildContextTypes 属性(需要和 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 查看。

参考资料:

本文永久链接

关注下面的标签,发现更多相似文章
评论