Que's Blog

WebFrontEnd Development

0%

react生命周期分析

目标

这一节分析一下react的声明周期

这里直接引用一下现有图片,图片来自这里

思考

这里需要认真思考一下的是:

  • 钩子函数在一个组件里是按怎样的数据形式保存的
  • 调用的时机在react内部是如何进行的
  • 钩子函数是在哪些模块内被触发调用的
  • 如何进行方便的调试和验证

步骤

我们还是依照测试文件直接debug。

目标文件为ReactComponentLifeCycle-test.js。这是react框架自带的lifecycle测试文件,我们不需要自己去写测试用例和DEMO。

初始化 Initialization

组件的props&&state初始化实质上和DOM无关,和更新相关生命周期也无关。

这里找到了ReactES6Class-test.js。使用下面的断言进行debug查看组件初始化环节。

1
2
3
4
5
6
7
8
9
10
11
12
it('renders based on state using initial values in this.props', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {bar: this.props.initialValue};
}
render() {
return <span className={this.state.bar} />;
}
}
test(<Foo initialValue="foo" />, 'SPAN', 'foo');
});

我们在super(props)这里打一个断点开始debug,断点到了之后step into。然后点下一步直接跳到了src/isomorphic/modern/class/ReactBaseClasses.js。不过ES6这里定义过于简单了(直接走了构造器将props和state挂在上面了)看不出太深入的,所以我们继续向下深入render。

1
2
3
4
5
6
7
8
function ReactComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}

这里断点放在render的return这一行,到了之后step into, 然后进入了src/isomorphic/classic/element/ReactElementValidator.js,光标放到line243点run to cursor,然后step into。这样就到了src/isomorphic/classic/element/ReactElement.js。

props这一块其实还是很容易理解,babel对jsx转换时候就将props传入了。

这个图其实说得很清晰了,我们主要分析state的处理。

getInitialState这个发生在组件产生之后,挂载之前。它是在mountComponentIntoNode中,具体一点的话,它的调用路径是这样的:

1
2
3
4
5
mountComponentIntoNode
-> ReactReconciler.mountComponent
-> internalInstance.mountComponent
-> ReactCompositeComponent.mountComponent
-> ReactCompositeComponent._constructComponent

之前对这个render是如何渲染到dom有过分析,完整的流程是这样的:

1
2
3
4
5
6
7
ReactMount.render
-> ReactMount._renderSubtreeIntoContainer
-> ReactMount._renderNewRootComponent
-> ReactUpdates.batchedUpdates
-> ReactUpdates.batchedMountComponentIntoNode
-> mountComponentIntoNode
-> ReactMount._mountImageIntoNode

具体一些来说,我们的分析环节是在mountComponentIntoNode中,排除ReactMount._mountImageIntoNode调用之外的地方。

回顾一下

mountComponentIntoNode做了这些事:

  • 调用ReactReconciler.mountComponent生成markup标记内部含有{children: markup数组, node:HTMLElement}
  • 调用ReactMount._mountImageIntoNode渲染markup

ReactReconciler.mountComponent生成markup这一节在之前分析过,详细看之前的分析,这里只就关键的引用代码instantiateReactComponent做说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function instantiateReactComponent(node, shouldHaveDebugID) {
var instance;
if (node === null || node === false) {
instance = ReactEmptyComponent.create(instantiateReactComponent);
} else if (typeof node === 'object') {
var element = node;
var type = element.type;
if (typeof type !== 'function' && typeof type !== 'string') {
var info = '';
info += getDeclarationErrorAddendum(element._owner);
invariant(
false,
'Element type is invalid: expected a string (for built-in components) ' +
'or a class/function (for composite components) but got: %s.%s',
type == null ? type : typeof type,
info,
);
}

// Special case string values
if (typeof element.type === 'string') {
instance = ReactHostComponent.createInternalComponent(element);
} else if (isInternalComponentType(element.type)) {
instance = new element.type(element);
if (!instance.getHostNode) {
instance.getHostNode = instance.getNativeNode;
}
} else {
instance = new ReactCompositeComponentWrapper(element);
}
} else if (typeof node === 'string' || typeof node === 'number') {
instance = ReactHostComponent.createInstanceForText(node);
} else {
invariant(false, 'Encountered invalid React node of type %s', typeof node);
}

instance._mountIndex = 0;
instance._mountImage = null;
return instance;
}

