解开React15生命周期以及setState面纱

1,332 阅读10分钟

本文章主要是通过阅读深入react技术栈这本书而积累总结读代码而来。所以文章中很多图和描述都来自此书。

react组件究竟是什么

  • 虚拟组件所需的元素只有如下:
    • 标签名

    • 节点属性,包含样式、属性、事件等  子节点

    • 标识 id 示例代码如下:

      {
          // 标签名
          tagName: 'div', 
          // 属性 
          properties: {
          // 样式
          style: {} 
          },
          // 子节点 
          children: [], // 唯一标识 
          key: 1
      }
      

1 创建react元素

creatElement创建虚拟dom节点元素,当使用 React 创建组件时,首先会调用 instantiateReactComponent,这是初始化组件的入口 函数,它通过判断 node 类型来区分不同组件的入口。

1)DOM组件

ReactDOMComponent 针对 Virtual DOM 标签的处理主要分为以下两个部分: 

  • 属性的更新,包括更新样式、更新属性、处理事件等;更新属性会先删除不需要的旧属性,再更新新的属性内容。
  • 子节点的更新,包括更新内容、更新子节点,此部分涉及 diff 算法。先删除不需要的子节点和内容,再更新子节点和内容。

2)自定义组件

ReactCompositeComponent 自定义组件实现了一整套 React 生命周期和 setState 机制,因此自定义组件是在生命周期的环境中进行更新属性、内容和子节点的操作。这些更新操作与 ReactDOMComponent 的操作类似。 生命周期的内容可以看下面小节。

react15的生命周期以及都干了啥

  • 生命周期执行顺序如下:

当使用 ES6 classes 构建 React 组件时,static defaultProps = {} 其实就是调用内部的 getDefaultProps 方法,constructor 中的 this.state = {} 其实就是调用内部的 getInitialState 方法。 因此,源码解读的部分与用 createClass 方法构建组件一样。

自定义组件(ReactCompositeComponent)的生命周期主要通过 3 个阶段进行管理—— MOUNTING、RECEIVE_PROPS 和 UNMOUNTING

1.createClass创建组件

createClass是自定义组件的入口,负责getDefaultProps。

2.阶段一:Mounting

mountComponent 负责管理生命周期中的 getInitialState、componentWillMount、render 和componentDidMount。 若存在 componentWillMount,则执行。如果此时在 componentWillMount 中调用 setState 方法,是不会触发 re-render的,而是会进行 state 合并(是和组件的state合并,而不是只更新到队列),因此 componentWillMount 中 的 this.state 并不是最新的,在 render 中才可以获取更新后的 this.state。

由于递归的特性,父组件的 componentWillMount 在其子组件的 componentWillMount 之前调用,而父组件的 componentDidMount 在其子组件的 componentDidMount 之后调用。

无状态组件没有更新队列只是专注于渲染。

3. 阶段二:RECEIVE_PROPS

只有在render和componentDidUpdate中才能获取最新的state队列,因为只有在render中才执行:inst.state = nextState.

  • 注意:禁止在componentWillUpdate以及shouldComponentUpdate中setstate,会导致循环,直至浏览器内存耗光而崩溃。

4.阶段三:UNMOUNTING

unmountComponent 负责管理生命周期中的 componentWillUnmount。

无状态组件

无状态组件没有状态,没有生命周期,只是简单地接受 props 渲染生成 DOM 结构,是一个 纯粹为渲染而生的组件。由于无状态组件有简单、便捷、高效等诸多优点,所以如果可能的话, 请尽量使用无状态组件。

//无状态组件使用
const HelloWorld = (props) => <div>{props.name}</div>; 
ReactDOM.render(<HelloWorld name="Hello World!" />, App);
//render 函数和 shouldConstruct 函数的代码如下(源码路径:/v15.0.0/src/renderers/shared/re-conciler/ReactCompositeComponent.js):
// 无状态组件只有一个 render 函数
StatelessComponent.prototype.render = function() {
    var Component = ReactInstanceMap.get(this)._currentElement.type;
    // 没有 state 状态
    var element = Component(this.props, this.context, this.updater);
    warnIfInvalidElement(Component, element);
    return element;
};

function shouldConstruct(Component) {
    return Component.prototype; 
    Component.prototype.isReactComponent;
}		
		

总周期

setState后发生了什么

setState是否异步

首先,setstate后,会更新一个更新队列的内容,会在某一个生命周期内,合并这个更新队列内容和inst.state(这里的inst就是react component)。在componentWillMount中setstate,不会立即re-render,inst.state = this._processPendingState(inst.props,inst.context).

React 初学者常会写出 this.state.value = 1 这样的代码,这是完全错误的写法。 setState 通过一个队列机制实现 state 更新。当执行 setState 时,会将需要更新的 state 合并 后放入状态队列,而不会立刻更新 this.state。 队列机制可以高效地批量更新 state。如果不通过 setState 而直接修改 this.state 的值,那么该 state 将不会被放入状态队列中,当下次调用 setState 并对状态队列进行合并时,将会忽略之前直接被修改的 state,而造成无法预知的错误。

