2023-09-05
ReactJS
00
请注意,本文编写于 1049 天前,最后修改于 209 天前,其中某些信息可能已经过时。

目录

React Concept
Rapid Response
CPU lag
IO lag
React 15 Architecture
Reconciler协调器
Renderer渲染器
React new architecture
Scheduler
Reconciler
Mental Models - Algebraic Effect
What is Fiber?
Relationship between JSX and Fiber nodes.
Render phase
The main tasks of beginWork
completeWork
Commit Phase
Diff
Diff's bottleneck
Single-node Diff
Multi-node Diff
Demo
States update
Concurrent Mode
simple react handwriting

React Concept

React is prefered way to build large, responsive web applications in JavaScript.
ui = render(data) --> One-way data flow

Clearly, the key is a rapid response.

Rapid Response

We know that Javascript excuation and DOM rendering are mutatually exclusive, Javascript execution blocks dom rendering, and DOM rendering similarly blocks Javascript execution. So why are browsers designed this way?
Becasue it is so simple! Javascript was initial just a collection of scripts for handing form validation, with very simple design considerations. Imagine if Javascrip execution and DOM rendering were to occur parallel, what would the DOM state be when Javascript manipulates it multiple times? Which operation should be responsed first? Solving this problem would require introducing thread locks, and at a deeper level, transaction mechanisms would also be needed, marking the whole process quite complex.

What factors limit rapid response?

  • CPO stuttering: When encountering computationally intensive operations or insuficident device performance causing frame drops, resulting in lag.
  • IO stuttering: After sending a network request, the system can't response quickly because it needs to wait for data to be returned before further operations can be performed.

Why do browsers drop frames during rendering?
Browsers render based on vertial synchronization signals, which has a period of 16.67ms. If JS execution or rendering too long, You will miss a vertical synchronization signal and having to wait for next one, 216.67=33ms2 * 16.67= 33ms, It exceeds the minimum frame rate that human eyes can distinguish, and is identified as a drop frame.

How does React solve the above problems?

CPU lag

  • In the time it takes for a browser to render one frame, reserve some time for js thread; React use this time to update components.(We can see it at source code , the reserve time is 5ms)
  • When the alloted time is insufficient, React returns thread control to the browser to give it time to render the UI, while React waits for the next frame of the time to continue interrupted work.
  • This operation of breaking down long tasks into individual frames and executing small segments of the task at a time, like ants carrying food, is called time slicing.
javascript
// we can use `ReactDOM.unstable_createRoot` to open `Concurrent Mode` // ReactDOM.render(<App/>, rootEl); ReactDOM.unstable_createRoot(rootEl).render(<App/>);

In summary: The key to solve CPU bottlenecks is time slicing, and the key to time slicing is transforming synchronous updates into interruptible asynchronous updates.

IO lag

Network latency is something developers cannot control, give that network latency is an objective reality, how can we reduce the user's feeling of it.
React's answer is, Intergrating the results of human-computer interaction research into real-world UIs, Therefore react implemented function of Suspense and a suit of hooks useDeferredValue

React 15 Architecture

The React 15 architecture can be divided into two layers:

  • Reconciler(协调器)   Responsible for find changed components
  • Renderer(渲染器)   Responsible for rendering the changing components onto the page.

Reconciler协调器

We know that React can trigger updates using APIs such as this.setStatethis.forceUpdateReactDOM.render. Whenever an update occurs, the Reconciler performs the following tasks:

  • Call the render method of a function component or a class component to convert the returned JSX into Virtual DOM
  • Comparison of old and new VDOM
  • Find VDOM elements that changed in this update.
  • Notify the Renderer to render the changed VDOM onto the page.

Renderer渲染器

Since React supports cross-platform compatibility, different platforms have different Renderers

  • ReactDOM  web渲染
  • ReactNative 渲染APP原生组件
  • ReactTest  渲染出纯JS对象,仅用于测试
  • ReactArt   渲染到Canvas/SVG或VML(IE8)

Disadvantages of React 15 architecture

  • React 15 uses recursive updates, and once the recursive starts, it cannot be interrupted, When DOM is very complex, the interaction will become lag.

