博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从源码全面剖析 React 组件更新机制
阅读量:5983 次
发布时间:2019-06-20

本文共 14925 字,大约阅读时间需要 49 分钟。

React 把组件看作状态机(有限状态机), 使用state来控制本地状态, 使用props来传递状态. 前面我们探讨了 React 如何映射状态到 UI 上(初始渲染), 那么接下来我们谈谈 React 时如何同步状态到 UI 上的, 也就是:

React 是如何更新组件的?

React 是如何对比出页面变化最小的部分?

这篇文章会为你解答这些问题.

在这之前

你已经了解了React (15-stable版本)内部的一些基本概念, 包括不同类型的组件实例、mount过程、事务、批量更新的大致过程(还没有? 不用担心, 为你准备好了、);

准备一个, 调试源码, 以便更好理解;

Keep calm and make a big deal !

React 是如何更新组件的?

TL;DR
  • 依靠事务进行批量更新;
  • 一次batch(批量)的生命周期就是从ReactDefaultBatchingStrategy事务perform之前(调用ReactUpdates.batchUpdates)到这个事务的最后一个close方法调用后结束;
  • 事务启动后, 遇到 setState 则将 partial state 存到组件实例的_pendingStateQueue上, 然后将这个组件存到dirtyComponents 数组中, 等到 ReactDefaultBatchingStrategy事务结束时调用runBatchedUpdates批量更新所有组件;
  • 组件的更新是递归的, 三种不同类型的组件都有自己的updateComponent方法来决定自己的组件如何更新, 其中 ReactDOMComponent 会采用diff算法对比子元素中最小的变化, 再批量处理.

这个更新过程像是一套流程, 无论你通过setState(或者replaceState)还是新的props去更新一个组件, 都会起作用.

那么具体是什么?

让我们从这套更新流程的开始部分讲起...

调用 setState 之前

首先, 开始一次batch的入口是在ReactDefaultBatchingStrategy里, 调用里面的batchedUpdates便可以开启一次batch:

// 批处理策略var ReactDefaultBatchingStrategy = {  isBatchingUpdates: false,   batchedUpdates: function(callback, a, b, c, d, e) {    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;    ReactDefaultBatchingStrategy.isBatchingUpdates = true; // 开启一次batch    if (alreadyBatchingUpdates) {      return callback(a, b, c, d, e);    } else {      // 启动事务, 将callback放进事务里执行      return transaction.perform(callback, null, a, b, c, d, e);      }  },};

在 React 中, 调用batchedUpdates有很多地方, 与更新流程相关的如下

// ReactMount.jsReactUpdates.batchedUpdates(      batchedMountComponentIntoNode,  // 负责初始渲染      componentInstance,      container,      shouldReuseMarkup,      context,);// ReactEventListener.jsdispatchEvent: function(topLevelType, nativeEvent) {    ...    try {      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);  // 处理事件    } finally {      TopLevelCallbackBookKeeping.release(bookKeeping);    }},

第一种情况, React 在首次渲染组件的时候会调用batchedUpdates, 然后开始渲染组件. 那么为什么要在这个时候启动一次batch呢? 不是因为要批量插入, 因为插入过程是递归的, 而是因为组件在渲染的过程中, 会依顺序调用各种生命周期函数, 开发者很可能在生命周期函数中(如componentWillMount或者componentDidMount)调用setState. 因此, 开启一次batch就是要存储更新(放入dirtyComponents), 然后在事务结束时批量更新. 这样以来, 在初始渲染流程中, 任何setState都会生效, 用户看到的始终是最新的状态.

第二种情况, 如果你在HTML元素上或者组件上绑定了事件, 那么你有可能在事件的监听函数中调用setState, 因此, 同样为了存储更新(放入dirtyComponents), 需要启动批量更新策略. 在回调函数被调用之前, React事件系统中的dispatchEvent函数负责事件的分发, 在dispatchEvent中启动了事务, 开启了一次batch, 随后调用了回调函数. 这样一来, 在事件的监听函数中调用的setState就会生效.

也就是说, 任何可能调用 setState 的地方, 在调用之前, React 都会启动批量更新策略以提前应对可能的setState

那么调用 batchedUpdates 后发生了什么?

