Description
问题
说一说 Vue3 中编译优化方面的 静态提升 是什么?以及为什么使用 静态提升 可以编译优化?
该问题可能引申自:
Vue3 中有哪些 编译优化 手段?
分析
案例来自于《Vuejs 设计与实现》
假如有如下 template 1:
<div>
<p>static text</p>
<p>{{ message}}</p>
</div>
以及如下的 template 2:
<div>
<p foo='foo'>{{ message }}</p>
</div>
对于 template 1,如果没有使用静态提升,渲染函数最后会变成:
const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("p", null, "static text"),
_createElementVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
如果响应式数据 message
发生了变化,render
函数会重新执行。但对于 'static text'
的 p
标签而言,额外的创建行为会带来性能消耗。
同理对于 template 2,如果没有使用静态提升,渲染函数最后会变成:
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("p", { foo: "foo" }, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
这里给出 renderer
的部分相关代码
function patchElement(n1, n2, container, parentComponent, anchor) {
const el = (n2.el = n1.el)
// children
patchChildren(n1, n2, el, parentComponent, anchor)
// props
const oldProps = n1.props || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
patchProps(el, oldProps, newProps)
}
function patchProps(el, oldProps, newProps) {
// #5857
if (oldProps !== newProps) {
// 省略 newProps 的处理代码
// 省略 oldProps 的处理代码
}
}
可以看到 patchElement
会执行 patchProps
,而 { foo: "foo" } !== { foo: "foo" }
会返回 true
,因此 newProps
和 oldProps
的处理代码都会执行,额外的处理行为无疑会带来性能消耗。
解决方案
template 1
对于 template 1 的场景而言,响应式数据的更新影响到了 静态节点,导致其重复创建。因此将其提升至 render
函数外,做一个引用即可:
const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, "static text", -1 /* HOISTED */)
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createElementVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
template 2
同理 template 1 的场景中,只需要将 props
提升到 render
函数外
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
const _hoisted_1 = { foo: "foo" }
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("p", _hoisted_1, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}
在 patchProps
时候,_hoisted_1 !== _hoisted_1
返回 false
,就不会进行多余的 props
对比和更新了。
回答
说一说 Vue3 中编译优化方面的 静态提升 是什么?以及为什么使用 静态提升 可以编译优化?
静态提升指的是:
-
对于静态的树(节点),使用类似如下的代码,将其提升到
render
函数外,即是 静态树(节点)的提升const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, "static text", -1 /* HOISTED */)
可以避免其他不相干响应式数据更新触发
render
,导致静态树(节点)重复创建的问题 -
对于动态的树(节点),如果其
props
是静态的,使用如下的代码,将其提升到render
函数外,即是 静态props
的提升const _hoisted_1 = { foo: "foo" }
可以避免在
patchProps
阶段,多余的newProps
和oldProps
的处理逻辑
不足
- 我
hoistStatic
的代码还没有看,实现部分要以后补上
后记
为什么要写这一篇呢?因为在 runtime-core
部分看到 patchProps
的代码时候,崔大说了句:为了性能加个 if (oldProps !== newProps)
语句,当时我就很不解,看了 HCY 的书,甚至还在 vue 3 里提了个 issue #5773,认为判断应该改为 hasPropsChanged
。因为形如下述的这个 case:
it('should not update element props which is already mounted when props are same', () => {
render(h('div', { id: 'bar' }, ['foo']), root)
expect(inner(root)).toBe('<div id="bar">foo</div>')
render(h('div', { id: 'bar' }, ['foo']), root)
expect(inner(root)).toBe('<div id="bar">foo</div>')
})
oldProps !== newProps
无论如何都会都会返回 true
的。
而群里的小伙伴 Salt 则直接提了个 PR #5857,认为这个判断是一个无效语句,应当删除。
后来 Evan You 回复了PR #5857,指出了 静态提升 的问题,这个判断是 有意为之。
所以我写了这篇文章,以便解答后续的小伙伴在看到 patchProps
的时候产生的困惑。产生困惑是正常的!因为此时眼光只局限在了 runtime-core
中,而没有 compiler-core
的大局观。仅以本篇鼓励所有学习的小伙伴,带着问题看源码,你会收获更多。