The React team has proposed a new solution, using interruptible asynchronous updates instead of synchronous updates.

React new architecture

The architure of React 16 is as follows

  • Scheduler(调度器)—— High-priority tasks are prioritized and enter the Reconciler
    • implement time slicing and scheduling priority(LAN Mode)
  • Reconciler(协调器)—— The component responsible for identify changes (render phase)
    • create or update Fiber(tag the coresponding fiber)generate an effectList linked list.
  • Renderer(渲染器)—— responsible for rendering the changing components onto the page. (commit phase)
    • Different platforms have differnt implementations, which process the effectList linked list to reflect side effects in real DOM.

As can be seen, React 16 added scheduler.

Scheduler

The key to solving the lag issue is to use interruptible asynchronous updates, So when can it be interrupted?
A mechanism is needed to determine if the browser has available time; in fact, some browsers already have this API
requestIdleCallback, However it suffers from compatibility and instability issues, and it does not support scheduling priorities
Therefore, React decided to implement its own schedule mechanism, which is the Scheduler.

Reconciler

The reconciler in React 15 uses recursion to process the VDOM, Recursive doesn't support interrupted updates, the DOM will be fully renderd during the update.

How to solve the problem of incomplete DOM updates?

Therefore, in React 16 the reconciler and renderer no longer work alternately, After the Scheduler hands over the task to Reconciler, Reconciler will mark changes to the VDOM (added/deleted/modified). The entire Scheduler and Reconciler process runs in memory. Only after all components have complete the Reconciler work will be handed over to the Renderer.

image.png

The steps in red box can be interrupted at any time.

  • Other higher-level tasks that need to be updated first.
  • There is no time remaining in current frame.

Since the work within the red box is performed in memory and does not update the DOM on the page, the user will not see an incompletely updated DOM even if the process is repeatedly interrupted.

Mental Models - Algebraic Effect

Fiber is a computer science term, translated into Chinese as 纤程, It, along with threads and coroutines, is a part of the program's execution process. We understands fibers and coroutines as manifestations or algebraic effect concept in javascript. In my opinion: React Fiber was designed to implement the algebraic effect . It is the unit for organizing the execution of fiber code.

  • A React core member said that they do in React is to implement of algebraic effect.
  • Algebraic effect is a conceptual in functional programming used to seperate side effects from function.
  • The first thing that comes to mind when thinking about ways to solve side effects is async/await, but async is contagious (The function calling it must also be async)
  • Thus, a grammer was created try...handle and performresume
javascript
function getPrice(id) { const price = perform id; return price; } function getTotalPirce(id1, id2) { const p1 = getPrice(id1); const p2 = getPrice(id2); return p1 + p2; } try { getTotalPrice('001', '002'); } handle (productId) { fetch(`xxx.com?id=${productId}`).then((res)=>{ resume with res.price }) } /* How to understand the code above? - the perform and handle are virtual syntax - When the code executes to the `perform` method, the execution of current function is paused and captured by `handle` method. - handle函数体会拿到productId参数获取数据之后resume价格 - resume会回到之前的perform暂停的地方并返回price - 这就完全把副作用分离到了getTotalPrice和getPrice之外。 The key process here is: perform pauses the function's execution. handle acquires execution control, and the resume give up execution control. */ // 上述代码是抽象的,实际代码如下 function usePrice(id){ useEffect(() => { fetch(`xxx.com?id=${productId}`).then((res) => res.price); }, []) } function TotalPrice({id1, id2}) { const price1 = usePrice(id1); const price2 = usePrice(id2); return <TotalPrice props={...}> }

Q: Why not use Generator?

  • Similar to async, it also has contagious.
  • The intermediate states of a generator are context-dependent.

If we only consider the interruption and continuation of a single task, the Generator can effectively implement asynchronous interruptible updates.

When high-priority requests are interrupted, a global variable needs to be introduced to store the intermediate state of current generator. Additionally, creating a new high-priority generator introduces new complexity.

