vue中v-for和v-if为啥不能同时使用

高频面试题,vue中的v-ifv-for为啥不能同时用?

答案是:用了也能出来预期的效果,但是会有性能浪费。
看以下例子:

new Vue({
  el: "#app",
  data() {
    return {
      list: ["TEXT-a", "TEXT-b", "TEXT-c", "TEXT-d"]
    };
  },
  template: `<ul><li v-if="index !== 1" v-for="(item, index) in list">{{item}}</li></ul>`
});

例子中,v-forv-if同时使用,并且特意把v-if放在前面,我们知道vue从模板到视图展示出来,会经历三个主要的流程,主要有编译、获取虚拟DOMpatch过程。

一、编译

1、ast生成过程

ast的生成过程中,ul的子元素li上的属性有if:"index !== 1"来描述if属性,有alias: "item"iterator1: "index"for: "list"的属性来描述v-for。通过optimize优化后,执行generate

2、generate生成可执行代码字符串

generate中主要的逻辑是genElement

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // ...
  }
}

可以看出el.for的优先级比v-if要高,整个code构建过程是递归的过程,当执行完成后会得到code码为:

"_c('ul',_l((list),function(item,index){return (index !== 1)?_c('li',[_v(_s(item))]):_e()}),0)"

可以看出,_l方法控制循环构建vNode主流程,返回的vNode又由内部的index !== 1作为条件控制,条件满足生成TEXT类型的vNode,条件不满足生成Comment类型的vNode。接下来看vNode获取过程:

二、vNode获取

通过编译过程,获取到的render函数为:

with(this){
    return _c('ul',_l((list),function(item,index){return (index !== 1)?_c('li',[_v(_s(item))]):_e()}),0)
}

当执行vnode = render.call(vm._renderProxy, vm.$createElement)的时候,会执行vue原型上挂载的函数_c_l_v_s_e。我们主要来看_l函数:

/**
 * Runtime helper for rendering v-for lists.
 */
export function renderList (
  val: any,
  render: (
    val: any,
    keyOrIndex: string | number,
    index?: number
  ) => VNode
): ?Array<VNode> {
  let ret: ?Array<VNode>, 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)
    }
  }
  // val为其他类型的场景...
}

文中例子中的listArray类型,所以这里ret的长度为4,这里的render就是:

function(item,index){return (index !== 1)?_c('li',[_v(_s(item))]):_e()}

当满足index !== 1会创建TEXT类型的vNode,当不满足时执行_e获得空注释节点:

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

注意这里的isCommenttrue,最终在条件不满足的时候创建的是空的注释节点。最终的vNode列表中包含3个以TEXTvNode为子节点的livNode1个空注释节点:

image.png

三、patch过程

patch的过程中,会执行到createElm,进而执行到createChildren

function createChildren (vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(children);
      }
      for (var i = 0; i < children.length; ++i) {
        createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
      }
    } else if (isPrimitive(vnode.text)) {
      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
    }
}

当第二次循环的时候i1,此时会执行到createElm中的:

// ...
else if (isTrue(vnode.isComment)) {
  vnode.elm = nodeOps.createComment(vnode.text);
  insert(parentElm, vnode.elm, refElm);
}
// ...

到这里发现,即使创建的空注释,也需要执行parentElm中插入空字符的操作。整个patch也是递归的过程,执行的是先插入子元素后插入父元素的操作,当执行到递归顶层的insert(parentElm, vnode.elm, refElm)的时候,ul中就插入了三个li元素和一个注释空节点:

image.png

总结

当对list通过过滤的方式处理后:

new Vue({
  el: "#app",
  data() {
    return {
      list: ["TEXT-a", "TEXT-b", "TEXT-c", "TEXT-d"]
    };
  },
  created() {
    this.list = this.list.filter((val, index) => index !== 1);
  },
  template: `<ul><li v-for="(item, index) in list">{{item}}</li></ul>`
});
  • 生成的code少了if的三目运算符
"_c('ul',_l((list),function(item,index){return _c('li',[_v(_s(item))])}),0)"
  • 生成的vNode少了Comment类型

image.png

  • 生成的真实DOM少了注释空节点

image.png

综上所述,v-ifv-for同时出现的场景可以拆分为,数据在渲染之前借助生命周期进行处理,也可通过计算属性的方式进行处理。减少vNode的生成,也减少整个渲染过程的流程,最终减少注释类的DOM节点,以达到性能优化的目的。1个数据不明显,那么如果有大量的数据,这种处理的收益就很可观。