从源码解惑,为什么v-if和v-for不应该一起用?

8,837 阅读4分钟

问题描述

在开发中,我们可能会写出如下代码

<!-- html模版 -->
<div id="app">
  <ul>
    <li v-for="item in list" v-if="item.age<30">
      <span>{{item.name}}</span>
      <span>{{item.age}}</span>
    </li>
  </ul>
</div>
// 列表数据
list: [
  {
    name: 'jack',
    age: 23
  },
  {
    name: 'john',
    age: 33
  },
  {
    name: 'petty',
    age: 20
  },
]

这个操作看起来很简单,就是过滤要展示的列表,但是官方是不推荐这么写的,官方链接。 官方给出了两点原因:

  1. 当 Vue 处理指令时,v-for 比 v-if 具有更高的优先级
  2. 哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。

懒得看原文的可以看下面的截图:

pic1
pic2

问题分析

问题1:当 Vue 处理指令时,v-for 比 v-if 具有更高的优先级

通过上文的描述,大概是懂了,嗯。。。但是看完还是不知所以然。
比如官网说,v-for比v-if优先级更高,为什么呢?你说优先就优先?🤔
我们可以做个简单的小实验,就是打印一下render函数,看一下vue对这两个指令是如何解析的。

// 打印出来的render函数
(function anonymous() {
    with (this) {
        return _c('div', {
            attrs: {
                "id": "app"
            }
        }, [_c('ul', _l((list), function(item) {
            return (item.age < 30) ? _c('li', [_c('span', [_v(_s(item.name))]), _v(" "), _c('span', [_v(_s(item.age))])]) : _e()
        }), 0)])
    }
})

直接看这个代码可能不知道各个函数名是什么意思,我们打开源码,可以看到在renderMixin的时候会把vue的原型传入下面的方法

// renderMixin方法执行时注册渲染快捷方法,全部挂载在vue原型上
// install runtime convenience helpers
installRenderHelpers(Vue.prototype)

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

// _c方法在render.js中定义,表示createElement
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

通过上述的函数映射关系,我们可以知道,vue通过_l(renderList)函数遍历list,在函数内部再通过三目语句处理v-if指令,如果条件为true,则创建li及子节点,否则执行_e(createEmptyVNode)创建一个空节点,实际上是一个没有文本的注释节点

// createEmptyVNode创建一个默认为空文本的注释节点
export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

我们在控制台中可以看到这个空的注释节点,通过对比list数据,可以知道,这个注释节点就是那个age>30item

pic3

通过这个小实验我们已经能够知道v-for确实比v-if的优先级更高了,但是你可能想问了,为什么你是这样的render函数?😂

那么我们再进一步的去探索生成render函数的函数
我们最终在compiler模版编译器找到了答案

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
  // 我们的render函数就是在这里生成的,里面的code通过下面的genElement方法生成
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

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)
    // 这里就是问题的核心,先处理了v-for
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
    // 然后再处理v-if
  } 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 {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }

      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

讲到这里,官方说的第一个问题就分析完了,那么说的第二点又是什么意思呢?

问题2: 哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。

这里为什么说每次重新渲染的时候都要遍历整个列表?其实是这样的,render函数执行以后会生成vnode,就是虚拟dom。每当数据发生变化时,会触发watcher执行update方法,就会重新执行render方法生成新的vnode,所以就需要重新遍历一遍数据

// 每个组件初始化挂载时(mountComponent)会定义一个渲染watcher
new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */
)
  // 每当组件数据变化时,就会执行这个方法
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

还有最后一句话不知道你注意到了没有 不论活跃用户是否发生了变化。你可能会问了,难道我的list数据没有发生变化也要重新遍历?
是的,在vue2中,为了优化性能,将watcher的粒度放大,变为一个组件一个watcher(用户自定义的watcher除外),这样,数据变化就只能通知到组件这一级别,至于组件里面到底哪个数据发生了变化,应该更新哪个节点,需要依靠新老数据生成的vnode虚拟节点进行diff对比才能知道。

结论

讲到这里,大家应该已经清楚了那篇文档的良苦用心了吧😂。我们在实际的开发中,应该尽量避免这种写法。如果要过滤数据,可以使用计算属性进行过滤,然后再丢给vue进行渲染,尽可能的提高性能。