React数据流

React的核心特征是“数据驱动视图”,即UI=render(data),也就是说React的视图会随着数据的变化而变化。那么,它到底如何灵活的处理数据呢?让我们揭开这层纱,透过它的四大通信方式,了解它

一、基于props的单向数据流

组件,从概念上类似于Javascript函数。它接受任意的入参(即"props"),并返回用于描述页面展示内容的React元素 —— React官方

换句话说:在React单向数据流的前提下,props是组件的入参,组件之间可通过入参来完成通信。

而所谓的单向数据流,就是当前组件的stateprops的形式流动时,只能流向组件树中比自己层级更低的组件。比如在父子组件中,只能由父组件向子组件传props,而不能反过来。

1、适用场景 —— 简单的父子组件、子父组件和兄弟组件之间的通信

基于props的单向数据流看似条条框框,却十分灵活,它可以轻松的实现父子组件、子父组件和兄弟组件之间的通信

  • 父->子的通信:父组件直接将this.props传入子组件
  • 子->父的通信:父组件传给子组件绑定自身上下文的函数,子组件调用该函数并将要交给父组件的数据以入参的形式传入
  • 兄弟组件之间共享同一个父组件,即兄弟1->兄弟2的通信可以转换为:兄弟1->父、父->兄弟2

基于props的单向数据流

结合上面的分析,我们手写个简单的demo1,验证父子组件、子父组件和兄弟组件之间的通信,可点击查看

2、不适用场景 —— 层层传递

对于简单的通信,基于props串联父子和兄弟组件是很灵活的。但是,处理复杂的嵌套数据流

层层传递

使用它,则弊大于利。它的弊端主要体现这两点:

  • 1、中间作为桥梁的组件会引入很多不属于自己的属性
  • 2、短时间内,给开发者带来庞大的工作量和代码量。拉长时间看,整个项目的维护成本也变得昂贵

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

“发布-订阅”模式,最经典的莫属于浏览器的DOM事件。它通过调用addEventListener方法创建一个事件监听器。一旦事件被触发,其对应的监听函数被执行(被“发布”出去)从而进行通信。

1、适用场景 —— 任意组件间的通信

前面我们提到的基于props数据流对于层层传递是要不得的,其主要问题出在“位置的限制”,而“发布-订阅”模式没有这个限制。

只要在同一个上下文里,监听事件的位置和触发事件的位置是不受限的,这个特性,恰好解决了跨层级通信的问题,适用任意组件间的通信。

2、实现一个“发布-订阅”模式

顾名思义,“发布-订阅”模式最重要的两个功能便是”发布“和”订阅“,对应的分别是事件的监听 和 事件的触发,再补充一个事件的销毁,便是一个简易的“发布-订阅”模式。它们其实都是函数,只是为了更好理解,将其具像化。下面从这三个函数切入,进一步了解它

a、 事件的监听(订阅)

on():负责注册事件的监听器,指定事件触发时的回调函数,即注册事件监听函数的过程,是一个“写”操作

2a5e74ceaf4de2dbundefined

b、 事件的触发(发布)

emit():负责触发事件,可通过传参使其在触发时携带数据,即触发安装在某个事件上的监听函数,是一个“读”操作

c、 事件的销毁(删除)

off():负责监听器的删除

结合上面的分析,我们在demo1的基础上做以下3处改动,以验证了“发布-订阅”模式驱动数据流的通信,,可点击查看

  • 在Child2组件上注册事件监听器to2Event,指定事件触发时的回调函数handle

  • 接着在Child1组件上触发to2Event事件,携带自身状态中需要让Child2感知的那部分数据params

  • 最后执行to2Event事件,Child2感知到Child1

3、不适用场景 —— 庞大复杂的项目

当组件之间关系复杂之后,就会形成一种网状的依赖结构,在这种结构下,暂且不说能不能理清它们之间的关系,光是可能出现的环形依赖所造成的死循环,就已经让开发者抓狂

三、redux

