packages/compiler-core/src/compile.ts

  1. function baseCompile(template, options = {}) {
  2. const prefixIdentifiers = false
  3. // 解析 template 生成 AST
  4. const ast = isString(template) ? baseParse(template, options) : template
  5. const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()
  6. // AST 转换
  7. transform(ast, extend({}, options, {
  8. prefixIdentifiers,
  9. nodeTransforms: [
  10. ...nodeTransforms,
  11. ...(options.nodeTransforms || [])
  12. ],
  13. directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {}
  14. )
  15. }))
  16. // 生成代码
  17. return generate(ast, extend({}, options, {
  18. prefixIdentifiers
  19. }))
  20. }

baseCompile函数主要做三件事情:

  • parse:解析 template 生成 AST
  • transform:AST 转换 (比如标记和转化vue的特定语法)
  • generate:生成render代码

AST

什么是AST?

抽象语法树 (Abstract Syntax Tree),简称 AST,它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

  1. <div class="app">
  2. <!-- 这是一段注释 -->
  3. <hello>
  4. <p>{{ msg }}</p>
  5. </hello>
  6. <p>This is an app</p>
  7. </div>

的AST:使用JSON格式化工具查看

  1. {
  2. "type": 0,
  3. "children": [{
  4. "type": 1,
  5. "ns": 0,
  6. "tag": "div",
  7. "tagType": 0,
  8. "props": [{
  9. "type": 6,
  10. "name": "class",
  11. "value": {
  12. "type": 2,
  13. "content": "app",
  14. "loc": {
  15. "start": {
  16. "column": 12,
  17. "line": 1,
  18. "offset": 11
  19. },
  20. "end": {
  21. "column": 17,
  22. "line": 1,
  23. "offset": 16
  24. },
  25. "source": "\"app\""
  26. }
  27. },
  28. "loc": {
  29. "start": {
  30. "column": 6,
  31. "line": 1,
  32. "offset": 5
  33. },
  34. "end": {
  35. "column": 17,
  36. "line": 1,
  37. "offset": 16
  38. },
  39. "source": "class=\"app\""
  40. }
  41. }],
  42. "isSelfClosing": false,
  43. "children": [{
  44. "type": 3,
  45. "content": " 这是一段注释 ",
  46. "loc": {
  47. "start": {
  48. "column": 3,
  49. "line": 2,
  50. "offset": 20
  51. },
  52. "end": {
  53. "column": 18,
  54. "line": 2,
  55. "offset": 35
  56. },
  57. "source": "<!-- 这是一段注释 -->"
  58. }
  59. }, {
  60. "type": 1,
  61. "ns": 0,
  62. "tag": "hello",
  63. "tagType": 1,
  64. "props": [],
  65. "isSelfClosing": false,
  66. "children": [{
  67. "type": 1,
  68. "ns": 0,
  69. "tag": "p",
  70. "tagType": 0,
  71. "props": [],
  72. "isSelfClosing": false,
  73. "children": [{
  74. "type": 5,
  75. "content": {
  76. "type": 4,
  77. "isStatic": false,
  78. "isConstant": false,
  79. "content": "msg",
  80. "loc": {
  81. "start": {
  82. "column": 11,
  83. "line": 4,
  84. "offset": 56
  85. },
  86. "end": {
  87. "column": 14,
  88. "line": 4,
  89. "offset": 59
  90. },
  91. "source": "msg"
  92. }
  93. },
  94. "loc": {
  95. "start": {
  96. "column": 8,
  97. "line": 4,
  98. "offset": 53
  99. },
  100. "end": {
  101. "column": 17,
  102. "line": 4,
  103. "offset": 62
  104. },
  105. "source": "{{ msg }}"
  106. }
  107. }],
  108. "loc": {
  109. "start": {
  110. "column": 5,
  111. "line": 4,
  112. "offset": 50
  113. },
  114. "end": {
  115. "column": 21,
  116. "line": 4,
  117. "offset": 66
  118. },
  119. "source": "<p>{{ msg }}</p>"
  120. }
  121. }],
  122. "loc": {
  123. "start": {
  124. "column": 3,
  125. "line": 3,
  126. "offset": 38
  127. },
  128. "end": {
  129. "column": 11,
  130. "line": 5,
  131. "offset": 77
  132. },
  133. "source": "<hello>\n <p>{{ msg }}</p>\n </hello>"
  134. }
  135. }, {
  136. "type": 1,
  137. "ns": 0,
  138. "tag": "p",
  139. "tagType": 0,
  140. "props": [],
  141. "isSelfClosing": false,
  142. "children": [{
  143. "type": 2,
  144. "content": "This is an app",
  145. "loc": {
  146. "start": {
  147. "column": 6,
  148. "line": 6,
  149. "offset": 83
  150. },
  151. "end": {
  152. "column": 20,
  153. "line": 6,
  154. "offset": 97
  155. },
  156. "source": "This is an app"
  157. }
  158. }],
  159. "loc": {
  160. "start": {
  161. "column": 3,
  162. "line": 6,
  163. "offset": 80
  164. },
  165. "end": {
  166. "column": 24,
  167. "line": 6,
  168. "offset": 101
  169. },
  170. "source": "<p>This is an app</p>"
  171. }
  172. }],
  173. "loc": {
  174. "start": {
  175. "column": 1,
  176. "line": 1,
  177. "offset": 0
  178. },
  179. "end": {
  180. "column": 7,
  181. "line": 7,
  182. "offset": 108
  183. },
  184. "source": "<div class=\"app\">\n <!-- 这是一段注释 -->\n <hello>\n <p>{{ msg }}</p>\n </hello>\n <p>This is an app</p>\n</div>"
  185. }
  186. }],
  187. "helpers": [],
  188. "components": [],
  189. "directives": [],
  190. "hoists": [],
  191. "imports": [],
  192. "cached": 0,
  193. "temps": 0,
  194. "loc": {
  195. "start": {
  196. "column": 1,
  197. "line": 1,
  198. "offset": 0
  199. },
  200. "end": {
  201. "column": 7,
  202. "line": 7,
  203. "offset": 108
  204. },
  205. "source": "<div class=\"app\">\n <!-- 这是一段注释 -->\n <hello>\n <p>{{ msg }}</p>\n </hello>\n <p>This is an app</p>\n</div>"
  206. }
  207. }
  • type 字段描述节点的类型
  • tag 字段描述节点的标签
  • props 描述节点的属性
  • loc 描述节点对应代码相关信息
  • children 指向它的子节点对象数组

