准备工作
这篇文章本来准备作为一篇笔记,不准备发到博客中来。但简单写完后想起自己初次接触react源码时像无头苍蝇一样到处乱撞,中间绕了很大的圈子才渐渐找到感觉,这里把自己近期看到的好文章及心得总结出来,希望能让像我一样的react源码“初学者”少走一些弯路。
精品文章
react-in-depth系列,这个是目前为止我看到的关于
react 源码写的最好的几篇文章,值得反复阅读。原文需要翻墙才能打开,知乎里也有人翻译了这几篇文章。The how and why on React’s usage of linked list in Fiber to walk the component’s tree,Inside Fiber: in-depth overview of the new reconciliation algorithm in React,In-depth explanation of state and props update in React
知乎文章,暂时推荐一篇,也可以搜索其他文章看一看。
Dan Abramov的博客,这个人不用说是谁了吧。
其他。
前端进阶之道,不错的一个入门解析
阅读本篇
阅读本篇文章之前,最好先阅读以上推荐文章,先对Fiber、ExpirationTime、workLoop等等概念有个大致的认识。
调试代码
1 | //html |
初始化流程
render阶段
ReactDOM.render(<App />, 'root')最终返回了HostRootFiber下的第一个子节点的实例,即App组件的instance。但在返回实例之前,react先创建了一个基础数据结构,挂载在了root DOM节点上。1
2
3
4
5
6
7
8
9
10root._internalRootContainer = {
_internalRoot: {
finishedWork: null,
current: HostRootFiber,
containerInfo: div#root,
}
};
ReactNode._internalRoot = FiberRoot;
FiberRoot.current = HostRootFiber;
HostRootFiber.stateNode = FiberRoot;结构有了,需要
updateContainer(),把fiber树建立起来。在进入workLoop()之前,都需要从当前操作的fiber节点回溯到FiberRoot,沿途更新父fiber的childExpirationTime,标记子fiber是否需要更新。(ExpirationTimer越大,优先级越高)。1
2const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
prepareFreshStack(root, expirationTime);同时,还需要通过
prepareFreshStack(root, expirationTime)方法,将workInProgress置为HostRootFiber,所以,不论初始化还是更新,workLoop()都是从HostRootFiber开始。第一次循环,
workInProgress (又称unitOfWork)是HostRootFiber,从performUnitOfWork()进入beginWork(),当前workInProgress.tag为HostRoot,走updateHostRoot()方法。这里,最重要的是调用了reconcilChildren(current, workInProgress, nextChildren),nextChildren是代表<App />的一个ReactElement,就是一个普通的对象,内部的props属性为空。1
2
3
4
5
6
7nextChildren = {
$$typeof: Symbol(react.element)
key: null
props: {}
ref: null
type: ƒn App(props)
}reconcilChildren做了一件事,使用传入的ReactElement生成一个fiberNode,并与workInProgress-fiber链接起来。1
2workInProgress.child = fiber;
fiber.return = workInProgress;最后,本次循环
beginWork返回了workInProgress.child,即App-fiber。进入下一个循环。此时,
workInProgress指向了App-fiber。这里要明白一点,此时我们并没有div.container、button、span的fiber节点,App-fiber的child为null。循环从performUnitOfWork()进入beginWork()。当前
workInProgress.tag为ClassComponent,走updateClassComponent()方法。初始化时,Class App还没有实例,要先constructor和mount组件类。a.
constructClassInstance(),执行类的constructor()创建实例instance,同时super(props)调用父类Component的构造函数,之后将this.updater指向classComponentUpdater对象,内有关键的enqueueSetState方法,用来之后更新组件。将App-fiber的stateNode指向instance。1
2
3
4
5
6
7
8
9
10
11
12
13const instance = new ctor(props, context); //new App(props, context)
instance.updater = classComponentUpdater;
workInProgress.stateNode = instance;
const classComponentUpdater = {
enqueueSetState(inst, payload, callback) {
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
}
};
//实例调用this.setState(payload, callback),实际上调用了Component.prototype.setState,这个原型方法内部直接调用了 this.updater.enqueueSetState(),也就是实例上的updater提供的方法。b.
mountClassInstance(),为instance初始化props、state、refs、context等属性、执行getDerivedStateFromProps()、为componentDidMount标记effectTag。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19const instance = workInProgress.stateNode;
instance.props = newProps;
instance.state = workInProgress.memoizedState; //{ count: 1 }
instance.refs = emptyRefsObject;
...
const getDerivedStateFromProps = ctor.getDerivedStateFromProps; //注意这里,类的static方法
if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(
workInProgress,
ctor,
getDerivedStateFromProps,
newProps,
);
instance.state = workInProgress.memoizedState;
}
...
if (typeof instance.componentDidMount === 'function') {
workInProgress.effectTag |= Update;
}c.
finishClassComponent(),执行instance.render(),返回嵌套的ReactElement,调用reconcileChildren()方法,链接App-fiber和它的子节点。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
36const nextUnitOfWork = finishClassComponent();
...
//finishClassComponent
const nextChildren = instance.render(); //调用App定义的render()方法,返回嵌套ReactElement
let nextChildren = {
$$typeof: Symbol(react.element),
key: null,
props: {
children: [
{
$$typeof: Symbol(react.element),
key: "1",
props: {
children: "点击",
onClick: fn()
},
ref: null,
type: "button",
},
{
$$typeof: Symbol(react.element),
key: "2",
props: {
children: 1
},
ref: null,
type: "span",
}
]
},
ref: null,
type: "div",
}
//reconcileChildren内部,使用nextChildren生成div.container-fiber,将这个fiber与App-fiber链接起来,注意,此时内部的button和span依然是ReactElement,组成了一个数组挂载在div-fiber的pendingProps.children内。
reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime);最后,本次循环
beginWork返回了workInProgress.child,即div-fiber。进入下一个循环。此时,
workInProgress指向了div-fiber。循环从performUnitOfWork()进入beginWork(),当前workInProgress.tag为HostComponent,走updateHostComponent()。没有什么特别的处理,直接调用reconcileChildren()进行fiber的创建和链接。这次的
reconcileChildren有些不同,因为nextChildren是一个数组,里面是button和span的ReactElement形式。react会将数组内的所有元素都创建为fiber节点,然后通过sibling链接起来,同时他们有共同的父fiber节点。1
2reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime);
//nextChildren为数组[{ $$typeof: ... }, { $$typeof: ... }]最后,
beginWork返回了div-fiber的第一个child,即button-fiber。workInProgress指向button-fiber,循环从performUnitOfWork()进入beginWork(),当前workInProgress.tag为HostComponent,走updateHostComponent()。这里,button-fiber没有child要链接,reconcileChildren()不做任何事情,beginWork()完成。接下来进入
completeUnitOfWork(),这个函数是一个中转函数,负责处理循环的走向(走向sibling还是return),button-fiber先进入completeWork()。completeWork()内也会根据workInProgress.tag进行不同的处理。对于HostComponent,react会使用原生的document.createElement()为fiber创建真正的DOM元素。1
2
3
4
5//type为button, rootContainerInstance为rootDOM
let instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
//内部调用react-dom/ReactDOMComponent.js,使用document.createElement()创建DOM
workInProgress.stateNode = instance;button-fiber的整个workLoop()走完。workInProgress指向其sibling,即span-fiber。span-fiber进入beginWork()。与button-fiber的流程一样,在completeWork()内创建了真正的DOM元素,但span-fiber的sibling为null,故返回了return节点,即div-fiber。span-fiber的workLoop()循环完成。div-fiber进入completeUnitOfWork(),随后进入completeWork()。同属于HostComponent(),通过createInstance()创建了真正的DOM节点,没有sibling,返回return节点,即App-fiber。div-fiber的workLoop()循环完成。App-fiber在completeUnitOfWork()和completeWork()没有什么特别的操作(类组件,无需创建DOM),没有sibling,返回return节点,即HostRootFiber。App-fiber的workLoop()完成。HostRootFiber同样在completeUnitOfWork()和completeWork()没有什么特别的操作,workLoop()完成。
至此,初始化的render phase完成。
commit阶段
接下来进入commit阶段。render阶段完成时,react会生成一条effect-list链表,记录着需要处理的side-effect(这个链表的生成过程另文总结)。由commitRoot()进入到commitRootImpl()。
1 | function commitRoot(root, finishedWork) { |
第一阶段称为
before mutation phase,这时可以读取DOM,但DOM还未更新,也不可执行side-effect,这个阶段主要用来执行getSnapshotBeforeUpdate()生命周期。需要注意的是,commit阶段的每个子步骤,react都会去遍历整个effect-list,将整个list上所有对应的work完成后再进入下个步骤,继续从头开始遍历。1
2
3
4
5
6
7
8
9
10
11
12function commitBeforeMutationEffects() {
while (nextEffect !== null) {
switch(workInProgress.tag) {
case ClassComponent: {
instance.getSnapshotBeforeUpdate();
return;
}
case HostComponent: { ... }
}
nextEffect = nextEffect.nextEffect;
}
}第二阶段称为
mutation phase,主要是用来进行DOM的更新,注意内部的commitWork()方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function commitMutationEffects() {
while (nextEffect !== null) {
case Placement: {
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: { ... }
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: { ... }
nextEffect = nextEffect.nextEffect;
}
}第三阶段我们需要将
workInProgress Tree替换为current Tree。1
root.current = finishedWork;
最后一个阶段,我们要执行具有
side-effect的生命周期函数。这里,指的就是componentDidMount和componentDidUpdate,依据的是effect-list fiber内的effectTag属性。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
39function commitLayoutEffects() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(
root,
current,
nextEffect,
committedExpirationTime,
);
}
if (effectTag & Ref) {
recordEffect();
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
function commitLayoutEffectOnFiber() {
switch (finishedWork.tag) {
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
instance.componentDidMount();
} else {
...
instance.componentDidUpdate();
}
}
}
case HostComponent: { ... },
...
}
}
至此,commit phase完成。
更新流程
更新过程大体上与初始化过程一致,比如fiber node依然会走workLoop()循环。但更新过程是由instance.setState()调用触发,对于Class组件来说需要执行不同的生命周期函数,还需要去diffing algorithm标记节点的变化,另外还需要标记effectTag。Fiber Tree的遍历也会跳过没有“变更”的节点,直达需要处理的节点。
render阶段
初始化阶段
reactDOM为实例绑定了updater属性,在实例调用setState()方法时,就会执行到ReactDOM提供的enqueueSetState方法,继而触发scheduleWork过程。同理,在执行workLoop()之前,react会从当前节点(App组件)回溯到root,沿途更新parent node的childExpirationTime,同时会将workInProgress指向HostRootFiber。不过,更新时react会跳过bails out那些没有任何work的节点,直接达到目标节点,即我们的App-fiber。我们假定的例子中,HostRootFiber下即为App-fiber,所以只跳过了HostRootFiber。这时,App-fiber进入了workLoop()。workInProgress指向App-fiber,在beginWork中依据wrokInProgress.tag执行updateClassComponent(),此时我们已经有了App组件的实例,继而进入updateClassInstance()方法。a. 依据
setState(payload, callback)计算新的state。b. 执行
getDerivedStateFromProps()和shouldComponentUpdate()生命周期函数。c. 为
getSnapshotBeforeUpdate和componentDidUpdate标记effectTag。d. 更新
instance的state、props和context属性。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
41
42
43function updateClassInstance(current, workInProgress, ctor, newProps, ...) {
const instance = workInProgress.stateNode;
//第一步,计算新的state
const oldState = workInProgress.memoizedState; //{ count: 1 }
let newState = (instance.state = oldState); //{ count: 1 }
let updateQueue = workInProgress.updateQueue; //{baseState: 1, firstUpdate:{ payload: {count: 2}, callback, }, lastUpdate:{ payload: {count: 2}, callback,}}
if (updateQueue !== null) {
processUpdateQueue(
workInProgress,
updateQueue,
newProps,
instance,
renderExpirationTime,
);
newState = workInProgress.memoizedState; // { count: 2 }
}
//第二步,执行getDerivedStateFromProps
if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(
workInProgress,
ctor,
getDerivedStateFromProps,
newProps,
);
newState = workInProgress.memoizedState;
}
//第三步,标记componentDidUpdate和getSnapshotBeforeUpdate
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.effectTag |= Update;
}
if (typeof instance.getSnapshotBeforeUpdate === 'function') {
workInProgress.effectTag |= Snapshot;
}
//第四步,更新props、state、context属性
instance.props = newProps;
instance.state = newState;
instance.context = nextContext;
return shouldUpdate;
}接下来进入
finishClassComponent()方法。这里,先执行instance.render()获取新的嵌套ReactElement,注意,此时render()内的JSX使用的都是最新的props和state,但DOM还没有更新,页面还没有展示出变化。拿到新的nextChildren后,同样调用reconcileChilder()方法,进行节点的diffing algorithm,实际上通过比较已有的fiber node和新生成的ReactElement,依据key、parent node的类型等属性,决定节点的删除、移动、更新等。这里,我们的span节点进行了内容的变更,前后节点变化为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 1},
pendingProps: {children: 1},
...
}
{
stateNode: new HTMLSpanElement,
type: "span",
key: "2",
memoizedProps: {children: 1},
pendingProps: {children: 2},
...
}这之后,返回了
App-fiber的子节点,进入div-fiber的workLoop()。接下来的
div-fiber和button-fiber的workLoop()过程没有什么特别的地方。不过,button-fiber的completeWork()阶段存疑,初始化时创建了DOM,更新时又会如何处理?这里存疑,还需深入研究。我们先跳过,直接进入span-fiber的workLoop()。进入
span-fiber的循环后,beginWork、completeUnitOfWork都没什么特别要处理的,直接进入completeWork()。在这里,需要为span-fiber准备好DOM的更新,标记好effectTag,同时,span-fiber的memoizedProps也会被更新为pendingProps。整个更新流程的
render phase就完成了,这次的render phase,我们为App组件标记了getSnapshotBeforeUpdate和componentDidUpdate两个effectTag(位运算,叠加),为span-fiber标记了DOM更新的effectTag。而且这两个节点的更新,我们使用firstEffect和nextEffect属性与HostRootFiber进行了链表链接,在接下来的commit phase阶段去处理。
commit阶段
这次的commit阶段与初始化时的commit阶段基本一致,可能在DOM更新等地方有所不同,还需深入研究。
总结
react的源码非常复杂,读起来非常的吃力。上面写了这么多,也只是浅尝即止。内部各任务如何调度、expirationTime如何设置和处理、side-effect链的生成,还有Hooks、ref、context的融合,DOM元素的生成等等都能单独拿出来研究很久。不过大致的流程过了一遍,也算打开了一条门缝,可借以窥探内部的种种奥秘。接下来会去单独研究一下effect-list和expirationTime的相关知识点。