Vue.js 3.0 核心源码解析 - 前百度、滴滴资深技术专家 - 拉勾教育

上一节课,我们已经知道了 transform 的核心流程主要有四步:创建 transform 上下文、遍历 AST 节点、静态提升以及创建根代码生成节点。这节课我们接着分析遍历 AST 节点中的 Text 节点的转换函数。

遍历 AST 节点

Text 节点转换函数

接下来,我们来看一下 Text 节点转换函数的实现:

  1. const transformText = (node, context) => {
  2. if (node.type === 0 ||
  3. node.type === 1 ||
  4. node.type === 11 ||
  5. node.type === 10 ) {
  6. return () => {
  7. const children = node.children
  8. let currentContainer = undefined
  9. let hasText = false
  10. for (let i = 0; i < children.length; i++) {
  11. const child = children[i]
  12. if (isText(child)) {
  13. hasText = true
  14. for (let j = i + 1; j < children.length; j++) {
  15. const next = children[j]
  16. if (isText(next)) {
  17. if (!currentContainer) {
  18. currentContainer = children[i] = {
  19. type: 8 ,
  20. loc: child.loc,
  21. children: [child]
  22. }
  23. }
  24. currentContainer.children.push(` + `, next)
  25. children.splice(j, 1)
  26. j--
  27. }
  28. else {
  29. currentContainer = undefined
  30. break
  31. }
  32. }
  33. }
  34. }
  35. if (!hasText ||
  36. (children.length === 1 &&
  37. (node.type === 0 ||
  38. (node.type === 1 &&
  39. node.tagType === 0 )))) {
  40. return
  41. }
  42. for (let i = 0; i < children.length; i++) {
  43. const child = children[i]
  44. if (isText(child) || child.type === 8 ) {
  45. const callArgs = []
  46. if (child.type !== 2 || child.content !== ' ') {
  47. callArgs.push(child)
  48. }
  49. if (!context.ssr && child.type !== 2 ) {
  50. callArgs.push(`${1 } ]} */`)
  51. }
  52. children[i] = {
  53. type: 12 ,
  54. content: child,
  55. loc: child.loc,
  56. codegenNode: createCallExpression(context.helper(CREATE_TEXT), callArgs)
  57. }
  58. }
  59. }
  60. }
  61. }
  62. }

transformText 函数只处理根节点、元素节点、 v-for 以及 v-if 分支相关的节点,它也会返回一个退出函数,因为 transformText 要保证所有表达式节点都已经被处理才执行转换逻辑。

transformText 主要的目的就是合并一些相邻的文本节点,然后为内部每一个文本节点创建一个代码生成节点。

在内部,静态文本节点和动态插值节点都被看作是一个文本节点,所以函数首先遍历节点的子节点,然后把子节点中的相邻文本节点合并成一个。

比如示例中的文本节点:<p>hello {{ msg + test }}</p>

在转换之前,p 节点对应的 children 数组有两个元素,第一个是纯文本节点,第二个是一个插值节点,这个数组也是前面提到的表达式节点转换后的结果:

  1. [
  2. {
  3. "type": 2,
  4. "content": "hello ",
  5. },
  6. {
  7. "type": 5,
  8. "content": {
  9. "type": 8,
  10. "children": [
  11. {
  12. "type": 4,
  13. "isConstant": false,
  14. "content": "_ctx.msg",
  15. "isStatic": false
  16. },
  17. " + ",
  18. {
  19. "type": 4,
  20. "isConstant": false,
  21. "content": "_ctx.test",
  22. "isStatic": false
  23. }
  24. ],
  25. "identifiers": []
  26. }
  27. }
  28. ]

转换后,这两个文本节点被合并成一个复合表达式节点,结果如下:

  1. [
  2. {
  3. "type": 8,
  4. "children": [
  5. {
  6. "type": 2,
  7. "content": "hello ",
  8. },
  9. " + ",
  10. {
  11. "type": 5,
  12. "content": {
  13. "type": 8,
  14. "children": [
  15. {
  16. "type": 4,
  17. "isConstant": false,
  18. "content": "_ctx.msg",
  19. "isStatic": false
  20. },
  21. " + ",
  22. {
  23. "type": 4,
  24. "isConstant": false,
  25. "content": "_ctx.test",
  26. "isStatic": false
  27. }
  28. ],
  29. "identifiers": []
  30. }
  31. }
  32. ]
  33. }
  34. ]