How can we understand how can fiber architecture pratices (simulates or implements) algebraic effect?

  • Strictly speaking, React not support algebraic effects. You can use try...handle to simulate try...catch, and place throw error in a microtask. However, interruption and recovery can't be simulated.
  • But, React has fiber, After the update of this fiber is completed, the execution is handed back to the browser, allowing the browser to decide how to schedule it. (Interruption and resumption can be understood as continuing to execute JS after rendering a frame; the fiber will save the itermediate state. )

Suspense is also an extension of this concept.

  • throw a Promise, 拿到数据通过Promise回调结果,外层组件收到回调结果触发setData, 给人的感觉是实现了代数效果

image.png

What is Fiber?

Definition: React implements an internal state update mechanism. It supports different task priorities, and can reuse the previous intermediate state after resumption.

Functions:

  1. The reconciler in the React 15 architecture executes recursively and is called stack reconciler. The Reconciler in React 16 architecture is implemented based on Fiber and is called Fiber Reconciler.
  2. As a static data structure, each Fiber node corresponds to React elmement, storing the state of that component(The function needs to be deleted/ inserted into page/updated...);
  3. As a dynamic unit of work, each Fiber node stores the changes made to the component during this update, as well as the tasks to be performed(add, delete, update).

Q: How does React Fiber update the DOM?
Use double caching (the technique of building in memory and performing replacement directly is called double caching).
In React, there can be a maximum of two fiber trees at the same time. The one display on screen is current fiber tree, and the fiber tree being built in memory is called workInProgress Fiber true. They are connected via a alternate attribute.

  • fiberRootNode  The root node of the entire application.
  • rootFiber   <APP/> The root node of the component.

All fiber nodes have the alternate attribute, any update to any component will modify the alternate pointer.

JSX and Fiber

  1. Are JSX and Fiber nodes the same thing?
  2. Are React Component and React Element the same thing? What is their relationship of JSX?

React.createElement method translates JSX into React elements. This is the reason why we import React from 'react';

Relationship between JSX and Fiber nodes.

  • JSX is a data structure that describes the current content of the current component. It does not contain the relevant information (schedule, reconciler, render) required by the components. (The component's priority during updates, the component's state, and the tags attached to the component for the Render).
  • Fiber is more of an update mechanism.
  • During component mounting, the Reconciler generates the corresponding Fiber nodes for the component based on component context described in the JSX.
  • During the update process, the Reconciler compared the data stored in JSX and Fiber nodes for the component, and marks the Fiber nodes based on comparison results.

Render phase

The execution of reconciler corresponds to the render phase, which mainly generates the new workInProgressFiberTree
Although fiber reconciler is refactored from stack reconciler, both implement interruptible asynchronous recursion through traversal.

  • beginWork  Enter the current Fiber node, create child Fiber node.
  • completeWork Different processing logic is invoked for different fiber.effectTag tags

The main tasks of beginWork

  • During the mounting: except for fiberRootNode, current === null. Different types of child Fiber will be created based on different fiber.tag values.
  • During the updating: If current exist, the current node can be reused under certain conditions. This allows current.child to be cloned as workInProgress.child, without needing to create a new workInProgress.child

Summary:beginWork is to create Fiber node

tree.png

effectTag

  • The render phase works in memory, Only the task is completed, the Renderer will be notified of the DOM operations. The specific type of DOM operation to be performed is stored in fiber.effectTag

completeWork

Different processing logic is generated based on different fiber.tag

  • When checking update, we also need to consider workInProgress.stateNode != null ?(i.e., whether the fiber node has corresponding DOM node)
  • when updating, The Fiber node already has a corresponding DOM node, so there is no need to generate a DOM node. The main task is to process props, such as:
    • onClickonChange etc. callback function registration.
    • processing style and prop
    • processing DANGEROUSLY_SET_INNER_HTML prop
    • processing children prop
  • when mounting
    • Generate corresponding DOM nodes for the Fiber nodes.
    • Insert the descendent DOM nodes into the newly generated DOM node.
    • The process of handling props is similar to that of updateHostComponent in the update logic.

Summary: completeWork is processing the fiber nodes according to effectTag. image.png

effectList

