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

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

前言

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

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

一、babel编译

很简单的一串代码,以下:github

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编译后以下:redux

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.bash

这样,咱们就明白了,为何首先会调用constructor了:constructor并非React组件的原型函数,而是babel编译后的一个构造函数。因此当实例化组件的时候,天然会先调用ES6中的constructor了。babel

二、consturcotr以后的顺序

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

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

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

图1:函数调用流程
图1:函数调用流程

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

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

所以:最后的顺序是constructor -> componentWillMount -> render -> componentDidMount

三、论证书中的两句话。

书中还提到一句话:

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

3.1 第一句话

最直观的从console.log来看

render返回的结构
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对象
最外层组件instantiateReactComponent对象

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

R instantiateReactComponent 的对象
R instantiateReactComponent 的对象

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

ReactDOMComponent组件
ReactDOMComponent组件

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

图2:函数调用流程
图2:函数调用流程

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

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

图3:TopLevelWrapper的render结构
图3:TopLevelWrapper的render结构

图4:R的render结构
图4:R的render结构

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

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

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

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

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

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

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

图5:markup
图5:markup

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

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

十二、ReactCompositeComponent里执行完performInitialMount以后,就会调用componentDidMount,看看他有没有componentDidMount这个函数。若是有的话,就会执行

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

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

1三、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);这句上。
这也是接下来须要研究的。

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

  • 一、ReactComponent群都是先render完以后, 统一作的componentDidMount
  • 二、 就是上面的,为何是先render -> DOM -> componentDidMount

最后

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

相关文章
相关标签/搜索