基于单向数据流的redux,组件之间的关系从“发布-订阅”模式的网状结构,调整为树状结构。在树状结构模型下,组件与组件之间只存在:父子和兄弟关系,且没有环形依赖,大大简化了关系复杂所产生的一系列问题。换句话说redux通过提供一个统一的状态容器,使得数据能够自由、有序的在组件间流动

1、适用场景 —— 庞大复杂的项目

在复杂的项目中,redux有着非同一般的优势,它通过让整个应用共享一个全局的Store,且强调每一次数据更新都保证State完全不可变,完全避免使用对象引用赋值的方式来更新状态,让整个应用具备可追溯,可回滚,可调试,能够快速来定位和解决问题

2、redux背后的架构思想 —— Flux架构

redux的设计在很大程度上受益于Flux架构,它是一套由脸书技术团队提出的应用架构,约束的是应用处理数据的模式,在Flux架构中,一个应用被拆分为4个部分:

  • View(视图层):可以是任何形式实现出来的用户界面,既可以是react,也可以是vue
  • Action(动作):View发出的“消息”,它会触发应用状态的改变
  • Dispatcher(派发器):负责对Action进行分发
  • Store(数据层):存储应用状态的“仓库”,它的变化会映射到View上去,同时在这里定义修改状态的逻辑

a、一个典型的Flux工作流

用户与View之间产生交互,通过View发起一个ActionDispatcherAction派发给Store,通知Store进行相应的状态更新,完成后,进一步通知View更新界面

afab12dc71943c24undefined

值得注意的是,箭头都是单向的,这也是Flux架构的核心特点:单向数据流,保证了状态的变化是可预测的。也就是说Store中的数据发生了变化,肯定是Dispatcher派发Action触发的。这也从根本避免了混乱的数据关系,使得流程变得清晰简单。

结合Flux架构的特性,再去品味官方给出的redux定义:

reduxJavascript状态容器,它提供可预测的状态管理

此时的你,想必理解可预测这三个字就更清晰了些

3、一个典型的redux工作流

reduxFlux在思想上确实是一脉相承,但实现层面并没有完全一致。比如Flux中允许多个Store存在,而redux中只有一个Store

a、redux三大关键要素

Flux架构中,一个应用主要由ViewActionDispatcherStore 四部分组成,而redux主要由StoreActionReducer三部分组成

  • Store:它是一个单一的数据源,而且是只读的
  • Action:它是对变化的描述
  • Reducer:是个函数,它负责对Action进行分发和处理,更新数据给到Store

b、redux工作流

redux的整个工作流中,数据流是严格单向的。假设你想修改数据,你只有一个途径:派发ActionAction会被Reducer读取,Reducer会根据Action内容的不同执行不同的计算逻辑,生成新的state,这个新的state将更新到Store对象里,进而驱动View作出相应的更新

80eda7526c3041f0undefined

4、从源码的视角,看redux如何工作

a、目录构成

这里略过编译等文件解析,直接进入src文件

b、引入redux

使用redux的第一步,即调用createStore函数,它接收三个入参:

  • reducer
  • 初始状态内容
  • 指定中间件
// 引入 redux
import { createStore } from 'redux'
// 创建 store
const store = createStore(
    reducer,
    initial_state,
    applyMiddleware(middleware1, middleware2, ...)
);

单纯从使用感来看,createStore函数似乎就只做一件事:创建一个store对象。可你不知道,其内部却涵盖了redux主流程中核心方法的定义,下面我们拎出createStore源码的主体逻辑,顺藤摸瓜揪出redux源码的主流程

c、createStore源码的主体逻辑

// 接收三个入参:reducer、初始状态内容和指定中间件
function createStore(reducer, preloadedState, enhancer) {
- 前置校验
  • 处理没有传入初始状态的情况
  • 若 enhancer 不为空,则用 enhancer 包装 createStore
    // 第一个参数和第二个参数都传 function的情况
    if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
        // 此时第二个参数会被认为是 enhancer(中间件)
        enhancer = preloadedState;
        preloadedState = undefined;
    }
    if (typeof enhancer !== 'undefined') {
        return enhancer(createStore)(reducer, preloadedState);
    }
