React生命周期详解

1 React生命周期流程

调用流程可以参看上图。分为实例化,存在期和销毁三个不同阶段。介绍生命周期流程的文章很多,相信大部分同学也有所了解,我们就不详细分析了。很多同学肯定有疑问,这些方法在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)

mountComponent

componentWillMount,render,componentDidMount都是在mountComponent中被调用。在
React源码分析2 — React组件插入DOM流程
一文中,我们讲过mountComponent被调用的时机。它是在渲染新的ReactComponent中被调用的。输入ReactComponent,返回组件对应的HTML。把这个HTML插入到DOM中,就可以生成组件对应的DOM对象了。所以mountComponent尤其关键。不同的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还是比较简单的,它就做三件事

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

5 总结

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339