This article is based on the Preact class React library as a reference, the specific implementation code has been hosted on Github

1 Implementation Route

Create a virtual DOM. Create a virtual DOM. Create a virtual DOM

Then, it is necessary to map the virtual DOM to the real DOM. In the process of mapping, we will first briefly understand the basic flow of the DIFF algorithm, and then we can convert the basic flow into concrete code implementation, and use the simple version of the DIFF algorithm to convert the virtual DOM into the real DOM

After the completion of the initial rendering, must learn the diff algorithm reuse “node” * * * *, and then reuse node actually can also be subdivided into two parts, the single node is a part of the diff, this part is relatively simple, does not need to involve too much logic, the most troublesome is the reuse of child nodes, youdao interview questions you must be done, So why do we loop through lists with a key for each child node

By the time we’ve learned the first three sections, we’ve already implemented the React idea, so it’s all about adding new things to it. So the first thing to add is a function component and a class component, the hook of a function component and the setState of a class component. The implementation of the life cycle is not covered in this section, but how to render a function component and a class component to the page

React 17 and React 18 are different from React 17 and React 18. React 17 and React 18 are different from React 17 and React 18. React 17 and React 18 are different from React 17

After setState, the class component lifecycle is implemented. By the time the lifecycle implementation is complete, a small Class React framework will have been completed.

Finally, learn the implementation of context and hooks

The Concurrent mode of React18 explains the rationale, not the implementation

2 virtual dom

2.1 What is the Virtual DOM

The virtual DOM is actually a JS object, through which we describe a DOM structure

2.2 Why is the virtual DOM needed

Why do you need the virtual DOM? We can take a look at one of yuxi’s answers in Zhihu, and conclude that there is only one advantage of projects built with virtual DOM: “high maintainability”. Note that the benefits here are performance-independent, meaning that the virtual DOM does not provide any performance improvement. Let’s start by looking at how the oldest front-end projects implement page interaction

You have an initial DOM, you get the initial data through the interface, you manipulate the DOM to render the data to the page, and so on

The point of the framework, then, is that by describing the structure on the page using the virtual DOM, the developer just needs to modify the variable data, and the framework does the rest. The developer doesn’t need to know what needs to change. Get data from previous, modify data, modify view. It becomes getting the data, modifying the data.

In the React framework, if we don’t use shouldComponentUpdate or Memo, the cost of diff can be very large. In some cases, libraries such as immutableJs or immerJs may also be needed to reduce the complexity of shouldComponentUpdate and Memo comparisons. So React’s flexibility comes with risks

2.3 Virtual DOM Properties

Now let’s take a look at what the basic structure of the virtual DOM looks like. The structure shown below is intended to be a minimal version (props doesn’t even support the ID attribute)

type ArrayType<T> = T | T[]

type LegalVNode = VNode | string | number | bigint | boolean | null | undefined

interface VNode {
  type: null | string | Function.props: Partial<{
    id: string
    style: Partial<CSSStyleDeclaration>,
    className: string.onClick: Function.onCaptureClick: Function.children: ArrayType<LegalVNode>
  }>,
  key: keyof any._dom: HTMLElement,
  _parent: VNode,
  _component: any.constructor: null
}
Copy the code

There are a few properties that are particularly important, and I’ll talk about them separately

2.3.1 type

This type is used to describe the type of the current virtual DOM. In general, there are three types

  • Element node, i.enodeType1domnode
  • The text node, i.enodeTypeFor 3domnode
  • Function component/class component

Element components, such as div, P, SPAN, etc., will store the tag’s tagName on type, such as

{ type: 'div' }
Copy the code

In the case of text nodes, type stores a NULL to distinguish it from element nodes

{ type: null }
Copy the code

If it is a function component/class component, type stores the method directly (the class is actually a method)

{ type: Component }
Copy the code

2.3.2 props

This is used to store properties bound to the current node, such as className, style, and events

Note that in the virtual DOM, there is also a children that holds the children of the current node. If the current node has only one child, then children is the value of the unique child, as follows

{ props: { children: '1'}}Copy the code

If the node has more than one value, the value of this property is an array

{ props: { children: [ '1'.'2']}}Copy the code

2.3.3 _children

As you can see from the description of props. Children, this property is of variable type, sometimes an array and sometimes not, so Preact mounts a _children on the virtual DOM to convert the child nodes into an array. Not only that, but Preact also converts the string, number, and Bigint types into a virtual DOM for comparison purposes, such as

{ type: 'div'.props: { children: '1'}}Copy the code

After the transformation

{ 
  type: 'div'.props: { children: '1' },
  _children: [{type: null.props: '1'}}]Copy the code

2.3.4 constructor

The reason for this property is that Preact made a small optimization, and its value is always null

In React, for example, nodes rendered must be legitimate virtual DOM nodes or underlying data types

This is not the case in Preact, which does not render if it is found that the node is not a valid node

Preact does this because js values other than null and undefined have constructor attributes, so Preact has only two legal possibilities if it finds that the current node is not null or undefined

  • Function/class component
  • Element/text node