Q:As the basis of DOM operation, the completeWork phase needs to find all Fiber nodes' effectTag and execute the corresponding operations for each effectTag in sequence. Does this mean we need to traverse the fiber tree again in the commit phase to find effectTag !== null Fiber nodes?
A: completeWork在上层函数completeUnitOfWork上维护了一个单向链表 effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。 类似appendAllChildren,在“归”阶段,所有effectTagFiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。

image.png

Commit Phase

commitRoot(root);

  • The rootFiber.firstEffect stores a singly linked list effectList containing Fiber nodes that need to have their side effects executed.
  • The updateQueue of these Fiber nodes stores the changed props.
  • The corresponding DOM operations for these side effects are performed at commit phase.
  • In addition, some lifecycle hooks (such as componentDidXXX) and hooks(such as useEffect) need to be executed during the commit phase.
  • The main work of commit phase(i.e. the Renderer workflow) consists of three parts:
  1. before mutation phase(before performing dom operations)
    1. The main tasks involve assigning value to variables and resetting states, as detailed below:
    2. handling DOM node rendering, deletion operations such as autoFocus and blur.
    3. getSnapshotBeforeUpdate
    4. useEffect
  2. mutation phase(performing dom operations)
    1. Reset text nodes based on ContentReset and effectTag.
    2. Update ref
    3. Process according to effectTag, the effectTag includes Placement , Update , Deletion and Hydrating.
  3. layout(after performing DOM operations)
    1. useEffect related processing
    2. Performance tracing related
    3. Some lifecycle hooks

Q: Why is the UNSAFE_ prefix added before componentWillXXX hooks starting from React 16?

In React updates, an update object is created each time an update is initiated. Mutiple Update Objects for the same component are stored in an updateQueue queue. Suppose a component updateQueue has four update calls, where the number represent priority.

javascript
baseState = ''; A1 - B2 - C1 - D2 // 为了保证更新的连贯性,第一个被跳过的update(B)和后面的update会作为第二次渲染的baseUpdate // 为BCD // 首次渲染后 baseState: '' Updates: [A1, C1] Result state: 'AC' // 第二次渲染,B在第一次渲染时被跳过, // 所以在他之后的C造成的渲染结果不会体现在第二次渲染的baseState中。 // 所以baseState为A而不是上次渲染的Result state AC。 // 这也是为了保证更新的连贯性 baseState: 'A' // 为了保证一致性,C不在 Updates: [B2, C1, D2] Result state: 'ABCD' // Updates里出现了两次C

Q:Why asynchronous scheduling?

Unlike componentDidMount and componentDidUpdate, the function passed to useEffect is called delayed after the browser has completed layout and rendering. This makes it suitable for many common side effect scenarios, such as setting up subscription and event handling, so operations that block the browser from updating the screen should not be performed in the function.

Prevent browser rendering from being blocked during synchronous execution.

Q:What's the operation with the highest time complexity in rendering the DOM?
getHostSibling(Get sibling DOM nodes)
When multiple operations are preformed sequentially under the same parent Fiber node, the complexity of the getHostSibling algorithm is exponential.
This is because Fiber nodes include more than HostComponent, so the fiber tree and the rendered DOM tree nodes are not in a one to one correspondence. finding a DOM node from a Fiber node may require traversing multiple levels.

javascript
function Item() { return <li><li>; } function App() { return ( <div> <Item/> </div> ) } ReactDOM.render(<App/>, document.getElementById('root')); // Fiber树 child child child child rootFiber -----> App -----> div -----> Item -----> li // DOM树 #root ---> div ---> li // 在div的子节点Item前加一个p function App() { return ( <div> <p></p> <Item/> </div> ) } // Fiber树 child child child rootFiber -----> App -----> div -----> p | sibling child | -------> Item -----> li // DOM树 #root ---> div ---> p | ---> li // 此时dom中p的兄弟节点是li // fiber中fiberP的兄弟节点是fiberItem, fiberItem的子节点才是li

Q:double caching switching execution time
After the mutation phase ends and before the layout phase begins.

  1. componentWillUnmount executes during the mutation phase. At this time, current Fiber tree still points to the previously updated Fiber tree, and the DOM retrieved within the lifecycle hooks is still the one from before the update.
  2. componentDidMount and componentDidUpdate will perform during layout phase. At this point , the current Fiber tree points to the updated fiber tree, and the dom retrieved within the lifecycle hooks is the updated fiber tree.

