vue模版解析之v-if指令分析

222 阅读3分钟

vue3 模版编译在上一篇分析,知道vue模版编译经过以下几个阶段

下面结合vue 中的v-if模版语法来深入解析一下整个编译过程;

来一个v-if代码转换例子

以下例子可以看到vue的条件渲染语法,最终生成了一个三目表达式

// vue模版
<div id="app">
  <h1 v-if="title">我是A</h1>
  <p v-else>我是B</p>
</div>
// 最终转换code
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    (_ctx.title)
    ? (_openBlock(), _createElementBlock("h1", _hoisted_2, "我是A"))
    : (_openBlock(), _createElementBlock("p", _hoisted_3, "我是B"))
  ]))
}

generate--生成code

generate的时候,根据AST 节点类型,判断是节点类型是js条件表达式(JS_CONDITIONAL_EXPRESSION),调用genConditionalExpression来生成条件表达式语句,

function genConditionalExpression(
  node: ConditionalExpression,
  context: CodegenContext,
) {
  // 从节点上拿到关键consequent, alternate属性
  const { test, consequent, alternate, newline: needNewline } = node
  const { push, indent, newline } = context
  // 判断如果是简单表达式, 调用genExpression生生成简单表达式
  if (test.type === NodeTypes.SIMPLE_EXPRESSION) {
    const needsParens = !isSimpleIdentifier(test.content)
    needsParens && push(`(`)
    genExpression(test, context)
    needsParens && push(`)`)
  // 否则处理节点
  } else {
    push(`(`)
    genNode(test, context)
    push(`)`)
  }
  needNewline && indent()
  needNewline || push(` `)
  push(`? `)
  // 条件分支A---解析consequent属性, 调用genNode生成(_openBlock(), _createElementBlock("h1", _hoisted_2, "我是A"))
  genNode(consequent, context)
  needNewline && newline()
  needNewline || push(` `)
  push(`: `)
  // 条件分支B---解析alternate属性,调用genNode生成(_openBlock(), _createElementBlock("p", _hoisted_3, "我是B"))
  genNode(alternate, context)
}

可以看到除了基础语法的生成,主要解析了AST节点上的consequent, alternate属性;

这个时候可以打印看到AST中的该节点的信息

transform--转换v-if语法

基于baseParse阶段生成的AST,并且在扩展了AST节点属性和指令节点的转换

transform(
    ast,
    extend({}, resolvedOptions, {
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []), // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {}, // user transforms
      ),
    }),
  )

主要转换的内容如下

   // 语法包括
    transformOnce,
    transformIf,
    transformMemo,
    transformFor,
    transformFilter,
    trackVForSlotScopes,
    transformExpression,
    transformSlotOutlet,
    transformElement,
    trackSlotScopes,
    transformText,

    // 指令包括
    {
      on: transformOn,
      bind: transformBind,
      model: transformModel,
    },
  • 判断语法中有if,会将之前生产的JS AST,创建branch节点属性,组装新的ifNode,替换原有的节点;

  • 如果是v-else语法,移除该节点,生成branch节点属性,赋值到兄弟节点的branchs属性上;

  • 处理v-if语法时,会识别到if|else|else-if,会替换v-if的节点,并且会生成对应的node中的codegenNode属性,并且在codegenNode属性下追加了alternate属性;
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
        }
      }
      return () => {
        if (isRoot) {
          // 新增codegenNode节点
          ifNode.codegenNode = createCodegenNodeForBranch(
            branch,
            key,
            context,
          ) as IfConditionalExpression
        } else {
          // 获取到父节点
          const parentCondition = getParentCondition(ifNode.codegenNode!)
          // 追加alternate属性
          parentCondition.alternate = createCodegenNodeForBranch(
            branch,
            key + ifNode.branches.length - 1,
            context,
          )
        }
      }
    })
  },
)
调用createCodegenNodeForBranch以后,生成的Node数据结构如下,含有consequent,alternate属性
{
    type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
    test,
    consequent,
    alternate,
    newline,
    loc: locStub,
  }

经过以上处理, 此时的AST 节点上完善了codegenNode, branches等节点属性

可以从接口中看到,在tranform阶段,tranformIf阶段,对baseParse生成的AST节点,做了以下属性的扩展

export interface ConditionalExpression extends Node {
  type: NodeTypes.JS_CONDITIONAL_EXPRESSION
  test: JSChildNode
  consequent: JSChildNode
  alternate: JSChildNode
  newline: boolean
}

baseParse生成AST

parse阶段,生成的AST, 此时的AST, 对比babel AST 仅仅扩展了components, dectives等相关属性, 还没有codegenNode, branches等节点属性

{
    "type": 0,
    "source": "<script setup>\nimport { ref } from 'vue'\nconst title = ref("aa")\n</script>\n\n<template>\n    <h1 v-if="title">我是A</h1>\n    <p v-else>我是B</p>\n  \n</template>\n",
    "children": [
        {
            "type": 1,
            "tag": "h1",
            "ns": 0,
            "tagType": 0,
            "props": [],
            "children": [
                {
                    "type": 2,
                    "content": "我是A",
                    "loc": {
                        "start": {
                            "column": 22,
                            "line": 7,
                            "offset": 108
                        },
                        "end": {
                            "column": 25,
                            "line": 7,
                            "offset": 111
                        },
                        "source": "我是A"
                    }
                }
            ],
            "loc": {
                "start": {
                    "column": 5,
                    "line": 7,
                    "offset": 91
                },
                "end": {
                    "column": 30,
                    "line": 7,
                    "offset": 116
                },
                "source": "<h1 v-if="title">我是A</h1>"
            }
        },
        {
            "type": 1,
            "tag": "p",
            "ns": 0,
            "tagType": 0,
            "props": [],
            "children": [
                {
                    "type": 2,
                    "content": "我是B",
                    "loc": {
                        "start": {
                            "column": 15,
                            "line": 8,
                            "offset": 131
                        },
                        "end": {
                            "column": 18,
                            "line": 8,
                            "offset": 134
                        },
                        "source": "我是B"
                    }
                }
            ],
            "loc": {
                "start": {
                    "column": 5,
                    "line": 8,
                    "offset": 121
                },
                "end": {
                    "column": 22,
                    "line": 8,
                    "offset": 138
                },
                "source": "<p v-else>我是B</p>"
            }
        }
    ],
    "helpers": {},
    "components": [],
    "directives": [],
    "hoists": [],
    "imports": [],
    "cached": 0,
    "temps": 0,
    "loc": {
        "start": {
            "line": 1,
            "column": 1,
            "offset": 0
        },
        "end": {
            "line": 1,
            "column": 1,
            "offset": 0
        },
        "source": ""
    }
}