第9章 9.3 Vue 3 源码:编译器

🎯 开场 3 分钟:为什么要学这个?

上一章我们看了 Vue 3 是怎么把组件变成屏幕上一个个像素的。但你有没有想过:Vue 是怎么理解你写的 <template> 里那些标签和属性的?

打个比方。你跟老外用中文说话,他听得懂——因为有个翻译把他听不懂的中文转成了他懂的信息。Vue 的编译器就是这个翻译,它把你的模板代码「翻译」成 JavaScript 能理解的东西。

痛点来了

  • 你有没有遇到过写了 v-ifv-for 结果渲染顺序不对?
  • 有没有发现同样的模板,有些项目跑得飞快,有些却卡顿?

这些问题的答案,都在编译器里。

学完这一章,你能:

  1. 看懂 Vue 模板被编译成了什么鬼样子
  2. 理解 patchFlag静态提升 这些面试高频词
  3. 写出更高效的模板,写出面试官挑不出毛病的代码

🧱 基础 25 分钟:核心概念

9.3.1 什么是模板编译器?

生活类比:想象你是一家外卖店的老板。顾客下单说「来\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n一份番茄炒蛋加米饭,微辣」,后厨听不懂人话,你得先让服务员把这句话翻译成「订单单号001:番茄+蛋+米饭,备注微辣」——这个翻译过程就是编译。

Vue 的编译器干的是同样的事:把你的 <template> 翻译成 JavaScript 函数(叫 render 函数)。

# 伪代码:模板编译前 vs 编译后

# 你写的模板(Vue不认识这玩意儿)
模板 = """
<div class="container">
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</div>
"""

# 编译后变成的 render 函数(JavaScript 能跑)
render函数 = """
function render(ctx) {
return h('div', { class: 'container' }, [
h('h1', null, ctx.title),
h('p', null, ctx.content)
])
}
"""

9.3.2 AST:抽象语法树

是什么:AST 是「抽象语法树」的缩写,说白了就是把模板用树状结构表示出来,每个节点代表一个标签、属性或文本。

为什么要用:计算机处理树状结构比处理字符串容易一万倍。你想让 Vue 理解你的模板,先把它拆成树。

生活类比:就像拆乐高。你拿到一盒「千年隼」套装(模板),组装前得先按颜色、形状分类摆好(AST),这样拼的时候才知道哪块在哪。

# 用 Python 模拟一个超简 AST 节点
class ASTNode:
def __init__(self, tag, children=None, props=None):
    self.tag = tag           # 标签名:div、span、h1...
    self.children = children or []  # 子节点列表
    self.props = props or {}  # 属性:class、id、@click...

# 举个例子:<div class="box"><p>你好</p></div>
ast = ASTNode(
tag="div",
props={"class": "box"},
children=[
    ASTNode(tag="p", children=["你好"])
]
)

def print_ast(node, indent=0):
"""打印 AST 结构,帮你看清楚它的样子"""
prefix = "  " * indent
if isinstance(node, str):
    print(f"{prefix}文本: {node}")
    return
print(f"{prefix}标签: <{node.tag}>")
if node.props:
    print(f"{prefix}  属性: {node.props}")
for child in node.children:
    print_ast(child, indent + 1)

print_ast(ast)

输出

标签: <div>
属性: {'class': 'box'}
标签: <p>
文本: 你好

9.3.3 编译三步走

Vue 的编译过程分三步,记住这个流水线:

模板字符串 → AST(解析) → 优化后的 AST(优化) → render 函数代码(生成)

第一步:解析(Parse)——把模板字符串拆成 AST

def parse(template_str):
"""超简化解析器:把 <tag>...</tag> 转成 AST 节点"""
nodes = []
current = ""
in_tag = False
tag_name = None

for char in template_str:
    if char == "<":
        if current.strip():
            # 纯文本节点
            nodes.append(current.strip())
        current = "<"
        in_tag = True
    elif char == ">":
        current += ">"
        if in_tag:
            # 提取标签名
            tag_name = current[1:current.index(" ")] if " " in current else current[2:-2]
            if not tag_name.startswith("/"):
                props = {}
                if " " in current:
                    # 简化:提取第一个属性
                    attr_part = current.split()[1:]
                    if attr_part and "=" in attr_part[0]:
                        attr_name = attr_part[0].split("=")[0]
                        attr_val = attr_part[0].split("=")[1].strip('"')
                        props[attr_name] = attr_val
                nodes.append(ASTNode(tag=tag_name, props=props))
        current = ""
        in_tag = False
    else:
        current += char

