浅曦Vue源码-30-挂载阶段-$mount-(19)genIf 和 v-if

206 阅读5分钟

「这是我参与2022首次更文挑战的第31天,活动详情查看:2022首次更文挑战」。

一、前情回顾 & 背景

本篇小作文讨论了 Vue 在编译时处理另一个常用指令 —— v-for 的渲染函数生成的方法 genFor,它的主要工作如下:

  1. 首先明确 genForgenElement 的一个分支流程,用于处理有 v-for 指令的 ast 节点;
  2. 标记 el.processedtrue,防止 genFor 递归 genElement 时进入死循环;
  3. 根据前面 parse 阶段所得到的 el.for/el.alias/el.iterator1/el.iterator2 信息拼接得到 v-for 的渲染函数代码结构,期间处理生成元素的具体逻辑还是有 genElement 方法实现的;

本篇小作文将聚焦于另一个 Vue 常用的指令 —— v-if 的处理方法 genFor,从源头上了解一下 v-if 是如何实现条件渲染的;其调用过程为 generate -> genElement -> genIf

1.1 genElement 中的 genIf 调用:

export function genElement (....): string {
  if (...) {
  } else if (el.if && !el.ifProcessed) {
    // 处理带有 v-if 指令的节点,得到一个三元表达式:condition ? render1 : render2
    return genIf(el, state)
  } else if (....) {
  } else {
    return code
  }
}

二、genIf

方法位置:src/compiler/codegen/index.js -> function genIf

方法参数:

  1. elast 节点对象;
  2. stateCodegenState 实例对象;
  3. altGen/altEmpty 暂时忽略;

方法作用:

  1. 标记 el.ifProcessed 属性为 true 防止后面递归调用 genElement 时进入死循序;
  2. 调用 genIfConditions 处理 el.ifConditions,生成三元表达式
export function genIf (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  // 标记当前节点的 v-if 指令已经被处理过了,避免死循环
  el.ifProcessed = true // avoid recursion
  // 得到三元表达式,condition ? render1 : render2
  // 思考?这里为啥传入的是 el.ifConditions.slice() 这个复制品呢?
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

2.1 el.ifConditions

el.ifConditions 是一个数组,数组项来自两处:

  1. 阶段的解析 html 模板过程中,当 parseHTML 解析到元素的 开始标签时,会调用 processIf 方法,processIf 会处理 v-if,大致代码如下:
parseHTML(template, {
  //.... 
  start (tag, attrs, unary, start, end) {
    if (...) {
    } else if (!element.processed) {
      // 给 v-if 的表达式添加到 el.ifConditons
      // 初始化 el.elseif / el.else 属性
      processIf(element)
    }

    if (!unary) {
      closeElement(element)
    }
  },
  end (...) {
    closeEement(element)
})

// processIf 代码
function processIf (el) {
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    // 初始化 el.if 然后条件语句即对应渲染的 ast 放到 el.ifConditions
    el.if = exp
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    if (getAndRemoveAttr(el, 'v-else') != null) {
     // 初始化 el.else
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      // 初始化 el.elseif
      el.elseif = elseif
    }
  }
}
  1. parseHTML 解析到自闭和标签的闭合 /> 或 非自闭和标签的闭合标签 如 </div> 就会调用 closeElement 方法,closeElement 会处理 v-else-ifv-else 大致代码如下:
function closeElement (element) {

  if (!stack.length && element !== root) {
   // root v-if 暂时忽略
  }
  if (currentParent && !element.forbidden) {
    if (element.elseif || element.else) {
      // 处理 v-else-if / v-else
      processIfConditions(element, currentParent)
    } else {
    }
  }
}

// processIfConditions 方法处理 el.elseif / el.else 
function processIfConditions (el, parent) {
  const prev = findPrevElement(parent.children)
  if (prev && prev.if) {
    addIfCondition(prev, {
       // 这里处理 el.else 时,exp 仍然取自 el.elseif ,
       // 所以是个 undefined,而 v-else 本身也没有值,所以 undefined 是符合预期的
      exp: el.elseif,
      block: el
    })
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +
      `used on element <${el.tag}> without corresponding v-if.`,
      el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']
    )
  }
}

