精简版 React 源码手稿

TLDR: 全是源码,适合看过源码想回头细看的同学

本次分享源码版本react@16.6.0和react@16.7.0

注:本此分享更多的是源码,概念性的东西读者可自行查阅官网文档或者 YouTube React Conference

代码结构

  • packages/react

  • packages/react-dom

  • packages/react-reconciler

平台无关 DOM 操作,包括任务调度

JSX 到 JS

React.createElement, 入参 type, config, children,返回一个对象

API 源码

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
// packages\react\src\React.js
// 暴露出来的API
const React = {
Children: {
map,
forEach,
count,
toArray,
only,
},

createRef,
Component,
PureComponent,

createContext,
forwardRef,
lazy,
memo,

Fragment: REACT_FRAGMENT_TYPE,
StrictMode: REACT_STRICT_MODE_TYPE,
unstable_ConcurrentMode: REACT_CONCURRENT_MODE_TYPE,
Suspense: REACT_SUSPENSE_TYPE,
unstable_Profiler: REACT_PROFILER_TYPE,

createElement: __DEV__ ? createElementWithValidation : createElement,
cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
isValidElement: isValidElement,

version: ReactVersion,
};

export default React;

React Element

  • React 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
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// packages\react\src\ReactElement.js
/**
* Create and return a new ReactElement of the given type.
* See https://reactjs.org/docs/react-api.html#createelement
* type 类型 原生标签的话是一个字符串,自定义组件的话是一个变量
* config 节点attributes
*/
export function createElement(type, config, children) {
let propName;

// Reserved names are extracted
const props = {};

let key = null;
let ref = null;
let self = null;
let source = null;

// 解析上述四个字段以及props属性
// for/in
if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = "" + config.key;
}

self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// Remaining properties are added to a new props object
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}

/**
* https://babeljs.io/repl
* const jsx =
<div key="435ksdfds" class="wrapper">
<span>matthew</span>
<span>green</span>
</div>
* 转换为js就是
* const jsx = React.createElement(
"div",
{
key: "435ksdfds",
class: "wrapper"
},
React.createElement("span", null, "matthew"),
React.createElement("span", null, "green")
);
* 可以看到children从三个参数开始
*/
// 找到props的children属性,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];
}
props.children = childArray;
}

// Resolve default props
// 这里只有Class组件才会走到,因为原生标签type就是一个字符串
// 判断有没有使用的是undefined
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current, // 当前Fiber节点, 后面解析
props
);
}

// packages\react\src\ReactElement.js
/**
* Factory method to create a new React element.
* 不是通过class模式创建的,所以不要使用new
* 也不要用instanceOf检测类型,而因该通过Symbol.for('react.element')检测类型
*
* ReactElement返回一个对象
* 这也是VDOM diff的对象
*/
const ReactElement = function (type, key, ref, self, source, owner, props) {
const element = {
// 通过Symbol唯一确定是否是React Element
// 同时兼具安全的功能
// Symbol参考:https://www.jianshu.com/p/f6e0972b24d0
$$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, // 父Fiber节点
};

return element;
};

// packages\shared\ReactSymbols.js
// 如果没有Symbol就使用一个简单的字符
const hasSymbol = typeof Symbol === "function" && Symbol.for;

export const REACT_ELEMENT_TYPE = hasSymbol
? Symbol.for("react.element")
: 0xeac7;

React Element 只是一个普通的 JavaScript 对象,用来告诉 React 怎么创建真实的 DOM

新的疑问: 1. $$typeof 如何使用的 2. type, key, ref,owner 如何使用的

React Component

  • 什么是 React Component?和 React Element 有啥不一样?

  • 猜测组件应该是 React 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
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
// packages\react\src\ReactElement.js
import { Component, PureComponent } from "./ReactBaseClasses";

// packages\react\src\ReactBaseClasses.js
// 更新Component state 的基类
function Component(props, context, updater) {
this.props = props;
// read from getInitialState
// 可用于跨层级通信
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

/**
* this.state是不可变的,想要修改需要手动调用setState修改
* this.state 不一定是立即更新的
* setState 不一定是同步的,因为有可能有批处理环节
* 如果有callback,它将会再setState完成后调用
* @param {object|function} partialState Next partial state or function to
* produce next partial state to be merged with current state.
* @param {?function} callback Called after state is updated.
*/
Component.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, "setState");
};

/**
* 默认情况下,当组件的 state 或 props 发生变化时,组件将重新渲染。
* 如果 render 方法依赖于其他数据,则可以调用 forceUpdate 强制让组件重新渲染
* 不会调用 shouldComponentUpdate,但会调用 componentWillUpdate 和 componentDidUpdate
*/
Component.prototype.forceUpdate = function (callback) {
this.updater.enqueueForceUpdate(this, callback, "forceUpdate");
};

/**
* 含有默认浅比较的Component
*/
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
Object.assign(pureComponentPrototype, Component.prototype);
// 看起来和Component就只有下面这点区别了
pureComponentPrototype.isPureReactComponent = true;

Component 是一个基类,提供了 setState 和 forceUpdate 两个方法

新的疑问: setState 和 forceUpdate 貌似进了 enqueueSetState 队列之类的东西,后面发生了什么

ReactRef & ref

用于获取 DOM 实例,应该尽量避免过多使用

有三种使用姿势: stringRef, function ref, React.createRef

Forwarding Refs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 举例使用ref
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.objRef = React.createRef();
}

componentDidMount() {
// 获取
// this.refs.stringRef
// this.methodRef
// this.myRef.current
}
render() {
return (
<>
<p ref="stringRef"></p>
<p ref={(obj) => (this.methodRef = obj)}></p>
<p ref={this.objRef}></p>
</>
);
}
}
1
2
3
4
5
6
7
8
// packages\react\src\ReactCreateRef.js
// 看了源码的发现就返回了这么一个简单对象
export function createRef(): RefObject {
const refObject = {
current: null,
};
return refObject;
}

新问题:Component 和 createRef 中 ref 后续是如何使用的

Context

两种使用方式,childContextType(即将在 react@17 版本中废弃), createContext

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
// demos\src\context\index.js
import React from "react";
import PropTypes from "prop-types";

const { Provider, Consumer } = React.createContext();

class Parent extends React.Component {
state = {
childContext: "123",
newContext: "456",
};

getChildContext() {
return { value: this.state.childContext };
}

render() {
return (
<>
<div>
<label>childContext: </label>
<input
type="text"
value={this.state.childContext}
onChange={(e) => this.setState({ childContext: e.target.value })}
/>

<br />
<br />
<label>newContext: </label>
<input
type="text"
value={this.state.newContext}
onChange={(e) => this.setState({ newContext: e.target.value })}
/>

<Provider value={this.state.newContext}>
{this.props.children}
</Provider>
</div>
</>
);
}
}

function Child1() {
return <Consumer>{(value) => <p>newContext: {value}</p>}</Consumer>;
}

class Child2 extends React.Component {
render() {
return <p>childContext: {this.context.value}</p>;
}
}

// 务必申明,否则拿不到
Child2.contextTypes = {
value: PropTypes.string,
};
// 务必申明,否则报错
Parent.childContextTypes = {
value: PropTypes.string,
};

const Demo = () => {
return (
<Parent>
<Child1 />
<Child2 />
</Parent>
);
};

export default Demo;

再看看源码

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
// packages\react\src\ReactElement.js
import { createContext } from "./ReactContext";

// packages\react\src\ReactContext.js
export function createContext<T>(
defaultValue: T,
calculateChangedBits: ?(a: T, b: T) => number
): ReactContext<T> {
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE,
// As a workaround to support multiple concurrent renderers, we categorize
// some renderers as primary and others as secondary. We only expect
// there to be two concurrent renderers at most: React Native (primary) and
// Fabric (secondary); React DOM (primary) and React ART (secondary).
// Secondary renderers store their context values on separate fields.
// 两个变量值相同,是同的地方不同而已
_currentValue: defaultValue,
_currentValue2: defaultValue,
// These are circular
Provider: (null: any),
Consumer: (null: any),
};

context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};

// 又指向了自己,意味着Consumer还可以是Provider继续向下无限传递
context.Consumer = context;

return context;
}

ConcurrentMode

之前是 AsyncMode
react@16 之后提出的一种优先级策略,它使得 react 的渲染是可以中断的, 从而可以操作渲染调度,最终让渲染更加流畅

案列可以参考 demo

1
2
3
4
5
// packages\react\src\React.js
...
// unstable_ConcurrentMode只是一个Symbol
unstable_ConcurrentMode: REACT_CONCURRENT_MODE_TYPE,
...

Suspense

参考:React 的未来:与 Suspense 共舞

  • React Suspense 是组件从缓存中加载数据时暂停呈现的一种通行方法。它解决的问题:渲染是和 I/O 绑定时的情况。

  • 支持 lazy, 此时 lazy 的组件会被 webpack 进行代码分割处理

1
2
3
4
5
6
7
8
9
function MyComponent() {
return (
// 在包裹的异步组件全部加载完毕之后Spinner才消失
<React.Suspense fallback={<Spinner />}>
<LazyComponent />
<LazyComponent2 />
</React.Suspense>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// packages\react\src\React.js
...
// Suspense只是一个Symbol
Suspense: REACT_SUSPENSE_TYPE,
...

// packages\react\src\React.js
import {lazy} from './ReactLazy';

// packages\react\src\ReactLazy.js
// 是一个函数,接受一个thenable函数
export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
return {
$$typeof: REACT_LAZY_TYPE,
_ctor: ctor,
// React uses these fields to store the result.
_status: -1,
_result: null,
};
}

Suspense 只是一个 Symbol

新的问题: 一个 Symbol 是如何承载元素的?

Hook

Hook 简介

react@16.7.0

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
// packages\react\src\React.js
if (enableHooks) {
React.useCallback = useCallback;
React.useContext = useContext;
React.useEffect = useEffect;
React.useImperativeMethods = useImperativeMethods;
React.useLayoutEffect = useLayoutEffect;
React.useMemo = useMemo;
React.useReducer = useReducer;
React.useRef = useRef;
React.useState = useState;
}

// packages\react\src\ReactHooks.js

export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}

export function useEffect(
create: () => mixed,
inputs: Array<mixed> | void | null
) {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, inputs);
}

// 这一步是在dom渲染的时候才拿到的
function resolveDispatcher() {
const dispatcher = ReactCurrentOwner.currentDispatcher;
return dispatcher;
}

// 渲染的时候从不同平台传进来的全局对象
const ReactCurrentOwner = {
current: (null: null | Fiber),
currentDispatcher: (null: null | Dispatcher),
};

hooks 都挂载在 ReactCurrentOwner 全局对象上

新的问题: ReactCurrentOwner 是什么

Children

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// packages\react\src\React.js
import { forEach, map, count, toArray, only } from "./ReactChildren";
const React = {
Children: {
map, // 和普通数组map区别在于最终结果是一维数组,不管你怎么嵌套
forEach,
count,
toArray,
only,
},
// ...
};

// packages\react\src\ReactChildren.js
function mapChildren(children, func, context) {
if (children == null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}

mapChildren 的处理流程如下图:
mapChildren

看懂流程图我们再深入源码

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// packages\react\src\ReactChildren.js
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
let escapedPrefix = "";
if (prefix != null) {
escapedPrefix = escapeUserProvidedKey(prefix) + "/";
}
const traverseContext = getPooledTraverseContext(
array,
escapedPrefix,
func,
context
);
traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
releaseTraverseContext(traverseContext);
}

// mapSingleChildIntoContext
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
// bookKeeping就是从对象池里面取出来的traverseContext
const { result, keyPrefix, func, context } = bookKeeping;

let mappedChild = func.call(context, child, bookKeeping.count++);
if (Array.isArray(mappedChild)) {
// 外层递归,此时对象池的作用就体现出来了
mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, (c) => c);
} else if (mappedChild != null) {
if (isValidElement(mappedChild)) {
// cloneAndReplaceKey
mappedChild = cloneAndReplaceKey(
mappedChild,
// Keep both the (mapped) and old keys if they differ, just as
// traverseAllChildren used to do for objects as children
keyPrefix +
(mappedChild.key && (!child || child.key !== mappedChild.key)
? escapeUserProvidedKey(mappedChild.key) + "/"
: "") +
childKey
);
}
result.push(mappedChild);
}
}

// traverseAllChildren
function traverseAllChildren(children, callback, traverseContext) {
if (children == null) {
return 0;
}

return traverseAllChildrenImpl(children, "", callback, traverseContext);
}

// traverseAllChildrenImpl
function traverseAllChildrenImpl(
children,
nameSoFar,
callback,
traverseContext
) {
const type = typeof children;

if (type === "undefined" || type === "boolean") {
// All of the above are perceived as null.
children = null;
}

let invokeCallback = false;

if (children === null) {
invokeCallback = true;
} else {
switch (type) {
case "string":
case "number":
invokeCallback = true;
break;
case "object":
switch (children.$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
invokeCallback = true;
}
}
}

if (invokeCallback) {
callback(
traverseContext,
children,
// If it's the only child, treat the name as if it was wrapped in an array
// so that it's consistent if the number of children grows.
nameSoFar === "" ? SEPARATOR + getComponentKey(children, 0) : nameSoFar
);
return 1;
}

let child;
let nextName;
let subtreeCount = 0; // Count of children found in the current subtree.
const nextNamePrefix =
nameSoFar === "" ? SEPARATOR : nameSoFar + SUBSEPARATOR;

if (Array.isArray(children)) {
// 开始递归调用
for (let i = 0; i < children.length; i++) {
child = children[i];
nextName = nextNamePrefix + getComponentKey(child, i);
subtreeCount += traverseAllChildrenImpl(
child,
nextName,
callback,
traverseContext
);
}
} else {
const iteratorFn = getIteratorFn(children);
if (typeof iteratorFn === "function") {
const iterator = iteratorFn.call(children);
let step;
let ii = 0;
while (!(step = iterator.next()).done) {
child = step.value;
nextName = nextNamePrefix + getComponentKey(child, ii++);
subtreeCount += traverseAllChildrenImpl(
child,
nextName,
callback,
traverseContext
);
}
}
}

return subtreeCount;
}