Diff

During render phase, for the components that are being updated, the current component is compared with Fiber nodes of last render(also known as diff algorithm), and the comparison results is used to generate a new fiber node.

the diffing algorithm

  1. Different types of elements:React uninstalls the original tree and generates a new tree, calling lifycycle functions.
  2. The same type elements
    1. Preserve DOM nodes and only compare attributes that have changed during the update.
  3. Compare similar component elements
    1. When a component is updated, the lifecyle function is called.( willMount, willUpdate, DidUpdate)
    2. Call render to perform diff.

Combing the render and commit phases, a DOM node can have a maximum of 4 associated nodes:

  1. current Fiber.   If the DOM node is already in the page, current Fiber represents the Fiber node corresponding to the DOM node;
  2. workInProgress Fiber.   If the DOM node will be rendered to the page in this update, workInProgress Fiber represents the Fiber node corresponding to the DOM node;
  3. The DOM node itself;
  4. JSX object

diff algorithm: Compare 1 and 4 to generate 2.

Diff's bottleneck

The diff operation itself incurs performance overhead. The React documentation states that even in state-of-the-art algorithms, the complexity of a complete comparison of two trees is O(n3)O(n^3). where nn is the number of elements in the tree; if this algorithm were used in React, the computation required to display 1000 elements would be in the billions. This overhead is simply too high;

To reduce algorithm complexity. React's diff has tree predefined limitations:

  1. The issue of element hierarchy movement is not considered;
  2. Tag replacement: Destroy descendant elements and recreate them
  3. The key indicates which child elements remain stable under different rendering conditions;

Single-node Diff

Based on the number of nodes at the same level, the Diff is divided into two categories:

  1. When newChild is of type object, number, or string, it means there is only one sibling node.
  2. When newChild is of type Array and there are multiple sibling nodes.

Single-node diff

  1. First, check the keys are the same, If the keys are the same, check if the types are the same. Only if they are the same can the dom node be reused;
  2. Deletion logic:
    1. When child !== null and keys are the same but the types are different, executing deleteRemainingChildren will mark the child and its sibling fibers as deleted;
    2. When child !== null and keys are different, only child tag is deleted;

Multi-node Diff

The approach provided by React team: 2 rounds of iteration.

  1. Process the update nodes;
  2. Process the non-upddated nodes;

First round of traversal --- Find reusable nodes (identified by their headers), lastIndex represents the index of last reusable node.

Second round of travelsal.

Let's first analyze the four possible outcomes after the first round of traversal.

  1. Both newChildren and oldFiber were traversed together.
    • ending
  2. The newChildren iteration was not completed, while the oldFiber iteration was completed
    • appending
  3. The newChildren iteration was completed, while the oldFiber iteration wasn't completed
    • deletion
  4. Neither newChildren nor oldFiber have been fully explored.
    • This will be the focus of our discussion.

How to handle updated notes? -- Mark whether the node has moved

