├── .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 | logo 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 | 2 | 3 | 4 | 5 | 6 | 7 | 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 | logo 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 | ![now error](Images/customRender_now.PNG) 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 | ![gif](Images/customRenderer.gif) 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 | ![lifecycles](Images/lifecycles.PNG) 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 | logo 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 | ![wrong](Images/event_wrong.PNG) 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 | ![click](Images/event_click.PNG) 358 | 359 | 可以看到触发了 computeInteractiveExpiration,而且只触发一次 scheduleCallbackWithExpirationTime。后面可能会因为耗尽了空闲时间而再次调用 scheduleCallbackWithExpirationTime。 360 | 361 | 在文本框中输入字符: 362 | ![change](Images/event_change.PNG) 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 | ![stackFrame](Images/Fiber_StackFrame.PNG) 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 | ![tree](Images/Fiber_Tree.PNG) 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 | ![suspense](Images/suspense.gif) 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 | ![suspense_flowchart](Images/suspense_flowchart.PNG) 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 | ![suspense](Images/suspense_simple.gif) 627 | 628 | [下一章](LifeCycles.md) -------------------------------------------------------------------------------- /Guide/Fiber_part2.md: -------------------------------------------------------------------------------- 1 | # Fiber 架构 2 | >本章的代码在Fiber分支中 3 | 4 | 在 CustomRenderer 章节,我们定义了一个简单组件,通过初次渲染和点击按钮,可以调试 React 的 mount 和 update 阶段。在 ReactReconciler 中的函数入口加上 console.log, 可以得到其触发函数的流程图。 5 | 6 | ![flowchart](Images/flowchart.PNG) 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 | ![gif](Images/customRenderer.gif) 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 --------------------------------------------------------------------------------