4-5、React 源码解析

官方源码:https://github.com/facebook/react

预防针:看过 Vue 源码的正常,不看 React 源码的也正常,因为它非常复杂,未必能看懂,所以看懂一部分就行了。

看了 React 源码与写好 React 没啥关系。

这篇文章能带来的是:搞清楚 React 的工作流程,基于 React 的 17 版本源码

引子

1
2
3
4
5
6
7
<div>
<h2> hello world </h2>
<div>{ text }</div>
{
list.map(item => <ChildItem item={item}></ChildItem>)
}
</div>

问题:list 数据改变时,怎么最快的判断出来如何更新?
答案:React 为了保持运行时的灵活性,一律采用从头(根节点)遍历,然后再跟之前的对比,把有区别的更新下
疑问:那若有很多 HTML,React 就比较耗性能了吧?是的!
方案:后续 React 的版本就一直在解决这个问题。比如采用了 fiber、使用异步可中断更新策略等等。

React 为什么要用 Fiber?

因为 React 采用的更新策略是从根节点递归遍历,然后再跟之前的对比,把有区别的更新下
然后在 15 版本,采用的是 Stack Reconciler 同步更新机制,由于是同步的,则会存在阻塞,并且还不支持中断
在之后的版本,采用了 Fiber Reconciler 更新机制,支持异步可中断的遍历/更新,若有优先级高的交互(输入、点击等),则中断遍历/更新,先去处理交互,再接着遍历/更新

React 大概的版本区别

  • V15 版本,Stack Reconciler:同步更新机制
  • 16.9 ~ 17.0.2 版本,Fiber Reconciler:异步可中断更新机制
    • 但在 17.0.2 里面只是先做了数据结构,但不稳定(可以理解为先吹了一波),有两个模式
      • legacy 模式:可通过 createa 脚手架创建,有 Fiber 的结构,但不会中断,源码文件为 xxx.old.js
      • concurrent 模式:需要自己去编译,无法通过 create 脚手架创建,实现了中断,源码文件为 xxx.new.js
  • 18 版本,可以理解为 concurrent 模式 ++

React 中的数据结构

v-dom / element

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
function App() {
return <div className="app">
<h2>hello world</h2>
<div id="list">
<ul>
<li>list 1</li>
<li>list 2</li>
<li>list 3</li>
</ul>
</div>
</div>
}

// 经过 babel 编译后为:
function App() {
return React.createElement('div', { className: 'app'},
React.createElement('h2', null, 'hello world'),
React.createElement('div', { id: 'list' },
React.createElement('ul', null,
React.createElement('li', null, 'list 1'),
React.createElement('li', null, 'list 2'),
React.createElement('li', null, 'list 3')
)
)
)
}

React.createElement到底是个啥?源码地址:点这个
核心代码:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
function ReactElement(type, key, ref, self, source, owner, props) {
const element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,

// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,

// Record the component responsible for creating this element.
_owner: owner,
};

// ...

return element;
}


export function createElement(type, config, children) {
let propName;

const props = {};

// ⭐️ 将 config 的值放入 props 中
if (config != null) {
// ...

for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}

// ⭐️ 将 children 的值放入 props.children 中
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}

// ...

return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}

通过上述的React.createElement源码可得如下结构(vDom):

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
43
44
45
46
47
48
49
50
51
52
53
function App() {
return React.createElement('div', { className: 'app'},
React.createElement('h2', null, 'hello world'),
React.createElement('div', { id: 'list' },
React.createElement('ul', null,
React.createElement('li', null, 'list 1'),
React.createElement('li', null, 'list 2'),
React.createElement('li', null, 'list 3')
)
)
)
}

// 得出如下结构(简版)
const vDom = {
type: "div",
props: {
className: "app",
children: [
{
type: "h2",
props: { children: "hello world" },
},
{
type: "div",
props: {
id: "list",
children: [
{
type: "ul",
props: {
children: [
{
type: "li",
props: { children: "list 1" },
},
{
type: "li",
props: { children: "list 2" },
},
{
type: "li",
props: { children: "list 3" },
},
],
},
},
],
},
},
],
},
};

fiber

本质就是一个链表形式的数据结构,用来表示 v-dom 的,大致结构如下:

1
2
3
4
5
6
7
8
9
10
11
FiberNode = {
tag, // 标明 fiber 的类型,如函数组件、类组件等
key, // React 元素的key,用于优化更新
elementType, // 标明 React 元素的类型,即调用 createElement 时的第一个参数。
type, // 标明 DOM 元素 的类型

// 链表形式
return, // 指向父 fiber
child, // 指向子 fiber
sibling // 指向下一个同级 fiber
}

current Fiber、workInProgress Fiber

current Fiber:对应当前页面中真实渲染的 DOM 的结构
workInProgress Fiber:更新时才创建的并已处理好变更的对应到真实 DOM 的虚拟 DOM 的 Fiber 链表
大致的链表结构如下:

React 的双缓存是什么?

双缓存来源于:图形渲染的优化技术
原理为:

  1. 在双缓存技术中,系统会在内存(通常为显存)中开辟两个区域:
    • 前缓冲(Front Buffer):这是与显示器相连的缓冲区,当前正在被显示的内容就存储在这里。
    • 后缓冲(Back Buffer):这是一个独立于前缓冲的内存区域,应用程序在该区域进行所有的绘图操作。
  2. 应用程序会在后缓冲中完成所有的图形绘制工作,而不会直接影响到屏幕上显示的内容。
  3. 当所有需要更新的画面内容都在后缓冲区绘制完成后,系统会执行一次“缓冲区交换”或“页面翻转”操作,迅速将前后缓冲区的角色互换,即将后缓冲区的内容复制到前缓冲区,并使其成为新的可见帧。
  4. 由于这个交换操作通常是硬件加速且非常快速的,因此用户看到的是一个完整、无闪烁的新帧画面,而不是半成品或者绘制过程中的中间状态。

简单理解为:预加载

React 中的双缓存指的是两个 Fiber 树,一个用于当前展示(current fiber),一个用于更新的(workInprogress fiber),有变化时则创建workInprogress fiber并在其中进行比较、计算等,之后直接一次性用整个workInprogress fiber(或部分) 替换掉整个current fiber(或部分),不用两个逐个比较与 DOM 更新,从而避免了大量的 DOM 操作带来的性能损耗。

React 的核心库

  • react 库1、提供虚拟 DOM 相关的 API;2、提供用户相关的 API(useState/use* 等)
  • react-dom 库提供操作 DOM 相关的 API
    • 其中的核心库react-reconciler通过调和、调度、提交等过程实现渲染
      • 其中核心的为:
        • ReactFiberBeginWork.js:创建 workInProgressFiber
        • ReactFiberCompleteWork.js:基于 workInProgressFiber 创建 EffectList
        • ReactFiberCommitWork.js:基于 EffectList 更新界面

React 的整体流程(V17 版本)

页面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function App() {
return <div className="app">
<h2>hello world</h2>
<div id="list">
<ul>
<li>list 1</li>
<li>list 2</li>
<li>list 3</li>
</ul>
</div>
</div>
}

// 入口:ReactDom.render(...)
ReactDom.render(<App />, document.getElementById('root'));

入口:页面的ReactDom.render(...),调用的源码是react-dom 库里面的render函数

源码位置:点这

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// react-dom 源码
export function render(
element: React$Element<any>,
container: Container,
callback: ?Function,
){
// ...

return legacyRenderSubtreeIntoContainer( // 渲染下一级的树到 Container 中
null,
element,
container,
false,
callback,
);
}

legacyRenderSubtreeIntoContainer:将子树渲染到容器中,首次渲染则调用legacyCreateRootFromDOMContainer