- 定义内部变量
  • currentReducer:记录当前的 reducer,因为 replaceReducer 会修改 reducer 的内容
  • currentState:记录当前的 state
  • currentListeners:声明 listeners 数组,这个数组用于记录在 subscribe 中订阅的事件
  • nextListeners:currentListeners 的快照
  • isDispatching:记录当前是否正在进行 dispatch
    let currentReducer = reducer;
    let currentState = preloadedState;
    let currentListeners = [];
    let nextListeners = currentListeners;
    let isDispatching = false
- 定义 ensureCanMutateNextListeners 方法
  • 确保 nextListeners 与 currentListeners 不指向同一个引用,即nextListeners是currentListeners 的副本,而不是其本身
    function ensureCanMutateNextListeners() {
        if (nextListeners === currentListeners) {
            nextListeners = currentListeners.slice();
        }
    }
- 定义 getState 方法
  • 获取当前的状态
    function getState() {
        return currentState;
    }
- 定义 subscribe (订阅)方法
  • 用于注册 listeners (订阅监听函数)
  • 它将会定义 dispatch 最后执行的 listeners 数组的内容
    function subscribe(listener) {
- 前置校验
  • 校验 listener 的类型:必须为function
  • 校验当前是否正在进行 dispatch:禁止在 reducer 中调用 subscribe
        if (typeof listener !== 'function') {
          throw new Error('Expected the listener to be a function.')
        }
        if (isDispatching) {
          throw new Error(
            'You may not call store.subscribe() while the reducer is executing. ' +
              'If you would like to be notified after the store has been updated, subscribe from a ' +
              'component and invoke store.getState() in the callback to access the latest state. ' +
              'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
          )
        }
- 调用ensureCanMutateNextListeners函数
  • 确保 nextListeners 与 currentListeners 不指向同一个引用
        ensureCanMutateNextListeners(); 
- 注册监听函数
  • 将入参 listener 函数推入 nextListeners 数组中
        nextListeners.push(listener); 
- 返回取消订阅当前 listener的 unsubscribe 方法
  • 定义变量 isSubscribed :用于防止调用多次 unsubscribe 函数
  • 调用ensureCanMutateNextListeners函数
  • 将当前的 listener 从 nextListeners 数组中删除
        let isSubscribed = true; 
        return function unsubscribe() {
            if (!isSubscribed) {
                return;
            }
            isSubscribed = false;
            ensureCanMutateNextListeners();
            const index = nextListeners.indexOf(listener);
            nextListeners.splice(index, 1);
            currentListeners = null
        };
    }
- 定义 dispatch 方法
  • 用于派发 action、调用 reducer 并触发订阅
    function dispatch(action) {
- 前置校验
  • 校验 action 的数据格式是否合法
  • 校验 action 中是否有 type 属性:type 属性作为 action 的唯一标识
  • 校验当前是否正在进行 dispatch:禁止套娃
        if (!isPlainObject(action)) {
          throw new Error(
            'Actions must be plain objects. ' +
              'Use custom middleware for async actions.'
          )
        }
        if (typeof action.type === 'undefined') {
          throw new Error(
            'Actions may not have an undefined "type" property. ' +
              'Have you misspelled a constant?'
          )
        }
        if (isDispatching) {
          throw new Error('Reducers may not dispatch actions.')
        }
- “上锁” - 计算新的state - “解锁”
  • “上锁” :将 isDispatching 设置为 true ,标记当前已经存在 dispatch 执行流程
  • 调用 reducer,计算新的 state
  • “解锁” :将 isDispatching 设置为 false,允许再次进行 dispatch
        try {
          isDispatching = true
          currentState = currentReducer(currentState, action)
        } finally {
          isDispatching = false
        }
- 触发订阅
  • 定义实际执行的监听数组listeners,并赋值为 currentListeners
  • currentListeners 被赋值为 nextListeners,因此 最终被执行的 listeners 和当前的 nextListeners 指向同一个引用
  • 监听函数依次执行
        const listeners = (currentListeners = nextListeners);
        for (let i = 0; i < listeners.length; i++) {
            const listener = listeners[i];
            listener();
        }
        return action;
    }
