v-if 与 v-for 背后的奥秘(二)

106 阅读16分钟

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 官方的模板编译工具

101.png

可以看到跟 Vue2 一样,Vue3 中的 v-if 指令也会被编译为三元表达式。

openBlock 函数,用于开启 Block 的收集。在 openBlock 函数中,如果 disableTrackingtrue ,会将 currentBlock 设置为 null;否则创建一个新的数组并赋值给 currentBlock,并 pushblockStack 中。

// 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-ifv-else-ifv-elsev-for 这些都属于结构化指令,因为它们能影响代码的组织结构。该函数接受两个参数 namefn

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 函数中使用的 IfConditionalExpressionCacheExpression 类型其实是 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 缓存时,生成的中间代码类型会被定义为 CacheExpressionCacheExpression 的节点类型是 NodeTypes.JS_CACHE_EXPRESSION

在 Vue 的 3.0.0 版本中,如果 v-if/v-else-if/v-elsev-once 同时使用,则会导致模板编译报错,如下面的例子:

102.png

此例子源自 issue:github.com/vuejs/core/…
解决此 issue 的 pr :github.com/vuejs/core/…

报错如下:

103.png

getParentCondition 函数就解决了这个问题。

不过要注意的是,如果只是 v-if 与 v-once 一起使用,v-if 没有其他兄弟条件分支或,是不会产生编译报错的:

104.png

v-if 与 v-once 一起使用,v-if 的兄弟条件分支不管有没有 v-once ,都会报错

111.png

笔者特意下载了 Vue 3.0.0 版本的代码,查看此处的实现,发现在 3.2.45 版本的代码实现中,多了 else if (node.type === NodeTypes.JS_CACHE_EXPRESSION) 的代码分支:

105.png

借助 VSCode 提供的调试工具,可以发现 parentCondition 中没有 alternate 属性

106.png

因此会报 Cannot read property 'type' of undefined 的错误

107.png

有读者可能会疑问,笔者是如何调试的,具体操作是这样的:

首先,将此 issue:github.com/vuejs/core/… 中提供的例子 clone 下来

然后在该例子的项目根目录下运行 npm run serve 的命令,发现,定位到是 compiler-core.cjs.js 文件的 2702 行报错:

108.png

在 node_modules 下找到 compiler-core.cjs.js 文件,并在 2700 行打上断点:

109.png

借助 VSCode 的调试工具,调试 npm run serve 命令,程序的执行会自动在断点处停下,这样就可以分析代码的执行过程了

110.png

归根结底,此 issue:github.com/vuejs/core/… 产生的原因是 v-if 与 v-once 在一起使用的,在生成中间代码的过程中,其节点类型是 NodeTypes.JS_CACHE_EXPRESSION ,该类型与 NodeTypes.JS_CONDITIONAL_EXPRESSION 不同,没有 alternate 属性,因此根据不同的类型做一下兼容,就可以解决此 issue :

112.png

在 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 属性:

113.png

链接为:github.com/vuejs/core/…

看完 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-ifv-else-ifv-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 官方的模板编译工具 114.png

115.png

116.png

117.png

接下来看 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 */))
  ]))
}

openBlockcreateElementBlock 在讲述 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-forv-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-forv-slotv-prev-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 中相应的计数就会达到多少:

118.png

然后创建 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 中解构,得到 sourcevaluekeyindex ,这四个值在 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>

通过断点调试,可以观察到 parseResultsourcevaluekeyindex 的值:

119.png

v-for 指令作用域管理,增加 v-for 的作用域计数,在非浏览器环境中,如果启用了标识符前缀,则将 v-for 指令表达式中解析出的变量(valuekeyindex)添加到编译上下文对象的标识符(identifiers)中。

标识符前缀是 Vue.js 中 AST 节点转换的一个选项配置:

120.png

如果 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 中的各个参数,如 sourcevaluekeyindex 等。其中,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 表达式中的左右两个参数,即 LHSRHS ,然后根据这两个参数构造出一个 ForParseResult 对象,其中 source 属性表示数据源,使用 createAliasExpression 函数创建一个别名表达式,valuekeyindex 属性则分别表示当前遍历的值、键和索引,初始值都为 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]*),这是第二个捕获组,字符或非空白字符,这表示循环中的数组或对象

借助正则表达式可视化工具可视化上面的正则表达式,则更加容易理解上面正则表达式的作用:

121.png

对于下面的模板:

<li v-for="(oVal, oKey, oIdx) in myObject">
  {{ oIdx }}. {{ oKey }}: {{ oVal }}
</li>