之前我们说到过instance = ReactHostComponent.createInternalComponent(element);的情况,然而我们关注的getInitialState,事情恰好发生在instance = new ReactCompositeComponentWrapper(element);

回顾instantiateReactComponent,看看ReactCompositeComponentWrapper的定义

1
2
3
4
5
6
7
8
9
10
var ReactCompositeComponentWrapper = function(element) {
this.construct(element);
};
Object.assign(
ReactCompositeComponentWrapper.prototype,
ReactCompositeComponent,
{
_instantiateReactComponent: instantiateReactComponent,
},
);

此时,生成markup的代码实质上是调用ReactCompositeComponent.mountComponent

1
2
3
4
5
6
7
var markup = internalInstance.mountComponent(
transaction,
hostParent,
hostContainerInfo,
context,
parentDebugID,
);

然而这个例子这里存在internalInstance.mountComponent被调用了两次的问题

第一次渲染

第一次渲染是TopLevelWrapper组件的渲染

在ReactCompositeComponent.mountComponent节点的函数中,可以看到inst.state = initialState = null

而这个inst是_constructComponent的返回值,所以最终的定义就在这个函数内部了,重点跟踪一下这个引用好了。

这个函数内部引用了ReactCompositeComponent._constructComponentWithoutOwner,这里核心代码引用到了Component(publicProps, publicContext, updateQueue) ,
这里的Component是_currentElement.type的引用,debugger断点打过来,最简单的元素初始渲染这里是TopLevelWrapper实例(同时也是Component实例)。
可以看到此时initialState === undefined,所以它最后被赋值为null了。

第一次定义state这里 仅仅相当于是一个初始化流程。最终定义还是看后面的定义

第二次渲染

到了第二次时候 也就是performInitialMount内执行到

1
2
3
4
5
6
7
8
var markup = ReactReconciler.mountComponent(
child,
transaction,
hostParent,
hostContainerInfo,
this._processChildContext(context),
debugID,
);

这一块代码时候触发的(关于这个第一次和第二次这个,可以参考后面提到的『jsx到js的转换 』)。

此时child是ReactCompositeComponentWrapper实例所以markup生成还是调用ReactCompositeComponent.mountComponent。

这时候就是我们的Foo组件了。这里获取实例使用了ReactReconciler._constructComponent,它又调用了_constructComponentWithoutOwner。它大致代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_constructComponentWithoutOwner: function(
doConstruct,
publicProps,
publicContext,
updateQueue,
) {
var Component = this._currentElement.type;
if (doConstruct) {
if (__DEV__) {
// ... 略
} else {
return new Component(publicProps, publicContext, updateQueue);
}
}
},

我们在之前代码里面可以找到this._currentElement = element,所以它实质上调用的是ReactElement实例的type属性。这个属性在这里对应的是我们的Foo函数。而Foo在构造器里面对state做了定义。这里就是state数据被录入的根源环节

但是Initialization此刻还没有完毕。继续往下走,直到ReactCompositeComponent.js line252-320这几行都执行完毕,我们的Initialization环节才正式完毕

1
2
3
4
5
6
7
8
9
10
11
12
13
inst.props = publicProps;
inst.context = publicContext;
inst.refs = emptyObject;
inst.updater = updateQueue;

this._instance = inst;

ReactInstanceMap.set(inst, this);

var initialState = inst.state;
if (initialState === undefined) {
inst.state = initialState = null;
}

by the way,我们再往下走一些,可以看到

1
2
3
4
5
6
7
if (inst.componentDidMount) {
if (__DEV__) {
// 略略略...
} else {
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
}
}

是的componentDidMount在初始化完毕后就接着被调用了。

PS:上面提到了type属性,这里再次温习一下jsx到js的转换防止一知半解。

jsx到js的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Bar extends React.Component {
constructor(props) {
super(props);
}
render() {
return <div>{this.props.name}</div>
}
}
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {bar: this.props.initialValue};
}
render() {
return <span className={this.state.bar}>
<Bar name="test"/>
</span>;
}
}

