资讯专栏INFORMATION COLUMN

React 更新视图过程

y1chuan / 495人阅读

摘要:最终的更新策略都在中。按以上两个状态的,那么遍历完就会记录下需要做两步变更新增一个节点插入到第二个位置,删除原来第二个位置上的。例遍历结果第二个节点新增一个,第四个节点新增一个,删除第二个。基本上之后到最终的变化的过程就是这么结束了。

说在前面,着重梳理实际更新组件和 dom 部分的代码,但是关于异步,transaction,批量合并新状态等新细节只描述步骤。一来因为这些细节在读源码的时候只读了部分,二来如果要把这些都写出来要写老长老长。

真实的 setState 的过程:

setState( partialState ) {
  // 1. 通过组件对象获取到渲染对象
  var internalInstance = ReactInstanceMap.get(publicInstance);
  // 2. 把新的状态放在渲染对象的 _pendingStateQueue 里面 internalInstance._pendingStateQueue.push( partialState )
  // 3. 查看下是否正在批量更新
  //   3.1. 如果正在批量更新,则把当前这个组件认为是脏组件,把其渲染对象保存到 dirtyComponents 数组中
  //   3.2. 如果可以批量更新,则调用 ReactDefaultBatchingStrategyTransaction 开启更新事务,进行真正的 vdom diff。
  //    |
  //    v
  // internalInstance.updateComponent( partialState )
}

updateComponent 方法的说明:

updateComponent( partialState ) {
  // 源码中 partialState 是从 this._pendingStateQueue 中获取的,这里简化了状态队列的东西,假设直接从外部传入
  var inst = this._instance;
  var nextState = Object.assign( {}, inst.state, partialState );
  // 获得组件对象,准备更新,先调用生命周期函数
      // 调用 shouldComponentUpdate 看看是否需要更新组件(这里先忽略 props 和 context的更新)
  if ( inst.shouldComponentUpdate(inst.props, nextState, nextContext) ) {
    // 更新前调用 componentWillUpdate
    isnt.componentWillUpdate( inst.props, nextState, nextContext );
    inst.state = nextState;
    // 生成新的 vdom
    var nextRenderedElement = inst.render();
    // 通过上一次的渲染对象获取上一次生成的 vdom
    var prevComponentInstance = this._renderedComponent; // render 中的根节点的渲染对象
    var prevRenderedElement = prevComponentInstance._currentElement; // 上一次的根节点的 vdom
    // 通过比较新旧 vdom node 来决定是更新 dom node 还是根据最新的 vdom node 生成一份真实 dom node 替换掉原来的
    if ( shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement) ) {
      // 更新 dom node
      prevComponentInstance.receiveComponent( nextRenderedElement )
    } else {
      // 生成新的 dom node 替换原来的(以下是简化版,只为了说明流程)
      var oldHostNode = ReactReconciler.getHostNode( prevComponentInstance );
      // 根据新的 vdom 生成新的渲染对象
      var child = instantiateReactComponent( nextRenderedElement );
      this._renderedComponent = child;
      // 生成新的 dom node
      var nextMarkup = child.mountComponent();
      // 替换原来的 dom node
      oldHostNode.empty();
      oldHostNode.appendChild( nextMarkup )
    }
  }
}

接下来看下 shouldUpdateReactComponent 方法:

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 {
    return (
      nextType === "object" &&
      prevElement.type === nextElement.type &&
      prevElement.key === nextElement.key
    );
  }
}

基本的思路就是比较当前 vdom 节点的类型,如果一致则更新,如果不一致则重新生成一份新的节点替换掉原来的。好了回到刚刚跟新 dom node这条路 prevComponentInstance.receiveComponent( nextRenderedElement ),即 render 里面根元素的渲染对象的 receiveComponent 方法做了最后的更新 dom 的工作。如果根节点的渲染对象是组件即 ReactCompositeComponent.receiveComponent,如果根节点是内置对象(html 元素)节点即 ReactDOMComponent.receiveComponent。ReactCompositeComponent.receiveComponent 最终还是调用的上面提到的 updateComponent 循环去生成 render 中的 vdom,这里就先不深究了。最终 html dom node 的更新策略都在 ReactDOMComponent.receiveComponent 中。

