问题描述
在开发中,我们可能会写出如下代码
<!-- 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
},
]
这个操作看起来很简单,就是过滤要展示的列表,但是官方是不推荐这么写的,官方链接。 官方给出了两点原因:
- 当 Vue 处理指令时,v-for 比 v-if 具有更高的优先级
- 哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。
懒得看原文的可以看下面的截图:
问题分析
问题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>30
的item
通过这个小实验我们已经能够知道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进行渲染,尽可能的提高性能。