合并完子文本节点后,接着判断如果是一个只带有单个文本子元素的纯元素节点,则什么都不需要转换,因为这种情况在运行时可以直接设置元素的 textContent 来更新文本。

最后就是去处理节点包含文本子节点且多个子节点的情况,举个例子:

  1. <p>
  2. hello {{ msg + test }}
  3. <a href="foo"/>
  4. hi
  5. </p>

上述 p 标签的子节点经过前面的文本合并流程后,还有 3 个子节点。针对这种情况,我们可以遍历子节点,找到所有的文本节点或者是复合表达式节点,然后为这些子节点通过 createCallExpression 创建一个调用函数表达式的代码生成节点。

我们来看 createCallExpression 的实现:

  1. function createCallExpression(callee, args = [], loc = locStub) {
  2. return {
  3. type: 14 ,
  4. loc,
  5. callee,
  6. arguments: args
  7. }
  8. }

createCallExpression 的实现很简单,就是返回一个类型为 JS_CALL_EXPRESSION 的对象,它包含了执行的函数名和参数。

这里,针对我们创建的函数表达式所生成的节点,它对应的函数名是 createTextVNode,参数 callArgs 是子节点本身 child,如果是动态插值节点,那么参数还会多一个 TEXT 的 patchFlag。

v-if 节点转换函数

接下来,我们来看一下 v-if 节点转换函数的实现:

  1. const transformIf = createStructuralDirectiveTransform(/^(if|else|else-if)$/, (node, dir, context) => {
  2. return processIf(node, dir, context, (ifNode, branch, isRoot) => {
  3. return () => {
  4. }
  5. })
  6. })

在分析函数的实现前,我们先来看一下 v-if 节点转换的目的,为了方便你的理解,我还是通过示例来说明:

  1. <hello v-if="flag"></hello>
  2. <div v-else>
  3. <p>hello {{ msg + test }}</p>
  4. <p>static</p>
  5. <p>static</p>
  6. </div>

在 parse 阶段,这个模板解析生成的 AST 节点如下:

  1. [
  2. {
  3. "children": [],
  4. "codegenNode": undefined,
  5. "isSelfClosing": false,
  6. "ns": 0,
  7. "props": [{
  8. "type": 7,
  9. "name": "if",
  10. "exp": {
  11. "type": 4,
  12. "content": "flag",
  13. "isConstant": false,
  14. "isStatic": false
  15. },
  16. "arg": undefined,
  17. "modifiers": []
  18. }],
  19. "tag": "hello",
  20. "tagType": 1,
  21. "type": 1
  22. },
  23. {
  24. "children": [
  25. ],
  26. "codegenNode": undefined,
  27. "isSelfClosing": false,
  28. "ns": 0,
  29. "props": [{
  30. "type": 7,
  31. "name": "else",
  32. "exp": undefined,
  33. "arg": undefined,
  34. "modifiers": []
  35. }],
  36. "tag": "div",
  37. "tagType": 0,
  38. "type": 1
  39. }
  40. ]

v-if 指令用于条件性地渲染一块内容,显然上述 AST 节点对于最终去生成条件的代码而言,是不够语义化的,于是我们需要对它们做一层转换,使其成为语义化强的代码生成节点。

现在我们回过头看 transformIf 的实现,它是通过 createStructuralDirectiveTransform 函数创建的一个结构化指令的转换函数,在 Vue.js 中,v-if、v-else-if、v-else 和 v-for 这些都属于结构化指令,因为它们能影响代码的组织结构。

我们来看一下 createStructuralDirectiveTransform 的实现:

  1. function createStructuralDirectiveTransform(name, fn) {
  2. const matches = isString(name)
  3. ? (n) => n === name
  4. : (n) => name.test(n)
  5. return (node, context) => {
  6. if (node.type === 1 ) {
  7. const { props } = node
  8. if (node.tagType === 3 && props.some(isVSlot)) {
  9. return
  10. }
  11. const exitFns = []
  12. for (let i = 0; i < props.length; i++) {
  13. const prop = props[i]
  14. if (prop.type === 7 && matches(prop.name)) {
  15. props.splice(i, 1)
  16. i--
  17. const onExit = fn(node, prop, context)
  18. if (onExit)
  19. exitFns.push(onExit)
  20. }
  21. }
  22. return exitFns
  23. }
  24. }
  25. }

