React组件:为什么调用顺序是constructor -> willMount -> render -> DidMount

本文深入探讨React组件的生命周期,特别是从constructor到componentDidMount的全过程。通过源码分析揭示了render函数的作用及其与DOM更新的关系。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

虽然常用React、redux编写SPA,但是这一块是如何运作,应该如何优化,还是比较困扰,最近开始阅读程墨的《深入浅出React和Redux》,结合之前读过的React源码和相关源码的文章后,打算从源码的角度,解释下书中的一些内容。

前言

书中有一段话,关于组件从初始化到挂载经过的声明周期:
流程:

  • 1、constructor
  • 2、componentWillMount
  • 3、render
  • 4、componentDidMount
    从流程上来看,发现了以前我的想当然错了!就是render是在componentDidMount之前调用的!!怪不得!每次在componentDidMount里调用异步的时候,render里面的object.xxx从报错!原来在componentDidMount之前,已经render过一次,而这个是后object是个null
    那么现在,我们从React源码上来看看,为什么是这样一个顺序

1、babel编译

很简单的一串代码,如下:

class R extends Component {
  constructor(props) {
    super(props);
    console.log('constructor');
    this.state = {value: props.value}
  }
  componentWillMount() {
    console.log('will mount');
  }
  componentDidMount() {
    console.log('did mount');
  }
  render() {
    console.log('render');
    return (<div>{this.state.value}</div>);
  }
}复制代码

当然,浏览器暂时还是没法识别ES6的语法的,所以需要通过babel编译。经过babel编译后如下:

var R = function (_Component) {
  _inherits(R, _Component);

  function R(props) {
    _classCallCheck(this, R);

    var _this = _possibleConstructorReturn(this, (R.__proto__ || Object.getPrototypeOf(R)).call(this, props));

    console.log('constructor');
    _this.state = { value: props.value };
    return _this;
  }

  _createClass(R, [{
    key: 'componentWillMount',
    value: function componentWillMount() {
      console.log('will mount');
    }
  }, {
    key: 'componentDidMount',
    value: function componentDidMount() {
      console.log('did mount');
    }
  }, {
    key: 'render',
    value: function render() {
      console.log('render');
      return React.createElement(
        'div',
        null,
        this.state.value
      );
    }
  }]);

  return R;
}(Component);复制代码

从上面来看,其实我比较开心且兴奋的看到了闭包*原型、继承,不清楚的小伙伴可以github.com/mqyqingfeng… 这篇文章补补。
再来看看constructor,从编译后的代码来看constructor并不是React原型的某个方法,而是babel转译后的这个下面的这块函数。

function R(props) {
    _classCallCheck(this, R);

    var _this = _possibleConstructorReturn(this, (R.__proto__ || Object.getPrototypeOf(R)).call(this, props));

    console.log('constructor');
    _this.state = { value: props.value };
    return _this;
}复制代码

根据上面复习的原型、继承, 这是个构造函数。super对应的是_possibleConstructorReturn.

这样,我们就明白了,为什么首先会调用constructor了:constructor并不是React组件的原型函数,而是babel编译后的一个构造函数。所以当实例化组件的时候,自然会先调用ES6中的constructor了。

2、consturcotr之后的顺序

组件是如何插入到DOM中呢?先看看ES代码:

ReactDOM.render(
    <R />,
    document.getElementById('example')
);复制代码

调用的是ReactDOM.render追踪源码,实际调用的是ReactMount.render。一层层追踪后如下图所示:

图1:函数调用流程

图片中黄色数字标识:步骤顺序。蓝色框表示里面剩下的步骤都是在performInitialMount完成的。
步骤按深度优先方式看。

从图中,我们能看到React组件周期,最早开始与performInitialMount这个函数里。其次是render函数,当执行完performInitialMount后,跳出环境栈,接着执行componentDidMount函数。

因此:最后的顺序是constructor -> componentWillMount -> render -> componentDidMount

