Analyzing VUE3 on the shoulders of giants is mainly used to record my impressions after reading the HcySunYang document and deepen my understanding. I suggest you read the original address
The core of a component is the Render function. The rest of the components, such as Data, Compouted, props, etc. provide data sources for the Render function, which produces the Virtual DOM.
The Virtual DOM is eventually rendered into a real DOM, a process called patch.
What is a Vnode
Vue first compiles the template, including the parse, optimize, and generate processes. Parse uses re to parse instructions, classes, and styles in the Template template to form an AST.
<ul id='list' class='item'>
<li class='item1'>Item 1</li>
<li class='item3' style='font-size: 20px'>Item 2</li>
</ul>
Copy the code
var element = {
tag: 'ul'.// Node label name
data: { // A DOM property that uses an object to store key-value pairs
class: 'item'.id: 'list'
},
children: [ // Children of this node
{tag: 'li'.data: {class: 'item1'}, children: {tag: null.data: null.children: "Item 1"}},
{tag: 'li'.data: {class: 'item3'.style: 'font-size: 20px'}, children: {tag: null.data: null.children: "Item 2"]}}},Copy the code
As described in the code above, a template template can be drawn using the AST syntax tree. We use the Tag attribute to store the tag’s name, and the Data attribute to store additional information about the tag, such as style, class, event, and so on. Children is used to describe child nodes.
Vnode species
These are common HTML tags such as div, SPAN, p, etc., but in the actual code development process we pull out a lot of components
<div>
<MyComponent />
</div>
Copy the code
Components like this still need to be described using vNodes. Add an identity to the vNodes used to describe components so that when mounted, there is a way to tell whether a VNode is a normal HTML tag or a component.
const elementVNode = {
tag: 'div'.data: null.children: {
tag: MyComponent,
data: null}}Copy the code
Therefore, we can use tags to determine the content to be mounted, and use different rendering functions to render the corresponding HTML structure.
Components can be divided into two types, one is functional components, one is stateful components. A functional component is a pure function that has no state of its own and only receives external data. A configured component is a class that needs to be instantiated and has its own state.
// Functional components
function MyComponent(props) {}
// Stateful components
class MyComponent {}
Copy the code
In addition to components, there are two types that need to be described, Fragment and Portal.
In Vue3, template no longer requires a big box to wrap all the HTML content, which is what we call the root element.
<template>
<td></td>
<td></td>
<td></td>
</template>
Copy the code
There is not just one TD tag in the template, but multiple TD tags, i.e. multiple root elements, requiring the introduction of an abstract element, the Fragment we will introduce. To mark a tag as a Fragment, simply render the children of the VNode to the page.
Let’s look at Portal. What is Portal? Is to render the child node to a given target.
const portalVNode = {
tag: Portal,
data: {
target: '#app-root'
},
children: {
tag: 'div'.data: {
class: 'overlay'}}}Copy the code
Wherever the component is used, it renders the content under the element id=”app-root”.
In general, vNodes can be divided into five categories: HTML/SVG elements, components, plain text, fragments, and Portal:
Optimization: Flags as VNode flags
Since there are categories for vNodes, it is necessary to use a unique identifier to indicate which category a particular VNode belongs to. At the same time, adding flags to VNode is also one of the optimization methods of Virtual DOM algorithm.
To distinguish vnode types in VUe2, perform the following steps:
- Take a Vnode and try to treat it as a component. If a component is successfully created, it means that the vnode is the component
- If not, check whether vNode. tag is defined. If so, treat it as a normal tag
- If vNode. tag is not defined, check if it is a fixation node
- If it is not a comment node, it is treated as text.
All the above judgments are made during the mount (or patch) phase. In other words, what exactly a VNode describes is known during the mount or patch phase. This leads to two problems: it can’t be optimized from the AOT level, and developers can’t manually optimize.
To solve this problem, the idea is to specify the VNode type by flags at the time of creation, so that the flags can be used during the mount or patch phase to avoid a lot of performance-consuming judgments. This is the renderer we need to talk about
// The mount function renders a VNode into a real DOM. Different types of vNodes need to be mounted differently
export function mount(vnode, container) {
const { flags } = vnode
if (flags & VNodeFlags.ELEMENT) {
// Mount common labels
mountElement(vnode, container)
} else if (flags & VNodeFlags.COMPONENT) {
// Mount the component
mountComponent(vnode, container)
} else if (flags & VNodeFlags.TEXT) {
// Mount plain text
mountText(vnode, container)
} else if (flags & VNodeFlags.FRAGMENT) {
/ / mount fragments
mountFragment(vnode, container)
} else if (flags & VNodeFlags.PORTAL) {
/ / mount Portal
mountPortal(vnode, container)
}
}
Copy the code
At this point, we have completed some design of VNode, so far we have designed the VNode object as follows:
export interface VNode {
The // _isVNode attribute, which was not mentioned above, is a value that is always true and allows us to determine whether an object is a VNode object
_isVNode: true
// The el attribute is also not mentioned above. When a VNode is rendered as a real DOM, the el attribute's value references the real DOM
el: Element | null
flags: VNodeFlags
tag: string | FunctionalComponent | ComponentClass | null
data: VNodeData | null
children: VNodeChildren
childFlags: ChildrenFlags
}
Copy the code
The h function creates a VNode
Flags type for the component
We can use functions to control how vnodes are automatically generated. This is one of the core APIS of Vue
The h function returns a “virtual node,” often shortened to VNode: a plain object that contains information describing to the Vue what kind of node it should render on the page, including descriptions of all child nodes. It is intended for hand-written rendering functions:
render() {
return h('h1'.null.' ')}Copy the code
function h() {
return {
_isVNode: true.flags: VNodeFlags.ELEMENT_HTML,
tag: 'h1'.data: null.children: null.childFlags: ChildrenFlags.NO_CHILDREN,
el: null}}Copy the code
The h function returns some VNode information in the code that takes three arguments tag, data, and children. You only need to specify the flags and childFlags types. Everything else is either default or passed as arguments.
Flags are determined by tag
In VUe2, an object is used as a component description. In VUe3, a stateful component is a class that inherits the base class and is an object component of VUe2. We can determine whether the component is a functional component by checking the true or false of the functional attribute of the object. In Vue3, because stateful components inherit from base classes, the prototype chain determines whether a stateful component is a stateful component by determining whether a render function is defined in its prototype
// Object component compatible with Vue2
if(tag ! = =null && typeof tag === 'object') {
flags = tag.functional
? VNodeFlags.COMPONENT_FUNCTIONAL // Functional components
: VNodeFlags.COMPONENT_STATEFUL_NORMAL // Stateful components
} else if (typeof tag === 'function') {
// Vue3 class component
flags = tag.prototype && tag.prototype.render
? VNodeFlags.COMPONENT_STATEFUL_NORMAL // Stateful components
: VNodeFlags.COMPONENT_FUNCTIONAL // Functional components
}
Copy the code
Flags type for children
Children can be divided into four kinds
- Children is an array h(‘ul’, NULL, [h(‘li’),h(‘li’)])
- Children is a vNode object h(‘div’, null, h(‘span’))
- Children is a plain text string h(‘div’, null, ‘I am text ‘)
- No children h (‘ div ‘)
The children array can be divided into two types, one with key and the other without key, which will be marked as KEYED_VNODES. NormalizeVNodes without key will be called for manual intervention to generate key
// Multiple child nodes, and the child nodes use key
childFlags = ChildrenFlags.KEYED_VNODES
children = normalizeVNodes(children)
Copy the code
The render function renders the Vnode into the real DOM
The renderer’s work is divided into two phases: mount and path. If the old vnode exists, the new vnode will be compared with the old vnode to try to complete the DOM update with the minimum resource cost. This process is called patch. If the old vnode does not exist, the new Vnode will be directly mounted to the new DOM, which is called mount.
The render function takes two arguments, the first is the vNode object to be rendered and the second is a container used to hold the content, usually called a mount point,
function render(vnode, container) {
const prevVNode = container.vnode
if (prevVNode == null) {
if (vnode) {
// There are no old vnodes, only new vnodes. Use the 'mount' function to mount a brand new VNode
mount(vnode, container)
// Add the new VNode to the container. VNode property so that the old VNode exists for the next rendering
container.vnode = vnode
}
} else {
if (vnode) {
// There are old vNodes and there are new vnodes. Then call 'patch' function to patch
patch(prevVNode, vnode, container)
/ / update the container. Vnode
container.vnode = vnode
} else {
// There is an old VNode but no new VNode, which indicates that the DOM should be removed and the removeChild function can be used in the browser.
container.removeChild(prevVNode.el)
container.vnode = null}}}Copy the code
The old vnode | New vnode | operation |
---|---|---|
❌ | ✅ | Call the mount function |
✅ | ❌ | Remove the DOM |
✅ | ✅ | Calling the patch function |
The renderer has a lot of responsibility because it’s not just a tool for rendering vNodes into real DOM, it also does the following:
- Controls some component lifecycle hook calls, component mount, unload call timing.
- Multi-terminal rendering of Bridges.
The essence of a custom renderer is to take platform-specific manipulation of the DOM away from the core algorithm and provide configurable solutions
- Asynchronous rendering has a direct relationship
Vue3’s asynchronous rendering is based on the scheduler implementation. To achieve asynchronous rendering, components cannot be mounted synchronously and dom changes need to be made at the appropriate time.
- Contains the core algorithm Diff algorithm
Renders normal tag elements
As mentioned above in flags, different tags will be flagged by h function, so we can distinguish the type of content to be rendered by flags. Different VNodes use different mount functions
Next we will complete the normal tag element rendering process around these three questions
- After vNodes are rendered as real DOM, no real DOM elements are referenced
- VNodeData is not applied to real DOM elements
- The child node, children, is not mounted further
Question 1
function mountElement(vnode, container) {
const el = document.createElement(vnode.tag)
vnode.el = el
container.appendChild(el)
}
Copy the code
Problem 2 By iterating through VNodeData, the switch value is applied to the element
/ / get the VNodeData
const data = vnode.data
if (data) {
// If VNodeData exists, it is iterated over
for(let key in data) {
// Key can be class, style, on, etc
switch(key) {
case 'style':
// If the value of key is style, the style is inline, apply the style rules to el one by one
for(let k in data.style) {
el.style[k] = data.style[k]
}
break}}}Copy the code
Problem 2: Mount child nodes recursively
// Get children and childFlags
const childFlags = vnode.childFlags
const children = vnode.children
// Check that no recursive mount is required if there are no child nodes
if(childFlags ! == ChildrenFlags.NO_CHILDREN) {if (childFlags & ChildrenFlags.SINGLE_VNODE) {
// Call mount if it is a single child node
mount(children, el)
} else if (childFlags & ChildrenFlags.MULTIPLE_VNODES) {
// If there is a single child node, traverse and mount it by calling mount
for (let i = 0; i < children.length; i++) {
mount(children[i], el)
}
}
Copy the code
The difference between arrtibutes and props is that after the browser loads the page, it parses the tags in the page and generates DOM objects corresponding to them. Each tag may contain some attributes. If these attributes are standard attributes, the DOM objects generated by parsing will also contain corresponding attributes. If it is nonstandard, it is treated as props
Other things like class, Arrtibutes, props, and events can be documented
Render plain text, fragments, and Portals
- Plain text
Plain text is the simplest; you just need to add elements to the page
function mountText(vnode, container) {
const el = document.createTextNode(vnode.children)
vnode.el = el
container.appendChild(el)
}
Copy the code
- Fragment
For fragments, there is no need to render, just mount children. If there are multiple children, mount them and if there are no children, create an empty text node and mount mountText.
A reference to the EL property of a Fragment VNode after it has been rendered as a real DOM
If there is only one node, the EL attribute points to that node; If there are multiple nodes, the el attribute value is a reference to the first node; If there are no nodes in the fragment, that is, empty fragments, the EL attribute refers to empty text node elements that are placeholders
So what’s the point of doing this?
When moving a DOM element in patch, make sure it is in the right place, instead of always using appendChild and sometimes insertBefore, we need to get the node application. The vnode.el attribute is essential, and even if the fragment has no child nodes we still need a placeholder empty text node as a reference to the location.
- Portal
A Portal mount is the same as a Fragment, except that the Portal tag is the mount point.
Who should the EL attribute of a VNode of Portal type point to
The content described by Protal can be mounted anywhere, but a placeholder element is still required, and the EL attribute of a VNode of type Protal should point to this placeholder element because of another feature of Portal: Although the content of a Portal can be rendered to any location, it still behaves like normal DOM elements, such as the capture/bubble mechanism for events, which is still implemented according to the DOM structure written by the code. To do this, you need a placeholder DOM element to hold the event. But for now, we’ll just use an empty text node as a placeholder
Rendering component
Vnode. flags is used to determine whether the mounted vNode is a stateful component or a function component.
Mount a stateful component, the class component
class MyComponent {
render() {
return h(
'div',
{
style: {
background: 'green'
}
},
[
h('span'.null.'I am the title of the component 1...... '),
h('span'.null.'I am the title of the component 2...... ')])}}Copy the code
// Component mount
function mountStatefulComponent(vnode, container) {
// Create a component instance
const instance = new vnode.tag()
/ / render VNode
instance.$vnode = instance.render()
/ / a mount
mount(instance.$vnode, container)
// Both the el attribute value and the component instance's $EL attribute refer to the component's root DOM element
instance.$el = vnode.el = instance.$vnode.el
}
Copy the code
Functional components return vNode functions directly
function MyFunctionalComponent() {
// Returns the description of the content to render, VNode
return h(
'div',
{
style: {
background: 'green'
}
},
[
h('span'.null.'I am the title of the component 1...... '),
h('span'.null.'I am the title of the component 2...... ')])}Copy the code
MountFunctionalComponent (mountFunctionalComponent)
function mountFunctionalComponent(vnode, container, isSVG) {
/ / get VNode
const $vnode = vnode.tag()
/ / a mount
mount($vnode, container)
// The el element references the root element of the component
vnode.el = $vnode.el
}
Copy the code
The patch function updates the rendered DOM
In the previous section, if you don’t have an old vnode, use the mount function to mount a new one. Then there are vNodes that should update the DOM in an appropriate way, which is often referred to as patch.
When a brand new Vnode is rendered with render, the mount function is called to mount the vnode and the container element stores a reference to the vNode object. This calls the renderer here to render the new VNode object to the same container element. Since the old vNode already exists, patch is called to update it in the appropriate way
Scheme 1: VNodes are of different types. Only vnodes of the same type can be compared. If they are different, the optimal scheme is to replace the old vnode with a new one. If the old and new VNodes are of the same type, different comparison functions are called based on the different types
Update the normal tag element
In scheme 2, different labels render different content, so comparison is meaningless
For example, only the LI tag can be rendered under the UL tag, so it makes no sense to compare the UL tag with a div tag. In this case, we will not patch the old tag element, but replace it with a new tag element.
If the old and new vnode tags are the same, the only differences are VNodeData and children. I’m essentially comparing these two values.
The following steps are used to update VNodeData:
- Step 1: Iterate over the new VNodeData when it exists.
- Step 2: Decibel attempts to read the old and new values based on the key in the new VNodeData. Namely prevValue and nextValue
- Step 3: Use switch… Case statements match different data for different update operations.
Take the style update as an example, as shown in the code above:
- Iterate over the new style data and apply all of the new style data to the element
- Iterate through the old style data, removing styles from elements that do not exist in the new style data
The update of the child node is mainly to recursively call patchChildren in the patchElement function. Note that comparisons to child nodes can only be same-level comparisons.
// Call patchChildren recursively to update the child node
patchChildren(
prevVNode.childFlags, // The type of the old VNode child
nextVNode.childFlags, // The type of the new VNode child
prevVNode.children, // Old VNode child node
nextVNode.children, // New VNode child node
el // The current label element, which is the parent of these child nodes
)
Copy the code
Because the state of child node can be divided into three types altogether, one is no child node, one is only one child node, and the last one is multiple child nodes, so there will be nine cases of child node peer comparison.
In fact, in the whole comparison between old and new children, only when the old and new child nodes are multiple children is it necessary to carry out the real core diff, so as to reuse the child nodes as much as possible. Later sections will also focus on how diff can reuse child nodes as much as possible.
Update plain text, Fragment, and Portal
- Plain text
Updates to plain text can read or set the contents of a text node (or comment node) through the nodeValue property of a DOM object
function patchText(prevVNode, nextVNode) {
// Take the text element el and make nextvNode. el point to that text element
const el = (nextVNode.el = prevVNode.el)
// Updates are necessary only if the old and new text contents are inconsistent
if(nextVNode.children ! == prevVNode.children) { el.nodeValue = nextVNode.children } }Copy the code
- Fragment
Since the Fragment does not wrap elements, only child nodes, our update to the Fragment is essentially updating the “child nodes” of the two fragments. The patchChildren function of the tag element is called directly, and only the reference to el is needed.
- If the new segment children is a single child, it means that the value of its vNode. children property is the vNode object nextvNode. el = NextvNode.children.el
- If the new fragment children is empty text node. Prevvnode. el attribute reference is the empty text node nextvNode. el = prevvNode. el
- If the new fragment children is multiple child nodes. nextVNode.el = nextVNode.children[0].el
- Portal
The same is true for portal, there is no element wrapping, just compare the child nodes and note that el points to nextvNode.el = prevvNode.el.
If the new container is different from the old one, move it. This section is not extended, but you can view the documentation if you are interested
Update the components
Update stateful components
Stateful component update can be divided into two types, one is active update and passive update.
Active update: An update caused by a change in the state of the component itself. For example, changes in data and so on
Passive updates: Changes to components caused by external factors, such as props
1. Be proactive
When the state of the component changes, all we need to do is re-execute the render function and generate a new VNode, and finally update the real DOM with the old and new VNodes.
Let’s say we need to update a component like this. How do we do that?
class MyComponent {
// Own state or local state
localState = 'one'
/ / mounted hook
mounted() {
After two seconds, change the value of the local state and re-call the _update() function to update the component
setTimeout(() = > {
this.localState = 'two'
this._update()
}, 2000)}render() {
return h('div'.null.this.localState)
}
}
Copy the code
Recall the component mount steps:
- Create an instance of the component
- Call render on the component to get the VNode
- Mount the vNode to the container element
- Both the EL attribute value and the component instance’s $EL attribute refer to the component’s root DOM element
We encapsulate all operations into a _update function.
function mountStatefulComponent(vnode, container, isSVG) {
// Create a component instance
const instance = new vnode.tag()
instance._update = function() {
// 1. Render VNode
instance.$vnode = instance.render()
// 2
mount(instance.$vnode, container, isSVG)
// The el attribute value and the component instance's $EL attribute refer to the component's root DOM element
instance.$el = vnode.el = instance.$vnode.el
// Call mounted hook
instance.mounted && instance.mounted()
}
instance._update()
}
Copy the code
_mounted Boolean specifies whether a component has been rendered for the first time or needs to be updated by calling _update again.
function mountStatefulComponent(vnode, container, isSVG) {
// Create a component instance
const instance = new vnode.tag()
instance._update = function() {
// If instance._mounted is true, the component is mounted and updated
if (instance._mounted) {
// Get the old VNode
const prevVNode = instance.$vnode
// 2. Re-render the new VNode
const nextVNode = (instance.$vnode = instance.render())
// 3. Patch update
patch(prevVNode, nextVNode, prevVNode.el.parentNode)
// update vnode.el and $el
instance.$el = vnode.el = instance.$vnode.el
} else {
// 1. Render VNode
instance.$vnode = instance.render()
// 2
mount(instance.$vnode, container, isSVG)
// 3. Indicates that the component has been mounted
instance._mounted = true
// The el attribute value and the component instance's $EL attribute refer to the component's root DOM element
instance.$el = vnode.el = instance.$vnode.el
// Call mounted hook
instance.mounted && instance.mounted()
}
}
instance._update()
}
Copy the code
Component updates can be roughly divided into three steps:
- Get the old VNode
- Recall the render function to generate a new VNode
- Call the patch function to compare the old and new VNodes
2. Passive updates
$props = vnode.data so that the component can access the props data passed in from the parent component through this.$props
Here’s an example:
The VNode output for the first rendering is:
const prevCompVNode = h(ChildComponent, {
text: 'one'
})
Copy the code
The VNode output for the second rendering is:
const prevCompVNode = h(ChildComponent, {
text: 'two'
})
Copy the code
As all rendered tags are components, patchComponent function will be called inside the Patch function for updating
function patchComponent(prevVNode, nextVNode, container) {
// Check whether the component is stateful
if (nextVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) {
// Get the component instance
const instance = (nextVNode.children = prevVNode.children)
// 2, update props
instance.$props = nextVNode.data
// update the component
instance._update()
}
}
Copy the code
Stateful component updates can be divided into three steps:
- Prevvnode.childredn to get the component instance
- Update the props to reset the component instance with the new VNodeData
$props
attribute - Due to the component’s
$props
Has been updated, so call the component’s _update method to make the component re-render
If the component type is different, it needs to be removed and re-rendered, and the unmounted life cycle function of the component will be executed.
function replaceVNode(prevVNode, nextVNode, container) {
container.removeChild(prevVNode.el)
// If the VNode type to be removed is a component, call the unmounted hook function of the component instance
if (prevVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) {
// A VNode of type stateful component whose children property is used to store component instance objects
const instance = prevVNode.children
instance.unmounted && instance.unmounted()
}
mount(nextVNode, container)
}
Copy the code
A special emphasis onshouldUpdateComponent
There is no shouldUpdateComponent in VUe2. In some cases, the component does not need to be updated, but the component still runs an update. Therefore, we used a simple comparison between patchFlag and props to determine whether to update. That’s what shouldUpdateComponent is for.
Update functional components
Both stateful and functional components are updated by components that execute _update to produce a comparison between the old and new VNodes.
- Functional component acceptances can only be passed in the mount phase
function mountFunctionalComponent(vnode, container, isSVG) {
/ / get props
const props = vnode.data
// Get the VNode function and pass props to the function
const $vnode = (vnode.children = vnode.tag(props))
/ / a mount
mount($vnode, container, isSVG)
// The el element references the root element of the component
vnode.el = $vnode.el
}
Copy the code
- Functional components can implement the entire mount process by defining a function and defining a handle function in VNode. The next update only needs to execute the Handle function of VNode.
vnode.handle = {
prev: null.next: vnode,
container,
update() {/ *... * /}}Copy the code
Parameter Description:
- Prev: Stores the old functional component vNodes. When first mounted, no old VNodes are available
- Next: Stores the new functional component vNode, which, when first mounted, is assigned the value of the currently mounted functional component
- Container: Mount container for storage
The specific implementation process is basically similar to stateful components, refer to the specific documentation
The next section,
The next section focuses on how diff algorithms reuse DOM elements as much as possible