txt
// before abcd // after acdb ===The first round of traversal begins=== a(after)vs a(before) The key remains the same and can be reused. At this point, the oldFiber corresponding to 'a'(the previous `a`) and has an index of 0 in the previous array (abcd), so `lastPlacedIndex = 0`; continue with the first round of traversal... c(after)vs b(before) the key changes, can't reuse, therefore, exit the first iteration, At this point `lastPlacedIndex === 0;` ===The first round of traversal ends=== ===The second round of traversal starts=== newChildren === cdb, Not used up, No need to delete old nodes. oldFiber === bcd, Not used up, No need to insert new nodes. Saving the remaining oldFiber(bcd) as a map // current oldFiber: bcd // current newChildren: cdb continue iterating through the remaining newChildren `key === c` exists in oldFiber const oldIndex = c(before).index; At this time `oldIndex === 2;` // 之前节点为 abcd,所以c.index === 2 Compare `oldIndex` with `lastPlacedIndex`; if `oldIndex >= lastPlacedIndex` This means the node is reusable and does not need to be moved. setting `lastPlacedIndex = oldIndex;` if `oldIndex < lastplacedIndex` 该可复用节点之前插入的位置索引小于这次更新需要插入的位置索引,This means the node needs to be moved to the right. In the example, `oldIndex 2 > lastPlacedIndex 0`, so lastPlacedIndex = 2; The position of node c remains unchanged Continue iterating through the remaining `newChildren` // current oldFiber :bd // current newChildren :db `key === d` exists in oldFiber const oldIndex = d(before).index; oldIndex 3 > lastPlacedIndex 2 // 之前节点为 abcd,所以d.index === 3 so lastPlacedIndex = 3; The position of node d remains unchanged Continue iterating through the remaining `newChildren` // current oldFiber :b // current newChildren :b `key === b` exists in oldFiber const oldIndex = b(before).index; oldIndex 1 < lastPlacedIndex 3 // 之前节点为 abcd,所以b.index === 1 Then node b needs to move to the right. ===The second round of traversal ends=== finally, nodes a, c and d did not move, while node b was marked as moved.

Let's revise the case and analyze it again

javascript
// before abcd // after dabc ===The first round of traversal begins=== d(after)vs a(before) key changed, can't be used, Break out of traversal ===The first round of traversal ends=== ===The second round of traversal begins=== newChildren === dabc, not used up, no need to delete old nodes. oldFiber === abcd, not used up, no need to insert new nodes. Save the remaining oldFiber(abcd) as a map continue iterating through the remaining newChildren // current oldFiber : abcd // current newChildren: dabc `key === d` exists in oldFiber const oldIndex = d(before).index; At this point oldIndex === 3; // 之前节点为 abcd,所以d.index === 3 compare oldIndex with lastPlacedIndex; oldIndex 3 > lastPlacedIndex 0 so `lastPlacedIndex = 3;` The position of node d remains unchanged continue iterating through the remaining newChildren // current oldFiber : abc // current newChildren : abc `key === a` exists in oldFiber const oldIndex = a(before).index; // 之前节点为 abcd,所以a.index === 0 At this point `oldIndex === 0;` compare oldIndex and lastPlacedIndex; oldIndex 0 < lastPlacedIndex 3 Then node a needs to move to the right. Continue iterating through the remaining newChildren // current oldFiber : bc // current newChildren : bc `key === b` exists in oldFiber const oldIndex = b(before).index; // 之前节点为 abcd,所以b.index === 1 At this point oldIndex === 1; compare oldIndex and lastPlacedIndex; oldIndex 1 < lastPlacedIndex 3 The node b needs to move to right Continue iterating through the remaining newChildren // current oldFiber : c // current newChildren : c key === c exists in oldFiber const oldIndex = c(before).index; // 之前节点为 abcd,所以c.index === 2 At this point `oldIndex === 2;` compre oldIndex and lastPlacedIndex; oldIndex 2 < lastPlacedIndex 3 Then node c needs to be moved to the right. ===The second round of traversal ends===

Summary: Set up the reference point to minimize the number of operations required to move nodes from the back to the front.

Demo