可以看到,createStructuralDirectiveTransform 接受 2 个参数,第一个 name 是指令的名称,第二个 fn 是构造转换退出函数的方法。

createStructuralDirectiveTransform 最后会返回一个函数,在我们的场景下,这个函数就是 transformIf 转换函数。

我们进一步看这个函数的实现,它只处理元素节点,这个很好理解,因为只有元素节点才会有 v-if 指令,接着会解析这个节点的 props 属性,如果发现 props 包含 if 属性,也就是节点拥有 v-if 指令,那么先从 props 删除这个结构化指令防止无限递归,然后执行 fn 获取对应的退出函数,最后将这个退出函数返回。

接着我们来看 fn 的实现,在我们这个场景下 fn 对应的是前面传入的匿名函数:

  1. (node, dir, context) => {
  2. return processIf(node, dir, context, (ifNode, branch, isRoot) => {
  3. return () => {
  4. }
  5. })
  6. }

可以看出,这个匿名函数内部执行了 processIf 函数,它会先对 v-if 和它的相邻节点做转换,然后返回一个退出函数,在它们的子节点都转换完毕后执行。

我们来看 processIf 函数的实现:

  1. function processIf(node, dir, context, processCodegen) {
  2. if (dir.name === 'if') {
  3. const branch = createIfBranch(node, dir)
  4. const ifNode = {
  5. type: 9 ,
  6. loc: node.loc,
  7. branches: [branch]
  8. }
  9. context.replaceNode(ifNode)
  10. if (processCodegen) {
  11. return processCodegen(ifNode, branch, true)
  12. }
  13. }
  14. else {
  15. }
  16. }

processIf 主要就是用来处理 v-if 节点以及 v-if 的相邻节点,比如 v-else-if 和 v-else,并且它们会走不同的处理逻辑。

我们先来看 v-if 的处理逻辑。首先,它会执行 createIfBranch 去创建一个分支节点:

  1. function createIfBranch(node, dir) {
  2. return {
  3. type: 10 ,
  4. loc: node.loc,
  5. condition: dir.name === 'else' ? undefined : dir.exp,
  6. children: node.tagType === 3 ? node.children : [node]
  7. }
  8. }

这个分支节点很好理解,因为 v-if 节点内部的子节点可以属于一个分支,v-else-if 和 v-else 节点内部的子节点也都可以属于一个分支,而最终页面渲染执行哪个分支,这取决于哪个分支节点的 condition 为 true。

所以分支节点返回的对象,就包含了 condition 条件,以及它的子节点 children。注意,如果节点 node 不是 template那么 children 指向的就是这个单个 node 构造的数组

接下来它会创建 IF 节点替换当前节点,IF 节点拥有 branches 属性,包含我们前面创建的分支节点,显然,相对于原节点,IF 节点的语义化更强,更利于后续生成条件表达式代码。

最后它会执行 processCodegen 创建退出函数。我们先不着急去分析退出函数的创建过程,先把 v-if 相邻节点的处理逻辑分析完:

  1. function processIf(node, dir, context, processCodegen) {
  2. if (dir.name === 'if') {
  3. }
  4. else {
  5. const siblings = context.parent.children
  6. let i = siblings.indexOf(node)
  7. while (i-- >= -1) {
  8. const sibling = siblings[i]
  9. if (sibling && sibling.type === 9 ) {
  10. context.removeNode()
  11. const branch = createIfBranch(node, dir)
  12. sibling.branches.push(branch)
  13. const onExit = processCodegen && processCodegen(sibling, branch, false)
  14. traverseNode(branch, context)
  15. if (onExit)
  16. onExit()
  17. context.currentNode = null
  18. }
  19. else {
  20. context.onError(createCompilerError(28 , node.loc))
  21. }
  22. break
  23. }
  24. }
  25. }

这段处理逻辑就是从当前节点往前面的兄弟节点遍历,找到 v-if 节点后,把当前节点删除,然后根据当前节点创建一个分支节点,把这个分支节点添加到前面创建的 IF 节点的 branches 中。此外,由于这个节点已经删除,那么需要在这里把这个节点的子节点通过 traverseNode 遍历一遍。

这么处理下来,就相当于完善了 IF 节点的信息了,IF 节点的 branches 就包含了所有分支节点了。

