虽然常用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
。一层层追踪后如下图所示:
图片中黄色数字标识:步骤顺序。蓝色框表示里面剩下的步骤都是在performInitialMount
完成的。
步骤按深度优先方式看。
从图中,我们能看到React组件周期,最早开始与performInitialMount
这个函数里。其次是render函数,当执行完performInitialMount
后,跳出环境栈,接着执行componentDidMount
函数。
因此:最后的顺序是constructor
-> componentWillMount
-> render
-> componentDidMount
3、论证书中的两句话。
书中还提到一句话:
render
函数并不做实际的渲染动作,他只是返回一个JSX描述的结构。render
函数被调用完之后,componentDidMount
函数并不是会被立刻调用。componentDidMount
被调用的时候,render
函数返回的东西已经引发了渲染,组件已经被『装载』到了DOM树上
3.1 第一句话
最直观的从console.log
来看
这么一看。好吧,这是一个
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
打日志,得到如下:
最外层组件的TopLevelWrapper是个null,_renderedComponent
是组件R
,在下一步:
第二个是打印出来的R组件对应的ReactComponent对象,图中能看到这个组件的下一个组件是ReactDOMComponent
。最后的打印出来的如下:
在图1上稍微完善了下,另外一个维度的大致调用流程如下图:
为什么说大致调用流程图,因为,因为在this.performInitialMount
函数里有一个递归的过程。然后React组件除了ReactCompositeComponent
类之外,还有上面提到的ReactDOMComponent
、ReactEmptyComponent
和另外一个内部类(这个类笔者不知道)。而图中,我们只是画了一个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
笔者理解为是一个组合组件,什么是组合组件呢,自认为就是把ReactDOMComponent
、ReactEmptyComponent
这两种包含到一起的组件。。。扯远了,看看ReactCompositeComponent.mountComponent
做了什么把。他调用了一个performInitialMount
方法。
4、那performInitialMount
方法干了个啥???进去一看!重点来了!!他先看看组件有没有componentWillMount
呀~有的话就调用。然后跳进_renderValidatedComponent
这个函数中去了!。好吧,那我们就去_renderValidatedComponent
这个函数中看看。
5、一看发现,_renderValidatedComponent
这个函数又调用了_renderValidatedComponentWithoutOwnerOrContext
方法。
6、那就进去看看把,一看便知,_renderValidatedComponentWithoutOwnerOrContext
调用了组件的render
方法。var renderedComponent = inst.render();
,一看,render有个返回值!!!那这个返回值是什么呢!!!打出来一看,图3、4:
好吧,这有验证了第三节开头说的第一句话
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)。
瞅着样子,感觉就是和
ReactElement
和
ReactCompositeComponent
就是不一样,感觉亲切多了!!废话少说!
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
最后
感觉很多话说的很重复,但是笔者就是想从不同角度说地更仔细点。文章栗子有限,说的只是大概。如有说不明白的或者说错的地方,麻烦指出。因为,笔者是一个热爱学习,追求进步的北京打工的外地人!!!!!!