如此一来,el.ifConditons 中包含了带有 v-if、v-else-if、v-else 各个条件下对应的渲染细节;el.ifConditions 中每一项都是一个包含 expblock 的对象:{ exp, block },所谓 exp 就是条件表达式,block 就是满足 exp 条件时要渲染的 ast 节点;

  • 以下面新增的三行条件渲染代码为例:
<div id="app">
  <div class="staticR">
    <article>hahahah</article>
  </div>
  <input :type="inputType" v-model="inputValue" />
  <span v-for="item in someArr" :key="index">{{item}}</span>
+  <div v-if="inputType === 'checkbox'">inputType=checkBox</div>
+  <div v-else-if="inputType === 'radio'">inputType=radio</div>
+  <div v-else>inputType=something-else</div>
  {{ msg }}
  <some-com :some-key="forProp"></some-com>
  <div>someComputed = {{someComputed}}</div>
  <div class="static-div">静态节点</div>
</div>

下图即为处理后的 el.ifConditions 的细节部分,

image.png

2.2 genIfConditions

方法位置:src/compiler/codegen/index.js -> function genIfConditions

方法参数:

  1. condtionsel.conditions 的复制对象
  2. stateCodegenState 实例对象
  3. altGen/altEmpty 暂时忽略

方法作用:genIfConditions 的核心作用就是将条件变成三元表达式,形如:

if 条件 ? block1 : block2

这个就是 v-ifv-else,那么有 v-else-if 怎么办?接着套娃就行,形如:

if 条件 : block1 : 满足 else-if 条件 ? block2 : block3

具体工作如下:

  1. 处理 conditions 为空时,返回 _e()` 这个渲染函数的帮助函数;
  2. conditions 中拿出一个 condition,判断是否有 exp 条件,如果有说明是 el.if 或者 el.elseif,这个时候构造三元表达式并返回,此时构造三元表达式的否则部分是要递归调用 genIfConditions 的,只不过此时的 conditions 已经是被拿出来一个了,如此循环下去直到 conditions 被清空,就完成了所有的条件渲染;
  3. 如果没有 exp 说明是 v-else,直接返回三元表达式;
function genIfConditions (
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  // 长度为空,则用 _e() 生成一个空节点
  if (!conditions.length) {
    return altEmpty || '_e()'
  }

  // 从 conditions 数组中拿出一个条件对象 { exp, block }
  // 从这里就可以理解为啥上面 genIf 调用 genIfConditions 是传入的是 el.ifConditions.slice() 这个复制品
  // 这是因为不能直接操作 el.ifConditions 这个编译结果,所以就要复制一份出来操作
  const condition = conditions.shift()

  // 返回值:condition.exp ? 渲染函数1 : 渲染函数2
  // 表示条件成立时 执行 渲染函数 1,否则就看看剩下的条件中是否有满足条件的,
  // 即递归调用 genIfConditions 直接找到条件成立的元素返回一个三元表达式
  if (condition.exp) {
    return `(${condition.exp})?${
      genTernaryExp(condition.block)
    }:${
      genIfConditions(conditions, state, altGen, altEmpty)
    }`
  } else {
    // 说明没有 condition.exp 就是 v-else 了 
    return `${genTernaryExp(condition.block)}`
  }

  // 返回渲染函数表达式,核心还是递归调用 genElement
  function genTernaryExp (el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}

2.3 genIfConditions 返回结果

经过上面的一通操作后最终得到的返回值如下,其整个是个字符串,其中的注释是我加上去的,原本没有这些注释:

"(inputType === 'checkbox') // v-if 条件
  ?  _c('div', [_v("inputType=checkBox")])
  : (inputType === 'radio') // v-else-if 条件
    ? _c('div', [_v("inputType=radio")])
    : _c('div', [_v("inputType=something-else")])" // v-else

三、总结

本篇小作文的笔墨放在了 v-if/v-else-if/v-else 这几个条件渲染指令了,看似神秘的条件渲染最终竟然是三元表达式,我说这话多少有点吹牛逼漏风😂😂。条件渲染的实现核心流程如下:

  1. 生成 astparse 阶段 parseHTML 的过程中会将 v-if、v-else-if、v-else 解析成 el.if/el.elseif/el.else,同时会条件和条件成立时渲染的元素组成对象: { exp, block } pushel.ifConditions 中;
  2. 接着就是利用 ast 生成 render 函数generate 阶段,调用 genElement,当判断 el.if 存在时调用 genIf 处理条件渲染并标记 el.ifProcessedtrue 防止重复处理;