3、论证书中的两句话。

书中还提到一句话:

  • render函数并不做实际的渲染动作,他只是返回一个JSX描述的结构。
  • render函数被调用完之后,componentDidMount函数并不是会被立刻调用。componentDidMount被调用的时候,render函数返回的东西已经引发了渲染,组件已经被『装载』到了DOM树上

3.1 第一句话

最直观的从console.log来看

render返回的结构

这么一看。好吧,这是一个 ReactElement。原来 render返回的jsx的结构就是个 ReactElement。其实,从babel那翻译过来的js也能看出, render返回的是一个 ReactElement

{
    key: 'render',
    value: function render() {
      console.log('render');
      return React.createElement(
        'div',
        null,
        this.state.value
  );
}复制代码

3.2 第二句话

想要解释第二句话,我们得更加仔细的分析源码:

class R extends Component {
  constructor(props) {
    super(props);
    console.log('constructor');
    this.state = {value: props.value}
  }
  componentWillMount() {
    console.log('will mount');
  }
  componentDidMount() {
    console.log('did mount');
  }
  render() {
    console.log('render');
    return (<div>{this.state.value}</div>);
  }
}
ReactDOM.render(
    <R />,
    document.getElementById('example')
);复制代码

首先,React会在R上面再包裹一层,叫做_topLevelWrapper的层,这个对象创建出来的是一个ReactCompositeComponentWrapper对象,他基于ReactCompositeComponent,如下:

var ReactCompositeComponentWrapper = function (element) {
  this.construct(element);
};
_assign(ReactCompositeComponentWrapper.prototype, ReactCompositeComponent.Mixin, {
  _instantiateReactComponent: instantiateReactComponent
});复制代码

instantiateReactComponent打日志,得到如下:

最外层组件instantiateReactComponent对象

最外层组件的TopLevelWrapper是个null,_renderedComponent是组件R,在下一步:

R instantiateReactComponent 的对象

第二个是打印出来的R组件对应的ReactComponent对象,图中能看到这个组件的下一个组件是ReactDOMComponent。最后的打印出来的如下:

ReactDOMComponent组件

在图1上稍微完善了下,另外一个维度的大致调用流程如下图:

图2:函数调用流程

为什么说大致调用流程图,因为,因为在this.performInitialMount函数里有一个递归的过程。然后React组件除了ReactCompositeComponent类之外,还有上面提到的ReactDOMComponentReactEmptyComponent和另外一个内部类(这个类笔者不知道)。而图中,我们只是画了一个ReactCompositeComponent

接下来,我们就说说这个的调用流程把:
ReactMount内部的调用方式,图上自认为画的已经是相当清楚了,所以这里不做详细说明。
从图中第3.1步骤开始说起:
1、在ReactMount中的mountComponentIntoNode里,调用了ReactReconciler.mountComponent方法,方法中的wrapperInstance这个时候,是ReactCompositeComponentWrapper对象。返回一个markup(暂时不说)。那我们看看ReactReconciler.mountComponent这个方法是什么。
2、在ReactReconciler.mountComponent方法中,调用了是internalInstance.mountComponent方法,也就是说我们调用了『第1步』中的ReactCompositeComponentWrapper对象的mountComponent方法,那我们再去ReactCompositeComponentWrapper.mountComponent看看。
3、ReactCompositeComponent笔者理解为是一个组合组件,什么是组合组件呢,自认为就是把ReactDOMComponentReactEmptyComponent这两种包含到一起的组件。。。扯远了,看看ReactCompositeComponent.mountComponent做了什么把。他调用了一个performInitialMount方法。
4、那performInitialMount方法干了个啥???进去一看!重点来了!!他先看看组件有没有componentWillMount呀~有的话就调用。然后跳进_renderValidatedComponent这个函数中去了!。好吧,那我们就去_renderValidatedComponent这个函数中看看。
5、一看发现,_renderValidatedComponent这个函数又调用了_renderValidatedComponentWithoutOwnerOrContext方法。
6、那就进去看看把,一看便知,_renderValidatedComponentWithoutOwnerOrContext调用了组件的render方法。var renderedComponent = inst.render();,一看,render有个返回值!!!那这个返回值是什么呢!!!打出来一看,图3、4:

图3:TopLevelWrapper的render结构

图4:R的render结构

好吧,这有验证了第三节开头说的第一句话

render函数并不做实际的渲染动作,他只是返回一个JSX描述的结构(ReactElement)

7、带着这个ReactElement结构,我们跳出了_renderValidatedComponentWithoutOwnerOrContext函数,返回了『第5步』后,在_renderValidatedComponent中,继续返回了这个结构,返回到了第4步中的方法performInitialMount中,执行到_renderValidatedComponent之后。执行下面一句话:

this._renderedComponent = this._instantiateReactComponent(renderedElement);复制代码

8、看过代码的人,很清楚了_instantiateReactComponent这个函数的主要作用就是根据不同ReactElement,返回不同类型的ReactComponent。接下来呢,在performInitialMount又执行了

ReactReconciler.mountComponent(this._renderedComponent, .......);复制代码

好了,我们又开始了『第二步』的流程。但是注意,这个时候,还是在当前这个组件的函数栈中。
9、假设,我们renderedElement返回结构是图4的结构,这个时候的this._renderedComponent那就是ReactDOMComponent对象。那这时,ReactReconciler.mountComponent里就会调用ReactDOMComponent.mountComponent
10、ReactDOMComponent.mountComponent返回的是一个图5结构的一样的东东(后面会叫他markup)。

图5:markup

瞅着样子,感觉就是和 ReactElementReactCompositeComponent就是不一样,感觉亲切多了!!废话少说!

11、既然『第10步』的ReactDOMComponent.mountComponent调用完了,我们就返回到『第9步』performInitialMount里,一看!执行完了,返回就是图5的markup。那么,这个函数出栈,回到『第3步』。

12、ReactCompositeComponent里执行完performInitialMount之后,就会调用componentDidMount,看看他有没有componentDidMount这个函数。如果有的话,就会执行

transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);复制代码