- 定义 replaceReducer 方法
  • 用于替换 reducer
    function replaceReducer(nextReducer) {
        currentReducer = nextReducer;
        dispatch({ type: ActionTypes.REPLACE });
        return store;
    }
- 初始化 state
  • 当派发一个 type 为 ActionTypes.INIT 的 action ,每个 reducer 都会返回它的初始值
    dispatch({ type: ActionTypes.INIT });
- observable 方法
  • 它在 redux 内部使用,开发者一般不会直接接触
    function observable() {
      // observable 方法的实现
    }
- 返回store 对象
  • 将定义的方法包裹在 store 对象里返回
    return {
      dispatch,
      subscribe,
      getState,
      replaceReducer,
      [$$observable]: observable
    }
}

d、核心模块

通过分析了createStore的工作逻辑,不难看出与redux主流程强相关的两个方法分别是最为核心的dispatch动作(dispatch) 和 redux自身独特的“发布-订阅”模式(subscribe)

d-1、redux的“发布-订阅”模式:subscript

subscript并不是一个严格必要的方法,只有在需要监听状态变化时,才会去调用,那么它是如何与主流程结合?

subscript是如何与主流程结合
  • store对象创建完成后,可调用store.subscribe来注册监听,也可调用subscript的返回函数来解绑监听(监听函数是listener数组维护)
  • dispatch action 发生时,redux 会在 reducer 执行完后,逐个执行listener数组中的监听函数
subscript工作流程中的ensureCanMutateNextListeners调用

结合上面源码的解析,我们知道ensureCanMutateNextListeners函数是为了确保 nextListenerscurrentListeners 不指向同一个引用。让我们思考一下:为什么会有nextListenerscurrentListeners 两个 listener数组呢?

在思考这个问题的时候,我们先搞清楚redux在 订阅 和 发布 两个过程分别是如何处理listener数组的

- 订阅过程中的listener数组
  • createStore初始化变量阶段,nextListeners被赋值为currentListeners的快照,此时这两者指向同一个引用
let nextListeners = currentListeners;
  • subscript被调用时,ensureCanMutateNextListeners每次都会在listener注册前无条件执行,将其纠正为内容一致,引用不同的对象
function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
        nextListeners = currentListeners.slice();
    }
}
  • 紧接着listener会被注册到nextListeners数组中去
nextListeners.push(listener)
  • 返回取消订阅当前 listener的 unsubscribe 方法解绑监听
nextListeners.splice(index, 1);
- 发布过程中的listener数组

触发订阅这个动作是由dispatch来处理的

const listeners = (currentListeners = nextListeners);

嗯哼!注册监听操作的是nextListeners,取消监听操作的是nextListeners,触发订阅的也是读取nextListeners。既然如此,为何要currentListeners?转换下思维,假设没有currentListeners会怎样?

- currentListeners数组用于确保监听函数执行过程的稳定性

假设我们在redux进行如下操作

// 定义监听函数 A
function listenerA() {}

// 订阅 A,并获取 A 的解绑函数
const unSubscribeA = store.subscribe(listenerA)

// 定义监听函数 B
function listenerB() {
  // 在 B 中解绑 A
  unSubscribeA()
}

// 定义监听函数 C
function listenerC() {}

// 订阅 B
store.subscribe(listenerB)

// 订阅 C
store.subscribe(listenerC)

执行完成后,nextListeners数组的内容是三个listener:

[ listenerA, listenerB, listenerC ]

当触发订阅时,即执行

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

当索引i=1时,即执行listenerB时,执行了unSubscribeA这个动作