得到的 LHS(oVal, oKey, oIdx)RHSmyObject

122.png

然后使用正则表达式 stripParensRE 去掉左边表达式(LHS)左右两边的括号:

let valueContent = LHS.trim().replace(stripParensRE, '').trim()

此时得到 valueContentoVal, oKey, oIdx

// packages/compiler-core/src/transforms/vFor.ts

const stripParensRE = /^\(|\)$/g

stripParensRE 用于匹配字符串开头的左括号'(' 或者字符串末尾的右括号 ')'。使用正则可视化工具可视化后的结果:

95.png

^:表示匹配输入的开始位置,在这里表示左括号('(')开头的字符串

(|):表示匹配左括号或者右括号

|:是一个逻辑或操作符,表示在匹配时可以选择两个条件中的任意一个

$:表示匹配输入的结束位置,在这里表示以括号结尾的字符串

g:表示进行全局匹配,即匹配所有符合条件的子字符串

然后使用 indexOf 方法得到 valueContent 在左侧表达式(LHS)中的偏移量 trimmedOffset ,后续可用该偏移量计算得到 keyContentindexContent

const trimmedOffset = LHS.indexOf(valueContent)

然后,使用正则表达式 forIteratorRE 匹配 valueContent 中的迭代器别名:

const iteratorMatch = valueContent.match(forIteratorRE)

匹配得到 iteratorMatch 为:

123.png

forIteratorRE 用于匹配一个逗号分隔的字符串,该字符串由逗号开头,并且后续由逗号和非逗号、非右大括号、非右中括号的字符组成。

const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/

借助正则表达式可视化工具可视化上面的正则表达式:

91.png

match 方法返回的数组中,在索引 0 处,为该正则完整的匹配项;在索引 1 处,第一个捕获括号匹配的内容,在索引 2 处,第二个捕获括号匹配的内容,以此类推。

forIteratorRE 正则中有两个捕获组和一个非捕获组:

93.png

捕获组匹配到的结果会作为 match 方法的返回数组中的元素,在索引 1 处为第一个捕获括号匹配的内容。在索引 2 处为第二个捕获括号匹配的内容,以此类推。

94.png

非捕获组匹配到的结果不会出现在 match 方法返回的数组中。

如果 forIteratorRE 匹配成功,即 iteratorMatch 有值,则将 valueContent 中的迭代器别名替换为空字符串,并提取出 keyContentindexContent。接着,根据 keyContentindexContent 计算出它们在表达式(exp)中的偏移量,并使用 createAliasExpression 函数创建对应的别名表达式节点,并将其存储到 result 对象的 keyindex 属性中 。

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 对象为:

124.png

分析完 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 函数生成 forNodecodegenNode :

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> 容器上:

125.png

👆 源自:cn.vuejs.org/guide/essen…

// 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 标签下有多个元素的场景:

126.png

v-for 循环渲染 template 标签,template 标签下是纯文本的场景:

127.png

对于循环渲染普通元素的场景,直接获取第一个子节点的 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)
  }
}

接下来判断 childBlockisBlock 属性与 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 的值更新 childBlockisBlock 属性:

childBlock.isBlock = !isStableFragment

根据 childBlockisBlock 属性,调用 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-forv-memo 一起使用的例子:

128.png

如果 v-for 没有和 v-memo 一起使用,则直接创建一个简单的循环函数表达式,处理子块(childBlock)的渲染:

if (memo) {
  // ...
} else {
  renderExp.arguments.push(
    createFunctionExpression(
      createForLoopParams(forNode.parseResult),
      childBlock,
      true /* force newline */
    ) as ForIteratorExpression
  )
}

综上,v-forv-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-forv-if 一样,都属于结构化指令,会由 createStructuralDirectiveTransform 函数创建 AST 的转换函数(transformFor),这个转换函数内部调用了 processFor 函数完成 v-for 指令的语法分析,构造出语义化更强,信息更加丰富的 codegenCode ,最后进入代码生成阶段,将 v-for 指令编译为 renderList 函数。

总结

v-ifv-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-ifv=for 都属于结构化指令,统一由 createStructuralDirectiveTransform 生成 tranform 函数。

对于 v-if 来说,会由 processIf 函数完成 v-if 指令的语法分析,构造出语义化更强,信息更加丰富的 codegenCode ,最后进入代码生成阶段,将 v-if 指令编译为三元表达式。

对于 v-for 来说,会由 processFor 函数完成 v-for 指令的语法分析,构造出语义化更强,信息更加丰富的 codegenCode ,最后进入代码生成阶段,将 v-for 指令编译为 renderList 函数。