上一篇文章中,我们已经知道了 transform 的核心流程主要有四步:创建 transform 上下文、遍历 AST 节点、静态提升以及创建根代码生成节点。这节课我们接着分析遍历 AST 节点中的 Text 节点的转换函数。
Text 节点转换函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| const transformText = (node, context) => { if (node.type === 0 || node.type === 1 || node.type === 11 || node.type === 10 ) { return () => { const children = node.children let currentContainer = undefined let hasText = false for (let i = 0; i < children.length; i++) { const child = children[i] if (isText(child)) { hasText = true for (let j = i + 1; j < children.length; j++) { const next = children[j] if (isText(next)) { if (!currentContainer) { currentContainer = children[i] = { type: 8 , loc: child.loc, children: [child] } } currentContainer.children.push(` + `, next) children.splice(j, 1) j-- } else { currentContainer = undefined break } } } } if (!hasText || (children.length === 1 && (node.type === 0 || (node.type === 1 && node.tagType === 0 )))) { return } for (let i = 0; i < children.length; i++) { const child = children[i] if (isText(child) || child.type === 8 ) { const callArgs = [] if (child.type !== 2 || child.content !== ' ') { callArgs.push(child) } if (!context.ssr && child.type !== 2 ) { callArgs.push(`${1 /* TEXT */} /* ${PatchFlagNames[1 /* TEXT */]} */`) } children[i] = { type: 12 , content: child, loc: child.loc, codegenNode: createCallExpression(context.helper(CREATE_TEXT), callArgs) } } } } } }
|
transformText 函数只处理根节点、元素节点、 v-for 以及 v-if 分支相关的节点,它也会返回一个退出函数,因为 transformText 要保证所有表达式节点都已经被处理才执行转换逻辑。
transformText 主要的目的就是合并一些相邻的文本节点,然后为内部每一个文本节点创建一个代码生成节点。
在内部,静态文本节点和动态插值节点都被看作是一个文本节点,所以函数首先遍历节点的子节点,然后把子节点中的相邻文本节点合并成一个。
比如示例中的文本节点:<p>hello {{ msg + test }}</p>
。
在转换之前,p 节点对应的 children 数组有两个元素,第一个是纯文本节点,第二个是一个插值节点,这个数组也是前面提到的表达式节点转换后的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| [ { "type": 2, "content": "hello ", }, { "type": 5, "content": { "type": 8, "children": [ { "type": 4, "isConstant": false, "content": "_ctx.msg", "isStatic": false }, " + ", { "type": 4, "isConstant": false, "content": "_ctx.test", "isStatic": false } ], "identifiers": [] } } ]
|
转换后,这两个文本节点被合并成一个复合表达式节点,结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| [ { "type": 8, "children": [ { "type": 2, "content": "hello ", }, " + ", { "type": 5, "content": { "type": 8, "children": [ { "type": 4, "isConstant": false, "content": "_ctx.msg", "isStatic": false }, " + ", { "type": 4, "isConstant": false, "content": "_ctx.test", "isStatic": false } ], "identifiers": [] } } ] } ]
|
合并完子文本节点后,接着判断如果是一个只带有单个文本子元素的纯元素节点,则什么都不需要转换,因为这种情况在运行时可以直接设置元素的 textContent 来更新文本。
最后就是去处理节点包含文本子节点且多个子节点的情况,举个例子:
1 2 3 4 5
| <p> hello {{ msg + test }} <a href="foo"/> hi </p>
|
上述 p 标签的子节点经过前面的文本合并流程后,还有 3 个子节点。针对这种情况,我们可以遍历子节点,找到所有的文本节点或者是复合表达式节点,然后为这些子节点通过 createCallExpression 创建一个调用函数表达式的代码生成节点。
我们来看 createCallExpression 的实现:
1 2 3 4 5 6 7 8
| function createCallExpression(callee, args = [], loc = locStub) { return { type: 14 , loc, callee, arguments: args } }
|
createCallExpression 的实现很简单,就是返回一个类型为 JS_CALL_EXPRESSION 的对象,它包含了执行的函数名和参数。
这里,针对我们创建的函数表达式所生成的节点,它对应的函数名是 createTextVNode,参数 callArgs 是子节点本身 child,如果是动态插值节点,那么参数还会多一个 TEXT 的 patchFlag。
v-if 节点转换函数
接下来,我们来看一下 v-if 节点转换函数的实现:
1 2 3 4 5 6 7
| const transformIf = createStructuralDirectiveTransform(/^(if|else|else-if)$/, (node, dir, context) => { return processIf(node, dir, context, (ifNode, branch, isRoot) => { return () => { } }) })
|
在分析函数的实现前,我们先来看一下 v-if 节点转换的目的,为了方便你的理解,我还是通过示例来说明:
1 2 3 4 5 6
| <hello v-if="flag"></hello> <div v-else> <p>hello {{ msg + test }}</p> <p>static</p> <p>static</p> </div>
|
在 parse 阶段,这个模板解析生成的 AST 节点如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| [ { "children": [], "codegenNode": undefined, "isSelfClosing": false, "ns": 0, "props": [{ "type": 7, "name": "if", "exp": { "type": 4, "content": "flag", "isConstant": false, "isStatic": false }, "arg": undefined, "modifiers": [] }], "tag": "hello", "tagType": 1, "type": 1 }, { "children": [ ], "codegenNode": undefined, "isSelfClosing": false, "ns": 0, "props": [{ "type": 7, "name": "else", "exp": undefined, "arg": undefined, "modifiers": [] }], "tag": "div", "tagType": 0, "type": 1 } ]
|
v-if 指令用于条件性地渲染一块内容,显然上述 AST 节点对于最终去生成条件的代码而言,是不够语义化的,于是我们需要对它们做一层转换,使其成为语义化强的代码生成节点。
现在我们回过头看 transformIf 的实现,它是通过 createStructuralDirectiveTransform 函数创建的一个结构化指令的转换函数,在 Vue.js 中,v-if、v-else-if、v-else 和 v-for 这些都属于结构化指令,因为它们能影响代码的组织结构。
我们来看一下 createStructuralDirectiveTransform 的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| function createStructuralDirectiveTransform(name, fn) { const matches = isString(name) ? (n) => n === name : (n) => name.test(n) return (node, context) => { if (node.type === 1 ) { const { props } = node if (node.tagType === 3 && props.some(isVSlot)) { return } const exitFns = [] for (let i = 0; i < props.length; i++) { const prop = props[i] if (prop.type === 7 && matches(prop.name)) { props.splice(i, 1) i-- const onExit = fn(node, prop, context) if (onExit) exitFns.push(onExit) } } return exitFns } } }
|
可以看到,createStructuralDirectiveTransform 接受 2 个参数,第一个 name 是指令的名称,第二个 fn 是构造转换退出函数的方法。
createStructuralDirectiveTransform 最后会返回一个函数,在我们的场景下,这个函数就是 transformIf 转换函数。
我们进一步看这个函数的实现,它只处理元素节点,这个很好理解,因为只有元素节点才会有 v-if 指令,接着会解析这个节点的 props 属性,如果发现 props 包含 if 属性,也就是节点拥有 v-if 指令,那么先从 props 删除这个结构化指令防止无限递归,然后执行 fn 获取对应的退出函数,最后将这个退出函数返回。
接着我们来看 fn 的实现,在我们这个场景下 fn 对应的是前面传入的匿名函数:
1 2 3 4 5 6 7
| (node, dir, context) => { return processIf(node, dir, context, (ifNode, branch, isRoot) => { return () => { } }) }
|
可以看出,这个匿名函数内部执行了 processIf 函数,它会先对 v-if 和它的相邻节点做转换,然后返回一个退出函数,在它们的子节点都转换完毕后执行。
我们来看 processIf 函数的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function processIf(node, dir, context, processCodegen) { if (dir.name === 'if') { const branch = createIfBranch(node, dir) const ifNode = { type: 9 , loc: node.loc, branches: [branch] } context.replaceNode(ifNode) if (processCodegen) { return processCodegen(ifNode, branch, true) } } else { } }
|
processIf 主要就是用来处理 v-if 节点以及 v-if 的相邻节点,比如 v-else-if 和 v-else,并且它们会走不同的处理逻辑。
我们先来看 v-if 的处理逻辑。首先,它会执行 createIfBranch 去创建一个分支节点:
1 2 3 4 5 6 7 8
| function createIfBranch(node, dir) { return { type: 10 , loc: node.loc, condition: dir.name === 'else' ? undefined : dir.exp, children: node.tagType === 3 ? node.children : [node] } }
|
这个分支节点很好理解,因为 v-if 节点内部的子节点可以属于一个分支,v-else-if 和 v-else 节点内部的子节点也都可以属于一个分支,而最终页面渲染执行哪个分支,这取决于哪个分支节点的 condition 为 true。
所以分支节点返回的对象,就包含了 condition 条件,以及它的子节点 children。注意,如果节点 node 不是 template,那么 children 指向的就是这个单个 node 构造的数组。
接下来它会创建 IF 节点替换当前节点,IF 节点拥有 branches 属性,包含我们前面创建的分支节点,显然,相对于原节点,IF 节点的语义化更强,更利于后续生成条件表达式代码。
最后它会执行 processCodegen 创建退出函数。我们先不着急去分析退出函数的创建过程,先把 v-if 相邻节点的处理逻辑分析完:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| function processIf(node, dir, context, processCodegen) { if (dir.name === 'if') { } else { const siblings = context.parent.children let i = siblings.indexOf(node) while (i-- >= -1) { const sibling = siblings[i] if (sibling && sibling.type === 9 ) { context.removeNode() const branch = createIfBranch(node, dir) sibling.branches.push(branch) const onExit = processCodegen && processCodegen(sibling, branch, false) traverseNode(branch, context) if (onExit) onExit() context.currentNode = null } else { context.onError(createCompilerError(28 , node.loc)) } break } } }
|
这段处理逻辑就是从当前节点往前面的兄弟节点遍历,找到 v-if 节点后,把当前节点删除,然后根据当前节点创建一个分支节点,把这个分支节点添加到前面创建的 IF 节点的 branches 中。此外,由于这个节点已经删除,那么需要在这里把这个节点的子节点通过 traverseNode 遍历一遍。
这么处理下来,就相当于完善了 IF 节点的信息了,IF 节点的 branches 就包含了所有分支节点了。
那么至此,进入 v-if、v-else-if、v-else 这些节点的转换逻辑我们就分析完毕了,即最终创建了一个 IF 节点,它包含了所有的分支节点。
接下来,我们再来分析这个退出函数的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| (node, dir, context) => { return processIf(node, dir, context, (ifNode, branch, isRoot) => { return () => { if (isRoot) { ifNode.codegenNode = createCodegenNodeForBranch(branch, 0, context) } else { let parentCondition = ifNode.codegenNode while (parentCondition.alternate.type === 19 ) { parentCondition = parentCondition.alternate } parentCondition.alternate = createCodegenNodeForBranch(branch, ifNode.branches.length - 1, context) } } }) }
|
可以看到,当 v-if 节点执行退出函数时,会通过 createCodegenNodeForBranch 创建 IF 分支节点的 codegenNode,我们来看一下它的实现:
1 2 3 4 5 6 7 8 9 10 11 12
| function createCodegenNodeForBranch(branch, index, context) { if (branch.condition) { return createConditionalExpression(branch.condition, createChildrenCodegenNode(branch, index, context), createCallExpression(context.helper(CREATE_COMMENT), [ (process.env.NODE_ENV !== 'production') ? '"v-if"' : '""', 'true' ])) } else { return createChildrenCodegenNode(branch, index, context) } }
|
当分支节点存在 condition 的时候,比如 v-if、和 v-else-if,它通过 createConditionalExpression 返回一个条件表达式节点:
1 2 3 4 5 6 7 8 9 10
| function createConditionalExpression(test, consequent, alternate, newline = true) { return { type: 19 , test, consequent, alternate, newline, loc: locStub } }
|
其中 consequent 在这里是 IF 主 branch 的子节点对应的代码生成节点,alternate 是后补 branch 子节点对应的代码生成节点。
接着,我们来看一下 createChildrenCodegenNode 的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| function createChildrenCodegenNode(branch, index, context) { const { helper } = context const keyProperty = createObjectProperty(`key`, createSimpleExpression(index + '', false)) const { children } = branch const firstChild = children[0] const needFragmentWrapper = children.length !== 1 || firstChild.type !== 1 if (needFragmentWrapper) { if (children.length === 1 && firstChild.type === 11 ) { const vnodeCall = firstChild.codegenNode injectProp(vnodeCall, keyProperty, context) return vnodeCall } else { return createVNodeCall(context, helper(FRAGMENT), createObjectExpression([keyProperty]), children, `${64 /* STABLE_FRAGMENT */} /* ${PatchFlagNames[64 /* STABLE_FRAGMENT */]} */`, undefined, undefined, true, false, branch.loc) } } else { const vnodeCall = firstChild .codegenNode; if (vnodeCall.type === 13 && (firstChild.tagType !== 1 || vnodeCall.tag === TELEPORT)) { vnodeCall.isBlock = true helper(OPEN_BLOCK) helper(CREATE_BLOCK) } injectProp(vnodeCall, keyProperty, context) return vnodeCall } }
|
createChildrenCodegenNode 主要就是判断每个分支子节点是不是一个 vnodeCall,如果这个子节点不是组件节点的话,则把它转变成一个 BlockCall,也就是让 v-if 的每一个分支都可以创建一个 Block。
这个行为是很好理解的,因为 v-if 是条件渲染的,我们知道在某些条件下某些分支是不会渲染的,那么它内部的动态节点就不能添加到外部的 Block 中的,所以它就需要单独创建一个 Block 来维护分支内部的动态节点,这样也就构成了 Block tree。
为了直观让你感受 v-if 节点最终转换的结果,我们来看前面示例转换后的结果,最终转换生成的 IF 节点对象大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| { "type": 9, "branches": [{ "type": 10, "children": [{ "type": 1, "tagType": 1, "tag": "hello" }], "condition": { "type": 4, "content": "_ctx.flag" } },{ "type": 10, "children": [{ "type": 1, "tagType": 0, "tag": "hello" }], "condition": { "type": 4, "content": "_ctx.flag" } }], "codegenNode": { "type": 19, "consequent": { "type": 13, "tag": "_component_hello", "children": undefined, "directives": undefined, "dynamicProps": undefined, "isBlock": false, "patchFlag": undefined }, "alternate": { "type": 13, "tag": "_component_hello", "children": [ ], "directives": undefined, "dynamicProps": undefined, "isBlock": false, "patchFlag": undefined } } }
|
可以看到,相比原节点,转换后的 IF 节点无论是在语义化还是在信息上,都更加丰富,我们可以依据它在代码生成阶段生成所需的代码。
静态提升
节点转换完毕后,接下来会判断编译配置中是否配置了 hoistStatic,如果是就会执行 hoistStatic 做静态提升:
1 2 3
| if (options.hoistStatic) { hoistStatic(root, context) }
|
静态提升也是 Vue.js 3.0 在编译阶段设计了一个优化策略,为了便于你理解,我先举个简单的例子:
1 2 3
| <p>>hello {{ msg + test }}</p> <p>static</p> <p>static</p>
|
我们为它配置了 hoistStatic,经过编译后,它的代码就变成了这样:
1 2 3 4 5 6 7 8 9 10
| import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue" const _hoisted_1 = _createVNode("p", null, "static", -1 ) const _hoisted_2 = _createVNode("p", null, "static", -1 ) export function render(_ctx, _cache) { return (_openBlock(), _createBlock(_Fragment, null, [ _createVNode("p", null, "hello " + _toDisplayString(_ctx.msg + _ctx.test), 1 ), _hoisted_1, _hoisted_2 ], 64 )) }
|
这里,我们先忽略 openBlock、Fragment ,我会在代码生成章节详细说明,重点看一下 _hoisted_1 和 _hoisted_2 这两个变量,它们分别对应模板中两个静态 p 标签生成的 vnode,可以发现它的创建是在 render 函数外部执行的。
这样做的好处是,不用每次在 render 阶段都执行一次 createVNode 创建 vnode 对象,直接用之前在内存中创建好的 vnode 即可。
那么为什么叫静态提升呢?
因为这些静态节点不依赖动态数据,一旦创建了就不会改变,所以只有静态节点才能被提升到外部创建。
了解以上背景知识后,我们接下来看一下静态提升的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| function hoistStatic(root, context) { walk( root, context, new Map(), isSingleElementRoot(root, root.children[0]) ); } function walk(node, context, resultCache, doNotHoistNode = false) { let hasHoistedNode = false let hasRuntimeConstant = false const { children } = node for (let i = 0; i < children.length; i++) { const child = children[i] if (child.type === 1 && child.tagType === 0 ) { let staticType if (!doNotHoistNode && (staticType = getStaticType(child, resultCache)) > 0) { if (staticType === 2 ) { hasRuntimeConstant = true } child.codegenNode.patchFlag = -1 + ((process.env.NODE_ENV !== 'production') ? ` /* HOISTED */` : ``) child.codegenNode = context.hoist(child.codegenNode) hasHoistedNode = true continue } else { const codegenNode = child.codegenNode if (codegenNode.type === 13 ) { const flag = getPatchFlag(codegenNode) if ((!flag || flag === 512 || flag === 1 ) && !hasDynamicKeyOrRef(child) && !hasCachedProps()) { const props = getNodeProps(child) if (props) { codegenNode.props = context.hoist(props) } } } } } else if (child.type === 12 ) { const staticType = getStaticType(child.content, resultCache) if (staticType > 0) { if (staticType === 2 ) { hasRuntimeConstant = true } child.codegenNode = context.hoist(child.codegenNode) hasHoistedNode = true } } if (child.type === 1 ) { walk(child, context, resultCache) } else if (child.type === 11 ) { walk(child, context, resultCache, child.children.length === 1) } else if (child.type === 9 ) { for (let i = 0; i < child.branches.length; i++) { walk(child.branches[i], context, resultCache, child.branches[i].children.length === 1) } } } if (!hasRuntimeConstant && hasHoistedNode && context.transformHoist) { context.transformHoist(children, context, node) } }
|
可以看到,hoistStatic 主要就是从根节点开始,通过递归的方式去遍历节点,只有普通元素和文本节点才能被静态提升,所以针对这些节点,这里通过 getStaticType 去获取静态类型,如果节点是一个元素类型,getStaticType 内部还会递归判断它的子节点的静态类型。
虽然有的节点包含一些动态子节点,但它本身的静态属性还是可以被静态提升的。
注意,如果 getStaticType 返回的 staticType 的值是 2,则表明它是一个运行时常量,由于它的值在运行时才能被确定,所以是不能静态提升的。
如果节点满足可以被静态提升的条件,节点对应的 codegenNode 会通过执行 context.hoist 修改为一个简单表达式节点:
1 2 3 4 5 6 7
| function hoist(exp) { context.hoists.push(exp); const identifier = createSimpleExpression(`_hoisted_${context.hoists.length}`, false, exp.loc, true) identifier.hoisted = exp return identifier } child.codegenNode = context.hoist(child.codegenNode)
|
改动后的 codegenNode 会在生成代码阶段帮助我们生成静态提升的相关代码。
createRootCodegen
完成静态提升后,我们来到了 AST 转换的最后一步,即创建根节点的代码生成节点。我们先来看一下 createRootCodegen 的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function createRootCodegen(root, context) { const { helper } = context; const { children } = root; const child = children[0]; if (children.length === 1) { if (isSingleElementRoot(root, child) && child.codegenNode) { const codegenNode = child.codegenNode; if (codegenNode.type === 13 ) { codegenNode.isBlock = true; helper(OPEN_BLOCK); helper(CREATE_BLOCK); } root.codegenNode = codegenNode; } else { root.codegenNode = child; } } else if (children.length > 1) { root.codegenNode = createVNodeCall(context, helper(FRAGMENT), undefined, root.children, `${64 /* STABLE_FRAGMENT */} /* ${PatchFlagNames[64 /* STABLE_FRAGMENT */]} */`, undefined, undefined, true); } }
|
createRootCodegen 做的事情很简单,就是为 root 这个虚拟的 AST 根节点创建一个代码生成节点,如果 root 的子节点 children 是单个元素节点,则将其转换成一个 Block,把这个 child 的 codegenNode 赋值给 root 的 codegenNode。
如果 root 的子节点 children 是多个节点,则返回一个 fragement 的代码生成节点,并赋值给 root 的 codegenNode。
这里,创建 codegenNode 就是为了后续生成代码时使用。
createRootCodegen 完成之后,接着把 transform 上下文在转换 AST 节点过程中创建的一些变量赋值给 root 节点对应的属性,在这里可以看一下这些属性:
1 2 3 4 5 6 7
| 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
|
这样后续在代码生成节点时,就可以通过 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
原文链接: https://xiaozhouguo.github.io/2022/10/24/vue3/ast-transform-two/
版权声明: 转载请注明出处.