ReactDOM.render(
<Foo className="test" />,
mountNode
)

// 此时<Foo className={this.state.bar} />在大体的结构上会转换成:
React.createElement( // a
React.createElement( // b
"span",
{ className: this.state.bar },
React.createElement( // c
React.createElement( // d
"div",
null,
this.props.name
)
, { name: "test" })
),
{ className: "test" }
}

这里需要注意的是:

  • React.createElement返回值是一个ReactElement对象, 但是它本身则是一个Function
  • 对 a|b|c标记的createElement来说,他们的返回值的type是一个Function,对应的是组件的定义函数
  • 对 a|b|c标记的createElement来说,他们的第一个参数并不真的就是直接传入React.createElement这个函数,而是组件的定义函数的转译(bable处理),它里面一定会有一个render,render里面会调用React.createElement()返回一个ReactElement对象
  • 而对于d标记的createElement来说,它的返回值是一个ReactElement对象。

关于这个render的调用。参见ReactCompositeComponent.performInitialMount

1
2
3
4
// If not a stateless component, we now render
if (renderedElement === undefined) {
renderedElement = this._renderValidatedComponent();
}

一路跟进引用最终你会发现render在这里被调用了。

挂载 Mounting

componentWillMount

这个函数在ReactCompositeComponent.performInitialMount中

1
2
3
4
5
6
7
8
9
10
11
12
if (inst.componentWillMount) {
if (__DEV__) {
// 略略略...
} else {
inst.componentWillMount();
}
// When mounting, calls to `setState` by `componentWillMount` will set
// `this._pendingStateQueue` without triggering a re-render.
if (this._pendingStateQueue) {
inst.state = this._processPendingState(inst.props, inst.context);
}
}

componentDidMount

这个不再废话,参见上文的『第二次渲染』。ReactCompositeComponent.mountComponent中

render

render这个也不再说了,参见『 jsx到js的转换』。ReactCompositeComponent.performInitialMount中,更细致一些,那就是在ReactCompositeComponent._renderValidatedComponent中。

更新 Updation

更新这个环节的钩子函数需要配合分析: 当收到新的props、state时,会发生什么?

不然就会发生这些钩子函数好找,但是何时会被触发却不明确的问题。

我们之前分析过render的流程,所以这里顺便也分析一下,update的流程。

State更新

观察State更新需要对这个更新是怎样被唤起的,所以会分为之前和之后两个环节。

Before 更新之前

这里使用这个测试用例(ReactES6Class-test.js里面)来测试观察state更新环节来对整体逻辑做初步概览。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
it('setState through an event handler', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {bar: props.initialValue};
}
handleClick() {
this.setState({bar: 'bar'});
}
render() {
return (
<Inner name={this.state.bar} onClick={this.handleClick.bind(this)} />
);
}
}
test(<Foo initialValue="foo" />, 'DIV', 'foo');
attachedListener();
expect(renderedName).toBe('bar');
});

这个setState的调用的定义在:

1
2
3
4
5
6
7
ReactComponent.prototype.setState = function(partialState, callback) {
// 略略略...
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};

step in方式进入了ReactUpdateQueue.enqueueSetState,进行了一些验证后,将这个state纯对象push进入了internalInstance._pendingStateQueue数组,然后调用ReactUpdates.enqueueUpdate(internalInstance)来更新这个组件。

1
2
3
4
5
6
7
8
9
10
11
function enqueueUpdate(component) {
ensureInjected();
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}

这个函数的调用有些绕而且看起来必要性不高因为ReactDefaultBatchingStrategy.batchedUpdates在此时必然会执行到transaction.perform(callback, null, a, b, c, d, e)——实质上它是callback(a, b, c, d, e)的事务版本,执行有前后置条件。最后调用自身将component推到dirtyComponents进行更新标记

1
2
3
4
5
6
7
8
9
10
batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
// The code is written this way to avoid extra allocations
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e);
}
}

在这个debug环节,transaction.perform基本就是一个大体的调用终点了。But,这个更新Component的调用去哪儿了?

