v-if
<span v-if="isShow">hello2</span>
上面这段模板会被 Vue 编译器编译为下面的渲染函数
import { openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_ctx.isShow)
? (_openBlock(), _createElementBlock("span", { key: 0 }, "hello2"))
: _createCommentVNode("v-if", true)
}
笔者采用的 Vue3 版本为 3.2.45
在 Vue3 中,要获得组件模板的渲染函数会比 Vue2 简单,只需在 Vue3 源码的根目录下运行 pnpm run dev-compiler
命令,然后再运行 pnpm run open
命令 ,就可以打开 Vue 官方的模板编译工具
可以看到跟 Vue2 一样,Vue3 中的 v-if
指令也会被编译为三元表达式。
openBlock
函数,用于开启 Block 的收集。在 openBlock
函数中,如果 disableTracking
为 true
,会将 currentBlock
设置为 null
;否则创建一个新的数组并赋值给 currentBlock
,并 push
到 blockStack
中。
// packages/runtime-core/src/vnode.ts
// 一个block栈用于存储
export const blockStack: (VNode[] | null)[] = []
// 一个数组,用于存储动态节点,最终会赋给虚拟 dom 上的 dynamicChildren 属性
export let currentBlock: VNode[] | null = null
export function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
}
Block
是一种特殊的虚拟节点(vnode
),它和普通虚拟节点(vnode
)相比,多出一个额外的dynamicChildren
属性,用来存储动态节点。Block
的出现优化了 Diff 的过程。在 Vue2 的 Diff 过程中,即使虚拟节点(vnode
)没有变化,也会进行一次比较,而Block
的出现减少了这种不必要的比较,由于Block
中的动态节点都会被收集到dynamicChildren
中,所以Block
间的patch
可以直接比较dynamicChildren
中的节点,减少了非动态节点之间的比较,减少了 Diff 的工作量。可以说,这是 Vue3 相对于 Vue2 做的一个优化。
createElementBlock
函数用于创建 Block 元素(Block 元素实际上的一个 vnode
),内部调用 setupBlock
函数对创建的 Block 元素做进一步设置和处理。
// packages/runtime-core/src/vnode.ts
export function createElementBlock(
type: string | typeof Fragment,
props?: Record<string, any> | null,
children?: any,
patchFlag?: number,
dynamicProps?: string[],
shapeFlag?: number
) {
return setupBlock(
createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
true /* isBlock */
)
)
}
setupBlock
函数接收虚拟节点(vnode
)作为参数,并判断 isBlockTreeEnabled
是否大于 0,如果大于 0 ,则将 currentBlock
保存在虚拟节点(vnode
)的 dynamicChildren
属性中,然后调用 closeBlock
函数,然后将传入的虚拟节点(vnode
)push 到 currentBlock
数组中。
// packages/runtime-core/src/vnode.ts
function setupBlock(vnode: VNode) {
// save current block children on the block vnode
vnode.dynamicChildren =
isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
// close block
closeBlock()
// a block is always going to be patched, so track it as a child of its
// parent block
if (isBlockTreeEnabled > 0 && currentBlock) {
currentBlock.push(vnode)
}
return vnode
}
closeBlock
函数会弹出 blockStack
栈顶元素,并将 currentBlock
设置为 blockStack
栈顶元素或 null 。主要是为了正确地处理 Block 间的嵌套关系。
// packages/runtime-core/src/vnode.ts
export function closeBlock() {
blockStack.pop()
currentBlock = blockStack[blockStack.length - 1] || null
}
createCommentVNode
函数的作用是创建一个注释类型的虚拟节点(vnode
),通常用于占位。如果 asBlock
参数为 true ,则会将创建的注释类型的虚拟节点作为 Block 收集。
// packages/runtime-core/src/vnode.ts
export function createCommentVNode(
text: string = '',
// when used as the v-else branch, the comment node must be created as a
// block to ensure correct updates.
asBlock: boolean = false
): VNode {
return asBlock
? (openBlock(), createBlock(Comment, null, text))
: createVNode(Comment, null, text)
}
Vue 的编译器用于编译模板生成渲染函数,期间一共经历三个过程,分别是 parse(解析)、transform(转换)和 codegen(代码生成)。
parse 阶段是一个词法分析过程,构造基础的 AST 节点对象。transform 阶段是语法分析过程,把 AST 节点做一层转换,构造出语义化更强,信息更加丰富的 codegenCode,transform 阶段在后续的代码生成阶段起着非常重要的作用。
v-if
指令相关模板经过 parse 阶段生成 AST 对象后,会由 transformIf
函数完成 AST 转换。
// packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// #1587: We need to dynamically increment the key based on the current
// node's sibling nodes, since chained v-if/else branches are
// rendered at the same depth
const siblings = context.parent!.children
let i = siblings.indexOf(ifNode)
let key = 0
while (i-- >= 0) {
const sibling = siblings[i]
if (sibling && sibling.type === NodeTypes.IF) {
key += sibling.branches.length
}
}
// Exit callback. Complete the codegenNode when all children have been
// transformed.
return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
} else {
// attach this branch's codegen node to the v-if root.
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
}
}
})
}
)
transformIf
函数由 createStructuralDirectiveTransform
返回:
// packages/compiler-core/src/transform.ts
export function createStructuralDirectiveTransform(
name: string | RegExp,
fn: StructuralDirectiveTransform
): NodeTransform {
const matches = isString(name)
? (n: string) => n === name
: (n: string) => name.test(n)
return (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
const { props } = node
// structural directive transforms are not concerned with slots
// as they are handled separately in vSlot.ts
if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
return
}
const exitFns = []
for (let i = 0; i < props.length; i++) {
const prop = props[i]
if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
// structural directives are removed to avoid infinite recursion
// also we remove them *before* applying so that it can further
// traverse itself in case it moves the node around
props.splice(i, 1)
i--
const onExit = fn(node, prop, context)
if (onExit) exitFns.push(onExit)
}
}
return exitFns
}
}
}
createStructuralDirectiveTransform
是个工厂函数,用于创建结构指令转换器,在 Vue.js 中,v-if
、v-else-if
、v-else
和 v-for
这些都属于结构化指令,因为它们能影响代码的组织结构。该函数接受两个参数 name
和 fn
。
name
是字符串或正则表达式,fn
是结构指令转换函数,也是构造转换退出函数的函数。
name
会转化成 matches
函数用于匹配指令,如果 name
是字符串则判断全等,否则正则校验。
const matches = isString(name)
? (n: string) => n === name
: (n: string) => name.test(n)
接着就是返回 NodeTransform 函数。在 NodeTransform 函数里面,我们先判断传入的节点类型是不是 ELEMENT
,接着判断 tagType
是否为 TEMPLATE
,即是否为 <template>
标签,且上面有 v-slot
指令,则不进行处理。这里不处理 v-slot
是因为他会被单独处理。
if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
return
}
// packages/compiler-core/src/utils.ts
export function isVSlot(p: ElementNode['props'][0]): p is DirectiveNode {
return p.type === NodeTypes.DIRECTIVE && p.name === 'slot'
}
接着定义 exitFns
数组,然后循环节点上面的 prop 来寻找符合要求的指令,如果符合,则从 props 上面移除,同时在移除之后调用传入的 fn
函数获取对应的退出函数,然后将退出函数 push 进 exitFns
数组,最后返回 exitFns
数组。
createStructuralDirectiveTransform
函数只处理元素节点,这很好理解,因为只有元素节点才会有 v-if 指令。
接下来看 createStructuralDirectiveTransform
传入的 fn,该 fn 是个匿名函数
// packages/compiler-core/src/transforms/vIf.ts
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// #1587: We need to dynamically increment the key based on the current
// node's sibling nodes, since chained v-if/else branches are
// rendered at the same depth
// 对于同一层级的 v-if/v-else-if/v-else,为他们创建一个自增的 key ,
// 使 Vue 可以正确地更新他们,具体看以下两个链接:
// https://github.com/vuejs/core/pull/1589
// https://github.com/vuejs/core/issues/1587
const siblings = context.parent!.children
let i = siblings.indexOf(ifNode)
let key = 0
while (i-- >= 0) {
const sibling = siblings[i]
if (sibling && sibling.type === NodeTypes.IF) {
// 对于兄弟节点也是 v-if 指令的情况,为他们创建自增的 key
key += sibling.branches.length
}
}
// Exit callback. Complete the codegenNode when all children have been
// transformed.
// 返回退出回调函数,当所有子节点转换完成执行
return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
} else {
// attach this branch's codegen node to the v-if root.
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
}
}
})
}
传给 processIf
的回调函数中,首先会对同一层级的 v-if/v-else-if/v-else 节点创建一个自增的 key ,使 Vue 可以正确地更新他们。具体看下面的例子,这个例子是 Vue 使用的是还未修复该 bug 时的版本,在浏览器中运行,会发现带有 v-if
指令的节点没有正确更新。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<div id="app">
<button @click="inc">change value</button>
<p>value%2 === {{value%2}}</p>
<component-a>
<template v-slot:name>
<span v-if="value%2 === 0">world</span>
<span v-if="value%2 !== 0">hello</span>
</template>
</component-a>
</div>
<script src="https://unpkg.com/vue@3.0.0-beta.21/dist/vue.global.js"></script>
<script>
const { createApp, createVNode, withCtx, toRaw } = Vue;
const A = {
render() {
return this.$slots.name();
}
}
const App = {
components: {
componentA: A
},
data() {
return {
value: 0
};
},
methods: {
inc() {
this.value += 1;
}
},
};
createApp(App).mount('#app');
</script>
</body>
</html>
此例子来自:jsbin.com/pepabemipi/…
传给 processIf
的回调函数最后会返回退出函数,在所有子节点转换完成时执行。
// packages/compiler-core/src/transforms/vIf.ts
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// ...
// 返回退出的回调函数,在所有子节点转换完成时执行
return () => {
if (isRoot) {
// v-if 节点的退出函数
// 创建 IF 节点的 codegenNode
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
} else {
// attach this branch's codegen node to the v-if root.
// v-else-if 、v-else 节点的退出函数
// 将此分支的 codegenNode 附加到 v-if 指令的根节点上
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
}
}
}
}
codegenNode 是 AST 转化为渲染函数的中间代码,解析原始 AST 语义而来。
getParentCondition
函数的作用是获取给定节点的父级条件表达式。
函数中使用了一个无限循环 while (true)
,在循环中判断节点的类型。如果节点的类型是 JS_CONDITIONAL_EXPRESSION
,则检查该节点的 alternate
是否也是 JS_CONDITIONAL_EXPRESSION
,如果是,将 node
更新为 node.alternate
,继续循环;如果不是,则返回当前的 node
。如果节点的类型是 JS_CACHE_EXPRESSION
,则将 node
更新为 node.value
,并将其类型断言为 IfConditionalExpression
。
该函数通过无限循环,直到找到最近的父级条件表达式,然后返回该条件表达式。
// packages/compiler-core/src/transforms/vIf.ts
function getParentCondition(
node: IfConditionalExpression | CacheExpression
): IfConditionalExpression {
while (true) {
if (node.type === NodeTypes.JS_CONDITIONAL_EXPRESSION) {
if (node.alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION) {
node = node.alternate
} else {
return node
}
} else if (node.type === NodeTypes.JS_CACHE_EXPRESSION) {
// 被 v-once 缓存,
// 即 v-if/v-else-if/v-else 与 v-once 在同一元素上的场景
node = node.value as IfConditionalExpression
}
}
}
getParentCondition
函数中使用的 IfConditionalExpression
、CacheExpression
类型其实是 Vue 在 ast 转换过程中定义的中间代码(codegen)类型。
// packages/compiler-core/src/ast.ts
export interface IfConditionalExpression extends ConditionalExpression {
consequent: BlockCodegenNode | MemoExpression
alternate: BlockCodegenNode | IfConditionalExpression | MemoExpression
}
// packages/compiler-core/src/ast.ts
export interface CacheExpression extends Node {
type: NodeTypes.JS_CACHE_EXPRESSION
index: number
value: JSChildNode
isVNode: boolean
}
当元素被 v-once
缓存时,生成的中间代码类型会被定义为 CacheExpression
,CacheExpression
的节点类型是 NodeTypes.JS_CACHE_EXPRESSION
。
在 Vue 的 3.0.0 版本中,如果 v-if/v-else-if/v-else
和 v-once
同时使用,则会导致模板编译报错,如下面的例子:
此例子源自 issue:github.com/vuejs/core/…
解决此 issue 的 pr :github.com/vuejs/core/…
报错如下:
getParentCondition
函数就解决了这个问题。
不过要注意的是,如果只是 v-if 与 v-once 一起使用,v-if 没有其他兄弟条件分支或,是不会产生编译报错的:
v-if 与 v-once 一起使用,v-if 的兄弟条件分支不管有没有 v-once ,都会报错
笔者特意下载了 Vue 3.0.0 版本的代码,查看此处的实现,发现在 3.2.45 版本的代码实现中,多了 else if (node.type === NodeTypes.JS_CACHE_EXPRESSION)
的代码分支:
借助 VSCode 提供的调试工具,可以发现 parentCondition
中没有 alternate 属性
因此会报 Cannot read property 'type' of undefined
的错误
有读者可能会疑问,笔者是如何调试的,具体操作是这样的:
首先,将此 issue:github.com/vuejs/core/… 中提供的例子 clone 下来
然后在该例子的项目根目录下运行 npm run serve 的命令,发现,定位到是 compiler-core.cjs.js
文件的 2702 行报错:
在 node_modules 下找到 compiler-core.cjs.js
文件,并在 2700 行打上断点:
借助 VSCode 的调试工具,调试 npm run serve 命令,程序的执行会自动在断点处停下,这样就可以分析代码的执行过程了
归根结底,此 issue:github.com/vuejs/core/… 产生的原因是 v-if 与 v-once 在一起使用的,在生成中间代码的过程中,其节点类型是 NodeTypes.JS_CACHE_EXPRESSION
,该类型与 NodeTypes.JS_CONDITIONAL_EXPRESSION
不同,没有 alternate
属性,因此根据不同的类型做一下兼容,就可以解决此 issue :
在 node_modules 下,getParentCondition
的代码如下:
function getParentCondition(node) {
while (true) {
if (node.type === 19 /* NodeTypes.JS_CONDITIONAL_EXPRESSION */) {
if (node.alternate.type === 19 /* NodeTypes.JS_CONDITIONAL_EXPRESSION */) {
node = node.alternate;
}
else {
return node;
}
}
else if (node.type === 20 /* NodeTypes.JS_CACHE_EXPRESSION */) {
node = node.value;
}
}
}
从 Vue 的测试用例中,也可以了解到当 v-if 与 v-once 一起使用时,生成的中间代码类型,该类型的 value 属性下才有 alternate 属性:
看完 getParentCondition
函数的实现后,我们再来看看 createCodegenNodeForBranch
函数的实现,当 v-if 节点执行退出函数时,会通过 createCodegenNodeForBranch
创建 IF 分支节点的 codegenNode:
// packages/compiler-core/src/transforms/vIf.ts
function createCodegenNodeForBranch(
branch: IfBranchNode,
keyIndex: number,
context: TransformContext
): IfConditionalExpression | BlockCodegenNode | MemoExpression {
if (branch.condition) {
return createConditionalExpression(
branch.condition,
createChildrenCodegenNode(branch, keyIndex, context),
// make sure to pass in asBlock: true so that the comment node call
// closes the current block.
createCallExpression(context.helper(CREATE_COMMENT), [
__DEV__ ? '"v-if"' : '""',
'true'
])
) as IfConditionalExpression
} else {
return createChildrenCodegenNode(branch, keyIndex, context)
}
}
当分支节点(branch
)存在 condition 的时候,比如 v-if
、和 v-else-if
,它通过 createConditionalExpression
函数返回一个条件表达式节点:
// packages/compiler-core/src/ast.ts
export function createConditionalExpression(
test: ConditionalExpression['test'],
consequent: ConditionalExpression['consequent'],
alternate: ConditionalExpression['alternate'],
newline = true
): ConditionalExpression {
return {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test,
consequent,
alternate,
newline,
loc: locStub
}
}
-
test
是主 IF 分支(v-if
)的条件表达式 -
consequent
是主 IF 分支(v-if
)条件成立时,要运行的 codeGenNode ,这里调用createChildrenCodegenNode
函数创建 -
alternate
是主 IF 分支(v-if
)条件不成立时,要运行的 codeGenNode ,默认是新建一个注释
v-if
与 v-else-if
、v-else
的 codegenNode 是通过 alternate 连接起来的,就像链表一样,而处于这个链表最顶端的,是 v-if
分支。
例如对于以下模板:
<span v-if="isShow">hello2</span>
<span v-else-if="isVisible">hello3</span>
<div v-else>world</div>
借助 Vue3 提供的 Template Explorer 工具,可得上面模板编译后的 AST 树为:
在 Vue3 源码的根目录下运行
pnpm run dev-compiler
命令,然后在浏览器中打开 http://localhost:5000/packages/template-explorer/local.html ,就可以打开 Vue 官方的模板编译工具
接下来看 createChildrenCodegenNode
函数的实现:
// packages/compiler-core/src/transforms/vIf.ts
function createChildrenCodegenNode(
branch: IfBranchNode,
keyIndex: number,
context: TransformContext
): BlockCodegenNode | MemoExpression {
const { helper } = context
const keyProperty = createObjectProperty(
`key`,
createSimpleExpression(
`${keyIndex}`,
false,
locStub,
ConstantTypes.CAN_HOIST
)
)
const { children } = branch
const firstChild = children[0]
const needFragmentWrapper =
children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT
if (needFragmentWrapper) {
if (children.length === 1 && firstChild.type === NodeTypes.FOR) {
// optimize away nested fragments when child is a ForNode
const vnodeCall = firstChild.codegenNode!
injectProp(vnodeCall, keyProperty, context)
return vnodeCall
} else {
let patchFlag = PatchFlags.STABLE_FRAGMENT
let patchFlagText = PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
// check if the fragment actually contains a single valid child with
// the rest being comments
if (
__DEV__ &&
!branch.isTemplateIf &&
children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
) {
patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT
patchFlagText += `, ${PatchFlagNames[PatchFlags.DEV_ROOT_FRAGMENT]}`
}
return createVNodeCall(
context,
helper(FRAGMENT),
createObjectExpression([keyProperty]),
children,
patchFlag + (__DEV__ ? ` /* ${patchFlagText} */` : ``),
undefined,
undefined,
true,
false,
false /* isComponent */,
branch.loc
)
}
} else {
const ret = (firstChild as ElementNode).codegenNode as
| BlockCodegenNode
| MemoExpression
const vnodeCall = getMemoedVNodeCall(ret)
// Change createVNode to createBlock.
if (vnodeCall.type === NodeTypes.VNODE_CALL) {
makeBlock(vnodeCall, context)
}
// inject branch key
injectProp(vnodeCall, keyProperty, context)
return ret
}
}
createChildrenCodegenNode
函数首先会创建 keyProperty
,用于优化 Diff 过程,会被注入到生成的 codeGenNode 中。
接着拿出 branch
中的 firstChild
判断是否需要被 Fragment 包住。
const needFragmentWrapper =
children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT
当 children
只有 1 个元素,firstChild 的节点类型是 NodeTypes.FOR
类型,这时不需要 Fragment 包住,因为在创建 FOR 类型的 codegenNode 时,本身会创建 Fragment 包住自身,因此这里为避免嵌套的 Fragment ,就不需要重复创建 Fragment 了。
对于非 NodeTypes.FOR
类型且需要 Fragment 包住的场景,则创建 patchFlag 为 STABLE_FRAGMENT 的 VNodeCall 。
VNodeCall 是一个对象,该对象用于
generate
阶段的一个代码生成。
对于不需要 Fragment 包住的 else 分支,即 children
长度为 1 且 type 为 NodeTypes.ELEMENT
的情况,调用 getMemoedVNodeCall
函数获取 VNodeCall
, 调用 makeBlock
函数把 createVNode 改变为 createBlock ,最后给 branch 注入 key 属性
injectProp(vnodeCall, keyProperty, context)
getMemoedVNodeCall
函数用于获取被 v-memo 缓存的 v-if 节点的 VNodeCall
// packages/compiler-core/src/utils.ts
export function getMemoedVNodeCall(node: BlockCodegenNode | MemoExpression) {
if (node.type === NodeTypes.JS_CALL_EXPRESSION && node.callee === WITH_MEMO) {
return node.arguments[1].returns as VNodeCall
} else {
return node
}
}
例如下面的例子:
<span v-if="isShow" v-memo="[]">hello2</span>
v-if 总结
Vue3 与 Vue2 一样,会将 v-if
最终编译为三元表达式。
Vue3 与 Vue2 不同的是,他会创建 Block
树,Block
是一种特殊的虚拟节点(vnode
),它和普通虚拟节点(vnode
)相比,多出一个额外的 dynamicChildren
属性,用来存储动态节点。Block
的出现优化了 Diff 的过程。在 Vue2 的 Diff 过程中,即使虚拟节点(vnode
)没有变化,也会进行一次比较,而 Block
的出现减少了这种不必要的比较,由于 Block
中的动态节点都会被收集到 dynamicChildren
中,所以 Block
间的 patch
可以直接比较 dynamicChildren
中的节点,减少了非动态节点之间的比较,减少了 Diff 的工作量。可以说,这是 Vue3 相对于 Vue2 做的一个优化。
但是 Vue3 与 Vue2 实现 v-if
的原理是一样的,就是通过分析 v-if
的条件表达式,最终将其编译为三元表达式执行。
v-for
<div id="app">
<li v-for="todo in todos" :key="todo.name">
{{ todo.name }}
</li>
</div>
上面的模板会被 Vue 编译器编译为下面的渲染函数
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", { id: "app" }, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.todos, (todo) => {
return (_openBlock(), _createElementBlock("li", {
key: todo.name
}, _toDisplayString(todo.name), 1 /* TEXT */))
}), 128 /* KEYED_FRAGMENT */))
]))
}
openBlock
、createElementBlock
在讲述 v-if 的部分已有说明,此处不再赘述。
可以看到 Vue3 与 Vue2 在实现 v-for 指令时是类似的,最终 v-for 指令会被编译为 renderList
函数执行。
具体看看 renderList
函数的实现:
// packages/runtime-core/src/helpers/renderList.ts
export function renderList(
source: any,
renderItem: (...args: any[]) => VNodeChild,
cache?: any[],
index?: number
): VNodeChild[] {
let ret: VNodeChild[]
const cached = (cache && cache[index!]) as VNode[] | undefined
if (isArray(source) || isString(source)) {
ret = new Array(source.length)
for (let i = 0, l = source.length; i < l; i++) {
ret[i] = renderItem(source[i], i, undefined, cached && cached[i])
}
} else if (typeof source === 'number') {
if (__DEV__ && !Number.isInteger(source)) {
warn(`The v-for range expect an integer value but got ${source}.`)
}
ret = new Array(source)
for (let i = 0; i < source; i++) {
ret[i] = renderItem(i + 1, i, undefined, cached && cached[i])
}
} else if (isObject(source)) {
if (source[Symbol.iterator as any]) {
ret = Array.from(source as Iterable<any>, (item, i) =>
renderItem(item, i, undefined, cached && cached[i])
)
} else {
const keys = Object.keys(source)
ret = new Array(keys.length)
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
ret[i] = renderItem(source[key], key, i, cached && cached[i])
}
}
} else {
ret = []
}
if (cache) {
cache[index!] = ret
}
return ret
}
可以发现 Vue3 中 renderList
函数的实现与 Vue2 中的实现非常像。都是判断传入的数据类型是否为数组、字符串、数字和对象,然后遍历他们,如果传入的数据不是数组、字符串、数字或对象类型的,则渲染空数组。
而与 Vue2 不同的是,Vue3 的 v-for 增加了对 v-memo 的支持。v-memo 也是 Vue3 新增的内置指令。
const cached = (cache && cache[index!]) as VNode[] | undefined
// ...
renderItem(source[i], i, undefined, cached && cached[i])
当传入的值(source
)为数组或字符串时,for 循环直接遍历:
// packages/runtime-core/src/helpers/renderList.ts
if (isArray(source) || isString(source)) {
ret = new Array(source.length)
for (let i = 0, l = source.length; i < l; i++) {
ret[i] = renderItem(source[i], i, undefined, cached && cached[i])
}
}
当传入的值(source
)为数字时,如果在开发环境,则会判断传入的数字是否为整数,如果不为整数,则打印提示信息。然后 for 循环直接遍历,传入的数字是多少则遍历多少次:
// packages/runtime-core/src/helpers/renderList.ts
if (isArray(source) || isString(source)) {
// ...
} else if (typeof source === 'number') {
if (__DEV__ && !Number.isInteger(source)) {
warn(`The v-for range expect an integer value but got ${source}.`)
}
ret = new Array(source)
for (let i = 0; i < source; i++) {
ret[i] = renderItem(i + 1, i, undefined, cached && cached[i])
}
}
当传入的值(source
)为对象时,则分为两种情况,当传入的对象是可迭代对象时,则使用 Array.from
遍历,这点与 Vue2 不太一样,在 Vue2 中是调用该对象内置的迭代器的 next() 方法进行遍历。当传入的对象是不可迭代的对象时,则通过 Object.key 生成对象的属性数组,然后通过 for 循环遍历,这点与 Vue2 是一样的。
// packages/runtime-core/src/helpers/renderList.ts
if (isArray(source) || isString(source)) {
// ...
} else if (typeof source === 'number') {
// ...
} else if (isObject(source)) {
if (source[Symbol.iterator as any]) {
ret = Array.from(source as Iterable<any>, (item, i) =>
renderItem(item, i, undefined, cached && cached[i])
)
} else {
const keys = Object.keys(source)
ret = new Array(keys.length)
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
ret[i] = renderItem(source[key], key, i, cached && cached[i])
}
}
}
当传入的值不是数组、字符串、数字和对象时,则将返回结果 (ret
)设置为空数组,即 v-for 渲染的为空数组
// packages/runtime-core/src/helpers/renderList.ts
if (isArray(source) || isString(source)) {
// ...
} else if (typeof source === 'number') {
// ...
} else if (isObject(source)) {
// ...
} else {
ret = []
}
// v-memo 相关的逻辑
if (cache) {
cache[index!] = ret
}
return ret
通过上面的分析可知,v-for
本质是调用 renderList
函数。那么 v-for
是如何被编译为 renderList
函数的?
Vue 的模板编译器会分析模板,将其解析为模板 AST ,然后将模板 AST 转换为用于描述渲染函数的 JavaScript AST ,最后根据 JavaScript AST 生成渲染函数代码。
v-for
和 v-if
一样,都属于结构化指令,因此由 createStructuralDirectiveTransform
生成转换函数 (transformFor
),该转换函数的作用是将模板 AST 转换为 JavaScript AST 。
// packages/compiler-core/src/transforms/vFor.ts
export const transformFor = createStructuralDirectiveTransform(
'for',
(node, dir, context) => {
const { helper, removeHelper } = context
return processFor(node, dir, context, forNode => {
// ...
// 返回退出回调函数,当所有子节点转换完成执行
return () => {
// ...
}
})
}
)
createStructuralDirectiveTransform
的函数实现在讲 v-if
的时候已经说过,这里不再赘述。
传入 createStructuralDirectiveTransform
函数的结构转换函数主要是执行 processFor
函数,processFor
函数的作用是处理 v-for
指令 codegenNode 的转换和生成过程。
// packages/compiler-core/src/transforms/vFor.ts
export function processFor(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext,
processCodegen?: (forNode: ForNode) => (() => void) | undefined
) {
if (!dir.exp) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc)
)
return
}
const parseResult = parseForExpression(
// can only be simple expression because vFor transform is applied
// before expression transform.
dir.exp as SimpleExpressionNode,
context
)
if (!parseResult) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
)
return
}
const { addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult
const forNode: ForNode = {
type: NodeTypes.FOR,
loc: dir.loc,
source,
valueAlias: value,
keyAlias: key,
objectIndexAlias: index,
parseResult,
children: isTemplateNode(node) ? node.children : [node]
}
context.replaceNode(forNode)
// bookkeeping
scopes.vFor++
if (!__BROWSER__ && context.prefixIdentifiers) {
// scope management
// inject identifiers to context
value && addIdentifiers(value)
key && addIdentifiers(key)
index && addIdentifiers(index)
}
const onExit = processCodegen && processCodegen(forNode)
return () => {
scopes.vFor--
if (!__BROWSER__ && context.prefixIdentifiers) {
value && removeIdentifiers(value)
key && removeIdentifiers(key)
index && removeIdentifiers(index)
}
if (onExit) onExit()
}
}
首先判断传入的指令节点 dir
是否存在表达式,如果不存在,则抛出 X_V_FOR_NO_EXPRESSION
的编译时错误:
if (!dir.exp) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc)
)
return
}
然后解析指令节点 dir
中带有的表达式,得到 parseResult
:
const parseResult = parseForExpression(
dir.exp as SimpleExpressionNode,
context
)
如果 parseResult
不存在,说明指令节点 dir
中带有的表达式的格式有问题,则抛出 X_V_FOR_MALFORMED_EXPRESSION
的编译时错误:
if (!parseResult) {
context.onError(
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc)
)
return
}
如果表达式解析成功,则从编译上下文对象 context
中取得 addIdentifiers
函数、removeIdentifiers
函数 、scopes
对象后续备用。
const { addIdentifiers, removeIdentifiers, scopes } = context
addIdentifiers
函数的作用就是给表达式添加唯一的 id 标识
// packages/compiler-core/src/transform.ts
const context: TransformContext = {
addIdentifiers(exp) {
// identifier tracking only happens in non-browser builds.
if (!__BROWSER__) {
if (isString(exp)) {
addId(exp)
} else if (exp.identifiers) {
exp.identifiers.forEach(addId)
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
addId(exp.content)
}
}
},
}
function addId(id: string) {
const { identifiers } = context
if (identifiers[id] === undefined) {
identifiers[id] = 0
}
identifiers[id]!++
}
removeIdentifiers
函数的作用是删除传入的表达式的唯一 id 标识:
// packages/compiler-core/src/transform.ts
const context: TransformContext = {
removeIdentifiers(exp) {
if (!__BROWSER__) {
if (isString(exp)) {
removeId(exp)
} else if (exp.identifiers) {
exp.identifiers.forEach(removeId)
} else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
removeId(exp.content)
}
}
}
}
function removeId(id: string) {
context.identifiers[id]!--
}
scopes
对象的作用是标识不同的指令环境(作用域),v-for
、v-slot
、v-pre
、v-once
,这里用计数来标记所处的环境,因为存在指令嵌套的情况。
// packages/compiler-core/src/transform.ts
export interface TransformContext
extends Required<
Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
>,
CompilerCompatOptions {
// ...
scopes: {
vFor: number
vSlot: number
vPre: number
vOnce: number
}
}
例如下面两层嵌套的 v-for
场景:
<script src="../../../dist/vue.global.js"></script>
<div id="demo">
<li v-for="oItem in outer" :key="oItem">
<span v-for="inItem in inner">
{{ oItem }} - {{ inItem }}
</span>
</li>
</div>
<script>
const { createApp } = Vue
createApp({
setup() {
const outer = [
'a',
'b',
'c'
]
const inner = [
11,
12,
13
]
return {
outer,
inner
}
}
}).mount('#demo')
</script>
通过断点调试,可以发现嵌套了几层,scopes
中相应的计数就会达到多少:
然后创建 forNode
对象,并用 forNode
对象替换当前节点:
const { source, value, key, index } = parseResult
const forNode: ForNode = {
type: NodeTypes.FOR,
loc: dir.loc,
source,
valueAlias: value,
keyAlias: key,
objectIndexAlias: index,
parseResult,
children: isTemplateNode(node) ? node.children : [node]
}
context.replaceNode(forNode)
我们从 v-for
指令的表达式解析的结果 parseResult
中解构,得到 source
、value
、key
、index
,这四个值在 Vue.js 源码中都被定义为 ExpressionNode
类型的,包含了相关的 v-for
表达式信息和节点类型。
// packages/compiler-core/src/transforms/vFor.ts
export interface ForParseResult {
source: ExpressionNode
value: ExpressionNode | undefined
key: ExpressionNode | undefined
index: ExpressionNode | undefined
}
例如下面这种场景:
<script src="../../../dist/vue.global.js"></script>
<div id="demo">
<li v-for="(oVal, oKey, oIdx) in myObject">
{{ oIdx }}. {{ oKey }}: {{ oVal }}
</li>
</div>
<script>
const { createApp } = Vue
createApp({
setup() {
const myObject = {
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
}
return {
myObject
}
}
}).mount('#demo')
</script>
通过断点调试,可以观察到 parseResult
中 source
、value
、key
、index
的值:
v-for
指令作用域管理,增加 v-for
的作用域计数,在非浏览器环境中,如果启用了标识符前缀,则将 v-for
指令表达式中解析出的变量(value
、key
、index
)添加到编译上下文对象的标识符(identifiers
)中。
标识符前缀是 Vue.js 中 AST 节点转换的一个选项配置:
如果 prefixIdentifiers
配置为 false ,则生成的表达式会包裹在 with 块中。
scopes.vFor++
if (!__BROWSER__ && context.prefixIdentifiers) {
// scope management
// inject identifiers to context
value && addIdentifiers(value)
key && addIdentifiers(key)
index && addIdentifiers(index)
}
v-for
指令的 codegenNode 生成后的处理:
const onExit = processCodegen && processCodegen(forNode)
如果给 processFor
函数提供了 processCodegen
回调,则调用他进行 codegenNode 生成的后续处理。
processCodegen
函数会返回一个退出函数(onExit
),在最后 processFor
返回的函数中执行。
最后 processFor
会返回清理函数,在 v-for
模板 AST 转换完毕后进行清理工作,比如减少作用域计数、移除标识符,这可确保 v-for
指令作用域管理的正确性。
return () => {
scopes.vFor--
if (!__BROWSER__ && context.prefixIdentifiers) {
value && removeIdentifiers(value)
key && removeIdentifiers(key)
index && removeIdentifiers(index)
}
if (onExit) onExit()
}
processFor
是一个复杂的函数,里面涉及了 v-for
表达式的解析、节点替换、作用域管理及标识符的处理。
在 processFor
函数中调用了 parseForExpression
函数解析 v-for
指令中的表达式,将其转换为一个对象,包含了 v-for
中的各个参数,如 source
、value
、key
和 index
等。其中,source
表示要遍历的数据源,value
表示当前遍历的值,key
表示当前遍历的键,index
表示当前遍历的索引。接下来看看 parseForExpression
函数的实现:
// packages/compiler-core/src/transforms/vFor.ts
export function parseForExpression(
input: SimpleExpressionNode,
context: TransformContext
): ForParseResult | undefined {
const loc = input.loc
const exp = input.content
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
const [, LHS, RHS] = inMatch
const result: ForParseResult = {
source: createAliasExpression(
loc,
RHS.trim(),
exp.indexOf(RHS, LHS.length)
),
value: undefined,
key: undefined,
index: undefined
}
if (!__BROWSER__ && context.prefixIdentifiers) {
result.source = processExpression(
result.source as SimpleExpressionNode,
context
)
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(result.source as SimpleExpressionNode, context)
}
let valueContent = LHS.trim().replace(stripParensRE, '').trim()
const trimmedOffset = LHS.indexOf(valueContent)
const iteratorMatch = valueContent.match(forIteratorRE)
if (iteratorMatch) {
valueContent = valueContent.replace(forIteratorRE, '').trim()
const keyContent = iteratorMatch[1].trim()
let keyOffset: number | undefined
if (keyContent) {
keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
result.key = createAliasExpression(loc, keyContent, keyOffset)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.key = processExpression(result.key, context, true)
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(
result.key as SimpleExpressionNode,
context,
true
)
}
}
if (iteratorMatch[2]) {
const indexContent = iteratorMatch[2].trim()
if (indexContent) {
result.index = createAliasExpression(
loc,
indexContent,
exp.indexOf(
indexContent,
result.key
? keyOffset! + keyContent.length
: trimmedOffset + valueContent.length
)
)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.index = processExpression(result.index, context, true)
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(
result.index as SimpleExpressionNode,
context,
true
)
}
}
}
}
if (valueContent) {
result.value = createAliasExpression(loc, valueContent, trimmedOffset)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.value = processExpression(result.value, context, true)
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(
result.value as SimpleExpressionNode,
context,
true
)
}
}
return result
}
parseForExpression
函数首先使用正则表达式 forAliasRE
匹配 v-for
表达式中的左右两个参数,即 LHS
和 RHS
,然后根据这两个参数构造出一个 ForParseResult
对象,其中 source
属性表示数据源,使用 createAliasExpression
函数创建一个别名表达式,value
、key
和 index
属性则分别表示当前遍历的值、键和索引,初始值都为 undefined
。
// packages/compiler-core/src/transforms/vFor.ts
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
const loc = input.loc
const exp = input.content
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
const [, LHS, RHS] = inMatch
const result: ForParseResult = {
source: createAliasExpression(
loc,
RHS.trim(),
exp.indexOf(RHS, LHS.length)
),
value: undefined,
key: undefined,
index: undefined
}
正则表达式 forAliasRE
匹配 v-for
表达式中的左右两个参数:
-
([sS]*?)
,这是一个捕获组,用来匹配任意空白字符或非空白字符,并且是非贪婪的,也就是说会尽可能少地匹配字符。这个捕获组的作用是匹配循环语句中的循环变量 -
s+
, 匹配至少一个空白字符 -
([sS]*)
,这是第二个捕获组,字符或非空白字符,这表示循环中的数组或对象
借助正则表达式可视化工具可视化上面的正则表达式,则更加容易理解上面正则表达式的作用:
对于下面的模板:
<li v-for="(oVal, oKey, oIdx) in myObject">
{{ oIdx }}. {{ oKey }}: {{ oVal }}
</li>
得到的 LHS
为 (oVal, oKey, oIdx)
,RHS
为 myObject
:
然后使用正则表达式 stripParensRE
去掉左边表达式(LHS
)左右两边的括号:
let valueContent = LHS.trim().replace(stripParensRE, '').trim()
此时得到 valueContent
为 oVal, oKey, oIdx
。
// packages/compiler-core/src/transforms/vFor.ts
const stripParensRE = /^\(|\)$/g
stripParensRE
用于匹配字符串开头的左括号'('
或者字符串末尾的右括号 ')'
。使用正则可视化工具可视化后的结果:
^
:表示匹配输入的开始位置,在这里表示左括号('('
)开头的字符串
(|)
:表示匹配左括号或者右括号
|
:是一个逻辑或操作符,表示在匹配时可以选择两个条件中的任意一个
$
:表示匹配输入的结束位置,在这里表示以括号结尾的字符串
g
:表示进行全局匹配,即匹配所有符合条件的子字符串
然后使用 indexOf 方法得到 valueContent
在左侧表达式(LHS
)中的偏移量 trimmedOffset
,后续可用该偏移量计算得到 keyContent
、indexContent
。
const trimmedOffset = LHS.indexOf(valueContent)
然后,使用正则表达式 forIteratorRE
匹配 valueContent
中的迭代器别名:
const iteratorMatch = valueContent.match(forIteratorRE)
匹配得到 iteratorMatch
为:
forIteratorRE
用于匹配一个逗号分隔的字符串,该字符串由逗号开头,并且后续由逗号和非逗号、非右大括号、非右中括号的字符组成。
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
借助正则表达式可视化工具可视化上面的正则表达式:
match 方法返回的数组中,在索引 0 处,为该正则完整的匹配项;在索引 1 处,第一个捕获括号匹配的内容,在索引 2 处,第二个捕获括号匹配的内容,以此类推。
forIteratorRE
正则中有两个捕获组和一个非捕获组:
捕获组匹配到的结果会作为 match 方法的返回数组中的元素,在索引 1 处为第一个捕获括号匹配的内容。在索引 2 处为第二个捕获括号匹配的内容,以此类推。
非捕获组匹配到的结果不会出现在 match 方法返回的数组中。
如果 forIteratorRE
匹配成功,即 iteratorMatch
有值,则将 valueContent
中的迭代器别名替换为空字符串,并提取出 keyContent
和 indexContent
。接着,根据 keyContent
和 indexContent
计算出它们在表达式(exp
)中的偏移量,并使用 createAliasExpression
函数创建对应的别名表达式节点,并将其存储到 result
对象的 key
和 index
属性中 。
if (iteratorMatch) {
valueContent = valueContent.replace(forIteratorRE, '').trim()
const keyContent = iteratorMatch[1].trim()
let keyOffset: number | undefined
if (keyContent) {
keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
result.key = createAliasExpression(loc, keyContent, keyOffset)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.key = processExpression(result.key, context, true)
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(
result.key as SimpleExpressionNode,
context,
true
)
}
}
if (iteratorMatch[2]) {
const indexContent = iteratorMatch[2].trim()
if (indexContent) {
result.index = createAliasExpression(
loc,
indexContent,
exp.indexOf(
indexContent,
result.key
? keyOffset! + keyContent.length
: trimmedOffset + valueContent.length
)
)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.index = processExpression(result.index, context, true)
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(
result.index as SimpleExpressionNode,
context,
true
)
}
}
}
}
最后使用 valueContent
,调用 createAliasExpression
函数创建 value 表达式节点,存储到 result
对象的 value
属性中,然后返回 result
对象,该对象包含了 v-for
指令表达式中的相关信息。
if (valueContent) {
result.value = createAliasExpression(loc, valueContent, trimmedOffset)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.value = processExpression(result.value, context, true)
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(
result.value as SimpleExpressionNode,
context,
true
)
}
}
return result
最后得到的 result
对象为:
分析完 parseForExpression
,咱们回到 createStructuralDirectiveTransform
函数中,createStructuralDirectiveTransform
函数的作用是创建 v-for
的转换函数,在调用 processFor
函数的时候,给 processFor
函数传入了 processCodegen
函数。
我们来分析一下 processCodegen
函数的实现:
// packages/compiler-core/src/transforms/vFor.ts\
export const transformFor = createStructuralDirectiveTransform(
'for',
(node, dir, context) => {
const { helper, removeHelper } = context
return processFor(node, dir, context, forNode => {
// 传入 processFor 函数的 processCodegen 回调函数
// create the loop render function expression now, and add the
// iterator on exit after all children have been traversed
const renderExp = createCallExpression(helper(RENDER_LIST), [
forNode.source
]) as ForRenderListExpression
const isTemplate = isTemplateNode(node)
const memo = findDir(node, 'memo')
const keyProp = findProp(node, `key`)
const keyExp =
keyProp &&
(keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!)
const keyProperty = keyProp ? createObjectProperty(`key`, keyExp!) : null
if (!__BROWSER__ && isTemplate) {
// #2085 / #5288 process :key and v-memo expressions need to be
// processed on `<template v-for>`. In this case the node is discarded
// and never traversed so its binding expressions won't be processed
// by the normal transforms.
if (memo) {
memo.exp = processExpression(
memo.exp! as SimpleExpressionNode,
context
)
}
if (keyProperty && keyProp!.type !== NodeTypes.ATTRIBUTE) {
keyProperty.value = processExpression(
keyProperty.value as SimpleExpressionNode,
context
)
}
}
const isStableFragment =
forNode.source.type === NodeTypes.SIMPLE_EXPRESSION &&
forNode.source.constType > ConstantTypes.NOT_CONSTANT
const fragmentFlag = isStableFragment
? PatchFlags.STABLE_FRAGMENT
: keyProp
? PatchFlags.KEYED_FRAGMENT
: PatchFlags.UNKEYED_FRAGMENT
forNode.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
renderExp,
fragmentFlag +
(__DEV__ ? ` /* ${PatchFlagNames[fragmentFlag]} */` : ``),
undefined,
undefined,
true /* isBlock */,
!isStableFragment /* disableTracking */,
false /* isComponent */,
node.loc
) as ForCodegenNode
return () => {
// finish the codegen now that all children have been traversed
let childBlock: BlockCodegenNode
const { children } = forNode
// check <template v-for> key placement
if ((__DEV__ || !__BROWSER__) && isTemplate) {
node.children.some(c => {
if (c.type === NodeTypes.ELEMENT) {
const key = findProp(c, 'key')
if (key) {
context.onError(
createCompilerError(
ErrorCodes.X_V_FOR_TEMPLATE_KEY_PLACEMENT,
key.loc
)
)
return true
}
}
})
}
const needFragmentWrapper =
children.length !== 1 || children[0].type !== NodeTypes.ELEMENT
const slotOutlet = isSlotOutlet(node)
? node
: isTemplate &&
node.children.length === 1 &&
isSlotOutlet(node.children[0])
? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
: null
if (slotOutlet) {
// <slot v-for="..."> or <template v-for="..."><slot/></template>
childBlock = slotOutlet.codegenNode as RenderSlotCall
if (isTemplate && keyProperty) {
// <template v-for="..." :key="..."><slot/></template>
// we need to inject the key to the renderSlot() call.
// the props for renderSlot is passed as the 3rd argument.
injectProp(childBlock, keyProperty, context)
}
} else if (needFragmentWrapper) {
// <template v-for="..."> with text or multi-elements
// should generate a fragment block for each loop
childBlock = createVNodeCall(
context,
helper(FRAGMENT),
keyProperty ? createObjectExpression([keyProperty]) : undefined,
node.children,
PatchFlags.STABLE_FRAGMENT +
(__DEV__
? ` /* ${PatchFlagNames[PatchFlags.STABLE_FRAGMENT]} */`
: ``),
undefined,
undefined,
true,
undefined,
false /* isComponent */
)
} else {
// Normal element v-for. Directly use the child's codegenNode
// but mark it as a block.
childBlock = (children[0] as PlainElementNode)
.codegenNode as VNodeCall
if (isTemplate && keyProperty) {
injectProp(childBlock, keyProperty, context)
}
if (childBlock.isBlock !== !isStableFragment) {
if (childBlock.isBlock) {
// switch from block to vnode
removeHelper(OPEN_BLOCK)
removeHelper(
getVNodeBlockHelper(context.inSSR, childBlock.isComponent)
)
} else {
// switch from vnode to block
removeHelper(
getVNodeHelper(context.inSSR, childBlock.isComponent)
)
}
}
childBlock.isBlock = !isStableFragment
if (childBlock.isBlock) {
helper(OPEN_BLOCK)
helper(getVNodeBlockHelper(context.inSSR, childBlock.isComponent))
} else {
helper(getVNodeHelper(context.inSSR, childBlock.isComponent))
}
}
if (memo) {
const loop = createFunctionExpression(
createForLoopParams(forNode.parseResult, [
createSimpleExpression(`_cached`)
])
)
loop.body = createBlockStatement([
createCompoundExpression([`const _memo = (`, memo.exp!, `)`]),
createCompoundExpression([
`if (_cached`,
...(keyExp ? [` && _cached.key === `, keyExp] : []),
` && ${context.helperString(
IS_MEMO_SAME
)}(_cached, _memo)) return _cached`
]),
createCompoundExpression([`const _item = `, childBlock as any]),
createSimpleExpression(`_item.memo = _memo`),
createSimpleExpression(`return _item`)
])
renderExp.arguments.push(
loop as ForIteratorExpression,
createSimpleExpression(`_cache`),
createSimpleExpression(String(context.cached++))
)
} else {
renderExp.arguments.push(
createFunctionExpression(
createForLoopParams(forNode.parseResult),
childBlock,
true /* force newline */
) as ForIteratorExpression
)
}
}
})
}
)
创建一个调用 RENDER_LIST
的表达式,这个表达式用于渲染列表:
const renderExp = createCallExpression(helper(RENDER_LIST), [
forNode.source
]) as ForRenderListExpression
检查当前节点是否是一个 template 标签(<template>
):
const isTemplate = isTemplateNode(node)
// packages/compiler-core/src/utils.ts
// 判断是否为 template 标签(<template>)节点
export function isTemplateNode(
node: RootNode | TemplateChildNode
): node is TemplateNode {
return (
node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.TEMPLATE
)
}
检查当前节点使用使用了 v-memo
指令,在 Vue3 中的 v-for
指令与 Vue2 中的 v-for
指令一个较大的区别就是 Vue3 中的 v-for
指令增加了对 v-memo
的支持。当然,v-memo
也是 Vue3 才有的指令。
const memo = findDir(node, 'memo')
// packages/compiler-core/src/utils.ts
// 查找传入节点中相关指令
export function findDir(
node: ElementNode,
name: string | RegExp,
allowEmpty: boolean = false
): DirectiveNode | undefined {
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (
p.type === NodeTypes.DIRECTIVE &&
(allowEmpty || p.exp) &&
(isString(name) ? p.name === name : name.test(p.name))
) {
return p
}
}
}
查找当前节点是否有 key
属性:
const keyProp = findProp(node, `key`)
// packages/compiler-core/src/utils.ts
// 查找传入节点的 prop
export function findProp(
node: ElementNode,
name: string,
dynamicOnly: boolean = false,
allowEmpty: boolean = false
): ElementNode['props'][0] | undefined {
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (p.type === NodeTypes.ATTRIBUTE) {
if (dynamicOnly) continue
if (p.name === name && (p.value || allowEmpty)) {
return p
}
} else if (
p.name === 'bind' &&
(p.exp || allowEmpty) &&
isStaticArgOf(p.arg, name)
) {
return p
}
}
}
根据 keyProp
类型创建一个简单表达式节点(SimpleExpressionNode
),如果 keyProp
存在,则创建一个对象属性,用于后续的渲染。
const keyExp =
keyProp &&
(keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!)
const keyProperty = keyProp ? createObjectProperty(`key`, keyExp!) : null
// packages/compiler-core/src/ast.ts
// 创建对象属性
export function createObjectProperty(
key: Property['key'] | string,
value: Property['value']
): Property {
return {
type: NodeTypes.JS_PROPERTY,
loc: locStub,
key: isString(key) ? createSimpleExpression(key, true) : key,
value
}
}
增加对 v-memo
的支持:
if (!__BROWSER__ && isTemplate) {
if (memo) {
memo.exp = processExpression(
memo.exp! as SimpleExpressionNode,
context
)
}
if (keyProperty && keyProp!.type !== NodeTypes.ATTRIBUTE) {
keyProperty.value = processExpression(
keyProperty.value as SimpleExpressionNode,
context
)
}
}
确定是否为稳定片段:
const isStableFragment =
forNode.source.type === NodeTypes.SIMPLE_EXPRESSION &&
forNode.source.constType > ConstantTypes.NOT_CONSTANT
判断片段标志,根据 isStableFragment
的值决定 fragmentFlag
的值。如果是稳定片段,使用 STABLE_FRAGMENT
作为标志;如果存在 keyProp
,使用 KEYED_FRAGMENT
;否则,使用 UNKEYED_FRAGMENT
。
const fragmentFlag = isStableFragment
? PatchFlags.STABLE_FRAGMENT
: keyProp
? PatchFlags.KEYED_FRAGMENT
: PatchFlags.UNKEYED_FRAGMENT
使用 createVNodeCall
函数生成 forNode
的 codegenNode
:
forNode.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
renderExp,
fragmentFlag +
(__DEV__ ? ` /* ${PatchFlagNames[fragmentFlag]} */` : ``),
undefined,
undefined,
true /* isBlock */,
!isStableFragment /* disableTracking */,
false /* isComponent */,
node.loc
) as ForCodegenNode
最后返回退出函数,在 codegen
生成完毕后执行:
return () => {
// ...
}
如果是 <template v-for>
,检查子元素是否有 key
属性,若有输出 X_V_FOR_TEMPLATE_KEY_PLACEMENT
编译时错误。
在 Vue.js 的官网中也有说明,当使用 <template v-for>
时,key
应该被放置在这个 <template>
容器上:
// check <template v-for> key placement
if ((__DEV__ || !__BROWSER__) && isTemplate) {
node.children.some(c => {
if (c.type === NodeTypes.ELEMENT) {
const key = findProp(c, 'key')
if (key) {
context.onError(
createCompilerError(
ErrorCodes.X_V_FOR_TEMPLATE_KEY_PLACEMENT,
key.loc
)
)
return true
}
}
})
}
判断是否需要 Fragment 包裹,如果子节点的数量不为 1 ,或者第一个子节点不是元素类型,则需要使用 Fragment 包裹。
const needFragmentWrapper =
children.length !== 1 || children[0].type !== NodeTypes.ELEMENT
检查当前节点是否是插槽(<slot>
),如果是插槽,直接使用他,如果是模板(<template>
)且只有一个子节点,并且该子节点是插槽,则取该子节点:
const slotOutlet = isSlotOutlet(node)
? node
: isTemplate &&
node.children.length === 1 &&
isSlotOutlet(node.children[0])
? (node.children[0] as SlotOutletNode) // api-extractor somehow fails to infer this
: null
处理 v-for
循环渲染插槽的场景,并向插槽(<slot>
)的 codegenNode 中注入 key
属性:
if (slotOutlet) {
// <slot v-for="..."> or <template v-for="..."><slot/></template>
childBlock = slotOutlet.codegenNode as RenderSlotCall
if (isTemplate && keyProperty) {
// <template v-for="..." :key="..."><slot/></template>
// we need to inject the key to the renderSlot() call.
// the props for renderSlot is passed as the 3rd argument.
injectProp(childBlock, keyProperty, context)
}
}
处理需要 Fragment 包裹的场景,当 v-for
循环渲染 template 标签(<template>
),template 标签下是纯文本或多个元素时,需要为每个循环创建一个 Fragment 块:
else if (needFragmentWrapper) {
// <template v-for="..."> with text or multi-elements
// should generate a fragment block for each loop
childBlock = createVNodeCall(
context,
helper(FRAGMENT),
keyProperty ? createObjectExpression([keyProperty]) : undefined,
node.children,
PatchFlags.STABLE_FRAGMENT +
(__DEV__
? ` /* ${PatchFlagNames[PatchFlags.STABLE_FRAGMENT]} */`
: ``),
undefined,
undefined,
true,
undefined,
false /* isComponent */
)
}
例如,v-for
循环渲染 template 标签,template 标签下有多个元素的场景:
v-for
循环渲染 template 标签,template 标签下是纯文本的场景:
对于循环渲染普通元素的场景,直接获取第一个子节点的 codegenNode
作为 childBlock
。
如果当前节点是一个模板(<template>
)并且存在 keyProperty
,则调用 injectProp
函数将 key
属性注入到 childBlock
中:
else if (needFragmentWrapper) {
//...
} else {
// Normal element v-for. Directly use the child's codegenNode
// but mark it as a block.
childBlock = (children[0] as PlainElementNode)
.codegenNode as VNodeCall
if (isTemplate && keyProperty) {
injectProp(childBlock, keyProperty, context)
}
}
接下来判断 childBlock
的 isBlock
属性与 isStableFragment
是否相同。如果不同,则需要对 childBlock
进行转换。
-
如果
childBlock
是块(isBlock
为真),则需要将其转换为虚拟节点(VNode),通过调用removeHelper
移除编译上下文中 helpers 对象中的计数 -
如果当前是虚拟节点,则需要将其转换为 Block ,通过调用
removeHelper
移除编译上下文中 helpers 对象中的计数
if (childBlock.isBlock !== !isStableFragment) {
if (childBlock.isBlock) {
// switch from block to vnode
removeHelper(OPEN_BLOCK)
removeHelper(
getVNodeBlockHelper(context.inSSR, childBlock.isComponent)
)
} else {
// switch from vnode to block
removeHelper(
getVNodeHelper(context.inSSR, childBlock.isComponent)
)
}
}
// packages/compiler-core/src/transform.ts
const context: TransformContext = {
removeHelper(name) {
const count = context.helpers.get(name)
if (count) {
const currentCount = count - 1
if (!currentCount) {
context.helpers.delete(name)
} else {
context.helpers.set(name, currentCount)
}
}
}
}
根据 isStableFragment
的值更新 childBlock
的 isBlock
属性:
childBlock.isBlock = !isStableFragment
根据 childBlock
的 isBlock
属性,调用 helper
函数:
if (childBlock.isBlock) {
helper(OPEN_BLOCK)
helper(getVNodeBlockHelper(context.inSSR, childBlock.isComponent))
} else {
helper(getVNodeHelper(context.inSSR, childBlock.isComponent))
}
// packages/compiler-core/src/transform.ts
const context: TransformContext = {
// 更新编译上下文对象中 helper 对象中的计数
helper(name) {
const count = context.helpers.get(name) || 0
context.helpers.set(name, count + 1)
return name
},
}
然后增加 v-for
指令对 v-memo
的支持,首先创建一个函数表达式,用于处理循环逻辑,并将 _cached
作为参数传入。
if (memo) {
const loop = createFunctionExpression(
createForLoopParams(forNode.parseResult, [
createSimpleExpression(`_cached`)
])
)
// ...
}
构建循环函数体,在函数体中,首先得到当前 v-memo
指令的依赖值数组(_memo
),接着检查 _cached
是否存在,并且是否与 _memo
相同。如果相同,则返回 _cached
,避免不必要的计算。
如果不相同,则计算子块(childBlock
)并将其赋值给 _item
,同时将 _memo
赋值给 _item.memo
。
if (memo) {
//...
loop.body = createBlockStatement([
createCompoundExpression([`const _memo = (`, memo.exp!, `)`]),
createCompoundExpression([
`if (_cached`,
...(keyExp ? [` && _cached.key === `, keyExp] : []),
` && ${context.helperString(
IS_MEMO_SAME
)}(_cached, _memo)) return _cached`
]),
createCompoundExpression([`const _item = `, childBlock as any]),
createSimpleExpression(`_item.memo = _memo`),
createSimpleExpression(`return _item`)
])
}
将构建好的循环函数表达式(loop
)和缓存信息(_cache
) push 到 renderExp.arguments
中,以便在渲染时使用。
具体看看 v-for
和 v-memo
一起使用的例子:
如果 v-for
没有和 v-memo
一起使用,则直接创建一个简单的循环函数表达式,处理子块(childBlock
)的渲染:
if (memo) {
// ...
} else {
renderExp.arguments.push(
createFunctionExpression(
createForLoopParams(forNode.parseResult),
childBlock,
true /* force newline */
) as ForIteratorExpression
)
}
综上,v-for
和 v-if
一样,都属于结构化指令,会由 createStructuralDirectiveTransform
函数创建 AST 的转换函数(transformFor
),这个转换函数内部调用了 processFor
函数完成 v-for
指令的语法分析,构造出语义化更强,信息更加丰富的 codegenCode
,最后进入代码生成阶段,将 v-for
指令编译为 renderList
函数。
v-for 总结
通过对源码的分析,可以知道 v-for
最终会被编译为 renderList
函数执行,这点与 Vue2 是一致的。
v-for
可以对数组、字符串、数字和对象进行遍历。当遍历的是数字时,他会把模板重复对应次数。
v-for
里面做了异常处理,当传入了不属于数组、字符串、数字和对象的值时,v-for
渲染的是一个空数组。
与 Vue2 不同的是,v-for
指令内部增加了对 v-memo
指令的支持,v-memo
也是 Vue3 才有的指令。
v-for
和 v-if
一样,都属于结构化指令,会由 createStructuralDirectiveTransform
函数创建 AST 的转换函数(transformFor
),这个转换函数内部调用了 processFor
函数完成 v-for
指令的语法分析,构造出语义化更强,信息更加丰富的 codegenCode
,最后进入代码生成阶段,将 v-for
指令编译为 renderList
函数。
总结
v-if
和 v-for
都是 Vue 提供的指令。
v-if
指令最终会被编译为三元表达式,v-for
指令最终被编译为内部的 renderList
函数执行。
Vue 提供的指令是在模板中使用的,Vue 模板经过解析,生成 AST 树。Vue3 与 Vue2 不同的是,在这个过程中,Vue3 会构建 Block
树,Block
是一种特殊的虚拟节点(vnode
),它和普通虚拟节点(vnode
)相比,多出一个额外的 dynamicChildren
属性,用来存储动态节点。Block
的出现优化了 Diff 的过程。在 Vue2 的 Diff 过程中,即使虚拟节点(vnode
)没有变化,也会进行一次比较,而 Block
的出现减少了这种不必要的比较,由于 Block
中的动态节点都会被收集到 dynamicChildren
中,所以 Block
间的 patch
可以直接比较 dynamicChildren
中的节点,减少了非动态节点之间的比较,减少了 Diff 的工作量。
v-if
和 v=for
都属于结构化指令,统一由 createStructuralDirectiveTransform
生成 tranform 函数。
对于 v-if
来说,会由 processIf
函数完成 v-if
指令的语法分析,构造出语义化更强,信息更加丰富的 codegenCode
,最后进入代码生成阶段,将 v-if
指令编译为三元表达式。
对于 v-for
来说,会由 processFor
函数完成 v-for
指令的语法分析,构造出语义化更强,信息更加丰富的 codegenCode
,最后进入代码生成阶段,将 v-for
指令编译为 renderList
函数。