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.e
nodeType
1dom
node - The text node, i.e
nodeType
For 3dom
node - 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 saved
key
- _dom:Save the current virtual
dom
Corresponding truthdom
Point to the - _parent:Save the current virtual
dom
The parent virtualdom
node - _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:
diff
Old 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 to
diff
function - Determine whether children need to move order (similar to sorting)
- Remove all unused old nodes from
dom
The 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
render
Used:React
This 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
diffChildren
In:3.2
The 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 objectsdom
Node, 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 virtual
dom
Node, cannot be replaced with an empty object - Store new virtual DOM nodes for later use
- call
diffChildren
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
- the
oldProps
In the presence,newProps
Attributes 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 DOM
null
orundefined
- NewDom: the reality of the node being processed
dom
Object (diff
After the function completes execution, mount thechildVNode._dom
Attributes)
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
oldVNode
Does it exist? If not, yesoldChildren
There are no reusable nodes inoldDom
withnewDom
Is 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 indiff
Method 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
- in
Component
Prepare one on the classsetState
Method, which takes two parameters. Remember thestatic getDerivedStateFromProps
In the preparation of_nextState
Variable? We just need to save the result of processing the first parameter in_nextState
In, then indiff
Method, will automatically help us process
-
Determine if the second argument is a method, and if so, place it directly in _renderCallbacks
-
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
- When all of the above work is done, the event loop mechanism will start processing the tasks in the microtask queue,
rerenderQueue
To save the class component instance that needs to be updatedrender
Methods do the same thing, except they don’t start at the root anymorediff
Because theReact
Is unidirectional flow, directly from the needupdate
The node ofdiff
The 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 copy
globalContext
- For shallow copy
globalContext
Objectkey
forctx1.id
.value
forctx1.Provider
Item 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 copy
globalContext
- For shallow copy
globalContext
Objectkey
forctx2.id
.value
forctx2.Provider
Item 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
- to
Consumer
Bind a static propertycontextType
- because
Consumer
Is a function component and is bound to onecontextType
Property, so that means that you canthis.context
Access to theprovider
Provide 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:storage
reducer
function - _value:storage
useReducer
The return value of, - _component:Store the current
useReducer
In the component instance, convenient in the triggerdispatch
Call directly aftersetState
update
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