React源码分析3 — React生命周期详解

简介: React源码系列文章,请多支持: [React源码分析1 — 组件和对象的创建(createClass,createElement)](https://www.atatech.org/articles/72905) [React源码分析2 — React组件插入DOM流程](http://www.

React源码系列文章,请多支持:
React源码分析1 — 组件和对象的创建(createClass,createElement)
React源码分析2 — React组件插入DOM流程
React源码分析3 — React生命周期详解
React源码分析4 — setState机制
React源码分析5 -- 组件通信,refs,key,ReactDOM
React源码分析6 — React合成事件系统

1 React生命周期流程

Markdown

调用流程可以参看上图。分为实例化,存在期和销毁三个不同阶段。介绍生命周期流程的文章很多,相信大部分同学也有所了解,我们就不详细分析了。很多同学肯定有疑问,这些方法在React内部是在哪些方法中被调用的呢,他们触发的时机又是什么时候呢。下面我们来详细分析。

2 实例化生命周期

getDefaultProps

在React.creatClass()初始化组件类时,会调用getDefaultProps(),将返回的默认属性挂载到defaultProps变量下。这段代码之前已经分析过了,参考 React源码分析1 — 组件和对象的创建(createClass,createElement).

这里要提的一点是,初始化组件类只运行一次。可以把它简单类比为Java中的Class对象。初始化组件类就是ClassLoader加载Class对象的过程。类对象的初始化不要错误理解成了实例对象的初始化。一个React组件类可能会在JSX中被多次调用,产生多个组件对象,但它只有一个类对象,也就是类加载后getDefaultProps就不会再调用了。

getInitialState

这个方法在创建组件实例对象的时候被调用,具体代码位于React.creatClass()的Constructor函数中。之前文章中已经分析了,参考 React源码分析1 — 组件和对象的创建(createClass,createElement)

每次创建React实例对象时,它都会被调用。

mountComponent

componentWillMount,render,componentDidMount都是在mountComponent中被调用。在React源码分析2 — React组件插入DOM流程一文中,我们讲过mountComponent被调用的时机。它是在渲染新的ReactComponent中被调用的。输入ReactComponent,返回组件对应的HTML。把这个HTML插入到DOM中,就可以生成组件对应的DOM对象了。所以mountComponent尤其关键。

和Java中的多态一样,不同的React组件的mountComponent实现都有所区别。下面我们来重点分析React自定义组件类,也就是ReactCompositeComponent的mountComponent。

  mountComponent: function (transaction, nativeParent, nativeContainerInfo, context) {
    this._context = context;
    this._mountOrder = nextMountID++;
    this._nativeParent = nativeParent;
    this._nativeContainerInfo = nativeContainerInfo;

    // 做propTypes是否合法的判断,这个只在开发阶段有用
    var publicProps = this._processProps(this._currentElement.props);
    var publicContext = this._processContext(context);

    var Component = this._currentElement.type;

    // 初始化公共类
    var inst = this._constructComponent(publicProps, publicContext);
    var renderedElement;

    // inst或者inst.render为空对应的是stateless组件,也就是无状态组件
    // 无状态组件没有实例对象,它本质上只是一个返回JSX的函数而已。是一种轻量级的React组件
    if (!shouldConstruct(Component) && (inst == null || inst.render == null)) {
      renderedElement = inst;
      warnIfInvalidElement(Component, renderedElement);
      inst = new StatelessComponent(Component);
    }

    // 设置变量
    inst.props = publicProps;
    inst.context = publicContext;
    inst.refs = emptyObject;
    inst.updater = ReactUpdateQueue;
    this._instance = inst;

    // 存储实例对象的引用到map中,方便以后查找
    ReactInstanceMap.set(inst, this);
    
    // 初始化state,队列等
    var initialState = inst.state;
    if (initialState === undefined) {
      inst.state = initialState = null;
    }
    this._pendingStateQueue = null;
    this._pendingReplaceState = false;
    this._pendingForceUpdate = false;

    var markup;
    if (inst.unstable_handleError) {
      // 挂载时出错,进行一些错误处理,然后performInitialMount,初始化挂载
      markup = this.performInitialMountWithErrorHandling(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    } else {
      // 初始化挂载
      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    }

    if (inst.componentDidMount) {
      // 调用componentDidMount,以事务的形式
      transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
    }

    return markup;
  },

mountComponent先做实例对象的props,state等初始化,然后调用performInitialMount初始化挂载,完成后调用componentDidMount。这个调用链还是很清晰的。下面我们重点来分析performInitialMountWithErrorHandling和performInitialMount

  performInitialMountWithErrorHandling: function (renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
    var markup;
    var checkpoint = transaction.checkpoint();
    try {
      // 放到try-catch中,如果没有出错则调用performInitialMount初始化挂载。可见这里没有什么特别的操作,也就是做一些错误处理而已
      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    } catch (e) {
      // handleError,卸载组件,然后重新performInitialMount初始化挂载
      transaction.rollback(checkpoint);
      this._instance.unstable_handleError(e);
      if (this._pendingStateQueue) {
        this._instance.state = this._processPendingState(this._instance.props, this._instance.context);
      }
      checkpoint = transaction.checkpoint();

      this._renderedComponent.unmountComponent(true);
      transaction.rollback(checkpoint);
      
      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    }
    return markup;
  },

可见performInitialMountWithErrorHandling只是多了一层错误处理而已,关键还是在performInitialMount中。

  performInitialMount: function (renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
    var inst = this._instance;
    if (inst.componentWillMount) {
      // render前调用componentWillMount
      inst.componentWillMount();
      // 将state提前合并,故在componentWillMount中调用setState不会触发重新render,而是做一次state合并。这样做的目的是减少不必要的重新渲染
      if (this._pendingStateQueue) {
        inst.state = this._processPendingState(inst.props, inst.context);
      }
    }

    // 如果不是stateless,即无状态组件,则调用render,返回ReactElement
    if (renderedElement === undefined) {
      renderedElement = this._renderValidatedComponent();
    }

    // 得到组件类型,如空组件ReactNodeTypes.EMPTY,自定义React组件ReactNodeTypes.COMPOSITE,DOM原生组件ReactNodeTypes.NATIVE
    this._renderedNodeType = ReactNodeTypes.getType(renderedElement);
    // 由ReactElement生成ReactComponent,这个方法在之前讲解过。根据不同type创建不同Component对象
    // 参考 http://blog.csdn.net/u013510838/article/details/55669769
    this._renderedComponent = this._instantiateReactComponent(renderedElement);

    // 递归渲染,渲染子组件
    var markup = ReactReconciler.mountComponent(this._renderedComponent, transaction, nativeParent, nativeContainerInfo, this._processChildContext(context));

    return markup;
  },

performInitialMount中先调用componentWillMount(),再将setState()产生的state改变进行state合并,然后调用_renderValidatedComponent()返回ReactElement,它会调用render()方法。然后由ReactElement创建ReactComponent。最后进行递归渲染。下面来看renderValidatedComponent()

  _renderValidatedComponent: function () {
    var renderedComponent;
    ReactCurrentOwner.current = this;
    try {
      renderedComponent = this._renderValidatedComponentWithoutOwnerOrContext();
    } finally {
      ReactCurrentOwner.current = null;
    }
    !(
    return renderedComponent;
  },
      
  _renderValidatedComponentWithoutOwnerOrContext: function () {
    var inst = this._instance;
    // 调用render方法,得到ReactElement。JSX经过babel转译后其实就是createElement()方法。这一点在前面也讲解过
    var renderedComponent = inst.render();
    return renderedComponent;
  },

3 存在期生命周期

组件实例对象已经生成时,我们可以通过setState()来更新组件。setState机制后面会有单独文章分析,现在只用知道它会调用updateComponent()来完成更新即可。下面来分析updateComponent

updateComponent: function(transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext
  ) {
    var inst = this._instance;
    var willReceive = false;
    var nextContext;
    var nextProps;

    // context对象如果有改动,则检查propTypes等,这在开发阶段可以报错提醒
    if (this._context === nextUnmaskedContext) {
      nextContext = inst.context;
    } else {
      nextContext = this._processContext(nextUnmaskedContext);
      willReceive = true;
    }

    // 如果父元素类型相同,则跳过propTypes类型检查
    if (prevParentElement === nextParentElement) {
      nextProps = nextParentElement.props;
    } else {
      nextProps = this._processProps(nextParentElement.props);
      willReceive = true;
    }

    // 调用componentWillReceiveProps,如果通过setState进入的updateComponent,则没有这一步
    if (willReceive && inst.componentWillReceiveProps) {
      inst.componentWillReceiveProps(nextProps, nextContext);
    }

    // 提前合并state,componentWillReceiveProps中调用setState不会重新渲染,在此处做合并即可,因为后面也是要调用render的
    // 这样可以避免没必要的渲染
    var nextState = this._processPendingState(nextProps, nextContext);
    
    // 调用shouldComponentUpdate给shouldUpdate赋值
    // 如果通过forceUpdate进入的updateComponent,即_pendingForceUpdate不为空,则不用判断shouldComponentUpdate.
    var shouldUpdate = true;
    if (!this._pendingForceUpdate && inst.shouldComponentUpdate) {
      shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
    }
    
    // 如果shouldUpdate为true,则会执行渲染,否则不会
    this._updateBatchNumber = null;
    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      // 执行更新渲染,后面详细分析
      this._performComponentUpdate(
        nextParentElement,
        nextProps,
        nextState,
        nextContext,
        transaction,
        nextUnmaskedContext
      );
    } else {
      // shouldUpdate为false,则不会更新渲染
      this._currentElement = nextParentElement;
      this._context = nextUnmaskedContext;
      inst.props = nextProps;
      inst.state = nextState;
      inst.context = nextContext;
    }
},

updateComponent中,先调用componentWillReceiveProps,然后合并setState导致的state变化。然后调用shouldComponentUpdate判断是否需要更新渲染。如果需要,则调用_performComponentUpdate执行渲染更新,下面接着分析performComponentUpdate。

_performComponentUpdate: function(nextElement,nextProps,nextState,nextContext,transaction,
                                     unmaskedContext
  ) {
    var inst = this._instance;

    // 判断是否已经update了
    var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
    var prevProps;
    var prevState;
    var prevContext;
    if (hasComponentDidUpdate) {
      prevProps = inst.props;
      prevState = inst.state;
      prevContext = inst.context;
    }

    // render前调用componentWillUpdate
    if (inst.componentWillUpdate) {
      inst.componentWillUpdate(nextProps, nextState, nextContext);
    }

    // state props等属性设置到内部变量inst上
    this._currentElement = nextElement;
    this._context = unmaskedContext;
    inst.props = nextProps;
    inst.state = nextState;
    inst.context = nextContext;

    // 内部会调用render方法,重新解析ReactElement并得到HTML
    this._updateRenderedComponent(transaction, unmaskedContext);

    // render后调用componentDidUpdate
    if (hasComponentDidUpdate) {
      transaction.getReactMountReady().enqueue(
        inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
        inst
      );
    }
},

_performComponentUpdate会调用componentWillUpdate,然后在调用updateRenderedComponent进行更新渲染,最后调用componentDidUpdate。下面来看看updateRenderedComponent中怎么调用render方法的。

_updateRenderedComponent: function(transaction, context) {
    var prevComponentInstance = this._renderedComponent;
    var prevRenderedElement = prevComponentInstance._currentElement;
    
    // _renderValidatedComponent内部会调用render,得到ReactElement
    var nextRenderedElement = this._renderValidatedComponent();
    
    // 判断是否做DOM diff。React为了简化递归diff,认为组件层级不变,且type和key不变(key用于listView等组件,很多时候我们没有设置type)才update,否则先unmount再重新mount
    if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
      // 递归updateComponent,更新子组件的Virtual DOM
      ReactReconciler.receiveComponent(prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context));
    } else {
      var oldNativeNode = ReactReconciler.getNativeNode(prevComponentInstance);
      
      // 不做DOM diff,则先卸载掉,然后再加载。也就是先unMountComponent,再mountComponent
      ReactReconciler.unmountComponent(prevComponentInstance, false);

      this._renderedNodeType = ReactNodeTypes.getType(nextRenderedElement);
      
      // 由ReactElement创建ReactComponent
      this._renderedComponent = this._instantiateReactComponent(nextRenderedElement);

      // mountComponent挂载组件,得到组件对应HTML
      var nextMarkup = ReactReconciler.mountComponent(this._renderedComponent, transaction, this._nativeParent, this._nativeContainerInfo, this._processChildContext(context));

      // 将HTML插入DOM中
      this._replaceNodeWithMarkup(oldNativeNode, nextMarkup, prevComponentInstance);
    }
},
    
