# 渲染机制
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文档中。