const POOL_SIZE = 10;
// 对象池
// mapFunction可能会频繁调用,对象频繁声明释放会很消耗内存,使用对象池保存对象引用,重复利用减少内存创建回收开销
// React合成事件系统中也用到了对象池进行性能优化
// 正常情况下对象池里面只有一个对象,但是如果像下面这样调用的时候对象池里面就不止一个了,真正的效果也就开始体现出来了
// React.Children.map(props.children, (c) => [c, [c, [c, c]]]) 对象池里面有三个
const traverseContextPool = [];
// getPooledTraverseContext
function getPooledTraverseContext(
mapResult,
keyPrefix,
mapFunction,
mapContext
) {
if (traverseContextPool.length) {
const traverseContext = traverseContextPool.pop();
traverseContext.result = mapResult;
traverseContext.keyPrefix = keyPrefix;
traverseContext.func = mapFunction;
traverseContext.context = mapContext;
traverseContext.count = 0;
return traverseContext;
} else {
return {
result: mapResult,
keyPrefix: keyPrefix,
func: mapFunction,
context: mapContext,
count: 0,
};
}
}

// releaseTraverseContext
function releaseTraverseContext(traverseContext) {
traverseContext.result = null;
traverseContext.keyPrefix = null;
traverseContext.func = null;
traverseContext.context = null;
traverseContext.count = 0;
if (traverseContextPool.length < POOL_SIZE) {
traverseContextPool.push(traverseContext);
}
}

两层递归,最终返回一维数组
学到一个优化: 对象池 pool

其他

  • memo

  • Fragment StrictMode

  • cloneElement createFactory

总结

React 只是创建了一些 DOM 节点,以及一些 ref 等,并没有具体的操作,好像是一个空壳子,而 React-DOM 和 React-Native 才是具体实现

案列 Demo


创建和更新

  • ReactDOM.render/hydrate

  • setState/replaceState[后者将会废弃]

  • forceUpdate

ReactDOM.render 步骤

  • 创建 ReactRoot

  • 创建 FiberRoot 和 RootFiber

  • 创建 更新

创建完更新,进入调度换节,调度换节不接暂时不讲

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
114
115
116
117
118
119
120
// packages\react-dom\src\client\ReactDOM.js
const ReactDOM = {
createPortal,
findDOMNode,
// hydrate 是 React 中提供在初次渲染的时候,去复用原本已经存在的 DOM 节点,减少重新生成节点以及删除原本 DOM 节点的开销,来加速初次渲染的功能。
// 主要使用场景是 服务端渲染或者像 prerender 等情况 。
// 与render区别在于第四个参数
hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
// TODO: throw or warn if we couldn't hydrate?
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
true,
callback
);
},
render(
element: React$Element<any>,
container: DOMContainer,
callback: ?Function
) {
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback
);
},

unstable_renderSubtreeIntoContainer,
unmountComponentAtNode,
unstable_createPortal, // TODO: remove in React 17
unstable_batchedUpdates: batchedUpdates,
unstable_interactiveUpdates: interactiveUpdates,
flushSync: flushSync,
unstable_createRoot: createRoot,
unstable_flushControlled: flushControlled,
};

export default ReactDOM;

// 先大致过一下render函数调用链

// packages\react-dom\src\client\ReactDOM.js
// ======================= //
// render
// legacyRenderSubtreeIntoContainer
// return new ReactRoot(container, isConcurrent, shouldHydrate);
// ReactRoot
// createContainer(container, isConcurrent, hydrate)
// ReactRoot.prototype.render = function(){...}
// ReactRoot.prototype.unmount = function(){...}
// ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function(){...}
// ReactRoot.prototype.createBatch = function(){...}

// packages\react-reconciler\src\ReactFiberReconciler.js
// ======================= //
// createContainer
// return createFiberRoot(containerInfo, isConcurrent, hydrate)

// packages\react-reconciler\src\ReactFiberRoot.js
// ======================= //
// createFiberRoot
// return root // 此处root就是Fiber节点数据对象 暂时停下

// 再详细看下render函数
// packages\react-dom\src\client\ReactDOM.js
ReactRoot.prototype.render = function (
children: ReactNodeList,
callback: ?() => mixed
): Work {
// ...
updateContainer(children, root, null, work._onCommit);
return work;
};

// packages\react-reconciler\src\ReactFiberReconciler.js
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function
): ExpirationTime {
const current = container.current;
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, current);
return updateContainerAtExpirationTime(
element,
container,
parentComponent,
expirationTime,
callback
);
}

export function updateContainerAtExpirationTime(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
expirationTime: ExpirationTime,
callback: ?Function
) {
// ...
return scheduleRootUpdate(current, element, expirationTime, callback);
}

function scheduleRootUpdate(
current: Fiber,
element: ReactNodeList,
expirationTime: ExpirationTime,
callback: ?Function
) {
const update = createUpdate(expirationTime);
flushPassiveEffects();
enqueueUpdate(current, update);
scheduleWork(current, expirationTime);
return expirationTime;
}

流程有点复杂,暂时先把流程记录下来

新的问题: hydrate 如何复用已有 DOM 节点?

FiberRoot

  • 整个应用的起点

  • 包含挂载的节点

  • 记录应用更新过程中的各种信息

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
// packages\react-reconciler\src\ReactFiberRoot.js
export function createFiberRoot(
containerInfo: any,
isConcurrent: boolean,
hydrate: boolean
): FiberRoot {
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber(isConcurrent);

let root = ({
// The currently active root fiber. This is the mutable root of the tree.
// 当前应用对应的Fiber对象,是Root Fiber
// 关于Fiber下节分享
current: uninitializedFiber,
// Any additional information from the host associated with this root.
// root节点,render方法接受的第二个参数
containerInfo: containerInfo,
// Used only by persistent updates.
// 只有在持久更新中会用到,也就是不支持增量更新的平台,react-dom不会用到
pendingChildren: null,

// Suspend和lazy相关
pingCache: null,

// The earliest and latest priority levels that are not known to be suspended.
// 标记最新和最老的不确定是否会挂起的任务
earliestPendingTime: NoWork,
latestPendingTime: NoWork,
// The following priority levels are used to distinguish between 1)
// uncommitted work, 2) uncommitted work that is suspended, and 3) uncommitted
// work that may be unsuspended. We choose not to track each individual
// pending level, trading granularity for performance.
// The earliest and latest priority levels that are suspended from committing.
// 标记最新和最老的在提交时候被挂起的任务
earliestSuspendedTime: NoWork,
latestSuspendedTime: NoWork,
// The latest priority level that was pinged by a resolved promise and can be retried.
// 标记最新的通过一个promise被resolve并且可以重新尝试的优先级任务
latestPingedTime: NoWork,

// If an error is thrown, and there are no more updates in the queue, we try
// rendering from the root one more time, synchronously, before handling
// the error.
// 如果有错误并且没有更多的更新存在,我们尝试在处理错误前同步重新从头渲染
// 在rednerRoot出现无法处理的错误的时候会被设置为true
didError: false,

// 标记正在等待提交的任务
pendingCommitExpirationTime: NoWork,
// A finished work-in-progress HostRoot that's ready to be committed.
// 已经完成的任务的FiberRoot对象,如果你只有一个Root,那它永远只可能是这个Root对下个的Fiber或者是null
// 在commit阶段只会处理这个值对应的任务
finishedWork: null,
// 在任务被挂起的时候通过setTimeout设置的返回内容,用来下一次如果有新的任务挂起时清理还没触发的timeout
// Timeout handle returned by setTimeout. Used to cancel a pending timeout, if
// it's superseded by a new one.
timeoutHandle: noTimeout,
// Top context object, used by renderSubtreeIntoContainer
// 顶层context对象,只有主动调用`renderSubtreeIntoContainer`时才会有用
context: null,
pendingContext: null,
// Determines if we should attempt to hydrate on the initial mount
// 用来确定第一次渲染的时候是否需要融合
hydrate,
// Remaining expiration time on this root.
// TODO: Lift this into the renderer
// 当前root上剩余的过期时间
nextExpirationTimeToWorkOn: NoWork,
// 当前更新对应的过期时间
expirationTime: NoWork,
// List of top-level batches. This list indicates whether a commit should be
// deferred. Also contains completion callbacks.
// TODO: Lift this into the renderer
// 顶层批次(批处理任务?)这个变量指明一个commit是否应该被推迟, 同时包括完成之后的回调
firstBatch: null,
// Linked-list of roots
// root之间关联的链表结构
nextScheduledRoot: null,
}

// FiberRoot ==> current ==> RootFiber
// RootFiber ==> stateNode ==> FiberRoot
uninitializedFiber.stateNode = root;

return root;
}

Fiber

  • 每一个 ReactElement 对应一个 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
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
// packages\react-reconciler\src\ReactFiber.js
export function createHostRootFiber(isConcurrent: boolean): Fiber {
let mode = isConcurrent ? ConcurrentMode | StrictMode : NoContext;

if (enableProfilerTimer && isDevToolsPresent) {
// Always collect profile timings when DevTools are present.
// This enables DevTools to start capturing timing at any point–
// Without some nodes in the tree having empty base times.
mode |= ProfileMode;
}

return createFiber(HostRoot, null, null, mode);
}

// This is a constructor function, rather than a POJO constructor, still
// please ensure we do the following:
// 1) Nobody should add any instance methods on this. Instance methods can be
// more difficult to predict when they get optimized and they are almost
// never inlined properly in static compilers.
// 2) Nobody should rely on `instanceof Fiber` for type testing. We should
// always know when it is a fiber.
// 3) We might want to experiment with using numeric keys since they are easier
// to optimize in a non-JIT environment.
// 4) We can easily go from a constructor to a createFiber object literal if that
// is faster.
// 5) It should be easy to port this to a C struct and keep a C implementation
// compatible.
const createFiber = function (
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode
): Fiber {
// $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
return new FiberNode(tag, pendingProps, key, mode);
};

// A Fiber is work on a Component that needs to be done or was done. There can
// be more than one per component.
// Fiber对应一个组件需要被处理或者已经处理了,一个组件可以有一个或者多个Fiber
export type Fiber = {|
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.

// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
// alternate versions of the tree. We put this on a single object for now to
// minimize the number of objects created during the initial render.

// Tag identifying the type of fiber.
// 标记不同的组件类型
tag: WorkTag,

// Unique identifier of this child.
// ReactElement里面的key
key: null | string,

// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
// ReactElement.type,也就是我们调用`createElement`的第一个参数
elementType: any,

// The resolved function/class/ associated with this fiber.
// 异步组件resolved之后返回的内容,一般是`function`或者`class`
type: any,

// The local state associated with this fiber.
// 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)
stateNode: any,

// Conceptual aliases
// parent : Instance -> return The parent happens to be the same as the
// return fiber since we've merged the fiber and instance.

// Remaining fields belong to Fiber

// The Fiber to return to after finishing processing this one.
// This is effectively the parent, but there can be multiple parents (two)
// so this is only the parent of the thing we're currently processing.
// It is conceptually the same as the return address of a stack frame.
// 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
return: Fiber | null,

// Singly Linked List Tree Structure.
// 单链表树结构
// 指向自己的第一个子节点
child: Fiber | null,
// 指向自己的兄弟结构
// 兄弟节点的return指向同一个父节点
// 所以我们会发现父节点不是指向所有子节点,而是指向第一个子结点,然后子结点之间通过sibling串联,并且所有子节点都通过return指向父节点
sibling: Fiber | null,
index: number,

// The ref last used to attach this node.
// I'll avoid adding an owner field for prod and model that as functions.
// ref属性
ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject,

// Input is the data coming into process this fiber. Arguments. Props.
// 新的变动带来的新的props
pendingProps: any, // This type will be more specific once we overload the tag.
// 上一次渲染完成之后的props
memoizedProps: any, // The props used to create the output.

// A queue of state updates and callbacks.
// 该Fiber对应的组件产生的Update会存放在这个队列里面
updateQueue: UpdateQueue<any> | null,

// The state used to create the output
// 上一次渲染的时候的state
memoizedState: any,

// A linked-list of contexts that this fiber depends on
// 一个列表,存放这个Fiber依赖的context
firstContextDependency: ContextDependency<mixed> | null,

// Bitfield that describes properties about the fiber and its subtree. E.g.
// the ConcurrentMode flag indicates whether the subtree should be async-by-
// default. When a fiber is created, it inherits the mode of its
// parent. Additional flags can be set at creation time, but after that the
// value should remain unchanged throughout the fiber's lifetime, particularly
// before its child fibers are created.
// 用来描述当前Fiber和他子树的`Bitfield`
// 共存的模式表示这个子树是否默认是异步渲染的
// Fiber被创建的时候他会继承父Fiber
// 其他的标识也可以在创建的时候被设置
// 但是在创建之后不应该再被修改,特别是他的子Fiber创建之前
mode: TypeOfMode,

// Effect
// 用来记录Side Effect
effectTag: SideEffectTag,

// Singly linked list fast path to the next fiber with side-effects.
// 单链表用来快速查找下一个side effect
nextEffect: Fiber | null,

// The first and last fiber with side-effect within this subtree. This allows
// us to reuse a slice of the linked list when we reuse the work done within
// this fiber.
// 子树中第一个side effect
firstEffect: Fiber | null,
// 子树中最后一个side effect
lastEffect: Fiber | null,

// Represents a time in the future by which this work should be completed.
// Does not include work found in its subtree.
// 代表任务在未来的哪个时间点应该被完成
// 不包括他的子树产生的任务
expirationTime: ExpirationTime,

// This is used to quickly determine if a subtree has no pending changes.
// 快速确定子树中是否有不在等待的变化
childExpirationTime: ExpirationTime,

// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
// 在Fiber树更新的过程中,每个Fiber都会有一个跟其对应的Fiber
// 我们称他为`current <==> workInProgress`
// 在渲染完成之后他们会交换位置
alternate: Fiber | null,

// 下面是调试相关的,收集每个Fiber和子树渲染时间的

// Time spent rendering this Fiber and its descendants for the current update.
// This tells us how well the tree makes use of sCU for memoization.
// It is reset to 0 each time we render and only updated when we don't bailout.
// This field is only set when the enableProfilerTimer flag is enabled.
actualDuration?: number,

// If the Fiber is currently active in the "render" phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.
actualStartTime?: number,

// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.
selfBaseDuration?: number,

// Sum of base times for all descedents of this Fiber.
// This value bubbles up during the "complete" phase.
// This field is only set when the enableProfilerTimer flag is enabled.
treeBaseDuration?: number,

// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
|};

Update & UpdateQueue

  • 用于记录组件状态的改变

  • 存放于 UpdateQueue 里面

  • 多个 Update 可以同时共存

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
// packages\react-reconciler\src\ReactUpdateQueue.js

export type Update<State> = {
// 更新的过期时间
expirationTime: ExpirationTime,

// export const UpdateState = 0;
// export const ReplaceState = 1;
// export const ForceUpdate = 2;
// export const CaptureUpdate = 3;
// 指定更新的类型,值为以上几种
tag: 0 | 1 | 2 | 3,
// 更新内容,比如`setState`接收的第一个参数
payload: any,
// 对应的回调,`setState`,`render`都有
callback: (() => mixed) | null,

// 指向下一个更新
next: Update<State> | null,
// 指向下一个`side effect`
nextEffect: Update<State> | null,
};

export type UpdateQueue<State> = {
// 每一次操作完更新之后的`state`
baseState: State,

// 队列中的第一个`Update`
firstUpdate: Update<State> | null,
// 队列中的最后一个`Update`
lastUpdate: Update<State> | null,

// 第一个捕获类型的`Update`
firstCapturedUpdate: Update<State> | null,
// 最后一个捕获类型的`Update`
lastCapturedUpdate: Update<State> | null,

// 第一个`side effect`
firstEffect: Update<State> | null,
// 最后一个`side effect`
lastEffect: Update<State> | null,

// 第一个和最后一个捕获产生的`side effect`
firstCapturedEffect: Update<State> | null,
lastCapturedEffect: Update<State> | null,
};

export function createUpdate(expirationTime: ExpirationTime): Update<*> {
return {
expirationTime: expirationTime,

tag: 0 | 1 | 2 | 3,

payload: null,
callback: null,

next: null,
nextEffect: null,
};
}

export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
const queue: UpdateQueue<State> = {
baseState,
firstUpdate: null,
lastUpdate: null,
firstCapturedUpdate: null,
lastCapturedUpdate: null,
firstEffect: null,
lastEffect: null,
firstCapturedEffect: null,
lastCapturedEffect: null,
};
return queue;
}

