玩命加载中 . . .

Vue源码阅读之属性、条件渲染与列表渲染


条件渲染

之前的代码解读中,为了简化代码,同时更加抓住主要内容——VNode,我们忽略了属性、条件渲染(v-if)、列表渲染(v-for)等。对属性的处理,其实在之前的笔记中有所提及;下面首先简单记录一下条件渲染。

带着条件渲染看Vue渲染全过程

<div>
  <div>Item 0</div>
  <div v-if="a > 0">Item 1</div>
  <div v-else-if="b > 0">Item 2</div>
  <div v-else>Item 3</div>
  <div>Item 4</div>
</div>

https://github.com/raphealguo/how-to-learn-vue2-blob/raw/master/figure/2.2.1/ifelse.png

从parse(template)看带条件渲染的AST树的生成

在AST树中, if 、else-if 、else的多个token节点会合成一个节点——if节点里边包含

[{
	exp:'if判断条件', 
	block:<if的ast节点>
}, 
{
	exp:'else-if判断条件', 
	block:<else-if的ast节点>,
}{
	exp: null,
	block:<else的ast节点>
}]

而在parse(template)函数中,有一个processIf(element)函数:

processIf(element),它用于处理element上的v-if指令(包含在属性键值对中,有待分离);该函数体内部的getAndRemoveAttr()用于获取并移除指定的属性键值对。

function processIf (el) {
  /**exp接收移除的v-if后面的值 */
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    el.if = exp
    // 为当前节点添加if条件
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    // 表示exp为空,处理v-else
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    // 继续处理v-else-if
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}

注意到,上述处理v-if条件渲染的代码中,还有一个addIfCondition()函数,它主要用于给element(即要生成的AST节点)添加ifConditions属性:

ifAstElm.ifConditions = [
  {exp: 'a > 0', block: item1AstEl },
  {exp: 'b > 0', block: item2AstEl },
  {exp: null,    block: item3AstEl },
]

这也再次印证,AST树中将多个v-if、v-else、v-else-if节点整合在了一起。

生成VNode render时对ifASTElement的处理

ifAstElm中的条件控制语句会变成连续的三元运算操作,运算结果产生一个VNode——v-if、v-else-if、v-else结构只会产生一个VNode。

我们知道,生成AST结构的下一步,是生成VNode-render(渲染VNode树的代码),因此我们把目光转移到compiler/codegen/index.js中,在生成VNode-render的次外层函数genElement(el)中,就有对ifAstElm的处理——genIf(el):

function genIf(el) {
  el.ifProcessed = true // 标记已经处理过当前这个if节点了,避免递归死循环
    // 将el.ifConditions的浅拷贝传递给函数
  return genIfConditions(el.ifConditions.slice())
}

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

  const condition = conditions.shift() // 因为我们并没有去真正删除 el.ifConditions 队列的元素,所以需要有el.ifProcessed = true来结束递归
  if (condition.exp) {
    return `(${condition.exp})?${genTernaryExp(condition.block)}:${genIfConditions(conditions)}`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

  function genTernaryExp(el) {
    return genElement(el)
  }
}

代码段中,我们看到生成VNode-render的三元表达式:

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

第一个分支,condition.exp是分支条件,condition.block是分支对应的AST节点(准确来说,condition对象来自前一步生成的AST树节点),采取递归方式,可以得到一个嵌套的三元表达式结构:

_c('div', {}, [ /* 留意children数组只有3个元素 */
  _c('div', {}, [ _v("Item 0") ]),

  /* 下边这个表达式只会产生一个 VNode 节点 */
  (a > 0) ?  // if
    _c('div', {}, [ _v("Item 1") ]) :
  (b > 0) ?  // elseif
    _c('div', {}, [ _v("Item 2") ]) :
         // else
    _c('div', {}, [ _v("Item 3") ]),
  /* 上边这个表达式只会产生一个 VNode 节点 */

  _c('div', {}, [ _v("Item 4") ])
])

第二个分支,表示当前condition.exp为null,即无条件了,对应v-else,为v-if系列中的最后一个分支。

后续步骤

上面已经得到了带有v-if的VNode-render表达式,后续就需要保证其运行正确,即可生成VNode树了。由VNode树到真实DOM树的过程与v-if无关了。

// 得到的VNode render代码示例:(renderCode)
var render = function () {
    with (this) {
        _c('div', {}, [
          _c('div', {}, [ _v("Item 0") ]),

          (a > 0) ?  // if
            _c('div', {}, [ _v("Item 1") ]) :
          (b > 0) ?  // elseif
            _c('div', {}, [ _v("Item 2") ]) :
                 // else
            _c('div', {}, [ _v("Item 3") ]),

          _c('div', {}, [ _v("Item 4") ])
        ])
    }
}

列表渲染

流程总览

以如下代码为例,简单看看整体流程示意图:

