# 渲染机制
Vue
的整个渲染过程,一共经历了5
个步骤:
模板解析,通过
htmlparser2
的库来解析模板生成包含指令节点、属性节点和文本节点的AST
树(抽象语法树)。生成渲染函数,先进行
AST
静态分析,再转换为渲染函数(render
函数)。挂载(mount),挂载时会执行渲染函数,并生成
VNode
(虚拟节点),并基于此创建实际的 DOM 节点。更新,执行 diff 算法,然后将必要的更新应用到真实 DOM 上去。。
# 1.模板解析
当Vue
应用启动时,会将模板传入Vue的编译器
进行解析和编译
。Vue的编译器
将模板解析成一个抽象语法树(AST),并将AST
转换为渲染函数
。
AST 是一种树状的数据结构,表示了模板中各个语法结构之间的关系。语法分析的目的是理解模板的结构,为后续的优化和代码生成提供基础。
下面是Vue
模板解析的基本流程:
1.解析
编译器首先会对Vue模板进行解析,将模板字符串解析为抽象语法树(AST)。AST是一种以对象的形式表示模板结构的数据结构。
2.静态分析
在此阶段,编译器会对AST进行静态分析,收集模板中的所有静态节点(不依赖组件实例数据的节点)。这样的静态节点可以在每次重新渲染时重复使用,从而提高性能。
# 2、生成渲染函数
解析完模板后,Vue
将AST
转换为渲染函数
(在编译阶段就完成了)。渲染函数
是一个JavaScript函数
,这个渲染函数接受组件实例的状态作为参数,然后执行渲染逻辑,生成 VNode
。
下面是一个简单的示例,假设我们有这样一个 Vue
模板:
<template>
<div>
<p>{{ message }}</p>
<button @click="handleClick">Click Me</button>
</div>
</template>
2
3
4
5
6
编译器将会把这个模板解析为 AST
,然后遍历 AST
生成一个渲染函数:
这是一个打印出来的组件对象:
function render(_ctx, _cache) {
// 返回一个虚拟节点
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */),
_createVNode("button", { onClick: _cache[1] || (_cache[1] = $event => _ctx.handleClick($event)) }, "Click Me")
]))
}
2
3
4
5
6
7
上面的渲染函数中,_ctx
是组件实例的上下文对象,_cache
是一个缓存对象,存储了事件处理函数等。
# 3.挂载
挂载时会首次执行渲染函数,渲染函数会根据当前的渲染上下文数据生成虚拟DOM节点。
render: function (createElement) {
return createElement(
"div", { innerHtml: "456" }
)
}
2
3
4
5
上面的代码执行后,会返回 vnode
,大概结构是这样的:
提示
createElement 别名 h 函数,h 函数接受三个参数:节点类型(字符串或组件)、属性对象、子节点,是 vue 提供的用于创建 vnode 的辅助函数。
# diff算法
diff
算法,也称为 “patch” 算法,是一种智能比较 Virtual DOM 的算法,旨在最小化对真实 DOM 的操作次数,提高渲染性能。
步骤:
- 节点比对:对新旧
VNode
进行深度优先遍历,比较节点类型、key
、data
等,找出相同节点和需要更新的节点。 - 对比完成后,得到了需要更新、添加和删除的节点,
Vue
会将这些操作打包成一个补丁(patch)
然后将补丁应用到真实DOM
上,从而完成更新。
实现:
- 1、如果新旧节点是同一个节点,直接比较它们的子节点。
- 2、如果新旧节点不是同一个节点,且它们都有子节点,则使用“最长递增子序列”算法对子节点进行匹配,将不需要更新的节点复用,需要更新的节点打上标记。
- 3、如果新旧节点不是同一个节点,且旧节点没有子节点,直接用新节点替换旧节点。
- 4、如果新旧节点不是同一个节点,且新节点没有子节点,删除旧节点。
判断是否是同一个节点
判断两个
VNode
节点是否是同一个节点,需要满足以下条件key
相同tag
(当前节点的标签名)相同isComment
(是否为注释节点)相同是否data(当前节点对应的对象,包含了具体的一些数据信息,是一个
VNodeData
类型,可以参考VNodeData
类型中的数据信息)都有定义
当标签是<input>
的时候,type
必须相同
:::
# 更新DOM
在Vue
执行完渲染函数并且对比新旧的vnode树后,如果发现需要更新DOM,那么就会执行patch
函数来完成DOM更新。
patch
函数定义在src/core/vdom/patch.js
中,其主要作用是比较新旧节点的差异,然后进行最小化的DOM操作,从而尽可能减少DOM操作的次数,提高渲染效率。
下面是patch函数的简化代码示例:
function patch(oldVnode, vnode) {
if (sameVnode(oldVnode, vnode)) {
// 如果新旧节点相同,执行updateChildren函数更新子节点
updateChildren(oldVnode, vnode)
} else {
// 如果新旧节点不同,直接替换旧节点
const parent = api.parentNode(oldVnode.elm)
api.insertBefore(parent, createElm(vnode), api.nextSibling(oldVnode.elm))
api.removeChild(parent, oldVnode.elm)
}
}
2
3
4
5
6
7
8
9
10
11
在代码示例中,updateChildren
函数用于更新新旧节点的子节点,它的实现过程就是通过比较新旧节点的子节点,找出它们之间的差异,然后进行最小化的DOM
操作。如果新旧节点本身不同,则直接通过DOM
操作替换旧节点。
总之,patch
函数的主要作用就是通过最小化的DOM
操作,将新的vnode
树渲染成真实的DOM
树,并将其插入到HTML
文档中。