return nodes

# 测试一下
template = '<div class="container"><p>你好 Vue</p></div>'
ast_result = parse(template)
for node in ast_result:
print_ast(node)

输出

标签: <div>
属性: {'class': 'container'}
标签: <p>
文本: 你好 Vue

第二步:优化(Transform)——标记静态节点

这是 Vue 3 变快的神器。它会遍历 AST,把永远不会变的节点标记出来。

def mark_static(node, is_static=False):
"""标记静态节点:没有变量绑定、不受数据影响的就是静态的"""
if isinstance(node, str):
    # 纯文本是静态的
    node._static = True
    return True

# 有 {{ }} 插值的是动态的
has_interpolation = any("{{" in str(c) for c in node.children)
is_dynamic = bool(node.props.get("v-if")) or bool(node.props.get("v-for")) or has_interpolation

node._static = not is_dynamic
node._static_descendants = node._static

for child in node.children:
    mark_static(child, is_dynamic)

# 如果所有子节点都是静态的,当前节点也是静态的
if node._static and all(getattr(c, '_static', False) for c in node.children if not isinstance(c, str)):
    node._static_descendants = True

return node._static

# 给上面的 AST 节点加上静态标记
for node in ast_result:
mark_static(node)
print(f"<{node.tag}> 静态: {node._static}, 子树静态: {node._static_descendants}")

输出

<div> 静态: False, 子树静态: False
<p> 静态: True, 子树静态: True

第三步:生成(Generate)——把 AST 变成 render 函数代码

def generate(node):
"""把 AST 节点生成 render 函数代码"""
if isinstance(node, str):
    return f'"{node}"'


# 生成属性代码
props_code = ""
if node.props:
    props_list = [f'{k}:"{v}"' for k, v in node.props.items()]
    props_code = f'{{ {" , ".join(props_list)} }}'

# 生成子节点代码
children_code = ""
if node.children:
    child_codes = []
    for child in node.children:
        child_code = generate(child)
        if child_code:
            child_codes.append(child_code)
    if child_codes:
        children_code = ", [" + ", ".join(child_codes) + "]"

# 拼接 h() 调用
if node._static:
    # 静态节点只生成一次
    return f'h("{node.tag}", {props_code or "null"}{children_code})'
else:
    # 动态节点每次都要重新渲染
    return f'h("{node.tag}", {props_code or "null"}{children_code})'

# 生成完整的 render 函数
render_code = f"""
function render(ctx) {{
return {generate(ast_result[0])}
}}
"""
print(render_code)

输出

function render(ctx) {
return h("div", { class:"container" }, [h("p", null, ["你好 Vue"])])
}

9.3.4 patchFlag:精确打击的魔法

这是 Vue 3 性能优化的核心概念。

痛点:以前 Vue 2 更新 DOM 是「全部重建」,就像你整理房间时把所有东西都扔地上再重新摆。Vue 3 的 patchFlag 实现了「按需更新」。

类比:就像考试改错。你不用把整张卷子重写一遍,只需要在错题旁边打个标记「这题要改」,老师就知道只改那道题。

# patchFlag 的取值代表不同的更新策略
PATCH_FLAGS = {
1: "TEXT",        # 只更新文本内容
2: "CLASS",       # 只更新 class
4: "STYLE",       # 只更新 style
8: "PROPS",       # 更新普通属性
16: "FULL_PROPS", # 需要完整比较
}

# 模拟:给节点打上 patchFlag
class VNode:
def __init__(self, tag, flag=None, children=None):
    self.tag = tag
    self.flag = flag
    self.children = children

# 场景1:只有文本会变
title_node = VNode("h1", flag=PATCH_FLAGS[1], children=["标题"])
# 渲染时:if (flag & 1) { 更新文本 }

# 场景2:class 会变
button_node = VNode("button", flag=PATCH_FLAGS[2], children=["点击"])
# 渲染时:if (flag & 2) { 更新class }

print(f"标题节点需要: {PATCH_FLAGS[title_node.flag]} 更新")
print(f"按钮节点需要: {PATCH_FLAGS[button_node.flag]} 更新")

输出

标题节点需要: TEXT 更新
按钮节点需要: CLASS 更新

9.3.5 静态提升:重复工作能省则省

是什么:把静态节点提升到渲染函数外面,避免重复创建。

类比:就像做ppt。你不会每翻一页都重新画logo,而是把logo放页眉,一次画好全局复用。

