The model for handling priorities in React 16 is ExpirationTime, which uses a length of time to describe the priority of a task.

React 17, on the other hand, uses the Lane model to handle task priorities, which is able to cover more boundary conditions by assigning different priorities to a bit and manipulating the priorities through 31-bit bitwise operations. In short: a binary number is used to represent the priority of a task.

For the sake of completeness of understanding, this article starts with the three phases of the React architecture: schedule , diff and commit.

1. Schedule

1.1 Creating update tasks

An update task is created on first render and user-triggered event, assigned a priority, and placed in the fiber.updateQueue update pair and given to the Scheduler to schedule the update

fiber.updateQueue is a ring structure with a pending pointer to the last update. New update insertion process.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const pending = sharedQueue.pending
if (pending === null) {
  // 第一个 update 进入队列,创建一个环形结构
  update.next = update
} else {
  // 最新的update插入到首尾中间
  update.next = pending.next
  pending.next = update
}
// pending 指向最新的 update
sharedQueue.pending = update

The ring structure is created so that the first and last nodes can be found at once

1.2 Priority

For interval priority, react uses a binary operation to determine if a lane is in an interval, up to 31 bits, each bit being a lane.

For example, to determine if a lane is in an interval.

1
2
3
4
// 0b0000000001111111111111111000000 & 0b0000000000000000000000001000000 === 0b0000000000000000000000001000000
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {
  return (set & subset) === subset
}

Merge lane.

1
2
3
4
// 0b0000000000000000000000010000000 | 0b0000000000000000000000100000000 === 0b0000000000000000000000110000000
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
  return a | b
}

1.3 updateQueue execution

  • Iterate through the updateQueue chain to collect tasks in the current update task interval, i.e., calculate whether lane is in the current interval, and if not, put it into newFirstBaseUpdate.... ...newLastBaseUpdate to defer execution.
  • Execute update, i.e. calculate the new state by getStateFromUpdate, store the result in newState, and insert the remaining update after lastBaseUpdate.
  • When the updateQueue is finished, the final result is stored in the baseState

1.4 Concurrency through time slicing

By time slicing, i.e., the task is decomposed into multiple units of work. For each completed unit of work, determine if there is a high priority job, and if so, let the browser interrupt the rendering

The time slicing effect can be simply implemented using requestIdleCallback.

1
2
3
4
5
6
7
8
9
function workLoop(deadline: IdleDeadline) {
  // 如果存在空闲时间
  while (workInProgress && deadline.timeRemaining() > 0) {
    workInProgress = performUnitOfWork(workInProgress)
  }
}

// 当js线程空闲时执行
requestIdleCallback(workLoop)

To reduce commit execution (a user-aware process), react is designed to track fiber root, also known as progress root or wipRoot, and commit fiber to dom only once all work is done, i.e., when there is no next unit of work.

2. Diff

2.1 Building the tree

Generate element nodes with the CreateElement function

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/**
 * @param {string} type    HTML标签类型 或 函数组件
 * @param {object} props   具有JSX属性中的所有键和值
 * @param {string | array} children JSX子节点列表
 */
function CreateElement(type, props, ...children) {
  return {
    $$typeOf, // ReactElement, FragmentElement...
    tag, // ClassComponent, FunctionComponent, HostComponent...
    type, // "div", [[Function]], [[constructor]]...
    props: {
      ...props,
      children,
    },
    ...
  }
}

Common JSX nodes are the following

  • function components, which generate element nodes as

    1
    2
    3
    4
    5
    
    {
    $$typeOf: ReactElement,
    tag: FunctionComponent,
    type: 函数本身
    }
    
  • Class component, whose generated element node is.

    1
    2
    3
    4
    5
    
    {
    $$typeOf: ReactElement,
    tag: ClassComponent,
    type: class 构造函数
    }
    
  • A native label or text node whose generated element node is

    1
    2
    3
    4
    5
    
    {
    $$typeOf: ReactElement,
    tag: HostComponent,
    type: "div""text"
    }
    

2.1.1 Simulating the execution of the CreateElement function

The following function component should not be new to users who have developed react, so let’s use it as an example and learn

1
2
3
4
function App(props) {
  return <h1 title="el_title">Hi {props.name}</h1> // HTML标签类型
}
const element = <App name="foo" /> // FC

Convert to CreateElement function call

1
2
3
4
function App(props) {
 return CreateElement("h1", {title:"el_title"}, "Hi ", props.name)
}
const element = CreateElement(App, {name:"foo"})

Executed and generated element nodes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[
  {
    "type": App, // 函数组件本身
    "props": {
      "name": "foo"            // key-value
      "children": [] // 函数组件执行结果
    },
  {
    "type": "h1",
    "props": {
      "title": "el_title"            // key-value
      "children": ["Hi", props.name] // 注意喔,是数组类型
    }
  }
]

Note that the children of the function component comes from the result of the function instead of props, i.e. children = type(props)

In this process, the returned TreeNode tree structure is [{...} ,{...}], is a normal tree structure based on recursive traversal and cannot achieve breakpoint back, while the Fiber chain table is constructed with 3 pointers for each fiber node, linked to its first child node child, next sibling node sibling and parent node return, and each fiber will become a working unit

2.2 Updating, Deleting

When we need to implement update and delete nodes, i.e. call setState, we need to compare the elements received in the render function with the last FiberTree submitted to dom. Therefore, we need to save the reference currentRoot of the last submission to FiberTree and add an alternate attribute to each fiber, recording the old fiber submitted in the previous stage

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let currentRoot = null
function Render(el, container) {
  wipRoot = {
    alternate: currentRoot,
  }
}

function CommitRoot() {
  currentRoot = wipRoot
  wipRoot = null
}

Note that the child creation process is accompanied by a comparison, i.e., old fiber is compared with new fiber while creating fiber for the element’s children

  • If old fiber and new fiber have the same type, keep the dom node and update its props and set the tag effectTag to UPDATE.
  • If type is different and it is new fiber, it means to create a new DOM node and set the tag effectTag to PLACEMENT; if it is old fiber, you need to delete the node and set the tag effectTag to DELETION

To detect changes quickly, React uses key. This makes it faster to detect when a child element has changed its position in the array of elements key->fiber

3. commit phase

3.1 Create operation

Committing a create operation performs real dom generation and ref initialization.

1
2
3
const stateNode = document.createElement(fiber.type)
fiber.stateNode = stateNode // 保存dom节点
fiber.props.ref(stateNode) // 执行props中的ref函数,传入dom节点

3.2 Update operations

The update phase will set props to dom

1
2
3
const oldProps = fiber.alternate.props
const newProps = fiber.props
// 对比oldProps和newProps,找到变化的key value然后设置到dom

3.3 Replace operation

Use the old node to find the parent node, then replace the dom node with the new dom node

1
2
3
4
const parent = fiber.alternate.return.stateNode
const oldDom = fiber.alternate.stateNode
const newDom = fiber.stateNode
parent.replaceChild(newDom, oldDom)

3.4 Delete operation

The new node does not exist, indicating that the current node is deleted

1
2
const oldDom = fiber.alternate.stateNode
oldDom.remove()