AST 中的节点是可以完整地描述它在模板中映射的节点信息。

Parse

把 template 解析生成 AST 对象,整个解析过程是一个自顶向下的分析过程,也就是从代码开始,通过语法分析,找到对应的解析处理逻辑,创建 AST 节点,处理的过程中也在不断前进代码,更新解析上下文,最终根据生成的 AST 节点数组创建 AST 根节点。

  1. function baseParse(content, options = {}) {
  2. // 创建解析上下文
  3. const context = createParserContext(content, options)
  4. const start = getCursor(context)
  5. // 解析子节点,并创建 AST
  6. return createRoot(
  7. parseChildren(context, 0 /* DATA */, []),
  8. getSelection(context, start)
  9. )
  10. }
  1. function createRoot(children, loc = locStub) {
  2. return {
  3. type: 0 /* ROOT */,
  4. children,
  5. helpers: [],
  6. components: [],
  7. directives: [],
  8. hoists: [],
  9. imports: [],
  10. cached: 0,
  11. temps: 0,
  12. codegenNode: undefined,
  13. loc
  14. }
  15. }

解析子节点,主要有四种情况:

  • 注释节点的解析
  • 插值的解析
  • 普通文本的解析
  • 元素节点的解析
    • 解析开始标签
    • 解析子节点
    • 解析闭合标签

Transform