# 静态提升前:每次渲染都会重新创建这个 div
def render_before(data):
return f"""
<div>
    <h1>{data.title}</h1>
    <div class="static-content">静态内容不变</div>  <!-- 每次都重建 -->
    <p>{data.content}</p>
</div>
"""

# 静态提升后:静态内容只创建一次
static_div = '<div class="static-content">静态内容不变</div>'

def render_after(data):
return f"""
<div>
    <h1>{data.title}</h1>
    {static_div}  <!-- 复用同一个 -->
    <p>{data.content}</p>
</div>
"""

print("提升前,每次调用都要处理静态部分")
print("提升后,静态部分在函数外部,只创建一次")

🔥 实战 35 分钟:3 个递进的小项目

项目 1(5 分钟):自己写一个迷你模板解析器

目标:把 <div id="app">{{ message }}</div> 解析成 AST。

class MiniParser:
"""迷你模板解析器"""

def parse(self, template):
    self.template = template
    self.pos = 0
    return self.parse_node()

def parse_node(self):
    nodes = []
    while self.pos < len(self.template):
        # 处理标签开始
        if self.template[self.pos] == "<":
            tag_end = self.template.find(">", self.pos)
            if tag_end == -1:
                break

            tag_content = self.template[self.pos + 1:tag_end]

            # 跳过结束标签
            if tag_content.startswith("/"):
                self.pos = tag_end + 1

                continue

            # 解析标签名和属性
            parts = tag_content.split()
            tag = parts[0]
            props = {}
            for part in parts[1:]:
                if "=" in part:
                    k, v = part.split("=", 1)
                    props[k] = v.strip('"')

            self.pos = tag_end + 1

            # 递归解析子节点
            children = []
            while self.pos < len(self.template):
                if self.template[self.pos:self.pos+2] == "</":
                    break
                child = self.parse_node()
                if child:
                    children.append(child)

            nodes.append(ASTNode(tag=tag, props=props, children=children))

            # 跳过结束标签
            if self.template[self.pos:self.pos+2] == "</":
                end_tag = self.template.find(">", self.pos)
                self.pos = end_tag + 1
        else:
            # 处理文本节点
            text = ""
            while self.pos < len(self.template) and self.template[self.pos] != "<":
                text += self.template[self.pos]
                self.pos += 1
            if text.strip():
                nodes.append(text.strip())

    return nodes

# 测试
parser = MiniParser()
template = '<div id="app"><h1>{{ title }}</h1><p>你好</p></div>'
result = parser.parse(template)

for node in result:
print_ast(node)

预期输出

标签: <div>
属性: {'id': 'app'}
标签: <h1>
文本: {{ title }}
标签: <p>
文本: 你好

项目 2(15 分钟):实现静态节点标记

目标:给 AST 节点打上「静态」标记,让动态节点和静态节点一目了然。

class StaticMarker:
"""静态节点标记器"""

def __init__(self, ast):
    self.ast = ast

def mark(self):
    """遍历 AST,标记静态节点"""
    for node in self.ast:
        self._mark_node(node, False)
    return self.ast

def _mark_node(self, node, parent_dynamic):
    if isinstance(node, str):
        node._static = True
        node._needs_proxy = False
        return

    # 有插值 {{ }} 的是动态的
    has_interpolation = any("{{" in str(c) for c in node.children if isinstance(c, str))

    # 有 v-if、v-for、@click 的是动态的
    special_attrs = any(attr.startswith("v-") or attr.startswith("@") or attr.startswith(":")
                      for attr in node.props.keys())

    is_dynamic = has_interpolation or special_attrs or parent_dynamic

    node._static = not is_dynamic
    node._needs_proxy = is_dynamic

    # 递归处理子节点
    for child in node.children:
        self._mark_node(child, is_dynamic)

    # 标记子树是否全静态
    node._static_descendants = node._static and all(
        getattr(c, '_static', False) for c in node.children if not isinstance(c, str)
    )

# 测试
from project_1 import MiniParser

parser = MiniParser()
template = '''
<div class="container">
<h1>{{ title }}</h1>
<div class="static-box">不变的内容</div>
<button @click="handleClick">点击</button>
</div>
'''
ast = parser.parse(template)

marker = StaticMarker(ast)
marker.mark()

print("=== 节点静态分析 ===")
for node in ast:
def show(node, indent=0):
    if isinstance(node, str):
        return
    flag = "✅静态" if node._static else "🔶动态"
    subtree = "✅全静态" if node._static_descendants else "🔶含动态"
    print("  " * indent + f"<{node.tag}> {flag} | 子树{subtree}")
    for child in node.children:
        show(child, indent + 1)