React 调用batchedUpdates时会传进去一个函数, batchedUpdates会启动ReactDefaultBatchingStrategyTransaction事务, 这个函数就会被放在事务里执行:

// ReactDefaultBatchingStrategy.jsvar transaction = new ReactDefaultBatchingStrategyTransaction(); // 实例化事务var ReactDefaultBatchingStrategy = {  ...  batchedUpdates: function(callback, a, b, c, d, e) {    ...      return transaction.perform(callback, null, a, b, c, d, e);  // 将callback放进事务里执行       ...};

ReactDefaultBatchingStrategyTransaction这个事务控制了批量策略的生命周期:

// ReactDefaultBatchingStrategy.jsvar FLUSH_BATCHED_UPDATES = {  initialize: emptyFunction,  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),  // 批量更新};var RESET_BATCHED_UPDATES = {  initialize: emptyFunction,  close: function() {    ReactDefaultBatchingStrategy.isBatchingUpdates = false;  // 结束本次batch  },};var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

无论你传进去的函数是什么, 无论这个函数后续会做什么, 都会在执行完后调用上面事务的close方法, 先调用flushBatchedUpdates批量更新, 再结束本次batch.

调用 setState 后发生了什么

// ReactBaseClasses.js :ReactComponent.prototype.setState = function(partialState, callback) {  this.updater.enqueueSetState(this, partialState);  if (callback) {    this.updater.enqueueCallback(this, callback, 'setState');  }};// => ReactUpdateQueue.js:enqueueSetState: function(publicInstance, partialState) {    // 根据 this.setState 中的 this 拿到内部实例, 也就是组件实例    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');    // 取得组件实例的_pendingStateQueue    var queue =      internalInstance._pendingStateQueue ||      (internalInstance._pendingStateQueue = []);    // 将partial state存到_pendingStateQueue    queue.push(partialState);    // 调用enqueueUpdate    enqueueUpdate(internalInstance); }// => ReactUpdate.js:function enqueueUpdate(component) {  ensureInjected(); // 注入默认策略        // 如果没有开启batch(或当前batch已结束)就开启一次batch再执行, 这通常发生在异步回调中调用 setState      // 的情况  if (!batchingStrategy.isBatchingUpdates) {    batchingStrategy.batchedUpdates(enqueueUpdate, component);    return;  }    // 如果batch已经开启就存储更新  dirtyComponents.push(component);  if (component._updateBatchNumber == null) {    component._updateBatchNumber = updateBatchNumber + 1;  }}

也就是说, 调用 setState 会首先拿到内部组件实例, 然后把要更新的partial state存到其_pendingStateQueue中, 然后标记当前组件为dirtyComponent, 存到dirtyComponents数组中. 然后就接着继续做下面的事情了, 并没有立即更新, 这是因为接下来要执行的代码里有可能还会调用 setState, 因此只做存储处理.

什么时候批量更新?

首先, 一个事务在执行的时候(包括initialize、perform、close阶段), 任何一阶段都有可能调用一系列函数, 并且开启了另一些事务. 那么只有等后续开启的事务执行完, 之前开启的事务才继续执行. 下图是我们刚才所说的第一种情况, 在初始渲染组件期间 setState 后, React 启动的各种事务和执行的顺序:

从图中可以看到, 批量更新是在ReactDefaultBatchingStrategyTransaction事务的close阶段, 在flushBatchedUpdates函数中启动了ReactUpdatesFlushTransaction事务负责批量更新.

怎么批量更新的?

开启批量更新事务、批量处理callback

我们接着看flushBatchedUpdates函数, 在ReactUpdates.js中

