transform
熟悉 Vue 2.x 版本源码的同学应该都知道它的 compile 阶段是没有 transform 过程的处理。而 transform 恰恰是整个 Vue 3 提高 VNode 更新性能实现的基础。因为,在这个阶段,会对 baseCompiler 后生成的 AST Element 打上优化标识 patchFlag,以及 isBlock 的判断。
实际上 Vue 3 的
transfrom并不是无米之炊,它本质上是 Vue 2.xcompiler阶段的optimize的升级版。
这里我们将对 AST Elment 的 transform 分为两类:
- 静态节点
transform应用,即节点不含有插值、指令、props、动态样式的绑定等。 - 动态节点
transform应用,即节点含有插值、指令、props、动态样式的绑定等。
静态节点 transform
那么,首先是静态节点 transform 应用。对于上面我们说到的这个栗子,静态节点就是 <div>hi vue3</div>这部分。而它在没有进行 transformText 之前,它对应的 AST 会是这样:
{
children: [{
content: "hi vue3"
loc: {start: {…}, end: {…}, source: "hi vue3"}
type: 2
}]
codegenNode: undefined
isSelfClosing: false
loc: {start: {…}, end: {…}, source: "<div>hi vue3</div>"}
ns: 0
props: []
tag: "div"
tagType: 0
type: 1
}
可以看出,此时它的 codegenNode 是 undefined。而在 transform 阶段则会根据 AST 递归应用对应的 plugin,然后,创建对应 AST Element 的 codegen 对象。所以,此时我们会命中 transformElement 和 transformText 的逻辑。
transformText
transformText 顾名思义,它和文本相关。很显然,我们此时 AST Element 所属的类型就是 Text。那么,我们先来看一下 transformText 函数对应的伪代码:
export const transformText: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ROOT ||
node.type === NodeTypes.ELEMENT ||
node.type === NodeTypes.FOR ||
node.type === NodeTypes.IF_BRANCH
) {
return () => {
const children = node.children
let currentContainer: CompoundExpressionNode | undefined = undefined
let hasText = false
for (let i = 0; i < children.length; i++) { // {1}
const child = children[i]
if (isText(child)) {
hasText = true
...
}
}
if (
!hasText ||
(children.length === 1 &&
(node.type === NodeTypes.ROOT ||
(node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT)))
) { // {2}
return
}
...
}
}
}
可以看到,这里我们会命中 {2} 逻辑,即如果对于节点含有单一文本 transformText 并不需要进行额外的处理。该节点仍然和 Vue 2.x 版本一样,会交给 runtime 时的 render 函数处理。
至于
transfromText真正发挥作用的场景是当存在<div>ab {a} {b}</div>情况时,它需要将两者放在一个单独的 AST Element(Compound Expression) 下。
transformElement
transformElement 是一个所有 AST Element 都会被执行的一个 plugin,它的核心是为 AST Element 生成最基础的 codegen属性。例如标识出对应 patchFlag,从而为生成 VNode 提供依据,即 dynamicChildren。
而对于静态节点,同样只是起到一个初始化它的 codegenNode 属性的作用。并且,从上面介绍的 patchFlag 的类型,我们可以知道它的 patchFlag 为默认值 0。所以,它的 codegenNode 属性值看起来会是这样:
{
children: {
content: "hi vue3"
loc: {start: {…}, end: {…}, source: "hi vue3"}
type: 2
}
directives: undefined
disableTracking: false
dynamicProps: undefined
isBlock: false
loc: {start: {…}, end: {…}, source: "<div>hi vue3</div>"}
patchFlag: undefined
props: undefined
tag: ""div""
type: 13
}
动态节点 transform
接下来是动态节点 transform 应用。这里,我们的动态节点是 <div></div>。它在 baseParse 后对应的 AST 会是这样:
{
children: [
{
content: {type: 4, isStatic: false, isConstant: false, content: "msg", loc: {…}}
loc: {start: {…}, end: {…}, source: "{{msg}}"}
type: 5
}
],
codegenNode: undefined,
isSelfClosing: false,
loc: {start: {…}, end: {…}, source: "<div>{{msg}}</div>"},
ns: 0,
props: [],
tag: "div",
tagType: 0,
type: 1
}
很显然 也是文本,所以也会命中和 hi vue3 一样的 transformText 函数的逻辑。
这里就不对
transformText做展开,因为表现和hi vue3一样。
transformElements
此时,对于插值文本,transfromElements 的价值就会体现出来了。而针对存在单一节点的插值文本,它会两件事:
- 标识
patchFlag为1 /* TEXT */,即动态的文本内容。 - 将插值文本对应的 AST Element 赋值给
VNodeChildren。
具体在源码中的表现会是这样(伪代码):
...
if (node.children.length === 1 && vnodeTag !== TELEPORT) {
const child = node.children[0]
const type = child.type
// check for dynamic text children
const hasDynamicTextChild =
type === NodeTypes.INTERPOLATION ||
type === NodeTypes.COMPOUND_EXPRESSION
if (hasDynamicTextChild && !getStaticType(child)) {
patchFlag |= PatchFlags.TEXT
}
if (hasDynamicTextChild || type === 2 /* TEXT */) {
vnodeChildren = child;
}
}
if (patchFlag !== 0) {
if (__DEV__) {
...
// bitwise flags
const flagNames = Object.keys(PatchFlagNames)
.map(Number)
.filter(n => n > 0 && patchFlag & n)
.map(n => PatchFlagNames[n])
.join(`, `)
vnodePatchFlag = patchFlag + ` /* ${flagNames} */`
...
}
...
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren,
vnodePatchFlag,
vnodeDynamicProps,
vnodeDirectives,
!!shouldUseBlock,
false /* disableTracking */,
node.loc
)
可以看到,处理后的 vnodePatchFlag 和 vnodeChildren 是作为参数传入 createVNodeCall,而 createVNode 最终会将这些参数转化为 AST Element 上属性的值,例如 children、patchFlag。所以,transformElement 处理后,其生成对应的 codegenNode 属性值会是这样:
{
children: {
type: 4,
isStatic: false,
isConstant: false,
content: "msg",
loc: {…}
},
directives: undefined,
dynamicProps: undefined,
isBlock: false,
isForBlock: false,
loc: {
start: {…},
end: {…},
source: "<div>{{msg}}</div>"
},
patchFlag: "1 /* TEXT */",
props: undefined,
tag: ""div"",
type: 13
}