show(node)

预期输出

=== 节点静态分析 ===
<div> 🔶动态 | 子树🔶含动态

<h1> 🔶动态 | 子树🔶含动态
<div> ✅静态 | 子树✅全静态
<button> 🔶动态 | 子树🔶含动态

项目 3(15 分钟):生成带 patchFlag 的渲染代码

目标:根据节点的动静特性,生成带有 patchFlag 优化标记的渲染代码。

def generate_optimized(node, static_vars=None):
"""生成优化后的渲染代码,支持 patchFlag"""
if static_vars is None:
    static_vars = {}

if isinstance(node, str):
    return f'"{node}"'

# 确定 patchFlag
flag = 0
if node._static:
    flag = 0  # 完全静态,不需要标记
else:
    # 根据节点特性计算 flag
    has_text_interpolation = any("{{" in str(c) for c in node.children if isinstance(c, str))
    if has_text_interpolation:
        flag |= 1  # TEXT
    if node.props.get("class"):
        flag |= 2  # CLASS
    if node.props.get("style"):
        flag |= 4  # STYLE
    if node.props.get("v-bind"):
        flag |= 8  # PROPS

# 生成属性
props_code = "null"
if node.props and not node._static:
    props_code = "{ " + ", ".join(f'{k}:"{v}"' for k, v in node.props.items() if not k.startswith("v-")) + " }"

# 生成子节点
children = []
for child in node.children:
    child_code = generate_optimized(child, static_vars)
    children.append(child_code)

children_code = "[" + ", ".join(children) + "]" if children else "null"

# 静态节点提升到外部
if node._static and node._static_descendants:
    var_name = f"_static_{len(static_vars)}"
    static_vars[var_name] = f'h("{node.tag}", {props_code}, {children_code})'
    return var_name

# 动态节点生成 h() 调用
flag_str = f", {flag}" if flag else ""
return f'h("{node.tag}", {props_code}, {children_code}{flag_str})'


# 完整流程测试
from project_1 import MiniParser
from project_2 import StaticMarker

# 1. 解析模板
template = '''
<div class="app">
<h1>{{ title }}</h1>
<p class="static-text">这是静态段落</p>
<span>{{ content }}</span>
</div>
'''

parser = MiniParser()
ast = parser.parse(template)

# 2. 标记静态
marker = StaticMarker(ast)
marker.mark()

# 3. 生成代码
static_vars = {}
render_code = generate_optimized(ast[0], static_vars)

print("=== 静态提升的变量 ===")
for name, value in static_vars.items():
print(f"const {name} = {value};")

print("\n=== render 函数 ===")
print(f"function render(ctx) {{")
print(f"  return {render_code}")
print(f"}}")

预期输出

=== 静态提升的变量 ===
const _static_0 = h("p", { class:"static-text" }, ["这是静态段落"]);

=== render 函数 ===
function render(ctx) {
return h("div", { class:"app" }, [
h("h1", null, [ctx.title], 1),
_static_0,
h("span", null, [ctx.content], 1)
])
}

💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:静态节点里藏了动态插值

# ❌ 错误:以为整个节点是静态的
template = '<div class="static"><span>{{ dynamic }}</span></div>'
# 实际:div 标签本身静态,但里面的 {{ dynamic }} 是动态的

# ✅ 正确:检查所有子节点,包括嵌套
# Vue 会把 div 标记为动态(因为子节点有插值)

坑 2:v-if 和 v-for 混用顺序搞反

# ❌ 错误:先 for 后 if(每个元素都判断 if)
template = '<li v-for="item in list" v-if="item.show">{{ item.name }}</li>'

# ✅ 正确:先用 computed 过滤,再用 v-for
# computed: filteredList = list.filter(item => item.show)
# template: '<li v-for="item in filteredList">{{ item.name }}</li>'

坑 3:把响应式数据放模板外

# ❌ 错误:静态内容没提升,每次渲染都重新创建
def render_bad(data):
return f'''
<div>
    <h1>{{ data.title }}</h1>
    <footer>© 2024 公司名</footer>
</div>
'''

# ✅ 正确:静态内容提出来
STATIC_FOOTER = '<footer>© 2024 公司名</footer>'
def render_good(data):
return f'''
<div>
    <h1>{{ data.title }}</h1>
    {STATIC_FOOTER}
</div>
'''

坑 4:滥用 v-show 而非 v-if

