vue3在线 compiler:https://vue-next-template-explorer.netlify.app/
the-super-tiny-compiler:https://github.com/lrzjason/the-super-tiny-compiler/blob/master/the-super-tiny-compiler.js
大圣写的demo:https://juejin.cn/post/6964664022541008932#heading-4
packages/compiler-core/src/compile.ts
function baseCompile(template, options = {}) {
const prefixIdentifiers = false
// 解析 template 生成 AST
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()
// AST 转换
transform(ast, extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || [])
],
directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {}
)
}))
// 生成代码
return generate(ast, extend({}, options, {
prefixIdentifiers
}))
}
baseCompile
函数主要做三件事情:
- parse:解析 template 生成 AST
- transform:AST 转换 (比如标记和转化vue的特定语法)
- generate:生成render代码
AST
什么是AST?
抽象语法树 (Abstract Syntax Tree),简称 AST,它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
<div class="app">
<!-- 这是一段注释 -->
<hello>
<p>{{ msg }}</p>
</hello>
<p>This is an app</p>
</div>
的AST:使用JSON格式化工具查看
{
"type": 0,
"children": [{
"type": 1,
"ns": 0,
"tag": "div",
"tagType": 0,
"props": [{
"type": 6,
"name": "class",
"value": {
"type": 2,
"content": "app",
"loc": {
"start": {
"column": 12,
"line": 1,
"offset": 11
},
"end": {
"column": 17,
"line": 1,
"offset": 16
},
"source": "\"app\""
}
},
"loc": {
"start": {
"column": 6,
"line": 1,
"offset": 5
},
"end": {
"column": 17,
"line": 1,
"offset": 16
},
"source": "class=\"app\""
}
}],
"isSelfClosing": false,
"children": [{
"type": 3,
"content": " 这是一段注释 ",
"loc": {
"start": {
"column": 3,
"line": 2,
"offset": 20
},
"end": {
"column": 18,
"line": 2,
"offset": 35
},
"source": "<!-- 这是一段注释 -->"
}
}, {
"type": 1,
"ns": 0,
"tag": "hello",
"tagType": 1,
"props": [],
"isSelfClosing": false,
"children": [{
"type": 1,
"ns": 0,
"tag": "p",
"tagType": 0,
"props": [],
"isSelfClosing": false,
"children": [{
"type": 5,
"content": {
"type": 4,
"isStatic": false,
"isConstant": false,
"content": "msg",
"loc": {
"start": {
"column": 11,
"line": 4,
"offset": 56
},
"end": {
"column": 14,
"line": 4,
"offset": 59
},
"source": "msg"
}
},
"loc": {
"start": {
"column": 8,
"line": 4,
"offset": 53
},
"end": {
"column": 17,
"line": 4,
"offset": 62
},
"source": "{{ msg }}"
}
}],
"loc": {
"start": {
"column": 5,
"line": 4,
"offset": 50
},
"end": {
"column": 21,
"line": 4,
"offset": 66
},
"source": "<p>{{ msg }}</p>"
}
}],
"loc": {
"start": {
"column": 3,
"line": 3,
"offset": 38
},
"end": {
"column": 11,
"line": 5,
"offset": 77
},
"source": "<hello>\n <p>{{ msg }}</p>\n </hello>"
}
}, {
"type": 1,
"ns": 0,
"tag": "p",
"tagType": 0,
"props": [],
"isSelfClosing": false,
"children": [{
"type": 2,
"content": "This is an app",
"loc": {
"start": {
"column": 6,
"line": 6,
"offset": 83
},
"end": {
"column": 20,
"line": 6,
"offset": 97
},
"source": "This is an app"
}
}],
"loc": {
"start": {
"column": 3,
"line": 6,
"offset": 80
},
"end": {
"column": 24,
"line": 6,
"offset": 101
},
"source": "<p>This is an app</p>"
}
}],
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 7,
"line": 7,
"offset": 108
},
"source": "<div class=\"app\">\n <!-- 这是一段注释 -->\n <hello>\n <p>{{ msg }}</p>\n </hello>\n <p>This is an app</p>\n</div>"
}
}],
"helpers": [],
"components": [],
"directives": [],
"hoists": [],
"imports": [],
"cached": 0,
"temps": 0,
"loc": {
"start": {
"column": 1,
"line": 1,
"offset": 0
},
"end": {
"column": 7,
"line": 7,
"offset": 108
},
"source": "<div class=\"app\">\n <!-- 这是一段注释 -->\n <hello>\n <p>{{ msg }}</p>\n </hello>\n <p>This is an app</p>\n</div>"
}
}
- type 字段描述节点的类型
- tag 字段描述节点的标签
- props 描述节点的属性
- loc 描述节点对应代码相关信息
- children 指向它的子节点对象数组
AST 中的节点是可以完整地描述它在模板中映射的节点信息。
Parse
把 template 解析生成 AST 对象,整个解析过程是一个自顶向下的分析过程,也就是从代码开始,通过语法分析,找到对应的解析处理逻辑,创建 AST 节点,处理的过程中也在不断前进代码,更新解析上下文,最终根据生成的 AST 节点数组创建 AST 根节点。
function baseParse(content, options = {}) {
// 创建解析上下文
const context = createParserContext(content, options)
const start = getCursor(context)
// 解析子节点,并创建 AST
return createRoot(
parseChildren(context, 0 /* DATA */, []),
getSelection(context, start)
)
}
function createRoot(children, loc = locStub) {
return {
type: 0 /* ROOT */,
children,
helpers: [],
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined,
loc
}
}
解析子节点,主要有四种情况:
- 注释节点的解析
- 插值的解析
- 普通文本的解析
- 元素节点的解析
- 解析开始标签
- 解析子节点
- 解析闭合标签
Transform
先通过 getBaseTransformPreset
方法获取节点和指令转换的方法,然后调用 transform
方法做 AST 转换,并且把这些节点和指令的转换方法作为配置的属性参数传入
// 获取节点和指令转换的方法
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()
// AST 转换
transform(ast, extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // 用户自定义 transforms
],
directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {} // 用户自定义 transforms
)
}))
function getBaseTransformPreset(prefixIdentifiers) {
//返回的节点和指令的转换方法:
return [
[
transformOnce,
transformIf,
transformFor,
transformExpression,
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
]
}
function transform(root, options) {
const context = createTransformContext(root, options)
traverseNode(root, context)
if (options.hoistStatic) {
hoistStatic(root, context)
}
if (!options.ssr) {
createRootCodegen(root, context)
}
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
}
transform
的核心流程主要有四步:
- 创建 transform 上下文
- 遍历 AST 节点
traverseNode
- 静态提升
- 创建根代码生成节点
traverseNode
函数的基本思路就是递归遍历 AST 节点,针对每个节点执行一系列的转换函数,有些转换函数还会设计一个退出函数,当你执行转换函数后,它会返回一个新函数,然后在你处理完子节点后再执行这些退出函数,这是因为有些逻辑的处理需要依赖子节点的处理结果才能继续执行。
Element 节点转换函数 transformElement
首先,判断这个节点是不是一个 Block 节点。
为了运行时的更新优化,Vue.js 3.0 设计了一个 Block tree 的概念。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block tree,Vue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,极大优化了 diff 的效率,模板的动静比越大,这个优化就会越明显。
因此在编译阶段,我们需要找出哪些节点可以构成一个 Block,其中动态组件、svg、foreignObject 标签以及动态绑定的 prop 的节点都被视作一个 Block。
其次,是处理节点的 props。
这个过程主要是从 AST 节点的 props 对象中进一步解析出指令 vnodeDirectives、动态属性 dynamicPropNames,以及更新标识 patchFlag。patchFlag 主要用于标识节点更新的类型,在组件更新的优化中会用到,我们在后续章节会详细讲。
接着,是处理节点的 children。
表达式节点转换函数 transformExpression
transformExpression 主要做的事情就是转换插值和元素指令中的动态表达式,把简单的表达式对象转换成复合表达式对象,内部主要是通过 processExpression 函数完成
{
"type": 4,
"isStatic": false,
"isConstant": false,
"content": "msg + test"
}
经过 processExpression 处理后,node.content 的值变成了一个复合表达式对象:
{
"type": 8,
"children": [
{
"type": 4,
"isConstant": false,
"content": "_ctx.msg",
"isStatic": false
},
" + ",
{
"type": 4,
"isConstant": false,
"content": "_ctx.test",
"isStatic": false
}
],
"identifiers": []
}
发现变量 msg 和 test 对应都加上了前缀 _ctx
为了书写模板方便,Vue.js 并没有让我们在模板中手动加组件实例的前缀
为什么 Vue.js 2.x 编译的结果没有 _ctx 前缀呢?这是因为 Vue.js 2.x 的编译结果使用了 with
,比如上述模板,在 Vue.js 2.x 最终编译的结果:with(this){return _s(msg + test)}
。它利用 with 的特性动态去 this 中查找 msg 和 test 属性,所以不需要手动加前缀。
Vue.js 3.0 在 Node.js 端的编译结果舍弃了 with,它会在 processExpression 过程中对表达式动态分析,给该加前缀的地方加上前缀。因为它内部依赖了 @babel/parser 库去解析表达式生成 AST 节点,并依赖了 estree-walker 库去遍历这个 AST 节点,然后对节点分析去判断是否需要加前缀,接着对 AST 节点修改,最终转换生成新的表达式对象。@babel/parser 这个库通常是在 Node.js 端用的,而且这库本身体积非常大,如果打包进 Vue.js 的话会让包体积膨胀 4 倍,所以我们并不会在生产环境的 Web 端引入这个库,Web 端生产环境下的运行时编译最终仍然会用 with 的方式。
所以只有在 Node.js 环境下的编译或者是 Web 端的非生产环境下才会执行 transformExpression。
静态提升
节点转换完毕后,接下来会判断编译配置中是否配置了 hoistStatic,如果是就会执行 hoistStatic 做静态提升
<p>>hello {{ msg + test }}</p>
<p>static</p>
<p>static</p>
配置了 hoistStatic,经过编译后:
import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "static", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "static", -1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode("p", null, "hello " + _toDisplayString(_ctx.msg + _ctx.test), 1 /* TEXT */),
_hoisted_1,
_hoisted_2
], 64 /* STABLE_FRAGMENT */))
}
重点看一下 _hoisted_1 和 _hoisted_2 这两个变量,它们分别对应模板中两个静态 p 标签生成的 vnode,可以发现它的创建是在 render 函数外部执行的。
- 那么为什么叫静态提升呢?
因为这些静态节点不依赖动态数据,一旦创建了就不会改变,所以只有静态节点才能被提升到外部创建。
如果说 parse 阶段是一个词法分析过程,构造基础的 AST 节点对象,那么 transform 节点就是语法分析阶段,把 AST 节点做一层转换,构造出语义化更强,信息更加丰富的 codegenCode,它在后续的代码生成阶段起着非常重要的作用。
生成代码
generate 主要做五件事情:
- 创建代码生成上下文
- 生成预设代码
- 生成渲染函数
- 生成资源声明代码
- 生成创建 VNode 树的表达式