本教程操作环境:windows7系统、vue3版,dell g3电脑。
本文主要来分析 vue3.0
编译阶段做的优化,在 patch
阶段是如何利用这些优化策略来减少比对次数。由于组件更新时依然需要遍历该组件的整个 vnode
树,比如下面这个模板:
(资料图)
static text
static text
{{ message }}
static text
static text
整个 diff 过程如图所示:
可以看到,因为这段代码中只有一个动态节点,所以这里有很多 diff 和遍历其实都是不需要的,这就会导致 vnode 的性能跟模版大小正相关,跟动态节点的数量无关,当一些组件的整个模版内只有少量动态节点时,这些遍历都是性能的浪费。对于上述例子,理想状态只需要 diff 这个绑定 message 动态节点的 p 标签即可。
vue.js 3.0
通过编译阶段对静态模板的分析,编译生成了 block tree
。
block tree
是一个将模板基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,而且每个区块只需要以一个 array
来追踪自身包含的动态节点。借助 block tree
,vue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,这是一个非常大的性能突破。
由于 diff
算法无法避免新旧虚拟 dom
中无用的比较操作,vue.js 3.0
引入了 patchflag
,用来标记动态内容。在编译过程中会根据不同的属性类型打上不同的标识,从而实现了快速 diff
算法。patchflags
的所有枚举类型如下所示:
export const enum patchflags { text = 1, // 动态文本节点 class = 1 << 1, // 动态class style = 1 << 2, // 动态style props = 1 << 3, // 除了class、style动态属性 full_props = 1 << 4, // 有key,需要完整diff hydrate_events = 1 << 5, // 挂载过事件的 stable_fragment = 1 << 6, // 稳定序列,子节点顺序不会发生变化 keyed_fragment = 1 << 7, // 子节点有key的fragment unkeyed_fragment = 1 << 8, // 子节点没有key的fragment need_patch = 1 << 9, // 进行非props比较, ref比较 dynamic_slots = 1 << 10, // 动态插槽 dev_root_fragment = 1 << 11, hoisted = -1, // 表示静态节点,内容变化,不比较儿子 bail = -2 // 表示diff算法应该结束}
左侧的 template
经过编译后会生成右侧的 render
函数,里面有 _openblock
、_createelementblock
、_todisplaystring
、_createelementvnode
(createvnode
) 等辅助函数。
let currentblock = nullfunction _openblock() { currentblock = [] // 用一个数组来收集多个动态节点}function _createelementblock(type, props, children, patchflag) { return setupblock(createvnode(type, props, children, patchflag));}export function createvnode(type, props, children = null, patchflag = 0) { const vnode = { type, props, children, el: null, // 虚拟节点上对应的真实节点,后续diff算法 key: props?.["key"], __v_isvnode: true, shapeflag, patchflag }; ... if (currentblock && vnode.patchflag > 0) { currentblock.push(vnode); } return vnode;}function setupblock(vnode) { vnode.dynamicchildren = currentblock; currentblock = null; return vnode;}function _todisplaystring(val) { return isstring(val) ? val : val == null ? "" : isobject(val) ? json.stringify(val) : string(val);}
此时生成的 vnode 如下:
此时生成的虚拟节点多出一个 dynamicchildren
属性,里面收集了动态节点 span
。
我们之前分析过,在 patch
阶段更新节点元素的时候,会执行 patchelement
函数,我们再来回顾一下它的实现:
const patchelement = (n1, n2) => { // 先复用节点、在比较属性、在比较儿子 let el = n2.el = n1.el; let oldprops = n1.props || {}; // 对象 let newprops = n2.props || {}; // 对象 patchprops(oldprops, newprops, el); if (n2.dynamicchildren) { // 只比较动态元素 patchblockchildren(n1, n2); } else { patchchildren(n1, n2, el); // 全量 diff }}
我们在前面组件更新的章节分析过这个流程,在分析子节点更新的部分,当时并没有考虑到优化的场景,所以只分析了全量比对更新的场景。
而实际上,如果这个 vnode
是一个 block vnode
,那么我们不用去通过 patchchildren
全量比对,只需要通过 patchblockchildren
去比对并更新 block
中的动态子节点即可。由此可以看出性能被大幅度提升,从 tree
级别的比对,变成了线性结构比对。
我们来看一下它的实现:
const patchblockchildren = (n1, n2) => { for (let i = 0; i < n2.dynamicchildren.length; i ) { patchelement(n1.dynamicchildren[i], n2.dynamicchildren[i]) }}
接下来我们看一下属性比对的优化策略:
const patchelement = (n1, n2) => { // 先复用节点、在比较属性、在比较儿子 let el = n2.el = n1.el; let oldprops = n1.props || {}; // 对象 let newprops = n2.props || {}; // 对象 let { patchflag, dynamicchildren } = n2 if (patchflag > 0) { if (patchflag & patchflags.full_props) { // 对所 props 都进行比较更新 patchprops(el, n2, oldprops, newprops, ...) } else { // 存在动态 class 属性时 if (patchflag & patchflags.class) { if (oldprops.class !== newprops.class) { hostpatchprop(el, "class", null, newprops.class, ...) } } // 存在动态 style 属性时 if (patchflag & patchflags.style) { hostpatchprop(el, "style", oldprops.style, newprops.style, ...) } // 针对除了 style、class 的 props if (patchflag & patchflags.props) { const propstoupdate = n2.dynamicprops! for (let i = 0; i < propstoupdate.length; i ) { const key = propstoupdate[i] const prev = oldprops[key] const next = newprops[key] if (next !== prev) { hostpatchprop(el, key, prev, next, ...) } } } if (patchflag & patchflags.text) { // 存在动态文本 if (n1.children !== n2.children) { hostsetelementtext(el, n2.children as string) } } } else if (dynamicchildren == null) { patchprops(el, n2, oldprops, newprops, ...) } }}function hostpatchprop(el, key, prevvalue, nextvalue) { if (key === "class") { // 更新 class patchclass(el, nextvalue) } else if (key === "style") { // 更新 style patchstyle(el, prevvalue, nextvalue) } else if (/^on[^a-z]/.test(key)) { // events addeventlistener patchevent(el, key, nextvalue); } else { // 普通属性 el.setattribute patchattr(el, key, nextvalue); }}function patchclass(el, nextvalue) { if (nextvalue == null) { el.removeattribute("class"); // 如果不需要class直接移除 } else { el.classname = nextvalue }}function patchstyle(el, prevvalue, nextvalue = {}){ ...}function patchattr(el, key, nextvalue){ ...}
总结: vue3
会充分利用 patchflag
和 dynamicchildren
做优化。如果确定只是某个局部的变动,比如 style
改变,那么只会调用 hostpatchprop
并传入对应的参数 style
做特定的更新(靶向更新);如果有 dynamicchildren
,会执行 patchblockchildren
做对比更新,不会每次都对 props 和子节点进行全量的对比更新。图解如下:
静态提升是将静态的节点或者属性提升出去,假设有以下模板:
hello {{name}}
编译生成的 render
函数如下:
export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openblock(), _createelementblock("div", null, [ _createelementvnode("span", null, "hello"), _createelementvnode("span", { a: "1", b: "2" }, _todisplaystring(_ctx.name), 1 /* text */), _createelementvnode("a", null, [ _createelementvnode("span", null, _todisplaystring(_ctx.age), 1 /* text */) ]) ]))}
我们把模板编译成 render
函数是这个酱紫的,那么问题就是每次调用 render
函数都要重新创建虚拟节点。
开启静态提升 hoiststatic
选项后
const _hoisted_1 = /*#__pure__*/_createelementvnode("span", null, "hello", -1 /* hoisted */)const _hoisted_2 = { a: "1", b: "2"}export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openblock(), _createelementblock("div", null, [ _hoisted_1, _createelementvnode("span", _hoisted_2, _todisplaystring(_ctx.name), 1 /* text */), _createelementvnode("a", null, [ _createelementvnode("span", null, _todisplaystring(_ctx.age), 1 /* text */) ]) ]))}
静态提升的节点都是静态的,我们可以将提升出来的节点字符串化。 当连续静态节点超过 10
个时,会将静态节点序列化为字符串。
假如有如下模板:
static static static static static static static static static static
开启静态提升 hoiststatic
选项后
const _hoisted_1 = /*#__pure__*/_createstaticvnode("staticstaticstaticstaticstaticstaticstaticstaticstaticstatic", 10)const _hoisted_11 = [ _hoisted_1]export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openblock(), _createelementblock("div", null, _hoisted_11))}
假如有如下模板:
v = event.target.value">
编译后:
const _hoisted_1 = ["onclick"]export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openblock(), _createelementblock("div", { onclick: event => _ctx.v = event.target.value }, null, 8 /* props */, _hoisted_1))}
每次调用 render
的时候要创建新函数,开启函数缓存 cachehandlers
选项后,函数会被缓存起来,后续可以直接使用
export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openblock(), _createelementblock("div", { onclick: _cache[0] || (_cache[0] = event => _ctx.v = event.target.value) }))}
以上几点即为 vuejs
在编译阶段做的优化,基于上面几点,vuejs
在 patch
过程中极大地提高了性能。
以上就是vue3编译做了哪些优化的详细内容,更多请关注php中文网其它相关文章!
关键词: