渲染整个流程
- 模板解析(parse):生成 AST Vue 需要将用户编写的 template(或 el 挂载的 DOM 模板)转换为可执行的 渲染函数(render function),这一步是 "模板 → 逻辑" 的转换。
代码生成(generate):AST → 渲染函数
- 渲染函数的核心是调用 h 函数(即 createElement),h 函数会生成虚拟 DOM(VNode)
- 虚拟 DOM → 真实 DOM(patch 首次渲染)
将模板编译成渲染函数
将模板编译成渲染函数可以分两个步骤,先将模板解析成AST(Abstract Syntax Tree,抽象语法树),然后再使用AST生成渲染函数。 在大体逻辑模版编译 分为三部分内容:
- 将模版解析成AST
- 遍历AST标记静态节点
- 使用AST生成渲染函数
模版编译的整体流程

渲染函数的作用是创建vnode
模板编译在整个渲染过程的位置 什么是模版编译 如何将模版编译成渲染函数
先将模版解析抽象语法树(AST)然后遍历AST标记静态节点 最后使用AST生成代码字符串。这部分内容分别对应内容分别对应三个模块: 解析器 优化器 代码生成器 。
解析器
AST数据结构
markdown
/**
* @internal
* 抽象语法树(AST)元素节点类型定义,用于表示模板中的HTML元素
*/
export type ASTElement = {
/**
* 节点类型标记,1表示元素节点
*/
type: 1
/**
* 元素标签名
*/
tag: string
/**
* 元素属性列表,按原始顺序存储
*/
attrsList: Array<ASTAttr>
/**
* 元素属性映射,键为属性名,值为属性值
*/
attrsMap: { [key: string]: any }
/**
* 原始属性映射,键为属性名,值为完整的ASTAttr对象
*/
rawAttrsMap: { [key: string]: ASTAttr }
/**
* 父元素节点引用
*/
parent: ASTElement | void
/**
* 子节点数组,包含元素、文本或表达式节点
*/
children: Array<ASTNode>
/**
* 元素在原始模板中的起始位置索引
*/
start?: number
/**
* 元素在原始模板中的结束位置索引
*/
end?: number
/**
* 是否已处理标记
*/
processed?: true
/**
* 是否为静态节点(内容不会随数据变化)
*/
static?: boolean
/**
* 是否为静态根节点(可被提升的静态节点)
*/
staticRoot?: boolean
/**
* 是否在v-for循环中的静态节点
*/
staticInFor?: boolean
/**
* 静态节点是否已处理
*/
staticProcessed?: boolean
/**
* 是否包含动态绑定
*/
hasBindings?: boolean
/**
* 元素的文本内容
*/
text?: string
/**
* 处理后的属性列表
*/
attrs?: Array<ASTAttr>
/**
* 动态属性列表
*/
dynamicAttrs?: Array<ASTAttr>
/**
* 组件属性(props)列表
*/
props?: Array<ASTAttr>
/**
* 是否为普通元素(没有特殊指令或绑定)
*/
plain?: boolean
/**
* 是否为<pre>标签,需要保留空白
*/
pre?: true
/**
* 元素的XML命名空间
*/
ns?: string
/**
* 组件名称
*/
component?: string
/**
* 是否使用内联模板
*/
inlineTemplate?: true
/**
* 过渡模式(in-out/out-in)
*/
transitionMode?: string | null
/**
* 插槽名称
*/
slotName?: string | null
/**
* 插槽目标
*/
slotTarget?: string | null
/**
* 插槽目标是否为动态的
*/
slotTargetDynamic?: boolean
/**
* 插槽作用域标识符
*/
slotScope?: string | null
/**
* 作用域插槽映射
*/
scopedSlots?: { [name: string]: ASTElement }
/**
* 引用标识符
*/
ref?: string
/**
* 引用是否在v-for循环中
*/
refInFor?: boolean
/**
* v-if条件表达式
*/
if?: string
/**
* v-if是否已处理
*/
ifProcessed?: boolean
/**
* v-else-if条件表达式
*/
elseif?: string
/**
* 是否为v-else分支
*/
else?: true
/**
* if条件分支列表
*/
ifConditions?: ASTIfConditions
/**
* v-for表达式
*/
for?: string
/**
* v-for是否已处理
*/
forProcessed?: boolean
/**
* 循环key属性
*/
key?: string
/**
* 循环变量别名
*/
alias?: string
/**
* 循环索引变量名
*/
iterator1?: string
/**
* 对象循环的值变量名
*/
iterator2?: string
/**
* 静态CSS类名
*/
staticClass?: string
/**
* 动态类绑定表达式
*/
classBinding?: string
/**
* 静态样式字符串
*/
staticStyle?: string
/**
* 动态样式绑定表达式
*/
styleBinding?: string
/**
* 事件处理器映射
*/
events?: ASTElementHandlers
/**
* 原生事件处理器映射
*/
nativeEvents?: ASTElementHandlers
/**
* 过渡动画名称或标记
*/
transition?: string | true
/**
* 初始渲染时是否应用过渡动画
*/
transitionOnAppear?: boolean
/**
* v-model指令配置
*/
model?: {
value: string // v-model绑定的值
callback: string // 更新值的回调函数
expression: string // 原始表达式
}
/**
* 应用在元素上的指令列表
*/
directives?: Array<ASTDirective>
/**
* 是否为禁止使用的元素
*/
forbidden?: true
/**
* 是否使用v-once指令
*/
once?: true
/**
* v-once是否已处理
*/
onceProcessed?: boolean
/**
* 数据包装函数,用于自定义代码生成
*/
wrapData?: (code: string) => string
/**
* 监听器包装函数,用于自定义代码生成
*/
wrapListeners?: (code: string) => string
/**
* 2.4版本SSR优化标记,指示节点的可优化程度
*/
ssrOptimizability?: number
}
解析器内部运行原理
解析器内部也分了好几个子解析器,比如HTML解析器、文本解析器以及过滤器解析器
html解析器
解析过程
ini
// 定义 AST 元素类
class ASTElement {
constructor(tag, attrs) {
this.type = 1; // 类型为元素
this.tag = tag; // 标签名
this.attrsList = attrs || []; // 属性列表
this.parent = null; // 父节点
this.children = []; // 子节点列表
this.text = ''; // 文本内容
}
}
// 定义 AST 文本类
class ASTText {
constructor(text) {
this.type = 3; // 类型为文本
this.text = text; // 文本内容
}
}
// 简化的 parseHTML 函数
function parseHTML(html) {
let index = 0; // 当前解析位置索引
let root; // AST 根节点
let currentParent; // 当前父节点
const stack = []; // 节点栈,用于处理嵌套关系
// 循环直到 html 字符串为空
while (html) {
// 查找下一个标签开始的位置
let textEnd = html.indexOf('<');
if (textEnd === 0) {
// 处理注释、条件注释、Doctype等特殊标签
// 匹配注释
const commentMatch = html.match(/^<!--([\s\S]*?)-->/);
if (commentMatch) {
advance(commentMatch[0].length); // 移动指针跳过注释
continue;
}
// 匹配 Doctype
const doctypeMatch = html.match(/^<!DOCTYPE [^>]+>/i);
if (doctypeMatch) {
advance(doctypeMatch[0].length); // 移动指针跳过 Doctype
continue;
}
// 处理结束标签
const endTagMatch = html.match(/^<\/([a-zA-Z_][^\t\r\n\f />]*)/);
if (endTagMatch) {
const tagName = endTagMatch[1];
advance(endTagMatch[0].length); // 移动指针跳过结束标签
closeElement(tagName.toLowerCase()); // 关闭当前元素
continue;
}
// 处理开始标签
const startTagMatch = html.match(/^[<]([a-zA-Z_][^\t\r\n\f />]+)/);
if (startTagMatch) {
const tagName = startTagMatch[1];
const attrMatch = [];
let end, attr;
// 匹配属性
while (
!(end = html.match(/^\/?>/)) && // 查找结束符号 > 或 />
(attr = html.match(
/^([^=><"'` ]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+))?/
))
) {
attrMatch.push(attr); // 收集属性
advance(attr[0].length); // 移动指针跳过属性
}
advance(end[0].length); // 移动指针跳过结束符号
const element = new ASTElement(tagName.toLowerCase(), attrMatch); // 创建 AST 元素
processAttrs(element); // 处理属性
if (!root) {
root = element; // 设置根节点
}
if (currentParent) {
currentParent.children.push(element); // 将元素添加到当前父节点的子节点列表
element.parent = currentParent; // 设置父节点
}
if (!end[0].includes('/')) {
stack.push(element); // 将元素压入栈中
currentParent = element; // 更新当前父节点
}
continue;
}
}
// 处理文本节点
if (textEnd >= 0) {
const text = html.substring(0, textEnd); // 获取文本内容
if (text) {
currentParent.children.push(new ASTText(text)); // 将文本节点添加到当前父节点的子节点列表
}
advance(textEnd); // 移动指针跳过文本
}
// 如果还有剩余的 HTML 字符串,则将其作为文本节点处理
if (html) {
const remainingText = html.trim();
if (remainingText) {
currentParent.children.push(new ASTText(remainingText)); // 将剩余文本作为文本节点添加
advance(html.length); // 移动指针跳过剩余字符串
}
}
}
return root; // 返回 AST 根节点
// 辅助函数:移动指针
function advance(n) {
index += n; // 更新索引
html = html.substring(n); // 截取剩余字符串
}
// 辅助函数:关闭元素
function closeElement(tagName) {
let element = stack.pop(); // 弹出栈顶元素
if (element && element.tag !== tagName) {
console.warn(`Mismatched tags: ${tagName}`); // 报告不匹配的标签
}
currentParent = stack[stack.length - 1]; // 更新当前父节点为新的栈顶元素
}
// 辅助函数:处理属性
function processAttrs(element) {
element.attrsList.forEach(attr => {
const name = attr[1]; // 属性名
const value = attr[3] || attr[4] || attr[5] || true; // 属性值
element.attrs[name] = value; // 将属性添加到元素的 attrs 对象中
});
}
}
// 示例用法
const htmlString = '<div id="app"><span class="greeting">Hello, Vue!</span></div>';
const astRoot = parseHTML(htmlString);
console.log(JSON.stringify(astRoot, null, 2)); // 打印生成的 AST 树
流程图
ini
// 定义 AST 元素类
class ASTElement {
constructor(tag, attrs) {
this.type = 1; // 类型为元素
this.tag = tag; // 标签名
this.attrsList = attrs || []; // 属性列表
this.parent = null; // 父节点
this.children = []; // 子节点列表
this.text = ''; // 文本内容
}
}
// 定义 AST 文本类
class ASTText {
constructor(text) {
this.type = 3; // 类型为文本
this.text = text; // 文本内容
}
}
// 简化的 parseHTML 函数
function parseHTML(html) {
let index = 0; // 当前解析位置索引
let root; // AST 根节点
let currentParent; // 当前父节点
const stack = []; // 节点栈,用于处理嵌套关系
// 循环直到 html 字符串为空
while (html) {
// 查找下一个标签开始的位置
let textEnd = html.indexOf('<');
if (textEnd === 0) {
// 处理注释、条件注释、Doctype等特殊标签
// 匹配注释
const commentMatch = html.match(/^<!--([\s\S]*?)-->/);
if (commentMatch) {
advance(commentMatch[0].length); // 移动指针跳过注释
continue;
}
// 匹配 Doctype
const doctypeMatch = html.match(/^<!DOCTYPE [^>]+>/i);
if (doctypeMatch) {
advance(doctypeMatch[0].length); // 移动指针跳过 Doctype
continue;
}
// 处理结束标签
const endTagMatch = html.match(/^<\/([a-zA-Z_][^\t\r\n\f />]*)/);
if (endTagMatch) {
const tagName = endTagMatch[1];
advance(endTagMatch[0].length); // 移动指针跳过结束标签
closeElement(tagName.toLowerCase()); // 关闭当前元素
continue;
}
// 处理开始标签
const startTagMatch = html.match(/^[<]([a-zA-Z_][^\t\r\n\f />]+)/);
if (startTagMatch) {
const tagName = startTagMatch[1];
const attrMatch = [];
let end, attr;
// 匹配属性
while (
!(end = html.match(/^\/?>/)) && // 查找结束符号 > 或 />
(attr = html.match(
/^([^=><"'` ]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+))?/
))
) {
attrMatch.push(attr); // 收集属性
advance(attr[0].length); // 移动指针跳过属性
}
advance(end[0].length); // 移动指针跳过结束符号
const element = new ASTElement(tagName.toLowerCase(), attrMatch); // 创建 AST 元素
processAttrs(element); // 处理属性
if (!root) {
root = element; // 设置根节点
}
if (currentParent) {
currentParent.children.push(element); // 将元素添加到当前父节点的子节点列表
element.parent = currentParent; // 设置父节点
}
if (!end[0].includes('/')) {
stack.push(element); // 将元素压入栈中
currentParent = element; // 更新当前父节点
}
continue;
}
}
// 处理文本节点
if (textEnd >= 0) {
const text = html.substring(0, textEnd); // 获取文本内容
if (text) {
currentParent.children.push(new ASTText(text)); // 将文本节点添加到当前父节点的子节点列表
}
advance(textEnd); // 移动指针跳过文本
}
// 如果还有剩余的 HTML 字符串,则将其作为文本节点处理
if (html) {
const remainingText = html.trim();
if (remainingText) {
currentParent.children.push(new ASTText(remainingText)); // 将剩余文本作为文本节点添加
advance(html.length); // 移动指针跳过剩余字符串
}
}
}
return root; // 返回 AST 根节点
// 辅助函数:移动指针
function advance(n) {
index += n; // 更新索引
html = html.substring(n); // 截取剩余字符串
}
// 辅助函数:关闭元素
function closeElement(tagName) {
let element = stack.pop(); // 弹出栈顶元素
if (element && element.tag !== tagName) {
console.warn(`Mismatched tags: ${tagName}`); // 报告不匹配的标签
}
currentParent = stack[stack.length - 1]; // 更新当前父节点为新的栈顶元素
}
// 辅助函数:处理属性
function processAttrs(element) {
element.attrsList.forEach(attr => {
const name = attr[1]; // 属性名
const value = attr[3] || attr[4] || attr[5] || true; // 属性值
element.attrs[name] = value; // 将属性添加到元素的 attrs 对象中
});
}
}
// 示例用法
const htmlString = '<div id="app"><span class="greeting">Hello, Vue!</span></div>';
const astRoot = parseHTML(htmlString);
console.log(JSON.stringify(astRoot, null, 2)); // 打印生成的 AST 树

当上面这个模版被HTML解析器解析时 ,所触发的钩子函数依次是 start start chars end 和 end

优化器
代码生成器
代码生成器是模板编译的最后一步,它的作用是将AST转换成渲染函数中的内容,这个内容可以称为代码字符串。代码字符串可以被包装在函数中执行,这个函数就是我们通常所说的渲染函数。渲染函数被执行之后,可以生成一份VNode,而虚拟DOM可以通过这个VNode来渲染视图。关于虚拟DOM如何使用VNode渲染视图,我们在第二篇中已经介绍过。
核心代码
scss
// 创建虚拟DOM元素的内部实现函数
export function _createElement(
// 组件上下文实例
context: Component,
// 标签名(字符串)、组件选项对象、构造函数或异步组件工厂函数
tag?: string | Component | Function | Object,
// 虚拟节点的数据对象,包含props、attrs、事件等
data?: VNodeData,
// 子节点,可以是字符串、数组或函数
children?: any,
// 子节点规范化类型:1-简单规范化,2-总是规范化
normalizationType?: number
): VNode | Array<VNode> {
// 检查data是否为响应式对象(有__ob__属性表示被Vue观测过)
if (isDef(data) && isDef((data as any).__ob__)) {
// 开发环境下警告:避免使用响应式对象作为vnode数据
__DEV__ &&
warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(
data
)}\n` + 'Always create fresh vnode data objects in each render!',
context
)
// 返回空虚拟节点,避免使用响应式数据
return createEmptyVNode()
}
// 处理动态组件语法(:is绑定)
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
// 将data.is的值赋给tag,实现动态组件
tag = data.is
}
// 如果tag不存在(可能是:is绑定到了假值)
if (!tag) {
// in case of component :is set to falsy value
// 返回空虚拟节点
return createEmptyVNode()
}
// 开发环境下检查key是否为原始值(字符串或数字)
// warn against non-primitive key
if (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {
// 警告:避免使用非原始值作为key,应该使用字符串或数字
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
// 支持将单个函数子节点作为默认作用域插槽
// support single function children as default scoped slot
if (isArray(children) && isFunction(children[0])) {
// 确保data对象存在
data = data || {}
// 将第一个函数子节点设置为默认作用域插槽
data.scopedSlots = { default: children[0] }
// 清空children数组,避免重复处理
children.length = 0
}
// 根据规范化类型处理子节点
if (normalizationType === ALWAYS_NORMALIZE) {
// 总是规范化:处理嵌套数组和函数子节点
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 简单规范化:只处理基本的一维数组
children = simpleNormalizeChildren(children)
}
// 声明虚拟节点和命名空间变量
let vnode, ns
// 判断tag类型:字符串标签 vs 组件对象
if (typeof tag === 'string') {
// 组件构造函数变量
let Ctor
// 获取标签的命名空间(SVG、MathML等特殊命名空间)
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 检查是否为平台保留标签(HTML标签或SVG标签)
if (config.isReservedTag(tag)) {
// platform built-in elements
// 开发环境下检查.native修饰符的使用是否正确
if (
__DEV__ &&
isDef(data) &&
isDef(data.nativeOn) &&
data.tag !== 'component'
) {
// 警告:.native修饰符只能用于组件,不能用于普通元素
warn(
`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
context
)
}
// 创建平台内置元素的虚拟节点
vnode = new VNode(
// 解析平台特定的标签名(处理如<transition>等抽象组件)
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context
)
} else if (
// 检查是否为已注册的组件
(!data || !data.pre) &&
isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
) {
// component
// 创建组件虚拟节点
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// 处理未知或未列出的命名空间元素
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
// 创建通用虚拟节点,可能在父节点规范化时分配命名空间
vnode = new VNode(tag, data, children, undefined, undefined, context)
}
} else {
// 直接处理组件选项对象或构造函数
// direct component options / constructor
// 创建组件虚拟节点
vnode = createComponent(tag as any, data, context, children)
}
// 处理返回结果:可能是数组或单个虚拟节点
if (isArray(vnode)) {
// 如果是数组,直接返回(处理函数式组件的情况)
return vnode
} else if (isDef(vnode)) {
// 如果虚拟节点存在
// 应用命名空间
if (isDef(ns)) applyNS(vnode, ns)
// 注册深度绑定(处理样式和类的响应式依赖)
if (isDef(data)) registerDeepBindings(data)
// 返回虚拟节点
return vnode
} else {
// 如果虚拟节点不存在,返回空虚拟节点
return createEmptyVNode()
}
}
代码字符串中的 _c其实是createElement的别名。createElement是虚拟DOM中所提供的方法,它的作用是创建虚拟节点,有三个参数,分别是:
- 标签名
- 一个包含模板相关属性的数据对象
- 子节点列表
我们介绍了代码生成器的作用及其内部原理,了解了代码生成器其实就是字符串拼接的过程。通过递归AST来生成字符串,最先生成根节点,然后在子节点字符串生成后,将其拼接在根节点的参数中,子节点的子节点拼接在子节点的参数中,这样一层一层地拼接,直到最后拼接成完整的字符串。同时还介绍了三种类型的节点,分别是元素节点、文本节点与注释节点。而不同类型的节点生成字符串的方式是不同的。最后,我们介绍了当字符串拼接好后,会将字符串拼在with中返回给调用者。