var flushBatchedUpdates = function () {  // 启动批量更新事务  while (dirtyComponents.length || asapEnqueued) {    if (dirtyComponents.length) {      var transaction = ReactUpdatesFlushTransaction.getPooled();      transaction.perform(runBatchedUpdates, null, transaction);      ReactUpdatesFlushTransaction.release(transaction);    }// 批量处理callback    if (asapEnqueued) {      asapEnqueued = false;      var queue = asapCallbackQueue;      asapCallbackQueue = CallbackQueue.getPooled();      queue.notifyAll();      CallbackQueue.release(queue);    }  }};
遍历dirtyComponents

flushBatchedUpdates启动了一个更新事务, 这个事务执行了runBatchedUpdates进行批量更新:

// ReactUpdates.jsfunction runBatchedUpdates(transaction) {  var len = transaction.dirtyComponentsLength;  // 排序保证父组件优先于子组件更新  dirtyComponents.sort(mountOrderComparator);  // 代表批量更新的次数, 保证每个组件只更新一次  updateBatchNumber++;  // 遍历 dirtyComponents  for (var i = 0; i < len; i++) {    var component = dirtyComponents[i];          var callbacks = component._pendingCallbacks;    component._pendingCallbacks = null;    ...    // 执行更新    ReactReconciler.performUpdateIfNecessary(      component,      transaction.reconcileTransaction,      updateBatchNumber,    );    ...    // 存储 callback以便后续按顺序调用    if (callbacks) {      for (var j = 0; j < callbacks.length; j++) {        transaction.callbackQueue.enqueue(          callbacks[j],          component.getPublicInstance(),        );      }    }  }}

前面 setState 后将组件推入了dirtyComponents, 现在就是要遍历dirtyComponents数组进行更新了.

根据不同情况执行更新

ReactReconciler会调用组件实例的performUpdateIfNecessary. 如果接收了props, 就会调用此组件的receiveComponent, 再在里面调用updateComponent更新组件; 如果没有接受props, 但是有新的要更新的状态(_pendingStateQueue不为空)就会直接调用updateComponent来更新:

// ReactCompositeComponent.jsperformUpdateIfNecessary: function (transaction) {    if (this._pendingElement != null) {        ReactReconciler.receiveComponent(this, this._pendingElement, transaction,                 this._context);    } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {        this.updateComponent(transaction, this._currentElement, this._currentElement,             this._context, this._context);    } else {        this._updateBatchNumber = null;    }}
调用组件实例的updateComponent

接下里就是重头戏updateComponent了, 它决定了组件如果更新自己和它的后代们. 需要特别注意的是, React 内部三种不同的组件类型, 每种组件都有自己的updateComponent, 有不同的行为.

对于 ReactCompositeComponent ():

updateComponent所做的事情 :

  • 调用此层级组件的一系列生命周期函数, 并且在合适的时机更新props、state、context;
  • re-render, 与之前 render 的 element 比较, 如果两者key && element.type 相等, 则进入下一层进行更新; 如果不等, 直接移除重新mount

对于 ReactDOMComponent:

updateComponent所做的事情 :

  • 更新这一层级DOM元素属性;
  • 更新子元素, 调用 ReactMultiChild 的 updateChildren, 对比前后变化、标记变化类型、存到updates中(diff算法主要部分);
  • 批量处理updates

对于 ReactDOMTextComponent :

上面只是每个组件自己更新的过程, 那么 React 是如何一次性更新所有组件的 ? 答案是递归.

递归调用组件的updateComponent

观察 ReactCompositeComponent 和 ReactDOMComponent 的更新流程, 我们发现 React 每次走到一个组件更新过程的最后部分, 都会有一个判断 : 如果 nextELement 和 prevElement key 和 type 相等, 就会调用receiveComponent. receiveComponentupdateComponent一样, 每种组件都有一个, 作用就相当于updateComponent 接受了新 props 的版本. 而这里调用的就是子元素的receiveComponent, 进而进行子元素的更新, 于是就形成了递归更新、递归diff. 因此, 整个流程就像这样() :

这种更新完一级、diff完一级再进入下一级的过程保证 React 只遍历一次组件树就能完成更新, 但代价就是只要前后 render 出元素的 type 和 key 有一个不同就删除重造, 因此, React 建议页面要尽量保持稳定的结构.

React 是如何对比出页面变化最小的部分?

你可能会说 React 用 virtual DOM 表示了页面结构, 每次更新, React 都会re-render出新的 virtual DOM, 再通过 diff 算法对比出前后变化, 最后批量更新. 没错, 很好, 这就是大致过程, 但这里存在着一些隐藏的深层问题值得探讨 :

  • React 是如何用 virtual DOM 表示了页面结构, 从而使任何页面变化都能被 diff 出来?
  • React 是如何 diff 出页面变化最小的部分?

React 如何表示页面结构

class C extends React.Component {    render () {        return (            
"dscsdcsd"
console.log(e)}>{this.state.val}
) }}// virtual DOM(React element){ $$typeof: Symbol(react.element) key: null props: { // props 代表元素上的所有属性, 有children属性, 描述子组件, 同样是元素 children: [ ""dscsdcsd"", {$$typeof: Symbol(react.element), type: "i", key: null, ref: null, props: {…}, …}, {$$typeof: Symbol(react.element), type: class Children, props: {…}, …} ] className: 'container' } ref: null type: "div" _owner: ReactCompositeComponentWrapper {...} // class C 实例化后的对象 _store: {validated: false} _self: null _source: null}