_renderValidatedComponent: function() {
    var renderedComponent;
    ReactCurrentOwner.current = this;
    try {
      renderedComponent =
        this._renderValidatedComponentWithoutOwnerOrContext();
    } finally {
      ReactCurrentOwner.current = null;
    }

    return renderedComponent;
},

_renderValidatedComponentWithoutOwnerOrContext: function() {
    var inst = this._instance;
    // 看到render方法了把,应该放心了把~
    var renderedComponent = inst.render();

    return renderedComponent;
},

    

和mountComponent中一样,updateComponent也是用递归的方式将各子组件进行update的。这里要特别注意的是DOM diff。DOM diff是React中渲染加速的关键所在,它会帮我们算出virtual DOM中真正变化的部分,并对这部分进行原生DOM操作。为了避免循环递归对比节点的低效率,React中做了假设,即只对层级不变,type不变,key不变的组件进行Virtual DOM更新。这其中的关键是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;
  // React DOM diff算法
  // 如果前后两次为数字或者字符,则认为只需要update(处理文本元素)
  // 如果前后两次为DOM元素或React元素,则必须在同一层级内,且type和key不变(key用于listView等组件,很多时候我们没有设置type)才update,否则先unmount再重新mount
  if (prevType === 'string' || prevType === 'number') {
    return (nextType === 'string' || nextType === 'number');
  } else {
    return (
      nextType === 'object' &&
      prevElement.type === nextElement.type &&
      prevElement.key === nextElement.key
    );
  }
}