那么至此,进入 v-if、v-else-if、v-else 这些节点的转换逻辑我们就分析完毕了,即最终创建了一个 IF 节点,它包含了所有的分支节点。

接下来,我们再来分析这个退出函数的逻辑:

  1. (node, dir, context) => {
  2. return processIf(node, dir, context, (ifNode, branch, isRoot) => {
  3. return () => {
  4. if (isRoot) {
  5. ifNode.codegenNode = createCodegenNodeForBranch(branch, 0, context)
  6. }
  7. else {
  8. let parentCondition = ifNode.codegenNode
  9. while (parentCondition.alternate.type ===
  10. 19 ) {
  11. parentCondition = parentCondition.alternate
  12. }
  13. parentCondition.alternate = createCodegenNodeForBranch(branch, ifNode.branches.length - 1, context)
  14. }
  15. }
  16. })
  17. }

可以看到,当 v-if 节点执行退出函数时,会通过 createCodegenNodeForBranch 创建 IF 分支节点的 codegenNode,我们来看一下它的实现:

  1. function createCodegenNodeForBranch(branch, index, context) {
  2. if (branch.condition) {
  3. return createConditionalExpression(branch.condition, createChildrenCodegenNode(branch, index, context),
  4. createCallExpression(context.helper(CREATE_COMMENT), [
  5. (process.env.NODE_ENV !== 'production') ? '"v-if"' : '""',
  6. 'true'
  7. ]))
  8. }
  9. else {
  10. return createChildrenCodegenNode(branch, index, context)
  11. }
  12. }

当分支节点存在 condition 的时候,比如 v-if、和 v-else-if,它通过 createConditionalExpression 返回一个条件表达式节点:

  1. function createConditionalExpression(test, consequent, alternate, newline = true) {
  2. return {
  3. type: 19 ,
  4. test,
  5. consequent,
  6. alternate,
  7. newline,
  8. loc: locStub
  9. }
  10. }

其中 consequent 在这里是 IF 主 branch 的子节点对应的代码生成节点,alternate 是后补 branch 子节点对应的代码生成节点。

接着,我们来看一下 createChildrenCodegenNode 的实现:

  1. function createChildrenCodegenNode(branch, index, context) {
  2. const { helper } = context
  3. const keyProperty = createObjectProperty(`key`, createSimpleExpression(index + '', false))
  4. const { children } = branch
  5. const firstChild = children[0]
  6. const needFragmentWrapper = children.length !== 1 || firstChild.type !== 1
  7. if (needFragmentWrapper) {
  8. if (children.length === 1 && firstChild.type === 11 ) {
  9. const vnodeCall = firstChild.codegenNode
  10. injectProp(vnodeCall, keyProperty, context)
  11. return vnodeCall
  12. }
  13. else {
  14. return createVNodeCall(context, helper(FRAGMENT), createObjectExpression([keyProperty]), children, `${64 } ]} */`, undefined, undefined, true, false, branch.loc)
  15. }
  16. }
  17. else {
  18. const vnodeCall = firstChild
  19. .codegenNode;
  20. if (vnodeCall.type === 13 &&
  21. (firstChild.tagType !== 1 ||
  22. vnodeCall.tag === TELEPORT)) {
  23. vnodeCall.isBlock = true
  24. helper(OPEN_BLOCK)
  25. helper(CREATE_BLOCK)
  26. }
  27. injectProp(vnodeCall, keyProperty, context)
  28. return vnodeCall
  29. }
  30. }

createChildrenCodegenNode 主要就是判断每个分支子节点是不是一个 vnodeCall,如果这个子节点不是组件节点的话,则把它转变成一个 BlockCall,也就是让 v-if 的每一个分支都可以创建一个 Block。

这个行为是很好理解的,因为 v-if 是条件渲染的,我们知道在某些条件下某些分支是不会渲染的,那么它内部的动态节点就不能添加到外部的 Block 中的,所以它就需要单独创建一个 Block 来维护分支内部的动态节点,这样也就构成了 Block tree。