每个标签, 无论是DOM元素还是自定义组件, 都会有 key、type、props、ref 等属性.

  • key 代表元素唯一id值, 意味着只要id改变, 就算前后元素种类相同, 元素也肯定不一样了;
  • type 代表元素种类, 有 function(空的wrapper)、class(自定义类)、string(具体的DOM元素名称)类型, 与key一样, 只要改变, 元素肯定不一样;
  • props 是元素的属性, 任何写在标签上的属性(如className='container')都会被存在这里, 如果这个元素有子元素(包括文本内容), props就会有children属性, 存储子元素; children属性是递归插入、递归更新的依据;

也就是说, 如果元素唯一标识符或者类别或者属性有变化, 那么它们re-render后对应的 key、type 和props里面的属性也会改变, 前后一对比即可找出变化. 综上来看, React 这么表示页面结构确实能够反映前后所有变化.

那么 React 是如何 diff 的?

React diff 每次只对同一层级的节点进行比对 :

上图的数字表示遍历更新的次序.

从父节点开始, 每一层 diff 包括两个地方

  • element diff—— 前后 render 出来的 element 的对比, 这个对比是为了找出前后节点是不是同一节点, 会对比前后render出来的元素它们的 key 和 type. element diff 包括两个地方, 组件顶层DOM元素对比和子元素的对比:

    组件顶层DOM元素对比 :

    // ReactCompositeComponent.js/updateComponent => _updateRenderedComponent_updateRenderedComponent: function(transaction, context) {    // re-render 出element    var nextRenderedElement = this._renderValidatedComponent();    // 对比前后变化    if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {      // 如果 key && type 没变进行下一级更新      ReactReconciler.receiveComponent(...);    } else {      // 如果变了移除重造      ReactReconciler.unmountComponent(prevComponentInstance, false);      ...      var child = this._instantiateReactComponent(...);        var nextMarkup = ReactReconciler.mountComponent(...);      this._replaceNodeWithMarkup(...);    }}

    子元素的对比:

    // ReactChildReconciler.jsupdateChildren: function(...) {    ...    for (name in nextChildren) {  // 遍历 re-render 出的elements      ...      if (        prevChild != null &&        shouldUpdateReactComponent(prevElement, nextElement)      ) {        // 如果key && type 没变进行下一级更新        ReactReconciler.receiveComponent(...);          nextChildren[name] = prevChild;  // 更新完放入 nextChildren, 注意放入的是组件实例      } else {        // 如果变了则移除重建                                       if (prevChild) {          removedNodes[name] = ReactReconciler.getHostNode(prevChild);          ReactReconciler.unmountComponent(prevChild, false);        }        var nextChildInstance = instantiateReactComponent(nextElement, true);        nextChildren[name] = nextChildInstance;                  var nextChildMountImage = ReactReconciler.mountComponent(...);        mountImages.push(nextChildMountImage);      }    }    // 再除掉 prevChildren 里有, nextChildren 里没有的组件    for (name in prevChildren) {      if (        prevChildren.hasOwnProperty(name) &&        !(nextChildren && nextChildren.hasOwnProperty(name))      ) {        prevChild = prevChildren[name];        removedNodes[name] = ReactReconciler.getHostNode(prevChild);        ReactReconciler.unmountComponent(prevChild, false);      }    }  },

    shouldComponentUpdate 函数:

    function shouldUpdateReactComponent(prevElement, nextElement) {    var prevEmpty = prevElement === null || prevElement === false;  var nextEmpty = nextElement === null || nextElement === false;  if (prevEmpty || nextEmpty) {    return prevEmpty === nextEmpty;  }  var prevType = typeof prevElement;  var nextType = typeof nextElement;  // 如果前后变化都是字符串、数字类型的则允许更新  if (prevType === 'string' || prevType === 'number') {    return nextType === 'string' || nextType === 'number';  } else {    // 否则检查 type && key    return (      nextType === 'object' &&      prevElement.type === nextElement.type &&      prevElement.key === nextElement.key    );  }}

    element diff 检测 type && key 都没变时会进入下一级更新, 如果变化则直接移除重造新元素, 然后遍历同级的下一个.

  • subtree diff ——组件顶层DOM元素包裹的所有子元素(也就是props.children里的元素)与之前版本的对比, 这个对比是为了找出同级所有子节点的变化, 包括移除、新建、同级范围的移动;

    // ReactMultiChild.js_updateChildren: function(...) {      var prevChildren = this._renderedChildren;      var removedNodes = {};      var mountImages = [];      // 拿到更新后子组件实例      var nextChildren = this._reconcilerUpdateChildren();      ...      // 遍历子组件实例      for (name in nextChildren) {           ...        var prevChild = prevChildren && prevChildren[name];        var nextChild = nextChildren[name];        // 因为子组件的更新是在原组件实例上更改的, 因此与之前的组件作引用比较即可判断        if (prevChild === nextChild) {            // 发生了移动          updates = enqueue(            updates,            this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex),          );          lastIndex = Math.max(prevChild._mountIndex, lastIndex);          prevChild._mountIndex = nextIndex;        } else {          ...          // 有新的组件          updates = enqueue(            updates,            this._mountChildAtIndex(              nextChild,              mountImages[nextMountIndex],              lastPlacedNode,              nextIndex,              transaction,              context,            ),          );          nextMountIndex++;        }        nextIndex++;        lastPlacedNode = ReactReconciler.getHostNode(nextChild);      }      // Remove children that are no longer present.      for (name in removedNodes) {          // removedNodes 记录了所有的移除节点        if (removedNodes.hasOwnProperty(name)) {          updates = enqueue(            updates,            this._unmountChild(prevChildren[name], removedNodes[name]),          );        }      }      if (updates) {        processQueue(this, updates); // 批量处理      }      this._renderedChildren = nextChildren;    },

    React 会将同一层级的变化标记, 如 MOVE_EXISTING、REMOVE_NODE、TEXT_CONTENT、INSERT_MARKUP 等, 统一放到 updates 数组中然后批量处理.

And that‘s it !

React 是一个激动人心的库, 它给我们带来了前所未有的开发体验, 但当我们沉浸在使用 React 快速实现需求的喜悦中时, 有必要去探究两个问题 : Why and How?

为什么 React 会如此流行, 原因是什么? 组件化、快速、足够简单、all in js、容易扩展、生态丰富、社区强大...

React 反映了哪些思想/理念/思路 ? 状态机、webComponents、virtual DOM、virtual stack、异步渲染、多端渲染、单向数据流、反应式更新、函数式编程...

React 这些理念/思路受什么启发 ? 怎么想到的 ? 又怎么实现的? ...

转载地址:http://gmrox.baihongyu.com/

你可能感兴趣的文章
我的友情链接
查看>>
heartbeat实现Mysql主主高可用
查看>>
2015年9月28
查看>>
java线程池
查看>>
Spring与SpringMVC整合时产生的两个上下文容器之间的关系
查看>>
阿里云ECS的一次简单试用
查看>>
软件加密授权工具比较
查看>>
基于CentOS 7的owncloud部署
查看>>
基于Cadence Virtuoso 设计平台的单片射频收发集成电路的设计过程
查看>>
桌面支持--电脑桌面图标全选怎么解除.png
查看>>
TCP/IP 协议
查看>>
删除cm-5.13.2目录报错
查看>>
公钥,私钥关系
查看>>
C++语言中的预编译指令记录
查看>>
JAVA 线程数量如何设置
查看>>
分布式缓存架构设计
查看>>
【翻译】CodeMix使用教程(五):构建管道和验证
查看>>
AWS 使用AWS CLI 创建ECS集群
查看>>
nodejs 黑名单中间件设计
查看>>
三, 跨语言微服务框架 - Istio官方示例(自动注入.请求路由.流量控制.故障注入)...
查看>>