class ReactDOMComponent {
  // @param {nextRenderedElement} 新的 vdom node
  receiveComponent( nextRenderedElement ) {
    var prevElement = this._currentElement;
    this._currentElement = nextRenderedElement;

    var lastProps = prevElement.props;
    var nextProps = this._currentElement.props;
    var lastChildren = lastProps.children;
    var nextChildren = nextProps.children;
    /*
      更新 props
      _updateDOMProperties 方法做了下面两步
      1. 记录下 lastProps 中有的,nextProps 没有的,删除
      2. 记录下 nextProps 中有的,且与 lastProps中不同的属性,setAttribute 之
    */
    this._updateDOMProperties(lastProps, nextProps, transaction);

    /*
      迭代更新子节点,源代码中是 this._updateDOMChildren(lastProps, nextProps, transaction, context);
      以下把 _updateDOMChildren 方法展开,对于子节点类型的判断源码比较复杂,这里只针对string|number和非string|number做一个简单的流程示例
    */
    // 1. 如果子节点从有到无,则删除子节点
    if ( lastChildren != null && nextChildren == null ) {
    
      if ( typeof lastChildren === "string" | "number" /* 伪代码 */ ) {
        this.updateTextContent("");
      } else {
        this.updateChildren( null, transaction, context );
      }
    }
    // 2. 如果新的子节点相对于老的是有变化的
    if ( nextChildren != null ) {
      if ( typeof lastChildren === "string" | "number" && lastChildren !== nextChildren /* 伪代码 */ ) {
        this.updateTextContent("" + nextChildren);
      } else if ( lastChildren !== nextChildren ) {
        this.updateChildren( nextChildren, transaction, context );
      }
    }
  }
}

this.updateChildren( nextChildren, transaction, context ) 中是真正的 diff 算法,就不以代码来说了(因为光靠代码很难说明清楚)

先来看最简单的情况:
例A:

按节点顺序开始遍历 nextChildren(遍历的过程中记录下需要对节点做哪些变更,等遍历完统一执行最终的 dom 操作),相同位置如果碰到和 prevChildren 中 tag 一样的元素认为不需要对节点进行删除,只需要更新节点的 attr,如果碰到 tag 不一样,则按照新的 vdom 中的节点重新生成一个节点,并把 prevChildren 中相同位置老节点删除。按以上两个状态的 vdom tree,那么遍历完就会记录下需要做两步 dom 变更:新增一个 span 节点插入到第二个位置,删除原来第二个位置上的 div。

再来看两个例子:
例B:

遍历结果:第二个节点新增一个span,删除第二个div和第四个div。

例C:

遍历结果:第二个节点新增一个span,第四个节点新增一个div,删除第二个div。

我们看到对于例C来说其实最便利的方法就是把 span 插入到第二的位置上,然后其他div只要做 attr 的更新而不需要再进行位置的增删,如果 attr 都没有变化,那么后两个 div 根本不需要变化。但是按例A里面的算法,我们需要进行好几步的 dom 操作。这是为算法减少时间复杂度,做了妥协。但是 react 对节点引入了 key 这个关键属性帮助优化这种情况。假设我们给所有节点都添加了唯一的 key 属性,如下面例D:
例D:

我们在遍历过程中对所要记录的东西进行优化,在某个位置碰到有 key 的节点我们去 prevChildren 中找有没有对应的节点,如果有,则我们会比较当前节点在前后两个 tree 中相对位置。如果相对位置没有变化,则不需要做dom的增删移,而只需要更新。如果位置不一样则需要记录把这个节点从老的位置移动到新的位置(具体算法需要借助前一次dom变化的记录这里不详述)。这样从例C到例D的优化减少了 dom 节点的增删。

但是 react 的这种算法的优化也带来了一种极端的情况:
例E:

遍历结果:3次节点位置移动:2到1,1到2,0到3。