// 将新的 state 合并到状态更新队列中
var nextState = this._processPendingState(nextProps, nextContext);
// 根据更新队列和 shouldComponentUpdate 的状态来判断是否需要更新组件
var shouldUpdate =this._pendingForceUpdate || !inst.shouldComponentUpdate ||
inst.shouldComponentUpdate(nextProps, nextState, nextContext);		

所以setstate其实并不是异步,只是由于一些react事件机制,让其表现异步。

setState流程

setstate会触发enqueuesetstate,会把需要更新的内容push到一个queue中,并调用enqueueUpdate,enqueueUpdate中会判断是否处在批量更新模式,如果 isBatchingUpdates 为 true,则对所有队列中的更新执行 batchedUpdates 方法,否则只 把当前组件(即调用了 setState 的组件)放入 dirtyComponents 数组中。

ReactComponent.prototype.setState = function (partialState, callback) {
//``其他特殊情况代码
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback);
  }
};
 enqueueSetState: function (publicInstance, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

    if (!internalInstance) {
      return;
    }

    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
},
function enqueueUpdate(component) {
  ensureInjected();
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
}

batchedUpdates 方法中有一个 transaction.perform 调用,这就需要说一下事务,后面小节会讲到。

setState循环调用风险

其中updateComponent代码: 会合并state

事务

  事务就是将需要执行的方法使用 wrapper 封装起来,再通过事务提供的 perform 方法执行。 而在 perform 之前,先执行所有 wrapper 中的 initialize 方法,执行完 perform 之后(即执行 method 方法后)再执行所有的 close 方法。一组 initialize 及 close 方法称为一个 wrapper。从 图3-16中可以看出,事务支持多个 wrapper 叠加。

import React, { Component } from 'react';
class Example extends Component { constructor() {
super(); this.state = {
val: 0 };
}
componentDidMount() {

    this.setState({val: this.state.val + 1});             console.log(this.state.val); // 第 1 次输出
    
    this.setState({val: this.state.val + 1}); console.log(this.state.val); // 第 2 次输出
    
    setTimeout(() => {
        this.setState({val: this.state.val + 1});             console.log(this.state.val); // 第 3 次输出

        this.setState({val: this.state.val + 1});
        console.log(this.state.val);// 第 4 次输出
    }, 0);
}
render() { 
    return null;
} }

4 次 console.log 打印出来的 val 分别是:0、0、2、3。

可以观察下图直接调用setState和setTimeout中的调用栈,可以看出 下面重点看看第一类 setState 的调 用栈,有没有发现什么?没错,就是 batchedUpdates 方法,原来早在 setState 调用前,已经处 于 batchedUpdates 执行的事务中了。 那这次 batchedUpdate 方法,又是谁调用的呢?让我们往前再追溯一层,原来是 ReactMount.js 中的_renderNewRootComponent 方法。也就是说,整个将 React 组件渲染到 DOM 中的过程就处于 一个大的事务中。 接下来的解释就顺理成章了,因为在 componentDidMount 中调用 setState 时,batchingStrategy 的 isBatchingUpdates 已经被设为 true,所以两次 setState 的结果并没有立即生效,而是被放进 了 dirtyComponents 中。这也解释了两次打印 this.state.val 都是 0 的原因。

再 反 观 setTimeout 中 的 两 次 setState , 因 为 没 有 前 置 的 batchedUpdate 调 用 , 所 以 batchingStrategy 的 isBatchingUpdates 标志位是 false,也就导致了新的 state 马上生效,没有 走到 dirtyComponents 分支。也就是说,setTimeout 中第一次执行 setState 时,this.state.val 为 1,而 setState 完成后打印时 this.state.val 变成了 2。第二次的 setState 同理。

react15中diff

diff操作分为tree diff、component diff、element diff。

1 tree diff

React 通过 updateDepth对 Virtual DOM 树进行层级控制,只会对相同层级的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

    /**
     * Updates the rendered children with new children.
     *
     * @param {?object} nextNestedChildrenElements Nested child element maps.
     * @param {ReactReconcileTransaction} transaction
     * @internal
     */
    updateChildren: function (nextNestedChildrenElements, transaction, context) {
      updateDepth++;
      var errorThrown = true;
      try {
        this._updateChildren(nextNestedChildrenElements, transaction, context);
        errorThrown = false;
      } finally {
        updateDepth--;
        if (!updateDepth) {
          if (errorThrown) {
            clearQueue();
          } else {
            processQueue();
          }
        }
      }
    },

    /**
     * Improve performance by isolating this hot code path from the try/catch
     * block in `updateChildren`.
     *
     * @param {?object} nextNestedChildrenElements Nested child element maps.
     * @param {ReactReconcileTransaction} transaction
     * @final
     * @protected
     */
    _updateChildren: function (nextNestedChildrenElements, transaction, context) {
      var prevChildren = this._renderedChildren;
      var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements, transaction, context);
      this._renderedChildren = nextChildren;
      if (!nextChildren && !prevChildren) {
        return;
      }
      var name;
      // `nextIndex` will increment for each child in `nextChildren`, but
      // `lastIndex` will be the last index visited in `prevChildren`.
      var lastIndex = 0;
      var nextIndex = 0;
      for (name in nextChildren) {
        if (!nextChildren.hasOwnProperty(name)) {
          continue;
        }
        var prevChild = prevChildren && prevChildren[name];
        var nextChild = nextChildren[name];
        if (prevChild === nextChild) {
          this.moveChild(prevChild, nextIndex, lastIndex);
          lastIndex = Math.max(prevChild._mountIndex, lastIndex);
          prevChild._mountIndex = nextIndex;
        } else {
          if (prevChild) {
            // Update `lastIndex` before `_mountIndex` gets unset by unmounting.
            lastIndex = Math.max(prevChild._mountIndex, lastIndex);
            this._unmountChild(prevChild);
          }
          // The child must be instantiated before it's mounted.
          this._mountChildByNameAtIndex(nextChild, name, nextIndex, transaction, context);
        }
        nextIndex++;
      }
      // Remove children that are no longer present.
      for (name in prevChildren) {
        if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) {
          this._unmountChild(prevChildren[name]);
        }
      }
    },

