├── .babelrc
├── src
├── index.css
├── index.js
├── index.html
├── react
│ ├── ReactComponent.js
│ ├── index.js
│ └── ReactElement.js
├── App.css
├── reconciler
│ ├── ReactFiberRoot.js
│ ├── ReactUpdateQueue.js
│ ├── ReactFiberExpirationTime.js
│ ├── ReactFiber.js
│ └── Reconciler.js
├── shared
│ ├── ReactWorkTags.js
│ ├── ReactTreeTraversal.js
│ └── ReactSideEffectTags.js
├── cache
│ └── ReactCache.js
├── event
│ └── isInteractiveEvent.js
├── App.js
├── logo.svg
└── CustomDom.js
├── Guide
├── Images
│ ├── flowchart.PNG
│ ├── suspense.gif
│ ├── Fiber_Tree.PNG
│ ├── event_change.PNG
│ ├── event_click.PNG
│ ├── event_wrong.PNG
│ ├── lifecycles.PNG
│ ├── customRenderer.gif
│ ├── Fiber_StackFrame.PNG
│ ├── customRender_now.PNG
│ ├── suspense_simple.gif
│ └── suspense_flowchart.PNG
├── Introduction.md
├── ReactCore.md
├── CustomRenderer.md
├── LifeCycles.md
├── Event.md
├── Fiber_part1.md
├── Suspense.md
└── Fiber_part2.md
├── package.json
├── README.ENG.md
└── README.md
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "stage-0", "react"]
3 | }
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/Guide/Images/flowchart.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luminqi/learn-react/HEAD/Guide/Images/flowchart.PNG
--------------------------------------------------------------------------------
/Guide/Images/suspense.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luminqi/learn-react/HEAD/Guide/Images/suspense.gif
--------------------------------------------------------------------------------
/Guide/Images/Fiber_Tree.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luminqi/learn-react/HEAD/Guide/Images/Fiber_Tree.PNG
--------------------------------------------------------------------------------
/Guide/Images/event_change.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luminqi/learn-react/HEAD/Guide/Images/event_change.PNG
--------------------------------------------------------------------------------
/Guide/Images/event_click.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luminqi/learn-react/HEAD/Guide/Images/event_click.PNG
--------------------------------------------------------------------------------
/Guide/Images/event_wrong.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luminqi/learn-react/HEAD/Guide/Images/event_wrong.PNG
--------------------------------------------------------------------------------
/Guide/Images/lifecycles.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luminqi/learn-react/HEAD/Guide/Images/lifecycles.PNG
--------------------------------------------------------------------------------
/Guide/Images/customRenderer.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luminqi/learn-react/HEAD/Guide/Images/customRenderer.gif
--------------------------------------------------------------------------------
/Guide/Images/Fiber_StackFrame.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luminqi/learn-react/HEAD/Guide/Images/Fiber_StackFrame.PNG
--------------------------------------------------------------------------------
/Guide/Images/customRender_now.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luminqi/learn-react/HEAD/Guide/Images/customRender_now.PNG
--------------------------------------------------------------------------------
/Guide/Images/suspense_simple.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luminqi/learn-react/HEAD/Guide/Images/suspense_simple.gif
--------------------------------------------------------------------------------
/Guide/Images/suspense_flowchart.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Luminqi/learn-react/HEAD/Guide/Images/suspense_flowchart.PNG
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from './react';
2 | import { CustomDom } from './CustomDom';
3 | import './index.css';
4 | import App from './App';
5 |
6 | CustomDom.render(, document.getElementById('root'));
7 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | React
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/react/ReactComponent.js:
--------------------------------------------------------------------------------
1 | export class Component {
2 | constructor (props) {
3 | this.props = props
4 | this.updater = {}
5 | }
6 | setState (partialState) {
7 | this.updater.enqueueSetState(this, partialState)
8 | }
9 | }
--------------------------------------------------------------------------------
/src/react/index.js:
--------------------------------------------------------------------------------
1 | import { createElement } from './ReactElement'
2 | import { Component } from './ReactComponent'
3 |
4 | const React = {
5 | Component,
6 | createElement,
7 | Suspense: Symbol.for('react.suspense')
8 | }
9 |
10 | export default React
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "learn-react",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "babel-preset-env": "^1.7.0",
8 | "babel-preset-react": "^6.24.1",
9 | "babel-preset-stage-0": "^6.24.1",
10 | "parcel-bundler": "^1.9.7"
11 | },
12 | "scripts": {
13 | "start": "parcel src/index.html"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/README.ENG.md:
--------------------------------------------------------------------------------
1 | # learn-react
2 |
3 | Learn react fiber architecture, time slicing, suspense. Start from writing a simple custom renderer, then dive into the source code and
4 | simplifying the realization.
5 |
6 | ## Motivation
7 | Try to figure out how React works internally by writing a simplified version of React
8 |
9 | ## Guide
10 | * [Introduction]
11 | * [Custom Renderer]
12 | * [Fiber Architecture]
13 | * [Event]
14 | * [Suspense]
15 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 80px;
8 | }
9 |
10 | .App-header {
11 | background-color: #222;
12 | height: 150px;
13 | padding: 20px;
14 | color: white;
15 | }
16 |
17 | .App-title {
18 | font-size: 1.5em;
19 | }
20 |
21 | .App-intro {
22 | font-size: large;
23 | }
24 |
25 | @keyframes App-logo-spin {
26 | from { transform: rotate(0deg); }
27 | to { transform: rotate(360deg); }
28 | }
29 |
--------------------------------------------------------------------------------
/src/react/ReactElement.js:
--------------------------------------------------------------------------------
1 | class ReactElement {
2 | constructor (type, props) {
3 | this.type = type
4 | this.props = props
5 | }
6 | }
7 |
8 | export function createElement(type, config, ...children) {
9 | const props = {}
10 | if (config !== null) {
11 | Object.keys(config).forEach(propName =>
12 | props[propName] = config[propName])
13 | }
14 | if (children.length >= 1) {
15 | props.children = children.length === 1 ? children[0] : children
16 | }
17 | return new ReactElement(type, props)
18 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # learn-react
2 |
3 | 学习React Fiber架构,理解如何实现时间分片(Time Slicing)和 Suspense.
4 |
5 | ## Motivation
6 | 完成一个简化版的React。
7 |
8 | ## Guide
9 | * [简介](/Guide/Introduction.md)
10 | * [实现自定义的渲染器](/Guide/CustomRenderer.md)
11 | * [实现 Fiber 架构 I](/Guide/Fiber_part1.md)
12 | * [实现 Fiber 架构 II](/Guide/Fiber_part2.md)
13 | * [实现事件处理](/Guide/Event.md)
14 | * [实现 React Core API](/Guide/ReactCore.md)
15 | * [实现 Suspense](/Guide/Suspense.md)
16 | * [实现生命周期函数](/Guide/LifeCycles.md)
17 |
18 | ## Links
19 | * [fresh-async-react](https://github.com/sw-yx/fresh-async-react)
20 | * [Hello World Custom React Renderer](https://medium.com/@agent_hunt/hello-world-custom-react-renderer-9a95b7cd04bc)
--------------------------------------------------------------------------------
/src/reconciler/ReactFiberRoot.js:
--------------------------------------------------------------------------------
1 | import { createHostRootFiber } from './ReactFiber'
2 | import { NoWork } from './ReactFiberExpirationTime'
3 |
4 | export function createFiberRoot (containerInfo) {
5 | let uninitializedFiber = createHostRootFiber()
6 | let root = {
7 | // The currently active root fiber. This is the mutable root of the tree.
8 | current: uninitializedFiber,
9 | // Any additional information from the host associated with this root.
10 | containerInfo: containerInfo,
11 | // A finished work-in-progress HostRoot that's ready to be committed.
12 | finishedWork: null,
13 | expirationTime: NoWork
14 | }
15 | uninitializedFiber.stateNode = root
16 | return root
17 | }
--------------------------------------------------------------------------------
/Guide/Introduction.md:
--------------------------------------------------------------------------------
1 | # 简介
2 |
3 | **我想理解React是如何工作的**
4 |
5 | 所有的一切开始于这个想法。直接阅读 React 源码是一件痛苦的事情,一方面源码包含了错误处理,性能分析(Profiler API),
6 | 服务器端渲染等等我不关心的功能,另一方面源码把所有的细节都呈现出来,而这对从来没有深入过 React 源码的我来说过于复杂。
7 |
8 | **那么,通过什么方法来理解React呢?**
9 |
10 | 写一个简单的Renderer,替换掉 ReactDom Renderer,用这个 Renderer 渲染一个简单的组件来调试实现时间分片的ReactReconciler 模块。
11 |
12 | 通过一些前提假设来简化 ReactReconciler 模块的代码,用简化版的代码替换掉原来的代码,再来调试 Suspense 等模块的代码
13 |
14 | 所以,通过一步一步地把 React 各个模块的源码替换成简化的代码,来完成一个我所理解的 SimpleReact。
15 |
16 | **结果**
17 |
18 | 最后完成的 simpleReact 仍然有一千多行的代码,但是复杂度已经降低到我能理解的程度。
19 | 保留的主要功能有 time slicing,suspense,事件处理,组件生命周期函数。
20 |
21 | **注意**
22 |
23 | 本项目是对 React v16.4.2 的简化,它的目的是为了理解 React 的主要原理,它并不能像 React 一样完成所有的工作。我只能确保它在一些情况下能够如我预期一样的运行。
24 |
25 | [开始吧](CustomRenderer.md)
26 |
--------------------------------------------------------------------------------
/src/shared/ReactWorkTags.js:
--------------------------------------------------------------------------------
1 | export const FunctionalComponent = 0;
2 | export const FunctionalComponentLazy = 1;
3 | export const ClassComponent = 2;
4 | export const ClassComponentLazy = 3;
5 | export const IndeterminateComponent = 4; // Before we know whether it is functional or class
6 | export const HostRoot = 5; // Root of a host tree. Could be nested inside another node.
7 | export const HostPortal = 6; // A subtree. Could be an entry point to a different renderer.
8 | export const HostComponent = 7;
9 | export const HostText = 8;
10 | export const Fragment = 9;
11 | export const Mode = 10;
12 | export const ContextConsumer = 11;
13 | export const ContextProvider = 12;
14 | export const ForwardRef = 13;
15 | export const ForwardRefLazy = 14;
16 | export const Profiler = 15;
17 | export const SuspenseComponent = 16;
--------------------------------------------------------------------------------
/src/cache/ReactCache.js:
--------------------------------------------------------------------------------
1 | export function createCache () {
2 | const resourceMap = new Map()
3 | const cache = {
4 | read (resourceType, key, loadResource) {
5 | let recordCache = resourceMap.get(resourceType)
6 | if (recordCache === undefined) {
7 | recordCache = new Map()
8 | resourceMap.set(resourceType, recordCache)
9 | }
10 | let record = recordCache.get(key)
11 | if (record === undefined) {
12 | const suspender = loadResource(key)
13 | suspender.then(value => {
14 | recordCache.set(key, value)
15 | return value
16 | })
17 | throw suspender
18 | }
19 | return record
20 | }
21 | }
22 | return cache
23 | }
24 |
25 | export function createResource (loadResource) {
26 | const resource = {
27 | read (cache, key) {
28 | return cache.read(resource, key, loadResource)
29 | }
30 | }
31 | return resource
32 | }
--------------------------------------------------------------------------------
/src/shared/ReactTreeTraversal.js:
--------------------------------------------------------------------------------
1 | import {HostComponent} from './ReactWorkTags';
2 |
3 | function getParent(inst) {
4 | do {
5 | inst = inst.return;
6 | // TODO: If this is a HostRoot we might want to bail out.
7 | // That is depending on if we want nested subtrees (layers) to bubble
8 | // events to their parent. We could also go through parentNode on the
9 | // host node but that wouldn't work for React Native and doesn't let us
10 | // do the portal feature.
11 | } while (inst && inst.tag !== HostComponent);
12 | if (inst) {
13 | return inst;
14 | }
15 | return null;
16 | }
17 |
18 | /**
19 | * Simulates the traversal of a two-phase, capture/bubble event dispatch.
20 | */
21 | export function traverseTwoPhase(inst, fn, arg) {
22 | const path = [];
23 | while (inst) {
24 | path.push(inst);
25 | inst = getParent(inst);
26 | }
27 | let i;
28 | for (i = path.length; i-- > 0; ) {
29 | fn(path[i], 'captured', arg);
30 | }
31 | for (i = 0; i < path.length; i++) {
32 | fn(path[i], 'bubbled', arg);
33 | }
34 | }
--------------------------------------------------------------------------------
/src/shared/ReactSideEffectTags.js:
--------------------------------------------------------------------------------
1 | // Don't change these two values. They're used by React Dev Tools.
2 | export const NoEffect = /* */ 0b00000000000;
3 | export const PerformedWork = /* */ 0b00000000001;
4 |
5 | // You can change the rest (and add more).
6 | export const Placement = /* */ 0b00000000010;
7 | export const Update = /* */ 0b00000000100;
8 | export const PlacementAndUpdate = /* */ 0b00000000110;
9 | export const Deletion = /* */ 0b00000001000;
10 | export const ContentReset = /* */ 0b00000010000;
11 | export const Callback = /* */ 0b00000100000;
12 | export const DidCapture = /* */ 0b00001000000;
13 | export const Ref = /* */ 0b00010000000;
14 | export const Snapshot = /* */ 0b00100000000;
15 |
16 | // Update & Callback & Ref & Snapshot
17 | export const LifecycleEffectMask = /* */ 0b00110100100;
18 |
19 | // Union of all host effects
20 | export const HostEffectMask = /* */ 0b00111111111;
21 |
22 | export const Incomplete = /* */ 0b01000000000;
23 | export const ShouldCapture = /* */ 0b10000000000;
--------------------------------------------------------------------------------
/src/reconciler/ReactUpdateQueue.js:
--------------------------------------------------------------------------------
1 | import {NoWork} from './ReactFiberExpirationTime'
2 | // Assume when processing the updateQueue, process all updates together
3 | class UpdateQueue {
4 | constructor (baseState) {
5 | this.baseState = baseState
6 | this.firstUpdate = null
7 | this.lastUpdate = null
8 | }
9 | }
10 |
11 | class Update {
12 | constructor () {
13 | this.payload = null
14 | this.next = null
15 | }
16 | }
17 |
18 | export function createUpdate () {
19 | return new Update()
20 | }
21 |
22 | function appendUpdateToQueue (queue, update) {
23 | // Append the update to the end of the list.
24 | if (queue.lastUpdate === null) {
25 | // Queue is empty
26 | queue.firstUpdate = queue.lastUpdate = update
27 | } else {
28 | queue.lastUpdate.next = update
29 | queue.lastUpdate = update
30 | }
31 | }
32 |
33 | export function enqueueUpdate (fiber, update) {
34 | // Update queues are created lazily.
35 | let queue = fiber.updateQueue
36 | if (queue === null) {
37 | queue = fiber.updateQueue = new UpdateQueue(fiber.memoizedState)
38 | }
39 | appendUpdateToQueue(queue, update)
40 | }
41 |
42 | function getStateFromUpdate (update, prevState) {
43 | const partialState = update.payload
44 | if (partialState === null || partialState === undefined) {
45 | // Null and undefined are treated as no-ops.
46 | return prevState
47 | }
48 | // Merge the partial state and the previous state.
49 | return Object.assign({}, prevState, partialState)
50 | }
51 |
52 | export function processUpdateQueue (workInProgress, queue) {
53 | // Iterate through the list of updates to compute the result.
54 | let update = queue.firstUpdate
55 | let resultState = queue.baseState
56 | while (update !== null) {
57 | resultState = getStateFromUpdate(update, resultState)
58 | update = update.next
59 | }
60 | queue.baseState = resultState
61 | queue.firstUpdate = queue.lastUpdate = null
62 | workInProgress.expirationTime = NoWork
63 | workInProgress.memoizedState = resultState
64 | }
65 |
--------------------------------------------------------------------------------
/src/reconciler/ReactFiberExpirationTime.js:
--------------------------------------------------------------------------------
1 | export const NoWork = 0
2 | export const Sync = 1
3 |
4 | const UNIT_SIZE = 10
5 | const MAGIC_NUMBER_OFFSET = 2
6 |
7 | // 1 unit of expiration time represents 10ms.
8 | export function msToExpirationTime(ms) {
9 | // Always add an offset so that we don't clash with the magic number for NoWork.
10 | return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET
11 | }
12 |
13 | export function expirationTimeToMs(expirationTime) {
14 | return (expirationTime - MAGIC_NUMBER_OFFSET) * UNIT_SIZE
15 | }
16 |
17 | function ceiling(num, precision) {
18 | return (((num / precision) | 0) + 1) * precision
19 | }
20 |
21 | function computeExpirationBucket(
22 | currentTime,
23 | expirationInMs,
24 | bucketSizeMs,
25 | ) {
26 | return (
27 | MAGIC_NUMBER_OFFSET +
28 | ceiling(
29 | currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
30 | bucketSizeMs / UNIT_SIZE,
31 | )
32 | )
33 | }
34 |
35 | export const LOW_PRIORITY_EXPIRATION = 5000
36 | export const LOW_PRIORITY_BATCH_SIZE = 250
37 |
38 | export function computeAsyncExpiration (currentTime) {
39 | return computeExpirationBucket(
40 | currentTime,
41 | LOW_PRIORITY_EXPIRATION,
42 | LOW_PRIORITY_BATCH_SIZE,
43 | )
44 | }
45 |
46 | // We intentionally set a higher expiration time for interactive updates in
47 | // dev than in production.
48 | //
49 | // If the main thread is being blocked so long that you hit the expiration,
50 | // it's a problem that could be solved with better scheduling.
51 | //
52 | // People will be more likely to notice this and fix it with the long
53 | // expiration time in development.
54 | //
55 | // In production we opt for better UX at the risk of masking scheduling
56 | // problems, by expiring fast.
57 | export const HIGH_PRIORITY_EXPIRATION = 500
58 | export const HIGH_PRIORITY_BATCH_SIZE = 100
59 |
60 | export function computeInteractiveExpiration (currentTime) {
61 | return computeExpirationBucket(
62 | currentTime,
63 | HIGH_PRIORITY_EXPIRATION,
64 | HIGH_PRIORITY_BATCH_SIZE,
65 | )
66 | }
--------------------------------------------------------------------------------
/Guide/ReactCore.md:
--------------------------------------------------------------------------------
1 | # ReactCore
2 | >本章的代码在ReactCore分支中
3 |
4 | ## 介绍
5 |
6 | React Core 提供了 React.Component, React.createElement, React.Fragment 等等API。这里只会简单地实现 React.Component 和 React.createElement,因为我们的简单组件仅仅只需要这两个 API。
7 |
8 | ### React.Component
9 |
10 | 提供了 setState 方法。
11 |
12 | ### React.createElement
13 |
14 | 我们写下的 JSX 实际上会被 Babel 转译成调用 React.createElement 的结果。而 React.createElement 的作用就是根据给定的类型创建 ReactElement 对象。
15 |
16 | 关于 JSX 可以看 [WTF is JSX](https://jasonformat.com/wtf-is-jsx/) 这篇文章。
17 |
18 | 你也可以在 [babel REPL](https://babeljs.io/repl) 中尝试转译JSX。
19 |
20 | ## 实现
21 |
22 | 新建 ReactComponent.js 和 ReactElement.js
23 |
24 | ### ReactComponent.js
25 |
26 | ```javascript
27 | export class Component {
28 | constructor (props) {
29 | this.props = props
30 | this.updater = {}
31 | }
32 | setState (partialState) {
33 | this.updater.enqueueSetState(this, partialState)
34 | }
35 | }
36 | ```
37 | 注意 updater 会在 adoptClassInstance 中被更新。
38 |
39 | ### ReactElement.js
40 |
41 | ```javascript
42 | class ReactElement {
43 | constructor (type, props) {
44 | this.type = type
45 | this.props = props
46 | }
47 | }
48 |
49 | export function createElement(type, config, ...children) {
50 | const props = {}
51 | if (config !== null) {
52 | Object.keys(config).forEach(propName =>
53 | props[propName] = config[propName])
54 | }
55 | if (children.length >= 1) {
56 | props.children = children.length === 1 ? children[0] : children
57 | }
58 | return new ReactElement(type, props)
59 | }
60 | ```
61 |
62 | createElement 会创建一个具有 type 和 props 属性的对象。type 可能是一个 html 标签名称字符串也可能是一个类。props 包含了 config 中所有的属性,而且可能还有一个额外的 children 属性,保存的是后代。注意文本后代直接用字符串表示。
63 |
64 | 在 src 下新建文件夹 react, 将这两个文件放入其中,新建 index.js。
65 | ```javascript
66 | import { createElement } from './ReactElement'
67 | import { Component } from './ReactComponent'
68 |
69 | const React = {
70 | Component,
71 | createElement
72 | }
73 |
74 | export default React
75 | ```
76 |
77 | 在 App.js 和 index.js 中修改为 import React from './react',运行项目。可以看到项目正常运行。
78 |
79 | [下一章](Suspense.md)
--------------------------------------------------------------------------------
/src/event/isInteractiveEvent.js:
--------------------------------------------------------------------------------
1 | const interactiveEventTypeNames = [
2 | 'blur',
3 | 'cancel',
4 | 'click',
5 | 'close',
6 | 'contextMenu',
7 | 'copy',
8 | 'cut',
9 | 'auxClick',
10 | 'doubleClick',
11 | 'dragEnd',
12 | 'dragStart',
13 | 'drop',
14 | 'focus',
15 | 'input',
16 | 'invalid',
17 | 'keyDown',
18 | 'keyPress',
19 | 'keyUp',
20 | 'mouseDown',
21 | 'mouseUp',
22 | 'paste',
23 | 'pause',
24 | 'play',
25 | 'pointerCancel',
26 | 'pointerDown',
27 | 'pointerUp',
28 | 'rateChange',
29 | 'reset',
30 | 'seeked',
31 | 'submit',
32 | 'touchCancel',
33 | 'touchEnd',
34 | 'touchStart',
35 | 'volumeChange',
36 | ]
37 |
38 | const nonInteractiveEventTypeNames = [
39 | 'abort',
40 | 'animationEnd',
41 | 'animationIteration',
42 | 'animationStart',
43 | 'canPlay',
44 | 'canPlayThrough',
45 | 'drag',
46 | 'dragEnter',
47 | 'dragExit',
48 | 'dragLeave',
49 | 'dragOver',
50 | 'durationChange',
51 | 'emptied',
52 | 'encrypted',
53 | 'ended',
54 | 'error',
55 | 'gotPointerCapture',
56 | 'load',
57 | 'loadedData',
58 | 'loadedMetadata',
59 | 'loadStart',
60 | 'lostPointerCapture',
61 | 'mouseMove',
62 | 'mouseOut',
63 | 'mouseOver',
64 | 'playing',
65 | 'pointerMove',
66 | 'pointerOut',
67 | 'pointerOver',
68 | 'progress',
69 | 'scroll',
70 | 'seeking',
71 | 'stalled',
72 | 'suspend',
73 | 'timeUpdate',
74 | 'toggle',
75 | 'touchMove',
76 | 'transitionEnd',
77 | 'waiting',
78 | 'wheel',
79 | ]
80 |
81 | export const eventTypeNames = [...interactiveEventTypeNames, ...nonInteractiveEventTypeNames]
82 | export const bubblePhaseRegistrationNames = eventTypeNames.map(
83 | name => 'on' + name[0].toLocaleUpperCase() + name.slice(1)
84 | )
85 | export const capturePhaseRegistrationNames = bubblePhaseRegistrationNames.map(
86 | name => name + 'Capture'
87 | )
88 | export const registrationNames = [...bubblePhaseRegistrationNames, ...capturePhaseRegistrationNames]
89 | export function isInteractiveEvent (eventType) {
90 | return interactiveEventTypeNames.includes(eventType)
91 | }
--------------------------------------------------------------------------------
/src/reconciler/ReactFiber.js:
--------------------------------------------------------------------------------
1 | import { NoEffect } from '../shared/ReactSideEffectTags'
2 | import { NoWork } from './ReactFiberExpirationTime'
3 | import { HostRoot } from '../shared/ReactWorkTags'
4 |
5 | export function FiberNode (tag, pendingProps) {
6 | // Instance
7 | // Tag identifying the type of fiber.
8 | this.tag = tag
9 | // Unique identifier of this child.
10 | // this.key = key
11 | // The function/class/module associated with this fiber.
12 | this.type = null
13 | // The local state associated with this fiber
14 | this.stateNode = null
15 |
16 | // Fiber
17 | // The Fiber to return to after finishing processing this one.
18 | // This is effectively the parent, but there can be multiple parents (two)
19 | // so this is only the parent of the thing we're currently processing.
20 | // It is conceptually the same as the return address of a stack frame.
21 | this.return = null
22 | // Singly Linked List Tree Structure.
23 | this.child = null
24 | this.sibling = null
25 |
26 | // Input is the data coming into process this fiber. Arguments. Props.
27 | this.pendingProps = pendingProps // This type will be more specific once we overload the tag.
28 | this.memoizedProps = null // The props used to create the output
29 |
30 | // A queue of state updates and callbacks.
31 | this.updateQueue = null
32 |
33 | // The state used to create the output
34 | this.memoizedState = null
35 |
36 | // Effects
37 | this.effectTag = NoEffect
38 | // Singly linked list fast path to the next fiber with side-effects.
39 | this.nextEffect = null
40 | // The first and last fiber with side-effect within this subtree. This allows
41 | // us to reuse a slice of the linked list when we reuse the work done within
42 | // this fiber.
43 | this.firstEffect = null
44 | this.lastEffect = null
45 |
46 | // Represents a time in the future by which this work should be completed.
47 | // Does not include work found in its subtree.
48 | this.expirationTime = NoWork
49 |
50 | // This is a pooled version of a Fiber. Every fiber that gets updated will
51 | // eventually have a pair. There are cases when we can clean up pairs to save
52 | // memory if we need to.
53 | this.alternate = null
54 | }
55 |
56 | export function createHostRootFiber () {
57 | return new FiberNode(HostRoot, null)
58 | }
59 |
60 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from './react'
2 | import logo from './logo.svg'
3 | import './App.css'
4 |
5 | class ColorText extends React.Component {
6 | constructor(props) {
7 | super(props)
8 | this.state = {
9 | colorIndex: 0
10 | }
11 | }
12 | render () {
13 | const colorPanel = ['red', 'blue']
14 | return (
15 | this.setState({ colorIndex: (this.state.colorIndex + 1) % colorPanel.length })}
19 | >
20 | {this.props.children}
21 |
22 | )
23 | }
24 | }
25 |
26 | class App extends React.Component {
27 | constructor(props) {
28 | super(props);
29 | this.state = {
30 | counter: 0,
31 | value: ''
32 | };
33 | this.handleChange = this.handleChange.bind(this)
34 | }
35 |
36 | shouldComponentUpdate (nextProps, nextState) {
37 | if (this.state.counter === 1) {
38 | return false
39 | }
40 | return true
41 | }
42 |
43 | componentDidMount () {
44 | this.setState({counter: 1})
45 | }
46 |
47 | componentDidUpdate (prevProps, prevState, snapshot) {
48 | if (this.state.counter === 3) {
49 | this.setState({counter: 0})
50 | }
51 | }
52 |
53 | handleChange (event) {
54 | this.setState({value: event.target.value});
55 | }
56 |
57 | render() {
58 | return (
59 |
60 |
61 |
62 | Welcome to React
63 |
64 |
65 |
66 |
67 |
68 |
73 |
{this.state.counter}
74 |
79 |
80 |
81 |
82 |
83 | );
84 | }
85 | }
86 |
87 | export default App
88 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/CustomDom.js:
--------------------------------------------------------------------------------
1 | import ReactReconciler from './reconciler/Reconciler'
2 | import { registrationNames } from './event/isInteractiveEvent'
3 |
4 | let customRenderer
5 |
6 | const hostConfig = {
7 | now: () => {
8 | return performance.now
9 | },
10 | shouldSetTextContent: (props) => {
11 | return typeof props.children === 'string' || typeof props.children === 'number'
12 | },
13 | createInstance: (type, props, internalInstanceHandle) => {
14 | const domElement = document.createElement(type)
15 | domElement.internalInstanceKey = internalInstanceHandle
16 | domElement.internalEventHandlersKey = props
17 | return domElement
18 | },
19 | finalizeInitialChildren: (domElement, props) => {
20 | Object.keys(props).forEach(propKey => {
21 | const propValue = props[propKey]
22 | if (propKey === 'children') {
23 | if (typeof propValue === 'string' || typeof propValue === 'number') {
24 | domElement.textContent = propValue
25 | }
26 | } else if (propKey === 'style') {
27 | const style = domElement.style
28 | Object.keys(propValue).forEach(styleName => {
29 | let styleValue = propValue[styleName]
30 | style.setProperty(styleName, styleValue)
31 | })
32 | } else if (propKey === 'className') {
33 | domElement.setAttribute('class', propValue)
34 | } else if (registrationNames.includes(propKey) || propKey === 'onChange') {
35 | let eventType = propKey.slice(2).toLocaleLowerCase()
36 | if (eventType.endsWith('capture')) {
37 | eventType = eventType.slice(0, -7)
38 | }
39 | document.addEventListener(eventType, customRenderer.dispatchEventWithBatch)
40 | } else {
41 | const propValue = props[propKey]
42 | domElement.setAttribute(propKey, propValue)
43 | }
44 | })
45 | },
46 | appendInitialChild: (parentInstance, child) => {
47 | parentInstance.appendChild(child)
48 | },
49 | appendChildToContainer: (container, child) => {
50 | container.appendChild(child)
51 | },
52 | removeChildFromContainer: (container, child) => {
53 | container.removeChild(child)
54 | },
55 | scheduleDeferredCallback: (callback, options) => {
56 | requestIdleCallback(callback, options)
57 | },
58 | prepareUpdate: (oldProps, newProps) => {
59 | let updatePayload = null
60 | let styleUpdates = null
61 | Object.keys(newProps).forEach(propKey => {
62 | let nextProp = newProps[propKey]
63 | let lastProp = oldProps[propKey]
64 | if (nextProp !== lastProp && (typeof nextProp === 'string' || typeof nextProp === 'number')) {
65 | (updatePayload = updatePayload || []).push(propKey, '' + nextProp)
66 | }
67 | if (propKey === 'style') {
68 | for (let styleName in nextProp) {
69 | if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {
70 | styleUpdates = nextProp
71 | break
72 | }
73 | }
74 | if (styleUpdates) {
75 | (updatePayload = updatePayload || []).push(propKey, styleUpdates)
76 | }
77 | }
78 | })
79 | return updatePayload
80 | },
81 | commitUpdate: (domElement, updatePayload) => {
82 | for (let i = 0; i < updatePayload.length; i += 2) {
83 | let propKey = updatePayload[i]
84 | let propValue = updatePayload[i + 1]
85 | if (propKey === 'children') {
86 | domElement.textContent = propValue
87 | } else if (propKey === 'style'){
88 | const style = domElement.style
89 | Object.keys(propValue).forEach(styleName => {
90 | let styleValue = propValue[styleName]
91 | style.setProperty(styleName, styleValue)
92 | })
93 | } else {
94 | domElement[propKey] = propValue
95 | }
96 | }
97 | }
98 | }
99 |
100 | customRenderer = ReactReconciler(hostConfig)
101 |
102 | export const CustomDom = {
103 | render: (reactElement, container) => {
104 | let root = container._reactRootContainer
105 | if (!root) {
106 | // initial mount
107 | const isConcurrent = true // concurrent mode
108 | root = container._reactRootContainer = customRenderer.createContainer(container, isConcurrent)
109 | }
110 | customRenderer.updateContainer(reactElement, root)
111 | }
112 | }
113 |
114 |
--------------------------------------------------------------------------------
/Guide/CustomRenderer.md:
--------------------------------------------------------------------------------
1 | # 自定义渲染器
2 | >本章的代码在CustomRender分支中
3 |
4 | ## 什么是renderer
5 |
6 | 简单地说 renderer 把我们的代码渲染在特定的环境中。比如 DomRenderer 把代码渲染在浏览器环境中,NativeRenderer 则是渲染在移动环境中。其他还有 ReactNoopRenderer 是 React 内部用来调试 Fiber 的, ReactTestRenderer 可以将 React 组件渲染成纯 JavaScript 对象,甚至都不需要依赖于 DOM 和原生移动环境。
7 |
8 | What's More
9 |
10 | 一个有趣的项目, 把React组件渲染成一个word文档: [Making-a-custom-React-renderer](https://github.com/nitin42/Making-a-custom-React-renderer)
11 |
12 | ## 怎样写一个自定义的渲染器
13 |
14 | 上面的项目已经包含了如何写一个渲染器的教程,你可以选择通过阅读它来完成自己的 Renderer。但是我的目标是在浏览器环境中调试 React, 所以我需要的是一个简单的 DomRenderer。我发现上面的教程对我来说仍然不够简单直接。
15 |
16 | [Hello World Custom React Renderer](https://medium.com/@agent_hunt/hello-world-custom-react-renderer-9a95b7cd04bc)
17 |
18 | 这篇文章(需要翻墙)论述了如何写一个非常简单的 DomRenderer。我强烈建议你自己通过文章里的方法构建一个自己的DomRenderer,因为很可能最终得到的结果和我有所不同(我的结果也和文章里的有区别)。
19 |
20 | **简单的说一下步骤:**
21 |
22 | 1. 用 [create-react-app](https://github.com/facebook/create-react-app) 初始化一个项目
23 | 2. 在 App.js 写一个简单的组件
24 | ```javascript
25 | import React, { Component } from 'react';
26 | import logo from './logo.svg';
27 | import './App.css';
28 |
29 | class App extends Component {
30 | constructor(props) {
31 | super(props);
32 | this.state = {
33 | counter: 0
34 | };
35 | }
36 |
37 | render() {
38 | return (
39 |
40 |
41 |
42 | Welcome to React
43 |
44 |
45 |
46 |
52 |
{this.state.counter}
53 |
59 |
60 |
61 |
62 | );
63 | }
64 | }
65 |
66 | export default App;
67 | ```
68 |
69 | 3. 安装 react-reconciler (yarn add react-reconciler)
70 |
71 | reconciler 模块是实现 fiber 架构的关键,它接受一个 hostConfig 对象, 返回一个 renderer。
72 | 这样设计的原因是为了复用 reconciler 模块, 这样不同的环境可以定义不同的 hostConfig 对象。
73 |
74 | 在 [React 官方文档](https://reactjs.org/docs/implementation-notes.html)中也提到了
75 |
76 | Renderers use injection to pass the host internal class to the reconciler. For example, React DOM tells the reconciler to use ReactDOMComponent as the host internal instance implementation
77 |
78 | 4. 在 src 目录下新建 CustomDom.js
79 | ```javascript
80 | import ReactReconciler from 'react-reconciler'
81 |
82 | const hostConfig = {}
83 |
84 | const customRenderer = ReactReconciler(hostConfig)
85 |
86 | export const CustomDom = {
87 | render: (reactElement, container) => {
88 | let root = container._reactRootContainer
89 | if (!root) {
90 | // initial mount
91 | const isConcurrent = true // concurrent mode
92 | root = container._reactRootContainer = customRenderer.createContainer(container, isConcurrent)
93 | }
94 | customRenderer.updateContainer(reactElement, root)
95 | }
96 | }
97 | ```
98 | hostConfig 目前只是一个空的对象,我们将会在下面一步一步完善它。
99 | CustomDom 是用来替换 ReacDom 的([ReactDom的源码](https://github.com/facebook/react/blob/master/packages/react-dom/src/client/ReactDOM.js))
100 | 注意我们已经做了很多简化工作,只保留了 ReactDom 的 render 函数,而且忽略了 render 函数可能传入的第三个 callback 参数。另外注意异步模式默认是不开启的,我们需要令 isConcurrent = true。当我自己实现了 createContainer
101 | 函数的时候,我会忽略 isConcurrent 参数, 因为我默认它是 true。
102 |
103 | 本来异步 React 是叫 Async React,但是现在改为叫 Concurrent React 了, 有兴趣可以看看 dan 在 twitter 上的[讨论](https://twitter.com/dan_abramov/status/1036940380854464512)
104 |
105 | 通过简化,render函数很容易理解:在第一次调用 CustomDom.render 的时候,即在mount过程中,会调用 createContainer 函数来构造一个 root。 然后将 root 保存到 container 的 _reactRootContainer 属性中。 这样下次再调用 CustomDom.render 的时候就可以使用之前的 root。最后调用 updateContainer 函数渲染组件。
106 |
107 | container其实就是一个Dom节点,组件会被渲染在它里面。
108 |
109 | 这里已经涉及到了 Element, Root 等概念:
110 |
111 | * render 函数的第一个参数 reactElement 是一个 Element 对象,关于其介绍以及和 Component,Instance 的区别可以看 React [官方文档](https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html)。
112 | 简单的来说它是通过 babel 将 jsx 编译成类似于
113 | ```javascript
114 | {
115 | type: 'div'
116 | props: {}
117 | }
118 | ```
119 | 这样的对象,具体如何实现请看 ReactCore 章节
120 | * Root 是一个 FiberRoot 类的实例,我将会和 Fiber 一起介绍它
121 |
122 | 5. 在 index.js 中用我们自定义的 Dom 替换 ReactDom
123 | ```javascript
124 | import React from 'react';
125 | import { CustomDom } from './CustomDom';
126 | // import ReactDOM from 'react-dom';
127 | import './index.css';
128 | import App from './App';
129 | import registerServiceWorker from './registerServiceWorker';
130 |
131 | CustomDom.render(, document.getElementById('root'));
132 | // ReactDOM.render(, document.getElementById('root'));
133 | registerServiceWorker();
134 | ```
135 | 如果现在运行程序,会抛出如下错误
136 |
137 | 
138 |
139 | 原因是 reconciler 模块会使用 hostConfig 中定义的 now 函数。 所以我们往 hostConfig 中加入 now 函数:
140 | ```javascript
141 | const hostConfig = {
142 | now: () => {
143 | return performance.now()
144 | }
145 | }
146 | ```
147 | 再次运行我们会得到相似的错误,提醒我们需要往 hostConfig 中加入更多函数。
148 | 最终完成的hostConfig:
149 | ```javascript
150 | const hostConfig = {
151 | now: () => {
152 | return performance.now
153 | },
154 | getRootHostContext: () => {
155 | return null
156 | },
157 | getChildHostContext: () => {
158 | return null
159 | },
160 | shouldSetTextContent: (type, props) => {
161 | return typeof props.children === 'string' || typeof props.children === 'number'
162 | },
163 | createInstance: (type) => {
164 | const domElement = document.createElement(type)
165 | return domElement
166 | },
167 | finalizeInitialChildren: (domElement, type, props) => {
168 | Object.keys(props).forEach(propKey => {
169 | const propValue = props[propKey]
170 | if (propKey === 'children') {
171 | if (typeof propValue === 'string' || typeof propValue === 'number') {
172 | domElement.textContent = propValue
173 | }
174 | } else if (propKey === 'className') {
175 | domElement.setAttribute('class', propValue)
176 | } else if (propKey === 'onClick') {
177 | domElement.addEventListener('click', propValue)
178 | } else {
179 | const propValue = props[propKey]
180 | domElement.setAttribute(propKey, propValue)
181 | }
182 | })
183 | return false
184 | },
185 | appendInitialChild: (parentInstance, child) => {
186 | parentInstance.appendChild(child)
187 | },
188 | prepareForCommit: () => {
189 | },
190 | resetAfterCommit: () => {
191 | },
192 | supportsMutation: true,
193 | appendChildToContainer: (container, child) => {
194 | container.appendChild(child)
195 | },
196 | scheduleDeferredCallback: (callback, options) => {
197 | requestIdleCallback(callback, options)
198 | },
199 | shouldDeprioritizeSubtree: () => {
200 | return false
201 | },
202 | prepareUpdate: (domElement, type, oldProps, newProps) => {
203 | let updatePayload = null
204 | Object.keys(newProps).forEach(propKey => {
205 | let nextProp = newProps[propKey]
206 | let lastProp = oldProps[propKey]
207 | if (nextProp !== lastProp && (typeof nextProp === 'string' || typeof nextProp === 'number')) {
208 | (updatePayload = updatePayload || []).push(propKey, '' + nextProp)
209 | }
210 | })
211 | return updatePayload
212 | },
213 | commitUpdate: (domElement, updatePayload) => {
214 | for (let i = 0; i < updatePayload.length; i += 2) {
215 | let propKey = updatePayload[i]
216 | let propValue = updatePayload[i + 1]
217 | domElement.textContent = propValue
218 | }
219 | }
220 | }
221 | ```
222 |
223 | 现在再运行项目
224 |
225 | 
226 |
227 | ## 理解hostConfig
228 |
229 | hostConfig 中所有的函数都会被 reconciler 模块用到,但是有些函数对我们来说不是很重要,因为我们实现的 SimpleReact 会忽略一些功能,包括 [Context API](https://reactjs.org/blog/2018/03/29/react-v-16-3.html)。
230 | 所以在此,我不会深究 getRootHostContext 和 getChildHostContext 函数。另外 prepareForCommit 和 resetAfterCommit函数对我们这个简单的组件也不重要。shouldDeprioritizeSubtree 函数和 hidden 属性有关,这里直接返回 false。
231 |
232 | 我会逐一介绍剩下的9个函数
233 |
234 | ### now
235 |
236 | 返回现在的时间
237 |
238 | ### shouldSetTextContent
239 |
240 | 判断组件的直接后代是否是文本或者数字
241 |
242 | ### createInstance
243 |
244 | 根据 type 创建 dom 节点, type 即为像 'div' 的等字符
245 |
246 | ### finalizeInitialChildren
247 |
248 | 设置 dom 节点的属性,需要注意的是简化了事件绑定,只能识别click事件。后面的事件处理章节将完善这部分逻辑。
249 |
250 | ### appendInitialChild 和 appendChildToContainer
251 |
252 | 向 dom 节点中添加子节点
253 |
254 | ### scheduleDeferredCallback
255 |
256 | 重点是这个函数,它是实现时间分片的基础。我这里为了简化直接调用了 [requestIdleCallback](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback)。React有它自己的实现
257 | [unstable_scheduleCallback](https://github.com/facebook/react/blob/master/packages/scheduler/src/Scheduler.js).
258 |
259 | 实现这个函数的目的就是在浏览器空闲时期完成相应的计算工作,这和Fiber架构息息相关,我将会在下一章节讨论这些问题。
260 |
261 | ### prepareUpdate 和 commitUpdate
262 |
263 | 这两个函数将会在组件更新的过程中扮演重要的角色。prepareUpdate 判断组件更新前后 props 是否有变化,
264 | 在我们这个简单组件的例子中主要是判断 props.children,即判断更新前后组件的后代是否发生了变化。将变化保存起来。
265 | commitUpdate 会真的应用这些变化,同样在我们的例子中,只考虑 children 的变化,所以设置 dom 节点的 textContent 属性为新的后代。
266 |
267 | ## 结论
268 |
269 | hostConfig 并没有完全完成。当我们实现了 reconciler 模块的时候,将去掉一些不必要的函数,加入一些必要的函数。
270 | 同时我需要在这里强调为了简化代码而做出的假设:
271 |
272 | **当我们考虑组件更新的时候,发生变化的只是文本节点**
273 |
274 | 这简化了 prepareUpdate 和 commitUpdate 的逻辑, 因为我们不需要考虑各种属性的变化, style 的变化等等。
275 |
276 | 对于 Fiber 架构, 目前为止我们只知道 reconciler 模块将使用 scheduleDeferredCallback 来实现时间分片。
277 |
278 | [下一章](Fiber_part1.md)
279 |
--------------------------------------------------------------------------------
/Guide/LifeCycles.md:
--------------------------------------------------------------------------------
1 | # 生命周期函数
2 | >本章代码在 master 分支中
3 |
4 | 
5 | [React lifecycle methods diagram](http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/)
6 |
7 | 关于生命周期函数的用法,官方文档描述的已经够详细了,在此不再赘述。
8 |
9 | ## render 阶段
10 |
11 | 从图中可见,render 阶段包含 getDerivedStateFromProps 和 shouldComponentUpdate 函数。
12 |
13 | * static getDerivedStateFromProps(props, state)
14 |
15 | 从上图可以看到 getDerivedStateFromProps 会在 render 函数之前触发,且在 mount 和 udpate 阶段都会触发。
16 | mount 阶段可以在 mountClassInstance 中实现,update 阶段可以在 updateClassInstance 中实现
17 |
18 | * shouldComponentUpdate(nextProps, nextState)
19 |
20 | shouldComponentUpdate 只在 udpate 时触发,可以在 updateClassInstance 中实现。
21 |
22 | ```javascript
23 | function applyDerivedStateFromProps (workInProgress, getDerivedStateFromProps, nextProps) {
24 | const prevState = workInProgress.memoizedState
25 | const partialState = getDerivedStateFromProps(nextProps, prevState)
26 | // Merge the partial state and the previous state.
27 | const memoizedState = partialState === null || partialState === undefined ? prevState : Object.assign({}, prevState, partialState)
28 | workInProgress.memoizedState = memoizedState
29 | // Once the update queue is empty, persist the derived state onto the
30 | // base state.
31 | const updateQueue = workInProgress.updateQueue
32 | if (updateQueue !== null && workInProgress.expirationTime === NoWork) {
33 | updateQueue.baseState = memoizedState
34 | }
35 | }
36 |
37 | function mountClassInstance(workInProgress, ctor, newProps) {
38 | let instance = workInProgress.stateNode
39 | instance.props = newProps
40 | instance.state = workInProgress.memoizedState
41 | const updateQueue = workInProgress.updateQueue
42 | if (updateQueue !== null) {
43 | processUpdateQueue(workInProgress, updateQueue)
44 | instance.state = workInProgress.memoizedState
45 | }
46 | const getDerivedStateFromProps = ctor.getDerivedStateFromProps
47 | if (typeof getDerivedStateFromProps === 'function') {
48 | applyDerivedStateFromProps(workInProgress, getDerivedStateFromProps, newProps)
49 | instance.state = workInProgress.memoizedState
50 | }
51 | }
52 |
53 | function checkShouldComponentUpdate(workInProgress, newProps, newState) {
54 | const instance = workInProgress.stateNode
55 | if (typeof instance.shouldComponentUpdate === 'function') {
56 | const shouldUpdate = instance.shouldComponentUpdate(newProps, newState)
57 | return shouldUpdate
58 | }
59 | return true
60 | }
61 |
62 | function updateClassInstance (current, workInProgress, ctor, newProps) {
63 | const instance = workInProgress.stateNode
64 | const oldProps = workInProgress.memoizedProps
65 | instance.props = oldProps
66 | const oldState = workInProgress.memoizedState
67 | let newState = instance.state = oldState
68 | let updateQueue = workInProgress.updateQueue
69 | if (updateQueue !== null) {
70 | processUpdateQueue(
71 | workInProgress,
72 | updateQueue
73 | )
74 | newState = workInProgress.memoizedState
75 | }
76 | if (oldProps === newProps && oldState === newState) {
77 | return false
78 | }
79 | const getDerivedStateFromProps = ctor.getDerivedStateFromProps
80 | if (typeof getDerivedStateFromProps === 'function') {
81 | applyDerivedStateFromProps(workInProgress, getDerivedStateFromProps, newProps)
82 | newState = workInProgress.memoizedState
83 | }
84 | const shouldUpdate = checkShouldComponentUpdate(workInProgress, newProps, newState)
85 | if (shouldUpdate) {
86 | if (typeof instance.componentDidUpdate === 'function') {
87 | // 需要标记上 Update 标签,才能被累积到 effect list 中,才能触发 componentDidUpdate 函数。
88 | workInProgress.effectTag |= Update
89 | }
90 | }
91 | instance.props = newProps
92 | instance.state = newState
93 | return shouldUpdate
94 | }
95 | ```
96 |
97 | ## pre-commit 和 commit 阶段
98 |
99 | pre-commit 阶段包含 getSnapshotBeforeUpdate 函数。commit 阶段包含 componentDidMount 和 componentDidUpdate 以及 componentWillUnmount 函数。
100 |
101 | * getSnapshotBeforeUpdate(prevProps, prevState)
102 |
103 | getSnapshotBeforeUpdate 在 update 阶段修改 DOM 之前触发。可以在 commitRoot 中实现。
104 |
105 | * componentDidMount() 和 componentDidUpdate(prevProps, prevState, snapshot)
106 |
107 | componentDidMount 在组件插入到 DOM 后触发,componentDidUpdate 在 DOM 更新之后触发。两个函数都在 commitRoot 中实现
108 |
109 | * componentWillUnmount()
110 |
111 | componentWillUnmount 在组件被卸载前触发,可以在 commitDeletion 中实现。
112 |
113 | ```javascript
114 | function commitBeforeMutationLifeCycles (firstEffect) {
115 | let nextEffect = firstEffect
116 | while (nextEffect !== null) {
117 | if (nextEffect.tag === ClassComponent) {
118 | const instance = nextEffect.stateNode
119 | const getSnapshotBeforeUpdate = nextEffect.stateNode.getSnapshotBeforeUpdate
120 | if (typeof getSnapshotBeforeUpdate === 'function') {
121 | const current = nextEffect.alternate
122 | const prevProps = current.memoizedProps
123 | const prevState = current.memoizedState
124 | instance.props = nextEffect.memoizedProps
125 | instance.state = nextEffect.memoizedState
126 | const snapshot = getSnapshotBeforeUpdate(prevProps, prevState)
127 | instance.__reactInternalSnapshotBeforeUpdate = snapshot
128 | }
129 | }
130 | nextEffect = nextEffect.nextEffect
131 | }
132 | }
133 |
134 | function commitAllLifeCycles (firstEffect) {
135 | let nextEffect = firstEffect
136 | while (nextEffect !== null) {
137 | if (nextEffect.tag === ClassComponent) {
138 | const instance = nextEffect.stateNode
139 | const componentDidMount = instance.componentDidMount
140 | const componentDidUpdate = instance.componentDidUpdate
141 | const current = nextEffect.alternate
142 | if (current === null) {
143 | if (typeof componentDidMount === 'function') {
144 | instance.props = nextEffect.memoizedProps
145 | instance.state = nextEffect.memoizedState
146 | instance.componentDidMount()
147 | }
148 | } else {
149 | if (typeof componentDidUpdate === 'function') {
150 | const prevProps = current.memoizedProps
151 | const prevState = current.memoizedState
152 | instance.props = nextEffect.memoizedProps
153 | instance.state = nextEffect.memoizedState
154 | instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate)
155 | }
156 | }
157 | }
158 | nextEffect = nextEffect.nextEffect
159 | }
160 | }
161 |
162 | function commitRoot (root, finishedWork) {
163 | isWorking = true
164 | isCommitting = true
165 | root.expirationTime = NoWork
166 | const firstEffect = finishedWork.firstEffect
167 | // Invoke instances of getSnapshotBeforeUpdate before mutation
168 | commitBeforeMutationLifeCycles(firstEffect)
169 | // Commit all the side-effects within a tree. We'll do this in two passes.
170 | // The first pass performs all the host insertions, updates, deletions and
171 | // ref unmounts.
172 | commitAllHostEffects(firstEffect)
173 | // The work-in-progress tree is now the current tree. This must come after
174 | // the first pass of the commit phase, so that the previous tree is still
175 | // current during componentWillUnmount, but before the second pass, so that
176 | // the finished work is current during componentDidMount/Update.
177 | root.current = finishedWork
178 | // In the second pass we'll perform all life-cycles and ref callbacks.
179 | // Life-cycles happen as a separate pass so that all placements, updates,
180 | // and deletions in the entire tree have already been invoked.
181 | // This pass also triggers any renderer-specific initial effects.
182 | commitAllLifeCycles(firstEffect)
183 | isCommitting = false
184 | isWorking = false
185 | }
186 |
187 | function commitUnmount (current) {
188 | if (current.tag === ClassComponent) {
189 | const instance = current.stateNode
190 | if (typeof instance.componentWillUnmount === 'function') {
191 | instance.props = current.memoizedProps
192 | instance.state = current.memoizedState
193 | instance.componentWillUnmount()
194 | }
195 | }
196 | }
197 |
198 | function commitNestedUnmounts (root) {
199 | // While we're inside a removed host node we don't want to call
200 | // removeChild on the inner nodes because they're removed by the top
201 | // call anyway. We also want to call componentWillUnmount on all
202 | // composites before this host node is removed from the tree. Therefore
203 | let node = root
204 | while (true) {
205 | commitUnmount(node)
206 | // Visit children because they may contain more composite or host nodes.
207 | if (node.child !== null) {
208 | node.child.return = node
209 | node = node.child
210 | continue
211 | }
212 | if (node === root) {
213 | return
214 | }
215 | while (node.sibling === null) {
216 | if (node.return === null || node.return === root) {
217 | return
218 | }
219 | node = node.return
220 | }
221 | node.sibling.return = node.return
222 | node = node.sibling
223 | }
224 | }
225 |
226 | function commitDeletion (current) {
227 | const parentFiber = getHostParentFiber(current)
228 | const parent = parentFiber.tag === HostRoot ? parentFiber.stateNode.containerInfo : parentFiber.stateNode
229 | let node = current
230 | while (true) {
231 | if (node.tag === HostComponent) {
232 | commitNestedUnmounts(node)
233 | // After all the children have unmounted, it is now safe to remove the
234 | // node from the tree.
235 | removeChildFromContainer(parent, node.stateNode)
236 | } else {
237 | commitUnmount(node)
238 | // Visit children because we may find more host components below.
239 | if (node.child !== null) {
240 | node.child.return = node
241 | node = node.child
242 | continue
243 | }
244 | }
245 | if (node === current) {
246 | break
247 | }
248 | while (node.sibling === null) {
249 | if (node.return === null || node.return === current) {
250 | break
251 | }
252 | node = node.return
253 | }
254 | node.sibling.return = node.return
255 | node = node.sibling
256 | }
257 | current.return = null
258 | current.child = null
259 | if (current.alternate) {
260 | current.alternate.child = null
261 | current.alternate.return = null
262 | }
263 | }
264 | ```
--------------------------------------------------------------------------------
/Guide/Event.md:
--------------------------------------------------------------------------------
1 | # 事件处理
2 | >本章的代码在Event分支中
3 | ## 学习资料
4 | [How exactly does React handles events](https://levelup.gitconnected.com/how-exactly-does-react-handles-events-71e8b5e359f2)
5 |
6 | 关于 React 的事件处理系统的整体介绍
7 |
8 | [Interactive updates](https://github.com/facebook/react/pull/12100)
9 |
10 | React 对事件的三种划分:
11 |
12 | * Controlled events:更新会被同步地执行。
13 | * Interactive events:比普通的异步更新优先级高,其实就是对应用 computeInteractiveExpiration 计算更新任务到期时间。
14 | * Non-interactive events:低优先级的异步更新,对应用 computeAsyncExpiration 计算更新任务到期时间。
15 |
16 | [Does React keep the order for state updates](https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates#)
17 |
18 | 所有发生在 React event handler 中的更新会被批量处理。即当在一个 React event handler 中,不管调用多少 this.setState,只会导致重新渲染一次。
19 |
20 | 这就是为什么需要 isBatchingUpdates 和 isBatchingInteractiveUpdates 变量。
21 |
22 | ## 调试
23 |
24 | 修改一下App.js
25 | ```javascript
26 | class ColorText extends Component {
27 | constructor(props) {
28 | super(props)
29 | this.state = {
30 | colorIndex: 0
31 | }
32 | }
33 | render () {
34 | const colorPanel = ['red', 'blue']
35 | return (
36 | this.setState({ colorIndex: (this.state.colorIndex + 1) % colorPanel.length })}
40 | >
41 | {this.props.children}
42 |
43 | )
44 | }
45 | }
46 |
47 | class App extends Component {
48 | constructor(props) {
49 | super(props);
50 | this.state = {
51 | counter: 0,
52 | value: ''
53 | };
54 | this.handleChange = this.handleChange.bind(this)
55 | }
56 |
57 | handleChange (event) {
58 | this.setState({value: event.target.value});
59 | }
60 |
61 | render() {
62 | return (
63 |
64 |
65 |
66 | Welcome to React
67 |
68 |
69 |
70 |
71 |
72 |
77 |
{this.state.counter}
78 |
83 |
84 |
85 |
86 |
87 | );
88 | }
89 | }
90 | ```
91 |
92 | 加入了一个新组件 ColorText,触发点击事件之后改变文本颜色。由于 ColorText 的更新发生在 div 元素的 style 属性上,且加入的 input 元素,它的更新发生在其 value 属性上,所以这违反了之前所作的假设:只有文本节点发生改变。所以需要完善 finalizeInitialChildren,prepareUpdate 和 commitUpdate 函数的逻辑。
93 |
94 | ```javascript
95 | hostConfig = {
96 | finalizeInitialChildren: (domElement, props) => {
97 | Object.keys(props).forEach(propKey => {
98 | const propValue = props[propKey]
99 | if (propKey === 'children') {
100 | if (typeof propValue === 'string' || typeof propValue === 'number') {
101 | domElement.textContent = propValue
102 | }
103 | } else if (propKey === 'style') {
104 | // 设置初始 style
105 | const style = domElement.style
106 | Object.keys(propValue).forEach(styleName => {
107 | let styleValue = propValue[styleName]
108 | style.setProperty(styleName, styleValue)
109 | })
110 | } else if (propKey === 'className') {
111 | domElement.setAttribute('class', propValue)
112 | } else if (propKey === 'onClick') {
113 | domElement.addEventListener('click', propValue)
114 | } else {
115 | const propValue = props[propKey]
116 | domElement.setAttribute(propKey, propValue)
117 | }
118 | })
119 | },
120 | prepareUpdate: (oldProps, newProps) => {
121 | let updatePayload = null
122 | let styleUpdates = null
123 | Object.keys(newProps).forEach(propKey => {
124 | let nextProp = newProps[propKey]
125 | let lastProp = oldProps[propKey]
126 | if (nextProp !== lastProp && (typeof nextProp === 'string' || typeof nextProp === 'number')) {
127 | (updatePayload = updatePayload || []).push(propKey, '' + nextProp)
128 | }
129 | if (propKey === 'style') {
130 | for (let styleName in nextProp) {
131 | if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {
132 | // 新的 style 对象有和之前不同的属性值
133 | styleUpdates = nextProp
134 | break
135 | }
136 | }
137 | if (styleUpdates) {
138 | (updatePayload = updatePayload || []).push(propKey, styleUpdates)
139 | }
140 | }
141 | })
142 | return updatePayload
143 | },
144 | commitUpdate: (domElement, updatePayload) => {
145 | for (let i = 0; i < updatePayload.length; i += 2) {
146 | let propKey = updatePayload[i]
147 | let propValue = updatePayload[i + 1]
148 | if (propKey === 'children') {
149 | // 更新后代
150 | domElement.textContent = propValue
151 | } else if (propKey === 'style'){
152 | // 更新样式
153 | const style = domElement.style
154 | Object.keys(propValue).forEach(styleName => {
155 | let styleValue = propValue[styleName]
156 | style.setProperty(styleName, styleValue)
157 | })
158 | } else {
159 | // 更新属性
160 | domElement[propKey] = propValue
161 | }
162 | }
163 | }
164 | }
165 | ```
166 |
167 | 在必要的函数添加 console.log,运行项目。
168 |
169 | 
170 |
171 | 如果点击 - 或 + 按钮,本应该调用 computeInteractiveExpiration,实际上却调用了 computeAsyncExpiration。而且会触发两次 scheduleCallbackWithExpirationTime,一次是 button 上绑定的回调函数被触发调用this.setState。另一次是由于事件冒泡,导致 ColorText 组件中的 div 上绑定的回调函数被触发调用 this.setState。这不是正确的行为,两次更新都 schedule 了更新。正确的行为应该是两次更新触发一次 scheduleCallbackWithExpirationTime。
172 |
173 | ## 实现
174 |
175 | 事件处理的实现完全基于我个人的理解,它只考虑了很少的情况,可能完全是错误的。实际上 React 准备 [Drastically simplify the event system](https://github.com/facebook/react/issues/13525)。
176 |
177 | 首先需要区分 Controlled events, Interactive events 和 Non-interactive events。React 源码 [SimpleEventPlugin.js](https://github.com/facebook/react/blob/master/packages/react-dom/src/events/SimpleEventPlugin.js) 中有 Interactive events 和 Non-interactive events 的完整列举。比如说 click,focus 属于 Interactive events,而 mouseMove 和 drag 属于 Non-interactive events。
178 |
179 | 那么什么是 Controlled events?
180 |
181 | 受控组件中,像 <input>, <textarea>, 和 <select> 这类表单元素的 change 事件是一个 Controlled event。
182 |
183 | 新建一个 isInteractiveEvent.js。
184 |
185 | ```javascript
186 | const interactiveEventTypeNames = [
187 | 'blur',
188 | 'cancel',
189 | 'click',
190 | // ...
191 | ]
192 | const nonInteractiveEventTypeNames = [
193 | 'abort',
194 | 'animationEnd',
195 | 'animationIteration',
196 | // ...
197 | ]
198 | export const eventTypeNames = [...interactiveEventTypeNames, ...nonInteractiveEventTypeNames]
199 | export const bubblePhaseRegistrationNames = eventTypeNames.map(
200 | name => 'on' + name[0].toLocaleUpperCase() + name.slice(1)
201 | )
202 | export const capturePhaseRegistrationNames = bubblePhaseRegistrationNames.map(
203 | name => name + 'Capture'
204 | )
205 | export const registrationNames = [...bubblePhaseRegistrationNames, ...capturePhaseRegistrationNames]
206 | export function isInteractiveEvent (eventType) {
207 | return interactiveEventTypeNames.includes(eventType)
208 | }
209 | ```
210 |
211 | * registrationNames 包含了所有 interactiveEvent 和 nonInteractiveEvent 的名称。
212 | * isInteractiveEvent 判断事件是否属于 Interactive events。
213 |
214 | createInstance 和 finalizeInitialChildren 需要完善逻辑:
215 |
216 | ```javascript
217 | hostConfig = {
218 | createInstance: (type, props, internalInstanceHandle) => {
219 | const domElement = document.createElement(type)
220 | // 将传入节点的 props 和该节点的 fiber 保存在创建的 DOM 节点上
221 | domElement.internalInstanceKey = internalInstanceHandle
222 | domElement.internalEventHandlersKey = props
223 | return domElement
224 | },
225 | finalizeInitialChildren: (domElement, props) => {
226 | Object.keys(props).forEach(propKey => {
227 | const propValue = props[propKey]
228 | if (propKey === 'children') {
229 | if (typeof propValue === 'string' || typeof propValue === 'number') {
230 | domElement.textContent = propValue
231 | }
232 | } else if (propKey === 'style') {
233 | const style = domElement.style
234 | Object.keys(propValue).forEach(styleName => {
235 | let styleValue = propValue[styleName]
236 | style.setProperty(styleName, styleValue)
237 | })
238 | } else if (propKey === 'className') {
239 | domElement.setAttribute('class', propValue)
240 | } else if (registrationNames.includes(propKey) || propKey === 'onChange') {
241 | // propKey 是事件名
242 | let eventType = propKey.slice(2).toLocaleLowerCase()
243 | if (eventType.endsWith('capture')) {
244 | eventType = eventType.slice(0, -7)
245 | }
246 | // 所有的事件都绑定在 document 上
247 | document.addEventListener(eventType, customRenderer.dispatchEventWithBatch)
248 | } else {
249 | const propValue = props[propKey]
250 | domElement.setAttribute(propKey, propValue)
251 | }
252 | })
253 | },
254 | }
255 | ```
256 |
257 | 在 Reconciler.js 中加入如下函数
258 |
259 | ```javascript
260 | let isDispatchControlledEvent = false
261 |
262 | function dispatchEventWithBatch (nativeEvent) {
263 | const type = nativeEvent.type
264 | let previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates
265 | let previousIsBatchingUpdates = isBatchingUpdates
266 | let previousIsDispatchControlledEvent = isDispatchControlledEvent
267 | if (type === 'change') {
268 | isDispatchControlledEvent = true
269 | }
270 | if (isInteractiveEvent(type)) {
271 | isBatchingInteractiveUpdates = true
272 | }
273 | isBatchingUpdates = true
274 |
275 | try {
276 | return dispatchEvent(nativeEvent)
277 | } finally {
278 | console.log('before leaving event handler')
279 | isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates
280 | isBatchingUpdates = previousIsBatchingUpdates
281 | if (!isBatchingUpdates && !isRendering) {
282 | if (isDispatchControlledEvent) {
283 | isDispatchControlledEvent = previousIsDispatchControlledEvent
284 | if (scheduledRoot) { // if event triggers update
285 | performSyncWork()
286 | }
287 | } else {
288 | if (scheduledRoot) {
289 | scheduleCallbackWithExpirationTime(scheduledRoot, scheduledRoot.expirationTime)
290 | }
291 | }
292 | }
293 | }
294 | }
295 |
296 | function dispatchEvent (nativeEvent) {
297 | let listeners = []
298 | const nativeEventTarget = nativeEvent.target || nativeEvent.srcElement
299 | const targetInst = nativeEventTarget.internalInstanceKey
300 | // 按照捕获和冒泡的顺序保存所有的回调函数
301 | traverseTwoPhase(targetInst, accumulateDirectionalDispatches.bind(null, listeners), nativeEvent)
302 | // 触发所有的回调函数
303 | listeners.forEach(listener => listener(nativeEvent))
304 | }
305 |
306 | function accumulateDirectionalDispatches (acc, inst, phase, nativeEvent) {
307 | let type = nativeEvent.type
308 | let registrationName = 'on' + type[0].toLocaleUpperCase() + type.slice(1)
309 | if (phase === 'captured') {
310 | registrationName = registrationName + 'Capture'
311 | }
312 | const stateNode = inst.stateNode
313 | const props = stateNode.internalEventHandlersKey
314 | const listener = props[registrationName]
315 | if (listener) {
316 | acc.push(listener)
317 | }
318 | }
319 |
320 | function getParent(inst) {
321 | do {
322 | inst = inst.return
323 | } while (inst && inst.tag !== HostComponent)
324 | if (inst) {
325 | return inst
326 | }
327 | return null
328 | }
329 |
330 | // Simulates the traversal of a two-phase, capture/bubble event dispatch.
331 | export function traverseTwoPhase(inst, fn, arg) {
332 | const path = []
333 | while (inst) {
334 | path.push(inst)
335 | inst = getParent(inst)
336 | }
337 | let i
338 | for (i = path.length; i-- > 0; ) {
339 | fn(path[i], 'captured', arg)
340 | }
341 | for (i = 0; i < path.length; i++) {
342 | fn(path[i], 'bubbled', arg)
343 | }
344 | }
345 | ```
346 |
347 | isDispatchControlledEvent 变量表示触发的事件是否是 controlled event。
348 |
349 | ### dispatchEventWithBatch
350 |
351 | * 判断事件属于什么类型,如果是 controlled event,让 isDispatchControlledEvent 为 true。如果是 interactive event, 让 isBatchingInteractiveUpdates 为 true。不管是什么类型的事件,让 isBatchingUpdates 为 true。
352 | * 在 try 语句块中调用 dispatchEvent,实际上会调用事件绑定的回调函数,调用 this.setState。由于在 requestWork 中会判断 isBatchingUpdates 是否为真,如果为真则直接返回。所以并不会导致更新。
353 | * 在 finally 语句块中判断触发的事件是否为 controlled event,如果是则调用 performSyncWork 同步渲染。
354 | 否则调用 scheduleCallbackWithExpirationTime 异步渲染。
355 |
356 | 现在再运行项目,点击 - 或 + 按钮:
357 | 
358 |
359 | 可以看到触发了 computeInteractiveExpiration,而且只触发一次 scheduleCallbackWithExpirationTime。后面可能会因为耗尽了空闲时间而再次调用 scheduleCallbackWithExpirationTime。
360 |
361 | 在文本框中输入字符:
362 | 
363 |
364 | 可以看到调用了 performSyncWork。
365 |
366 | [下一章](ReactCore.md)
367 |
--------------------------------------------------------------------------------
/Guide/Fiber_part1.md:
--------------------------------------------------------------------------------
1 | # Fiber 架构--序言
2 | >本章的代码在Fiber分支中
3 |
4 | ## 为什么需要 Fiber ?
5 |
6 | 在 Fiber 之前,Stack Reconciler 负责完成组件的渲染。简单的说, 一旦我们调用 ReactDOM.render 来进行第一次组件挂载,或是用户交互触发了 this.setState 更新过程,整个挂载或者更新过程不可能被打断。如果计算量庞大,会使得浏览器渲染一帧的时间过长,造成卡顿,用户的交互也无法得到实时的反馈。
7 |
8 | 关于 Stack Reconciler 的实现,可以看官网 [Implementation Notes](https://reactjs.org/docs/implementation-notes.html)。为什么整个过程不能被打断,从 mount 过程来看,遇到 class 组件的时候,会创建一个实例并调用实例的 render 函数来明确它要渲染什么;遇到 functional 组件的时候,会调用此函数来明确它要渲染什么;遇到原生节点的时候,会创建一个 DOM 节点并添加到父节点下。 这个过程是递归的,最终我们将得到一颗完整的 DOM 树。从最开始到修改 DOM,整个过程由 javascript stack 控制,而我们无法控制 js 栈,我们不能对js引擎说:噢,我们已经花了太长时间在计算上了,我们该停止工作了,让浏览器做些渲染。
9 |
10 | ## 为什么 Fiber 能解决这些问题 ?
11 |
12 | 简要地说,Fiber 架构把工作分成一份一份,并用上一章节提到的 scheduleDeferredCallback 把每一小份工作分散到浏览器的空闲时期来完成。这就不会使 js 主线程占用过长的时间,导致上面发生的问题。
13 |
14 | ## 什么是 Fiber ?
15 |
16 | 社区已经有很多介绍 Fiber 的资料,我首推 [fresh-async-react](https://github.com/sw-yx/fresh-async-react)。 这个项目涵盖了目前为止关于 React 的各种官方和社区的资料,并且在不断更新中。
17 |
18 | 为了理解 Fiber 架构,我觉得这几个视频有必要重点看一下:
19 | * [Beyond React 16 ](https://www.youtube.com/watch?v=v6iR3Zk4oDY)
20 |
21 | 演示了 time slicing 和 suspense。
22 |
23 | * [Lin Clark's A Cartoon Intro to Fiber](https://www.youtube.com/watch?v=ZCuYPiUIONs)
24 |
25 | 生动形象地介绍了 Fiber 架构。 我简要提一下视频中提到的 render 和 commit 阶段(后面的陈述可能会涉及这两个名词):
26 |
27 | 阶段一,render phase,这个阶段会构建 work-in-progress fiber 树,得到和之前的 fiber 树的差别,但是不会应用这些差别到 DOM。这个阶段能够被打断。
28 |
29 | 阶段二,commit phase,这个阶段才会真正的修改 DOM。 这个阶段不能被打断。
30 |
31 | * [Algebraic Effects, Fibers, Coroutines](https://www.youtube.com/watch?v=7GcrT0SBSnI)
32 |
33 | 最后一个视频非常有用,它让我对 Fiber 为什么被设计成这样,time slicing 和 suspense 是如何实现的有了一个概观。
34 |
35 | 之前 React 在挂载和更新过程中,本质上就是在调用函数,而函数的执行是由 javascript call stack 控制的,stack frame 则代表了函数的调用。stack frame 的创建销毁都是由 js 引擎完成的,我们不能在程序中使用它。
36 |
37 | 
38 |
39 | 整个Fiber 架构可以看作实现了一个类似于 javascript call stack 的 React call stack,而具体的单个 fiber 实例可以看作是一个包含了组件信息的 stack frame。 而现在这个call stack是我们能够完全控制的,我们可以创建,删除,复制 stack frame。
40 |
41 | 就像一个 stack frame 包含了指向当前函数的指针,函数的返回地址,函数参数,临时变量等等,一个 fiber 实例包含了当前的组件信息,父组件 fiber,props, state 等等。
42 |
43 | 现在,让我们看看真正的 fiber 是什么样子:
44 | ```javascript
45 | // A Fiber is work on a Component that needs to be done or was done. There can
46 | // be more than one per component.
47 | type Fiber = {|
48 | // Tag identifying the type of fiber.
49 | tag: WorkTag,
50 |
51 | // Unique identifier of this child.
52 | key: null | string,
53 |
54 | // The function/class/module associated with this fiber.
55 | type: any,
56 |
57 | // The local state associated with this fiber.
58 | stateNode: any,
59 |
60 | // Conceptual aliases
61 | // parent : Instance -> return The parent happens to be the same as the
62 | // return fiber since we've merged the fiber and instance.
63 |
64 | // Remaining fields belong to Fiber
65 |
66 | // The Fiber to return to after finishing processing this one.
67 | // This is effectively the parent, but there can be multiple parents (two)
68 | // so this is only the parent of the thing we're currently processing.
69 | // It is conceptually the same as the return address of a stack frame.
70 | return: Fiber | null,
71 |
72 | // Singly Linked List Tree Structure.
73 | child: Fiber | null,
74 | sibling: Fiber | null,
75 | index: number,
76 |
77 | // The ref last used to attach this node.
78 | // I'll avoid adding an owner field for prod and model that as functions.
79 | ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,
80 |
81 | // Input is the data coming into process this fiber. Arguments. Props.
82 | pendingProps: any, // This type will be more specific once we overload the tag.
83 | memoizedProps: any, // The props used to create the output.
84 |
85 | // A queue of state updates and callbacks.
86 | updateQueue: UpdateQueue | null,
87 |
88 | // The state used to create the output
89 | memoizedState: any,
90 |
91 | // A linked-list of contexts that this fiber depends on
92 | firstContextDependency: ContextDependency | null,
93 |
94 | // Bitfield that describes properties about the fiber and its subtree. E.g.
95 | // the ConcurrentMode flag indicates whether the subtree should be async-by-
96 | // default. When a fiber is created, it inherits the mode of its
97 | // parent. Additional flags can be set at creation time, but after that the
98 | // value should remain unchanged throughout the fiber's lifetime, particularly
99 | // before its child fibers are created.
100 | mode: TypeOfMode,
101 |
102 | // Effect
103 | effectTag: SideEffectTag,
104 |
105 | // Singly linked list fast path to the next fiber with side-effects.
106 | nextEffect: Fiber | null,
107 |
108 | // The first and last fiber with side-effect within this subtree. This allows
109 | // us to reuse a slice of the linked list when we reuse the work done within
110 | // this fiber.
111 | firstEffect: Fiber | null,
112 | lastEffect: Fiber | null,
113 |
114 | // Represents a time in the future by which this work should be completed.
115 | // Does not include work found in its subtree.
116 | expirationTime: ExpirationTime,
117 |
118 | // This is used to quickly determine if a subtree has no pending changes.
119 | childExpirationTime: ExpirationTime,
120 |
121 | // This is a pooled version of a Fiber. Every fiber that gets updated will
122 | // eventually have a pair. There are cases when we can clean up pairs to save
123 | // memory if we need to.
124 | alternate: Fiber | null,
125 |
126 | // Time spent rendering this Fiber and its descendants for the current update.
127 | // This tells us how well the tree makes use of sCU for memoization.
128 | // It is reset to 0 each time we render and only updated when we don't bailout.
129 | // This field is only set when the enableProfilerTimer flag is enabled.
130 | actualDuration?: number,
131 |
132 | // If the Fiber is currently active in the "render" phase,
133 | // This marks the time at which the work began.
134 | // This field is only set when the enableProfilerTimer flag is enabled.
135 | actualStartTime?: number,
136 |
137 | // Duration of the most recent render time for this Fiber.
138 | // This value is not updated when we bailout for memoization purposes.
139 | // This field is only set when the enableProfilerTimer flag is enabled.
140 | selfBaseDuration?: number,
141 |
142 | // Sum of base times for all descedents of this Fiber.
143 | // This value bubbles up during the "complete" phase.
144 | // This field is only set when the enableProfilerTimer flag is enabled.
145 | treeBaseDuration?: number,
146 |
147 | // Conceptual aliases
148 | // workInProgress : Fiber -> alternate The alternate used for reuse happens
149 | // to be the same as work in progress.
150 | // __DEV__ only
151 | _debugID?: number,
152 | _debugSource?: Source | null,
153 | _debugOwner?: Fiber | null,
154 | _debugIsCurrentlyTiming?: boolean,
155 | |};
156 | ```
157 | React 内部用了flow 作为类型检查。我会介绍下面这些属性。
158 |
159 | ### tag
160 | 一个 fiber 对应了一个节点,节点可能是用户定义的组件 <App>,也可能是原生节点<div>。所以 fiber 也有不同的类型,而 tag 代表了 fiber 的类型。可能的类型在 [ReactWorkTags.js](https://github.com/facebook/react/blob/master/packages/shared/ReactWorkTags.js) 中。
161 | 为了简化,本项目将只支持 ClassComponent,HostRoot, HostComponent 类型
162 | * ClassComponent:表示用户自定义的 class 组件的 fiber
163 | * HostRoot:表示根节点的 fiber,根节点就是调用 ReactDOM.render 时传入的第二个参数 container。
164 | * HostComponent: 表示特定环境中的原生节点的 fiber,如 DOM 中 <div>, Native 中的 <View>
165 |
166 | ### key
167 |
168 | 创建元素数组时需要包含的特殊字符串, 在某些元素被增加或删除的时候帮助 React 识别哪些元素发生了变化。为了简化,本项目不会使用 key 作为识别变化的依据。
169 |
170 | ### type
171 |
172 | * HostRoot 类型的 fiber,type 是 null
173 | * ClassComponent 类型的 fiber, type 是用户声明的组件类的构造函数
174 | * HostComponent 类型的 fiber, type 是节点的标签的字符串表示,即表示 <div> 的 fiber 的 type 是字符串 'div'
175 |
176 | ### stateNode
177 |
178 | * HostRoot 类型的 fiber,stateNode 是一个 FiberRoot 类的实例
179 | * ClassComponent 类型的 fiber,stateNode 是一个用户声明的组件类的实例
180 | * HostComponent 类型的 fiber,stateNode 是该 fiber 表示的 DOM 节点
181 |
182 | ### return, child 和 sibling
183 |
184 | 
185 |
186 | return,child 和 sibling 属性构造了一颗 fiber 树。注意 child 引用的是当前 fiber 下第一个子 fiber。
187 |
188 | ### index
189 |
190 | index 会用来判断元素是否发生了移动,我会忽略这个属性,因为我假设**更新永远不会改变元素的位置**。
191 |
192 | ### ref
193 |
194 | 我不会实现 [refs 功能](https://react.docschina.org/docs/glossary.html#refs),忽略这个属性。
195 |
196 | ### pendingProps, memoizedProps 和 memoizedState
197 |
198 | * pendingProps:组件收到的新的 props
199 | * memoizedProps:组件保存的旧的 props
200 | * memoizedState:组件保存的旧的 state
201 |
202 | ### updateQueue
203 | 状态更新保存在这里,实际上组件新的 state 会通过 processUpdateQueue 而得到。
204 |
205 | ### mode
206 |
207 | 表示 fiber 的工作模式,可能的值在 [ReactTypeOfMode.js](https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactTypeOfMode.js) 中。
208 | 我假设所有的 fiber 只工作在 ConcurrentMode,因此忽略这个属性。
209 |
210 | ### effectTag
211 |
212 | effectTag 代表了此 fiber 包含的副作用,可能的值在 [ReactSideEffectTags.js](https://github.com/facebook/react/blob/master/packages/shared/ReactSideEffectTags.js) 中。
213 | 为了简化,我假设**组件在渲染过程中只会包含 Placement 和 Update 这两种副作用**。 Placement 副作用表示该 fiber 第一次被挂载,Update 副作用表示该 fiber 包含状态更新。
214 |
215 | ### nextEffect, firstEffect 和 lastEffect
216 |
217 | nextEffect 属性构成了所有包含副作用的 fiber 的一个单向链表。firstEffect 和 lastEffect 分别指向该 fiber 子树(不包含该 fiber 自身)中第一个和最后一个包含副作用的fiber
218 |
219 | ### expirationTime
220 |
221 | 代表此 fiber 自身需要被 commit,需要被挂载或更新的最后期限,超过这个期限将会导致这个 fiber 不再等待浏览器的空闲时间来完成它的工作,而是表现的和未激活异步模式一样,同步的完成它的工作。
222 |
223 | ### childExpirationTime
224 |
225 | 代表子树中的未完成的 fiber 的 expirationTime。我会忽略这个属性。
226 |
227 | ### alternate
228 |
229 | 在任何情况下,每个组件实例最多有两个 fiber 何其关联。一个是被 commit 过后的 fiber,即它所包含的副作用已经被应用到了 DOM 上了,称它为 current fiber;另一个是现在未被 commit 的 fiber,称为 work-in-progress fiber。
230 |
231 | current fiber 的 alternate 是 work-in-progress fiber, 而 work-in-progress fiber 的 alternate 是 current fiber。
232 |
233 | 最后简化了的 fiber 的构造函数 [ReactFiber.js](/src/reconciler/ReactFiber.js)
234 |
235 |
236 | ## 什么是 FiberRoot ?
237 | ```javascript
238 | type BaseFiberRootProperties = {|
239 | // Any additional information from the host associated with this root.
240 | containerInfo: any,
241 | // Used only by persistent updates.
242 | pendingChildren: any,
243 | // The currently active root fiber. This is the mutable root of the tree.
244 | current: Fiber,
245 |
246 | // The following priority levels are used to distinguish between 1)
247 | // uncommitted work, 2) uncommitted work that is suspended, and 3) uncommitted
248 | // work that may be unsuspended. We choose not to track each individual
249 | // pending level, trading granularity for performance.
250 | //
251 | // The earliest and latest priority levels that are suspended from committing.
252 | earliestSuspendedTime: ExpirationTime,
253 | latestSuspendedTime: ExpirationTime,
254 | // The earliest and latest priority levels that are not known to be suspended.
255 | earliestPendingTime: ExpirationTime,
256 | latestPendingTime: ExpirationTime,
257 | // The latest priority level that was pinged by a resolved promise and can
258 | // be retried.
259 | latestPingedTime: ExpirationTime,
260 |
261 | // If an error is thrown, and there are no more updates in the queue, we try
262 | // rendering from the root one more time, synchronously, before handling
263 | // the error.
264 | didError: boolean,
265 |
266 | pendingCommitExpirationTime: ExpirationTime,
267 | // A finished work-in-progress HostRoot that's ready to be committed.
268 | finishedWork: Fiber | null,
269 | // Timeout handle returned by setTimeout. Used to cancel a pending timeout, if
270 | // it's superseded by a new one.
271 | timeoutHandle: TimeoutHandle | NoTimeout,
272 | // Top context object, used by renderSubtreeIntoContainer
273 | context: Object | null,
274 | pendingContext: Object | null,
275 | // Determines if we should attempt to hydrate on the initial mount
276 | +hydrate: boolean,
277 | // Remaining expiration time on this root.
278 | // TODO: Lift this into the renderer
279 | nextExpirationTimeToWorkOn: ExpirationTime,
280 | expirationTime: ExpirationTime,
281 | // List of top-level batches. This list indicates whether a commit should be
282 | // deferred. Also contains completion callbacks.
283 | // TODO: Lift this into the renderer
284 | firstBatch: Batch | null,
285 | // Linked-list of roots
286 | nextScheduledRoot: FiberRoot | null,
287 | |};
288 | ```
289 | 我会选择性地介绍一些属性:
290 |
291 | ### containerInfo
292 |
293 | 保存的是根 DOM 节点, 即传入 CustomDOM.render 的第二个参数 container
294 |
295 | ### current
296 |
297 | 代表的是这个 root 对应的 fiber
298 |
299 | ### earliestSuspendedTime, latestSuspendedTime, earliestPendingTime, latestPendingTime 和 latestPingedTime
300 |
301 | * earliestSuspendedTime, latestSuspendedTime 代表了组件抛出的 promise 还没有 resolve 时,被暂停的发生时间最先和最迟的工作。
302 | * latestPingedTime 代表了组件抛出的 promise 已经 resolve 时,等待被 commit 的工作
303 | * earliestPendingTime, latestPendingTime 代表了普通未被 commit 的最先和最迟的工作
304 |
305 | 与 suspense 相关的论述可以看 suspense 章节。
306 |
307 | 需要这些优先级的区分,是因为一个 root 中可以同时有多个需要被完成的工作,这些工作的完成期限可能各不相同。比如说,组件抛出了 promise, 还未被 resolve, 这时候 root 下的另外一个组件又被触发了状态更新;或者高优先级的工作打断了低优先级的工作。
308 |
309 | 为了简化,我会忽略这些属性。我假设**在无论什么情况下,root 下最多存在一个待完成的工作**。这意味着,一旦我们触发了状态更新,直到这个更新被完成了,不会有新的更新被触发。所以不需要考虑低优先级被高优先级的工作打断之后如何恢复的问题。实际上 React 直到现在(2018.10.1)还没有完全实现 resume。 最新的进展可以关注官方 [Releasing Time Slicing](https://github.com/facebook/react/issues/13306)。
310 |
311 | ### pendingCommitExpirationTime 和 finishedWork
312 |
313 | 在完成了 render 阶段的工作后, 在被 commit 之前,pendingCommitExpirationTime 和 finishedWork 都会被更新,finishedWork 引用的是即将被 commit 的类型为 HostRoot 的 fiber。我会忽略 pendingCommitExpirationTime。
314 |
315 | ### expirationTime
316 |
317 | 注意 fiber 也有 expirationTime 属性。root 的 expirationTime 属性保存的是这个 root 下还未 commit 的 fiber 的 expirationTime。
318 |
319 | 由于我将不考虑 context ,batch 以及 error, 剩余的属性我都会忽略,最后简化的 FiberRoot 的构造函数:
320 | ```javascript
321 | function createFiberRoot (containerInfo) {
322 | let uninitializedFiber = createHostRootFiber()
323 | let root = {
324 | // The currently active root fiber. This is the mutable root of the tree.
325 | current: uninitializedFiber,
326 | // Any additional information from the host associated with this root.
327 | containerInfo: containerInfo,
328 | // A finished work-in-progress HostRoot that's ready to be committed.
329 | finishedWork: null,
330 | expirationTime: NoWork
331 | }
332 | uninitializedFiber.stateNode = root
333 | return root
334 | }
335 | ```
336 | 注意 fiberRoot 的 current 所指的 fiber, 其 stateNode 属性又指向该 root 本身,这是一个环状结构。
337 |
338 | ## ExpirationTime 和 UpdateQueue
339 |
340 | 上面介绍 fiber 的时候已经提到了这两个属性,实际上 react 有两个单独的文件 [ReactFiberExpirationTime.js](https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberExpirationTime.js) 和 [ReactUpdateQueue.js](https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactUpdateQueue.js) 定义它们。
341 | 作为基础,我觉得有必要先介绍一下它们。
342 |
343 | React 用 expirationTime 代表一个未来的时间点,更新需要在这个时间点之前完成。一个更新任务的优先级就是其 expirationTime 和当前时间的差值,差值越小,代表优先级越高。这样随着时间的流逝,一个更新的优先级会越来越高,这样就可以避免 starvation, 即低优先级的工作一直被高优先级的工作打断,而无法完成。
344 |
345 | 我会直接引用 ReactFiberExpirationTime.js,因为实际上理解它并不难,我只简单地介绍一下其中的两个函数。
346 | computeAsyncExpiration 和 computeInteractiveExpiration 这两个方程都只接受一个参数,现在的时间,返回一个未来的期限。computeAsyncExpiration 会得到更长的期限,对应的是普通的异步任务。computeInteractiveExpiration 会得到相对更短的期限,意味着要更快完成,对应的是用户交互产生的任务,比如说用户的点击事件。
347 |
348 | 对于 ReactUpdateQueue.js,我做了很多简化。我建议你去看看源码,里面有不错的注释,能帮你加深对 Fiber 的理解。
349 |
350 | 简化之后的 ReactUpdateQueue.js:
351 |
352 | ```javascript
353 | import {NoWork} from './ReactFiberExpirationTime'
354 | // Assume when processing the updateQueue, process all updates together
355 | class UpdateQueue {
356 | constructor (baseState) {
357 | this.baseState = baseState
358 | this.firstUpdate = null
359 | this.lastUpdate = null
360 | }
361 | }
362 |
363 | class Update {
364 | constructor () {
365 | this.payload = null
366 | this.next = null
367 | }
368 | }
369 |
370 | export function createUpdate () {
371 | return new Update()
372 | }
373 |
374 | function appendUpdateToQueue (queue, update) {
375 | // Append the update to the end of the list.
376 | if (queue.lastUpdate === null) {
377 | // Queue is empty
378 | queue.firstUpdate = queue.lastUpdate = update
379 | } else {
380 | queue.lastUpdate.next = update
381 | queue.lastUpdate = update
382 | }
383 | }
384 |
385 | export function enqueueUpdate (fiber, update) {
386 | // Update queues are created lazily.
387 | let queue = fiber.updateQueue
388 | if (queue === null) {
389 | queue = fiber.updateQueue = new UpdateQueue(fiber.memoizedState)
390 | }
391 | appendUpdateToQueue(queue, update)
392 | }
393 |
394 | function getStateFromUpdate (update, prevState) {
395 | const partialState = update.payload
396 | if (partialState === null || partialState === undefined) {
397 | // Null and undefined are treated as no-ops.
398 | return prevState
399 | }
400 | // Merge the partial state and the previous state.
401 | return Object.assign({}, prevState, partialState)
402 | }
403 |
404 | export function processUpdateQueue (workInProgress, queue) {
405 | // Iterate through the list of updates to compute the result.
406 | let update = queue.firstUpdate
407 | let resultState = queue.baseState
408 | while (update !== null) {
409 | resultState = getStateFromUpdate(update, resultState)
410 | update = update.next
411 | }
412 | queue.baseState = resultState
413 | queue.firstUpdate = queue.lastUpdate = null
414 | workInProgress.expirationTime = NoWork
415 | workInProgress.memoizedState = resultState
416 | }
417 |
418 | ```
419 | ### UpdateQueue 和 Update
420 |
421 | * 一个 UpdateQueue 实例有三个属性,baseState 表示现在的初始状态,firstUpdate 和 lastUpdate 分别指向第一个更新和最后一个更新。
422 | * 一个 Update 实例的 payload 表示更新的内容,next 指向下一个更新。
423 |
424 | ### createUpdate, enqueueUpdate 和 processUpdateQueue
425 |
426 | * createUpdate 生成一个更新。
427 | * enqueueUpdate 将更新加入 fiber 的 updateQueue。
428 | * processUpdateQueue 处理 fiber 的 updateQueue,将 updateQueue 的 baseState 更新为最终状态,清空 firstUpdate 和 lastUpdate,把最终状态保存到 fiber 的 memoizedState 属性中。
429 |
430 | [下一章](Fiber_part2.md)
431 |
--------------------------------------------------------------------------------
/Guide/Suspense.md:
--------------------------------------------------------------------------------
1 | # Suspense
2 | >本章的代码在Suspense分支中
3 |
4 | ## 学习资料
5 |
6 | [fresh-async-react](https://github.com/sw-yx/fresh-async-react)
7 |
8 | 有很多 Suspense 部分的资料。
9 |
10 | [kentcdodds/react-suspense-simple-example ](https://www.youtube.com/watch?v=7LmrS2sdMlo&feature=youtu.be&a=)
11 |
12 | 如何写一个简单的 suspense cache。
13 |
14 | 注意 [React 16.6 canary release](https://github.com/facebook/react/pull/13799) 中激活了 Suspense。而且将 Placeholder 重新命名为 Suspense。
15 |
16 | ## 原理
17 |
18 | [@acdlite on how Suspense works](https://twitter.com/acdlite/status/969171217356746752)
19 |
20 | * 在 render 函数中,从缓存中读取数据。
21 | * 如果数据已经被缓存,继续正常的渲染。
22 | * 如果数据没有被缓存,意味着可能需要向服务器发起请求,这时候就会抛出一个 promise。React 会捕获这个 promise 且暂停渲染。
23 | * 当这个 promise resolves,React 会重新开始渲染。
24 |
25 | ## 演示
26 |
27 | ```javascript
28 | import React, { unstable_Suspense as Suspense } from 'react'
29 | import { cache } from './cache'
30 | import { createResource } from 'react-cache'
31 |
32 | const sleep = (time, resolvedValue) =>
33 | new Promise(resolve => {
34 | setTimeout(() => resolve(resolvedValue), time)
35 | })
36 | const myResource = createResource(id => sleep(3000, id))
37 |
38 | class Foo extends React.Component {
39 | render () {
40 | const value = myResource.read(cache, 'foo')
41 | return (
42 | {value}
43 | )
44 | }
45 | }
46 | class App extends React.Component {
47 | render () {
48 | return (
49 | 'Loading....'}>
50 |
51 |
52 | )
53 | }
54 | }
55 |
56 | export default App
57 | ```
58 | Suspense 组件会捕获其下的子组件抛出的 promise, 然后决定渲染什么。 当 promise 还没有 resolves 时,将渲染 fallback,当 promise resolves 时,再渲染 Foo 组件。maxDuration 表示等待多长的时间再渲染 fallback,因为如果数据获取足够迅速,并没有必要渲染 fallback。
59 |
60 | 
61 |
62 | 可以看到点击刷新,等待 1 秒之后渲染 fallback,再等待 2 秒之后渲染 Foo 组件。
63 |
64 | ## 实现
65 |
66 | 实现 Suspense 将分为两个部分,第一个部分实现 suspense cache,第二个部分完善 fiber 架构来支持 suspense。
67 |
68 | 注意不会实现 maxDuration,意味着 fallback 必然会被渲染。
69 |
70 | ### suspense cache
71 |
72 | 新建 ReactCache.js
73 |
74 | ```javascript
75 | export function createCache () {
76 | const resourceMap = new Map()
77 | const cache = {
78 | read (resourceType, key, loadResource) {
79 | let recordCache = resourceMap.get(resourceType)
80 | if (recordCache === undefined) {
81 | // 为每个 resource 创建单独的 recordCache
82 | recordCache = new Map()
83 | resourceMap.set(resourceType, recordCache)
84 | }
85 | let record = recordCache.get(key)
86 | if (record === undefined) {
87 | // 记录不存在
88 | const suspender = loadResource(key)
89 | suspender.then(value => {
90 | // 将数据保存在 recordCache 中
91 | recordCache.set(key, value)
92 | return value
93 | })
94 | // 抛出 promise
95 | throw suspender
96 | }
97 | // 直接返回记录
98 | return record
99 | }
100 | }
101 | return cache
102 | }
103 |
104 | export function createResource (loadResource) {
105 | const resource = {
106 | read (cache, key) {
107 | return cache.read(resource, key, loadResource)
108 | }
109 | }
110 | return resource
111 | }
112 | ```
113 |
114 | * 调用 createCache,会返回一个 cache。cache 提供了一个 read 方法,会为每一个 resource 创建一个 recordCache,然后尝试从 recordCache 中读取数据。如果数据存在,直接返回该数据。否则,抛出一个 promise。
115 | 当 promise resolves 时,会将数据保存在 recordCache 中。
116 | * 调用 createResource 返回的 resource 也将提供一个 read 方法,调用 resource.read 其实就是在调用 cache.read。
117 | * React 实现的 cache 还将提供 prelaod 方法。[@sebmarkbage on when to use preload() vs read()](https://twitter.com/sebmarkbage/status/1026514420908744704)。
118 |
119 | ### 完善 fiber 架构
120 |
121 | 1. 需要引入 React.Suspense。
122 |
123 | 修改 react 文件加下的 index.js.
124 |
125 | ```javascript
126 | const React = {
127 | Component,
128 | createElement,
129 | // add React.Suspense
130 | Suspense: Symbol.for('react.suspense')
131 | }
132 | ```
133 |
134 | 现在我们可以 import { Suspense } from React。
135 |
136 | 因为新增了 Suspense 组件,我们需要新增一个 workTag。修改 Reconciler.js。
137 |
138 | ```javascript
139 | import {
140 | ClassComponent,
141 | HostRoot,
142 | HostComponent,
143 | // suspense workTag
144 | SuspenseComponent
145 | } from '../shared/ReactWorkTags'
146 | ```
147 |
148 | 2. JSX 中的 实际上会被 Babel 调用 createElement 从而生成一个 ReactElement 对象。这个对象的 type 属性是 Symbol(react.suspense)。考虑到我们是通过 createFiberFromElement 函数把 ReactElement 对象转化为 fiber,所以需要修改 createFiberFromElement 函数。
149 |
150 | ```javascript
151 | function createFiberFromElement (element, expirationTime) {
152 | let fiber
153 | const type = element.type
154 | const pendingProps = element.props
155 | let fiberTag
156 | if (typeof type === 'function') {
157 | fiberTag = ClassComponent
158 | } else if (typeof type === 'string') {
159 | fiberTag = HostComponent
160 | } else {
161 | // type is Symbol(react.suspense)
162 | fiberTag = SuspenseComponent
163 | }
164 | fiber = new FiberNode(fiberTag, pendingProps)
165 | fiber.type = type
166 | fiber.expirationTime = expirationTime
167 | return fiber
168 | }
169 | ```
170 |
171 | 3. 现在有了 tag 为 SuspenseComponent 的 fiber,所有包含 switch(workInProgress.tag) 的函数都需要修改。
172 |
173 | ```javascript
174 | function beginWork (current, workInProgress, renderExpirationTime) {
175 | workInProgress.expirationTime = NoWork
176 | const Component = workInProgress.type
177 | const unresolvedProps = workInProgress.pendingProps
178 | switch (workInProgress.tag) {
179 | case ClassComponent: {
180 | return updateClassComponent(current, workInProgress, Component, unresolvedProps, renderExpirationTime)
181 | }
182 | case HostRoot: {
183 | return updateHostRoot(current, workInProgress, renderExpirationTime)
184 | }
185 | case HostComponent: {
186 | return updateHostComponent(current, workInProgress, renderExpirationTime)
187 | }
188 | // new case
189 | case SuspenseComponent: {
190 | return updateSuspenseComponent(current, workInProgress, renderExpirationTime)
191 | }
192 | default:
193 | throw new Error('unknown unit of work tag')
194 | }
195 | }
196 |
197 | function completeWork (current, workInProgress) {
198 | const newProps = workInProgress.pendingProps
199 | switch(workInProgress.tag) {
200 | case ClassComponent: {
201 | break
202 | }
203 | case HostRoot: {
204 | break
205 | }
206 | case HostComponent: {
207 | const type = workInProgress.type
208 | if (current !== null && workInProgress.stateNode != null) {
209 | const oldProps = current.memoizedProps
210 | const updatePayload = prepareUpdate(oldProps, newProps)
211 | workInProgress.updateQueue = updatePayload
212 | if (updatePayload) {
213 | markUpdate(workInProgress)
214 | }
215 | } else {
216 | const _instance = createInstance(type, newProps, workInProgress)
217 | appendAllChildren(_instance, workInProgress)
218 | finalizeInitialChildren(_instance, newProps)
219 | workInProgress.stateNode = _instance
220 | }
221 | break
222 | }
223 | // new case
224 | case SuspenseComponent: {
225 | break
226 | }
227 | default: {
228 | throw new Error('Unknown unit of work tag')
229 | }
230 | }
231 | return null
232 | }
233 |
234 | function commitWork (finishedWork) {
235 | switch (finishedWork.tag) {
236 | case HostRoot:
237 | case ClassComponent: {
238 | return
239 | }
240 | case HostComponent: {
241 | const instance = finishedWork.stateNode
242 | if (instance != null) {
243 | const updatePayload = finishedWork.updateQueue
244 | finishedWork.updateQueue = null
245 | if (updatePayload !== null) {
246 | commitUpdate(instance, updatePayload)
247 | }
248 | }
249 | return
250 | }
251 | // new case
252 | case SuspenseComponent: {
253 | return
254 | }
255 | default: {
256 | throw new Error('This unit of work tag should not have side-effects')
257 | }
258 | }
259 | }
260 | ```
261 |
262 | 4. 在 beginWork 中新增了 updateSuspenseComponent 函数。
263 |
264 | ```javascript
265 | import { DidCapture } from '../shared/ReactSideEffectTags'
266 |
267 | function updateSuspenseComponent (current, workInProgress, renderExpirationTime) {
268 | const nextProps = workInProgress.pendingProps
269 | const nextDidTimeout = (workInProgress.effectTag & DidCapture) !== NoEffect
270 | const nextChildren = nextDidTimeout ? nextProps.fallback : nextProps.children
271 | workInProgress.memoizedProps = nextProps
272 | workInProgress.memoizedState = nextDidTimeout
273 | reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime)
274 | return workInProgress.child
275 | }
276 | ```
277 |
278 | * 通过 DidCapture 标签判断渲染 fallback 还是 children。
279 |
280 | 5. 当 mount 阶段调用 updateSuspenseComponent 时,会尝试渲染 children。在上面提到的例子中,会渲染 Foo组件,继而调用 Foo 组件的 render 函数,这时候就会抛出一个 promise。所以需要在某个地方 catch 这个 promise。
281 |
282 | ```javascript
283 | function renderRoot (root, isYieldy) {
284 | isWorking = true
285 | const expirationTime = root.expirationTime
286 | if (expirationTime !== nextRenderExpirationTime || nextUnitOfWork === null) {
287 | nextRenderExpirationTime = expirationTime
288 | nextUnitOfWork = createWorkInProgress(root.current, null, nextRenderExpirationTime)
289 | }
290 | do {
291 | try {
292 | workLoop(isYieldy)
293 | } catch (thrownValue) {
294 | const sourceFiber = nextUnitOfWork
295 | const returnFiber = sourceFiber.return
296 | throwException(root, returnFiber, sourceFiber, thrownValue, nextRenderExpirationTime)
297 | nextUnitOfWork = completeUnitOfWork(sourceFiber)
298 | continue
299 | }
300 | break
301 | } while (true)
302 | isWorking = false
303 | if (nextUnitOfWork !== null) {
304 | return
305 | }
306 | root.finishedWork = root.current.alternate
307 | }
308 |
309 | function throwException(root, returnFiber, sourceFiber, value, renderExpirationTime) {
310 | // The source fiber did not complete.
311 | sourceFiber.effectTag |= Incomplete
312 | // Its effect list is no longer valid.
313 | sourceFiber.firstEffect = sourceFiber.lastEffect = null
314 | if (
315 | value !== null &&
316 | typeof value === 'object' &&
317 | typeof value.then === 'function'
318 | ) {
319 | // This is a thenable.
320 | const thenable = value
321 | // Schedule the nearest Placeholder to re-render the timed out view
322 | let workInProgress = returnFiber
323 | do {
324 | if (workInProgress.tag === SuspenseComponent) {
325 | // Found the nearest boundary.
326 | // Attach a listener to the promise to "ping" the root and retry
327 | const onResolve = retrySuspendedRoot.bind(
328 | null,
329 | root,
330 | workInProgress
331 | )
332 | thenable.then(onResolve)
333 | workInProgress.expirationTime = renderExpirationTime
334 | return
335 | }
336 | workInProgress = workInProgress.return
337 | } while (workInProgress !== null)
338 | }
339 | }
340 |
341 | function retrySuspendedRoot (root, fiber) {
342 | const currentTime = requestCurrentTime()
343 | const retryTime = computeExpirationForFiber(currentTime)
344 | root.expirationTime = retryTime
345 | scheduleWorkToRoot(fiber, retryTime)
346 | requestWork(root, root.expirationTime)
347 | }
348 | ```
349 |
350 | **renderRoot**
351 |
352 | * 在 renderRoot 中,将 workLoop 放在 try 语句块中,如果抛出 promise ,将 promise 传入 catch 语句块。
353 | * 如果有 promise 被抛出,执行完 throwException 和 completeUnitOfWork 后,将再次执行 workLoop。
354 |
355 | **throwException**
356 |
357 | * 传入 throwException 的 sourceFiber 参数是抛出 promise 的 fiber,将其标记上 Incomplete 标签,清空它的 effect list。
358 | * 传入 throwException 的 value 参数即是抛出的 promise。在 promise 上绑定一个 resolves 时执行的函数 retrySuspendedRoot。
359 |
360 |
361 | 6. 由于在 throwException 中标记了 Incomplete,代表这个 fiber 还没有完成。所以需要完善 completeUnitOfWork。
362 |
363 | ```javascript
364 | function completeUnitOfWork (workInProgress) {
365 | while (true) {
366 | const current = workInProgress.alternate
367 | const returnFiber = workInProgress.return
368 | const siblingFiber = workInProgress.sibling
369 | if ((workInProgress.effectTag & Incomplete) === NoEffect) {
370 | // This fiber completed.
371 | completeWork(current, workInProgress)
372 | if (returnFiber !== null &&
373 | (returnFiber.effectTag & Incomplete) === NoEffect) {
374 | if (returnFiber.firstEffect === null) {
375 | returnFiber.firstEffect = workInProgress.firstEffect
376 | }
377 | if (workInProgress.lastEffect !== null) {
378 | if (returnFiber.lastEffect !== null) {
379 | returnFiber.lastEffect.nextEffect = workInProgress.firstEffect
380 | }
381 | returnFiber.lastEffect = workInProgress.lastEffect
382 | }
383 | const effectTag = workInProgress.effectTag
384 | if (effectTag >= Placement) {
385 | if (returnFiber.lastEffect !== null) {
386 | returnFiber.lastEffect.nextEffect = workInProgress
387 | } else {
388 | returnFiber.firstEffect = workInProgress
389 | }
390 | returnFiber.lastEffect = workInProgress
391 | }
392 | }
393 | if (siblingFiber !== null) {
394 | return siblingFiber
395 | } else if (returnFiber !== null) {
396 | workInProgress = returnFiber
397 | continue
398 | } else {
399 | return null
400 | }
401 | } else {
402 | // This fiber did not complete because something threw.
403 | if (workInProgress.tag === SuspenseComponent) {
404 | const effectTag = workInProgress.effectTag
405 | workInProgress.effectTag = effectTag & ~Incomplete | DidCapture
406 | return workInProgress
407 | }
408 |
409 | if (returnFiber !== null) {
410 | // Mark the parent fiber as incomplete and clear its effect list.
411 | returnFiber.firstEffect = returnFiber.lastEffect = null
412 | returnFiber.effectTag |= Incomplete
413 | }
414 | if (siblingFiber !== null) {
415 | // If there is more work to do in this returnFiber, do that next.
416 | return siblingFiber
417 | } else if (returnFiber !== null) {
418 | // If there's no more work in this returnFiber. Complete the returnFiber.
419 | workInProgress = returnFiber
420 | continue
421 | } else {
422 | return null
423 | }
424 | }
425 | }
426 | }
427 | ```
428 |
429 | **completeUnitOfWork**
430 |
431 | * 当抛出 promise 的 fiber 没有完成时,会将这个 fiber 的 returnFiber 标记为 Incomplete。
432 | * 所以当 complete 这个 returnFiber 时,会再次到未完成的分支。
433 | * 直到当 complete 的 fiber 是 SuspenseComponent 时,会将其 Incomplete 标签去掉并标记上 DidCapture 标签。注意这时候直接返回 SuspenseComponent fiber。
434 | * 这时候再执行 workLoop,在 updateSuspenseComponent 中就会决定渲染 fallback。
435 |
436 | 现在 Suspense 的整体流程已经很清楚了。以上面的简单组件为例,触发的函数流程图:
437 |
438 | 
439 |
440 | 7. 由于例子包含了从 <div>Loading...</div> 更新为 <Foo /> 组件的情况,所以需要先删除 <div>Loading...</div>,再插入 <Foo /> 组件。因此就必须引入删除的逻辑。
441 |
442 | ```javascript
443 | // in CustomDom.js
444 | const hostConfig = {
445 | // ...
446 | removeChildFromContainer: (container, child) => {
447 | container.removeChild(child)
448 | }
449 | }
450 |
451 | // in Reconciler.js
452 | function reconcileChildrenArray (returnFiber, currentFirstChild, newChildren, expirationTime) {
453 | let resultingFirstChild = null
454 | let previousNewFiber = null
455 | let oldFiber = currentFirstChild
456 | let newIdx = 0
457 | for (; oldFiber !== null && newIdx < newChildren.length; newIdx ++) {
458 | let newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], expirationTime)
459 | if (shouldTrackSideEffects) {
460 | if (oldFiber && newFiber.alternate === null) {
461 | // We matched the slot, but we didn't reuse the existing fiber, so we
462 | // need to delete the existing child.
463 | deleteChild(returnFiber, oldFiber)
464 | newFiber.effectTag = Placement
465 | }
466 | }
467 | if (resultingFirstChild === null) {
468 | resultingFirstChild = newFiber
469 | } else {
470 | previousNewFiber.sibling = newFiber
471 | }
472 | previousNewFiber = newFiber
473 | oldFiber = oldFiber.sibling
474 | }
475 |
476 | if (oldFiber === null) {
477 | for (; newIdx < newChildren.length; newIdx++) {
478 | let _newFiber = createChild(returnFiber, newChildren[newIdx], expirationTime)
479 | if (shouldTrackSideEffects && _newFiber.alternate === null) {
480 | _newFiber.effectTag = Placement
481 | }
482 | if (resultingFirstChild === null) {
483 | resultingFirstChild = _newFiber
484 | } else {
485 | previousNewFiber.sibling = _newFiber
486 | }
487 | previousNewFiber = _newFiber
488 | }
489 | return resultingFirstChild
490 | }
491 | }
492 |
493 | function deleteChild (returnFiber, childToDelete) {
494 | // Deletions are added in reversed order so we add it to the front.
495 | // At this point, the return fiber's effect list is empty except for
496 | // deletions, so we can just append the deletion to the list. The remaining
497 | // effects aren't added until the complete phase. Once we implement
498 | // resuming, this may not be true.
499 | const last = returnFiber.lastEffect
500 | if (last !== null) {
501 | last.nextEffect = childToDelete
502 | returnFiber.lastEffect = childToDelete
503 | } else {
504 | returnFiber.firstEffect = returnFiber.lastEffect = childToDelete
505 | }
506 | childToDelete.nextEffect = null
507 | childToDelete.effectTag = Deletion
508 | }
509 |
510 | function updateElement (returnFiber, current, element, expirationTime) {
511 | if (current !== null && current.type === element.type) {
512 | // Update
513 | const existing = useFiber(current, element.props, expirationTime)
514 | existing.return = returnFiber
515 | return existing
516 | } else {
517 | // Insert
518 | const created = createFiberFromElement(element, expirationTime)
519 | created.return = returnFiber
520 | return created
521 | }
522 | }
523 |
524 | function commitAllHostEffects (firstEffect) {
525 | let nextEffect = firstEffect
526 | while (nextEffect !== null) {
527 | const effectTag = nextEffect.effectTag
528 | switch(effectTag & (Placement | Update | Deletion)) {
529 | case Placement: {
530 | commitPlacement(nextEffect)
531 | nextEffect.effectTag &= ~Placement
532 | break
533 | }
534 | case Update: {
535 | commitWork(nextEffect)
536 | break
537 | }
538 | case Deletion: {
539 | commitDeletion(nextEffect)
540 | break
541 | }
542 | }
543 | nextEffect = nextEffect.nextEffect
544 | }
545 | }
546 |
547 | function commitDeletion (current) {
548 | // Recursively delete all host nodes from the parent.
549 | // Detach refs and call componentWillUnmount() on the whole subtree.
550 | const parentFiber = getHostParentFiber(current)
551 | // We only have the top Fiber that was deleted but we need recurse down its
552 | // children to find all the terminal nodes.
553 | const parent = parentFiber.tag === HostRoot ? parentFiber.stateNode.containerInfo : parentFiber.stateNode
554 | let node = current
555 | while (true) {
556 | if (node.tag === HostComponent) {
557 | removeChildFromContainer(parent, node.stateNode)
558 | } else if (node.child !== null) {
559 | node.child.return = node
560 | node = node.child
561 | continue
562 | }
563 | if (node === current) {
564 | break
565 | }
566 | while (node.sibling === null) {
567 | if (node.return === null || node.return === current) {
568 | break
569 | }
570 | node = node.return
571 | }
572 | node.sibling.return = node.return
573 | node = node.sibling
574 | }
575 | // Cut off the return pointers to disconnect it from the tree. Ideally, we
576 | // should clear the child pointer of the parent alternate to let this
577 | // get GC:ed but we don't know which for sure which parent is the current
578 | // one so we'll settle for GC:ing the subtree of this child. This child
579 | // itself will be GC:ed when the parent updates the next time.
580 | current.return = null
581 | current.child = null
582 | if (current.alternate) {
583 | current.alternate.child = null
584 | current.alternate.return = null
585 | }
586 | }
587 | ```
588 |
589 | 修改 App.js
590 |
591 | ```javascript
592 | import React from './react'
593 | import { createCache, createResource } from './cache/ReactCache'
594 |
595 | const cache = createCache()
596 | const sleep = (time, resolvedValue) =>
597 | new Promise(resolve => {
598 | setTimeout(() => resolve(resolvedValue), time)
599 | })
600 | const myResource = createResource(id => sleep(3000, id))
601 |
602 | class Foo extends React.Component {
603 | render () {
604 | const value = myResource.read(cache, 'foo')
605 | return (
606 | {value}
607 | )
608 | }
609 | }
610 |
611 | class App extends React.Component {
612 | render () {
613 | return (
614 | Loading....}>
615 |
616 |
617 | )
618 | }
619 | }
620 |
621 | export default App
622 | ```
623 |
624 | 现在运行项目:
625 |
626 | 
627 |
628 | [下一章](LifeCycles.md)
--------------------------------------------------------------------------------
/Guide/Fiber_part2.md:
--------------------------------------------------------------------------------
1 | # Fiber 架构
2 | >本章的代码在Fiber分支中
3 |
4 | 在 CustomRenderer 章节,我们定义了一个简单组件,通过初次渲染和点击按钮,可以调试 React 的 mount 和 update 阶段。在 ReactReconciler 中的函数入口加上 console.log, 可以得到其触发函数的流程图。
5 |
6 | 
7 |
8 | 注意这不是完整的 React 内部被触发的函数流程图。我已经省略了许多与错误处理等功能相关的函数,并且没有把我决定留下的函数都放在图中,这是为了尽可能不让它看起来过于复杂。
9 |
10 | 我会按函数的触发先后顺序来介绍它们。并且我把这整个流程图大致分为三个阶段:schedule 阶段,render 阶段 和 commit 阶段。这样划分只是为了方便陈述,如果你看了 [Lin Clark's A Cartoon Intro to Fiber](https://www.youtube.com/watch?v=ZCuYPiUIONs),里面只有 render 和 commit 的划分。
11 |
12 | 在 src 文件夹下新建一个文件夹 reconciler, 与 reconciler 模块相关的代码都将放在其中。将上一章提到的 ReactFiber.js,ReactFiberRoot.js,ReactFiberExpirationTIme.js 和 ReactUpdateQueue.js 复制到里面,并新建一个文件 Reconciler.js.
13 |
14 | Reconciler.js 的代码结构:
15 | ```javascript
16 | // do some import
17 |
18 | function Reconciler (hostConfig) {
19 |
20 | const now = hostConfig.now
21 | const shouldSetTextContent = hostConfig.shouldSetTextContent
22 | const createInstance = hostConfig.createInstance
23 | const finalizeInitialChildren = hostConfig.finalizeInitialChildren
24 | const appendInitialChild = hostConfig.appendInitialChild
25 | const scheduleDeferredCallback = hostConfig.scheduleDeferredCallback
26 | const prepareUpdate = hostConfig.prepareUpdate
27 | const appendChildToContainer = hostConfig.appendChildToContainer
28 | const commitUpdate = hostConfig.commitUpdate
29 |
30 | // Variables
31 |
32 | // functions
33 |
34 | return {
35 | createContainer,
36 | updateContainer
37 | }
38 | }
39 |
40 | export default Reconciler
41 | ```
42 |
43 | 在 CustomDom.js 中原来 import 的是 'react-reconciler', 现在改为 import ReactReconciler from './reconciler/Reconciler'。让我们一步一步完善 Reconciler.js。
44 |
45 | ## schedule 阶段
46 | ```javascript
47 | // in index.js
48 | CustomDom.render(, document.getElementById('root'))
49 |
50 | // in CustomDom.js
51 | const CustomDom = {
52 | render: (reactElement, container) => {
53 | let root = container._reactRootContainer
54 | if (!root) {
55 | root = container._reactRootContainer = customRenderer.createContainer(container)
56 | }
57 | customRenderer.updateContainer(reactElement, root)
58 | }
59 | }
60 |
61 | // in Reconciler.js
62 | let scheduledRoot = null
63 | let isRendering = false
64 | let isWorking = false
65 | let isCommitting = false
66 | let originalStartTimeMs = now()
67 | let currentRendererTime = msToExpirationTime(originalStartTimeMs)
68 | let currentSchedulerTime = currentRendererTime
69 | let nextRenderExpirationTime = NoWork
70 | let isBatchingInteractiveUpdates = false
71 |
72 | function createContainer (containerInfo) {
73 | return createFiberRoot(containerInfo)
74 | }
75 |
76 | function updateContainer (element, container) {
77 | const current = container.current
78 | const currentTime = requestCurrentTime()
79 | const expirationTime = computeExpirationForFiber(currentTime)
80 | return scheduleRootUpdate(current, element, expirationTime)
81 | }
82 |
83 | function requestCurrentTime() {
84 | if (isRendering) {
85 | return currentSchedulerTime
86 | }
87 | if (!scheduledRoot) {
88 | recomputeCurrentRendererTime()
89 | currentSchedulerTime = currentRendererTime
90 | return currentSchedulerTime
91 | }
92 | return currentSchedulerTime
93 | }
94 |
95 | function recomputeCurrentRendererTime () {
96 | let currentTimeMs = now() - originalStartTimeMs
97 | currentRendererTime = msToExpirationTime(currentTimeMs)
98 | }
99 |
100 | function computeExpirationForFiber (currentTime) {
101 | let expirationTime
102 | if (isWorking) {
103 | if (isCommitting) {
104 | expirationTime = Sync
105 | } else {
106 | expirationTime = nextRenderExpirationTime
107 | }
108 | } else {
109 | if (isBatchingInteractiveUpdates) {
110 | expirationTime = computeInteractiveExpiration(currentTime)
111 | } else {
112 | expirationTime = computeAsyncExpiration(currentTime)
113 | }
114 | }
115 | return expirationTime
116 | }
117 | ```
118 |
119 | 当我们初次调用 CustomDom.render 时,首先会调用 createContainer 创建一个 fiberRoot,然后调用 updateContainer 来渲染组件。注意传入 createContainer 的参数 container 就是我们希望组件渲染在其之下的 DOM 节点。传入 updateContainer 的第一个参数就是<App /> 对应的 Elmenet 对象。Elmenet 对象将在 ReactCore 章节介绍。
120 |
121 | 在 updateContainer 中, 会调用 requestCurrentTime 得到现在的相对时间,然后调用 computeExpirationForFiber 计算这个 fiber 预期被完成的期限, 最后调用 scheduleRootUpdate。这里有几个全局变量:
122 |
123 | * isRendering:注意这个变量的名字有点混淆,它并不代表 render 阶段,而代表了 React 正在渲染。渲染的阶段包括了 render 和 commit 阶段。真正的 render 阶段并没有相应的变量来指示,而是通过 isWorking && !isCommitting 来判断。
124 | * isWorking:表示 fiber 是否在 render 或者 commit 阶段
125 | * isCommitting: 表示 fiber 是否在 commit 阶段
126 | * scheduledRoot:表示是否存在待完成的工作
127 | * originalStartTimeMs:表示最初的时间起点
128 | * currentRendererTime 和 currentSchedulerTime:请看 [Always batch updates of like priority within the same event (#13071)](https://github.com/facebook/react/pull/13071)
129 | * nextRenderExpirationTime: 表示目前在 render 阶段的 fiber 的 expirationTime
130 | * isBatchingInteractiveUpdates:表示是否是在 batch 用户交互触发的更新(与事件处理相关,现在可以认为它的值一直为 false)
131 |
132 | ### requestCurrentTime
133 |
134 | requestCurrentTime 函数有三种情况:
135 |
136 | * 如果现在 React 正在渲染,将返回现在的 currentSchedulerTime,对应的情况:在生命周期函数中调用 this.setState。
137 | * 如果 React 不在渲染且没有待完成的工作,将重新计算现在的相对时间。
138 | * 如果有待完成的工作,将返回现在的 currentSchedulerTime,对应的情况:在一个事件触发回调函数中调用多个 this.setState,所有的更新都将拥有相同的 currentTime。
139 |
140 | ### computeExpirationForFiber
141 |
142 | computeExpirationForFiber 函数有四种情况:
143 |
144 | * 如果现在在 commit 阶段,返回 Sync(同步),对应的情况:在 commit 阶段的生命周期函数中调用 this.setState。
145 | * 如果现在在 render 阶段,返回目前在 render 阶段的 fiber 的 expirationTime。
146 | * 如果本次更新是用户交互触发的更新,调用 computeInteractiveExpiration 计算相应的到期时间
147 | * 如果本次更新是普通的异步更新,调用 computeAsyncExpiration 计算相应的到期时间
148 |
149 | ```javascript
150 | let isBatchingUpdates = false
151 | let deadline = null
152 | let deadlineDidExpire = false
153 |
154 | function scheduleRootUpdate (current, element, expirationTime) {
155 | const update = createUpdate()
156 | update.payload = {element}
157 | enqueueUpdate(current, update)
158 | scheduleWork(current, expirationTime)
159 | return expirationTime
160 | }
161 |
162 | function scheduleWorkToRoot (fiber, expirationTime) {
163 | if (
164 | fiber.expirationTime === NoWork ||
165 | fiber.expirationTime > expirationTime
166 | ) {
167 | fiber.expirationTime = expirationTime
168 | }
169 | let alternate = fiber.alternate
170 | if (
171 | alternate !== null &&
172 | (alternate.expirationTime === NoWork ||
173 | alternate.expirationTime > expirationTime)
174 | ) {
175 | alternate.expirationTime = expirationTime
176 | }
177 | let node = fiber
178 | while (node !== null) {
179 | if (node.return === null && node.tag === HostRoot) {
180 | // return a FiberRoot instance
181 | return node.stateNode
182 | }
183 | node = node.return
184 | }
185 | return null
186 | }
187 |
188 | function scheduleWork (fiber, expirationTime) {
189 | const root = scheduleWorkToRoot(fiber, expirationTime)
190 | root.expirationTime = expirationTime
191 | requestWork(root, expirationTime)
192 | }
193 |
194 | function requestWork (root, expirationTime) {
195 | scheduledRoot = root
196 | if (isRendering) {
197 | return
198 | }
199 |
200 | if (isBatchingUpdates) {
201 | return
202 | }
203 |
204 | if (expirationTime === Sync) {
205 | performSyncWork()
206 | } else {
207 | scheduleCallbackWithExpirationTime(root, expirationTime)
208 | }
209 | }
210 |
211 | function scheduleCallbackWithExpirationTime(root, expirationTime) {
212 | const currentMs = now() - originalStartTimeMs
213 | const expirationTimeMs = expirationTimeToMs(expirationTime)
214 | const timeout = expirationTimeMs - currentMs
215 | scheduleDeferredCallback(performAsyncWork, {timeout})
216 | }
217 |
218 | function performSyncWork() {
219 | performWork(null)
220 | }
221 |
222 | function performAsyncWork (dl) {
223 | performWork(dl)
224 | }
225 |
226 | function performWork (dl) {
227 | deadline = dl
228 | // Keep working on roots until there's no more work, or until we reach
229 | // the deadline.
230 | if (deadline !== null) {
231 | recomputeCurrentRendererTime()
232 | currentSchedulerTime = currentRendererTime
233 | while (
234 | scheduledRoot !== null &&
235 | (!deadlineDidExpire || currentRendererTime >= scheduledRoot.expirationTime)
236 | ) {
237 | performWorkOnRoot(
238 | scheduledRoot,
239 | currentRendererTime >= scheduledRoot.expirationTime
240 | )
241 | recomputeCurrentRendererTime()
242 | currentSchedulerTime = currentRendererTime
243 | }
244 | } else {
245 | while (scheduledRoot !== null) {
246 | performWorkOnRoot(scheduledRoot, true)
247 | }
248 | }
249 | // We're done flushing work. Either we ran out of time in this callback,
250 | // or there's no more work left with sufficient priority.
251 | // If there's work left over, schedule a new callback.
252 | if (scheduledRoot) {
253 | scheduleCallbackWithExpirationTime(
254 | scheduledRoot,
255 | scheduledRoot.expirationTime,
256 | )
257 | }
258 | // Clean-up.
259 | deadline = null
260 | deadlineDidExpire = false
261 | }
262 |
263 | function shouldYield () {
264 | if (deadlineDidExpire) {
265 | return true
266 | }
267 | if (deadline === null || deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
268 | return false
269 | }
270 | deadlineDidExpire = true
271 | return true
272 | }
273 |
274 | function performWorkOnRoot(root, isExpired) {
275 | isRendering = true
276 | if (isExpired) {
277 | // Flush work without yielding.
278 | let finishedWork = root.finishedWork
279 | if (finishedWork !== null) {
280 | // This root is already complete. We can commit it.
281 | completeRoot(root, finishedWork)
282 | } else {
283 | root.finishedWork = null
284 | const isYieldy = false
285 | renderRoot(root, isYieldy)
286 | finishedWork = root.finishedWork
287 | if (finishedWork !== null) {
288 | // We've completed the root. Commit it.
289 | completeRoot(root, finishedWork)
290 | }
291 | }
292 | } else {
293 | // Flush async work.
294 | let finishedWork = root.finishedWork
295 | if (finishedWork !== null) {
296 | // This root is already complete. We can commit it.
297 | completeRoot(root, finishedWork)
298 | } else {
299 | root.finishedWork = null
300 | const isYieldy = true
301 | renderRoot(root, isYieldy)
302 | finishedWork = root.finishedWork
303 | if (finishedWork !== null) {
304 | // We've completed the root. Check the deadline one more time
305 | // before committing.
306 | if (!shouldYield()) {
307 | // Still time left. Commit the root.
308 | completeRoot(root, finishedWork)
309 | } else {
310 | // There's no time left. Mark this root as complete. We'll come
311 | // back and commit it later.
312 | root.finishedWork = finishedWork
313 | }
314 | }
315 | }
316 | }
317 | isRendering = false
318 | }
319 | ```
320 | 这里有几个新出现的全局变量:
321 |
322 | * isBatchingUpdates: 表明现在正在 batch 更新(与事件处理相关,现在可以认为它的值一直为 false)。
323 | * deadline:用来保存 requestIdleCallback 传递给即将被调用的函数的名为 deadline 的参数。deadline 具有一个 timeRemaining 属性,可以通过调用 deadline.timeRemaining() 来得到剩余的空闲时间。如果不理解请看 requestIdleCallback 的[介绍](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback)。
324 | * deadlineDidExpire: 表示本次调用 requestIdleCallback 分配的空闲时间是否已经用完。
325 |
326 | ### scheduleRootUpdate
327 |
328 | 需要注意 update 的 payload,是一个对象。这个对象有一个 element 属性,保存的是一个 ReactElement 对象。
329 |
330 | ### scheduleWork
331 |
332 | * 调用 scheduleWorkToRoot 更新 fiber 的 expirationTime, 如果 fiber 有 alternate,也更新 alternate 的 expirationTime。scheduleWorkToRoot 会返回此 fiber 的 root,注意这个 root 是一个 FiberRoot 实例。
333 | * 更新 root 的 expirationTime。
334 | * 调用 requestWork。
335 |
336 | ### requestWork
337 |
338 | * scheduledRoot = root 将当前存在更新的 root 赋值给 scheduledRoot,在下面介绍的 completeRoot 函数中将会重置 scheduledRoot。
339 | * 如果 React 正在渲染,直接返回,对应在生命周期函数中调用 this.setState。
340 | * 如果正在批量更新,直接返回,对应在一个事件回调中调用 this.setState。
341 | * 如果是同步更新,调用 performSyncWork,否则调用 scheduleCallbackWithExpirationTime。
342 |
343 | ### scheduleCallbackWithExpirationTime
344 |
345 | * 计算工作期限
346 | * 调用 scheduleDeferredCallback(requestIdleCallback) 让 performAsyncWork 在浏览器空闲时间再运行。
347 |
348 | ### performSyncWork 和 performAsyncWork
349 |
350 | 两者都调用了 performWork,performSyncWork 传入 null, performAsyncWork 传入 deadline。
351 |
352 | ### performWork
353 |
354 | * 值得注意的是 while 循环的判断条件。异步情况的条件意味着当 scheduledRoot === null,即没有待完成的工作的时候,会退出循环。当 deadlineDidExpire === true && currentRendererTime < scheduledRoot.expirationTime 的时候也会退出循环
355 | , 这种情况意味着本次调用 requestIdleCallback 分配的空闲时间已经用完,而本次工作并没有超过预期完成的期限,所以需要再次调用 requestIdleCallback 来分配空闲时间。
356 | * 当异步任务的 currentRendererTime >= scheduledRoot.expirationTime,即已经超过了本次工作的期限,实际上就会和同步任务一样调用 performWorkOnRoot(scheduledRoot, true)。
357 | * 根据上面的陈述,当我们退出循环的时候,要么已经完成了工作,要么本次分配的空闲时间已经用完,所以如果仍然有待完成的工作,就调用 scheduleCallbackWithExpirationTime,为 performAsyncWork 再分配空闲时间。
358 | * 重置 deadline 和 deadlineDidExpire,注意重置的时机在下一次执行 performAsyncWork 之前。
359 |
360 | ### performWorkOnRoot
361 |
362 | * 根据第二个参数 isExpired 的值来判断工作是否已经到期,同步更新等同于异步更新到期。
363 | * 不论工作有没有到期,都先看 root.finishedWork 是否存在,如果存在表示我们已经可以进入 commit 阶段了。
364 | * 否则先调用 renderRoot 进入 render 阶段,结束之后再调用 completeRoot 进入 commit 阶段。工作到期或者没到期的区别在于传入 renderRoot 的第二个参数不同,以及在调用 completeRoot 之前是否需要再判断一次是否有剩余的空闲时间。
365 | * 异步任务有两种情况会因为没有空闲时间而回到 performWork 中,一种是在执行 renderRoot 过程中就已经用完了空闲时间,一种是在执行 completeRoot 之前已经没有了空闲时间。回到 performWork 中之后,如果任务的预期完成期限已经到期,就会同步执行任务,否则调用 scheduleCallbackWithExpirationTime 分配新的空闲时间。
366 | * 需要注意的是 isRendering 在 performWorkOnRoot 开始时置为 true,结束时置为 false,标志着渲染过程的开始和结束。
367 |
368 | ### shouldYield
369 |
370 | 判断本次调用 requestIdleCallback 分配的空闲时间是否有剩余,同时更新 deadlineDidExpire 的值。注意判断的依据 timeHeuristicForUnitOfWork 的值为 1。
371 |
372 | ## render 阶段
373 | ```javascript
374 | let nextUnitOfWork = null
375 |
376 | // This is used to create an alternate fiber to do work on.
377 | function createWorkInProgress(current, pendingProps, expirationTime) {
378 | let workInProgress = current.alternate
379 | if (workInProgress === null) {
380 | // We use a double buffering pooling technique because we know that we'll
381 | // only ever need at most two versions of a tree. We pool the "other" unused
382 | // node that we're free to reuse. This is lazily created to avoid allocating
383 | // extra objects for things that are never updated. It also allow us to
384 | // reclaim the extra memory if needed.
385 | workInProgress = new FiberNode(current.tag, pendingProps)
386 | workInProgress.type = current.type
387 | workInProgress.stateNode = current.stateNode
388 | workInProgress.alternate = current
389 | current.alternate = workInProgress
390 | } else {
391 | workInProgress.pendingProps = pendingProps
392 |
393 | // We already have an alternate.
394 | // Reset the effect tag.
395 | workInProgress.effectTag = NoEffect
396 |
397 | // The effect list is no longer valid.
398 | workInProgress.nextEffect = null
399 | workInProgress.firstEffect = null
400 | workInProgress.lastEffect = null
401 | }
402 |
403 | if (pendingProps !== current.pendingProps) {
404 | // This fiber has new props.
405 | workInProgress.expirationTime = expirationTime
406 | } else {
407 | // This fiber's props have not changed.
408 | workInProgress.expirationTime = current.expirationTime
409 | }
410 |
411 | workInProgress.child = current.child
412 | workInProgress.memoizedProps = current.memoizedProps
413 | workInProgress.memoizedState = current.memoizedState
414 | workInProgress.updateQueue = current.updateQueue
415 |
416 | // These will be overridden during the parent's reconciliation
417 | workInProgress.sibling = current.sibling
418 |
419 | return workInProgress
420 | }
421 |
422 | function renderRoot (root, isYieldy) {
423 | isWorking = true
424 | const expirationTime = root.expirationTime
425 | if (expirationTime !== nextRenderExpirationTime || nextUnitOfWork === null) {
426 | nextRenderExpirationTime = expirationTime
427 | nextUnitOfWork = createWorkInProgress(root.current, null, nextRenderExpirationTime)
428 | }
429 | workLoop(isYieldy)
430 | // We're done performing work. Time to clean up.
431 | isWorking = false
432 | if (nextUnitOfWork !== null) {
433 | return
434 | }
435 | // Ready to commit.
436 | root.finishedWork = root.current.alternate
437 | }
438 |
439 | function workLoop (isYieldy) {
440 | if (!isYieldy) {
441 | // Flush work without yielding
442 | while (nextUnitOfWork !== null) {
443 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
444 | }
445 | } else {
446 | // Flush asynchronous work until the deadline runs out of time.
447 | while (nextUnitOfWork !== null && !shouldYield()) {
448 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
449 | }
450 | }
451 | }
452 |
453 | function performUnitOfWork (workInProgress) {
454 | const current = workInProgress.alternate
455 | let next = null
456 | next = beginWork(current, workInProgress, nextRenderExpirationTime)
457 | if (next === null) {
458 | next = completeUnitOfWork(workInProgress)
459 | }
460 | return next
461 | }
462 |
463 | function beginWork (current, workInProgress, renderExpirationTime) {
464 | // Before entering the begin phase, clear the expiration time.
465 | workInProgress.expirationTime = NoWork
466 | const Component = workInProgress.type
467 | const unresolvedProps = workInProgress.pendingProps
468 | switch (workInProgress.tag) {
469 | case ClassComponent: {
470 | return updateClassComponent(current, workInProgress, Component, unresolvedProps, renderExpirationTime)
471 | }
472 | case HostRoot: {
473 | return updateHostRoot(current, workInProgress, renderExpirationTime)
474 | }
475 | case HostComponent:{
476 | return updateHostComponent(current, workInProgress, renderExpirationTime)
477 | }
478 | default:
479 | throw new Error('unknown unit of work tag')
480 | }
481 | }
482 | ```
483 | 新出现的全局变量 nextUnitOfWork 代表我们正在工作的 work-in-progress fiber。当我们在初始 mount 阶段时,current fiber 树只有一个根节点。在 renderRoot 函数中,会调用 createWorkInProgress 生成一个 work-in-progress 根节点,然后这个节点就会作为第一个被传入 performUnitOfWork 的 fiber。performUnitOfWork 会最终返回一个传入节点的子节点,这个节点本身又会被传入 performUnitOfWork,直到生成一颗完整的 work-in-progress fiber 树。这颗 work-in-progress fiber 树被 commit 之后,就会赋值给 root.current,成为新的 current fiber 树。
484 |
485 | 在 update 阶段,一样的过程被重复,不同的地方是current fiber 树不仅仅只有一个根节点,而是之前被 commit 的 work-in-progress fiber 树。
486 |
487 | ### renderRoot
488 |
489 | * nextUnitOfWork 初始化为根节点的 work-in-progress fiber。
490 | * 开始 workLoop,注意 workLoop 的前后 isWorking 的值。
491 | * 从 workLoop 中返回之后,如果 nextUnitOfWork 仍然存在, 那么直接返回。这种情况意味着我们在 workLoop 中耗尽了本次分配的空闲时间,而没有完成所有的任务。
492 | * 否则,设置 root.finishedWork 为 root 的 work-in-progress fiber,注意不是 root.current,因为所有的副作用都标志在 work-in-progress fiber 上。
493 |
494 | ### workLoop
495 |
496 | * 同步和异步的区别在于 while 循环是否需要判断空闲时间是否使用完,同步任务会一直执行到结束,而异步任务会因为没有空闲时间而返回。
497 |
498 | ### performUnitOfWork
499 |
500 | * 先调用 beginWork 生成 work-in-progress fiber 的 child
501 | * 如果 child 不存在,表示我们当前的 work-in-progress fiber 没有子节点了,调用 completeUnitOfWork。
502 |
503 | ### beginWork
504 |
505 | * 根据 tag 来调用函数。简化了逻辑,只支持 ClassComponent,HostRoot 和 HostComponent。三个函数都会生成并返回 workInProgress.child。
506 | * 在前面一章已经提到,tag 为 ClassComponent 的 fiber 的 type 属性是构造函数,这个构造函数会传入 updateClassComponent。
507 |
508 | ```javascript
509 | function updateClassComponent (current, workInProgress, Component, nextProps, renderExpirationTime) {
510 | let shouldUpdate
511 | if (current === null) {
512 | constructClassInstance(workInProgress, Component, nextProps)
513 | mountClassInstance(workInProgress, Component, nextProps)
514 | shouldUpdate = true
515 | } else {
516 | shouldUpdate = updateClassInstance(current, workInProgress, Component, nextProps)
517 | }
518 | return finishClassComponent(current, workInProgress, shouldUpdate, renderExpirationTime)
519 | }
520 |
521 | function constructClassInstance (workInProgress, ctor, props) {
522 | let instance = new ctor(props)
523 | workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state : null
524 | adoptClassInstance(workInProgress, instance)
525 | return instance
526 | }
527 |
528 | function get(key) {
529 | return key._reactInternalFiber
530 | }
531 |
532 | function set(key, value) {
533 | key._reactInternalFiber = value
534 | }
535 |
536 | const classComponentUpdater = {
537 | enqueueSetState: function (inst, payload) {
538 | const fiber = get(inst)
539 | const currentTime = requestCurrentTime()
540 | const expirationTime = computeExpirationForFiber(currentTime)
541 | const update = createUpdate()
542 | update.payload = payload
543 | enqueueUpdate(fiber, update)
544 | scheduleWork(fiber, expirationTime)
545 | }
546 | }
547 |
548 | function adoptClassInstance (workInProgress, instance) {
549 | instance.updater = classComponentUpdater
550 | workInProgress.stateNode = instance
551 | set(instance, workInProgress)
552 | }
553 |
554 | function mountClassInstance(workInProgress, ctor, newProps) {
555 | let instance = workInProgress.stateNode
556 | instance.props = newProps
557 | instance.state = workInProgress.memoizedState
558 | const updateQueue = workInProgress.updateQueue
559 | if (updateQueue !== null) {
560 | processUpdateQueue(workInProgress, updateQueue)
561 | instance.state = workInProgress.memoizedState
562 | }
563 | }
564 |
565 | function updateClassInstance (current, workInProgress, ctor, newProps) {
566 | const instance = workInProgress.stateNode
567 | const oldProps = workInProgress.memoizedProps
568 | instance.props = oldProps
569 | const oldState = workInProgress.memoizedState
570 | let newState = instance.state = oldState
571 | let updateQueue = workInProgress.updateQueue
572 | if (updateQueue !== null) {
573 | processUpdateQueue(
574 | workInProgress,
575 | updateQueue
576 | )
577 | newState = workInProgress.memoizedState
578 | }
579 | if (oldProps === newProps && oldState === newState) {
580 | return false
581 | }
582 | instance.props = newProps
583 | instance.state = newState
584 | return true
585 | }
586 |
587 | function finishClassComponent (current, workInProgress, shouldUpdate, renderExpirationTime) {
588 | if (!shouldUpdate) {
589 | cloneChildFibers(workInProgress)
590 | } else {
591 | const instance = workInProgress.stateNode
592 | const nextChildren = instance.render()
593 | reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime)
594 | memoizeState(workInProgress, instance.state)
595 | memoizeProps(workInProgress, instance.props)
596 | }
597 | return workInProgress.child
598 | }
599 |
600 | function cloneChildFibers(workInProgress) {
601 | if (workInProgress.child === null) {
602 | return
603 | }
604 | let currentChild = workInProgress.child
605 | let newChild = createWorkInProgress(currentChild, currentChild.pendingProps, currentChild.expirationTime)
606 | workInProgress.child = newChild
607 | newChild.return = workInProgress
608 | while (currentChild.sibling !== null) {
609 | currentChild = currentChild.sibling
610 | newChild = newChild.sibling = createWorkInProgress(currentChild, currentChild.pendingProps, currentChild.expirationTime)
611 | newChild.return = workInProgress
612 | }
613 | newChild.sibling = null
614 | }
615 |
616 | function memoizeProps(workInProgress, nextProps) {
617 | workInProgress.memoizedProps = nextProps
618 | }
619 |
620 | function memoizeState(workInProgress, nextState) {
621 | workInProgress.memoizedState = nextState
622 | }
623 | ```
624 | 更新 class 组件的逻辑最复杂,mount 和 update 阶段需要做的事情不同。updateClassComponent 根据 current 参数是否存在来判断是 mount 阶段还是 update 阶段。可以这么做的原因上面已经提到过,是因为如果是 update 阶段,则必然存在一个与之对应的current fiber。而 mount 阶段只存在 work-in-progress fiber。
625 |
626 | ### constructClassInstance
627 |
628 | 第二个参数是类的构造函数,首先创建一个实例,然后用实例的 state 来更新 workInProgress.memoizedState,最后调用 adoptClassInstance。
629 |
630 | ### adoptClassInstance
631 |
632 | 当我们声明一个类组件时,会继承 React.Component 的 setState 函数。
633 |
634 | ``` javascript
635 | // React 源码
636 | Component.prototype.setState = function(partialState, callback) {
637 | this.updater.enqueueSetState(this, partialState, callback, 'setState')
638 | }
639 | ```
640 | 而实例的 updater 属性会在 adoptClassInstance 中被更新。所以当我们调用 this.setState 时,实际上会调用 enqueueSetState。
641 | 注意 workInProgress.stateNode 会设置为这个实例,而这个实例的 _reactInternalFiber 属性会设置为 workInProgress。
642 |
643 | ### enqueueSetState
644 |
645 | 首先从实例的 _reactInternalFiber 属性中得到对应的 fiber。然后计算 fiber 的工作期限。接着更新 fiber 的 updateQueue 属性。
646 | 注意更新的 payload 是我们调用 this.setState 时传入的对象。最后调用 scheduleWork。
647 |
648 | ### mountClassInstance
649 | * 更新 instance.props
650 | * processUpdateQueue, 更新 instance.state
651 | * 实际上这里会调用生命周期函数 getDerivedStateFromProps。
652 |
653 | ### updateClassInstance
654 |
655 | * 和 mountClassInstance 一样需要更新实例的 props 和 state
656 | * 这里实际上会调用生命周期函数 shouldComponentUpdate 来判断是否需要更新。这里简化为直接比较新旧 props 和 state 是否有变化,如果没有变化返回 false, 如果有变化则返回 true。
657 |
658 | ### finishClassComponent
659 |
660 | * 如果不需要更新,调用 cloneChildFibers 直接复制现在的子 fiber 节点
661 | * 否则先调用实例的 render 函数,得到一个表示后代的 ReactElement 对象, 然后调用 reconcileChildren。
662 |
663 | ``` javascript
664 | let shouldTrackSideEffects = true
665 |
666 | function reconcileChildren (current, workInProgress, nextChildren, renderExpirationTime) {
667 | if (current === null) {
668 | shouldTrackSideEffects = false
669 | workInProgress.child = reconcileChildFibers(workInProgress, null, nextChildren, renderExpirationTime)
670 | } else {
671 | shouldTrackSideEffects = true
672 | workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderExpirationTime)
673 | }
674 | }
675 |
676 | function reconcileChildFibers(returnFiber, currentFirstChild, newChild, expirationTime) {
677 | if (newChild) {
678 | const childArray = Array.isArray(newChild) ? newChild : [newChild]
679 | return reconcileChildrenArray(returnFiber, currentFirstChild, childArray, expirationTime)
680 | } else {
681 | return null
682 | }
683 | }
684 |
685 | function reconcileChildrenArray (returnFiber, currentFirstChild, newChildren, expirationTime) {
686 | let resultingFirstChild = null
687 | let previousNewFiber = null
688 | let oldFiber = currentFirstChild
689 | let newIdx = 0
690 | // update
691 | for (; oldFiber !== null && newIdx < newChildren.length; newIdx ++) {
692 | let newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], expirationTime)
693 | if (resultingFirstChild === null) {
694 | resultingFirstChild = newFiber
695 | } else {
696 | previousNewFiber.sibling = newFiber
697 | }
698 | previousNewFiber = newFiber
699 | oldFiber = oldFiber.sibling
700 | }
701 | // placement
702 | if (oldFiber === null) {
703 | for (; newIdx < newChildren.length; newIdx++) {
704 | let _newFiber = createChild(returnFiber, newChildren[newIdx], expirationTime)
705 | if (shouldTrackSideEffects && _newFiber.alternate === null) {
706 | _newFiber.effectTag = Placement
707 | }
708 | if (resultingFirstChild === null) {
709 | resultingFirstChild = _newFiber
710 | } else {
711 | previousNewFiber.sibling = _newFiber
712 | }
713 | previousNewFiber = _newFiber
714 | }
715 | return resultingFirstChild
716 | }
717 | }
718 | ```
719 |
720 | shouldTrackSideEffects 代表了是否需要标记副作用。
721 |
722 | ### reconcileChildren
723 |
724 | 注意当我们在初始 mount 阶段时, 除了根节点有 current fiber 之外,其它的节点都没有 current fiber。所以initial mount 阶段只会标记根 fiber 节点的 effectTag。
725 |
726 | ### reconcileChildrenArray
727 |
728 | 这是核心的函数,是实现 [The Diffing Algorithm](https://reactjs.org/docs/reconciliation.html#the-diffing-algorithm) 的地方。这里会生成新的 work-in-progress fiber,且标记上 effectTag。
729 |
730 | 为了简化,这里我并没有考虑删除的逻辑,因为对现在这个简单的组件来说,只存在初始 mount 阶段时,placement 的逻辑和后续点击按钮触发更新时,update 的逻辑。我也没有考虑,更新时 fiber 的类型发生变化的情况,因为这里从始至终只有文本节点在发生变化。我也没有考虑节点发生位移的情况,这需要用到 fiber 的 index 属性。所以这里的实现仅仅满足我们的这个简单组件的情况。
731 |
732 | * oldFiber 初始化为 returnFiber 下第一个子节点。如果存在,则代表发生的是更新,调用 updateSlot。
733 | * oldFiber 如果一开始就不存在或者完成了 update 的循环之后仍然有新的子节点存在,表示需要插入节点,调用 createChild 生成新的 work-in-progress fiber。如果需要标记副作用, 就标记上 Placement。
734 |
735 | ```javascript
736 | function updateSlot (returnFiber, oldFiber, newChild, expirationTime) {
737 | if (typeof newChild === 'object' && newChild !== null) {
738 | return updateElement(returnFiber, oldFiber, newChild, expirationTime)
739 | }
740 | return null
741 | }
742 |
743 | function updateElement (returnFiber, current, element, expirationTime) {
744 | if (current !== null && current.type === element.type) {
745 | const existing = useFiber(current, element.props, expirationTime)
746 | existing.return = returnFiber
747 | return existing
748 | }
749 | return null
750 | }
751 |
752 | function useFiber (fiber, pendingProps, expirationTime) {
753 | let clone = createWorkInProgress(fiber, pendingProps, expirationTime)
754 | clone.sibling = null
755 | return clone
756 | }
757 |
758 | function createChild (returnFiber, newChild, expirationTime) {
759 | if (typeof newChild === 'object' && newChild !== null) {
760 | let created = createFiberFromElement(newChild, expirationTime)
761 | created.return = returnFiber
762 | return created
763 | }
764 | return null
765 | }
766 |
767 | function createFiberFromElement (element, expirationTime) {
768 | let fiber
769 | const type = element.type
770 | const pendingProps = element.props
771 | let fiberTag
772 | if (typeof type === 'function') {
773 | fiberTag = ClassComponent
774 | } else if (typeof type === 'string') {
775 | fiberTag = HostComponent
776 | }
777 | fiber = new FiberNode(fiberTag, pendingProps)
778 | fiber.type = type
779 | fiber.expirationTime = expirationTime
780 | return fiber
781 | }
782 | ```
783 | ### updateSlot 和 createChild
784 |
785 | 需要注意的是传入这两个函数的 newChild 参数应是一个 ReactElement 对象。
786 |
787 | ### createFiberFromElement
788 |
789 | 生成一个 ReactElement 对象相应的 fiber,注意区分不同的 tag。
790 |
791 | ```javascript
792 | function updateHostRoot (current, workInProgress, renderExpirationTime) {
793 | const updateQueue = workInProgress.updateQueue
794 | const prevState = workInProgress.memoizedState
795 | const prevChildren = prevState !== null ? prevState.element : null
796 | processUpdateQueue(workInProgress, updateQueue)
797 | const nextState = workInProgress.memoizedState
798 | const nextChildren = nextState.element
799 | if (nextChildren === prevChildren) {
800 | cloneChildFibers(workInProgress)
801 | return workInProgress.child
802 | }
803 | reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime)
804 | return workInProgress.child
805 | }
806 |
807 | function updateHostComponent (current, workInProgress, renderExpirationTime) {
808 | const nextProps = workInProgress.pendingProps
809 | let nextChildren = nextProps.children
810 | const isDirectTextChild = shouldSetTextContent(nextProps)
811 | if (isDirectTextChild) {
812 | nextChildren = null
813 | }
814 | reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime)
815 | memoizeProps(workInProgress, nextProps)
816 | return workInProgress.child
817 | }
818 | ```
819 |
820 | ### updateHostRoot 和 updateHostComponent
821 |
822 | * 注意在 scheduleRootUpdate 中,我们生成了一个 payload 是 { element } 的 Update 对象。所以在 updateHostRoot 中 processUpdateQueue 之后,可以通过 workInProgress.memoizedState.element 得到子节点。如果子节点未发生变化,调用 cloneChildFibers。 否则 reconcileChildren。
823 | * updateHostComponent 中如果子节点是一个文本节点,将置 nextChildren = null,这时候调用 reconcileChildren 实际上不会做任何事,这样就不会生成额外的 tag 为 HostText 的 fiber 节点。能这么做的原因是 finalizeInitialChildren 中有判断 props.children 是文本节点的逻辑。
824 |
825 | ```javascript
826 | function completeUnitOfWork (workInProgress) {
827 | // Attempt to complete the current unit of work, then move to the
828 | // next sibling. If there are no more siblings, return to the
829 | // parent fiber.
830 | while (true) {
831 | const current = workInProgress.alternate
832 | const returnFiber = workInProgress.return
833 | const siblingFiber = workInProgress.sibling
834 | completeWork(current, workInProgress)
835 | if (returnFiber !== null &&
836 | // Do not append effects to parents if a sibling failed to complete
837 | (returnFiber.effectTag & Incomplete) === NoEffect) {
838 | // Append all the effects of the subtree and this fiber onto the effect
839 | // list of the parent. The completion order of the children affects the
840 | // side-effect order.
841 | if (returnFiber.firstEffect === null) {
842 | returnFiber.firstEffect = workInProgress.firstEffect
843 | }
844 | if (workInProgress.lastEffect !== null) {
845 | if (returnFiber.lastEffect !== null) {
846 | returnFiber.lastEffect.nextEffect = workInProgress.firstEffect
847 | }
848 | returnFiber.lastEffect = workInProgress.lastEffect
849 | }
850 | // If this fiber had side-effects, we append it AFTER the children's
851 | // side-effects. We can perform certain side-effects earlier if
852 | // needed, by doing multiple passes over the effect list. We don't want
853 | // to schedule our own side-effect on our own list because if end up
854 | // reusing children we'll schedule this effect onto itself since we're
855 | // at the end.
856 | const effectTag = workInProgress.effectTag
857 | // Skip both NoWork and PerformedWork tags when creating the effect list.
858 | // PerformedWork effect is read by React DevTools but shouldn't be committed.
859 | if (effectTag >= Placement) {
860 | if (returnFiber.lastEffect !== null) {
861 | returnFiber.lastEffect.nextEffect = workInProgress
862 | } else {
863 | returnFiber.firstEffect = workInProgress
864 | }
865 | returnFiber.lastEffect = workInProgress
866 | }
867 | }
868 | if (siblingFiber !== null) {
869 | // If there is more work to do in this returnFiber, do that next.
870 | return siblingFiber
871 | } else if (returnFiber !== null) {
872 | // If there's no more work in this returnFiber. Complete the returnFiber.
873 | workInProgress = returnFiber
874 | continue
875 | } else {
876 | // We've reached the root.
877 | return null
878 | }
879 | }
880 | }
881 |
882 | function completeWork (current, workInProgress) {
883 | const newProps = workInProgress.pendingProps
884 | switch(workInProgress.tag) {
885 | case ClassComponent: {
886 | break
887 | }
888 | case HostRoot: {
889 | break
890 | }
891 | case HostComponent: {
892 | const type = workInProgress.type
893 | if (current !== null && workInProgress.stateNode != null) {
894 | const oldProps = current.memoizedProps
895 | const updatePayload = prepareUpdate(oldProps, newProps)
896 | workInProgress.updateQueue = updatePayload
897 | if (updatePayload) {
898 | markUpdate(workInProgress)
899 | }
900 | } else {
901 | //initial pass
902 | const _instance = createInstance(type, newProps, workInProgress)
903 | appendAllChildren(_instance, workInProgress)
904 | finalizeInitialChildren(_instance, newProps)
905 | workInProgress.stateNode = _instance
906 | }
907 | break
908 | }
909 | default: {
910 | throw new Error('Unknown unit of work tag')
911 | }
912 | }
913 | return null
914 | }
915 |
916 | function markUpdate(workInProgress) {
917 | workInProgress.effectTag |= Update
918 | }
919 |
920 | function appendAllChildren (parent, workInProgress) {
921 | let node = workInProgress.child
922 | while (node !== null) {
923 | if (node.tag === HostComponent) {
924 | appendInitialChild(parent, node.stateNode)
925 | } else if (node.child !== null) {
926 | node.child.return = node
927 | node = node.child
928 | continue
929 | }
930 | if (node === workInProgress) {
931 | return
932 | }
933 | while (node.sibling === null) {
934 | if (node.return === null || node.return === workInProgress) {
935 | return
936 | }
937 | node = node.return
938 | }
939 | node.sibling.return = node.return
940 | node = node.sibling
941 | }
942 | }
943 | ```
944 |
945 | completeUnitOfWork 的逻辑 React 注释已经解释的很清楚了,这里不再赘述。我只强调一下最后所有有副作用的 fiber 都将汇集到根节点 fiber 中。
946 |
947 | ### completeWork
948 |
949 | * 如果是 initial mount 阶段,调用 createInstance 生成 DOM 节点,再调用 appendAllChildren 把当前节点下所有的直接子节点 append 到生成的 DOM 节点下,最后调用 finalizeInitialChildren 设置 DOM 节点的属性,绑定事件。
950 | * 如果是 update 阶段,调用 prepareUpdate 准备好更新的内容,并标记上 effectTag,在 commit 阶段的时候会调用 commitUpdate 应用这些更新。
951 |
952 | ## commit 阶段
953 |
954 | ```javascript
955 | function completeRoot(root, finishedWork) {
956 | root.finishedWork = null
957 | scheduledRoot = null
958 | commitRoot(root, finishedWork)
959 | }
960 |
961 | function commitRoot(root, finishedWork) {
962 | isWorking = true
963 | isCommitting = true
964 | root.expirationTime = NoWork
965 | const firstEffect = finishedWork.firstEffect
966 | commitAllHostEffects(firstEffect)
967 | root.current = finishedWork
968 | isCommitting = false
969 | isWorking = false
970 | }
971 |
972 | function commitAllHostEffects (firstEffect) {
973 | let nextEffect = firstEffect
974 | while (nextEffect !== null) {
975 | const effectTag = nextEffect.effectTag
976 | switch(effectTag & (Placement | Update)) {
977 | case Placement: {
978 | commitPlacement(nextEffect)
979 | nextEffect.effectTag &= ~Placement
980 | break
981 | }
982 | case Update: {
983 | commitWork(nextEffect)
984 | break
985 | }
986 | }
987 | nextEffect = nextEffect.nextEffect
988 | }
989 | }
990 |
991 | function commitPlacement (finishedWork) {
992 | const parentFiber = getHostParentFiber(finishedWork)
993 | const parent = parentFiber.tag === HostRoot ? parentFiber.stateNode.containerInfo : parentFiber.stateNode
994 | let node = finishedWork
995 | while (true) {
996 | if (node.tag === HostComponent) {
997 | appendChildToContainer(parent, node.stateNode)
998 | } else if (node.child !== null) {
999 | node.child.return = node
1000 | node = node.child
1001 | continue
1002 | }
1003 | if (node === finishedWork) {
1004 | return
1005 | }
1006 | while (node.sibling === null) {
1007 | if (node.return === null || node.return === finishedWork) {
1008 | return
1009 | }
1010 | node = node.return
1011 | }
1012 | node.sibling.return = node.return
1013 | node = node.sibling
1014 | }
1015 | }
1016 |
1017 | function getHostParentFiber(fiber) {
1018 | let parent = fiber.return
1019 | while (parent !== null) {
1020 | if (isHostParent(parent)) {
1021 | return parent
1022 | }
1023 | parent = parent.return
1024 | }
1025 | }
1026 |
1027 | function isHostParent(fiber) {
1028 | return fiber.tag === HostComponent || fiber.tag === HostRoot
1029 | }
1030 |
1031 | function commitWork (finishedWork) {
1032 | switch (finishedWork.tag) {
1033 | case HostRoot:
1034 | case ClassComponent: {
1035 | return
1036 | }
1037 | case HostComponent: {
1038 | const instance = finishedWork.stateNode
1039 | if (instance != null) {
1040 | const updatePayload = finishedWork.updateQueue
1041 | finishedWork.updateQueue = null
1042 | if (updatePayload !== null) {
1043 | commitUpdate(instance, updatePayload)
1044 | }
1045 | }
1046 | return
1047 | }
1048 | default: {
1049 | throw new Error('This unit of work tag should not have side-effects')
1050 | }
1051 | }
1052 | }
1053 | ```
1054 |
1055 | commit 阶段的逻辑并不复杂。首先调用 completeRoot 重置 root.finishedWork 和 scheduledRoot,然后调用 commitRoot。注意在 commitRoot 中,在修改了 DOM 之后,root.current = finishedWork 将已经完成的 work-in-progress fiber 树变成了 current fiber 树。我忽略了 commitRoot 中的生命周期函数的实现。
1056 |
1057 | 最后运行项目
1058 |
1059 | 
1060 |
1061 | [下一章](Event.md)
1062 |
1063 |
1064 |
1065 |
1066 |
1067 |
1068 |
1069 |
1070 |
1071 |
--------------------------------------------------------------------------------
/src/reconciler/Reconciler.js:
--------------------------------------------------------------------------------
1 | import {
2 | NoWork,
3 | Sync,
4 | msToExpirationTime,
5 | expirationTimeToMs,
6 | computeAsyncExpiration,
7 | computeInteractiveExpiration
8 | } from './ReactFiberExpirationTime'
9 | import { createUpdate, enqueueUpdate, processUpdateQueue } from './ReactUpdateQueue'
10 | import { createFiberRoot } from './ReactFiberRoot'
11 | import { FiberNode } from './ReactFiber'
12 | import { isInteractiveEvent } from '../event/isInteractiveEvent'
13 | import {
14 | ClassComponent,
15 | HostRoot,
16 | HostComponent,
17 | SuspenseComponent
18 | } from '../shared/ReactWorkTags'
19 | import {
20 | NoEffect,
21 | Placement,
22 | Update,
23 | Deletion,
24 | DidCapture,
25 | Incomplete,
26 | } from '../shared/ReactSideEffectTags'
27 | import { traverseTwoPhase } from '../shared/ReactTreeTraversal'
28 |
29 | function Reconciler (hostConfig) {
30 | const now = hostConfig.now
31 | const shouldSetTextContent = hostConfig.shouldSetTextContent
32 | const createInstance = hostConfig.createInstance
33 | const finalizeInitialChildren = hostConfig.finalizeInitialChildren
34 | const appendInitialChild = hostConfig.appendInitialChild
35 | const scheduleDeferredCallback = hostConfig.scheduleDeferredCallback
36 | const prepareUpdate = hostConfig.prepareUpdate
37 | const appendChildToContainer = hostConfig.appendChildToContainer
38 | const removeChildFromContainer = hostConfig.removeChildFromContainer
39 | const commitUpdate = hostConfig.commitUpdate
40 |
41 |
42 | let scheduledRoot = null
43 | let isRendering = false
44 | let deadline = null
45 | let deadlineDidExpire = false
46 | let isBatchingInteractiveUpdates = false
47 | let isBatchingUpdates = false
48 | let isDispatchControlledEvent = false
49 | let originalStartTimeMs = now()
50 | let currentRendererTime = msToExpirationTime(originalStartTimeMs)
51 | let currentSchedulerTime = currentRendererTime
52 | let isWorking = false
53 | let isCommitting = false
54 | let nextUnitOfWork = null
55 | let nextRenderExpirationTime = NoWork
56 | let shouldTrackSideEffects = true
57 | const timeHeuristicForUnitOfWork = 1
58 |
59 | function createContainer (containerInfo) {
60 | return createFiberRoot(containerInfo)
61 | }
62 |
63 | function updateContainer (element, container) {
64 | const current = container.current
65 | const currentTime = requestCurrentTime()
66 | const expirationTime = computeExpirationForFiber(currentTime)
67 | return scheduleRootUpdate(current, element, expirationTime)
68 | }
69 |
70 | function requestCurrentTime() {
71 | if (isRendering) {
72 | return currentSchedulerTime
73 | }
74 | if (!scheduledRoot) {
75 | recomputeCurrentRendererTime()
76 | currentSchedulerTime = currentRendererTime
77 | return currentSchedulerTime
78 | }
79 | return currentSchedulerTime
80 | }
81 |
82 | function recomputeCurrentRendererTime () {
83 | let currentTimeMs = now() - originalStartTimeMs
84 | currentRendererTime = msToExpirationTime(currentTimeMs)
85 | }
86 |
87 | function computeExpirationForFiber (currentTime) {
88 | let expirationTime
89 | if (isWorking) {
90 | if (isCommitting) {
91 | expirationTime = Sync
92 | } else {
93 | expirationTime = nextRenderExpirationTime
94 | }
95 | } else {
96 | if (isBatchingInteractiveUpdates) {
97 | expirationTime = computeInteractiveExpiration(currentTime)
98 | } else {
99 | expirationTime = computeAsyncExpiration(currentTime)
100 | }
101 | }
102 | return expirationTime
103 | }
104 |
105 | function scheduleRootUpdate (current, element, expirationTime) {
106 | const update = createUpdate()
107 | update.payload = {element}
108 | enqueueUpdate(current, update)
109 | scheduleWork(current, expirationTime)
110 | return expirationTime
111 | }
112 |
113 | function scheduleWorkToRoot (fiber, expirationTime) {
114 | if (
115 | fiber.expirationTime === NoWork ||
116 | fiber.expirationTime > expirationTime
117 | ) {
118 | fiber.expirationTime = expirationTime
119 | }
120 | let alternate = fiber.alternate
121 | if (
122 | alternate !== null &&
123 | (alternate.expirationTime === NoWork ||
124 | alternate.expirationTime > expirationTime)
125 | ) {
126 | alternate.expirationTime = expirationTime
127 | }
128 | let node = fiber
129 | while (node !== null) {
130 | if (node.return === null && node.tag === HostRoot) {
131 | return node.stateNode
132 | }
133 | node = node.return
134 | }
135 | return null
136 | }
137 |
138 | function scheduleWork (fiber, expirationTime) {
139 | const root = scheduleWorkToRoot(fiber, expirationTime)
140 | root.expirationTime = expirationTime
141 | requestWork(root, expirationTime)
142 | }
143 |
144 | function requestWork (root, expirationTime) {
145 | scheduledRoot = root
146 | if (isRendering) {
147 | return
148 | }
149 | if (isBatchingUpdates) {
150 | return
151 | }
152 | if (expirationTime === Sync) {
153 | performSyncWork()
154 | } else {
155 | scheduleCallbackWithExpirationTime(root, expirationTime)
156 | }
157 | }
158 |
159 | function scheduleCallbackWithExpirationTime(root, expirationTime) {
160 | const currentMs = now() - originalStartTimeMs
161 | const expirationTimeMs = expirationTimeToMs(expirationTime)
162 | const timeout = expirationTimeMs - currentMs
163 | scheduleDeferredCallback(performAsyncWork, {timeout})
164 | }
165 |
166 | function performSyncWork() {
167 | performWork(null)
168 | }
169 |
170 | function performAsyncWork (dl) {
171 | performWork(dl)
172 | }
173 |
174 | function performWork (dl) {
175 | deadline = dl
176 | if (deadline !== null) {
177 | recomputeCurrentRendererTime()
178 | currentSchedulerTime = currentRendererTime
179 | while (
180 | scheduledRoot !== null &&
181 | (!deadlineDidExpire || currentRendererTime >= scheduledRoot.expirationTime)
182 | ) {
183 | performWorkOnRoot(
184 | scheduledRoot,
185 | currentRendererTime >= scheduledRoot.expirationTime
186 | )
187 | recomputeCurrentRendererTime()
188 | currentSchedulerTime = currentRendererTime
189 | }
190 | } else {
191 | while (scheduledRoot !== null) {
192 | performWorkOnRoot(scheduledRoot, true)
193 | }
194 | }
195 | if (scheduledRoot) {
196 | scheduleCallbackWithExpirationTime(
197 | scheduledRoot,
198 | scheduledRoot.expirationTime,
199 | )
200 | }
201 | deadline = null
202 | deadlineDidExpire = false
203 | }
204 |
205 |
206 | function shouldYield () {
207 | if (deadlineDidExpire) {
208 | return true
209 | }
210 | if (deadline === null || deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
211 | return false
212 | }
213 | deadlineDidExpire = true
214 | return true
215 | }
216 |
217 | function performWorkOnRoot(root, isExpired) {
218 | isRendering = true
219 | if (isExpired) {
220 | let finishedWork = root.finishedWork
221 | if (finishedWork !== null) {
222 | completeRoot(root, finishedWork)
223 | } else {
224 | root.finishedWork = null
225 | const isYieldy = false
226 | renderRoot(root, isYieldy)
227 | finishedWork = root.finishedWork
228 | if (finishedWork !== null) {
229 | completeRoot(root, finishedWork)
230 | }
231 | }
232 | } else {
233 | let finishedWork = root.finishedWork
234 | if (finishedWork !== null) {
235 | completeRoot(root, finishedWork)
236 | } else {
237 | root.finishedWork = null
238 | const isYieldy = true
239 | renderRoot(root, isYieldy)
240 | finishedWork = root.finishedWork
241 | if (finishedWork !== null) {
242 | if (!shouldYield()) {
243 | completeRoot(root, finishedWork)
244 | } else {
245 | root.finishedWork = finishedWork
246 | }
247 | }
248 | }
249 | }
250 | isRendering = false
251 | }
252 |
253 | function createWorkInProgress(current, pendingProps, expirationTime) {
254 | let workInProgress = current.alternate
255 | if (workInProgress === null) {
256 | workInProgress = new FiberNode(current.tag, pendingProps)
257 | workInProgress.type = current.type
258 | workInProgress.stateNode = current.stateNode
259 | workInProgress.alternate = current
260 | current.alternate = workInProgress
261 | } else {
262 | workInProgress.pendingProps = pendingProps
263 | workInProgress.effectTag = NoEffect
264 | workInProgress.nextEffect = null
265 | workInProgress.firstEffect = null
266 | workInProgress.lastEffect = null
267 | }
268 | if (pendingProps !== current.pendingProps) {
269 | workInProgress.expirationTime = expirationTime
270 | } else {
271 | workInProgress.expirationTime = current.expirationTime
272 | }
273 | workInProgress.child = current.child
274 | workInProgress.memoizedProps = current.memoizedProps
275 | workInProgress.memoizedState = current.memoizedState
276 | workInProgress.updateQueue = current.updateQueue
277 | workInProgress.sibling = current.sibling
278 | return workInProgress
279 | }
280 |
281 | function renderRoot (root, isYieldy) {
282 | isWorking = true
283 | const expirationTime = root.expirationTime
284 | if (expirationTime !== nextRenderExpirationTime || nextUnitOfWork === null) {
285 | nextRenderExpirationTime = expirationTime
286 | nextUnitOfWork = createWorkInProgress(root.current, null, nextRenderExpirationTime)
287 | }
288 | do {
289 | try {
290 | workLoop(isYieldy)
291 | } catch (thrownValue) {
292 | const sourceFiber = nextUnitOfWork
293 | const returnFiber = sourceFiber.return
294 | throwException(root, returnFiber, sourceFiber, thrownValue, nextRenderExpirationTime)
295 | nextUnitOfWork = completeUnitOfWork(sourceFiber)
296 | continue
297 | }
298 | break
299 | } while (true)
300 | isWorking = false
301 | if (nextUnitOfWork !== null) {
302 | return
303 | }
304 | root.finishedWork = root.current.alternate
305 | }
306 |
307 | function throwException(root, returnFiber, sourceFiber, value, renderExpirationTime) {
308 | sourceFiber.effectTag |= Incomplete
309 | sourceFiber.firstEffect = sourceFiber.lastEffect = null
310 | if (
311 | value !== null &&
312 | typeof value === 'object' &&
313 | typeof value.then === 'function'
314 | ) {
315 | const thenable = value
316 | let workInProgress = returnFiber
317 | do {
318 | if (workInProgress.tag === SuspenseComponent) {
319 | const onResolve = retrySuspendedRoot.bind(
320 | null,
321 | root,
322 | workInProgress
323 | )
324 | thenable.then(onResolve)
325 | workInProgress.expirationTime = renderExpirationTime
326 | return
327 | }
328 | workInProgress = workInProgress.return
329 | } while (workInProgress !== null)
330 | }
331 | }
332 |
333 | function retrySuspendedRoot (root, fiber) {
334 | const currentTime = requestCurrentTime()
335 | const retryTime = computeExpirationForFiber(currentTime)
336 | root.expirationTime = retryTime
337 | scheduleWorkToRoot(fiber, retryTime)
338 | requestWork(root, root.expirationTime)
339 | }
340 |
341 | function workLoop (isYieldy) {
342 | if (!isYieldy) {
343 | while (nextUnitOfWork !== null) {
344 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
345 | }
346 | } else {
347 | while (nextUnitOfWork !== null && !shouldYield()) {
348 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
349 | }
350 | }
351 | }
352 |
353 | function performUnitOfWork (workInProgress) {
354 | const current = workInProgress.alternate
355 | let next = null
356 | next = beginWork(current, workInProgress, nextRenderExpirationTime)
357 | if (next === null) {
358 | next = completeUnitOfWork(workInProgress)
359 | }
360 | return next
361 | }
362 |
363 | function beginWork (current, workInProgress, renderExpirationTime) {
364 | workInProgress.expirationTime = NoWork
365 | const Component = workInProgress.type
366 | const unresolvedProps = workInProgress.pendingProps
367 | switch (workInProgress.tag) {
368 | case ClassComponent: {
369 | return updateClassComponent(current, workInProgress, Component, unresolvedProps, renderExpirationTime)
370 | }
371 | case HostRoot: {
372 | return updateHostRoot(current, workInProgress, renderExpirationTime)
373 | }
374 | case HostComponent: {
375 | return updateHostComponent(current, workInProgress, renderExpirationTime)
376 | }
377 | case SuspenseComponent: {
378 | return updateSuspenseComponent(current, workInProgress, renderExpirationTime)
379 | }
380 | default:
381 | throw new Error('unknown unit of work tag')
382 | }
383 | }
384 |
385 | function updateSuspenseComponent (current, workInProgress, renderExpirationTime) {
386 | const nextProps = workInProgress.pendingProps
387 | const nextDidTimeout = (workInProgress.effectTag & DidCapture) !== NoEffect
388 | const nextChildren = nextDidTimeout ? nextProps.fallback : nextProps.children
389 | workInProgress.memoizedProps = nextProps
390 | workInProgress.memoizedState = nextDidTimeout
391 | reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime)
392 | return workInProgress.child
393 | }
394 |
395 | function get(key) {
396 | return key._reactInternalFiber
397 | }
398 |
399 | function set(key, value) {
400 | key._reactInternalFiber = value
401 | }
402 |
403 | const classComponentUpdater = {
404 | enqueueSetState: function (inst, payload) {
405 | const fiber = get(inst)
406 | const currentTime = requestCurrentTime()
407 | const expirationTime = computeExpirationForFiber(currentTime)
408 | const update = createUpdate()
409 | update.payload = payload
410 | enqueueUpdate(fiber, update)
411 | scheduleWork(fiber, expirationTime)
412 | }
413 | }
414 | function adoptClassInstance (workInProgress, instance) {
415 | instance.updater = classComponentUpdater
416 | workInProgress.stateNode = instance
417 | set(instance, workInProgress)
418 | }
419 |
420 | function constructClassInstance (workInProgress, ctor, props) {
421 | let instance = new ctor(props)
422 | workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state : null
423 | adoptClassInstance(workInProgress, instance)
424 | return instance
425 | }
426 |
427 | function applyDerivedStateFromProps (workInProgress, getDerivedStateFromProps, nextProps) {
428 | const prevState = workInProgress.memoizedState
429 | const partialState = getDerivedStateFromProps(nextProps, prevState)
430 | const memoizedState = partialState === null || partialState === undefined ? prevState : Object.assign({}, prevState, partialState)
431 | workInProgress.memoizedState = memoizedState
432 | const updateQueue = workInProgress.updateQueue
433 | if (updateQueue !== null && workInProgress.expirationTime === NoWork) {
434 | updateQueue.baseState = memoizedState
435 | }
436 | }
437 |
438 | function mountClassInstance(workInProgress, ctor, newProps) {
439 | let instance = workInProgress.stateNode
440 | instance.props = newProps
441 | instance.state = workInProgress.memoizedState
442 | const updateQueue = workInProgress.updateQueue
443 | if (updateQueue !== null) {
444 | processUpdateQueue(workInProgress, updateQueue)
445 | instance.state = workInProgress.memoizedState
446 | }
447 | const getDerivedStateFromProps = ctor.getDerivedStateFromProps
448 | if (typeof getDerivedStateFromProps === 'function') {
449 | applyDerivedStateFromProps(workInProgress, getDerivedStateFromProps, newProps)
450 | instance.state = workInProgress.memoizedState
451 | }
452 | }
453 |
454 | function checkShouldComponentUpdate(workInProgress, newProps, newState) {
455 | const instance = workInProgress.stateNode
456 | if (typeof instance.shouldComponentUpdate === 'function') {
457 | const shouldUpdate = instance.shouldComponentUpdate(newProps, newState)
458 | return shouldUpdate
459 | }
460 | return true
461 | }
462 |
463 | function updateClassInstance (current, workInProgress, ctor, newProps) {
464 | const instance = workInProgress.stateNode
465 | const oldProps = workInProgress.memoizedProps
466 | instance.props = oldProps
467 | const oldState = workInProgress.memoizedState
468 | let newState = instance.state = oldState
469 | let updateQueue = workInProgress.updateQueue
470 | if (updateQueue !== null) {
471 | processUpdateQueue(
472 | workInProgress,
473 | updateQueue
474 | )
475 | newState = workInProgress.memoizedState
476 | }
477 | if (oldProps === newProps && oldState === newState) {
478 | return false
479 | }
480 | const getDerivedStateFromProps = ctor.getDerivedStateFromProps
481 | if (typeof getDerivedStateFromProps === 'function') {
482 | applyDerivedStateFromProps(workInProgress, getDerivedStateFromProps, newProps)
483 | newState = workInProgress.memoizedState
484 | }
485 | const shouldUpdate = checkShouldComponentUpdate(workInProgress, newProps, newState)
486 | if (shouldUpdate) {
487 | if (typeof instance.componentDidUpdate === 'function') {
488 | workInProgress.effectTag |= Update
489 | }
490 | }
491 | instance.props = newProps
492 | instance.state = newState
493 | return shouldUpdate
494 | }
495 |
496 | function updateClassComponent (current, workInProgress, Component, nextProps, renderExpirationTime) {
497 | let shouldUpdate
498 | if (current === null) {
499 | constructClassInstance(workInProgress, Component, nextProps)
500 | mountClassInstance(workInProgress, Component, nextProps)
501 | shouldUpdate = true
502 | } else {
503 | shouldUpdate = updateClassInstance(current, workInProgress, Component, nextProps)
504 | }
505 | return finishClassComponent(current, workInProgress, shouldUpdate, renderExpirationTime)
506 | }
507 |
508 | function cloneChildFibers(workInProgress) {
509 | if (workInProgress.child === null) {
510 | return
511 | }
512 | let currentChild = workInProgress.child
513 | let newChild = createWorkInProgress(currentChild, currentChild.pendingProps, currentChild.expirationTime)
514 | workInProgress.child = newChild
515 | newChild.return = workInProgress
516 | while (currentChild.sibling !== null) {
517 | currentChild = currentChild.sibling
518 | newChild = newChild.sibling = createWorkInProgress(currentChild, currentChild.pendingProps, currentChild.expirationTime)
519 | newChild.return = workInProgress
520 | }
521 | newChild.sibling = null
522 | }
523 |
524 | function finishClassComponent (current, workInProgress, shouldUpdate, renderExpirationTime) {
525 | if (!shouldUpdate) {
526 | cloneChildFibers(workInProgress)
527 | } else {
528 | const instance = workInProgress.stateNode
529 | const nextChildren = instance.render()
530 | reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime)
531 | memoizeState(workInProgress, instance.state)
532 | memoizeProps(workInProgress, instance.props)
533 | }
534 | return workInProgress.child
535 | }
536 |
537 | function reconcileChildren (current, workInProgress, nextChildren, renderExpirationTime) {
538 | if (current === null) {
539 | shouldTrackSideEffects = false
540 | workInProgress.child = reconcileChildFibers(workInProgress, null, nextChildren, renderExpirationTime)
541 | } else {
542 | shouldTrackSideEffects = true
543 | workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderExpirationTime)
544 | }
545 | }
546 |
547 | function reconcileChildFibers(returnFiber, currentFirstChild, newChild, expirationTime) {
548 | if (newChild) {
549 | const childArray = Array.isArray(newChild) ? newChild : [newChild]
550 | return reconcileChildrenArray(returnFiber, currentFirstChild, childArray, expirationTime)
551 | } else {
552 | return null
553 | }
554 | }
555 |
556 | function createFiberFromElement (element, expirationTime) {
557 | let fiber
558 | const type = element.type
559 | const pendingProps = element.props
560 | let fiberTag
561 | if (typeof type === 'function') {
562 | fiberTag = ClassComponent
563 | } else if (typeof type === 'string') {
564 | fiberTag = HostComponent
565 | }else {
566 | fiberTag = SuspenseComponent
567 | }
568 | fiber = new FiberNode(fiberTag, pendingProps)
569 | fiber.type = type
570 | fiber.expirationTime = expirationTime
571 | return fiber
572 | }
573 |
574 | function useFiber (fiber, pendingProps, expirationTime) {
575 | let clone = createWorkInProgress(fiber, pendingProps, expirationTime)
576 | clone.sibling = null
577 | return clone
578 | }
579 |
580 | function createChild (returnFiber, newChild, expirationTime) {
581 | if (typeof newChild === 'object' && newChild !== null) {
582 | let created = createFiberFromElement(newChild, expirationTime)
583 | created.return = returnFiber
584 | return created
585 | }
586 | return null
587 | }
588 |
589 | function updateElement (returnFiber, current, element, expirationTime) {
590 | if (current !== null && current.type === element.type) {
591 | const existing = useFiber(current, element.props, expirationTime)
592 | existing.return = returnFiber
593 | return existing
594 | } else {
595 | const created = createFiberFromElement(element, expirationTime)
596 | created.return = returnFiber
597 | return created
598 | }
599 | }
600 |
601 | function updateSlot (returnFiber, oldFiber, newChild, expirationTime) {
602 | if (typeof newChild === 'object' && newChild !== null) {
603 | return updateElement(returnFiber, oldFiber, newChild, expirationTime)
604 | }
605 | return null
606 | }
607 |
608 | function deleteChild (returnFiber, childToDelete) {
609 | const last = returnFiber.lastEffect
610 | if (last !== null) {
611 | last.nextEffect = childToDelete
612 | returnFiber.lastEffect = childToDelete
613 | } else {
614 | returnFiber.firstEffect = returnFiber.lastEffect = childToDelete
615 | }
616 | childToDelete.nextEffect = null
617 | childToDelete.effectTag = Deletion
618 | }
619 |
620 | function reconcileChildrenArray (returnFiber, currentFirstChild, newChildren, expirationTime) {
621 | let resultingFirstChild = null
622 | let previousNewFiber = null
623 | let oldFiber = currentFirstChild
624 | let newIdx = 0
625 | for (; oldFiber !== null && newIdx < newChildren.length; newIdx ++) {
626 | let newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], expirationTime)
627 | if (shouldTrackSideEffects) {
628 | if (oldFiber && newFiber.alternate === null) {
629 | deleteChild(returnFiber, oldFiber)
630 | newFiber.effectTag = Placement
631 | }
632 | }
633 | if (resultingFirstChild === null) {
634 | resultingFirstChild = newFiber
635 | } else {
636 | previousNewFiber.sibling = newFiber
637 | }
638 | previousNewFiber = newFiber
639 | oldFiber = oldFiber.sibling
640 | }
641 | if (oldFiber === null) {
642 | for (; newIdx < newChildren.length; newIdx++) {
643 | let _newFiber = createChild(returnFiber, newChildren[newIdx], expirationTime)
644 | if (shouldTrackSideEffects && _newFiber.alternate === null) {
645 | _newFiber.effectTag = Placement
646 | }
647 | if (resultingFirstChild === null) {
648 | resultingFirstChild = _newFiber
649 | } else {
650 | previousNewFiber.sibling = _newFiber
651 | }
652 | previousNewFiber = _newFiber
653 | }
654 | return resultingFirstChild
655 | }
656 | }
657 |
658 | function memoizeProps(workInProgress, nextProps) {
659 | workInProgress.memoizedProps = nextProps
660 | }
661 |
662 | function memoizeState(workInProgress, nextState) {
663 | workInProgress.memoizedState = nextState
664 | }
665 |
666 | function updateHostRoot (current, workInProgress, renderExpirationTime) {
667 | const updateQueue = workInProgress.updateQueue
668 | const prevState = workInProgress.memoizedState
669 | const prevChildren = prevState !== null ? prevState.element : null
670 | processUpdateQueue(workInProgress, updateQueue)
671 | const nextState = workInProgress.memoizedState
672 | const nextChildren = nextState.element
673 | if (nextChildren === prevChildren) {
674 | cloneChildFibers(workInProgress)
675 | return workInProgress.child
676 | }
677 | reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime)
678 | return workInProgress.child
679 | }
680 |
681 | function updateHostComponent (current, workInProgress, renderExpirationTime) {
682 | const nextProps = workInProgress.pendingProps
683 | let nextChildren = nextProps.children
684 | const isDirectTextChild = shouldSetTextContent(nextProps)
685 | if (isDirectTextChild) {
686 | nextChildren = null
687 | }
688 | reconcileChildren(current, workInProgress, nextChildren, renderExpirationTime)
689 | memoizeProps(workInProgress, nextProps)
690 | return workInProgress.child
691 | }
692 |
693 | function markUpdate(workInProgress) {
694 | workInProgress.effectTag |= Update
695 | }
696 |
697 | function appendAllChildren (parent, workInProgress) {
698 | let node = workInProgress.child
699 | while (node !== null) {
700 | if (node.tag === HostComponent) {
701 | appendInitialChild(parent, node.stateNode)
702 | } else if (node.child !== null) {
703 | node.child.return = node
704 | node = node.child
705 | continue
706 | }
707 | if (node === workInProgress) {
708 | return
709 | }
710 | while (node.sibling === null) {
711 | if (node.return === null || node.return === workInProgress) {
712 | return
713 | }
714 | node = node.return
715 | }
716 | node.sibling.return = node.return
717 | node = node.sibling
718 | }
719 | }
720 |
721 | function completeWork (current, workInProgress) {
722 | const newProps = workInProgress.pendingProps
723 | switch(workInProgress.tag) {
724 | case ClassComponent: {
725 | break
726 | }
727 | case HostRoot: {
728 | break
729 | }
730 | case HostComponent: {
731 | const type = workInProgress.type
732 | if (current !== null && workInProgress.stateNode != null) {
733 | const oldProps = current.memoizedProps
734 | const updatePayload = prepareUpdate(oldProps, newProps)
735 | workInProgress.updateQueue = updatePayload
736 | if (updatePayload) {
737 | markUpdate(workInProgress)
738 | }
739 | } else {
740 | const _instance = createInstance(type, newProps, workInProgress)
741 | appendAllChildren(_instance, workInProgress)
742 | finalizeInitialChildren(_instance, newProps)
743 | workInProgress.stateNode = _instance
744 | }
745 | break
746 | }
747 | case SuspenseComponent: {
748 | break
749 | }
750 | default: {
751 | throw new Error('Unknown unit of work tag')
752 | }
753 | }
754 | return null
755 | }
756 |
757 | function completeUnitOfWork (workInProgress) {
758 | while (true) {
759 | const current = workInProgress.alternate
760 | const returnFiber = workInProgress.return
761 | const siblingFiber = workInProgress.sibling
762 | if ((workInProgress.effectTag & Incomplete) === NoEffect) {
763 | completeWork(current, workInProgress)
764 | if (returnFiber !== null &&
765 | (returnFiber.effectTag & Incomplete) === NoEffect) {
766 | if (returnFiber.firstEffect === null) {
767 | returnFiber.firstEffect = workInProgress.firstEffect
768 | }
769 | if (workInProgress.lastEffect !== null) {
770 | if (returnFiber.lastEffect !== null) {
771 | returnFiber.lastEffect.nextEffect = workInProgress.firstEffect
772 | }
773 | returnFiber.lastEffect = workInProgress.lastEffect
774 | }
775 | const effectTag = workInProgress.effectTag
776 | if (effectTag >= Placement) {
777 | if (returnFiber.lastEffect !== null) {
778 | returnFiber.lastEffect.nextEffect = workInProgress
779 | } else {
780 | returnFiber.firstEffect = workInProgress
781 | }
782 | returnFiber.lastEffect = workInProgress
783 | }
784 | }
785 | if (siblingFiber !== null) {
786 | return siblingFiber
787 | } else if (returnFiber !== null) {
788 | workInProgress = returnFiber
789 | continue
790 | } else {
791 | return null
792 | }
793 | } else {
794 | if (workInProgress.tag === SuspenseComponent) {
795 | const effectTag = workInProgress.effectTag
796 | workInProgress.effectTag = effectTag & ~Incomplete | DidCapture
797 | return workInProgress
798 | }
799 | if (returnFiber !== null) {
800 | returnFiber.firstEffect = returnFiber.lastEffect = null
801 | returnFiber.effectTag |= Incomplete
802 | }
803 | if (siblingFiber !== null) {
804 | return siblingFiber
805 | } else if (returnFiber !== null) {
806 | workInProgress = returnFiber
807 | continue
808 | } else {
809 | return null
810 | }
811 | }
812 | }
813 | }
814 |
815 | function completeRoot(root, finishedWork) {
816 | root.finishedWork = null
817 | scheduledRoot = null
818 | commitRoot(root, finishedWork)
819 | }
820 |
821 | function getHostParentFiber(fiber) {
822 | let parent = fiber.return
823 | while (parent !== null) {
824 | if (isHostParent(parent)) {
825 | return parent
826 | }
827 | parent = parent.return
828 | }
829 | }
830 |
831 | function isHostParent(fiber) {
832 | return fiber.tag === HostComponent || fiber.tag === HostRoot
833 | }
834 |
835 | function commitPlacement (finishedWork) {
836 | const parentFiber = getHostParentFiber(finishedWork)
837 | const parent = parentFiber.tag === HostRoot ? parentFiber.stateNode.containerInfo : parentFiber.stateNode
838 | let node = finishedWork
839 | while (true) {
840 | if (node.tag === HostComponent) {
841 | appendChildToContainer(parent, node.stateNode)
842 | } else if (node.child !== null) {
843 | node.child.return = node
844 | node = node.child
845 | continue
846 | }
847 | if (node === finishedWork) {
848 | return
849 | }
850 | while (node.sibling === null) {
851 | if (node.return === null || node.return === finishedWork) {
852 | return
853 | }
854 | node = node.return
855 | }
856 | node.sibling.return = node.return
857 | node = node.sibling
858 | }
859 | }
860 |
861 | function commitWork (finishedWork) {
862 | switch (finishedWork.tag) {
863 | case HostRoot:
864 | case ClassComponent: {
865 | return
866 | }
867 | case HostComponent: {
868 | const instance = finishedWork.stateNode
869 | if (instance != null) {
870 | const updatePayload = finishedWork.updateQueue
871 | finishedWork.updateQueue = null
872 | if (updatePayload !== null) {
873 | commitUpdate(instance, updatePayload)
874 | }
875 | }
876 | return
877 | }
878 | case SuspenseComponent: {
879 | return
880 | }
881 | default: {
882 | throw new Error('This unit of work tag should not have side-effects')
883 | }
884 | }
885 | }
886 |
887 | function commitUnmount (current) {
888 | if (current.tag === ClassComponent) {
889 | const instance = current.stateNode
890 | if (typeof instance.componentWillUnmount === 'function') {
891 | instance.props = current.memoizedProps
892 | instance.state = current.memoizedState
893 | instance.componentWillUnmount()
894 | }
895 | }
896 | }
897 |
898 | function commitNestedUnmounts (root) {
899 | let node = root
900 | while (true) {
901 | commitUnmount(node)
902 | if (node.child !== null) {
903 | node.child.return = node
904 | node = node.child
905 | continue
906 | }
907 | if (node === root) {
908 | return
909 | }
910 | while (node.sibling === null) {
911 | if (node.return === null || node.return === root) {
912 | return
913 | }
914 | node = node.return
915 | }
916 | node.sibling.return = node.return
917 | node = node.sibling
918 | }
919 | }
920 |
921 | function commitDeletion (current) {
922 | const parentFiber = getHostParentFiber(current)
923 | const parent = parentFiber.tag === HostRoot ? parentFiber.stateNode.containerInfo : parentFiber.stateNode
924 | let node = current
925 | while (true) {
926 | if (node.tag === HostComponent) {
927 | commitNestedUnmounts(node)
928 | removeChildFromContainer(parent, node.stateNode)
929 | } else {
930 | commitUnmount(node)
931 | if (node.child !== null) {
932 | node.child.return = node
933 | node = node.child
934 | continue
935 | }
936 | }
937 | if (node === current) {
938 | break
939 | }
940 | while (node.sibling === null) {
941 | if (node.return === null || node.return === current) {
942 | break
943 | }
944 | node = node.return
945 | }
946 | node.sibling.return = node.return
947 | node = node.sibling
948 | }
949 | current.return = null
950 | current.child = null
951 | if (current.alternate) {
952 | current.alternate.child = null
953 | current.alternate.return = null
954 | }
955 | }
956 |
957 | function commitAllHostEffects (firstEffect) {
958 | let nextEffect = firstEffect
959 | while (nextEffect !== null) {
960 | const effectTag = nextEffect.effectTag
961 | switch(effectTag & (Placement | Update | Deletion)) {
962 | case Placement: {
963 | commitPlacement(nextEffect)
964 | nextEffect.effectTag &= ~Placement
965 | break
966 | }
967 | case Update: {
968 | commitWork(nextEffect)
969 | break
970 | }
971 | case Deletion: {
972 | commitDeletion(nextEffect)
973 | break
974 | }
975 | }
976 | nextEffect = nextEffect.nextEffect
977 | }
978 | }
979 |
980 | function commitBeforeMutationLifeCycles (firstEffect) {
981 | let nextEffect = firstEffect
982 | while (nextEffect !== null) {
983 | if (nextEffect.tag === ClassComponent) {
984 | const instance = nextEffect.stateNode
985 | const getSnapshotBeforeUpdate = nextEffect.stateNode.getSnapshotBeforeUpdate
986 | if (typeof getSnapshotBeforeUpdate === 'function') {
987 | const current = nextEffect.alternate
988 | const prevProps = current.memoizedProps
989 | const prevState = current.memoizedState
990 | instance.props = nextEffect.memoizedProps
991 | instance.state = nextEffect.memoizedState
992 | const snapshot = getSnapshotBeforeUpdate(prevProps, prevState)
993 | instance.__reactInternalSnapshotBeforeUpdate = snapshot
994 | }
995 | }
996 | nextEffect = nextEffect.nextEffect
997 | }
998 | }
999 |
1000 | function commitAllLifeCycles (firstEffect) {
1001 | let nextEffect = firstEffect
1002 | while (nextEffect !== null) {
1003 | if (nextEffect.tag === ClassComponent) {
1004 | const instance = nextEffect.stateNode
1005 | const componentDidMount = instance.componentDidMount
1006 | const componentDidUpdate = instance.componentDidUpdate
1007 | const current = nextEffect.alternate
1008 | if (current === null) {
1009 | if (typeof componentDidMount === 'function') {
1010 | instance.props = nextEffect.memoizedProps
1011 | instance.state = nextEffect.memoizedState
1012 | instance.componentDidMount()
1013 | }
1014 | } else {
1015 | if (typeof componentDidUpdate === 'function') {
1016 | const prevProps = current.memoizedProps
1017 | const prevState = current.memoizedState
1018 | instance.props = nextEffect.memoizedProps
1019 | instance.state = nextEffect.memoizedState
1020 | instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate)
1021 | }
1022 | }
1023 | }
1024 | nextEffect = nextEffect.nextEffect
1025 | }
1026 | }
1027 |
1028 | function commitRoot(root, finishedWork) {
1029 | isWorking = true
1030 | isCommitting = true
1031 | root.expirationTime = NoWork
1032 | const firstEffect = finishedWork.firstEffect
1033 | commitBeforeMutationLifeCycles(firstEffect)
1034 | commitAllHostEffects(firstEffect)
1035 | root.current = finishedWork
1036 | commitAllLifeCycles(firstEffect)
1037 | isCommitting = false
1038 | isWorking = false
1039 | }
1040 |
1041 | function dispatchEventWithBatch (nativeEvent) {
1042 | const type = nativeEvent.type
1043 | let previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates
1044 | let previousIsBatchingUpdates = isBatchingUpdates
1045 | let previousIsDispatchControlledEvent = isDispatchControlledEvent
1046 | if (type === 'change') {
1047 | isDispatchControlledEvent = true
1048 | }
1049 | if (isInteractiveEvent(type)) {
1050 | isBatchingInteractiveUpdates = true
1051 | }
1052 | isBatchingUpdates = true
1053 |
1054 | try {
1055 | return dispatchEvent(nativeEvent)
1056 | } finally {
1057 | isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates
1058 | isBatchingUpdates = previousIsBatchingUpdates
1059 | if (!isBatchingUpdates && !isRendering) {
1060 | if (isDispatchControlledEvent) {
1061 | isDispatchControlledEvent = previousIsDispatchControlledEvent
1062 | if (scheduledRoot) {
1063 | performSyncWork()
1064 | }
1065 | } else {
1066 | if (scheduledRoot) {
1067 | scheduleCallbackWithExpirationTime(scheduledRoot, scheduledRoot.expirationTime)
1068 | }
1069 | }
1070 | }
1071 | }
1072 | }
1073 |
1074 | function dispatchEvent (nativeEvent) {
1075 | let listeners = []
1076 | const nativeEventTarget = nativeEvent.target || nativeEvent.srcElement
1077 | const targetInst = nativeEventTarget.internalInstanceKey
1078 | traverseTwoPhase(targetInst, accumulateDirectionalDispatches.bind(null, listeners), nativeEvent)
1079 | listeners.forEach(listener => listener(nativeEvent))
1080 | }
1081 |
1082 | function accumulateDirectionalDispatches (acc, inst, phase, nativeEvent) {
1083 | let type = nativeEvent.type
1084 | let registrationName = 'on' + type[0].toLocaleUpperCase() + type.slice(1)
1085 | if (phase === 'captured') {
1086 | registrationName = registrationName + 'Capture'
1087 | }
1088 | const stateNode = inst.stateNode
1089 | const props = stateNode.internalEventHandlersKey
1090 | const listener = props[registrationName]
1091 | if (listener) {
1092 | acc.push(listener)
1093 | }
1094 | }
1095 |
1096 | return {
1097 | createContainer,
1098 | updateContainer,
1099 | dispatchEventWithBatch
1100 | }
1101 | }
1102 |
1103 | export default Reconciler
--------------------------------------------------------------------------------