但是其实这里只需要更新每个节点的 attr,他们的位置根本不需要做变化。所以如果要给元素指定 key 最好避免元素的位置有太多太大的跃迁变化。

基本上 setState 之后到最终的 dom 变化的过程就是这么结束了。

后记:
梳理的比较简单,很多细节我没有精力作一一的总结,因为我自己看源码看了好久,代码中涉及到很多异步,事务等等干扰项,然后我自己又不想过多的借助现有的资料-_-。当我快要把最后一点写完的时候发现 pure render 专栏的作者陈屹出了一本《深入React技术栈》里面有相当详细的源码分析,所以我感觉我这篇“白写”了,贴出这本书就可以了,不过陈屹的这本书是良心之作,必须安利下。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83011.html

相关文章

  • 玩转 React(五)- 组件的内部状态和生命周期

    摘要:另外本文中会介绍一个通过类继承方式定义的组件的生命周期,以及在各个生命周期函数中能做什么,不能或尽量不要做什么。各个生命周期函数介绍及使用经验。获取组件的初始内部状态在中。该声明周期函数可能在两种情况下被调用组件接收到了新的属性。 文章标题总算是可以正常一点了…… 通过之前的文章我们已经知道:在 React 体系中所谓的 在 JavaScript 中编写 HTML 代码 指的是 Rea...

    Rocture 评论0 收藏0
  • 玩转 React(五)- 组件的内部状态和生命周期

    摘要:另外本文中会介绍一个通过类继承方式定义的组件的生命周期,以及在各个生命周期函数中能做什么,不能或尽量不要做什么。各个生命周期函数介绍及使用经验。获取组件的初始内部状态在中。该声明周期函数可能在两种情况下被调用组件接收到了新的属性。 文章标题总算是可以正常一点了…… 通过之前的文章我们已经知道:在 React 体系中所谓的 在 JavaScript 中编写 HTML 代码 指的是 Rea...

    ASCH 评论0 收藏0
  • 浅谈MVC,MVP,MVVM渐进变化及React与Vue比较

    摘要:将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库。此示例使用类似的语法,称为。执行更快,因为它在编译为代码后进行了优化。基于的模板使得将已有的应用逐步迁移到更为容易。 前言 因为没有明确的界定,这里不讨论正确与否,只表达个人对前端MV*架构模式理解看法,再比较React和Vue两种框架不同.写完之后我知道这文章好水,特别是框架对比部分都是别人说烂的,而我也是打算把...

    DrizzleX 评论0 收藏0
  • React学习之认识Flux架构模式

    摘要:是用户建立客户端应用的前端架构,它通过利用一个单向的数据流补充了的组合视图组件,这更是一种模式而非正式框架,你能够无需许多新代码情况下立即开始使用。结构和数据流一个单向数据流是模式的核心。 Flux是Facebook用户建立客户端Web应用的前端架构,它通过利用一个单向的数据流补充了React的组合视图组件,这更是一种模式而非正式框架,你能够无需许多新代码情况下立即开始使用Flux。 ...

    EasonTyler 评论0 收藏0
  • React渲染流程分析Diff算法

    摘要:什么是虚拟在中,执行的结果得到的并不是真正的节点,结果仅仅是轻量级的对象,我们称之为。后来产出的架构模式,期望从代码组织方式来降低维护难度。 1、什么是虚拟DOM 在React中,render执行的结果得到的并不是真正的DOM节点,结果仅仅是轻量级的JavaScript对象,我们称之为virtual DOM。 简单的说,其实所谓的virtual DOM就是JavaScript对象到H...

    lykops 评论0 收藏0
  • React渲染流程分析Diff算法

    摘要:什么是虚拟在中,执行的结果得到的并不是真正的节点,结果仅仅是轻量级的对象,我们称之为。后来产出的架构模式,期望从代码组织方式来降低维护难度。 1、什么是虚拟DOM 在React中,render执行的结果得到的并不是真正的DOM节点,结果仅仅是轻量级的JavaScript对象,我们称之为virtual DOM。 简单的说,其实所谓的virtual DOM就是JavaScript对象到H...

    sutaking 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<