准备工作
这篇文章本来准备作为一篇笔记,不准备发到博客中来。但简单写完后想起自己初次接触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
的相关知识点。