为了直观让你感受 v-if 节点最终转换的结果,我们来看前面示例转换后的结果,最终转换生成的 IF 节点对象大致如下:

  1. {
  2. "type": 9,
  3. "branches": [{
  4. "type": 10,
  5. "children": [{
  6. "type": 1,
  7. "tagType": 1,
  8. "tag": "hello"
  9. }],
  10. "condition": {
  11. "type": 4,
  12. "content": "_ctx.flag"
  13. }
  14. },{
  15. "type": 10,
  16. "children": [{
  17. "type": 1,
  18. "tagType": 0,
  19. "tag": "hello"
  20. }],
  21. "condition": {
  22. "type": 4,
  23. "content": "_ctx.flag"
  24. }
  25. }],
  26. "codegenNode": {
  27. "type": 19,
  28. "consequent": {
  29. "type": 13,
  30. "tag": "_component_hello",
  31. "children": undefined,
  32. "directives": undefined,
  33. "dynamicProps": undefined,
  34. "isBlock": false,
  35. "patchFlag": undefined
  36. },
  37. "alternate": {
  38. "type": 13,
  39. "tag": "_component_hello",
  40. "children": [
  41. ],
  42. "directives": undefined,
  43. "dynamicProps": undefined,
  44. "isBlock": false,
  45. "patchFlag": undefined
  46. }
  47. }
  48. }

可以看到,相比原节点,转换后的 IF 节点无论是在语义化还是在信息上,都更加丰富,我们可以依据它在代码生成阶段生成所需的代码。

静态提升

节点转换完毕后,接下来会判断编译配置中是否配置了 hoistStatic,如果是就会执行 hoistStatic 做静态提升:

  1. if (options.hoistStatic) {
  2. hoistStatic(root, context)
  3. }

静态提升也是 Vue.js 3.0 在编译阶段设计了一个优化策略,为了便于你理解,我先举个简单的例子:

  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 = _createVNode("p", null, "static", -1 )
  3. const _hoisted_2 = _createVNode("p", null, "static", -1 )
  4. export function render(_ctx, _cache) {
  5. return (_openBlock(), _createBlock(_Fragment, null, [
  6. _createVNode("p", null, "hello " + _toDisplayString(_ctx.msg + _ctx.test), 1 ),
  7. _hoisted_1,
  8. _hoisted_2
  9. ], 64 ))
  10. }

这里,我们先忽略 openBlock、Fragment ,我会在代码生成章节详细说明,重点看一下 _hoisted_1 和 _hoisted_2 这两个变量,它们分别对应模板中两个静态 p 标签生成的 vnode,可以发现它的创建是在 render 函数外部执行的。
这样做的好处是,不用每次在 render 阶段都执行一次 createVNode 创建 vnode 对象,直接用之前在内存中创建好的 vnode 即可。

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

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

了解以上背景知识后,我们接下来看一下静态提升的实现:

  1. function hoistStatic(root, context) {
  2. walk(root, context, new Map(),
  3. isSingleElementRoot(root, root.children[0]));
  4. }
  5. function walk(node, context, resultCache, doNotHoistNode = false) {
  6. let hasHoistedNode = false
  7. let hasRuntimeConstant = false
  8. const { children } = node
  9. for (let i = 0; i < children.length; i++) {
  10. const child = children[i]
  11. if (child.type === 1 &&
  12. child.tagType === 0 ) {
  13. let staticType
  14. if (!doNotHoistNode &&
  15. // 获取静态节点的类型,如果是元素,则递归检查它的子节点
  16. (staticType = getStaticType(child, resultCache)) > 0) {
  17. if (staticType === 2 ) {
  18. hasRuntimeConstant = true
  19. }
  20. child.codegenNode.patchFlag =
  21. -1 + ((process.env.NODE_ENV !== 'production') ? ` ` : ``)
  22. child.codegenNode = context.hoist(child.codegenNode)
  23. hasHoistedNode = true
  24. continue
  25. }
  26. else {
  27. const codegenNode = child.codegenNode
  28. if (codegenNode.type === 13 ) {
  29. const flag = getPatchFlag(codegenNode)
  30. if ((!flag ||
  31. flag === 512 ||
  32. flag === 1 ) &&
  33. !hasDynamicKeyOrRef(child) &&
  34. !hasCachedProps()) {
  35. const props = getNodeProps(child)
  36. if (props) {
  37. codegenNode.props = context.hoist(props)
  38. }
  39. }
  40. }
  41. }
  42. }
  43. else if (child.type === 12 ) {
  44. const staticType = getStaticType(child.content, resultCache)
  45. if (staticType > 0) {
  46. if (staticType === 2 ) {
  47. hasRuntimeConstant = true
  48. }
  49. child.codegenNode = context.hoist(child.codegenNode)
  50. hasHoistedNode = true
  51. }
  52. }
  53. if (child.type === 1 ) {
  54. walk(child, context, resultCache)
  55. }
  56. else if (child.type === 11 ) {
  57. walk(child, context, resultCache, child.children.length === 1)
  58. }
  59. else if (child.type === 9 ) {
  60. for (let i = 0; i < child.branches.length; i++) {
  61. walk(child.branches[i], context, resultCache, child.branches[i].children.length === 1)
  62. }
  63. }
  64. }
  65. if (!hasRuntimeConstant && hasHoistedNode && context.transformHoist) {
  66. context.transformHoist(children, context, node)
  67. }
  68. }