return function unsubscribe() {
    if (!isSubscribed) return;
    isSubscribed = false;
    ensureCanMutateNextListeners(); 
    const index = nextListeners.indexOf(listener);
    nextListeners.splice(index, 1);
};

假设不存在currentListeners,则不再执行ensureCanMutateNextListeners,直接执行nextListeners.splice(index, 1)
那么 listenerA 会同时在 listenersnextListeners 移除,因为两者指向同一个引用

[ listenerB, listenerC ]

for循环不会感知到这一点,继续执行i=2,但此时listeners[2]已经是undefined了,也就是原本在这个索引的listenerC被前置了,当undefined 代替 listenerC 执行时,便会引发函数异常

因此,需要一个不会被变更的、内容稳定的数组记录当前正在工作的listeners数组,并将它与可能发生变更的nextListeners 剥离开来,来确保监听函数执行过程的稳定性

d-2、redux工作流的核心:dispatch动作

结合上面的源码,我们不难看出dispatch的工作流刚好将actionreducerstore三者串联起来。在其“打配合”的过程中,有几个点值得我们关注

- 通过“上锁”避免“套娃式”的dispatch

更准确的说,是为了避免开发者在reducer中手动调用dispatchreducer的本质是store的更新规则,它指定了应用状态的变化如何响应action并发送到store

- 为何要“禁止套娃”?
  • 从设计的角度:作为一个“计算state专用函数”,它必须是“纯净”的,不应该执行除了计算之外的“脏操作”
  • 从执行的角度:若真的在reducer中手动调用dispatch,那么dispatch又会反过来调用reducerreducer又再次调用dispatch...这样反复相互调用,进入死循环,造成非常严重的误操作

3、redux 中间件

在这里不展开解析,下一篇进行详细探讨,待续...

4、redux 弊端

redux往往在复杂项目中才会体现出它的优势和必要性,那么与其对冲的弊端,有哪些呢?

  • 首当其中的就是它要求开发者要完全按照官方所描述的那样,写大量的actionreducer这种的样板代码,会让代码行数大量膨胀,使得开发一个小功能变得非常繁琐
  • 使用单一的state来管理数据,则要求就开发者自己去完成state的结构设计
  • 不可变数据状态管理仅仅是redux所强调的一种思想和要求而已,并没有提供有效避免对象引用赋值的解决方案,这就要求开发者时刻遵守这种模式,以免对不可变造成破坏
  • 归根结底,redux对数据流的约束背后是不可忽略的成本

四、React16 Context API 维护全局状态

React16下的Context API能够跨越层级,换句话说就是组件树内人人都可消费数据。其设计目的是为了共享那些对于一个组件树而言是“全局”的数据,类似主题、身份认证等

1、Context API 的三大关键要素

e416125ad3716b39undefined

a、React.creatContext:创建一个context对象

// 可选择性的传入defaultValue
const AppContext = React.creatContext([defaultValue])
// context对象包含了Provider和Consumer
const { Provider, Consumer } = AppContext

b、Provider:数据的提供者

在根组件进行包裹,value即是后续组件间流动的“数据”,可被Consumer消费

因为context会使用参考标识来决定何时进行渲染,可能会发生:当provider的父组件进行重渲染时,在consumers组件中触发意外的渲染,因为value属性总是被赋值为新的对象,为了防止这种情况,将 value 状态提升到父节点的 state 里

this.state = {
  value: {something: 'something'},
};
<Provider value={this.state.value}>
    <Title/>
</Provider>

c、Consumer:数据的消费者

不仅能读取到Provider下发的数据,还能读取到这些数据后续的更新。意味着数据在生产者和消费者之间能够及时同步。若没有对应的Provider,则直接读取creatContextdefaultValue

// 接收一个返回组件的函数作为子元素
<Consumer>
    {value => <div>{value.title}</div>}
</Consumer>

结合上面的分析,我们在demo2的基础上添加一些全局数据,可点击查看

2、适用场景

  • 组件树中有不同层级的组件需要访问同样的一批数据,即共享的数据
  • 管理当前的 locale,theme,或者一些缓存数据