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 生成 ASTconst ast = isString(template) ? baseParse(template, options) : templateconst [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)// 解析子节点,并创建 ASTreturn 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.hoistsroot.temps = context.tempsroot.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 树的表达式
 