# ❌ 错误:频繁切换的用 v-show(会频繁改 display)
template = '<div v-show="isLoading">加载中...</div>'

# ✅ 正确:v-show 适合不频繁切换;v-if 适合条件很少变化
# 加载状态切换频繁 → v-show
# 权限控制这种很少变 → v-if

坑 5:组件根节点只有一个但模板里写了多个

# ❌ 错误:多个根节点(片段),无法使用 patchFlag 优化
template = '''
<header>...</header>
<main>...</main>
<footer>...</footer>
'''

# ✅ 正确:用 div 包裹,或者用 Vue 3 的 Fragment
template = '''
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
'''

性能小贴士:善用 v-memo

# 如果一个子组件依赖多个 props,但不需要全部更新
template = '''
<子组件
:key="id"
:title="title"
:content="content"
:date="date"
/>
'''

# ❌ 低效:任何一个 prop 变都重新渲染

# ✅ 高效:用 v-memo 缓存,只有指定字段变才更新
template = '''
<子组件
v-memo="[title, content]"
:key="id"
:title="title"
:content="content"
:date="date"
/>
'''

调试技巧:用 Vue DevTools 看编译结果

# 在浏览器控制台查看编译后的 render 函数
# 1. 安装 Vue DevTools 扩展
# 2. 打开 Vue 开发者面板
# 3. 选择组件,查看 "Components" 面板
# 4. 点击 "Render" 可以看到编译后的代码

# 或者用 Vue 官方提供的编译器在线体验:
# https://template-explorer.vuejs.org/
# 粘贴你的模板,右边直接看到编译结果

✏️ 练习题 + 作业题

练习 1(2 分钟):看懂静态标记

  • 输入:'<div><p static>静态文字</p></div>' 的 AST
  • 预期输出:标记出哪些节点是静态的
  • 提示:直接调用 StaticMarker 类的 mark() 方法

练习 2(2 分钟):给节点加属性

  • 输入:在项目 1 的解析结果上,给 <p> 标签加一个 class="highlight"
  • 预期输出:打印出的 AST 里 <p>class: highlight 属性
  • 提示:修改 MiniParser.parse_node() 中解析属性的逻辑

练习 3(3 分钟):处理新的模板字符串

  • 输入:'<article><h2>{{ name }}</h2><section>正文内容</section></article>'
  • 预期输出:区分出动态节点(h2 有插值)和静态节点(section 是纯文本)
  • 提示:复用项目 2 的 StaticMarker

练习 4(3 分钟):串接解析和标记

  • 输入:任意模板字符串
  • 预期输出:先用 MiniParser 解析,再用 StaticMarker 标记,最后打印节点树
  • 提示:这是项目 1 + 项目 2 的串联

练习 5(5 分钟):分析报错

  • 输入:以下代码的报错信息
# 运行 generate_optimized 时
template = '<div>{{ message }}</div>'
  • 预期输出:报 AttributeError: 'str' object has no attribute '_static'
  • 提示:文本节点是字符串,不是 ASTNode,检查 generate_optimized 的类型判断

作业:做一个「模板编译可视化工具」

需求描述
做一个命令行工具,输入 Vue 模板字符串,输出编译过程的详细信息。

功能点
1. 解析模板成 AST,并打印树状结构
2. 标记静态节点,统计动态/静态比例
3. 生成优化后的 render 函数代码

加分项
1. 支持从文件读取模板
2. 生成 HTML 报告(AST 可视化)

验收标准
- 能跑起来
- 输出符合预期
- 代码有注释

提交方式:评论区贴代码或 GitHub 链接


📚 总结 + 资源

本文学了 3 个核心点

  1. 模板编译器把 <template> 翻译成 render 函数,经历「解析→优化→生成」三步
  2. AST 是树状结构,让计算机能理解模板的层级关系
  3. 静态提升和 patchFlag 是 Vue 3 性能起飞的关键

延伸学习资源

  1. Vue 3 官方模板编译器文档 - 看官方怎么说的
  2. Vue 3 模板编译源码解读 - 想深入就看这里
  3. 《深入浅出 Vue 3》 - 进阶必读

互动钩子:你在项目里遇到过因为模板写得不好导致页面卡顿的情况吗?用的什么方法解决的?评论区聊聊,老粉优先回复!

下一章我们要聊聊 Vue 3 的新特性——Composition API、Suspense、Teleport 这些东西怎么让代码更优雅。剧透一点:学完下一章,你能写出让同事惊呼「还能这么写」的 Vue 代码。敬请期待!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。