4 销毁

前面提到过,更新组件时,如果不满足DOM diff条件,会先unmountComponent, 然后再mountComponent,下面我们来分析下unmountComponent时都发生了什么事。和mountComponent的多态一样,不同type的ReactComponent也会有不同的unmountComponent行为。我们来分析下React自定义组件,也就是ReactCompositeComponent中的unmountComponent。

  unmountComponent: function(safely) {
    if (!this._renderedComponent) {
      return;
    }
    var inst = this._instance;

    // 调用componentWillUnmount
    if (inst.componentWillUnmount && !inst._calledComponentWillUnmount) {
      inst._calledComponentWillUnmount = true;
      // 安全模式下,将componentWillUnmount包在try-catch中。否则直接componentWillUnmount
      if (safely) {
        var name = this.getName() + '.componentWillUnmount()';
        ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst));
      } else {
        inst.componentWillUnmount();
      }
    }

    // 递归调用unMountComponent来销毁子组件
    if (this._renderedComponent) {
      ReactReconciler.unmountComponent(this._renderedComponent, safely);
      this._renderedNodeType = null;
      this._renderedComponent = null;
      this._instance = null;
    }

    // reset等待队列和其他等待状态
    this._pendingStateQueue = null;
    this._pendingReplaceState = false;
    this._pendingForceUpdate = false;
    this._pendingCallbacks = null;
    this._pendingElement = null;

    // reset内部变量,防止内存泄漏
    this._context = null;
    this._rootNodeID = null;
    this._topLevelWrapper = null;

    // 将组件从map中移除,还记得我们在mountComponent中将它加入了map中的吧
    ReactInstanceMap.remove(inst);
  },

