React进阶系列之数据流

2,672 阅读6分钟

最近在读修言的《深入浅出搞定 React》,笔者的文笔和文风都非常有趣,又不乏干货,重读几遍后仍收获满满,整理了下笔记分享给大家。本系列大概有 15 篇,如果觉得有帮助可以给个 star,如果发现问题请不吝在评论区指正。

数据流

React 的核心特征是 数据驱动视图,这个特征在业内有一个非常有名的函数式来表达:

UI = render(data)
UI = f(data)

React 的视图会随着数据的变化而变化,我们说的组件通信其实就是组件之间建立的数据上的连接,这背后是一套环环相扣的 React 数据流解决方案。

基于 props 的单向数据流

基于 props 传参,可以实现简单的父子,子父和兄弟组件通信,所谓单向数据流,指的就是当前组件的 state 以 props 的形式流动时,只能流向组件树中比自己层级更低的组件。React 中的单向数据流场景包括,

  • 基于 props 的父子通信:父组件的 state 作为子组件的 props
  • 基于 props 的子父通信:父组件传递一个绑定自身上下文的函数
  • 基于 props 的兄弟组件通信:以父组件未桥梁,转换为子父 + 父子通信

以上是 props 传参比较适合处理的三种场景,如果通信需求较为复杂,基于 props 的单向数据流可能并不适合,我们需要考虑其他更灵活的方案,比如通信类问题的 “万金油”:发布-订阅模式。

利用 “发布-订阅” 模式驱动数据流

发布-订阅模式的优点在于,只要组件在同一个上下文里,监听事件的位置和触发事件的位置是不受限的,所以原理上我们可以基于发布订阅模式实现任意组件的通信,下面是一个简单的 EventEmitter,

class EventEmitter {
  constructor() {
    this.eventMap = {};
  }

  on(type, handler) {
    if (!(handler instanceof Function)) {
      throw new Error('event handler expect to be a function');
    }
    if (!this.eventMap[type]) {
      this.eventMap[type] = [];
    }
    this.eventMap[type].push(handler);
  }

  emit(type, params) {
    if (this.eventMap[type]) {
      this.eventMap[type].forEach((handler, index) => {
        handler(params);
      });
    }
  }

  off(type, handler) {
    if (this.eventMap[type]) {
      this.eventMap[type].splice(this.eventMap[type].indexOf(handler) >>> 0, 1);
    }
  }
}

除了上述介绍的两种方式,我们还可以使用 React 原生提供的全局通信方式 Context API。

使用 Context API 维护全局状态

Context API 是 React 官方提供的一种组件树全局通信的方式。

Context 基于生产者-消费者模式,对应到 React 中有三个关键的要素:React.createContext、Provider、Consumer。通过调用 React.createContext,可以创建出一组 Provider。Provider 作为数据的提供方,可以将数据下发给自身组件树中任意层级的 Consumer,而 Cosumer 不仅能够读取到 Provider 下发的数据,还能读取到这些数据后续的更新,

const AppContext = React.createContext();
const [Provider, Consumer] = AppContext;

<Provider value={ content: this.state.content }>
  <Content />
</Provider>

<Consumer>
  {value => <div>{value.content}</div>}
</Consumer>

下面是 Context 工作流的简单图解,

context.png

但是在 V16.3 之前,由于存在种种局限性,Context 并不被 React 官方提倡使用,旧的 Context 存在哪些局限呢?

  • 代码不够优雅:生产者需要定义 childContextTypes 和 getChildContext,消费者需要定义 contextTypes 才能通过 this.context 访问生产者提供的数据,属性设置和 API 编写过于繁琐,很难辨别谁是 Provider,谁是 Consumer

  • 数据可能无法及时同步:这个问题在 React 官方中有过介绍,如果组件提供的一个 Context 发生了变化,而中间父组件的 shouldComponentUpdate 返回了 false,那么使用到该值的后代组件不会进行更新,这违背了模式中的 “Cosumer 不仅能够读取到 Provider 下发的数据,还能读取到这些数据后续的更新” 的定义,导致数据在生产者和消费者之间可能不能及时同步。

V16.3 后新的 Context API 改进了这一点,即使组件的 shouldComponentUpdate 返回 false,它仍然可以“穿透”组件继续向后代组件进行传播,再加上更优雅的语义化声明式写法,Context 成为一种确实可行的 React 组件通信解决方案。

理解了 Context API 的前世今生,接下来我们继续串联 React 组件间通信的解决方案。

三方数据流框架的“课代表”:Redux

简单的跨层级组件通信,可以使用发布订阅模式或者 Context API 搞定,随着应用的复杂度不断提升,需要维护的状态会越来越多,组件间关系也越来越复杂,这时我们可以考虑引入三方的数据流框架,比如 Redux。

Redux 是 JavaScript 状态容器,它提供可预测的状态管理。

简单解读一下这句话,首先 Redux 是为了 Javascript 应用而生的,也就是说它不是 React 的专利,任何框架或原生 Javascript 都可以用。我们知道状态其实就是数据,所谓状态容器,就是一个存放公共数据的仓库

要理解可预测的状态管理,我们得先知道 Redux 是什么以及它的工作流是什么样的。

Redux 主要由三个部分组成:store、reducer 和 action。

  • store 是一个只读的单一数据源
  • action 是一个描述状态变化的对象
  • reducer 是一个对变化进行分发和处理的纯函数

在 Redux 的整个工作过程中,数据流是严格单向的

image.png

下面我们从编码的角度来理解 Redux 工作流,

使用 createStore 创建 store 对象

import { createStore } from 'redux';
const store = createStore(reducer, initialState, applyMiddleware());

createStore 接受三个入参:reducer、初始状态和中间件。

reducer 的作用是将新的 state 返回给 store

reducer 就是一个接受旧的状态和变化,返回一个新的状态的纯函数,没有任何副作用,

const reducer = (state, action) => newState

当我们基于 reducer 去创建 store 的时候,其实就是给这个 store 指定了一套更新规则。

action 的作用是通知 reducer “让改变发生”

action 是一个包含自身唯一标识的对象,在浩如烟海的 store 状态库中,想要命中某个希望发生改变的 state,必须使用正确的 action 来驱动,

const action = { type: 'ACTION_TYPE', payload: {} }

dispatch 用来派发 action

action 本身只是一个对象,想要让 reducer 感知到 action,还需要派发 action 这个动作,这个动作是由 store.dispatch 完成的,

store.dispatch(action)

派发 action 后,对应的 reducer 会做出响应从而触发 store 中状态的更新

相关文章

写在最后

本文首发于我的 博客,才疏学浅,难免有错误,文章有误之处还望不吝指正!

如果有疑问或者发现错误,可以在评论区进行提问和勘误,

如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

(完)