If the constructor of the current node returns a value, then the developer must have passed in a value that is difficult to handle. Break (

2.3.5 Other Attributes

  • Key:The child element is assigned when the loop list is savedkey
  • _dom:Save the current virtualdomCorresponding truthdomPoint to the
  • _parent:Save the current virtualdomThe parent virtualdomnode
  • _component: This property holds the instance of the class component if it is one

2.4 Creating the Virtual DOM

To prevent code run-throughs, specific code implementations have been placed in/Packages /1 and are available for self-inspection

Against 2.4.1 JSX

The React project is based on JSX, as shown below

JSX is not a valid JS syntax. To use it in a browser, you need to compile it into JS, for example with Babel

Classic 2.4.2

We can go to Babel’s website, click here and give it a try

React Runtime is changed to Classic, input the source code on the left side of the editor, and the output legitimate JS code will be displayed on the right side

JSX uses the React. CreateElement method. React object JSX uses the React

2.4.3 Automatic

React introduced a “new” JSX conversion in Version 17, which is a slight optimization for runtime performance. When the React Runtime is Automatic, you can see that legitimate code is not created using the React. CreateElement method. Instead, the JSX function is automatically introduced. In addition, the new transformation improves performance a bit by placing children directly in the second parameter so that it does not need to be placed at run time

3 first diff

3.1 Understand the DIFF algorithm

Now that you know how to create the virtual DOM, the goal is to render the virtual DOM tree onto the page, but before we learn rendering in detail, we need to understand the basic flow of the Diff algorithm. When a developer calls the Render function, it normally passes two parameters, the first parameter being the first render virtual DOM and the second parameter being the container in which the virtual DOM is stored

Developers can call Render repeatedly to re-render, and render internally compares the two virtual DOM changes and makes minimal updates. To verify that the div on the page was regenerated after the render call, we can do the following test. After the first render, we can get the only div object on the page, and after rendering again, we can compare whether the div is the same

The specific code implementation I have placed in git repository Packages /2 folder. Once it’s up and running, click button and you can see that the two div objects are indeed the same in the console print

3.2 Basic diff process

The problem is that the old virtual DOM doesn’t exist the first time you render it, so how do you compare? In Preact, the old virtual DOM is stored on the container. For example, in the example above, the old virtual DOM is stored on the #root DOM. If it is not available, an empty object is prepared and diff with the first virtual DOM. Assign the passed virtual DOM to #root so that the old virtual DOM can be fetched at #root next time

As you can see from the example above, the virtual DOM is actually a tree. So we can process the tree through traversal. Similarly, diFF algorithm will only compare and reuse elements of the same layer during traversal, because in general business, modification of DOM structure occurs between the same layer, and time complexity explosion of cross-layer comparison is generally not used except for drag and drop scenes

So “peer comparison” actually leads to two logic, one is to look for nodes in the same layer that can be reused, after the search is done, diff can reuse the properties of the old and new nodes, after the processing is done, then the child nodes, so that two methods can be abstracted

  • The diff: diffOld and new nodes that can be reused (handling properties, event bindings, etc.)
  • DiffChildren:
    • Compare the children of the old node with the children of the new node to find nodes that can be reused
    • After finding old and new nodes that can be reused, pass todifffunction
    • Determine whether children need to move order (similar to sorting)
    • Remove all unused old nodes fromdomThe tree removal

4 First Render

Now that the groundwork is in, how to render the virtual DOM onto the page is the next step. For brevity, the rest of this tutorial will focus on minimal implementation, regardless of edge cases

4.1 Function Definition

If you just want to render the virtual DOM onto the page, you should prepare three functions, as follows

  • renderUsed:ReactThis function must be very familiar to the developers of
/ * * *@param Vnode requires the virtual DOM * to be rendered to the page@param ParentDom Needs to render the container */
declare function render(vnode: VNode, parentDom: HTMLElement) :void;
Copy the code
  • diffChildrenIn:3.2The section explains what this function does to handle reusable child nodes
/ * * *@param ParentDom To which DOM the child node should be mounted *@param NewChildren The child node * to process@param NewParentVNode New parent virtual DOM node *@param OldParentVNode The old parent virtual DOM node */
declare function diffChildren(
  parentDom: HTMLElement,
  newChildren: Array<LegalVNode>,
  newParentVNode: VNode,
  oldParentVNode: VNode
) :void;
Copy the code
  • diff: Compares two reusable virtual objectsdomNode, modify properties
/ * * *@param ParentDom Under which DOM the current node needs to be hung *@param NewVNode New virtual DOM node *@param OldVNode a reusable virtual DOM node */
declare function diff(parentDom: HTMLElement, newVNode: VNode, oldVNode: VNode) :void;
Copy the code

4.1 render

See /render.js for specific code implementation

Next, prepare a virtual DOM that covers “most” cases. First, there are cases where children are arrays or single nodes, and second, there are basic properties, styles, and event-binding handling in props

const style = { border: '3px solid #D6D6D6'.margin: '5px' }

const element = (
  createElement(
    'div', { className: 'A1', style },
    'A-text',
    createElement(
      'div', { className: 'B1', style },
      'B1-text',
      createElement('div', { className: 'C1', style, onClick: () = > alert(1)},'C1-text'),
      createElement('div', { className: 'C2', style }, 'C2-text')
    ),
    createElement('div', { className: 'B2', style }, 'B2-text')))Copy the code

The current render function only needs to consider three things

  • Fetch the old virtualdomNode, cannot be replaced with an empty object
  • Store new virtual DOM nodes for later use
  • calldiffChildren

4.2 Recursive logic

2 diffChildren

See /children.js for specific code implementation

Now the Render method passes the child to diffChildren, remember the _children property in the virtual DOM, which is used to store children that are easy to handle, so you need to traverse the new node, handling null, undefined, string, number, Bigint, the following is the initial render logic

Since this is the first rendering, the logic to find old nodes from oldParentVNode that can be reused has been removed

4.2.2 the diff

The logic of the diff rendered for the first time is relatively simple, because there are no old nodes, so you just need to determine whether the current node is an element node or a text node, call the corresponding DOM creation API, and mount the created element to the _DOM of the virtual DOM. Why is it convenient to check whether the mount is correct when you first mount the className property to the DOM

By now, the basic DOM structure has been fully rendered to the page, and the demo is placed under Packages /3

4.3 props processing

For details, see /props. Js.

The processing of props can be separated into a method to be processed separately. The type definition is as follows

/ * * *@param Dom Real DOM node * corresponding to the current virtual DOM@param NewProps Props property * of a new virtual DOM node@param OldProps Props property of the old virtual DOM node */
declare function diffProps(
  dom: HTMLElement,
  newProps: Pick<VNode, 'props'>,
  oldProps: Pick<VNode, 'props'>
) :void;
Copy the code

There are two things you need to do in diffProps

  • theoldPropsIn the presence,newPropsAttributes that do not exist in the
  • Distribute attributes to different functions for separate processing (only three types are handled here:style.event.Other attributes)

The basic flow chart is as follows

This style

The React style needs to be written as an object, and the CSS properties use camel name, as shown below

{
  backgroundColor: 'red'.borderBottomColor: 'green'. }Copy the code

However, we can’t simply concatenate it and assign it directly to cssText, because the hump nomenclature is not legal in HTML, so we need to convert it to “dash”.

Replace has a function overload. The first parameter can be passed a re, and the second parameter can be passed a method. The parameter received is the value returned by the re call exec

'borderBottomColor'.replace(/[A-Z]/g.s= > ` -${s.toLocaleLowerCase()}`)  // 'border-bottom-color'
Copy the code

The next step is to do a loop, concatenating each attribute and value together

Note that there is no logic to automatically add PX to attributes related to size

4.3.2 Event Binding

React implements a set of event delegation mechanisms to bind all events to the same root node. Preact implements a set of event capture and bubble mechanisms to achieve event binding. Here’s a look at how Preact implements event binding. In JS, event binding is divided into two types, one is capture stage and the other is bubble stage. In virtual DOM, capture and bubble are distinguished by different naming methods, as follows

  • OnCatureClick: Event capture phase is triggered
  • OnClick: Event bubble phase is triggered

Preact allows you to determine whether the event is bubbling or being captured. Preact then adds a _Listeners empty object to each DOM object to store all the events in the DOM binding. So here’s the name of the event and it doesn’t really require anything, as long as it’s useful to distinguish between the methods that trigger each event, okay

Next, prepare two proxy methods, of which only one exists for the entire page running cycle, through which all event triggers pass

function eventProxy(e) {
  this._listeners[e.type](e)
}

function eventProxyCapture(e) {
  this._listeners[e.type + 'Capture'](e)
}
Copy the code

Finally, make a logical judgment. The reason for doing this is that domDiff is frequently triggered in ordinary services, and it would be a waste of performance to re-bind domDiff every time. Therefore, all events are bound only once, saving the objects to be triggered on _listeners. When domDiff function changes, Simply replace the methods in _listeners and the cost of changing an object’s value is always less than unbinding and re-binding

4.3.3 Other Attributes

The remaining properties can be set using the following logic

5 Node Overcommitment

The overall logic for node multiplexing is shown in/Packages /5 and can be started using $NPM run 5

Then comes the core of diff algorithm, how to find reusable nodes from old nodes and how to arrange dom positions after diff is completed. Here’s a quick overview of how Preact’s diff algorithm is implemented. Note that the following diff algorithm is abbreviated for ease of understanding

5.1 Key Functions

The answer to the interview question “Why use key in a circular list, index is not recommended” can be found on the Internet in a long essay, but it only takes one sentence to answer the question. In React, old and new nodes use the same type and key to determine whether they can be reused. If index is used as the key, error reuse will occur

For example, in oldChildren, each li node has different descendant nodes. After setState, the sequence of the list is shuffled without modifying the descendant nodes. However, in React, it only knows that the type of Li. 1 is li, the key is 0, and the type of li2 is li. If the key is 0, it thinks that these two nodes are the same node, and there are different numbers of descendants. This, in turn, turns a simple order reversal into a complex operation of removing descendant nodes and creating new ones, wasting extra performance

5.2 Finding Reusable Nodes

The logic of the Diff algorithm must be based on the fact that most of what happens in a project is not the location of the DOM being moved, but rather the following

So when show is Boolean, the child node has the following two situations

In section 4.2.1, we explained the rendering logic of child nodes, that is, traversing the child node will fetch the index of the current array. In Preact, the index of the current child node will fetch the same position in the old array of child nodes. For example, if a, c, and both the old and new array indexes are 0, then it can be hit directly

If there is no hit, Preact uses the silly method of traversing directly from zero in O(n) time. If hit, set the dummy DOM corresponding to the index in oldChildren to null in preparation for removing unused nodes

Because of this optimization mechanism, the following format should never be written, otherwise it will not be reused correctly

5.3 the diff

The diff method in this section is performed in the for loop in diffChildren (same as the initial render). Having found the reusable nodes in the previous section, the next step is to pass the new and reusable nodes into the diff method

Since it is a reusable node, it is not possible to create a node directly as described in section 4.2.2. The logic is as follows

5.4 Moving a Node position

5.4.1 theory

Out-of-loop variables:

Before the newChildren for loop, you need to prepare a variable, oldDom, which you can think of as a pointer to the first real DOM object in the current list. For example, if the current page is [#a,#b,#c,#d], then oldDom refers to #a, if not, assign a null

Variable in loop:

In each for loop, prepare the following three variables

  • ChildVNode: The new child node that is currently being processed
  • OldVNode: found an old virtual DOM node that can be reused in the old virtual DOMnullorundefined
  • NewDom: the reality of the node being processeddomObject (diffAfter the function completes execution, mount thechildVNode._domAttributes)

Then, to determine whether childVNode is the virtual DOM of oldDom, you only need to make the following two judgments. If both values are true, it indicates the same node

  • oldVNodeDoes it exist? If not, yesoldChildrenThere are no reusable nodes in
  • oldDomwithnewDomIs it the same

If it is the same node, it does not need to be processed. Continue to loop through the next new node to be processed and change oldDom to the younger node of the new node

If it is not the same node, oldDom is checked to see if it exists. If not, newDom is added directly to the end of the parent DOM, and the next loop is passed

If oldDom exists, does newDom come after oldDom, if it comes after oldDom, do nothing, if it comes before oldDom, and then enter the next new child node loop

5.4.2 practice

For example, the element on the page now is

After the update for

Before looping, take an oldDom, which is now li#a

Deal with e

The DOM on the page will look like this

[ #e, #a, #b, #c, #d ]
Copy the code

To deal with d

The DOM on the page will look like this, oldDom becomes null

[ #e, #a, #b, #c, #d ]
Copy the code

With c

The DOM on the page will look like this

[ #e, #a, #b, #d, #c ]
Copy the code

To deal with b

The DOM on the page will look like this

[ #e, #a, #d, #c, #b ]
Copy the code

5.5 Removing Invalid Nodes

After a hit, set the dummy DOM for the corresponding index in oldChildren to null. Let’s use the example above, and oldChildren now looks like this

[{type: 'li', { key: 'a'.id: 'a' }, 'a' }, 
	null.null.null.null
]
Copy the code

All we need to do is remove the non-null elements from the page, and the whole diff algorithm is done. After the deletion, the DOM on the page looks like this

[ #e, #d, #c, #b ]
Copy the code

6 Component Rendering

The logic for component rendering is shown in/Packages /6 and can be started using $NPM run 6

6.1 Component

To implement a class Component, create a Component class that the class Component inherits. This class has setState, forceUpdate, and other methods, which are not implemented yet. Because a class is really a syntactic sugar for a method, there is no way to determine whether it is a class or a method in the code, so we need to bind a static property to the class that identifies it as a class component

This way, you just need to be indiffMethod to determine what is the current component

6.2 Function Components

In Preact, function components are treated the same as class components. For example, if it is a class component, it must be instantiated within the framework. How is the function instantiated? We can see how the class component compares to the function component below

Preact instantiates a Component if it is a function Component, but it instantiates a Component instead of a function Component, and then adds a Render property to the Component object. The value of the Render property is naturally the function component

So, the Render method in the Preact class component has one more function than React to receive props directly, without needing to fetch the props from the context, because that’s by the way

6.3 Class/function component rendering

Now let’s not consider the nesting case, the so-called nesting is the most common higher-order components, a class component or function component return out of the root node is also a class component or function component, now only consider one case, that is, return out of an element node or text node. So in this case, it’s the easiest thing to do

First, we need to modify the branch statement in the diff function. Since we only dealt with native nodes before, we need to add a branch to check whether type is a function type

Remember that there is a _component property in the virtual DOM to keep track of whether the component has already been instantiated, so if it has been instantiated before, there is no need to instantiate it again and reuse the previous object, and vice versa. One thing to remember here is that the function Component uses Component as a proxy and then overrides the Render method of Component

Finally, simply execute render, get the returned virtual DOM, and pass it into diffChildren with the children of the old node, and the diff function is done

However, this is not the end, and diffChildren needs to be treated a bit

6.4 diffChildren

If you look at the following example, when the left virtual DOM is updated to the right virtual DOM, you will see that d and E are not removed

This is because the function component does not convert to the real DOM, and what is actually displayed on the page is the content in Render, which results in an empty _DOM attribute on the virtual DOM of the function component. In order to solve this problem, we can use the feature of diff algorithm, which is the depth-first recursive strategy. This means that the child node always diff before the parent node, and we can get the real DOM reference of the child node in diffChildren, which we assign directly to the _DOM attribute of the parent virtual DOM

newParentVNode._dom = childDom
Copy the code

This is because the child node completes before the parent node. If the parent node is a real DOM, the _DOM is reassigned; if not, the _DOM points to the first real DOM returned in Render, so that the function component can’t be removed when the DOM is removed

But that’s not the end of it, as the addition of the Fragment component in Act16 means render may not return a single root node

7 Fragment

Fragment related implementations see/Packages /7 and can be started with $NPM run 7

7.1 fragments implementation

Fragment is simply a function component that returns children inside

7.2 Problems of DIFF algorithm

Using fragments means that a class/function component does not return a single node. How to render it the same way is bound to cause problems, as in the following example

In the page it will render like this

The _DOM attribute of the function component points to the first real DOM returned by render, which means that the current virtual DOM tree is as follows. Since both Fragment and FragmentTest are functions, the div#1 node will be mounted on the Fragment and FragmentTest nodes, keep that in mind

Since both Fragment and FragmentTest are functional components, they don’t actually map to the DOM tree, so what happens? First, because of the recursive depth-first strategy, the algorithm will first insert div#0 into the parent node, and then process the child Fragment of the FragmentTest. Div1 and div2 are inserted into the parent node in the following order

At this point, the sort in memory is correct

The _DOM of the Fragment node is mounted to the parent node, and the _DOM of the Fragment node is also mounted to the dom tree. But because the DOM cannot be repeatedly mounted to the same DOM tree, movement of dom nodes occurs

Similarly, if the parent node is then processed upwards, the parent node operation is repeated…

7.3 Modifying the DIFF Algorithm

So, to solve this problem, it’s really easy to use the “recursive depth-first strategy” and “function components do not render to the page” rules to solve this problem

However deep the diffChildren hierarchy is, the leaf nodes are processed first, and the parentDom of the same DOM layer is the same. This means that when diffChildren processes function components, the children are already sorted, and the function components do not need to be sorted by themselves

In other words, it is not necessary to perform [5.4](##5.4 move node position) operation to determine the current virtual DOM is a function component, and just return

7.4 Fragment in the List

One small detail that needs to be addressed in Preact is the rendering of lists that often appears in business writing

The props structure of the virtual DOM is described as follows

In Preact, if a child node is currently an array, a layer of Fragment will be wrapped

The life cycle of the first render

The related implementation of the first render lifecycle is shown in/Packages /8 and can be started using $NPM run 8

Instead of addressing UNSAFE and error-handling, the first rendering triggers only two lifecycle functions, the getDerivedStateFromProps static method and componentDidMount

8.1 the static getDerivedStateFromProps

I got a picture from the Internet

The getDerivedStateFromProps is the props for the new virtual DOM. The nextProps is the props for the new virtual DOM. PrevState is the value of state in the current component

type GetDerivedStateFromProps<P, S> = (nextProps: Readonly<P>, prevState: S) = > Partial<S> | null;
Copy the code

The specific code implementation is also very simple. You need to prepare a variable _nextState to store the next state. The reason for doing this is that later in the lifecycle implementation, you need to receive the old and new states, so you can’t overwrite each other

8.2 componentDidMount

ComponentDidMount is not executed until all nodes have been rendered, but the render method is synchronous, which means that a task queue can be stored before the diff method is executed. After the diFF method is completed, Execute all pending methods in the task queue, and you’re done

Because a class component, there may be a number of functions to be triggered after the completion of processing, such as the setState callback, componentDidUpdate, etc., so we can save a queue on each component instance, the need to execute the callback, The class instance is stored in commitQueue. The advantage of this is that if only one method is stored, the context of that method cannot be determined

CommitRoot logic is as follows

9 setState

The related implementation of setState is shown in/Packages /9 and can be started using $NPM run 9

9.1 React17的setState

React Interview question: How many render times will the console print after clicking the React button? How many count will the console print after clicking the Render button

The answer is 3 times, and the count on the page has changed to 4, because the setState of the previous two times has been merged, setTimeout has been removed from the current execution stack, and the React merge update mechanism is invalid

In version 17 React provides the unstable_batchedUpdates method in the React-Dom, which allows us to manually merge updates

Now after clicking a button, only two render will be printed on the page

9.2 React18的setState

After the update to version 18, reactApps created with createRoot().render() don’t need to be merged

However, using render to create a ReactApp behaves exactly like React17.

9.3 Implement merge update with task Queue

Next, you’ll see how Preact implements merge updates. Any developer who has worked with JS knows that one of the features of JavaScript is single threading. Then the single thread will involve an event loop mechanism, speaking of the event loop, and has to lead to js macro task queue and micro task queue concept. The following is the type of interview questions you must have done

console.log(1)

setTimeout(() = > {
  console.log(2)})new Promise((resolve) = > {
  console.log(3)
  resolve()
}).then(() = > {
  console.log(4)
  setTimeout(() = > {
    console.log(5)
  })
})

queueMicrotask(() = > {
  console.log(6)})setTimeout(() = > {
  console.log(7)})Copy the code

The answer is 1,3,4,6,2,7,5

Then we can implement a merge update using the micro/macro event queue model. The following is the core code implementation, where the isDirty variable is crucial to mark whether a component isDirty or not, avoiding repeated pushes to the microtask queue

9.4 setState

The first parameter of setState can pass two types, and the second parameter can pass a callback. This is easy to implement, just put it in the _renderCallbacks task queue. After executing its task, it is handed over to the commitRoot, and setState does not need to care about it

setState<K extends keyof S>(
	state:
		| ((prevState: Readonly, props: Readonly

) = >
Pick<S, K> | Partial<S> | null) | (Pick<S, K> | Partial<S> | null), callback? :() = > void) :void; Copy the code

The specific implementation logic is as follows

  1. inComponentPrepare one on the classsetStateMethod, which takes two parameters. Remember thestatic getDerivedStateFromPropsIn the preparation of_nextStateVariable? We just need to save the result of processing the first parameter in_nextStateIn, then indiffMethod, will automatically help us process

  1. Determine if the second argument is a method, and if so, place it directly in _renderCallbacks

  2. Globally prepare an update queue rerenderQueue, because it is possible that multiple class components need to be updated, and prepare an enqueueRender method that receives instances of class components that need to be updated and stores the component instances in rerenderQueue. In this method, the operation of merging update is realized by means of microtask queue

  1. When all of the above work is done, the event loop mechanism will start processing the tasks in the microtask queue,rerenderQueueTo save the class component instance that needs to be updatedrenderMethods do the same thing, except they don’t start at the root anymorediffBecause theReactIs unidirectional flow, directly from the needupdateThe node ofdiffThe whole tree will do

9.5 forceUpdate

The internal implementation of forceUpdate is the same as setState, but without the processing logic of the first step. After calling it, shouldComponentUpdate method is not triggered, call Render to get the new virtual DOM, and then directly enter the diff phase. To do this, we can prepare a variable called _force on the class component instance with a value of true. If the variable is true, shouldComponentUpdate should not be executed

10 Update life cycle implementation

The update implementation is shown in/Packages /10 and can be started using $NPM run 10

10.1 shouldComponentUpdate

ShouldComponentUpdate type is defined as follows

shouldComponentUpdate? (// nextProps next time
  nextProps: Readonly<P>,
  // Next nextState
  nextState: Readonly<S>,
  // Forget about the context
  nextContext: any) :boolean;
Copy the code

Because shouldComponentUpdate is not static, in this method, we can access the state and props before the update. So you need to pass in the props and state for the next time, which is actually pretty easy to implement, because nextProps is going to be lifted from the new virtual DOM, and nextState is already mounted to _nextState

10.2 getSnapshotBeforeUpdate/componentDidUpdate

GetSnapshotBeforeUpdate needs to be combined with componentDidUpdate. First, look at the type definitions for both

getSnapshotBeforeUpdate? (prevProps: Readonly<P>, prevState: Readonly<S>): SS |null; componentDidUpdate? (prevProps: Readonly<P>, prevState: Readonly<S>, snapshot? : SS):void;
Copy the code

Note that getSnapshotBeforeUpdate fires before update, which means you can do some DOM state saving in this function, but at this point, the state and props in the component instance are overridden, So the parameters received by this lifecycle are props and state from the previous session. And the return value of getSnapshotBeforeUpdate is passed in as the third parameter to componentDidUpdate

Once you know the parameters you need to pass, the implementation is fairly easy

11 Component Uninstallation

The related implementation of unmount is shown in/Packages /11 and can be started using $NPM run 11

The componentWillUnmount method is triggered when a component is unmounted, but in the previous diff tutorial, removing an unused component is simply removing the DOM element. Because the browser will help us remove all the children of a node. So to implement componentWillUnmount, all you need to do is recurse

12 Context

Context is implemented in/Packages /12 and can be started using $NPM run 12

12.1 rerenderQueue与_dirty

Before we go into Context, let’s review the application of rerenderQueue to Context in setState, in the following example, now in the virtual DOM tree. A1 provides the content downward, and both B2 and C2 use it

When the contents of A1 change, Preact adds an identifier _dirty to the component that needs to be updated and assigns it a value of true. Each node updates recursively downward from rerenderQueue, so we need to check whether the component instance _dirty in the current update queue is true, because if it is not true, it has been processed and does not need to update

When the update is complete, Preact assigns _dirty to false, so B2 and C1 are already diff A1, so the second and third nodes in the queue are not processed

If rerenderQueue is not set to an array, B1 should add a shouldComponentUpdate lifecycle to prevent the update. Then C1’s consumption behavior is invalid throughout the life of the page

The advantage of storing in an array is that each node in the rerenderQueue is processed directly by traversing it through the rerenderQueue, since A1 is recursively updated downwards, B2 is processed along the way, C1 is not processed due to the parent node blocking the update, but C1 is in the rerenderQueue, and it will be processed naturally after traversing A1 and B2

12.2 createContext

12.2.1 globalContext and contextId

A1 and B1 provide Provider respectively. Why B2 can only consume CTX1 while C1 can consume CTX1 and 2

First of all, a Provider is a function, which means that it works like a Fragment and is instantiated with a Component

Each time createContext is executed, a unique ID is generated for each context. The id is contextId. For example, create two contexts, assuming their ids are CTx1 and CTx2

At the diff entry, Preact prepares an empty object called globalContext

First, if you find that ctx1.provider is used in the large diff A1 node (which you can view as a function component), two things happen internally

  • A shallow copyglobalContext
  • For shallow copyglobalContextObjectkeyforctx1.id.valueforctx1.ProviderItem of an instance of

Next, pass the shallow-copy globalContext to B1 and B2 instead of the old globalContext

When diff B1 is a large node and ctX2. Provider is used, two things will happen internally

  • Shallow copy A previous shallow copyglobalContext
  • For shallow copyglobalContextObjectkeyforctx2.id.valueforctx2.ProviderItem of an instance of

Next, pass the shallow-copy globalContext to C1 instead of the old globalContext. So now in the whole tree, the globalContext points to something like this

Each child node can now retrieve the contents of the corresponding scope from the globalContext

12.2.2 the Provider implementation

Providers, because they are functions, are instantiated internally by the Component proxy, so they can implement a publisk-subscribe mechanism internally by leveraging the life cycle of a class Component

At instantiation time, you only need to determine whether getChildContext exists on the current node to know if you need to override globalContext

12.3 the static contextType

React uses the static contextType property of a class component to consume the context, and then access the data from this.context

To do this, use the globalContext property to get the instance of the corresponding Provider from the globalContext

Next, during component initialization, place the component instance using static ContextType into the Provider’s subscription table

And store the context on an instance of the class

Note that since the Set hash table is unordered, you can add a _depth attribute to each virtual DOM. The deeper the level, the greater the _depth attribute

This allows you to place the shallowest virtual DOM first when performing an update by using _depth sorting, thus achieving the effect of [12.1](##12.1 rerenderQueue and _dirty)

12.4 Consumer

The implementation of Consumer is very clever and can be done in two lines of code

  • toConsumerBind a static propertycontextType
  • becauseConsumerIs a function component and is bound to onecontextTypeProperty, so that means that you canthis.contextAccess to theproviderProvide the data directly calledprops.children(this.context)I’ll be able to render

13 hooks

The related implementation of hooks is in/Packages /13 and can be started using $NPM run 13

13.1 Preparations

13.1.1 Hooks Basic Principles

Hooks cannot be used in loop judgments because function components execute repeatedly like render in class components. Whereas Vue3’s setup, solidJs’s function component, is more like a constructor and fires only once, so they don’t need to worry too much about the order of hooks

The hooks of useMemo, useCallback, useRef, etc. are used for caching, so how do these hooks know what needs to be cached and what needs to be reexecuted? In Preact, it is implemented as follows: first, the function Component is propped up by the Component. Then, on the instantiation object, if hooks are found in the function Component, a __hook variable is stored on the class instance, structured as follows

{ 
  __hook: {
    // Store some data of hook for next judgment and reuse
    _list: [].// useEffect
    _pendingEffects: []}}Copy the code

Next, define a variable hookIndex with an initial value of 0

_list[hookIndex], and then internally +1 after each hooks execution. Therefore, if the hooks are not fixed in position each time a function component executes, cache utilization will fail

13.1.2 Obtaining the currently executing component instance

During the execution of a function Component, hooks need to get the virtual DOM object of the current function Component from a place where they can get the Component proxy object, and store and write data to that object. This is easy to implement, as you can see from the above implementation that the whole process is actually dealing with the diff function, so just store the current newVNode in one place before the DIff function executes render to retrieve the virtual DOM. Because hooks are optional, there is no need to store newVNode when hooks are not used

Preact does this by storing an empty object for options globally. If the hooks function is imported, it registers a function called _render on options. Then in the diff function, before calling Component.render, Call options. And _render

Then save the instance in options._render

13.2 implementation useMemo

Now that you’ve done your homework, you can implement the simplest hooks to get more information

The useMemo type is defined as follows. The second parameter can be thought of as a “dependency” for convenience (it is not really a dependency, just an identifier)

type Inputs = ReadonlyArray<unknown>;

function useMemo<T> (factory: () => T, inputs: Inputs | undefined) :T;
Copy the code

UseMemo stores the inputs and the generic T. If the inputs are the same for the second time, factory does not need to repeat the call and returns the last stored T

Prepare a getHookState to get and set the data stored on the current function component instance

Then prepare a function, argsChanged, to determine whether the old and new dependencies are the same

Finally, the useMemo implementation only needs the help of these two functions

13.3 useCallback/useRef

In Preact, useCallback/useRef is all implemented with useMemo

13.4 useReducer/useState

UseReducer can also be implemented using getHookState and argsChanged. You need to store three variables on hookState

  • _reducer:storagereducerfunction
  • _value:storageuseReducerThe return value of,
  • _component:Store the currentuseReducerIn the component instance, convenient in the triggerdispatchCall directly aftersetStateupdate

UseState can be implemented using useReducer

13.5 memo

13.5.1 PureComponent implementation

PureComponent should inherit Component and add a shouldComponentUpdate method to implement PureComponent

13.5.2 memo to realize

The Memo can be implemented with PureComponent, as follows

13.6 useContext

Because you can get the current component instance when hooks are called, that is, the globalContext on the current instance, there are only two things you need to do in useContext. The first step is to get the instance of the Provider component, and the second step is to subscribe to the component

13.7 useLayoutEffect

13.7.1 Execution Process

The hooks are relatively simple to implement, useLayoutEffect is a bit more cumbersome, because useLayoutEffect acts as a lifecycle, its first callback returns a function that will be executed before the next update, depending on the “dependency” changes, If there are no dependencies, they are executed when the component is uninstalled

Note the word “similar” as shown below, because the properties and life cycles are not exactly the same

When the page is initialized, the console prints the following

useLayoutEffect
useLayoutEffect before 1
Copy the code

When props. A + 1, print the following

useLayoutEffect after 1
useLayoutEffect before 2
Copy the code

The following is printed when component A is uninstalled

useLayoutEffect unmounted
useLayoutEffect after 2
Copy the code

13.7.2 update implementation

function useLayoutEffect(effect: EffectCallback, inputs? : Inputs) :void;
Copy the code

Check whether inputs have changed in their inputs. If this has changed, three values will be stored in the hookState of their inputs

_effect stores the first callback, which does not immediately trigger _inputs to store the second dependency, _cleanup, which stores the return value of a call to _effectCopy the code

Then store hookState on currentComponent._renderCallbacks

Because _renderCallbacks stores methods by default, hookState is an object. Therefore, select hookState and other methods before executing commitRoot. You can inject a method on options just as you would on a component instance

This method requires that _renderCallbacks be filtered, hookState taken out, and the rest written back into _renderCallbacks. The extracted hookState is passed into the invokeEffect function, which does only one thing: execute _effect and assign the return value to _cleanup

When does this _cleanup run? It is executed before invokeEffect, so you can define another function called invokeCleanup, which does _cleanup and _cleanup when it’s done

So the complete code for _commit is as follows

13.7.3 unmount to realize

By now, most of the functionality of useLayoutEffect is complete, except for a method that is called when a component is uninstalled

The secondary function is already stored in _clearnUp when it is initialized. It is not triggered because the dependencies have not changed during the second update, so hookState is not stored in _renderCallbacks. Execute all the stored _clearnUp. Just inject a method into options and execute it in unmount

Next, determine if _clearnUp is stored, and if so, execute

13.8 useEffect

In Preact, useEffect is different from React. When Preact components are uninstalled, useEffect is triggered before useLayoutEffect. Therefore, the following code does not rely on Preact

UseEffect is implemented similarly to useLayoutEffect except instead of storing hookState in _renderCallbacks, it stores hookState in _pendingEffects. You need to add a _isUseEffect property to distinguish it from useLayoutEffect

Next, add a _DIFFed method on options to execute the method on _pendingEffects, which fires after all child nodes have completed

Because Useeffects do not block page rendering, you can do a setState-like update queue and put all Useeffects into the next macro task to execute all

For the same reason, prepare an options._unmounted method to execute all clearnUp methods

UseEffect will be executed before useLayoutEffect and will still access the DOM on the page. This is because the options._unmounted method does not work. All _cleanUp methods are finished in options._unmount. To fix this, apply the _isUseEffect flag and modify the options._unmount method

14 concurrent mode

14.1 Problems with recursion

One problem with using recursion in diff is that during the whole process of diff, the page is stuck and the user can’t operate the page, which will cause a bad user experience. On a 60-frame monitor, for example, it takes 16.6ms to render a frame. If the page is displaying an animation, and if the render time of a frame exceeds 16.6ms, there will be a lag. This usually occurs at some point in time when the task takes too long

However, if js execution takes too long, the following situation occurs

Is there an API that tells JS how much free time is left in a frame, and if not, simply pause the js program, wait until the next frame, and repeat until the task is complete? RequestIdleCallback, an API that tells developers if the browser is idle, is in the experimental stage, so React has implemented its own

14.2 Basic principles of Concurrent Mode

Now that the problem of acquiring free time is solved, how many tasks should be performed in a free time? React officially introduced a new data structure to describe the structure of a page. It is called Fiber. Each fiber can be regarded as a minimum execution unit and cannot be separated

{
	// What node is it
  tag: null.// The actual DOM address in the current fiber page
  stateNode: null.// The type of the virtual DOM
  type: null.// Props for the virtual DOM
  props: null.// The parent of the current node
  return: null.// Sibling of the current node
  sibling: null.// The first child of the current node
  child: null.// Side effect pointer
  firstEffect: null.nextEffect: null.lastEffect: null,}Copy the code

Fiber is just another way to describe the virtual DOM. On the left side of the page is the tree of the DOM structure, and on the right is the basic structure of Fiber

Fiber doesn’t come out of thin air, and it doesn’t happen at precompile time, it happens at runtime. This means fiber also wastes some of its performance at runtime, converting from the virtual DOM to Fiber. As a bonus, the cell is small enough that the current frame’s free time will not be occupied by executing a cell (if a cell takes too long, the frame will still be dropped). This can be done with the help of the requestIdleCallback API

When a fiber is processed, React will determine whether the current fiber needs to be updated and mount it to the parent node. When the parent node is processed, React will determine whether the child node or itself needs to be updated. If the parent node needs to be mounted again, and so on. All the fibers that need to be updated are connected through a linked list and then updated in a while loop

14.3 Why not view Concurrent Mode

You yuxi once said of React

Concurrent Mode does not provide performance improvements per se and may cause performance degradation. New concepts will also be introduced, with developers raising the bar and adding an interview question. Someone on Github asked Yu Why Vue3 removed the time slice. The Preact developers also agreed with the answer, which was that the Fiber architecture was not designed to solve the problem of over-rendering, but rather to solve the problem of over-rendering a large task

Vue has already done this for developers, so even if you hand it to someone with very poor JS skills, the written application performance won’t be that bad. The optimizations in React are left to developers, most of whom don’t even bother to optimize, as long as they can run. Fiber degrades performance again for these developers, so I’m not a big fan of Concurrent Mode

Of course, although not optimistic, but learn or have to learn, Vue3 have come, React18 will be far