可见,unmountComponent还是比较简单的,它就做三件事

  1. 调用componentWillUnmount()
  2. 递归调用unmountComponent(),销毁子组件
  3. 将内部变量置空,防止内存泄漏

5 总结

React自定义组件创建期,存在期,销毁期三个阶段的生命周期调用上面都讲完了。三个入口函数mountComponent,updateComponent,unmountComponent尤其关键。大家如果有兴趣,还可以自行分析ReactDOMEmptyComponent,ReactDOMComponent和ReactDOMTextComponent的这三个方法。

深入学习React生命周期源码可以帮我们理清各个方法的调用顺序,明白它们都是什么时候被调用的,哪些条件下才会被调用等等。阅读源码虽然有点枯燥,但能够大大加深对上层API接口的理解,并体会设计者设计这些API的良苦用心。

目录
相关文章
|
16天前
|
前端开发 JavaScript
React 组件生命周期
React 组件生命周期
29 0
|
2月前
|
前端开发 JavaScript 开发者
介绍一下React生命周期
介绍一下React生命周期
88 9
|
19天前
|
存储 前端开发 JavaScript
深入理解React组件的生命周期与Hooks
【10月更文挑战第7天】深入理解React组件的生命周期与Hooks
54 0
|
3月前
|
前端开发 JavaScript
React的生命周期演示-新(12)
【8月更文挑战第15天】React的生命周期演示-新
41 5
React的生命周期演示-新(12)
|
3月前
|
前端开发 JavaScript
React的生命周期简介(十)
【8月更文挑战第15天】React的生命周期简介
43 2
React的生命周期简介(十)
|
2月前
|
前端开发 API UED
React组件生命周期详解
【9月更文挑战第4天】在React应用开发中,掌握组件生命周期对于管理状态和属性至关重要,并能有效提升应用性能。本文详细介绍了React组件生命周期的三个阶段:挂载、更新和卸载,并通过代码示例展示了如何避免状态更新导致的死循环及优化网络请求等问题,帮助开发者构建更高效、可维护的应用。
64 2
|
3月前
|
前端开发 JavaScript
React 组件的生命周期阶段详解
【8月更文挑战第30天】
44 7
|
3月前
|
前端开发 JavaScript
React 组件生命周期方法详解
【8月更文挑战第30天】
42 5
|
3月前
|
前端开发 JavaScript
React的生命周期演示-旧(11)
【8月更文挑战第15天】React的生命周期演示-旧(11)
30 3
|
3月前
|
前端开发 JavaScript 开发者
React生命周期方法完全指南:深入理解并高效应用每个阶段的钩子——从初始化到卸载的全方位解析
【8月更文挑战第31天】本文详细介绍了React组件生命周期方法,包括初始化、挂载、更新和卸载四个阶段的关键钩子。通过探讨每个阶段的方法,如`componentDidMount`和`componentWillUnmount`,帮助开发者在正确时机执行所需操作,提升应用性能。文章还提供了最佳实践,指导如何避免常见错误并充分利用最新钩子。
77 0