源码位置:点这

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
// 作用:将子树渲染到容器中。
// 具体来说,它是在 React 的旧版本和新的 Fiber 架构之间进行兼容性处理的一部分。
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>, // 父组件(可能为null)
children: ReactNodeList, // 要渲染的子节点。
container: Container, // 容器,即 DOM 元素或 React 容器
forceHydrate: boolean, // 一个布尔值,决定是否强制进行 hydration(将服务器端渲染的HTML转换为客户端React组件)。
callback: ?Function, // 回调函数,在渲染完成后调用
): React$Component<any, any> | PublicInstance | null {
// ...

// 从给定的容器中获取 _reactRootContainer ,这可能是根容器
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
if (!root) { // 不存在,即容器是首次渲染:
// ⭐️ Initial mount
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;

// ...

unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else { // 存在,即容器不是首次渲染
fiberRoot = root._internalRoot;

// ...

// Update,更新容器的内容
updateContainer(children, fiberRoot, parentComponent, callback);
}

// 返回根容器的公共实例
return getPublicRootInstance(root);
}

legacyCreateRootFromDOMContainer:用于创建 React 的根节点(FiberRoot),核心为createLegacyRoot

源码位置:点这

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function legacyCreateRootFromDOMContainer(
container: Container,
forceHydrate: boolean,
): RootType {
// ...

return createLegacyRoot(
container,
shouldHydrate
? {
hydrate: true,
}
: undefined,
);
}

createLegacyRoot:用于创建 React 的根节点(FiberRoot),核心为ReactDOMBlockingRoot

源码位置:点这

1
2
3
4
5
6
export function createLegacyRoot(
container: Container,
options?: RootOptions,
): RootType {
return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}

ReactDOMBlockingRoot:核心调用createRootImpl

源码位置:点这

1
2
3
4
5
6
7
8
9
10
function ReactDOMBlockingRoot(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
this._internalRoot = createRootImpl(container, tag, options);
}

ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render = function(){ ... }
ReactDOMRoot.prototype.unmount = ReactDOMBlockingRoot.prototype.unmount = function(): void { ... }

createRootImpl:核心调用createContainer

源码位置:点这

1
2
3
4
5
6
7
8
9
10
11
12
function createRootImpl(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
// ...
const root = createContainer(container, tag, hydrate, hydrationCallbacks);

// ...

return root;
}

createContainer:核心调用createFiberRoot

源码位置:点这

1
2
3
4
5
6
7
8
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}

createFiberRoot:核心调用new FiberRootNode

源码位置:点这

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);

const rootFiber = createHostRootFiber(tag);
root.current = rootFiber; // ⭐️ current Fiber
// ...

return root;
}

FiberRootNode:一个构造函数,生成Fiber Root Node的数据结构

源码位置:点这

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
function FiberRootNode(containerInfo, tag, hydrate) {
this.tag = tag;
this.containerInfo = containerInfo;
this.pendingChildren = null;
this.current = null;
this.pingCache = null;
this.finishedWork = null;
this.timeoutHandle = noTimeout;
this.context = null;
this.pendingContext = null;
this.hydrate = hydrate;
this.callbackNode = null;
this.callbackPriority = NoLanePriority;
this.eventTimes = createLaneMap(NoLanes);
this.expirationTimes = createLaneMap(NoTimestamp);

this.pendingLanes = NoLanes;
this.suspendedLanes = NoLanes;
this.pingedLanes = NoLanes;
this.expiredLanes = NoLanes;
this.mutableReadLanes = NoLanes;
this.finishedLanes = NoLanes;

this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);

if (supportsHydration) {
this.mutableSourceEagerHydrationData = null;
}

if (enableSchedulerTracing) {
this.interactionThreadID = unstable_getThreadID();
this.memoizedInteractions = new Set();
this.pendingInteractionMap = new Map();
}
if (enableSuspenseCallback) {
this.hydrationCallbacks = null;
}
}

总结一下前面创建Fiber Root的流程

创建之后,后续关键调用为updateContainer,其中关键为scheduleUpdateOnFiber

源码位置:点这

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// ...

const current = container.current;

// ...

const update = createUpdate(eventTime, lane);

// ...

scheduleUpdateOnFiber(current, lane, eventTime);

return lane;
}

scheduleUpdateOnFiber:关键调用performSyncWorkOnRoot

源码位置:点这

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
// ...

const root = markUpdateLaneFromFiberToRoot(fiber, lane);


// ...

if (lane === SyncLane) {
// ...

// 关键函数
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbackQueue();
}
}
} else {
// Schedule a discrete update but only if it's not Sync.
if (
(executionContext & DiscreteEventContext) !== NoContext &&
// Only updates at user-blocking priority or greater are considered
// discrete, even inside a discrete event.
(priorityLevel === UserBlockingSchedulerPriority ||
priorityLevel === ImmediateSchedulerPriority)
) {
// This is the result of a discrete event. Track the lowest priority
// discrete update per root so we can flush them early, if needed.
if (rootsWithPendingDiscreteUpdates === null) {
rootsWithPendingDiscreteUpdates = new Set([root]);
} else {
rootsWithPendingDiscreteUpdates.add(root);
}
}
// Schedule other updates after in case the callback is sync.
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
}

// We use this when assigning a lane for a transition inside
// `requestUpdateLane`. We assume it's the same as the root being updated,
// since in the common case of a single root app it probably is. If it's not
// the same root, then it's not a huge deal, we just might batch more stuff
// together more than necessary.
mostRecentlyUpdatedRoot = root;
}

performSyncWorkOnRoot:关键调用renderRootSynccommitRoot

源码位置:点这

1
2
3
4
5
6
7
8
9
10
11
12
13
function performSyncWorkOnRoot(root) {
// ...

exitStatus = renderRootSync(root, lanes);

// ...

commitRoot(root);

// ...

return null;
}

renderRootSync:关键调用workLoopSync

源码位置:点这

1
2
3
4
5
6
7
8
9
function renderRootSync(root: FiberRoot, lanes: Lanes) {
// ...

workLoopSync();

// ...

return workInProgressRootExitStatus;
}

workLoopSync:递归调用performUnitOfWork

源码位置:点这

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
// 一个递归函数,逻辑为根据最开始的页面代码来调用的
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}

// 最开始的页面代码
function App() {
return <div className="app">
<h2>hello world</h2>
<div id="list">
<ul>
<li>list 1</li>
<li>list 2</li>
<li>list 3</li>
</ul>
</div>
</div>
}
// 入口:ReactDom.render(...)
ReactDom.render(<App />, document.getElementById('root'));

则 workLoopSync 调用 performUnitOfWork 时的参数为:
第①次:tag = 3,elementType = null,代表 container
第②次:tag = 2,elementType = f App(),代表 function App() { ...} 函数
第③次:tag = 5,elementType = 'div',代表 <div className="app"> 元素
第④次:tag = 5,elementType = 'h2',代表 <h2> 元素
第⑤次:tag = 5,elementType = 'div',代表 <div id="list"> 元素
...
第n次:tag = 5,elementType = 'li',代表 <li>list 3</li> 元素

performUnitOfWork:被递归调用,核心调用beginWorkcompleteUnitOfWork

源码位置:点这

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function performUnitOfWork(unitOfWork: Fiber): void {
// ...

let next;

// ...

// 调用 beginWork 处理当前元素,返回值为子节点,没有则返回 null
next = beginWork(current, unitOfWork, subtreeRenderLanes);

// ...

if (next === null) {
// 若无子节点,则完成当前任务
completeUnitOfWork(unitOfWork);
} else {
// 否则继续处理子节点
workInProgress = next;
}

ReactCurrentOwner.current = null;
}


所以递归逻辑为:先子级后同级(先深度后广度)
workLoopSync递归调用performUnitOfWork处理同级,performUnitOfWork调用beginWork处理子级

1
2
3
4
5
6
7
8
9
10
11
12
function App() {
return <div className="app">
<h2>hello world</h2>
<div id="list">
<ul>
<li>list 1</li>
<li>list 2</li>
<li>list 3</li>
</ul>
</div>
</div>
}

递归调用逻辑图(Fiber 链表结构)


上图体现的就是Fiber的数据结构,核心就是:return(父)、child(子)、sibling(兄)

beginWork:作用是创建 workInProgressFiber

源码位置:点这
作用:生成v-dom,然后和current fiber对比,向下调和的过程
就是由 fiberRoot 按照 child 指针逐层往下调和,期间会执行:函数组件、类组件,DIFF 子节点,从而打上不同的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
39
40
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const updateLanes = workInProgress.lanes;

// ...

if (current !== null) {
// 初次渲染不会进

// 新老对比
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
}

// 匹配节点的 tag 类型,然后调用对应的方法去打上不同的 effectTag
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
updateLanes,
renderLanes,
);
}
// ...
}
}

completeUnitOfWork:核心调用completeWork

源码位置:点这

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;

// Check if the work completed or if something threw.
if ((completedWork.flags & Incomplete) === NoFlags) {
// ...

// ⭐️ 核心调用
next = completeWork(current, completedWork, subtreeRenderLanes);
} else {
// ...
}

// ...
}

completeWork:核心调用createInstance

源码位置:点这
effectList将需要更新的数据形成一个新的 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
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;

switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case ContextConsumer:
// ...
case HostComponent: {
// ...
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);

// ...
invariant(
false,
'Unknown unit of work tag (%s). This error is likely caused by a bug in ' +
'React. Please file an issue.',
workInProgress.tag,
);
}

createInstance:核心调用createElement

源码位置:点这
作用:调用@react-dom 库里面的 createInstance创建真实的 DOM(document.createElement()),但还不会渲染到页面上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): Instance {
let parentNamespace: string;
// ...

const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
return domElement;
}

createElement:核心调用document.createElement

源码位置:点这
创建真实的 DOM

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
export function createElement(
type: string,
props: Object,
rootContainerElement: Element | Document,
parentNamespace: string,
): Element {
const ownerDocument: Document = getOwnerDocumentFromRootContainer(
rootContainerElement,
);
let domElement: Element;

if (namespaceURI === HTML_NAMESPACE) {
if (type === 'script') {
const div = ownerDocument.createElement('div');

div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
const firstChild = ((div.firstChild: any): HTMLScriptElement);
domElement = div.removeChild(firstChild);
} else if (typeof props.is === 'string') {
domElement = ownerDocument.createElement(type, {is: props.is});
} else {
domElement = ownerDocument.createElement(type);
if (type === 'select') {
const node = ((domElement: any): HTMLSelectElement);
if (props.multiple) {
node.multiple = true;
} else if (props.size) {
node.size = props.size;
}
}
}
} else {
domElement = ownerDocument.createElementNS(namespaceURI, type);
}

// ...

return domElement;
}

再总结一下前面的调用栈流程

commitRoot

源码位置:点这
当前面的beginWork、completeWork完成后,后续关键流程为commitWork,核心调用方法commitRoot,它其中核心调用commitRootImpl

1
2
3
4
5
6
7
8
function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediateSchedulerPriority,
commitRootImpl.bind(null, root, renderPriorityLevel),
);
return null;
}

commitRootImpl

源码位置:点这
核心调用:flushPassiveEffects、commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects

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
function commitRootImpl(root, renderPriorityLevel) {
do {
// ⭐️ 处理一些还未执行完毕的 useEffect
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);

// ...

// ⭐️ 更新前
commitBeforeMutationEffects(finishedWork);

// ...

// ⭐️ 更新时
commitMutationEffects(finishedWork, root, renderPriorityLevel);

if (shouldFireAfterActiveInstanceBlur) {
afterActiveInstanceBlur();
}
resetAfterCommit(root.containerInfo);

// finishedWork = workInProgressFiber
root.current = finishedWork; // ⭐️ 实现双缓存的切换


// ⭐️ 更新后
commitLayoutEffects(root);

// ...

return null;
}

flushPassiveEffects

处理一些还未执行完毕的 useEffect

commitBeforeMutationEffects(更新前)

调用getSnapshotBeforeUpdate生命周期

commitMutationEffects(更新时)

通过 Fiber 链表结构,一层层进行处理增删改

commitLayoutEffects(更新后)

进行 Layout(布局)阶段,执行一些生命周期:componentDidMount、componentWillUpdate 等、执行 setState 的 callback、调用 useLayoutEffect、将 useEffect 存储起来

React 的异步可中断(V18 版本)

中断的是下一次的 beginWork

1
2
3
4
5
6
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}

shouldYield意思是交出执行权。

浏览器的任务执行顺序:宏任务 -> 微任务 -> RAF(requestAnimationFrame) -> render -> RIC(requestIdelCallback) -> 新的宏任务 -> 新的微任务 -> …

当面对长任务时,React 会采用分段执行,但又需要满足可打断,先执行后面的,那可以使用的方案如下:

  • setTimeout:宏任务,满足 render 后再执行,但它存在 4ms 的延迟
  • Promise:微任务,不满足 render 后再执行,宏任务执行后就立马执行了
  • requestIdelCallback:在浏览器空闲时期执行,满足 render 后再执行,但它执行时间不确定,兼容性差
  • MessageChannel:浏览器创建的低延迟通信,可以创建一个宏观上的异步操作,与 render 执行无关

仿照 React 源码,模拟实现打断

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// 仿照 React 源码,模拟实现打断

// 存放 任务 的队列
const queue = []

// 存放 flush 函数的队列
const transition = []

let deadTime;
const split = 5 // 任务执行超过 5ms,则让出执行权
const now = () => performance.now() // 获取当前时间,计时精度更高

const peek = arr => arr[0] // 拿出数组的第一个值

const poseMessage = (()= >{

const { port1, port2 } = new MessageChannel()
// ⭐️ 当 port2.postMessage 调用后,类似于在 setTimeout 后再执行 port1.ommessgae

// 恢复执行
port1.ommessgae = () => {
// 取出 transition 中的第一个 flush 并执行
peek(transition)()
}

// poseMessage() 执行的是这个 return 的函数
return () => {
port2.postMessage() // 让出执行权
}
})()

function startTransition(flush) {
// 将 flush 放到 transition 数组里面 && 执行 poseMessage()
transition.push(flush) && poseMessage()
}

function shouldYield() {
// 5ms 到了 或者 有更高优先级的操作
return now() >= deadTime
|| navigatior.scheduling.isInputPending // 模拟更高优先级的操作
}

// 时间分片的函数
function flush() {
// 任务执行的截止时间为:当前时间 + 5ms
deadTime = now() + split
const task = peek(queue) // 拿出第一个任务,task = { task }
while(task && !shouldYield()) {
const { task } = task
task.task = null // queue[0] = { task: null }
// 执行任务
const next = task()

// 如果 next 是函数,则任务还未执行完,类似于 workInProgress benginWork 还未完
if(next && typeof next === 'function') {
// 没执行完的,重新放进去
task.task = next // queue[0] = { task: next }
} else {
// 执行完了
queue.shift()
}
}

// 代码执行到这后,就两种情况:
// 1、task 执行完了
// 2、task 还有,但 showYield() 为 true 让出执行权了
task && startTransition(flush)
}

// 用 schedule 来调度任务
function schedule(task) {
queue.push( { task } ) // 将任务放进队列中

startTransition(flush)
}

function myTask() {
return () => {
console.log(1)

return () => {
console.log(2)

return () => {
console.log(3)

return () => {
console.log(4)
}
}
}
}
}

高优先级操作

  • 用户交互事件处理
  • 错误边界
  • 引发布局或绘制的操作

Suspense 组件

本质是一个错误边界
可以作为组件的异步加载,也可以将异步请求改写为同步逻辑

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
const fetchList = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(["a", "b", "c", "d"]);
}, 1000);
});
};

const readApi = (fn) => {
let state = "pending";
let res;

const suspense = fn()
.then((r) => {
state = "success";
res = r;
})
.catch((err) => {
state = "error";
res = err;
});

function read() {
if (state === "pending") {
throw suspense;
} else if (state === "error") {
throw res;
} else if (state === "success") {
return res;
}
}

return read;
};

const ListApi = readApi(fetchList);

const List = () => {
// const [list, setList] = useState([]);

// useEffect(() => {
// fetchList().then((res) => {
// setList(res);
// });
// }, []);

const list = ListApi();

return (
<div>
<ul>
{list.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
};

export default function FuncComName(props) {
return (
<div>
<Suspense fallback={<div>loading...</div>}>
<List />
</Suspense>
</div>
);
}

// 逻辑解析:当第一次加载 List 组件时,走的是这个逻辑
// if (state === "pending") {
// throw suspense;
// }
// 抛出了 suspense 错误,被 <Suspense 的 fallback 捕获,所以显示 loading...
// 但由于 suspense 是个 Promise,所以等它执行完毕后又重新渲染 List 组件
// 然后走的先这个逻辑
// else if (state === "success") {
// return res;
// }
// 所以最终又正常显示了

简易手写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MySuspense extends Component {
constructor(props) {
super(props);

this.state = {
loading: false,
};
}

componentDidCatch(error) {
if (typeof error.then === "function") {
this.setState({ loading: true });

error.then(() => this.setState({ loading: false }));
}
}

render() {
return this.state.loading ? this.props.fallback : this.props.children;
}
}

补充

SetTimeout

存在 4ms 的延迟问题
就算设置的是 0,但浏览器可能会因为其他任务(DOM 更新、事件处理等),延迟执行回调函数
所以嵌套调用越多,时间越不准确

MessageChannel

是浏览器提供的一个高级通信机制,它允许在不同上下文之间(比如窗口、iframe 或者 worker 线程)进行安全且异步的消息传递。

适用于在两个窗口之间建立直接的通信通道。

创建 MessageChannel

1
2
3
4
const messageChannel = new MessageChannel();
// 分别获取两个端口
const port1 = messageChannel.port1;
const port2 = messageChannel.port2;

发送、接受信息

1
2
3
4
5
6
7
// 在一个端口上接受消息
port1.onmessage = (event) => {
console.log('收到的信息:', event.data)
}

// 在一个端口上发送消息
port1.postMessage('喂喂,收得到吗?')

BroadcastChannel

在相同源的不同上下文之间(包括但不限于标签页)实现双向、异步通信

适用于同一浏览器下的不同窗口进行广播式的通信。

创建 BroadcastChannel

1
2
// 创建一个频道名称为 "my_channel" 的 BroadcastChannel
const channel = new BroadcastChannel("my_channel");

发送、接受信息

1
2
3
4
5
6
7
// 接受消息
channel.onmessage = (event) => {
console.log('收到的信息:', event.data)
}

// 发送消息
channel.postMessage('喂喂,收得到吗?')

面试题

什么是 Fiber?

本质是一个对象形式的数据结构:其中包含了虚拟 DOM、链表、EffectList 等数据
在 React 中存在 currentFiber 与 workInProgressFiber 两个 Fiber,每次的更新通过 currentFiber 与虚拟 DOM 对比去生成 workInProgressFiber,最终用 workInProgressFiber 替换掉 currentFiber,实现双缓存的替换
双缓存步骤:

  • 创建 fiberRoot 对象,将所有的 dom 串起来
  • 在通过 beginWork 的递归调用,创建出 workInProgressFiber,并打上 EffectTag
  • 然后在 completeWork 中,生成 EffectList,并创建真实的 DOM,在更新前执行一些生命周期,在更新时实现双缓存的切换,在更新后执行一些生命周期

useState 实现原理?

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
43
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}

// ...

function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
invariant(
dispatcher !== null,
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
return dispatcher;
}

// ...

import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};

export default ReactCurrentDispatcher;

// ...

export type Dispatcher = {|
readContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
): T,
useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
// ...
}

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
/**
* 创建一个hook节点,并将其挂载到 currentlyRenderingFiber 链表的最后
* @type {Hook}
*/
const hook = mountWorkInProgressHook();

if (typeof initialState === 'function') {
// 若传入的是函数,则使用执行该函数后得到的结果
initialState = initialState();
}

/**
* 设置该 hook 的初始值
* memoizedState 用来存储当前hook要显示的数据
* baseState 用来存储执行setState()的初始数据
**/
hook.memoizedState = hook.baseState = initialState;

// 为该 hook 添加一个 queue 结构,用来存放所有的 setState() 操作
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});

// 创建一个 dispatch() 方法,提前绑定好 currentlyRenderingFiber 和 queue,最终用它来执行 setState()
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber, // 当前处理的 fiber 节点
queue, // 该 hook 的 queue 结构,用来挂载 setState() 中的操作的;
));
return [hook.memoizedState, dispatch]; // useState() 返回的数据
}

/**
* 对当前的 state 执行的基本操作,若传入的不是函数类型,则直接返回该值,
* 若传入的是函数类型,返回执行该函数的结果
* @param {S} state 当前节点的state
* @param {BasicStateAction<S>} action 接下来要对该state执行的操作
* @returns {S}
*/
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}

function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber);

const update: Update<S, A> = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};

// Append the update to the end of the list.
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;

const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
} else {
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;

// ...

try {
const currentState: S = (queue.lastRenderedState: any);
// 执行传入的 action(setState(action)),返回新的 state
const eagerState = lastRenderedReducer(currentState, action);

// ...
}
}

// ...

// ⭐️ 调用更新操作
scheduleUpdateOnFiber(fiber, lane, eventTime);
}

// ...
}

简单理解:但调用 useState 时,会传入一个初始值,useState 会返回一个数组,数组的第一个元素是当前 state 的值,第二个元素是一个 dispatch 方法,并且会记录当前该 hook 对应当前的节点信息(Fiber);若初始值为函数时,还会执行一下获得函数的返回值。

当调用 setState 即 dispatch 方法时,会去触发对应的 Queue.dispatch,本质是执行 setState 传入的内容,然后调用 scheduleUpdateOnFiber 去触发更新逻辑

为什么要使用 useState?

因为 react 的数据本身是不支持响应式的,因为最开始 react 关注的是数据、视图,但后来发现缺少了数据状态的管理,所以也出现了第三方的 Redux 等库来处理。

最后官方意识到了,所以提供了 hooks 的形式,调用它来主动更新页面


4-5、React 源码解析
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/4-5、React 源码解析/
作者
黄智强
发布于
2024年1月13日
许可协议