这里不得不说事务概念了,ReactDefaultBatchingStrategy的事物实例是有两个事务注册的:

1
2
3
4
5
6
7
8
9
10
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function() {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
},
};
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

我们都更新环节就是在ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)这里。

1
2
3
4
5
6
7
8
9
10
var flushBatchedUpdates = function() {
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
}
// 略略略...
}
};

这样,当我们处理好了函数执行之后,组件也会随之被runBatchedUpdates更新。这其中还有很多细节,但是这里仅仅做好整理逻辑梳理即可,暂时不做细致分析(但是很显然,相关更新的钩子函数会在里面)。

After 正式开始

继续上一步分析。在这个例子中,执行到runBatchedUpdates后,这里的更新最终会执行到ReactCompositeComponent.updateComponent。所以后面的生命周期函数都会在这里一一展现。这里回顾一下更新环节生命周期的顺序。

1
2
3
4
shouldComponentUpdate
-> componentWillUpdate
-> render
-> componentDidUpdate

shouldComponentUpdate

ReactCompositeComponent line 838-860

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (!this._pendingForceUpdate) {
if (inst.shouldComponentUpdate) {
if (__DEV__) {
// 略略略...
} else {
shouldUpdate = inst.shouldComponentUpdate(
nextProps,
nextState,
nextContext,
);
}
} else {
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate =
!shallowEqual(prevProps, nextProps) ||
!shallowEqual(inst.state, nextState);
}
}
}

这一步生命周期完成之后我们进入了ReactCompositeComponent._performComponentUpdate(具体流程参见ReactCompositeComponent.updateComponent)

componentWillUpdate

ReactCompositeComponent line 955-965

1
2
3
4
5
6
7
if (inst.componentWillUpdate) {
if (__DEV__) {
// 略略略...
} else {
inst.componentWillUpdate(nextProps, nextState, nextContext);
}
}

render

ReactCompositeComponent line 973

1
this._updateRenderedComponent(transaction, unmaskedContext);

componentDidUpdate

ReactCompositeComponent line 975-1002

这一步还是很好理解,如果有定义这个函数,那么就通过事务机制再render完成之后执行componentDidUpdate。至此State更新环节的流程就完整、畅通了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (hasComponentDidUpdate) {
if (__DEV__) {
// 略略略...
} else {
transaction
.getReactMountReady()
.enqueue(
inst.componentDidUpdate.bind(
inst,
prevProps,
prevState,
prevContext,
),
inst,
);
}
}

Props更新

props引起的更新可能会不太容易理解。

setState这个是一个函数理解它的触发没有太大难度。这里回顾一下props和state的区别: state是组件内部的状态,是可以修改;而props是父组件上传下来的,他不能在组件内部被修改。

Before 更新之前

为了辅助说明,这里还是使用上面提到过的测试用例,但是我们这里换个角度来看这个用例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
it('setState through an event handler', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {bar: props.initialValue};
}
handleClick() {
this.setState({bar: 'bar'});
}
render() {
return (
<Inner name={this.state.bar} onClick={this.handleClick.bind(this)} />
);
}
}
test(<Foo initialValue="foo" />, 'DIV', 'foo');
attachedListener();
expect(renderedName).toBe('bar');
});

稍微分析一下这个测试用例,可以看到Inner组件在handleClick被触发以后,它的this.props.name的值从foo变为了bar。所以我们这里重点分析一下,这个变化是如何最终反应到DOM上的。

在一切开始之前,我们还是得回顾,这个this.props.name到底是如何传递到Inner组件上的——this.setState,是的,就是this.setState。

回顾一下state的钩子函数触发顺序:

1
2
3
4
shouldComponentUpdate
-> componentWillMount
-> render
-> componentDidUpdate

实际上此时,对于Inner来讲,它在State钩子函数的render环节,我们就开始了一轮Props的更新生命周期函数

