vue3在哪些方面提升了性能


通过响应式系统的重写编译优化源码体积的优化(按需加载)三个方面提升了性能。

1. 响应式系统提升

vue2在初始化的时候,通过Object.defineProperty对data的每个属性进行访问和修改的拦截,getter进行依赖收集、setter派发更新。在属性值是对象的时候还需要递归调用defineproperty。看下大致实现的代码:

function observe(target) {
if (target && typeof target === "Object") {
Object.keys(target).forEach((key) => {
defineReactive(target, key, target[key])
})
}
}
function defineReactive(obj, key, val) {
const dep = new Dep();
observe(val) // 如果属性值是对象就遍历它的属性
Object.defineProperty(obj, key, {
get() {
return val
},
set(v) {
val = v
dep.notify();
}
})
}

而如果属性是数组,还需要覆盖数组的七个方法(会改变原数组的七个方法)进行变更的通知:

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
ob.dep.notify()
return result
})
})

从这几段代码可以看出Object.defineProperty的几个缺点:

  • 初始化时需要遍历对象所有key,层级多的情况下,性能有一定影响
  • 动态新增、删除对象属性无法拦截,只能用set/delete api代替
  • 不支持新的Map、Set等数据结构
  • 无法监控到数组下标的变化(监听的性能代价太大)

所以在vue3中用了proxy全面代替Object.defineProperty的响应式系统。proxy是比较新的浏览器特性,拦截的是整个对象而不是对象的属性,可以拦截多种方法,包括属性的访问、赋值、删除等操作,不需要初始化的时候遍历所有属性,并且是懒执行的特性,也就是在访问到的时候才会触发,当访问到对象属性的时候才会递归代理这个对象属性,所以性能比vue2有明显的优势。

总结下proxy的优势:

  • 可以监听多种操作方法,包括动态新增的属性和删除属性、has、apply等操作
  • 可以监听数组的索引和 length 等属性
  • 懒执行,不需要初始化的时候递归遍历
  • 浏览器新标准,性能更好,并且有持续优化的可能

看下大致实现拦截对象的方法。

export function reactive(target: object) {
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
)
{
const proxy = new Proxy(
target,
baseHandlers
)
proxyMap.set(target, proxy) // 用weakMap收集
return proxy
}

2. 编译优化(虚拟dom优化)

编译优化主要是通过重写虚拟dom。优化的点包括编译模板的静态标记静态提升事件缓存

  • 静态标记(PatchFlag)

根据尤大直播所说,更新的性能提升1.3~2倍,ssr提升2~3倍。 在对更新的节点进行对比的时候,只会去对比带有静态标记的节点。并且 PatchFlag 枚举定义了十几种类型,用以更精确的定位需要对比节点的类型。

看这段代码

<div id="app">
<p>前端好好玩</p>
<div>{{message}}</div>
</div>

vue2编译后的渲染函数:

function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('p', [_v("前端好好玩")]), _c('div', [_v(
_s(message))])])
}
}

这个render函数会返回vnode,后面更新的时候vue2会调patch函数比旧vnode进行diff算法更新(在我的上篇文章有解析过),这时候对比是整个vnode,包括里面的静态节点<p>前端好好玩</p>,这样就会有一定的性能损耗。

vue3编译后的渲染函数:

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", { id: "app" }, [
_createVNode("p", null, "前端好好玩"),
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}

只有_createVNode这个函数带有第四个参数的才是非静态节点,也就是需要后续diff的节点。第四个参数是这个节点具体包含需要被diff的类型,比如是text节点,只有{{}}这种模板变量的绑定,后续只需要对比这个text即可,看下源码中定义了哪些枚举的元素类型:

  TEXT = 1,// 动态的文本节点
CLASS = 1 << 1, // 2,动态Class的节点
STYLE = 1 << 2, // 4,表示动态样式
PROPS = 1 << 3, // 8,动态属性
FULL_PROPS = 1 << 4, // 16 动态键名
HYDRATE_EVENTS = 1 << 5, // 32 带有事件监听器的节点
STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的
KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性
UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key
NEED_PATCH = 1 << 9, // 512
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
HOISTED = -1, // 静态提升的标记,不会被diff,下面的静态提升会提到
BAIL = -2 //

//位运算,有符号右移运算符,不了解的可以看我掘金的第一篇文章juejin.cn/post/688518…[1]

  • 静态提升

静态提升的意思就是把函数里的某些变量放到外面来,这样再次执行这个函数的时候就不会重新声明。vue3在编译阶段做了这个优化。还是上面那段代码,分别看下vue2和vue3编译后的不同

vue2:

function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('p', [_v("前端好好玩")]), _c('div', [_v(_s(message))])])
}
}

vue3:

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "前端好好玩", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", _hoisted_1, [
_hoisted_2,
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
]))
}

可以看到vue3将不变的节点声明放到了外面去执行,后面再渲染的时候直接去_hoited变量就行,而vue2每次render都需要执行_c生成新的节点。这里还有一个点,_hoisted_2的_createVNode第四个参数-1,标记这个节点永远不需要diff。

  • 事件缓存

默认情况下事件被认为是动态变量,所以每次更新视图的时候都会追踪它的变化。但是正常情况下,我们的 @click 事件在视图渲染前和渲染后,都是同一个事件,基本上不需要去追踪它的变化,所以 Vue 3.0 对此作出了相应的优化叫事件监听缓存

<div id="app">
<p @click="handleClick">前端好好玩</p>
</div>

vue3编译后:

import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

const _hoisted_1 = { id: "app" }

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", _hoisted_1, [
_createVNode("p", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
}, "前端好好玩")
]))
}

可以看到onClick有一个_cache判断缓存赋值的操作,从而变成静态节点

3. 源码体积的优化

vue3通过重构全局api和内部api,支持了tree shaking,任何一个函数,如ref、reavtived、computed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小

参考资料

[1]

https://juejin.cn/post/6885185633028538376: https://juejin.cn/post/6885185633028538376


原文始发于微信公众号(消失的程序员):vue3在哪些方面提升了性能

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/250633.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!