function cloneUpdateQueue<State>(
currentQueue: UpdateQueue<State>
): UpdateQueue<State> {
const queue: UpdateQueue<State> = {
baseState: currentQueue.baseState,
firstUpdate: currentQueue.firstUpdate,
lastUpdate: currentQueue.lastUpdate,

// TODO: With resuming, if we bail out and resuse the child tree, we should
// keep these effects.
firstCapturedUpdate: null,
lastCapturedUpdate: null,

firstEffect: null,
lastEffect: null,

firstCapturedEffect: null,
lastCapturedEffect: null,
};
return queue;
}

function appendUpdateToQueue<State>(
queue: UpdateQueue<State>,
update: Update<State>
) {
// Append the update to the end of the list.
if (queue.lastUpdate === null) {
// Queue is empty
queue.firstUpdate = queue.lastUpdate = update;
} else {
queue.lastUpdate.next = update;
queue.lastUpdate = update;
}
}

export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
// Update queues are created lazily.
const alternate = fiber.alternate;
let queue1;
let queue2;
if (alternate === null) {
// There's only one fiber.
queue1 = fiber.updateQueue;
queue2 = null;
if (queue1 === null) {
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
}
} else {
// There are two owners.
queue1 = fiber.updateQueue;
queue2 = alternate.updateQueue;
if (queue1 === null) {
if (queue2 === null) {
// Neither fiber has an update queue. Create new ones.
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
queue2 = alternate.updateQueue = createUpdateQueue(
alternate.memoizedState
);
} else {
// Only one fiber has an update queue. Clone to create a new one.
queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
}
} else {
if (queue2 === null) {
// Only one fiber has an update queue. Clone to create a new one.
queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
} else {
// Both owners have an update queue.
}
}
}
if (queue2 === null || queue1 === queue2) {
// There's only a single queue.
appendUpdateToQueue(queue1, update);
} else {
// There are two queues. We need to append the update to both queues,
// while accounting for the persistent structure of the list — we don't
// want the same update to be added multiple times.
if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
// One of the queues is not empty. We must add the update to both queues.
appendUpdateToQueue(queue1, update);
appendUpdateToQueue(queue2, update);
} else {
// Both queues are non-empty. The last update is the same in both lists,
// because of structural sharing. So, only append to one of the lists.
appendUpdateToQueue(queue1, update);
// But we still need to update the `lastUpdate` pointer of queue2.
queue2.lastUpdate = update;
}
}
}

expirationTime

1
2
3
4
5
6
7
8
// packages\react-reconciler\src\ReactFiberScheduler.js
// computeExpirationForFiber
// computeInteractiveExpiration

// packages\react-reconciler\src\ReactFiberExpirationTime.js
// computeInteractiveExpiration

// 最终计算公式:((((currentTime - 2 + 5000 / 10) / 25) | 0) + 1) * 25

不同类型的 expirationTime

  • Sync 模式,优先级最高

  • Asnyc/Concurrent 模式

  • 指定 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// packages\react-reconciler\src\ReactFiberScheduler.js
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
let expirationTime;
if (expirationContext !== NoWork) {
// An explicit expiration context was set;
expirationTime = expirationContext;
} else if (isWorking) {
if (isCommitting) {
// Updates that occur during the commit phase should have sync priority
// by default.
expirationTime = Sync;
} else {
// Updates during the render phase should expire at the same time as
// the work that is being rendered.
expirationTime = nextRenderExpirationTime;
}
} else {
// No explicit expiration context was set, and we're not currently
// performing work. Calculate a new expiration time.
if (fiber.mode & ConcurrentMode) {
// 往往是事件onClick等
if (isBatchingInteractiveUpdates) {
// This is an interactive update
expirationTime = computeInteractiveExpiration(currentTime);
} else {
// This is an async update
expirationTime = computeAsyncExpiration(currentTime);
}
// If we're in the middle of rendering a tree, do not update at the same
// expiration time that is already rendering.
if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
expirationTime -= 1;
}
} else {
// This is a sync update
expirationTime = Sync;
}
}
if (isBatchingInteractiveUpdates) {
// This is an interactive update. Keep track of the lowest pending
// interactive expiration time. This allows us to synchronously flush
// all interactive updates when needed.
if (
lowestPriorityPendingInteractiveExpirationTime === NoWork ||
expirationTime < lowestPriorityPendingInteractiveExpirationTime
) {
lowestPriorityPendingInteractiveExpirationTime = expirationTime;
}
}
return expirationTime;
}

// packages\react-reconciler\src\ReactTypeOfMode.js

export type TypeOfMode = number;

export const NoContext = 0b000;
export const ConcurrentMode = 0b001;
export const StrictMode = 0b010;
export const ProfileMode = 0b100;
// fiber.mode & ConcurrentMode表示fiber.mode中是否包含ConcurrentMode
// 使用二进制便于组合,可参考:https://blog.csdn.net/zl544434558/article/details/79252751
// 关于位运算的性只,可参考:https://www.jianshu.com/p/7faf7c22c146
// fiber.mode |= ConcurrentMode 添加类型ConcurrentMode
// fiber.mode ^ ConcurrentMode 提出类型ConcurrentMode

setState/forceUpdate

  • 给节点的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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// packages\react\src\ReactBaseClasses.js
Component.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, "setState");
};

Component.prototype.forceUpdate = function (callback) {
this.updater.enqueueForceUpdate(this, callback, "forceUpdate");
};

// packages\react-reconciler\src\ReactFiberClassComponent.js
const classComponentUpdater = {
isMounted,
// 可以看到和updateContainer方法流程是一样的
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);

const update = createUpdate(expirationTime);
update.payload = payload;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}

flushPassiveEffects();
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
// 和enqueueSetState唯一的差别就在类型上update.tag
enqueueForceUpdate(inst, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);

const update = createUpdate(expirationTime);
// enqueueForceUpdate标记类型是ForceUpdate
// export const UpdateState = 0;
// export const ReplaceState = 1;
// export const ForceUpdate = 2;
// export const CaptureUpdate = 3;
update.tag = ForceUpdate;

if (callback !== undefined && callback !== null) {
update.callback = callback;
}

flushPassiveEffects();
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
};

fiber-conciler

基于Fiber,给任务区分不同优先级,以更好地控制渲染

FiberReconciler 总体流程

  • TODO 流程图

scheduleWork

  • 找到更新对应的FiberRoot节点

  • 如果符合条件重置stack

  • 如果符合条件就请求工作调度

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
// packages\react-reconciler\src\ReactFiberScheduler.js
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
// 获取FiberRoot
const root = scheduleWorkToRoot(fiber, expirationTime);
if (root === null) {
return;
}

if (
!isWorking &&
nextRenderExpirationTime !== NoWork &&
expirationTime > nextRenderExpirationTime
) {
// This is an interruption. (Used for performance tracking.)
interruptedBy = fiber;
// 中断,优先执行高优先级渲染
resetStack();
}
markPendingPriorityLevel(root, expirationTime);
if (
// If we're in the render phase, we don't need to schedule this root
// for an update, because we'll do it before we exit...
!isWorking ||
isCommitting ||
// ...unless this is a different root than the one we're rendering.
nextRoot !== root
) {
const rootExpirationTime = root.expirationTime;
requestWork(root, rootExpirationTime);
}
if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
// Reset this back to zero so subsequent updates don't throw.
nestedUpdateCount = 0;
}
}

function resetStack() {
if (nextUnitOfWork !== null) {
let interruptedWork = nextUnitOfWork.return;
while (interruptedWork !== null) {
// undo setState
unwindInterruptedWork(interruptedWork);
interruptedWork = interruptedWork.return;
}
}

nextRoot = null;
nextRenderExpirationTime = NoWork;
nextLatestAbsoluteTimeoutMs = -1;
nextRenderDidError = false;
nextUnitOfWork = null;
}

// 遍历找到FiberRoot
// 并对Fiber的return和alternate的expirationTime做处理
function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
// Update the source fiber's expiration time
if (fiber.expirationTime < expirationTime) {
fiber.expirationTime = expirationTime;
}
let alternate = fiber.alternate;
if (alternate !== null && alternate.expirationTime < expirationTime) {
alternate.expirationTime = expirationTime;
}
// Walk the parent path to the root and update the child expiration time.
let node = fiber.return;
let root = null;
if (node === null && fiber.tag === HostRoot) {
root = fiber.stateNode;
} else {
while (node !== null) {
alternate = node.alternate;
if (node.childExpirationTime < expirationTime) {
node.childExpirationTime = expirationTime;
if (
alternate !== null &&
alternate.childExpirationTime < expirationTime
) {
alternate.childExpirationTime = expirationTime;
}
} else if (
alternate !== null &&
alternate.childExpirationTime < expirationTime
) {
alternate.childExpirationTime = expirationTime;
}
if (node.return === null && node.tag === HostRoot) {
root = node.stateNode;
break;
}
node = node.return;
}
}

return root;
}

requestWork

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
// requestWork is called by the scheduler whenever a root receives an update.
// It's up to the renderer to call renderRoot at some point in the future.
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
addRootToSchedule(root, expirationTime);
if (isRendering) {
// Prevent reentrancy. Remaining work will be scheduled at the end of
// the currently rendering batch.
return;
}

// 批处理 重要
if (isBatchingUpdates) {
// Flush work at the end of the batch.
if (isUnbatchingUpdates) {
// ...unless we're inside unbatchedUpdates, in which case we should
// flush it now.
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, false);
}
return;
}

// 开始进入调度
// TODO: Get rid of Sync and use current time?
if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpirationTime(root, expirationTime);
}
}

function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
// Add the root to the schedule.
// Check if this root is already part of the schedule.
if (root.nextScheduledRoot === null) {
// This root is not already scheduled. Add it.
root.expirationTime = expirationTime;
if (lastScheduledRoot === null) {
firstScheduledRoot = lastScheduledRoot = root;
root.nextScheduledRoot = root;
} else {
lastScheduledRoot.nextScheduledRoot = root;
lastScheduledRoot = root;
lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
}
} else {
// This root is already scheduled, but its priority may have increased.
const remainingExpirationTime = root.expirationTime;
if (expirationTime > remainingExpirationTime) {
// Update the priority.
root.expirationTime = expirationTime;
}
}
}

问题:performSyncWork 和 scheduleCallbackWithExpirationTime 有什么区别?

batchedUpdates

先看下案例演示 demos/batchedUpdates

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// packages\react-reconciler\src\ReactFiberScheduler.js
// TODO: Batching should be implemented at the renderer level, not inside
// the reconciler.
function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
const previousIsBatchingUpdates = isBatchingUpdates;
isBatchingUpdates = true;
try {
return fn(a);
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
}
}

问题:setState 是同步的还是异步的?

setState 方法本身调用是同步的,但是调用了 setState 并不标志着 state 立马更新,这个更新过程取决于当前执行环境的上下文,如果处于 batchedUpdates 状态,那 state 不是立马更新的。反之有可能是立马更新的

比如,flushSync 是立马更新的,而 ConcurrentMode 不是立马更新的

scheduler

  • 维护时间片

  • 模拟 requestIdleCallback

  • 调度列表和超时判断

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
// packages\react-reconciler\src\ReactFiberScheduler.js
import {
now,
scheduleDeferredCallback,
cancelDeferredCallback,
shouldYield,
prepareForCommit,
resetAfterCommit,
scheduleTimeout,
cancelTimeout,
noTimeout,
} from "./ReactFiberHostConfig";
function scheduleCallbackWithExpirationTime(
root: FiberRoot,
expirationTime: ExpirationTime
) {
if (callbackExpirationTime !== NoWork) {
// A callback is already scheduled. Check its expiration time (timeout).
if (expirationTime < callbackExpirationTime) {
// Existing callback has sufficient timeout. Exit.
return;
} else {
if (callbackID !== null) {
// Existing callback has insufficient timeout. Cancel and schedule a
// new one.
cancelDeferredCallback(callbackID);
}
}
// The request callback timer is already running. Don't start a new one.
} else {
startRequestCallbackTimer();
}

callbackExpirationTime = expirationTime;
const currentMs = now() - originalStartTimeMs;
const expirationTimeMs = expirationTimeToMs(expirationTime);
const timeout = expirationTimeMs - currentMs;
callbackID = scheduleDeferredCallback(performAsyncWork, { timeout });
}

// packages\react-dom\src\client\ReactDOMHostConfig.js
export {
unstable_now as now,
unstable_scheduleCallback as scheduleDeferredCallback,
unstable_shouldYield as shouldYield,
unstable_cancelCallback as cancelDeferredCallback,
} from "scheduler";

// packages\scheduler\src\Scheduler.js
export {
ImmediatePriority as unstable_ImmediatePriority,
UserBlockingPriority as unstable_UserBlockingPriority,
NormalPriority as unstable_NormalPriority,
IdlePriority as unstable_IdlePriority,
LowPriority as unstable_LowPriority,
unstable_runWithPriority,
unstable_scheduleCallback,
unstable_cancelCallback,
unstable_wrapCallback,
unstable_getCurrentPriorityLevel,
unstable_shouldYield,
unstable_continueExecution,
unstable_pauseExecution,
unstable_getFirstCallbackNode,
getCurrentTime as unstable_now,
};

function unstable_scheduleCallback(callback, deprecated_options) {
var startTime =
currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();

var expirationTime;
if (
typeof deprecated_options === "object" &&
deprecated_options !== null &&
typeof deprecated_options.timeout === "number"
) {
// FIXME: Remove this branch once we lift expiration times out of React.
expirationTime = startTime + deprecated_options.timeout;
} else {
switch (currentPriorityLevel) {
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
expirationTime = startTime + USER_BLOCKING_PRIORITY;
break;
case IdlePriority:
expirationTime = startTime + IDLE_PRIORITY;
break;
case LowPriority:
expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
}
}

var newNode = {
callback,
priorityLevel: currentPriorityLevel,
expirationTime,
next: null,
previous: null,
};

// Insert the new callback into the list, ordered first by expiration, then
// by insertion. So the new callback is inserted any other callback with
// equal expiration.
if (firstCallbackNode === null) {
// This is the first callback in the list.
firstCallbackNode = newNode.next = newNode.previous = newNode;
ensureHostCallbackIsScheduled();
} else {
var next = null;
var node = firstCallbackNode;
do {
if (node.expirationTime > expirationTime) {
// The new callback expires before this one.
next = node;
break;
}
node = node.next;
} while (node !== firstCallbackNode);

if (next === null) {
// No callback with a later expiration was found, which means the new
// callback has the latest expiration in the list.
next = firstCallbackNode;
} else if (next === firstCallbackNode) {
// The new callback has the earliest expiration in the entire list.
firstCallbackNode = newNode;
ensureHostCallbackIsScheduled();
}

var previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
}

return newNode;
}

function unstable_cancelCallback(callbackNode) {
var next = callbackNode.next;
if (next === null) {
// Already cancelled.
return;
}

if (next === callbackNode) {
// This is the only scheduled callback. Clear the list.
firstCallbackNode = null;
} else {
// Remove the callback from its position in the list.
if (callbackNode === firstCallbackNode) {
firstCallbackNode = next;
}
var previous = callbackNode.previous;
previous.next = next;
next.previous = previous;
}

callbackNode.next = callbackNode.previous = null;
}

function ensureHostCallbackIsScheduled() {
if (isExecutingCallback) {
// Don't schedule work yet; wait until the next time we yield.
return;
}
// Schedule the host callback using the earliest expiration in the list.
var expirationTime = firstCallbackNode.expirationTime;
if (!isHostCallbackScheduled) {
isHostCallbackScheduled = true;
} else {
// Cancel the existing host callback.
cancelHostCallback();
}
requestHostCallback(flushWork, expirationTime);
}

var hasNativePerformanceNow =
typeof performance === "object" && typeof performance.now === "function";
if (hasNativePerformanceNow) {
var Performance = performance;
getCurrentTime = function () {
return Performance.now();
};
} else {
getCurrentTime = function () {
return localDate.now();
};
}

// 模拟requestIdleCallback

var requestHostCallback;
var cancelHostCallback;
var shouldYieldToHost;

var globalValue = null;
if (typeof window !== "undefined") {
globalValue = window;
} else if (typeof global !== "undefined") {
globalValue = global;
}

if (globalValue && globalValue._schedMock) {
// Dynamic injection, only for testing purposes.
// 测试使用代码片段
// ...
} else if (
// If Scheduler runs in a non-DOM environment, it falls back to a naive
// implementation using setTimeout.
typeof window === "undefined" ||
// Check if MessageChannel is supported, too.
typeof MessageChannel !== "function"
) {
// 非浏览器环境,如JavaScriptCore,使用原生setTimeout实现
// ...
} else {
// 浏览器环境
// 开始模拟requestIdleCallback

var scheduledHostCallback = null;
var isMessageEventScheduled = false;
var timeoutTime = -1;

var isAnimationFrameScheduled = false;

var isFlushingHostCallback = false;

var frameDeadline = 0;
// We start out assuming that we run at 30fps but then the heuristic tracking
// will adjust this value to a faster fps if we get more frequent animation
// frames.
var previousFrameTime = 33;
var activeFrameTime = 33;

shouldYieldToHost = function () {
return frameDeadline <= getCurrentTime();
};

// We use the postMessage trick to defer idle work until after the repaint.
var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = function (event) {
isMessageEventScheduled = false;

var prevScheduledCallback = scheduledHostCallback;
var prevTimeoutTime = timeoutTime;
scheduledHostCallback = null;
timeoutTime = -1;

var currentTime = getCurrentTime();

var didTimeout = false;
if (frameDeadline - currentTime <= 0) {
// There's no time left in this idle period. Check if the callback has
// a timeout and whether it's been exceeded.
if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
// Exceeded the timeout. Invoke the callback even though there's no
// time left.
didTimeout = true;
} else {
// No timeout.
if (!isAnimationFrameScheduled) {
// Schedule another animation callback so we retry later.
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
// Exit without invoking the callback.
scheduledHostCallback = prevScheduledCallback;
timeoutTime = prevTimeoutTime;
return;
}
}

if (prevScheduledCallback !== null) {
isFlushingHostCallback = true;
try {
prevScheduledCallback(didTimeout);
} finally {
isFlushingHostCallback = false;
}
}
};

var animationTick = function (rafTime) {
if (scheduledHostCallback !== null) {
// Eagerly schedule the next animation callback at the beginning of the
// frame. If the scheduler queue is not empty at the end of the frame, it
// will continue flushing inside that callback. If the queue *is* empty,
// then it will exit immediately. Posting the callback at the start of the
// frame ensures it's fired within the earliest possible frame. If we
// waited until the end of the frame to post the callback, we risk the
// browser skipping a frame and not firing the callback until the frame
// after that.
requestAnimationFrameWithTimeout(animationTick);
} else {
// No pending work. Exit.
isAnimationFrameScheduled = false;
return;
}

var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
if (
nextFrameTime < activeFrameTime &&
previousFrameTime < activeFrameTime
) {
if (nextFrameTime < 8) {
// Defensive coding. We don't support higher frame rates than 120hz.
// If the calculated frame time gets lower than 8, it is probably a bug.
nextFrameTime = 8;
}
// If one frame goes long, then the next one can be short to catch up.
// If two frames are short in a row, then that's an indication that we
// actually have a higher frame rate than what we're currently optimizing.
// We adjust our heuristic dynamically accordingly. For example, if we're
// running on 120hz display or 90hz VR display.
// Take the max of the two in case one of them was an anomaly due to
// missed frame deadlines.
activeFrameTime =
nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
} else {
previousFrameTime = nextFrameTime;
}
frameDeadline = rafTime + activeFrameTime;
if (!isMessageEventScheduled) {
isMessageEventScheduled = true;
port.postMessage(undefined);
}
};

requestHostCallback = function (callback, absoluteTimeout) {
scheduledHostCallback = callback;
timeoutTime = absoluteTimeout;
if (isFlushingHostCallback || absoluteTimeout < 0) {
// Don't wait for the next frame. Continue working ASAP, in a new event.
port.postMessage(undefined);
} else if (!isAnimationFrameScheduled) {
// If rAF didn't already schedule one, we need to schedule a frame.
// TODO: If this rAF doesn't materialize because the browser throttles, we
// might want to still have setTimeout trigger rIC as a backup to ensure
// that we keep performing work.
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
};

cancelHostCallback = function () {
scheduledHostCallback = null;
isMessageEventScheduled = false;
timeoutTime = -1;
};
}

// 将所有过期任务强制输出
function flushWork(didTimeout) {
// Exit right away if we're currently paused

if (enableSchedulerDebugging && isSchedulerPaused) {
return;
}

isExecutingCallback = true;
const previousDidTimeout = currentDidTimeout;
currentDidTimeout = didTimeout;
try {
if (didTimeout) {
// Flush all the expired callbacks without yielding.
while (
firstCallbackNode !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
// TODO Wrap i nfeature flag
// Read the current time. Flush all the callbacks that expire at or
// earlier than that time. Then read the current time again and repeat.
// This optimizes for as few performance.now calls as possible.
var currentTime = getCurrentTime();
// 此处循环链表,执行flushFirstCallback直到第一个不超时的任务
if (firstCallbackNode.expirationTime <= currentTime) {
do {
flushFirstCallback();
} while (
firstCallbackNode !== null &&
firstCallbackNode.expirationTime <= currentTime &&
!(enableSchedulerDebugging && isSchedulerPaused)
);
continue;
}
break;
}
} else {
// Keep flushing callbacks until we run out of time in the frame.
if (firstCallbackNode !== null) {
do {
if (enableSchedulerDebugging && isSchedulerPaused) {
break;
}
flushFirstCallback();
} while (firstCallbackNode !== null && !shouldYieldToHost()); // 帧时间还有的时候
}
}
} finally {
isExecutingCallback = false;
currentDidTimeout = previousDidTimeout;
if (firstCallbackNode !== null) {
// There's still work remaining. Request another callback.
ensureHostCallbackIsScheduled();
} else {
isHostCallbackScheduled = false;
}
}
}

performWork

  • 是否有 Deadline 区分

  • 循环渲染 Root 的条件

  • 超过时间片的处理

renderRoot

  • 调用 workLoop 进行循环单元更新

  • 捕获错误并进行处理

  • 走完流程之后进行善后

1
2
// packages\react-reconciler\src\ReactFiberScheduler.js
// renderRoot ==> workLoop

节点更新 beginWork

beginWork

workLoop ==> performUnitOfWork ==> beginWork ==> completeUnitOfWork

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
// packages\react-reconciler\src\ReactFiberBeginWork.js
// beginWork
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime
): Fiber | null {
const updateExpirationTime = workInProgress.expirationTime;

if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps === newProps &&
!hasLegacyContextChanged() &&
updateExpirationTime < renderExpirationTime
) {
// This fiber does not have any pending work. Bailout without entering
// the begin phase. There's still some bookkeeping we that needs to be done
// in this optimized path, mostly pushing stuff onto the stack.
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
resetHydrationState();
break;
case HostComponent:
pushHostContext(workInProgress);
break;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
}
break;
}
case HostPortal:
pushHostContainer(
workInProgress,
workInProgress.stateNode.containerInfo
);
break;
case ContextProvider: {
const newValue = workInProgress.memoizedProps.value;
pushProvider(workInProgress, newValue);
break;
}
case Profiler:
if (enableProfilerTimer) {
workInProgress.effectTag |= Update;
}
break;
case SuspenseComponent: {
const state: SuspenseState | null = workInProgress.memoizedState;
const didTimeout = state !== null;
if (didTimeout) {
// If this boundary is currently timed out, we need to decide
// whether to retry the primary children, or to skip over it and
// go straight to the fallback. Check the priority of the primary
// child fragment.
const primaryChildFragment: Fiber = (workInProgress.child: any);
const primaryChildExpirationTime =
primaryChildFragment.childExpirationTime;
if (
primaryChildExpirationTime !== NoWork &&
primaryChildExpirationTime >= renderExpirationTime
) {
// The primary children have pending work. Use the normal path
// to attempt to render the primary children again.
return updateSuspenseComponent(
current,
workInProgress,
renderExpirationTime
);
} else {
// The primary children do not have pending work with sufficient
// priority. Bailout.
// 跳过更新
const child = bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime
);
if (child !== null) {
// The fallback children have pending work. Skip over the
// primary children and work on the fallback.
return child.sibling;
} else {
return null;
}
}
}
break;
}
}
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime
);
}
}

// Before entering the begin phase, clear the expiration time.
workInProgress.expirationTime = NoWork;

switch (workInProgress.tag) {
case IndeterminateComponent: {
const elementType = workInProgress.elementType;
return mountIndeterminateComponent(
current,
workInProgress,
elementType,
renderExpirationTime
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
updateExpirationTime,
renderExpirationTime
);
}
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderExpirationTime);
case HostComponent:
return updateHostComponent(current, workInProgress, renderExpirationTime);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(
current,
workInProgress,
renderExpirationTime
);
case HostPortal:
return updatePortalComponent(
current,
workInProgress,
renderExpirationTime
);
case ForwardRef: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === type
? unresolvedProps
: resolveDefaultProps(type, unresolvedProps);
return updateForwardRef(
current,
workInProgress,
type,
resolvedProps,
renderExpirationTime
);
}
case Fragment:
return updateFragment(current, workInProgress, renderExpirationTime);
case Mode:
return updateMode(current, workInProgress, renderExpirationTime);
case Profiler:
return updateProfiler(current, workInProgress, renderExpirationTime);
case ContextProvider:
return updateContextProvider(
current,
workInProgress,
renderExpirationTime
);
case ContextConsumer:
return updateContextConsumer(
current,
workInProgress,
renderExpirationTime
);
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
updateExpirationTime,
renderExpirationTime
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
updateExpirationTime,
renderExpirationTime
);
}
case IncompleteClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return mountIncompleteClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime
);
}
default:
invariant(
false,
"Unknown unit of work tag. This error is likely caused by a bug in " +
"React. Please file an issue."
);
}
}

FunctionComponent

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
// packages\react-reconciler\src\ReactFiberBeginWork.js
// ...
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Suspend相关
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
// ...

function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderExpirationTime,
) {
const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
const context = getMaskedContext(workInProgress, unmaskedContext);

let nextChildren;
prepareToReadContext(workInProgress, renderExpirationTime);
prepareToUseHooks(current, workInProgress, renderExpirationTime);
// 函数组件里面又传入了第二个参数context
nextChildren = Component(nextProps, context);
nextChildren = finishHooks(Component, nextProps, nextChildren, context);

// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
// 核心步骤,用于调和子结点nextChildren
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}

reconcileChildren

  • 根据 props.children 生成 Fiber 子树

  • 判断 Fiber 对象是否可以复用

  • 列表根据 key 优化

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
// packages\react-reconciler\src\ReactFiberBeginWork.js
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderExpirationTime: ExpirationTime
) {
if (current === null) {
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime
);
} else {
// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.

// If we had any progressed work already, that is invalid at this point so
// let's throw it out.
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderExpirationTime
);
}
}

// packages\react-reconciler\src\ReactChildFiber.js
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

// This wrapper function exists because I expect to clone the code in each path
// to be able to optimize each path individually by branching early. This needs
// a compiler or we can do it manually. Helpers that don't need this branching
// live outside of this function.
function ChildReconciler(shouldTrackSideEffects) {
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
if (!shouldTrackSideEffects) {
// Noop.
return;
}
// Deletions are added in reversed order so we add it to the front.
// At this point, the return fiber's effect list is empty except for
// deletions, so we can just append the deletion to the list. The remaining
// effects aren't added until the complete phase. Once we implement
// resuming, this may not be true.
const last = returnFiber.lastEffect;
if (last !== null) {
last.nextEffect = childToDelete;
returnFiber.lastEffect = childToDelete;
} else {
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
}
childToDelete.nextEffect = null;
childToDelete.effectTag = Deletion;
}

function deleteRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber | null
): null {
if (!shouldTrackSideEffects) {
// Noop.
return null;
}

// TODO: For the shouldClone case, this could be micro-optimized a bit by
// assuming that after the first child we've already added everything.
let childToDelete = currentFirstChild;
while (childToDelete !== null) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
return null;
}

function mapRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber
): Map<string | number, Fiber> {
// Add the remaining children to a temporary map so that we can find them by
// keys quickly. Implicit (null) keys get added to this set with their index
// instead.
const existingChildren: Map<string | number, Fiber> = new Map();

let existingChild = currentFirstChild;
while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
return existingChildren;
}

function useFiber(
fiber: Fiber,
pendingProps: mixed,
expirationTime: ExpirationTime
): Fiber {
// We currently set sibling to null and index to 0 here because it is easy
// to forget to do before returning it. E.g. for the single child case.
const clone = createWorkInProgress(fiber, pendingProps, expirationTime);
clone.index = 0;
clone.sibling = null;
return clone;
}

function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number
): number {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
// Noop.
return lastPlacedIndex;
}
const current = newFiber.alternate;
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
newFiber.effectTag = Placement;
return lastPlacedIndex;
} else {
// This item can stay in place.
return oldIndex;
}
} else {
// This is an insertion.
newFiber.effectTag = Placement;
return lastPlacedIndex;
}
}

function placeSingleChild(newFiber: Fiber): Fiber {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.effectTag = Placement;
}
return newFiber;
}

function updateTextNode(
returnFiber: Fiber,
current: Fiber | null,
textContent: string,
expirationTime: ExpirationTime
) {
if (current === null || current.tag !== HostText) {
// Insert
const created = createFiberFromText(
textContent,
returnFiber.mode,
expirationTime
);
created.return = returnFiber;
return created;
} else {
// Update
const existing = useFiber(current, textContent, expirationTime);
existing.return = returnFiber;
return existing;
}
}

function updateElement(
returnFiber: Fiber,
current: Fiber | null,
element: ReactElement,
expirationTime: ExpirationTime
): Fiber {
if (current !== null && current.elementType === element.type) {
// Move based on index
const existing = useFiber(current, element.props, expirationTime);
existing.ref = coerceRef(returnFiber, current, element);
existing.return = returnFiber;
return existing;
} else {
// Insert
const created = createFiberFromElement(
element,
returnFiber.mode,
expirationTime
);
created.ref = coerceRef(returnFiber, current, element);
created.return = returnFiber;
return created;
}
}

function updatePortal(
returnFiber: Fiber,
current: Fiber | null,
portal: ReactPortal,
expirationTime: ExpirationTime
): Fiber {
if (
current === null ||
current.tag !== HostPortal ||
current.stateNode.containerInfo !== portal.containerInfo ||
current.stateNode.implementation !== portal.implementation
) {
// Insert
const created = createFiberFromPortal(
portal,
returnFiber.mode,
expirationTime
);
created.return = returnFiber;
return created;
} else {
// Update
const existing = useFiber(current, portal.children || [], expirationTime);
existing.return = returnFiber;
return existing;
}
}

function updateFragment(
returnFiber: Fiber,
current: Fiber | null,
fragment: Iterable<*>,
expirationTime: ExpirationTime,
key: null | string
): Fiber {
if (current === null || current.tag !== Fragment) {
// Insert
const created = createFiberFromFragment(
fragment,
returnFiber.mode,
expirationTime,
key
);
created.return = returnFiber;
return created;
} else {
// Update
const existing = useFiber(current, fragment, expirationTime);
existing.return = returnFiber;
return existing;
}
}

function createChild(
returnFiber: Fiber,
newChild: any,
expirationTime: ExpirationTime
): Fiber | null {
if (typeof newChild === "string" || typeof newChild === "number") {
// Text nodes don't have keys. If the previous node is implicitly keyed
// we can continue to replace it without aborting even if it is not a text
// node.
const created = createFiberFromText(
"" + newChild,
returnFiber.mode,
expirationTime
);
created.return = returnFiber;
return created;
}

if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
const created = createFiberFromElement(
newChild,
returnFiber.mode,
expirationTime
);
created.ref = coerceRef(returnFiber, null, newChild);
created.return = returnFiber;
return created;
}
case REACT_PORTAL_TYPE: {
const created = createFiberFromPortal(
newChild,
returnFiber.mode,
expirationTime
);
created.return = returnFiber;
return created;
}
}

if (isArray(newChild) || getIteratorFn(newChild)) {
const created = createFiberFromFragment(
newChild,
returnFiber.mode,
expirationTime,
null
);
created.return = returnFiber;
return created;
}

throwOnInvalidObjectType(returnFiber, newChild);
}

return null;
}

function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
expirationTime: ExpirationTime
): Fiber | null {
// Update the fiber if the keys match, otherwise return null.

const key = oldFiber !== null ? oldFiber.key : null;

if (typeof newChild === "string" || typeof newChild === "number") {
// Text nodes don't have keys. If the previous node is implicitly keyed
// we can continue to replace it without aborting even if it is not a text
// node.
if (key !== null) {
return null;
}
return updateTextNode(
returnFiber,
oldFiber,
"" + newChild,
expirationTime
);
}

if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
if (newChild.type === REACT_FRAGMENT_TYPE) {
return updateFragment(
returnFiber,
oldFiber,
newChild.props.children,
expirationTime,
key
);
}
return updateElement(
returnFiber,
oldFiber,
newChild,
expirationTime
);
} else {
return null;
}
}
case REACT_PORTAL_TYPE: {
if (newChild.key === key) {
return updatePortal(
returnFiber,
oldFiber,
newChild,
expirationTime
);
} else {
return null;
}
}
}

if (isArray(newChild) || getIteratorFn(newChild)) {
if (key !== null) {
return null;
}

return updateFragment(
returnFiber,
oldFiber,
newChild,
expirationTime,
null
);
}

throwOnInvalidObjectType(returnFiber, newChild);
}
return null;
}

function updateFromMap(
existingChildren: Map<string | number, Fiber>,
returnFiber: Fiber,
newIdx: number,
newChild: any,
expirationTime: ExpirationTime
): Fiber | null {
if (typeof newChild === "string" || typeof newChild === "number") {
// Text nodes don't have keys, so we neither have to check the old nor
// new node for the key. If both are text nodes, they match.
const matchedFiber = existingChildren.get(newIdx) || null;
return updateTextNode(
returnFiber,
matchedFiber,
"" + newChild,
expirationTime
);
}

if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
const matchedFiber =
existingChildren.get(
newChild.key === null ? newIdx : newChild.key
) || null;
if (newChild.type === REACT_FRAGMENT_TYPE) {
return updateFragment(
returnFiber,
matchedFiber,
newChild.props.children,
expirationTime,
newChild.key
);
}
return updateElement(
returnFiber,
matchedFiber,
newChild,
expirationTime
);
}
case REACT_PORTAL_TYPE: {
const matchedFiber =
existingChildren.get(
newChild.key === null ? newIdx : newChild.key
) || null;
return updatePortal(
returnFiber,
matchedFiber,
newChild,
expirationTime
);
}
}

if (isArray(newChild) || getIteratorFn(newChild)) {
const matchedFiber = existingChildren.get(newIdx) || null;
return updateFragment(
returnFiber,
matchedFiber,
newChild,
expirationTime,
null
);
}

throwOnInvalidObjectType(returnFiber, newChild);
}

return null;
}

// 通过key尽可能地复用可复用的Fiber节点,减少对象声明和内存回收的损耗
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
expirationTime: ExpirationTime
): Fiber | null {
// This algorithm can't optimize by searching from boths ends since we
// don't have backpointers on fibers. I'm trying to see how far we can get
// with that model. If it ends up not being worth the tradeoffs, we can
// add it later.

// Even with a two ended optimization, we'd want to optimize for the case
// where there are few changes and brute force the comparison instead of
// going for the Map. It'd like to explore hitting that path first in
// forward-only mode and only go for the Map once we notice that we need
// lots of look ahead. This doesn't handle reversal as well as two ended
// search but that's unusual. Besides, for the two ended optimization to
// work on Iterables, we'd need to copy the whole set.

// In this first iteration, we'll just live with hitting the bad case
// (adding everything to a Map) in for every insert/move.

// If you change this code, also update reconcileChildrenIterator() which
// uses the same algorithm.
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;

let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
expirationTime
);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}

if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}

if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
expirationTime
);
if (!newFiber) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}

// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

// Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
expirationTime
);
if (newFiber) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}

if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach((child) => deleteChild(returnFiber, child));
}

return resultingFirstChild;
}

function reconcileChildrenIterator(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildrenIterable: Iterable<*>,
expirationTime: ExpirationTime
): Fiber | null {
// This is the same implementation as reconcileChildrenArray(),
// but using the iterator instead.

const iteratorFn = getIteratorFn(newChildrenIterable);
const newChildren = iteratorFn.call(newChildrenIterable);
invariant(newChildren != null, "An iterable object provided no iterator.");

let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;

let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;

let step = newChildren.next();
for (
;
oldFiber !== null && !step.done;
newIdx++, step = newChildren.next()
) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
step.value,
expirationTime
);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (!oldFiber) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
// need to delete the existing child.
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}

if (step.done) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}

if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; !step.done; newIdx++, step = newChildren.next()) {
const newFiber = createChild(returnFiber, step.value, expirationTime);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}

// Add all children to a key map for quick lookups.
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

// Keep scanning and use the map to restore deleted items as moves.
for (; !step.done; newIdx++, step = newChildren.next()) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
step.value,
expirationTime
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}

if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach((child) => deleteChild(returnFiber, child));
}

return resultingFirstChild;
}

function reconcileSingleTextNode(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
textContent: string,
expirationTime: ExpirationTime
): Fiber {
// There's no need to check for keys on text nodes since we don't have a
// way to define them.
if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
// We already have an existing node so let's just update it and delete
// the rest.
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
const existing = useFiber(currentFirstChild, textContent, expirationTime);
existing.return = returnFiber;
return existing;
}
// The existing first child is not a text node so we need to create one
// and delete the existing ones.
deleteRemainingChildren(returnFiber, currentFirstChild);
const created = createFiberFromText(
textContent,
returnFiber.mode,
expirationTime
);
created.return = returnFiber;
return created;
}

function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
expirationTime: ExpirationTime
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
if (
child.tag === Fragment
? element.type === REACT_FRAGMENT_TYPE
: child.elementType === element.type
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(
child,
element.type === REACT_FRAGMENT_TYPE
? element.props.children
: element.props,
expirationTime
);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
} else {
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}

if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
expirationTime,
element.key
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(
element,
returnFiber.mode,
expirationTime
);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}

function reconcileSinglePortal(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
portal: ReactPortal,
expirationTime: ExpirationTime
): Fiber {
const key = portal.key;
let child = currentFirstChild;
while (child !== null) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
if (
child.tag === HostPortal &&
child.stateNode.containerInfo === portal.containerInfo &&
child.stateNode.implementation === portal.implementation
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(
child,
portal.children || [],
expirationTime
);
existing.return = returnFiber;
return existing;
} else {
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}

const created = createFiberFromPortal(
portal,
returnFiber.mode,
expirationTime
);
created.return = returnFiber;
return created;
}

// This API will tag the children with the side-effect of the reconciliation
// itself. They will be added to the side-effect list as we pass through the
// children and the parent.
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
expirationTime: ExpirationTime
): Fiber | null {
// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
// not as a fragment. Nested arrays on the other hand will be treated as
// fragment nodes. Recursion happens at the normal flow.

// Handle top level unkeyed fragments as if they were arrays.
// This leads to an ambiguity between <>{[...]}</> and <>...</>.
// We treat the ambiguous cases above the same.
const isUnkeyedTopLevelFragment =
typeof newChild === "object" &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}

// Handle object types
const isObject = typeof newChild === "object" && newChild !== null;

if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
expirationTime
)
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
expirationTime
)
);
}
}

if (typeof newChild === "string" || typeof newChild === "number") {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
"" + newChild,
expirationTime
)
);
}

// 数组
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
expirationTime
);
}

if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
expirationTime
);
}

if (isObject) {
throwOnInvalidObjectType(returnFiber, newChild);
}
// ...
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}

return reconcileChildFibers;
}

ClassComponent

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
// packages\react-reconciler\src\ReactFiberBeginWork.js
function updateClassComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps,
renderExpirationTime: ExpirationTime
) {
// Push context providers early to prevent context stack mismatches.
// During mounting we don't know the child context yet as the instance doesn't exist.
// We will invalidate the child context in finishClassComponent() right after rendering.
let hasContext;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
hasContext = false;
}
prepareToReadContext(workInProgress, renderExpirationTime);

const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
if (current !== null) {
// An class component without an instance only mounts if it suspended
// inside a non- concurrent tree, in an inconsistent state. We want to
// tree it like a new mount, even though an empty version of it already
// committed. Disconnect the alternate pointers.
current.alternate = null;
workInProgress.alternate = null;
// Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.effectTag |= Placement;
}
// In the initial pass we might need to construct the instance.
constructClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime
);
mountClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime
);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime
);
} else {
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderExpirationTime
);
}
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime
);
return nextUnitOfWork;
}

// packages\react-reconciler\src\ReactFiberClassComponent.js

IndeterminateComponent

  • FunctionComponent 首次渲染初始化的时候就是 IndeterminateComponent
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
function mountIndeterminateComponent(
_current,
workInProgress,
Component,
renderExpirationTime
) {
if (_current !== null) {
// An indeterminate component only mounts if it suspended inside a non-
// concurrent tree, in an inconsistent state. We want to treat it like
// a new mount, even though an empty version of it already committed.
// Disconnect the alternate pointers.
_current.alternate = null;
workInProgress.alternate = null;
// Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.effectTag |= Placement;
}

const props = workInProgress.pendingProps;
const unmaskedContext = getUnmaskedContext(workInProgress, Component, false);
const context = getMaskedContext(workInProgress, unmaskedContext);

prepareToReadContext(workInProgress, renderExpirationTime);
prepareToUseHooks(null, workInProgress, renderExpirationTime);

let value = Component(props, context);
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;

if (
// 此条件下面会举个列子
typeof value === "object" &&
value !== null &&
typeof value.render === "function" &&
value.$$typeof === undefined
) {
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;

// Throw out any hooks that were used.
resetHooks();

// Push context providers early to prevent context stack mismatches.
// During mounting we don't know the child context yet as the instance doesn't exist.
// We will invalidate the child context in finishClassComponent() right after rendering.
let hasContext = false;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
hasContext = false;
}

workInProgress.memoizedState =
value.state !== null && value.state !== undefined ? value.state : null;

const getDerivedStateFromProps = Component.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === "function") {
applyDerivedStateFromProps(
workInProgress,
Component,
getDerivedStateFromProps,
props
);
}

adoptClassInstance(workInProgress, value);
mountClassInstance(workInProgress, Component, props, renderExpirationTime);
return finishClassComponent(
null,
workInProgress,
Component,
true,
hasContext,
renderExpirationTime
);
} else {
// Proceed under the assumption that this is a function component
workInProgress.tag = FunctionComponent;
value = finishHooks(Component, props, value, context);
reconcileChildren(null, workInProgress, value, renderExpirationTime);
return workInProgress.child;
}
}

// 举例Demo
// 说明此时`let value = Component(props, context);`得到的结果会进入下面的if最终
// `workInProgress.tag = ClassComponent;` 即被认为是ClassComponent
import React, { useEffect } from "react";

export default function IndeterminateComponentDemo() {
// 这样写就会报错,既然是ClassComponent了
// useEffect(() => {
// console.log("useEffect invoked");
// return () => {
// console.log("useEffect cleanup");
// };
// }, []);
return {
componentDidMount() {
console.log("componentDidMount invoked");
},
render() {
return <h2>IndeterminateComponentDemo</h2>;
},
};
}

HostRoot 的更新

HostComponent 和 HostText 的更新

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
// packages\react-reconciler\src\ReactFiberBeginWork.js
function updateHostComponent(current, workInProgress, renderExpirationTime) {
pushHostContext(workInProgress); // 为什么原生节点需要context

if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}

const type = workInProgress.type;
const nextProps = workInProgress.pendingProps;
const prevProps = current !== null ? current.memoizedProps : null;

let nextChildren = nextProps.children;
const isDirectTextChild = shouldSetTextContent(type, nextProps);

if (isDirectTextChild) {
// We special case a direct text child of a host node. This is a common
// case. We won't handle it as a reified child. We will instead handle
// this in the host environment that also have access to this prop. That
// avoids allocating another HostText fiber and traversing it.
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
// If we're switching from a direct text child to a normal child, or to
// empty, we need to schedule the text content to be reset.
workInProgress.effectTag |= ContentReset;
}

markRef(current, workInProgress);

// Check the host config to see if the children are offscreen/hidden.
// 设置hidden意味着永远不会被更新
if (
renderExpirationTime !== Never &&
workInProgress.mode & ConcurrentMode &&
shouldDeprioritizeSubtree(type, nextProps)
) {
// Schedule this fiber to re-render at offscreen priority. Then bailout.
workInProgress.expirationTime = Never;
return null;
}

reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime
);
return workInProgress.child;
}

function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
}
// Nothing to do here. This is terminal. We'll do the completion step
// immediately after.
return null;
}

Portal 的更新

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
// packages\react-dom\src\client\ReactDOM.js
function createPortal(
children: ReactNodeList,
container: DOMContainer,
key: ?string = null
) {
// TODO: pass ReactDOM portal implementation as third argument
return createPortalImpl(children, container, null, key);
}

// packages\shared\ReactPortal.js
export function createPortal(
children: ReactNodeList,
containerInfo: any,
// TODO: figure out the API for cross-renderer implementation.
implementation: any,
key: ?string = null
): ReactPortal {
return {
// This tag allow us to uniquely identify this as a React Portal
$$typeof: REACT_PORTAL_TYPE,
key: key == null ? null : "" + key,
children,
containerInfo,
implementation,
};
}

// packages\react-reconciler\src\ReactChildFiber.js
// reconcileSinglePortal

// packages\react-reconciler\src\ReactFiberBeginWork.js
// updatePortalComponent

ForwardRef 的更新

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
function updateForwardRef(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderExpirationTime: ExpirationTime
) {
const render = Component.render;
const ref = workInProgress.ref;

// The rest is a fork of updateFunctionComponent
let nextChildren;
prepareToReadContext(workInProgress, renderExpirationTime);
prepareToUseHooks(current, workInProgress, renderExpirationTime);
nextChildren = render(nextProps, ref);
nextChildren = finishHooks(render, nextProps, nextChildren, ref);

// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime
);
return workInProgress.child;
}

Memo 组件的更新

更新完成 completeUnitOfWork

  • 根据是否有中断调用不同的处理

  • 判断是否有兄弟节点调用不同的处理

  • 完成节点后赋值 effect 链

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// packages\react-reconciler\src\ReactFiberScheduler.js
// performUnitOfWork
// beginWork
// completeUnitOfWork
function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
// Attempt to complete the current unit of work, then move to the
// next sibling. If there are no more siblings, return to the
// parent fiber.
while (true) {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
// progress.
const current = workInProgress.alternate;
const returnFiber = workInProgress.return;
const siblingFiber = workInProgress.sibling;

if ((workInProgress.effectTag & Incomplete) === NoEffect) {
// This fiber completed.
// Remember we're completing this unit so we can find a boundary if it fails.
nextUnitOfWork = workInProgress;
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
startProfilerTimer(workInProgress);
}
nextUnitOfWork = completeWork(
current,
workInProgress,
nextRenderExpirationTime
);
if (workInProgress.mode & ProfileMode) {
// Update render duration assuming we didn't error.
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
}
} else {
nextUnitOfWork = completeWork(
current,
workInProgress,
nextRenderExpirationTime
);
}

stopWorkTimer(workInProgress);
resetChildExpirationTime(workInProgress, nextRenderExpirationTime);

if (nextUnitOfWork !== null) {
// Completing this fiber spawned new work. Work on that next.
return nextUnitOfWork;
}

if (
returnFiber !== null &&
// Do not append effects to parents if a sibling failed to complete
(returnFiber.effectTag & Incomplete) === NoEffect
) {
// Append all the effects of the subtree and this fiber onto the effect
// list of the parent. The completion order of the children affects the
// side-effect order.
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = workInProgress.firstEffect;
}
if (workInProgress.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
}
returnFiber.lastEffect = workInProgress.lastEffect;
}

// If this fiber had side-effects, we append it AFTER the children's
// side-effects. We can perform certain side-effects earlier if
// needed, by doing multiple passes over the effect list. We don't want
// to schedule our own side-effect on our own list because if end up
// reusing children we'll schedule this effect onto itself since we're
// at the end.
const effectTag = workInProgress.effectTag;
// Skip both NoWork and PerformedWork tags when creating the effect list.
// PerformedWork effect is read by React DevTools but shouldn't be committed.
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress;
} else {
returnFiber.firstEffect = workInProgress;
}
returnFiber.lastEffect = workInProgress;
}
}

if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
return siblingFiber;
} else if (returnFiber !== null) {
// If there's no more work in this returnFiber. Complete the returnFiber.
workInProgress = returnFiber;
continue;
} else {
// We've reached the root.
return null;
}
} else {
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// Record the render duration for the fiber that errored.
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);

// Include the time spent working on failed children before continuing.
let actualDuration = workInProgress.actualDuration;
let child = workInProgress.child;
while (child !== null) {
actualDuration += child.actualDuration;
child = child.sibling;
}
workInProgress.actualDuration = actualDuration;
}

// This fiber did not complete because something threw. Pop values off
// the stack without entering the complete phase. If this is a boundary,
// capture values if possible.
const next = unwindWork(workInProgress, nextRenderExpirationTime);
// Because this fiber did not complete, don't reset its expiration time.
if (workInProgress.effectTag & DidCapture) {
// Restarting an error boundary
stopFailedWorkTimer(workInProgress);
} else {
stopWorkTimer(workInProgress);
}
if (next !== null) {
stopWorkTimer(workInProgress);
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
ReactFiberInstrumentation.debugTool.onCompleteWork(workInProgress);
}

// If completing this work spawned new work, do that next. We'll come
// back here again.
// Since we're restarting, remove anything that is not a host effect
// from the effect tag.
next.effectTag &= HostEffectMask;
return next;
}

if (returnFiber !== null) {
// Mark the parent fiber as incomplete and clear its effect list.
returnFiber.firstEffect = returnFiber.lastEffect = null;
returnFiber.effectTag |= Incomplete;
}

if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
return siblingFiber;
} else if (returnFiber !== null) {
// If there's no more work in this returnFiber. Complete the returnFiber.
workInProgress = returnFiber;
continue;
} else {
return null;
}
}
}

// Without this explicit null return Flow complains of invalid return type
// TODO Remove the above while(true) loop
// eslint-disable-next-line no-unreachable
return null;
}

重设 childExpirationTime

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
function resetChildExpirationTime(
workInProgress: Fiber,
renderTime: ExpirationTime
) {
if (renderTime !== Never && workInProgress.childExpirationTime === Never) {
// The children of this component are hidden. Don't bubble their
// expiration times.
return;
}

let newChildExpirationTime = NoWork;

// Bubble up the earliest expiration time.
if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
// We're in profiling mode.
// Let's use this same traversal to update the render durations.
let actualDuration = workInProgress.actualDuration;
let treeBaseDuration = workInProgress.selfBaseDuration;

// When a fiber is cloned, its actualDuration is reset to 0.
// This value will only be updated if work is done on the fiber (i.e. it doesn't bailout).
// When work is done, it should bubble to the parent's actualDuration.
// If the fiber has not been cloned though, (meaning no work was done),
// Then this value will reflect the amount of time spent working on a previous render.
// In that case it should not bubble.
// We determine whether it was cloned by comparing the child pointer.
const shouldBubbleActualDurations =
workInProgress.alternate === null ||
workInProgress.child !== workInProgress.alternate.child;

let child = workInProgress.child;
while (child !== null) {
const childUpdateExpirationTime = child.expirationTime;
const childChildExpirationTime = child.childExpirationTime;
if (childUpdateExpirationTime > newChildExpirationTime) {
newChildExpirationTime = childUpdateExpirationTime;
}
if (childChildExpirationTime > newChildExpirationTime) {
newChildExpirationTime = childChildExpirationTime;
}
if (shouldBubbleActualDurations) {
actualDuration += child.actualDuration;
}
treeBaseDuration += child.treeBaseDuration;
child = child.sibling;
}
workInProgress.actualDuration = actualDuration;
workInProgress.treeBaseDuration = treeBaseDuration;
} else {
let child = workInProgress.child;
while (child !== null) {
const childUpdateExpirationTime = child.expirationTime;
const childChildExpirationTime = child.childExpirationTime;
if (childUpdateExpirationTime > newChildExpirationTime) {
newChildExpirationTime = childUpdateExpirationTime;
}
if (childChildExpirationTime > newChildExpirationTime) {
newChildExpirationTime = childChildExpirationTime;
}
child = child.sibling;
}
}

workInProgress.childExpirationTime = newChildExpirationTime;
}

completeWork

  • pop 各种 context 相关内容

  • 对 HostComponent 执行初始化

  • 初始化监听事件

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime
): Fiber | null {
const newProps = workInProgress.pendingProps;

switch (workInProgress.tag) {
case IndeterminateComponent:
break;
case LazyComponent:
break;
case SimpleMemoComponent:
case FunctionComponent:
break;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
break;
}
case HostRoot: {
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
fiberRoot.pendingContext = null;
}
if (current === null || current.child === null) {
// If we hydrated, pop so that we can delete any remaining children
// that weren't hydrated.
popHydrationState(workInProgress);
// This resets the hacky state to fix isMounted before committing.
// TODO: Delete this when we delete isMounted and findDOMNode.
workInProgress.effectTag &= ~Placement;
}
updateHostContainer(workInProgress);
break;
}
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance
);

if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
if (!newProps) {
invariant(
workInProgress.stateNode !== null,
"We must have new props for new mounts. This error is likely " +
"caused by a bug in React. Please file an issue."
);
// This can happen when we abort work.
break;
}

const currentHostContext = getHostContext();
// TODO: Move createInstance to beginWork and keep it on a context
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on we want to add then top->down or
// bottom->up. Top->down is faster in IE11.
let wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
if (
prepareToHydrateHostInstance(
workInProgress,
rootContainerInstance,
currentHostContext
)
) {
// If changes to the hydrated node needs to be applied at the
// commit-phase we mark this as such.
markUpdate(workInProgress);
}
} else {
let instance = createInstance(
// createElement
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress
);

appendAllChildren(instance, workInProgress, false, false);

// Certain renderers require commit-time effects for initial mount.
// (eg DOM renderer supports auto-focus for certain elements).
// Make sure such renderers get scheduled for later work.
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext
)
) {
markUpdate(workInProgress);
}
workInProgress.stateNode = instance;
}

if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
break;
}
case HostText: {
let newText = newProps;
if (current && workInProgress.stateNode != null) {
const oldText = current.memoizedProps;
// If we have an alternate, that means this is an update and we need
// to schedule a side-effect to do the updates.
updateHostText(current, workInProgress, oldText, newText);
} else {
if (typeof newText !== "string") {
invariant(
workInProgress.stateNode !== null,
"We must have new props for new mounts. This error is likely " +
"caused by a bug in React. Please file an issue."
);
// This can happen when we abort work.
}
const rootContainerInstance = getRootHostContainer();
const currentHostContext = getHostContext();
let wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
if (prepareToHydrateHostTextInstance(workInProgress)) {
markUpdate(workInProgress);
}
} else {
workInProgress.stateNode = createTextInstance(
newText,
rootContainerInstance,
currentHostContext,
workInProgress
);
}
}
break;
}
case ForwardRef:
break;
case SuspenseComponent: {
const nextState = workInProgress.memoizedState;
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
// Something suspended. Re-render with the fallback children.
workInProgress.expirationTime = renderExpirationTime;
// Do not reset the effect list.
return workInProgress;
}

const nextDidTimeout = nextState !== null;
const prevDidTimeout = current !== null && current.memoizedState !== null;

if (current !== null && !nextDidTimeout && prevDidTimeout) {
// We just switched from the fallback to the normal children. Delete
// the fallback.
// TODO: Would it be better to store the fallback fragment on
// the stateNode during the begin phase?
const currentFallbackChild: Fiber | null = (current.child: any).sibling;
if (currentFallbackChild !== null) {
// Deletions go at the beginning of the return fiber's effect list
const first = workInProgress.firstEffect;
if (first !== null) {
workInProgress.firstEffect = currentFallbackChild;
currentFallbackChild.nextEffect = first;
} else {
workInProgress.firstEffect = workInProgress.lastEffect =
currentFallbackChild;
currentFallbackChild.nextEffect = null;
}
currentFallbackChild.effectTag = Deletion;
}
}

// The children either timed out after previously being visible, or
// were restored after previously being hidden. Schedule an effect
// to update their visiblity.
if (
//
nextDidTimeout !== prevDidTimeout ||
// Outside concurrent mode, the primary children commit in an
// inconsistent state, even if they are hidden. So if they are hidden,
// we need to schedule an effect to re-hide them, just in case.
((workInProgress.effectTag & ConcurrentMode) === NoContext &&
nextDidTimeout)
) {
workInProgress.effectTag |= Update;
}
break;
}
case Fragment:
break;
case Mode:
break;
case Profiler:
break;
case HostPortal:
popHostContainer(workInProgress);
updateHostContainer(workInProgress);
break;
case ContextProvider:
// Pop provider fiber
popProvider(workInProgress);
break;
case ContextConsumer:
break;
case MemoComponent:
break;
case IncompleteClassComponent: {
// Same as class component case. I put it down here so that the tags are
// sequential to ensure this switch is compiled to a jump table.
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
break;
}
default:
invariant(
false,
"Unknown unit of work tag. This error is likely caused by a bug in " +
"React. Please file an issue."
);
}

return null;
}

// 重要的也就HostComponent,HostText和SuspenseComponent

初次渲染中 completeWork 对于 DOM 节点的创建和 appendAllChil

  • diff properties 计算需要更新的内容

  • 不同 dom properties 处理不同

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
// packages\react-reconciler\src\ReactFiberCompleteWork.js
let appendAllChildren;
let updateHostContainer;
let updateHostComponent;
let updateHostText;
if (supportsMutation) {
// Mutation mode

appendAllChildren = function (
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean
) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
let node = workInProgress.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
// 找到第一层HostComponent并执行appendChild, 并不会嵌套appendChild
appendInitialChild(parent, node.stateNode);
} else if (node.tag === HostPortal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return; // 向上查找
}
node.sibling.return = node.return;
node = node.sibling; // 兄弟节点之间
}
};

updateHostContainer = function (workInProgress: Fiber) {
// Noop
};
updateHostComponent = function (
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container
) {
// If we have an alternate, that means this is an update and we need to
// schedule a side-effect to do the updates.
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
// In mutation mode, this is sufficient for a bailout because
// we won't touch this node even if children changed.
return;
}

// If we get updated because one of our children updated, we don't
// have newProps so we'll have to reuse them.
// TODO: Split the update API as separate for the props vs. children.
// Even better would be if children weren't special cased at all tho.
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
// TODO: Experiencing an error where oldProps is null. Suggests a host
// component is hitting the resume path. Figure out why. Possibly
// related to `hidden`.
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext
);
// TODO: Type this specific to this type of component.
workInProgress.updateQueue = (updatePayload: any);
// If the update payload indicates that there is a change or if there
// is a new ref we mark this as an update. All the work is done in commitWork.
if (updatePayload) {
markUpdate(workInProgress);
}
};
updateHostText = function (
current: Fiber,
workInProgress: Fiber,
oldText: string,
newText: string
) {
// If the text differs, mark it as an update. All the work in done in commitWork.
if (oldText !== newText) {
markUpdate(workInProgress);
}
};
} else if (supportsPersistence) {
// ...
} else {
// ...
}

更新 DOM 时进行的 diff 判断

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
// packages\react-dom\src\client\ReactDOMHostConfig.js
export function prepareUpdate(
domElement: Instance,
type: string,
oldProps: Props,
newProps: Props,
rootContainerInstance: Container,
hostContext: HostContext
): null | Array<mixed> {
return diffProperties(
domElement,
type,
oldProps,
newProps,
rootContainerInstance
);
}

// packages\react-dom\src\client\ReactDOMComponent.js
// Calculate the diff between the two objects.
export function diffProperties(
domElement: Element,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
rootContainerElement: Element | Document
): null | Array<mixed> {
let updatePayload: null | Array<any> = null;

let lastProps: Object;
let nextProps: Object;
switch (tag) {
case "input":
lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case "option":
lastProps = ReactDOMOptionGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMOptionGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case "select":
lastProps = ReactDOMSelectGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMSelectGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
case "textarea":
lastProps = ReactDOMTextareaGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMTextareaGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
default:
lastProps = lastRawProps;
nextProps = nextRawProps;
if (
typeof lastProps.onClick !== "function" &&
typeof nextProps.onClick === "function"
) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
}

assertValidProps(tag, nextProps);

let propKey;
let styleName;
let styleUpdates = null;
for (propKey in lastProps) {
if (
nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey) ||
lastProps[propKey] == null
) {
continue;
}
if (propKey === STYLE) {
const lastStyle = lastProps[propKey];
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = "";
}
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) {
// Noop. This is handled by the clear text mechanism.
} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (propKey === AUTOFOCUS) {
// Noop. It doesn't work on updates anyway.
} else if (registrationNameModules.hasOwnProperty(propKey)) {
// This is a special case. If any listener updates we need to ensure
// that the "current" fiber pointer gets updated so we need a commit
// to update this element.
if (!updatePayload) {
updatePayload = [];
}
} else {
// For all other deleted properties we add it to the queue. We use
// the whitelist in the commit phase instead.
(updatePayload = updatePayload || []).push(propKey, null);
}
}
for (propKey in nextProps) {
const nextProp = nextProps[propKey];
const lastProp = lastProps != null ? lastProps[propKey] : undefined;
if (
!nextProps.hasOwnProperty(propKey) ||
nextProp === lastProp ||
(nextProp == null && lastProp == null)
) {
continue;
}
if (propKey === STYLE) {
if (lastProp) {
// Unset styles on `lastProp` but not on `nextProp`.
for (styleName in lastProp) {
if (
lastProp.hasOwnProperty(styleName) &&
(!nextProp || !nextProp.hasOwnProperty(styleName))
) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = "";
}
}
// Update styles that changed since `lastProp`.
for (styleName in nextProp) {
if (
nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]
) {
if (!styleUpdates) {
styleUpdates = {};
}
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
// Relies on `updateStylesByID` not mutating `styleUpdates`.
if (!styleUpdates) {
if (!updatePayload) {
updatePayload = [];
}
updatePayload.push(propKey, styleUpdates);
}
styleUpdates = nextProp;
}
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
const nextHtml = nextProp ? nextProp[HTML] : undefined;
const lastHtml = lastProp ? lastProp[HTML] : undefined;
if (nextHtml != null) {
if (lastHtml !== nextHtml) {
(updatePayload = updatePayload || []).push(propKey, "" + nextHtml);
}
} else {
// TODO: It might be too late to clear this if we have children
// inserted already.
}
} else if (propKey === CHILDREN) {
if (
lastProp !== nextProp &&
(typeof nextProp === "string" || typeof nextProp === "number")
) {
(updatePayload = updatePayload || []).push(propKey, "" + nextProp);
}
} else if (
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
propKey === SUPPRESS_HYDRATION_WARNING
) {
// Noop
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp != null) {
// We eagerly listen to this even though we haven't committed yet.
ensureListeningTo(rootContainerElement, propKey);
}
if (!updatePayload && lastProp !== nextProp) {
// This is a special case. If any listener updates we need to ensure
// that the "current" props pointer gets updated so we need a commit
// to update this element.
updatePayload = [];
}
} else {
// For any other property we always add it to the queue and then we
// filter it out using the whitelist during the commit.
(updatePayload = updatePayload || []).push(propKey, nextProp);
}
}
if (styleUpdates) {
(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
}
return updatePayload;
}

// Apply the diff.
export function updateProperties(
domElement: Element,
updatePayload: Array<any>,
tag: string,
lastRawProps: Object,
nextRawProps: Object
): void {
// Update checked *before* name.
// In the middle of an update, it is possible to have multiple checked.
// When a checked radio tries to change name, browser makes another radio's checked false.
if (
tag === "input" &&
nextRawProps.type === "radio" &&
nextRawProps.name != null
) {
ReactDOMInputUpdateChecked(domElement, nextRawProps);
}

const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
// Apply the diff.
updateDOMProperties(
domElement,
updatePayload,
wasCustomComponentTag,
isCustomComponentTag
);

// TODO: Ensure that an update gets scheduled if any of the special props
// changed.
switch (tag) {
case "input":
// Update the wrapper around inputs *after* updating props. This has to
// happen after `updateDOMProperties`. Otherwise HTML5 input validations
// raise warnings and prevent the new value from being assigned.
ReactDOMInputUpdateWrapper(domElement, nextRawProps);
break;
case "textarea":
ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);
break;
case "select":
// <select> value update needs to occur after <option> children
// reconciliation
ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);
break;
}
}
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean
): void {
// TODO: Handle wasCustomComponentTag
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}

completeWork 阶段对于 HostText 的更新

renderRoot 中对于错误的处理

  • 给报错节点增加 InComplate 副作用

  • 给父链上具有 error boundary 的节点增加副作用

  • 创建错误相关的更新

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
// packages\react-reconciler\src\ReactFiberScheduler.js

// onUncaughtError

// throwException

// completeUnitOfWork 停止子节点渲染

// thenable处理
// renderDidError
// createCapturedValue
// createClassErrorUpdate

// 向上查找第一个能处理错误的逻辑
// getDerivedStateFromError
// componentDidCatch
function createClassErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
expirationTime: ExpirationTime
): Update<mixed> {
const update = createUpdate(expirationTime);
update.tag = CaptureUpdate;
const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
if (typeof getDerivedStateFromError === "function") {
const error = errorInfo.value;
update.payload = () => {
return getDerivedStateFromError(error);
};
}

const inst = fiber.stateNode;
if (inst !== null && typeof inst.componentDidCatch === "function") {
update.callback = function callback() {
if (typeof getDerivedStateFromError !== "function") {
// To preserve the preexisting retry behavior of error boundaries,
// we keep track of which ones already failed during this batch.
// This gets reset before we yield back to the browser.
// TODO: Warn in strict mode if getDerivedStateFromError is
// not defined.
markLegacyErrorBoundaryAsFailed(this);
}
const error = errorInfo.value;
const stack = errorInfo.stack;
logError(fiber, errorInfo);
this.componentDidCatch(error, {
componentStack: stack !== null ? stack : "",
});
};
}
return update;
}

unwindWork 以及 React 中的错误处理

  • 类似于 completeWork 对不同类型组件处理

  • 对于 ShouldCapture 组件设置 DidCapture 副作用

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
// packages\react-reconciler\src\ReactFiberUnwindWork.js
function unwindWork(
workInProgress: Fiber,
renderExpirationTime: ExpirationTime
) {
switch (workInProgress.tag) {
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
const effectTag = workInProgress.effectTag;
if (effectTag & ShouldCapture) {
// effectTag包含ShouldCapture的话,就把ShouldCapture去掉,加上DidCapture
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
return workInProgress;
}
return null;
}
case HostRoot: {
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
const effectTag = workInProgress.effectTag;
invariant(
(effectTag & DidCapture) === NoEffect,
"The root failed to unmount after an error. This is likely a bug in " +
"React. Please file an issue."
);
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
return workInProgress;
}
case HostComponent: {
popHostContext(workInProgress);
return null;
}
case SuspenseComponent: {
const effectTag = workInProgress.effectTag;
if (effectTag & ShouldCapture) {
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
// Captured a suspense effect. Re-render the boundary.
return workInProgress;
}
return null;
}
case HostPortal:
popHostContainer(workInProgress);
return null;
case ContextProvider:
popProvider(workInProgress);
return null;
default:
return null;
}
}

提交阶段 commitRoot

  • 预备工作

  • 三个循环

  • 其他工作

入口 completeRoot

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
// packages\react-reconciler\src\ReactFiberScheduler.js
function completeRoot(
root: FiberRoot,
finishedWork: Fiber,
expirationTime: ExpirationTime
): void {
// Check if there's a batch that matches this expiration time.
const firstBatch = root.firstBatch;
if (firstBatch !== null && firstBatch._expirationTime >= expirationTime) {
if (completedBatches === null) {
completedBatches = [firstBatch];
} else {
completedBatches.push(firstBatch);
}
if (firstBatch._defer) {
// This root is blocked from committing by a batch. Unschedule it until
// we receive another update.
root.finishedWork = finishedWork;
root.expirationTime = NoWork;
return;
}
}

// Commit the root.
root.finishedWork = null;

// Check if this is a nested update (a sync update scheduled during the
// commit phase).
if (root === lastCommittedRootDuringThisBatch) {
// If the next root is the same as the previous root, this is a nested
// update. To prevent an infinite loop, increment the nested update count.
nestedUpdateCount++;
} else {
// Reset whenever we switch roots.
lastCommittedRootDuringThisBatch = root;
nestedUpdateCount = 0;
}
commitRoot(root, finishedWork);
}

// commitRoot
function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
isWorking = true;
isCommitting = true;
startCommitTimer();
const committedExpirationTime = root.pendingCommitExpirationTime;
root.pendingCommitExpirationTime = NoWork;

// Update the pending priority levels to account for the work that we are
// about to commit. This needs to happen before calling the lifecycles, since
// they may schedule additional updates.
const updateExpirationTimeBeforeCommit = finishedWork.expirationTime;
const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime;
const earliestRemainingTimeBeforeCommit =
childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit
? childExpirationTimeBeforeCommit
: updateExpirationTimeBeforeCommit;
markCommittedPriorityLevels(root, earliestRemainingTimeBeforeCommit);

let prevInteractions: Set<Interaction> = (null: any);
// Reset this to null before calling lifecycles
ReactCurrentOwner.current = null;

let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
// A fiber's effect list consists only of its children, not itself. So if
// the root has an effect, we need to add it to the end of the list. The
// resulting list is the set that would belong to the root's parent, if
// it had one; that is, all the effects in the tree including the root.
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// There is no effect on the root.
firstEffect = finishedWork.firstEffect;
}

prepareForCommit(root.containerInfo);

// Invoke instances of getSnapshotBeforeUpdate before mutation.
nextEffect = firstEffect;
startCommitSnapshotEffectsTimer();
while (nextEffect !== null) {
let didError = false;
let error;
try {
commitBeforeMutationLifecycles();
} catch (e) {
didError = true;
error = e;
}
if (didError) {
invariant(
nextEffect !== null,
"Should have next effect. This error is likely caused by a bug " +
"in React. Please file an issue."
);
captureCommitPhaseError(nextEffect, error);
// Clean-up
if (nextEffect !== null) {
nextEffect = nextEffect.nextEffect;
}
}
}
stopCommitSnapshotEffectsTimer();

// Commit all the side-effects within a tree. We'll do this in two passes.
// The first pass performs all the host insertions, updates, deletions and
// ref unmounts.
nextEffect = firstEffect;
startCommitHostEffectsTimer();
while (nextEffect !== null) {
let didError = false;
let error;

try {
commitAllHostEffects();
} catch (e) {
didError = true;
error = e;
}
if (didError) {
invariant(
nextEffect !== null,
"Should have next effect. This error is likely caused by a bug " +
"in React. Please file an issue."
);
captureCommitPhaseError(nextEffect, error);
// Clean-up
if (nextEffect !== null) {
nextEffect = nextEffect.nextEffect;
}
}
}
stopCommitHostEffectsTimer();

resetAfterCommit(root.containerInfo);

// The work-in-progress tree is now the current tree. This must come after
// the first pass of the commit phase, so that the previous tree is still
// current during componentWillUnmount, but before the second pass, so that
// the finished work is current during componentDidMount/Update.
root.current = finishedWork;

// In the second pass we'll perform all life-cycles and ref callbacks.
// Life-cycles happen as a separate pass so that all placements, updates,
// and deletions in the entire tree have already been invoked.
// This pass also triggers any renderer-specific initial effects.
nextEffect = firstEffect;
startCommitLifeCyclesTimer();
while (nextEffect !== null) {
let didError = false;
let error;

try {
commitAllLifeCycles(root, committedExpirationTime);
} catch (e) {
didError = true;
error = e;
}
if (didError) {
invariant(
nextEffect !== null,
"Should have next effect. This error is likely caused by a bug " +
"in React. Please file an issue."
);
captureCommitPhaseError(nextEffect, error);
if (nextEffect !== null) {
nextEffect = nextEffect.nextEffect;
}
}
}

if (
enableHooks &&
firstEffect !== null &&
rootWithPendingPassiveEffects !== null
) {
// This commit included a passive effect. These do not need to fire until
// after the next paint. Schedule an callback to fire them in an async
// event. To ensure serial execution, the callback will be flushed early if
// we enter rootWithPendingPassiveEffects commit phase before then.
let callback = commitPassiveEffects.bind(null, root, firstEffect);
passiveEffectCallbackHandle = Schedule_scheduleCallback(callback);
passiveEffectCallback = callback;
}

isCommitting = false;
isWorking = false;
stopCommitLifeCyclesTimer();
stopCommitTimer();
onCommitRoot(finishedWork.stateNode);

const updateExpirationTimeAfterCommit = finishedWork.expirationTime;
const childExpirationTimeAfterCommit = finishedWork.childExpirationTime;
const earliestRemainingTimeAfterCommit =
childExpirationTimeAfterCommit > updateExpirationTimeAfterCommit
? childExpirationTimeAfterCommit
: updateExpirationTimeAfterCommit;
if (earliestRemainingTimeAfterCommit === NoWork) {
// If there's no remaining work, we can clear the set of already failed
// error boundaries.
legacyErrorBoundariesThatAlreadyFailed = null;
}
onCommit(root, earliestRemainingTimeAfterCommit);
}

第一个循环 commitBeforeMutationLifecycles

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
// commitBeforeMutationLifecycles
function commitBeforeMutationLifecycles() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & Snapshot) {
recordEffect();
const current = nextEffect.alternate;
commitBeforeMutationLifeCycles(current, nextEffect);
}

nextEffect = nextEffect.nextEffect;
}
}

// packages\react-reconciler\src\ReactFiberCommitWork.js
function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
commitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork);
return;
}
case ClassComponent: {
if (finishedWork.effectTag & Snapshot) {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
startPhaseTimer(finishedWork, "getSnapshotBeforeUpdate");
const instance = finishedWork.stateNode;
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
// 获取快照getSnapshotBeforeUpdate
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState
);
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
stopPhaseTimer();
}
}
return;
}
case HostRoot:
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// Nothing to do for these component types
return;
default: {
invariant(
false,
"This unit of work tag should not have side-effects. This error is " +
"likely caused by a bug in React. Please file an issue."
);
}
}
}

第二个循环 commitAllHostEffects

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
function commitAllHostEffects() {
while (nextEffect !== null) {
recordEffect();

const effectTag = nextEffect.effectTag;

// 文本节点Reset
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}

// Ref节点Detach
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}

// 插入,更新,删除
// The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every
// possible bitmap value, we remove the secondary effects from the
// effect tag and switch on that value.
let primaryEffectTag = effectTag & (Placement | Update | Deletion);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted
// does and isMounted is deprecated anyway so we should be able
// to kill this.
nextEffect.effectTag &= ~Placement; // 现在是在循环里面,所以Placement执行完了就剔除
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
nextEffect.effectTag &= ~Placement;

// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
commitDeletion(nextEffect);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}

Placement commitPlacement

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
// packages\react-reconciler\src\ReactFiberCommitWork.js
function commitPlacement(finishedWork: Fiber): void {
// Recursively insert all host nodes into the parent.
const parentFiber = getHostParentFiber(finishedWork);

// Note: these two variables *must* always be updated together.
let parent;
let isContainer;

switch (parentFiber.tag) {
case HostComponent:
parent = parentFiber.stateNode;
isContainer = false;
break;
case HostRoot:
parent = parentFiber.stateNode.containerInfo;
isContainer = true;
break;
case HostPortal:
parent = parentFiber.stateNode.containerInfo;
isContainer = true;
break;
default:
invariant(
false,
"Invalid host parent fiber. This error is likely caused by a bug " +
"in React. Please file an issue."
);
}
if (parentFiber.effectTag & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.effectTag &= ~ContentReset;
}

const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need recurse down its
// children to find all the terminal nodes.
let node: Fiber = finishedWork;
while (true) {
if (node.tag === HostComponent || node.tag === HostText) {
if (before) {
if (isContainer) {
insertInContainerBefore(parent, node.stateNode, before);
} else {
insertBefore(parent, node.stateNode, before);
}
} else {
if (isContainer) {
appendChildToContainer(parent, node.stateNode);
} else {
appendChild(parent, node.stateNode);
}
}
} else if (node.tag === HostPortal) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === finishedWork) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === finishedWork) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}

Update commitWork

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
if (!supportsMutation) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// Note: We currently never use MountMutation, but useLayout uses
// UnmountMutation.
commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
return;
}
}

commitContainer(finishedWork);
return;
}

switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// Note: We currently never use MountMutation, but useLayout uses
// UnmountMutation.
commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
return;
}
case ClassComponent: {
return;
}
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
// 这个就是需要更新的对象数组updatePayload
const updatePayload: null | UpdatePayload =
(finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork
);
}
}
return;
}
case HostText: {
invariant(
finishedWork.stateNode !== null,
"This should have a text node initialized. This error is likely " +
"caused by a bug in React. Please file an issue."
);
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldText: string =
current !== null ? current.memoizedProps : newText;
commitTextUpdate(textInstance, oldText, newText);
return;
}
case HostRoot: {
return;
}
case Profiler: {
return;
}
case SuspenseComponent: {
let newState: SuspenseState | null = finishedWork.memoizedState;

let newDidTimeout;
let primaryChildParent = finishedWork;
if (newState === null) {
newDidTimeout = false;
} else {
newDidTimeout = true;
primaryChildParent = finishedWork.child;
if (newState.timedOutAt === NoWork) {
// If the children had not already timed out, record the time.
// This is used to compute the elapsed time during subsequent
// attempts to render the children.
newState.timedOutAt = requestCurrentTime();
}
}

if (primaryChildParent !== null) {
hideOrUnhideAllChildren(primaryChildParent, newDidTimeout);
}

// If this boundary just timed out, then it will have a set of thenables.
// For each thenable, attach a listener so that when it resolves, React
// attempts to re-render the boundary in the primary (pre-timeout) state.
const thenables: Set<Thenable> | null = (finishedWork.updateQueue: any);
if (thenables !== null) {
finishedWork.updateQueue = null;
let retryCache = finishedWork.stateNode;
if (retryCache === null) {
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
}
thenables.forEach((thenable) => {
// Memoize using the boundary fiber to prevent redundant listeners.
let retry = retryTimedOutBoundary.bind(null, finishedWork, thenable);
if (enableSchedulerTracing) {
retry = Schedule_tracing_wrap(retry);
}
if (!retryCache.has(thenable)) {
retryCache.add(thenable);
thenable.then(retry, retry);
}
});
}

return;
}
case IncompleteClassComponent: {
return;
}
default: {
invariant(
false,
"This unit of work tag should not have side-effects. This error is " +
"likely caused by a bug in React. Please file an issue."
);
}
}
}

// packages\react-dom\src\client\ReactDOMHostConfig.js
export function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object
): void {
// Update the props handle so that we know which props are the ones with
// with current event handlers.
updateFiberProps(domElement, newProps);
// Apply the diff to the DOM node.
updateProperties(domElement, updatePayload, type, oldProps, newProps);
}

第三个循环 commitAllLifeCycles

其他

context

event 事件系统初始化 注入平台事件插件

  • 确定插件注入顺序

  • 注入插件模块序

  • 计算 registrationModuleNames 等属性

Hooks

Hooks 的定义以及执行前后的准备和重置

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
// packages\react\src\React.js
import {enableHooks} from 'shared/ReactFeatureFlags';
import {
useCallback,
useContext,
useEffect,
useImperativeMethods,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
} from './ReactHooks';
const React = {
// ...
if (enableHooks) {
React.useCallback = useCallback;
React.useContext = useContext;
React.useEffect = useEffect;
React.useImperativeMethods = useImperativeMethods;
React.useLayoutEffect = useLayoutEffect;
React.useMemo = useMemo;
React.useReducer = useReducer;
React.useRef = useRef;
React.useState = useState;
}

export default React;


// packages\react\src\ReactHooks.js
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}


import ReactCurrentOwner from './ReactCurrentOwner';

function resolveDispatcher() {
const dispatcher = ReactCurrentOwner.currentDispatcher;
return dispatcher;
}

// packages\react\src\ReactCurrentOwner.js
/**
* Keeps track of the current owner.
*
* The current owner is the component who should own any components that are
* currently being constructed.
*/
const ReactCurrentOwner = {
/**
* @internal
* @type {ReactComponent}
*/
current: (null: null | Fiber),
currentDispatcher: (null: null | Dispatcher),
};

export default ReactCurrentOwner;

// packages\react-reconciler\src\ReactFiberScheduler.js
function renderRoot(root: FiberRoot, isYieldy: boolean): void {
flushPassiveEffects();

isWorking = true;
if (enableHooks) {
ReactCurrentOwner.currentDispatcher = Dispatcher;
} else {
ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks;
}
// ...
}

// packages\react-reconciler\src\ReactFiberDispatcher.js
import {readContext} from './ReactFiberNewContext';
import {
useCallback,
useContext,
useEffect,
useImperativeMethods,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
} from './ReactFiberHooks';

export const Dispatcher = {
readContext,
useCallback,
useContext,
useEffect,
useImperativeMethods,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
};
export const DispatcherWithoutHooks = {
readContext,
};

// packages\react-reconciler\src\ReactFiberHooks.js
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return useReducer(
basicStateReducer,
// useReducer has a special case to support lazy useState initializers
(initialState: any),
);
}

function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}

export function useReducer<S, A>(
reducer: (S, A) => S,
initialState: S,
initialAction: A | void | null,
): [S, Dispatch<A>] {
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
workInProgressHook = createWorkInProgressHook();
let queue: UpdateQueue<A> | null = (workInProgressHook.queue: any);
if (queue !== null) {
// Already have a queue, so this is an update.
if (isReRender) {
// This is a re-render. Apply the new render phase updates to the previous
// work-in-progress hook.
const dispatch: Dispatch<A> = (queue.dispatch: any);
if (renderPhaseUpdates !== null) {
// Render phase updates are stored in a map of queue -> linked list
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
renderPhaseUpdates.delete(queue);
let newState = workInProgressHook.memoizedState;
let update = firstRenderPhaseUpdate;
do {
// Process this render phase update. We don't have to check the
// priority because it will always be the same as the current
// render's.
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null);

workInProgressHook.memoizedState = newState;

// Don't persist the state accumlated from the render phase updates to
// the base state unless the queue is empty.
// TODO: Not sure if this is the desired semantics, but it's what we
// do for gDSFP. I can't remember why.
if (workInProgressHook.baseUpdate === queue.last) {
workInProgressHook.baseState = newState;
}

return [newState, dispatch];
}
}
return [workInProgressHook.memoizedState, dispatch];
}

// The last update in the entire queue
const last = queue.last;
// The last update that is part of the base state.
const baseUpdate = workInProgressHook.baseUpdate;

// Find the first unprocessed update.
let first;
if (baseUpdate !== null) {
if (last !== null) {
// For the first update, the queue is a circular linked list where
// `queue.last.next = queue.first`. Once the first update commits, and
// the `baseUpdate` is no longer empty, we can unravel the list.
last.next = null;
}
first = baseUpdate.next;
} else {
first = last !== null ? last.next : null;
}
if (first !== null) {
let newState = workInProgressHook.baseState;
let newBaseState = null;
let newBaseUpdate = null;
let prevUpdate = baseUpdate;
let update = first;
let didSkip = false;
do {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
if (!didSkip) {
didSkip = true;
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
// Update the remaining priority in the queue.
if (updateExpirationTime > remainingExpirationTime) {
remainingExpirationTime = updateExpirationTime;
}
} else {
// Process this update.
const action = update.action;
newState = reducer(newState, action);
}
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);

if (!didSkip) {
newBaseUpdate = prevUpdate;
newBaseState = newState;
}

workInProgressHook.memoizedState = newState;
workInProgressHook.baseUpdate = newBaseUpdate;
workInProgressHook.baseState = newBaseState;
}

const dispatch: Dispatch<A> = (queue.dispatch: any);
return [workInProgressHook.memoizedState, dispatch];
}

// There's no existing queue, so this is the initial render.
if (reducer === basicStateReducer) {
// Special case for `useState`.
if (typeof initialState === 'function') {
initialState = initialState();
}
} else if (initialAction !== undefined && initialAction !== null) {
initialState = reducer(initialState, initialAction);
}
workInProgressHook.memoizedState = workInProgressHook.baseState = initialState;
queue = workInProgressHook.queue = {
last: null,
dispatch: null,
};
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [workInProgressHook.memoizedState, dispatch];
}

useState 的底层结构还是 Fiber,在函数组件里面函数组件本身是一个 Fiber,而 hooks 则是更细粒度的 Fiber