blog icon indicating copy to clipboard operation
blog copied to clipboard

React进阶系列之数据流

Open campcc opened this issue 4 years ago • 0 comments

数据流

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,对作者也是一种鼓励。

(完)

campcc avatar Aug 23 '21 01:08 campcc