• home > webfront > ECMAS > react >

    React16源码分析(0):对象池/合成事件/事务机制等概念科普

    Author:[email protected] Date:

    react入门中的复习,基础概念科普。讲解react的基本概念,对react框架个整体的了解。ReactElement ReactComponent ReactClass 对象池 事件分发 生命周期等,可以作为复习提纲用。

    ReactElement

    • 数据类,只包含 props refs key 等

    • 由 React.creatElement(ReactElement.js) 创建,React.createClass 中 render 中返回的实际也是个 ReactElement

    ReactComponent

    • 控制类,包含组件状态,操作方法等

    • 包括字符组件、原生 DOM 组件、自定义组件(和空组件)

    • 在挂载组件(mountComponent)的时候,会调用到 instantiateReactComponent 方法,利用工厂模式,通过不同的输入返回不同的 component

    • 代码(instantiateReactComponent.js)

    • ReactDOMTextComponent 只关心文本,ReactDOMComponent 会稍微简单一些,ReactCompositeComponent 需要关心的最多,包括得到原生 DOM 的渲染内容

    ReactClass

    这个比较特殊,对比 ES5 写法: var MyComponent = React.createClass({}),ES6写法:class MyComponent extends React.Component,为什么用createClass却得到了Component呢?通过源码来看,这两个 api 的实现几乎是一样的,也可以看到,ES6 的写法简洁的多,不用那些getInitialState等特定 api,React 在之后的版本也会抛弃createClass这个 api。并且,在此 api 中,React 进行了autobind。

    对象池

    首先你用JavaScript声明的变量不再使用时, js引擎会在某些时间回收它们, 这个回收时间是耗时的.

    Marking latency depends on the number of live objects that have to be marked, with marking of the whole heap potentially taking more than 100 ms for large webpages.

    尽管V8引擎对垃圾回收有优化, 但为了避免重复创建临时对象造成GC不断启动以及复用对象, React使用了对象池来复用对象, 对GC表明, 我一直在使用它们, 请不要启动回收.

    React 实现的对象池其实就是对类进行了包装, 给类添加一个实例队列, 用时取, 不用时再放回, 防止重复实例化:

    const DEFAULT_POOL_SIZE = 10;// 默认的池大小
    const DEFAULT_POOLER = null;  // 默认的对象池获取器,这里假设它是未定义的,实际中可能需要具体实现
    
    // 添加对象池功能到指定类
    var addPoolingTo = function (CopyConstructor, pooler) {
      var NewKlass = CopyConstructor; // 拿到类
      NewKlass.instancePool = []; // 添加实例队列属性
      NewKlass.getPooled = pooler || DEFAULT_POOLER;  // 添加拿到实例方法
      if (!NewKlass.poolSize) {// 实例队列默认为10个
        NewKlass.poolSize = DEFAULT_POOL_SIZE;
      }
      NewKlass.release = standardReleaser;// 将实例释放回对象池的标准方法
      return NewKlass;// 从对象池申请一个实例.对于不同参数数量的类,React分别处理, 这里是一个参数的类的申请实例的方
    };
    
    // 一个参数的类的对象池获取器
    var oneArgumentPooler = function(copyFieldsFrom) {
      var Klass = this;  // this 指的就是传进来的类
      if (Klass.instancePool.length) {  // 如果类的实例队列有实例, 则拿出来一个
        var instance = Klass.instancePool.pop();
        Klass.call(instance, copyFieldsFrom);
        return instance;
      } else {
        return new Klass(copyFieldsFrom); // 否则说明是第一次实例化, new 一个
      }
    };
    
    // 释放实例到类的实例队列中的标准方法
    var standardReleaser = function(instance) {
      var Klass = this;
      // 假设每个实例都有一个 destructor 方法用于清理
      if (instance.destructor) {
        instance.destructor(); // 调用类的解构函数
      }
      if (Klass.instancePool.length < Klass.poolSize) {
        Klass.instancePool.push(instance); // 放到队列
      }
    };
    
    // 示例:为 ReactReconcileTransaction 添加对象池功能
    // 假设 ReactReconcileTransaction 是一个构造函数
    // PooledClass 是管理对象池的工具类(这里未给出完整实现)
    PooledClass.addPoolingTo(ReactReconcileTransaction, oneArgumentPooler);
    
    // 注意:这里的 ReactReconcileTransaction 和 PooledClass 需要在外部定义或引入
    // 且 ReactReconcileTransaction 需要有合适的构造函数和可能的 destructor 方法

    可以看到, React对象池就是给类维护一个实例队列, 用到就pop一个, 不用就push回去. 在React源码中, 用完实例后要立即释放, 也就是申请和释放成对出现, 达到优化性能的目的.

    • 开辟空间是需要一定代价的

    • 如果引用释放而进入 gc,gc 会比较消耗性能和时间,如果内存抖动(大量的对象被创建又在短时间内马上被释放)而频繁 gc 则会影响用户体验

    • 既然创建和销毁对象是很耗时的,所以要尽可能减少创建和销毁对象的次数

    • 使用时候申请(getPooled)和释放(release)成对出现,使用一个对象后一定要释放还给池子(释放时候要对内部变量置空方便下次使用)

    • 代码(PooledClass.js):

      • 可以看到,如果短时间内生成了大量的对象占满了池子,后续的对象是不能复用只能新建的

      • 对比连接池、线程池:完成任务后并不销毁,而是可以复用去执行其他任务

    React合成事件系统

    React快速的原因之一就是React很少直接操作DOM,浏览器事件也是一样。原因是太多的浏览器事件会占用很大内存。

    React为此自己实现了一套合成系统,在DOM事件体系基础上做了很大改进,减少了内存消耗,简化了事件逻辑,最大化解决浏览器兼容问题。其基本原理就是:

    所有在JSX声明的事件都会被委托在顶层document节点上并根据事件名和组件名存储回调函数(listenerBank)。每次当某个组件触发事件时,在document节点上绑定的监听函数(dispatchEvent)就会找到这个组件和它的所有父组件(ancestors),对每个组件创建对应React合成事件(SyntheticEvent)并批处理(runEventQueueInBatch(events)),从而根据事件名和组件名调用(invokeGuardedCallback)回调函数。

    由于React合成事件系统模拟事件冒泡的方法是构建一个自己及父组件队列,因此也带来一个问题,合成事件不能阻止原生事件,原生事件可以阻止合成事件用 event.stopPropagation() 并不能停止事件传播,应该使用 event.preventDefault()

    React事件系统

    • ReactEventListener:负责事件注册和事件分发。React将DOM事件全都注册到document这个节点上,这个我们在事件注册小节详细讲。事件分发主要调用dispatchEvent进行,从事件触发组件开始,向父元素遍历。我们在事件执行小节详细讲。

    • ReactEventEmitter:负责每个组件上事件的执行。

    • EventPluginHub:负责事件的存储,合成事件以对象池的方式实现创建和销毁,大大提高了性能。

    • SimpleEventPlugin等plugin:根据不同的事件类型,构造不同的合成事件。如focus对应的React合成事件为SyntheticFocusEvent

    事件分发

    • 框图(ReactBrowserEventEmitter.js

      框图(ReactBrowserEventEmitter.js)

    • 组件上声明的事件最终绑定到了 document 上,而不是 React 组件对应的 DOM 节点,这样简化了 DOM 原生事件,减少了内存开销

    • 以队列的方式,从触发事件的组件向父组件回溯,调用相应 callback,也就是 React 自身实现了一套事件冒泡机制,虽然 React 对合成事件封装了stopPropagation,但是并不能阻止自己手动绑定的原生事件的冒泡,所以项目中要避免手动绑定原生事件

    • 使用对象池来管理合成事件对象的创建和销毁,好处在上文中有描述

    • ReactEventListener:负责事件注册和事件分发

    • ReactEventEmitter:负责事件执行

    • EventPluginHub:负责事件的存储,具体存储在listenerBank

    • Plugin: 根据不同的事件类型,构造不同的合成事件,可以连接原生事件和组件

    • 当事件触发时,会调用ReactEventListener.dispatchEvent,进行分发:找到具体的 ReactComponent,然后向上遍历父组件,实现冒泡

    • 代码较多,就不具体分析了,这种统一收集然后分发的思路,可以用在具体项目中

    事务机制

    • React 通过事务机制来完成一些特定操作,比如 merge state,update component

    • 示意图(Transaction.js):

      示意图(Transaction.js)

      • 可以看到和后端的事务是有差异的(有点类似AOP),虽然都叫transaction,并没有commit,而是自动执行,初始方法没有提供rollback,有二次封装提供的(ReactReconcileTransaction.js)

    用大白话说就是在实际的 useState/setState 前后各加了段逻辑给包了起来。只要是在同一个事务中的 setState 会进行合并(注意,useState不会进行state的合并)处理。

    为什么 setTimeout 不能进行事务操作

    由于 react 的事件委托机制,调用 onClick 执行的事件,是处于 react 的控制范围的。

    而 setTimeout 已经超出了 react 的控制范围,react 无法对 setTimeout 的代码前后加上事务逻辑(除非 react 重写 setTimeout)。

    所以当遇到 setTimeout/setInterval/Promise.then(fn)/fetch 回调/xhr 网络回调时,react 都是无法控制的

    相关react 源码如下:

    if (executionContext === NoContext) {
      // Flush the synchronous work now, unless we're already working or inside
      // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
      // scheduleCallbackForFiber to preserve the ability to schedule a callback
      // without immediately flushing it. We only do this for user-initiated
      // updates, to preserve historical behavior of legacy mode.
      flushSyncCallbackQueue()
    }

    executionContext 代表了目前 react 所处的阶段,而 NoContext 你可以理解为是 react 已经没活干了的状态。而 flushSyncCallbackQueue 里面就会去同步调用我们的 this.setState ,也就是说会同步更新我们的 state 。所以,我们知道了,当 executionContext 为 NoContext 的时候,我们的 setState 就是同步的

    1. 在正常的react的事件流里(如onClick等)

    2. setState和useState是异步执行的(不会立即更新state的结果)

    3. 多次执行setState和useState,只会调用一次重新渲染render

    4. 不同的是,setState会进行state的合并,而useState则不会

    5. 在setTimeout,Promise.then等异步事件中

    6. setState和useState是同步执行的(立即更新state的结果)

    7. 多次执行setState和useState,每一次的执行setState和useState,都会调用一次render


    生命周期

    • 整体流程:

      react生命周期整体流程图

    • 主要讲述mount和update,里面也有很多相类似的操作

    • componentWillMount,render,componentDidMount 都是在 mountComponent 中被调用

    • 分析 ReactCompositeComponent.js 中的mountComponent,发现输出是@return {?string} Rendered markup to be inserted into the DOM.

      • 主要讲述mount和update,里面也有很多相类似的操作

      • componentWillMount,render,componentDidMount 都是在 mountComponent 中被调用

    • 分析 ReactCompositeComponent.js 中的mountComponent,发现输出是@return {?string} Rendered markup to be inserted into the DOM.

      • 可以看到,mountComponet 先做实例对象的初始化(props, state 等),然后调用performInitialMount挂载(performInitialMountWithErrorHandling最终也会调用performInitialMount,只是多了错误处理),然后调用componentDidMount

      • transaction.getReactMountReady()会得到CallbackQueue,所以只是加入到队列中,后续执行

    • 我们来看performInitialMount(依然在 ReactCompositeComponent.js 中)

      • performInitialMount 中先调用componentWillMount,这个过程中 merge state,然后调用_renderValidatedComponent(最终会调用inst.render() )返回 ReactElement,然后调用_instantiateReactComponent 由 ReactElement 创建 ReactComponent,最后进行递归渲染。

      • 挂载之后,可以通过setState来更新(机制较为复杂,后文会单独分析),此过程通过调用updateComponent来完成更新。我们来看updateComponent(依然在 ReactCompositeComponent.js 中)

      • updateComponent中,先调用componentWillReceiveProps,然后 merge state,然后调用shouldComponentUpdate判断是否需要更新,可以看到,如果组件内部没有自定义,且用的是 PureComponent,会对 state 进行浅比较,设置shouldUpdate,最终调用_performComponentUpdate来进行更新。而在_performComponentUpdate中,会先调用componentWillUpdate,然后调用updateRenderedComponent进行更新,最后调用componentDidUpdate(过程较简单,就不列代码了)。下面看一下updateRenderedComponent的更新机制(依然在 ReactCompositeComponent.js 中)

    整个mount过程是递归渲染的(矢量图):

    react render 渲染周期剖析

    setState异步更新

    React快的原因之一就是,在执行this.setState()时,React没有忙着立即更新state,只是把新的state存到一个队列(batchUpdate)

    然后再统一处理(批处理),触发重新渲染过程,因此只重新渲染一次,结果只增加了一次。这样做是非常明智的,因为在一个函数里调用多个setState是常见的,如果每一次调用setState都要引发重新渲染,显然不是最佳实践。React官方文档里也说了:

    Think of setState() as a request rather than an immediate command to update the component.

    把setState() 看作是重新render的一次请求而不是立刻更新组件的指令。

    那么调用this.setState()后什么时候this.state才会更新?

    即将要执行下一次的render函数时。

    setState调用后,React会执行一个事务(Transaction),在这个事务中,React将新state放进一个队列中,当事务完成后,React就会刷新队列,然后启动另一个事务,这个事务包括执行 shouldComponentUpdate 方法来判断是否重新渲染,如果是,React就会进行state合并(state merge),生成新的state和props;如果不是,React仍然会更新this.state,只不过不会再render了。



    参考文章:

    React 源码解析 https://zhuanlan.zhihu.com/p/28697362

    React源码分析7 — React合成事件系统 https://blog.csdn.net/u013510838/article/details/61224760

    对React一些原理的理解 https://zhuanlan.zhihu.com/p/30032664

    问:React的useState和setState到底是同步还是异步呢? https://cloud.tencent.com/developer/article/2132184



    转载本站文章《React16源码分析(0):对象池/合成事件/事务机制等概念科普》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/jsBase/2016_0520_7833.html