Skip to content

Latest commit

 

History

History
277 lines (232 loc) · 8.45 KB

v-if.md

File metadata and controls

277 lines (232 loc) · 8.45 KB

本篇文章,我们要讲解的是v-if指令的解析过程,同样我们还是从一个例子入手:

<div id="app">
  <p v-if="value == 1">v-if块的内容</p>
  <p v-else-if="value == 2">v-else-if块的内容</p>
  <p v-else>v-else块的内容</p>
</div>
<script type="text/javascript">
  var vm = new Vue({
	el: '#app',
	data: {
	  value: 2
	}
  })
</script>

相信大家一眼就猜到页面中显示的是v-else-if块的内容。同时打开页面审查元素,会发现其它两块内容都没有渲染到页面上。

我们还是简单的带着大家过一下解析的过程。

生成ast

与其它指令类似,parse函数中,调用了一个单独的函数来处理v-if指令——processIf:

function processIf (el) {
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    el.if = exp
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}

我们这里有三个p标签,所以会分别生成astendchars的处理就略过了,我们只看start中的处理~

1、 v-if

第一个p标签,在执行processIf函数时,exp = getAndRemoveAttr(el, 'v-if')结果返回value == 1,所以会走到if块。

function addIfCondition (el, condition) {
  if (!el.ifConditions) {
    el.ifConditions = []
  }
  el.ifConditions.push(condition)
}

addIfCondition会给el添加一个ifConditions来保存当前v-if相关的元素。

2、 v-else-if

第二个p标签,同样会在processIf函数中进行处理,这次会走到else,并使得el.elseif = "value == 2"

接着往下执行,会走到如下判断条件:

if (currentParent && !element.forbidden) {
  if (element.elseif || element.else) {
    processIfConditions(element, currentParent)
  } else if (element.slotScope) { // scoped slot
    ...
  } else {
    currentParent.children.push(element)
    element.parent = currentParent
  }
}

如果当前标签是elseifelse,如果我们自己实现,首先想到的是从当前元素往前,找到第一个有v-if的标签。Vue中其实也是这样:

function processIfConditions (el, parent) {
  const prev = findPrevElement(parent.children)
  if (prev && prev.if) {
    addIfCondition(prev, {
      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.`
    )
  }
}
function findPrevElement (children: Array<any>): ASTElement | void {
  let i = children.length
  while (i--) {
    if (children[i].type === 1) {
      return children[i]
    } else {
      if (process.env.NODE_ENV !== 'production' && children[i].text !== ' ') {
        warn(
          `text "${children[i].text.trim()}" between v-if and v-else(-if) ` +
          `will be ignored.`
        )
      }
      children.pop()
    }
  }
}

findPrevElement会先拿到当前元素前面的兄弟结点,然后从后往前寻找第一个标签元素。夹在当前元素和v-if元素之间的文本结点会被删除,并在开发环境给予提示。

如果prev元素存在且prev.if存在,则把当前元素和条件添加到previfConditions数组中。

从上面的代码中我们还看到,如果element.elseif || element.else返回true,是不会走到最后一个else块,也就是说不会建立当前元素和currentParent元素的父子关系,我们的例子中,divchildren中只会有v-if的标签。

3、 v-else

v-else的处理和v-else-if基本一致,区别就是最后添加到v-if元素ifConditions数组中的对象的exp值为undefined(此时el.elseifundefined)。

所以最终生成的ast主要结构如下(详细的ast太长,大家有兴趣的可以自己打印一下):

{
  type: 1,
  tag: 'div',
  plain: false,
  children: [{
    type: 1,
    tag: 'p',
    children: [{
      text: 'v-if块的内容',
      type: 3
    }]
    if: 'value == 1',
    ifConditions: [{
      exp: "value == 1",
      block: {
	    type: 1,
	    tag: 'p',
	    children: [{
	      text: 'v-if块的内容',
	      type: 3
	    }],
	    if: 'value == 1',
	    ifConditions: [],
	    plain: true
	  }
    }, {
      exp: "value == 2",
      block: {
	    type: 1,
	    tag: 'p',
	    children: [{
	      text: 'v-else-if块的内容',
	      type: 3
	    }],
	    elseif: 'value == 2',
	    plain: true
	  }
    }, {
      exp: undefined,
      block: {
	    type: 1,
	    tag: 'p',
	    children: [{
	      text: 'v-else块的内容',
	      type: 3
	    }],
	    else: true,
	    plain: true
	  }
    }]
  }]
}

生成render

拿到ast,经过静态内容处理,我们就到了把ast转换为render函数字符串的步骤。

src/compiler/codegen/index.js文件中对v-if的处理是在genIf函数中:

function genIf (el: any): string {
  el.ifProcessed = true // avoid recursion
  return genIfConditions(el.ifConditions.slice())
}

function genIfConditions (conditions: ASTIfConditions): string {
  if (!conditions.length) {
    return '_e()'
  }

  const condition = conditions.shift()
  if (condition.exp) {
    return `(${condition.exp})?${genTernaryExp(condition.block)}:${genIfConditions(conditions)}`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  function genTernaryExp (el) {
    return el.once ? genOnce(el) : genElement(el)
  }
}

genIfConditions会循环处理ifConditions里面的每一个元素,直到找到exp返回true的元素。

如果conditions.length为0,则返回_e(),该方法对应的是createEmptyVNode

否则取出conditions中第一个元素,如果condition.exp不为空,则进入if块,此时返回的是一个三目运算符,如果表达式为真,则返回genTernaryExp(condition.block)的返回值,否则再次调用genIfConditions(conditions)。如果condition.exp为空,则直接返回genTernaryExp(condition.block)

genTernaryExp会判断el.once即当前元素上是否有v-once指令,如果有,则返回getOnce(el),否则返回genElement(el)

最终生成的render函数字符串如下:

with(this){return _c('div',{attrs:{"id":"app"}},[(value == 1)?_c('p',[_v("v-if块的内容")]):(value == 2)?_c('p',[_v("v-else-if块的内容")]):_c('p',[_v("v-else块的内容")])])}

直接看子元素数组部分,如果value == 1返回真,则创建第一个有v-if指令的p标签及其子内容,否则如果value == 2返回真,则创建第二个有v-else-if指令的p标签及其子内容,否则创建最后有v-else指令的p标签及其子内容。

v-for类似,v-if也只是控制了要绘制哪些元素,相关数据不会带到VNode对象中。

补充

Vue中,我们知道一个组件只能有一个根元素,但是如果根元素上有v-if指令,则可以有多个同级元素。

<div id="app"></div>
<script type="text/javascript">
  new Vue({
    template: '<p v-if="value == 1">v-if块的内容</p>\
	<p v-else-if="value == 2">v-else-if块的内容</p>\
	<p v-else>v-else块的内容</p>',
    data: {
      value: 3
    }
  }).$mount('#app');
</script>

比如如上例子,是可以正常在页面中渲染的。在parse中,有如下代码:

  if (!root) {
    root = element
    checkRootConstraints(root)
  } else if (!stack.length) {
    // allow root elements with v-if, v-else-if and v-else
    if (root.if && (element.elseif || element.else)) {
      checkRootConstraints(element)
      addIfCondition(root, {
        exp: element.elseif,
        block: element
      })
    } else if (process.env.NODE_ENV !== 'production') {
      warnOnce(
        `Component template should contain exactly one root element. ` +
        `If you are using v-if on multiple elements, ` +
        `use v-else-if to chain them instead.`
      )
    }
  }

第一个p标签会当成根元素,所以解析第二个p标签时,会走到else if,之后的操作和我们上面的讲的大致相同。其实从上面内容我们也可以知道,对于v-if的解析,最终生成的ast中只有v-if所在的标签,其它关联内容都在ifCondition内。