(通篇来看,这个调用的频率还是挺高的,具体做什么,以后再说)

13、ReactCompositeComponent.mountComponent之后,ReactCompositeComponent.mountComponent函数出栈,回到『第一步』:mountComponentIntoNode。返回markup,之后。调用React._mountImageIntoNode函数。这个函数里,匆匆扫了一下关键字:发现,就是将『图5』的markup结构,转化成了对应的html结构。里面有一个笔者认为的这么说的点睛之笔是setInnerHTML

综上所属:
我们可以得出的结论是:

  • constructor -> componentWillMount -> render -> componentDidMount
  • render函数返回的是一个jsx的ReactElement结构。

至于第三个:

render函数被调用完之后,componentDidMount函数并不是会被立刻调用。componentDidMount被调用的时候,render函数返回的东西已经引发了渲染,组件已经被『装载』到了DOM树上。

笔者还得看看~~ 不过相信,这句话肯定是对的!要不然,怎么会出书,要不然为什么叫做componentDidMount,函数字面意思,就是render之后,先插入DOM,再调用componentDidMount函数。看来,关键的地方在transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);这句上。
这也是接下来需要研究的。

限于文章不能写太长。先暂时这样,抛出几个待讨论的点,省的忘记。

  • 1、ReactComponent群都是先render完之后, 统一做的componentDidMount
  • 2、 就是上面的,为什么是先render -> DOM -> componentDidMount

最后

感觉很多话说的很重复,但是笔者就是想从不同角度说地更仔细点。文章栗子有限,说的只是大概。如有说不明白的或者说错的地方,麻烦指出。因为,笔者是一个热爱学习,追求进步的北京打工的外地人!!!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值