typescript
/** * react diff * 1. abcd -> acdb * 2. abcd -> dabc */ export function diffChildren(newStr: string, oldStr: string): string[] { // 第一轮循环 找出头部无变更的元素 let lastPlaceIndex = 0; const result: string[] = []; for (let i = 0; i < newStr.length; i++) { if (newStr[i] && oldStr[i] && newStr[i] === oldStr[i]) { lastPlaceIndex = i+1; } else { break; } } if ( lastPlaceIndex === newStr.length && lastPlaceIndex === oldStr.length ) { result.push("第一轮遍历结束,全部节点都可以复用"); return result; } if (lastPlaceIndex === oldStr.length) { result.push("第一轮遍历结束, 新children有剩余"); result.push(`将剩余的新节点${newStr.slice(lastPlaceIndex)}插入尾部`); return result; } if (lastPlaceIndex === newStr.length) { result.push("第一轮遍历结束, 旧children有剩余"); result.push(`将剩余的新节点${oldStr.slice(lastPlaceIndex)}删除`); return result; } result.push(`第一轮遍历结束新旧children都有剩余的情况`); // 第二轮循环 const restOfOld = [...oldStr.slice(lastPlaceIndex)]; const [map, deletionsMap] = restOfOld.reduce(([map1, map2], item, index) => { map1[item] = index + lastPlaceIndex; const map2Index = index + lastPlaceIndex + ''; map2[map2Index] = true; return [map1, map2]; }, [{}, {}] as [{[key: string]: number}, {[key: string]: boolean}]); const restOfNew = [...newStr.slice(lastPlaceIndex)]; for (let j = 0; j < restOfNew.length; j++) { const oldIndex = map[restOfNew[j]]; if(oldIndex !== undefined) { deletionsMap[oldIndex] = false; } if(oldIndex === undefined) { result.push(`将新元素${restOfNew[j]}插入到尾部`); lastPlaceIndex = Math.max(oldStr.length, lastPlaceIndex) + 1; } else if (oldIndex > lastPlaceIndex) { lastPlaceIndex = oldIndex; } else if (oldIndex < lastPlaceIndex) { result.push(`将${restOfNew[j]}移动到尾部`); } } Object.keys(deletionsMap) .filter(index => deletionsMap[index]) .forEach(index => { console.log(`删除${oldStr[index]}`) result.push(`删除${oldStr[index]}`); }) return result; } // diff.test.ts import { diffChildren } from "./diff"; import { describe, it, expect } from "vitest"; describe("diffChildren", () => { it("第一轮遍历结束,全部节点都可以复用", async () => { const result = diffChildren('abcd', 'abcd'); console.log(result); expect(result).toEqual(['第一轮遍历结束,全部节点都可以复用']) }); it("第一轮遍历结束,新children有剩余", () => { const result = diffChildren('abcd', 'abc'); console.log(result); expect(result).toEqual([ '第一轮遍历结束, 新children有剩余', '将剩余的新节点d插入尾部' ]) }) it("第一轮遍历结束,旧children有剩余", () => { const result = diffChildren('abc', 'abcd'); console.log(result); expect(result).toEqual([ '第一轮遍历结束, 旧children有剩余', '将剩余的新节点d删除' ]) }) it("元素移动:abcd --> acdb", () => { const result = diffChildren('acdb', 'abcd'); console.log(result); expect(result).toEqual([ '第一轮遍历结束新旧children都有剩余的情况', '将b移动到尾部' ]) }) it("元素移动:abcd --> dabc", () => { const result = diffChildren('dabc', 'abcd'); console.log(result); expect(result).toEqual([ '第一轮遍历结束新旧children都有剩余的情况', "将a移动到尾部", '将b移动到尾部', '将c移动到尾部', ]) }); it("有元素移动和新增元素:abcd --> dabce", () => { const result = diffChildren('dabce', 'abcd'); console.log(result); expect(result).toEqual([ '第一轮遍历结束新旧children都有剩余的情况', "将a移动到尾部", '将b移动到尾部', '将c移动到尾部', "将新元素e插入到尾部", ]); }); it("有元素移动和新增元素:abcd --> daebc", () => { const result = diffChildren('daebc', 'abcd'); console.log(result); expect(result).toEqual([ '第一轮遍历结束新旧children都有剩余的情况', "将a移动到尾部", "将新元素e插入到尾部", '将b移动到尾部', '将c移动到尾部', ]); }) it("有元素移动和新增元素:abcd --> dac", () => { const result = diffChildren('dac', 'abcd'); console.log(result); expect(result).toEqual([ '第一轮遍历结束新旧children都有剩余的情况', "将a移动到尾部", '将c移动到尾部', '删除b', ]); }); });

States update

In React, the operations that trigger state updates includes:

  • ReactDOM.render
  • this.setState
  • this.forceUpdate
  • useState
  • useReducer

Q: How can we integrate the same state management mechanism across different application scenarios?

Each time the state is updated, an object related to the updated state is created and stored, and is called Update, in the beginwork of render, a new state is obtained based on Update

image.png

Update approach, it can be compared to code version control.
Update and contact with Fiber:

  • Multi Update events on a fiber node will form a linked list and can be contained in a fiber.updateQueue
  • A Fiber node can have a maximum of two updateQueues at the same time:current fiber/workInProgress fiber

updateQueue

typescript
const queue: UpdateQueue<State> = { baseState: fiber.memoizedState, firstBaseUpdate: null, lastBaseUpdate: null, shared: { pending: null, }, effects: null, };
  • baseState:   The state of this fiber node before this update, Update calculates the updated state based on this state, You can think of baseState as the master branch in a mental model;
  • firstBaseUpdate and lastBaseUpdate:   The fiber node had saved the Update data prior to this update. It exists as a linked list, with the firstBashUpdate of the head and lastBaseUpdate at the tail. The reason why an Update exists within a fiber node before the Update is generated, is because some Updates have low priority and were skipped (when the state was calculated by the Update during the last render phase). You can draw an analogy between baseUpdate and commit (node D) upon which git rebase is based in a mental model.
  • shared.pending:   When an update is triggered, the generated Update will be stored in shared.pending, forming a singly linked circular list. 当由Update计算state时这个环会被剪开并连接在lastBaseUpdate后面,可以将shared.pending类比心智模型中本次需要提交的commit(节点ABC)。
  • effects:   Array. save update.callback !== null for Update;

Q: The render phase may be interrupted. How can we ensure the Update data of updateQueue is not lose?

During the render phase在, the shared.pending loop is cut and connected after updateQueue.lastBaseUpdate.
In fact, shared.pending is connected after workInProgress updateQueue.lastBaseUpdate and current updateQueue.lastBaseUpdate.
When the render phase is interrupted and then restarted, a workInProgress updateQueue will be cloned based on the current updateQueue. Since current updateQueue.lastBaseUpdate has already saved the previous Update, it will not be lost. Because the previous Update is store in workInProgress updateQueue.lastBaseUpdate, the update will not be lost even after the the workInProgress Fiber tree become the current Fiber tree.

Concurrent Mode

Architecture operation stategy--lane mode
Based on the current architecture, when an update is interrupted during operation and then resumes after a period of time, this is called an "asynchronous interruptible update"
When an update is interrupted during its execution, a new update will be started instead. We can say that the later update interrupt the previous update;
This is concept of priority: The later update has higher priority and will interrupted the previous update that is in progress.
How can multi priorities interrupt each other? Can priority be increased and decreased? What priority should this update be given?
This requires a model to control the relationships and behaviors between different priorities, which is lane mode.

Time slicing principle
The essence of time slicing is simulation implementation requestIdleCallback.
除去“浏览器重排/重绘”,下图是浏览器一帧中可以用于执行JS的时机。 一个task(宏任务) -- 队列中全部job(微任务) -- requestAnimationFrame -- 浏览器重排/重绘 -- requestIdleCallback

Scheduler的时间切片功能是通过task(宏任务)实现的。

  • setTimeout:最常见
  • MessageChannel:执行时机比setTimeout更早

所以Scheduler将需要被执行的回调函数作为MessageChannel的回调执行。如果当前宿主环境不支持MessageChannel,则使用setTimeout 在React的render阶段,开启Concurrent Mode时,每次遍历前,都会通过Scheduler提供的shouldYield方法判断是否需要中断遍历,使浏览器有时间渲染:

simple react handwriting

build-your-own-react


TODO
  • scheduler中使用了小顶堆
  • 调度实现使用了messageChannel
  • 在render阶段的reconciler中使用了fiber、update、链表这几种数据结构
  • lane模型使用了二进制掩码

为什么条件语句里面不能写hooks?

  • 如果我们写个多个hooks会创建一个链表, 如果hooks放在条件中,这些链表的顺序就可能不对了

legacy模式 与 concurrent模式的差异

legacy模式 使用ReactDOM.render()创建的应用 并发模式concurrent 使用ReactDOM.createRoot().render()创建的应用

  1. legacy模式下 useLayoutEffect与useEffect的差异
  2. 高优先级任务插队在两种模式下的差异
  3. setState差异

本文作者:郭敬文

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!