先通过 getBaseTransformPreset方法获取节点和指令转换的方法,然后调用 transform方法做 AST 转换,并且把这些节点和指令的转换方法作为配置的属性参数传入

  1. // 获取节点和指令转换的方法
  2. const [nodeTransforms, directiveTransforms] = getBaseTransformPreset()
  3. // AST 转换
  4. transform(ast, extend({}, options, {
  5. prefixIdentifiers,
  6. nodeTransforms: [
  7. ...nodeTransforms,
  8. ...(options.nodeTransforms || []) // 用户自定义 transforms
  9. ],
  10. directiveTransforms: extend({}, directiveTransforms, options.directiveTransforms || {} // 用户自定义 transforms
  11. )
  12. }))
  1. function getBaseTransformPreset(prefixIdentifiers) {
  2. //返回的节点和指令的转换方法:
  3. return [
  4. [
  5. transformOnce,
  6. transformIf,
  7. transformFor,
  8. transformExpression,
  9. transformSlotOutlet,
  10. transformElement,
  11. trackSlotScopes,
  12. transformText
  13. ],
  14. {
  15. on: transformOn,
  16. bind: transformBind,
  17. model: transformModel
  18. }
  19. ]
  20. }
  1. function transform(root, options) {
  2. const context = createTransformContext(root, options)
  3. traverseNode(root, context)
  4. if (options.hoistStatic) {
  5. hoistStatic(root, context)
  6. }
  7. if (!options.ssr) {
  8. createRootCodegen(root, context)
  9. }
  10. root.helpers = [...context.helpers]
  11. root.components = [...context.components]
  12. root.directives = [...context.directives]
  13. root.imports = [...context.imports]
  14. root.hoists = context.hoists
  15. root.temps = context.temps
  16. root.cached = context.cached
  17. }

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 函数完成

  1. {
  2. "type": 4,
  3. "isStatic": false,
  4. "isConstant": false,
  5. "content": "msg + test"
  6. }

经过 processExpression 处理后,node.content 的值变成了一个复合表达式对象:

  1. {
  2. "type": 8,
  3. "children": [
  4. {
  5. "type": 4,
  6. "isConstant": false,
  7. "content": "_ctx.msg",
  8. "isStatic": false
  9. },
  10. " + ",
  11. {
  12. "type": 4,
  13. "isConstant": false,
  14. "content": "_ctx.test",
  15. "isStatic": false
  16. }
  17. ],
  18. "identifiers": []
  19. }

发现变量 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 做静态提升

  1. <p>>hello {{ msg + test }}</p>
  2. <p>static</p>
  3. <p>static</p>

配置了 hoistStatic,经过编译后:

  1. import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue"
  2. const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "static", -1 /* HOISTED */)
  3. const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "static", -1 /* HOISTED */)
  4. export function render(_ctx, _cache) {
  5. return (_openBlock(), _createBlock(_Fragment, null, [
  6. _createVNode("p", null, "hello " + _toDisplayString(_ctx.msg + _ctx.test), 1 /* TEXT */),
  7. _hoisted_1,
  8. _hoisted_2
  9. ], 64 /* STABLE_FRAGMENT */))
  10. }

重点看一下 _hoisted_1 和 _hoisted_2 这两个变量,它们分别对应模板中两个静态 p 标签生成的 vnode,可以发现它的创建是在 render 函数外部执行的。

  • 那么为什么叫静态提升呢?

因为这些静态节点不依赖动态数据,一旦创建了就不会改变,所以只有静态节点才能被提升到外部创建。

如果说 parse 阶段是一个词法分析过程,构造基础的 AST 节点对象,那么 transform 节点就是语法分析阶段,把 AST 节点做一层转换,构造出语义化更强,信息更加丰富的 codegenCode,它在后续的代码生成阶段起着非常重要的作用。

生成代码

generate 主要做五件事情:

  • 创建代码生成上下文
  • 生成预设代码
  • 生成渲染函数
  • 生成资源声明代码
  • 生成创建 VNode 树的表达式