2 component diff

当组件D变为组件G时,即使这两个组件结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除组件 D,重新创建组件 G 及其子节 点。虽然当两个组件是不同类型但结构相似时,diff 会影响性能,但正如 React 官方博客所言: 不同类型的组件很少存在相似 DOM 树的情况,因此这种极端因素很难在实际开发过程中造成重 大的影响。

3 element diff

下图的变化,如果没有key值,那么会全都删除重新加载。但是有了key,会比较key值然后移动,下方有说明。

当节点发生了后移才移动,否则不移动位置。

function enqueueInsertMarkup(parentID, markup, toIndex) {
  // NOTE: Null values reduce hidden classes.
  updateQueue.push({
    parentID: parentID,
    parentNode: null,
    type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
    markupIndex: markupQueue.push(markup) - 1,
    content: null,
    fromIndex: null,
    toIndex: toIndex
  });
}

/**
 * Enqueues moving an existing element to another index.
 *
 * @param {string} parentID ID of the parent component.
 * @param {number} fromIndex Source index of the existing element.
 * @param {number} toIndex Destination index of the element.
 * @private
 */
function enqueueMove(parentID, fromIndex, toIndex) {
  // NOTE: Null values reduce hidden classes.
  updateQueue.push({
    parentID: parentID,
    parentNode: null,
    type: ReactMultiChildUpdateTypes.MOVE_EXISTING,
    markupIndex: null,
    content: null,
    fromIndex: fromIndex,
    toIndex: toIndex
  });
}

/**
 * Enqueues removing an element at an index.
 *
 * @param {string} parentID ID of the parent component.
 * @param {number} fromIndex Index of the element to remove.
 * @private
 */
function enqueueRemove(parentID, fromIndex) {
  // NOTE: Null values reduce hidden classes.
  updateQueue.push({
    parentID: parentID,
    parentNode: null,
    type: ReactMultiChildUpdateTypes.REMOVE_NODE,
    markupIndex: null,
    content: null,
    fromIndex: fromIndex,
    toIndex: null
  });
}

/**
 * Enqueues setting the markup of a node.
 *
 * @param {string} parentID ID of the parent component.
 * @param {string} markup Markup that renders into an element.
 * @private
 */
function enqueueSetMarkup(parentID, markup) {
  // NOTE: Null values reduce hidden classes.
  updateQueue.push({
    parentID: parentID,
    parentNode: null,
    type: ReactMultiChildUpdateTypes.SET_MARKUP,
    markupIndex: null,
    content: markup,
    fromIndex: null,
    toIndex: null
  });
}

/**
 * Enqueues setting the text content.
 *
 * @param {string} parentID ID of the parent component.
 * @param {string} textContent Text content to set.
 * @private
 */
function enqueueTextContent(parentID, textContent) {
  // NOTE: Null values reduce hidden classes.
  updateQueue.push({
    parentID: parentID,
    parentNode: null,
    type: ReactMultiChildUpdateTypes.TEXT_CONTENT,
    markupIndex: null,
    content: textContent,
    fromIndex: null,
    toIndex: null
  });
}

在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作。当节点数量过大或更新操作过于频繁时,这在一定程度上会影响 React 的渲染性能。

主要的更新流程是,setstate后,会在willmount或者willupdate函数最后合并更新队列,然后updatecomponent会进行diff操作,render阶段里,会引起receivecomponent阶段对属性进行更新。

如下图:

React patch

这里为什么可以直接依次插入节点呢?原因就是在 diff 阶段添加差异节点到差异队列时,本 身就是有序添加。也就是说,新增节点(包括 move 和 insert)在队列里的顺序就是最终真实 DOM 的顺序,因此可以直接依次根据 index 去插入节点。而且,React 并不是计算出一个差异就去执 行一次 Patch,而是计算出全部差异并放入差异队列后,再一次性地去执行 Patch 方法完成真实 DOM 的更新。