可以看到,hoistStatic 主要就是从根节点开始,通过递归的方式去遍历节点,只有普通元素和文本节点才能被静态提升,所以针对这些节点,这里通过 getStaticType 去获取静态类型,如果节点是一个元素类型,getStaticType 内部还会递归判断它的子节点的静态类型。

虽然有的节点包含一些动态子节点,但它本身的静态属性还是可以被静态提升的。

注意,如果 getStaticType 返回的 staticType 的值是 2则表明它是一个运行时常量由于它的值在运行时才能被确定所以是不能静态提升的

如果节点满足可以被静态提升的条件,节点对应的 codegenNode 会通过执行 context.hoist 修改为一个简单表达式节点:

  1. function hoist(exp) {
  2. context.hoists.push(exp);
  3. const identifier = createSimpleExpression(`_hoisted_${context.hoists.length}`, false, exp.loc, true)
  4. identifier.hoisted = exp
  5. return identifier
  6. }
  7. child.codegenNode = context.hoist(child.codegenNode)

改动后的 codegenNode 会在生成代码阶段帮助我们生成静态提升的相关代码。

createRootCodegen

完成静态提升后,我们来到了 AST 转换的最后一步,即创建根节点的代码生成节点。我们先来看一下 createRootCodegen 的实现:

  1. function createRootCodegen(root, context) {
  2. const { helper } = context;
  3. const { children } = root;
  4. const child = children[0];
  5. if (children.length === 1) {
  6. if (isSingleElementRoot(root, child) && child.codegenNode) {
  7. const codegenNode = child.codegenNode;
  8. if (codegenNode.type === 13 ) {
  9. codegenNode.isBlock = true;
  10. helper(OPEN_BLOCK);
  11. helper(CREATE_BLOCK);
  12. }
  13. root.codegenNode = codegenNode;
  14. }
  15. else {
  16. root.codegenNode = child;
  17. }
  18. }
  19. else if (children.length > 1) {
  20. root.codegenNode = createVNodeCall(context, helper(FRAGMENT), undefined, root.children, `${64 } ]} */`, undefined, undefined, true);
  21. }
  22. }

createRootCodegen 做的事情很简单,就是为 root 这个虚拟的 AST 根节点创建一个代码生成节点,如果 root 的子节点 children 是单个元素节点,则将其转换成一个 Block,把这个 child 的 codegenNode 赋值给 root 的 codegenNode。

如果 root 的子节点 children 是多个节点,则返回一个 fragement 的代码生成节点,并赋值给 root 的 codegenNode。

这里,创建 codegenNode 就是为了后续生成代码时使用。

createRootCodegen 完成之后,接着把 transform 上下文在转换 AST 节点过程中创建的一些变量赋值给 root 节点对应的属性,在这里可以看一下这些属性:

  1. root.helpers = [...context.helpers]
  2. root.components = [...context.components]
  3. root.directives = [...context.directives]
  4. root.imports = [...context.imports]
  5. root.hoists = context.hoists
  6. root.temps = context.temps
  7. root.cached = context.cached

这样后续在代码生成节点时,就可以通过 root 这个根节点访问到这些变量了。

总结

好的,到这里我们这一节的学习就结束啦,通过这节课的学习,你应该对 AST 节点内部做了哪些转换有所了解。

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

最后,给你留一道思考题目,我们已经知道静态提升的好处是,针对静态节点不用每次在 render 阶段都执行一次 createVNode 创建 vnode 对象,但它有没有成本呢?为什么?欢迎你在留言区与我分享。

本节课的相关代码在源代码中的位置如下:
packages/compiler-core/src/ast.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/transformText.ts
packages/compiler-core/src/transforms/vIf.ts
packages/compiler-core/src/transforms/hoistStatic.ts