在这个环节中,有几个特别关键的地方值得深思(理解它有助于深入理解React)。

  • 『jsx到js的转换』 环节里面jsx 转换为React.createElement的结构,和提到的相关的注意项
  • React.createElement嵌套的层级问题,它不仅仅可以从TopLevelWrapper开始渲染,实际上,他可以从任意中间的任何一个React.createElement开始渲染
  • 为什么说React是最小局部更新?除了调度和算法的作用,React.createElement嵌套结构可以从任何环节开始向下级渲染,是一个非常关键的机制,它极大程度上缩小了需要更新到DOM的数据。

以上可能是最重要的,如果没有理解上面的那么值得卡在这里琢磨。

After 正式开始

如果上面的准备就绪了可以继续下面的了。回顾一下,当state触发的生命周期开始render环节的时候,会执行ReactCompositeComponent line 973的代码这里仅仅是比State多一个componentWillReceiveProps。

1
this._updateRenderedComponent(transaction, unmaskedContext);

这里直接在测试用例的attachedListener()这一行打一个断点,待它停住以后,在render里面打一个断点,最后点执行。当它再次停住以后,这样我们就捕获了state之后的render环节。最后将光标定位到ReactCompositeComponent line 973,点run to corsur,我们就对程序进行了精准的捕获。

接下来它的调用栈:

1
2
3
4
ReactCompositeComponent._updateRenderedComponent
-> ReactReconciler.receiveComponent
-> internalInstance.receiveComponent
-> ReactCompositeComponent.updateComponent

到这个环节生命周期函数的调用就简单多了。先看整体的生命周期:

1
2
3
4
5
componentWillReceiveProps
->shouldComponentUpdate
-> componentWillUpdate
-> render
-> componentDidUpdate

componentWillReceiveProps

ReactCompositeComponent.updateComponent内, ReactCompositeComponent.js line 823-833

1
2
3
4
5
6
7
if (willReceive && inst.componentWillReceiveProps) {
if (__DEV__) {
// 略
} else {
inst.componentWillReceiveProps(nextProps, nextContext);
}
}

shouldComponentUpdate

ReactCompositeComponent.updateComponent内, ReactCompositeComponent.js line 838-860

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (!this._pendingForceUpdate) {
if (inst.shouldComponentUpdate) {
if (__DEV__) {
// 略
} else {
shouldUpdate = inst.shouldComponentUpdate(
nextProps,
nextState,
nextContext,
);
}
} else {
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate =
!shallowEqual(prevProps, nextProps) ||
!shallowEqual(inst.state, nextState);
}
}
}

componentWillUpdate

ReactCompositeComponent.updateComponent 调用了ReactCompositeComponent._performComponentUpdate。

这个生命周期执行在这个函数内, 代码定位line955-965

1
2
3
4
5
6
7
if (inst.componentWillUpdate) {
if (__DEV__) {
// 略
} else {
inst.componentWillUpdate(nextProps, nextState, nextContext);
}
}

render

ReactCompositeComponent._performComponentUpdate内

1
this._updateRenderedComponent(transaction, unmaskedContext);

componentDidUpdate

ReactCompositeComponent._performComponentUpdate内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (hasComponentDidUpdate) {
if (__DEV__) {
// 略
} else {
transaction
.getReactMountReady()
.enqueue(
inst.componentDidUpdate.bind(
inst,
prevProps,
prevState,
prevContext,
),
inst,
);
}
}

卸载

卸载这个环节的生命周期就比较好理解了。

我们的render环节会调用一个ReactCompositeComponent._updateRenderedComponent。

这个函数又一个非常显眼的逻辑分支。

1
2
3
4
5
6
7
8
9
10
11
12
if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
ReactReconciler.receiveComponent(
prevComponentInstance,
nextRenderedElement,
transaction,
this._processChildContext(context),
);
} else {
var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
ReactReconciler.unmountComponent(prevComponentInstance, false);
// 略
}

ReactReconciler.receiveComponent这个方法里面调用了internalInstance.unmountComponent(safely),它可以对各种组件实例的unmountComponent进行调用。

常规来说,我们最常用到的是是ReactCompositeComponent.unmountComponent。其中内容不再赘述。

总结

至此整个React的声明周期分析也算完整了。我们整体的分析了初次渲染、更新、卸载过程中的生命周期函数式如何被调用、在哪里调用,以及涉及到相关知识点,乃至如何进行方便的调试。