<ul>
  <li v-for="(item, index) in items">{{ item }</li>
</ul>

https://github.com/raphealguo/how-to-learn-vue2-blob/raw/master/figure/2.2.2/for.png

HTML到AST的列表渲染处理

在生成AST节点时,需要提取出HTML模板中的v-for属性值。

function processFor (el) {
  let exp
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    // 此时,exp已经保存到了v-for的值,下面需要提取出指定的内容
    const inMatch = exp.match(forAliasRE)
    // v-for="item in list"             =>     ["item in list", "item", "list"]
    // v-for="(item, index) in list"    =>     ["(item, index) in list", "(item, index)", "list"]
    // v-for="(value, key, index) in object"    =>     ["(value, key, index) in object", "(value, key, index)", "object"]

    if (!inMatch) { // v-for语法有错误的时候,提示编译错误
      warn(
        `Invalid v-for expression: ${exp}`
      )
      return
    }
    // 注意,string.trim()将会从字符串两端删除空白字符
    /**el.for:string,保存的是v-for命令值中,in后面的内容(可迭代对象) */
    el.for = inMatch[2].trim()
    const alias = inMatch[1].trim()
    const iteratorMatch = alias.match(forIteratorRE)
    if (iteratorMatch) { // v-for="(item, index) in list"  或者 // v-for="(value, key, index) in object"
      el.alias = iteratorMatch[1].trim()
      el.iterator1 = iteratorMatch[2].trim()
      if (iteratorMatch[3]) {
        el.iterator2 = iteratorMatch[3].trim()
      }
    } else {
      el.alias = alias // alias = "item"
    }
  }
}

看懂这段代码的关键之一是明白JavaScript的正则表达式。比如,forAliasRE、forIteratorRE都是匹配模式串。以上面的html模板字符串为例,inMatch、iteratorMatch的内容如下:

// inMatch: Array
[
  '(item, index) in list',
  '(item, index)',
  'list',
  index: 0,
  input: '(item, index) in list',
  groups: undefined
]
// iteratorMatch: Array
[
  '(item, index)',
  'item',
  ' index',
  undefined,
  index: 0,
  input: '(item, index)',
  groups: undefined
]

由此,我们给出创建出来的AST节点el信息:

// v-for="(item, index) in list"
el.for = 'list'	// inMatch[2]
const alias = '(item, index)'	// inMatch[1]
el.alias = 'item'	// iterator[1]
el.iterator1 = 'index'	// iterator[2]

alias,别名,此处用以表示在v-for循环中给可迭代对象的当前遍历项,即它的别名。

从AST到Vnode render

在次外层函数genElement()中,genFor()用以生成列表渲染部分的renderCode:

function genFor(el) {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

  if (!el.key) { // v-for 最好声明key属性
    warn(
      `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
      `v-for should have explicit keys. ` +
      `See https://vuejs.org/guide/list.html#key for more info.`,
      true /* tip */
    )
  }

  // v-for="(item, index) in list"
  // alias = item, iterator1 = index

  // v-for="(value, key, index) in object"
  // alias = value, iterator1 = key, iterator2 = index


  // _l(val, render)
  // val = list
  // render = function (alias, iterator1, iterator2) { return genElement(el) }
  el.forProcessed = true // avoid recursion
  return `_l((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
    `return ${genElement(el)}` +
    '})'
}

生成的renderCode即为:

_l((items),function(item, index){
    return _c('li', {}, [_v,( _s(item) )])
})

运行时的renderHelpersFunc中的渲染列表函数_l()

上面已经得到了带有列表渲染的renderCode,接下来就需要分析renderCode中的渲染函数_l()了——它是渲染出一个完整VNode列表的关键。

// core/instance/render-helpers/render-list.js
export function renderList (val, render) {
  let ret, i, l, keys, key
  if (Array.isArray(val) || typeof val === 'string') {
    ret = new Array(val.length)
    for (i = 0, l = val.length; i < l; i++) {
      ret[i] = render(val[i], i)
    }
  } else if (typeof val === 'number') { // 支持 v-for="n in 10"
    ret = new Array(val)
    for (i = 0; i < val; i++) {
      ret[i] = render(i + 1, i)
    }
  } else if (isObject(val)) {
    keys = Object.keys(val)
    ret = new Array(keys.length)
    for (i = 0, l = keys.length; i < l; i++) {
      key = keys[i]
      ret[i] = render(val[key], key, i)
    }
  }
  return ret
}

// core/instance/render.js
Vue.prototype._l = renderList

我们先看看最熟悉的renderCode情况:items是一个可迭代数组,那么函数内部将会创建一个ret数组,ret数组中的每一个元素都是可迭代数组中对应元素被渲染后的结果(单个VNode节点):

_l((items),function(item, index){
    return _c('li', {}, [_v,( _s(item) )])
})
  • items就是renderList(val, render)函数的第一个参数;
  • function(item, index){…}是第二个参数render,注意它返回_c(…)的结果,而_c()用于创建VNode并返回之,因此此时的render返回值为单个VNode。

另外,除了可迭代数组,数字、字符串和对象均可以作为v-for的迭代对象。这里说明一下对象作为迭代对象时,首先取出对象的所有键组成的数组keys,建立与之对应的ret[],ret数组中的每一个元素都是keys中每一个键、对应的值的渲染(render)结果。


文章作者: 鹿卿
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 鹿卿 !
评论
  目录