Standing on the shoulders of giants and analyzing vue3, it is mainly used to record my feelings after reading the HcySunYang document and deepen my understanding. I suggest that you read original address
The core of a component is the render
function. The rest of the content, such as data, compouted, props, etc., provide data source services for the render function. The render function can produce Virtual DOM.
Virtual DOM must eventually be rendered into a real DOM. This process is called patching.
What is Vnode
Vue first compiles the template, which includes the three processes of parse, optimize, and generate.
Parse will use regular methods to parse the instructions, class, style and other data 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>
var element = {
tag: 'ul', // 节点标签名
data: { // DOM的属性,用一个对象存储键值对
class: 'item',
id: 'list'
},
children: [ // 该节点的子节点
{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"}},
]
}
As described in the code above, a template template can be described with an AST syntax tree. We use the tag
attribute to store the name of the tag, and the data
attribute to store the additional information of the tag, such as style, class, event, etc. children
used to describe the child node.
Vnode type
The above is about some common HTML tags, such as div, span, p, but in the actual code development process we will extract many components
<div>
<MyComponent />
</div>
Components like this still need to use VNode to describe <MyComponent/>. And add a logo to the VNode used to describe the component, so that when mounting, there is a way to distinguish whether a VNode is a normal html tag or a component.
const elementVNode = {
tag: 'div',
data: null,
children: {
tag: MyComponent,
data: null
}
}
Therefore, we can use tag
to determine the content to be mounted, and render the corresponding HTML structure through different rendering functions.
Components can be divided into two categories, one is functional components and the other is stateful components. A functional component is just a pure function, it does not have its own state and only receives external data; a configuration component is a class that needs to be instantiated and has its own state.
// 函数式组件
function MyComponent(props) {}
// 有状态组件
class MyComponent {}
In addition to components, there are two types that need to be described, namely Fragment and Portal.
In vue3, template no longer needs a big box to wrap all html content, which is the root element for short.
<template>
<td></td>
<td></td>
<td></td>
</template>
There is not only one td tag in the template, but multiple td tags, that is, multiple root elements. An abstract element needs to be introduced, which is the Fragment we are going to introduce. To mark the tag as Fragment, you only need to render the child nodes of the VNode to the page.
Let’s take a look at Portal again. What is Portal? Is to render the child node to the given target.
const portalVNode = {
tag: Portal,
data: {
target: '#app-root'
},
children: {
tag: 'div',
data: {
class: 'overlay'
}
}
}
No matter where you use the <Overlay/> component, it will render the content under the element with id="app-root".
In general, we can divide VNode into five categories, namely: html/svg elements, components, plain text, Fragment, and Portal:
Optimization: flags as the identifier of VNode
Since VNodes are classified into categories, it is necessary for us to use a unique identifier to indicate which category a certain VNode belongs to. At the same time, adding flags to VNode is also one of the optimization methods of the Virtual DOM algorithm.
Steps to distinguish vnode types in vue2:
- Get the vnode and try to process it as a component. If the assembly is successfully created, it means that the vnode is the formed vnode
- If it is not created successfully, check if vnode.tag has a definition, if there is a definition, treat it as a normal tag
- If vnode.tag is not defined, check whether it is a watched node
- If it is not a comment node, treat it as text.
The above judgments are made during the mounting (or patch) phase. In other words, what a VNode describes is only known at the time of mounting or patching. This brings two problems: it cannot be AOT level, and developers cannot manually optimize it.
In order to solve this problem, our idea is to indicate the type of the VNode through flags when the VNode is created, so that a lot of performance-consuming judgments can be directly avoided by using flags during the mounting or patch phase. This is also the renderer we need to talk about
// mount 函数的作用是把一个 VNode 渲染成真实 DOM,根据不同类型的 VNode 需要采用不同的挂载方式
export function mount(vnode, container) {
const { flags } = vnode
if (flags & VNodeFlags.ELEMENT) {
// 挂载普通标签
mountElement(vnode, container)
} else if (flags & VNodeFlags.COMPONENT) {
// 挂载组件
mountComponent(vnode, container)
} else if (flags & VNodeFlags.TEXT) {
// 挂载纯文本
mountText(vnode, container)
} else if (flags & VNodeFlags.FRAGMENT) {
// 挂载 Fragment
mountFragment(vnode, container)
} else if (flags & VNodeFlags.PORTAL) {
// 挂载 Portal
mountPortal(vnode, container)
}
}
So far, we have completed a certain design of VNode. The VNode objects we have designed so far are as follows:
export interface VNode {
// _isVNode 属性在上文中没有提到,它是一个始终为 true 的值,有了它,我们就可以判断一个对象是否是 VNode 对象
_isVNode: true
// el 属性在上文中也没有提到,当一个 VNode 被渲染为真实 DOM 之后,el 属性的值会引用该真实DOM
el: Element | null
flags: VNodeFlags
tag: string | FunctionalComponent | ComponentClass | null
data: VNodeData | null
children: VNodeChildren
childFlags: ChildrenFlags
}
h function to create VNode
The flags type of the component
The above mentioned how to use vnode to describe a template structure. We can use functions to control how to automatically generate vnode. This is also a core api of vue, vue3 h function
The h function returns a "virtual node", usually abbreviated as VNode: an ordinary object that contains information describing to Vue what kind of node it should render on the page, including the description of all child nodes. Its purpose is to be used for manually written rendering functions:
render() {
return h('h1', null, '')
}
function h() {
return {
_isVNode: true,
flags: VNodeFlags.ELEMENT_HTML,
tag: 'h1',
data: null,
children: null,
childFlags: ChildrenFlags.NO_CHILDREN,
el: null
}
}
The h function returns some vnode information in the code, and it needs to accept three parameters tag, data and children. One only needs to determine the types of flags and childFlags, and the others are defaulted or passed through parameters.
Determine the flags by tag
to note about is 1609b51bbcc90e: In vue2, an object is used as the description of the component. In vue3, a stateful component is a class that inherits the base class and is an object component of Vue2. We check the object The true or false of the functional property determines whether the component is a functional component. In Vue3, because the stateful component inherits the base class, the prototype chain is used to determine whether there is a definition of the render function in the prototype to determine whether the component is a stateful component
// 兼容 Vue2 的对象式组件
if (tag !== null && typeof tag === 'object') {
flags = tag.functional
? VNodeFlags.COMPONENT_FUNCTIONAL // 函数式组件
: VNodeFlags.COMPONENT_STATEFUL_NORMAL // 有状态组件
} else if (typeof tag === 'function') {
// Vue3 的类组件
flags = tag.prototype && tag.prototype.render
? VNodeFlags.COMPONENT_STATEFUL_NORMAL // 有状态组件
: VNodeFlags.COMPONENT_FUNCTIONAL // 函数式组件
}
Flags type of children
children can be divided into four types
- children is an array h('ul', null, [h('li'),h('li')])
- children is a vnode object h('div', null, h('span'))
- children is an ordinary text string h('div', null,'I am text')
- No children h('div')
Children is an array that can be divided into two types, one with key and the other without key will be marked as KEYED_VNODES. If there is no key, normalizeVNodes will be called for manual intervention to generate keys.
// 多个子节点,且子节点使用key
childFlags = ChildrenFlags.KEYED_VNODES
children = normalizeVNodes(children)
The render function renders Vnode into real DOM
The work of the renderer is mainly divided into two stages: 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 minimal resource overhead. This process is called patch
. If the old vnode does not exist, mount the new vnode directly Into a brand new DOM, this is called mount
.
The render function receives two parameters, the first parameter is the vnode object to be rendered, and the second parameter is a container used to carry content, usually called the mount point
function render(vnode, container) {
const prevVNode = container.vnode
if (prevVNode == null) {
if (vnode) {
// 没有旧的 VNode,只有新的 VNode。使用 `mount` 函数挂载全新的 VNode
mount(vnode, container)
// 将新的 VNode 添加到 container.vnode 属性下,这样下一次渲染时旧的 VNode 就存在了
container.vnode = vnode
}
} else {
if (vnode) {
// 有旧的 VNode,也有新的 VNode。则调用 `patch` 函数打补丁
patch(prevVNode, vnode, container)
// 更新 container.vnode
container.vnode = vnode
} else {
// 有旧的 VNode 但是没有新的 VNode,这说明应该移除 DOM,在浏览器中可以使用 removeChild 函数。
container.removeChild(prevVNode.el)
container.vnode = null
}
}
}
Old vnode | New vnode | operating |
---|---|---|
❌ | ✅ | Call mount function |
✅ | ❌ | Remove DOM |
✅ | ✅ | Call the patch function |
The responsibility of the renderer is very large, because it is not only a tool for rendering VNode into a real DOM, it is also responsible for the following tasks:
- Control the calling of some component life cycle hooks, the mounting of components, and the timing of unloading calls.
- Multi-end rendering bridge.
The essence of the custom renderer is to extract the method of operating dom on a specific platform from the core algorithm and provide a configurable solution
- Asynchronous rendering is directly related
The asynchronous rendering of vue3 is based on the implementation of the scheduler. If you want to achieve asynchronous rendering, the mounting wine of the component cannot be performed synchronously. The change of dom should be at the right opportunity. ? ?
- Contains the core algorithm Diff algorithm
Render normal label elements
As mentioned above when talking about flags, different tags will be marked with flags by the h function. Through the difference of flags, we can distinguish what type of content needs to be rendered. Different vnodes use different mount functions
Next, we will focus on these three issues to complete the process of rendering ordinary label elements
- After VNode is rendered as the real DOM, there is no reference to the real DOM element
- VNodeData is not applied to real DOM elements
- Did not continue to mount child nodes, namely children
Question 1
function mountElement(vnode, container) {
const el = document.createElement(vnode.tag)
vnode.el = el
container.appendChild(el)
}
Question 2 traverses VNodeData, and the switch value is applied to the element
// 拿到 VNodeData
const data = vnode.data
if (data) {
// 如果 VNodeData 存在,则遍历之
for(let key in data) {
// key 可能是 class、style、on 等等
switch(key) {
case 'style':
// 如果 key 的值是 style,说明是内联样式,逐个将样式规则应用到 el
for(let k in data.style) {
el.style[k] = data.style[k]
}
break
}
}
}
question 2 recursively mount child nodes
// 拿到 children 和 childFlags
const childFlags = vnode.childFlags
const children = vnode.children
// 检测如果没有子节点则无需递归挂载
if (childFlags !== ChildrenFlags.NO_CHILDREN) {
if (childFlags & ChildrenFlags.SINGLE_VNODE) {
// 如果是单个子节点则调用 mount 函数挂载
mount(children, el)
} else if (childFlags & ChildrenFlags.MULTIPLE_VNODES) {
// 如果是单多个子节点则遍历并调用 mount 函数挂载
for (let i = 0; i < children.length; i++) {
mount(children[i], el)
}
}
The difference between arrtibutes and props: After loading the page, the browser will parse the tags in the page and generate a DOM object that matches it. Each tag may contain some attributes. If these attributes are standard attributes, then the analysis will be generated. The corresponding attributes will also be included in the DOM object. If it is a non-standard property, it will be treated as props
For other processing such as class, arrtibutes, props, and events, you can move to document
Render plain text, Fragment and Portal
- Plain Text
Plain text is the simplest, just add elements to the page
function mountText(vnode, container) {
const el = document.createTextNode(vnode.children)
vnode.el = el
container.appendChild(el)
}
- Fragment
For Fragment, no rendering is required, only children need to be mounted. If there are multiple child nodes, then traverse the mount and call mount. If it is an empty node, create an empty text node and call mountText to mount.
VNode of 1609b51bbcd192 Fragment type, when it is rendered as a real DOM, its el attribute references
If there is only one node, then 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 is no node in the fragment, that is, an empty fragment, the el attribute refers to a placeholder Empty text node element
So what is the point of such a design?
When moving the dom element in the patch phase, you should ensure that it is placed in the correct position, instead of always using the appendChild function, and sometimes the insertBefore function is needed. At this time, we need to get the corresponding node application. At this time, the vnode.el property Is essential, even if the fragment has no child nodes, we still need a placeholder empty text node as a reference to the location.
- Portal
Portal mounting is the same as Fragment, only need to pay special attention to Portal tag is the mount point.
Who should the el attribute of Portal type VNode point to?
The content described by protal can be mounted anywhere, but a placeholder element is still required, and the el attribute of the vnode of the protal type should point to the placeholder element. This is because of another feature of Portal: Although Portal content can be rendered To any position, but it still behaves like a normal DOM element, such as the event capture/bubbling mechanism, still implements according to the DOM structure written by the code. To achieve this function, a placeholder DOM element is required to accept the event. But for now, we can use an empty text node as a placeholder
Rendering component
Still use vnode.flags to determine whether the mounted vnode is a stateful component or a functional component.
Mount a stateful component, which is the class component
class MyComponent {
render() {
return h(
'div',
{
style: {
background: 'green'
}
},
[
h('span', null, '我是组件的标题1......'),
h('span', null, '我是组件的标题2......')
]
)
}
}
// 组件挂载
function mountStatefulComponent(vnode, container) {
// 创建组件实例
const instance = new vnode.tag()
// 渲染VNode
instance.$vnode = instance.render()
// 挂载
mount(instance.$vnode, container)
// el 属性值 和 组件实例的 $el 属性都引用组件的根DOM元素
instance.$el = vnode.el = instance.$vnode.el
}
The functional component directly returns the function of the vnode
function MyFunctionalComponent() {
// 返回要渲染的内容描述,即 VNode
return h(
'div',
{
style: {
background: 'green'
}
},
[
h('span', null, '我是组件的标题1......'),
h('span', null, '我是组件的标题2......')
]
)
}
The following is the implementation of the mountFunctionalComponent function of the functional component:
function mountFunctionalComponent(vnode, container, isSVG) {
// 获取 VNode
const $vnode = vnode.tag()
// 挂载
mount($vnode, container)
// el 元素引用该组件的根元素
vnode.el = $vnode.el
}
The patch function updates the rendered DOM
In the previous section, there is no old vnode. Use the mount function to mount a brand new vnode. Then there is a suitable way for vnode to update the DOM. This is what we often call patch.
When using render to render a brand new vnode, the mount function will be called to mount the vnode, and the container element will store the reference of the vnode object. In this way, the renderer is called here to render the new vnode object to the same container element. Since the old vnode already exists, the patch function will be called to update it in a suitable way
Solution 1: There are types of vnodes. Only vnodes of the same type have comparative significance. If they are different, the best solution is to replace the old vnode with the new vnode. If the new and old VNodes are of the same type, different comparison functions will be called according to different types
Update common label elements
Option two, different tags render different content, so the comparison is meaningless
For example, only the li tag can be rendered under the ul tag, so it is meaningless to compare the ul tag with a div tag. In this case, we will not patch the old tag element, but replace it with the new tag element Old label elements.
If the old and new vnode labels are the same, the only difference is VNodeData and children. Essentially, these two values are compared.
idea when updating VNodeData divided into the following steps:
- Step 1: When a new VNodeData exists, traverse the new VNodeData.
- Step 2: According to the key in the new VNodeData, decibels try to read the old value and the new value. Namely prevValue and nextValue
- Step 3: Use switch...case statement to match different data for different update operations.
Take the update of style as an example, the update process shown in the above code is:
- Traverse the new style data and apply all the new style data to the element
- Traverse the old style data and remove those styles that do not exist in the new style data from the element
The update of child nodes is mainly to call patchChildren recursively in the patchElement function. Note that the comparison of child nodes can only be same level.
// 调用 patchChildren 函数递归地更新子节点
patchChildren(
prevVNode.childFlags, // 旧的 VNode 子节点的类型
nextVNode.childFlags, // 新的 VNode 子节点的类型
prevVNode.children, // 旧的 VNode 子节点
nextVNode.children, // 新的 VNode 子节点
el // 当前标签元素,即这些子节点的父节点
)
Because the status of child nodes can be divided into three types, one is that there is no child node, the other is that there is only one child node, and the last is the case of multiple child nodes. Therefore, there will be nine situations when the child nodes are at the same level. .
In fact, in the entire comparison of the old and new children, only when the old and new child nodes are multiple child nodes is it necessary to perform a real core diff, so as to reuse the child nodes as much as possible. following chapters of 1609b51bbcda4d will also focus on how diff reuses child nodes as much as possible.
Update plain text, Fragment and Portal
- Plain Text
The update of the plain text can read or set the content of the text node (or comment node) through the nodeValue property of the DOM object
function patchText(prevVNode, nextVNode) {
// 拿到文本元素 el,同时让 nextVNode.el 指向该文本元素
const el = (nextVNode.el = prevVNode.el)
// 只有当新旧文本内容不一致时才有必要更新
if (nextVNode.children !== prevVNode.children) {
el.nodeValue = nextVNode.children
}
}
- Fragment
Since Fragment has no wrapping elements, only child nodes, our update to Fragment is essentially to update the "child nodes" of the two fragments. Call the patchChildren function of the label element directly, just pay attention to the point of el.
- If the new fragment children is a single child node, it means that the value of its vnode.children property is the VNode object nextVNode.el = nextVNode.children.el
- If the new fragment children are empty text nodes. The prevVNode.el attribute reference is the empty text node nextVNode.el = prevVNode.el
- If the new fragment children are multiple child nodes. nextVNode.el = nextVNode.children[0].el
- Portal
The same is true for the portal. There is no element package, only the child nodes need to be compared, and pay attention to the point of el to nextVNode.el = prevVNode.el .
If the old and new containers are different, they need to be moved. This piece will not be expanded, and those who are interested can check the document
Update components
Update stateful components
Stateful component updates can be divided into two types, one is active update and
passive update.
actively update : it is an update caused by a change in the status of the component itself. For example, changes in data, etc.
passive update : it is caused by external factors of the component, such as changes in props
1. Active update
When the state of the component changes, all we need to do is to re-execute the rendering function and generate a new vnode, and finally complete the real dom update through the new and old vnodes.
For example, what should we do if we need to update such a component?
class MyComponent {
// 自身状态 or 本地状态
localState = 'one'
// mounted 钩子
mounted() {
// 两秒钟之后修改本地状态的值,并重新调用 _update() 函数更新组件
setTimeout(() => {
this.localState = 'two'
this._update()
}, 2000)
}
render() {
return h('div', null, this.localState)
}
}
You can recall the mounting steps of the component:
- Create an instance of the component
- Call component's render to get vnode
- Mount the vnode to the container element
- Both the el attribute value and the $el attribute of the component instance refer to the root DOM element of the component
We encapsulate all operations into a _update function.
function mountStatefulComponent(vnode, container, isSVG) {
// 创建组件实例
const instance = new vnode.tag()
instance._update = function() {
// 1、渲染VNode
instance.$vnode = instance.render()
// 2、挂载
mount(instance.$vnode, container, isSVG)
// 4、el 属性值 和 组件实例的 $el 属性都引用组件的根DOM元素
instance.$el = vnode.el = instance.$vnode.el
// 5、调用 mounted 钩子
instance.mounted && instance.mounted()
}
instance._update()
}
When updating, you only need to call the _update function again, that is, how to determine whether a component is rendered for the first time or need to update you, by setting a variable _mounted boolean type to mark it.
function mountStatefulComponent(vnode, container, isSVG) {
// 创建组件实例
const instance = new vnode.tag()
instance._update = function() {
// 如果 instance._mounted 为真,说明组件已挂载,应该执行更新操作
if (instance._mounted) {
// 1、拿到旧的 VNode
const prevVNode = instance.$vnode
// 2、重渲染新的 VNode
const nextVNode = (instance.$vnode = instance.render())
// 3、patch 更新
patch(prevVNode, nextVNode, prevVNode.el.parentNode)
// 4、更新 vnode.el 和 $el
instance.$el = vnode.el = instance.$vnode.el
} else {
// 1、渲染VNode
instance.$vnode = instance.render()
// 2、挂载
mount(instance.$vnode, container, isSVG)
// 3、组件已挂载的标识
instance._mounted = true
// 4、el 属性值 和 组件实例的 $el 属性都引用组件的根DOM元素
instance.$el = vnode.el = instance.$vnode.el
// 5、调用 mounted 钩子
instance.mounted && instance.mounted()
}
}
instance._update()
}
The update of components 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 vnode
2. Passive update
We can initialize the props of the component immediately after the component instance is created,instance.$props = vnode.data
this way, the child component can access the props data passed in from the parent component through this.$props.text
Give a case:
The VNode produced by the first rendering is:
const prevCompVNode = h(ChildComponent, {
text: 'one'
})
The VNode produced by the second rendering is:
const prevCompVNode = h(ChildComponent, {
text: 'two'
})
Since the rendered tags are all components, the patchComponent function will be called inside the patch function to update
function patchComponent(prevVNode, nextVNode, container) {
// 检查组件是否是有状态组件
if (nextVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) {
// 1、获取组件实例
const instance = (nextVNode.children = prevVNode.children)
// 2、更新 props
instance.$props = nextVNode.data
// 3、更新组件
instance._update()
}
}
The stateful component update can be divided into three steps:
- Get the component instance through prevVNode.childredn
- Update props, use the new VNodeData to reset the
$props
property of the component instance - Since the
$props
component has been updated, the _update method of the component is called to let the component re-render
Different component types need 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)
// 如果将要被移除的 VNode 类型是组件,则需要调用该组件实例的 unmounted 钩子函数
if (prevVNode.flags & VNodeFlags.COMPONENT_STATEFUL_NORMAL) {
// 类型为有状态组件的 VNode,其 children 属性被用来存储组件实例对象
const instance = prevVNode.children
instance.unmounted && instance.unmounted()
}
mount(nextVNode, container)
}
Special emphasis shouldUpdateComponent
There is no life cycle of shouldUpdateComponent in vue2. In some cases, the component does not need to be updated, but the component still ran an update. Therefore, we use a simple comparison of patchFlag and props to determine whether to update. This is the role of shouldUpdateComponent.
Update functional components
Whether it is a stateful component or a functional component, the component _update
new and old vnodes generated by executing 0609b51bbce4a6 to complete the update.
- Functional components accept props can only be passed in the mount phase
function mountFunctionalComponent(vnode, container, isSVG) {
// 获取 props
const props = vnode.data
// 获取 VNode 执行函数并将props传递到函数中
const $vnode = (vnode.children = vnode.tag(props))
// 挂载
mount($vnode, container, isSVG)
// el 元素引用该组件的根元素
vnode.el = $vnode.el
}
- The functional component implements the entire mounting process by defining a function and defining a handle function in VNode. The next time you update, you only need to execute the handle function of vnode.
vnode.handle = {
prev: null,
next: vnode,
container,
update() {/*...*/}
}
Parameter Description:
- prev: Store the old functional component vnode, there is no old vnode when it is first mounted
- next: Store the new functional component vnode, which is assigned to the functional component currently being mounted when it is first mounted
- container: the mounted container for storage
The specific implementation process is basically similar to that of stateful components. For details, please refer to document
Next section
The next section mainly introduces if the diff algorithm reuses dom elements as much as possible
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。