你不知道的 Vue3 优化。
前言
使用 Vue2 的时候,碰到了一些性能瓶颈,然后在看 Vue3 源码的时候,发现 Vue3 的优化有点意思,大多数人写过的 Vue3 优化我就不说了,说一说比较细的几个方面。
方面
- 响应式初始化的优化。
- 编译上的优化。
- patch 上的优化。
- 插槽对组件更新的影响。
响应式初始化的优化
在 Vue2 中,对非数组数据的劫持是通过 Object.defineProperty,因为要确保所有数据的变化都能被监听到,不论数据数据多深,用到没,只能无脑递归。
// data.js
// 模拟大数据
const data = []
for (let i = 0; i < 150; i++) {
data.push({ name: 1, sex: 1, age: 1 })
}
const children = JSON.stringify(data)
for (const item of data) {
item.children = JSON.parse(children)
for (const it of item.children) {
it.children = JSON.parse(children)
}
}
Vue2
<!-- html -->
<div id="app">
我是 Vue2,<button @click="changeData">增加数据</button>
</div>
<!-- script -->
<script src="https://unpkg.com/vue@2.6.14"></script>
<script src="./data.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
data: []
}
},
methods: {
changeData() {
this.data = data
}
}
})
</script>
Vue2 性能
页面加载之后,我们点击按钮,来看下性能。我们能发现响应式数据耗费了大概 8.7s,哪怕我们页面中并没有用到这个数据。
Vue3
<!-- html -->
<div id="app">
我是 Vue2,<button @click="changeData">增加数据</button>
</div>
<!-- script -->
<script src="https://unpkg.com/vue@latest"></script>
<script src="./data.js"></script>
<script>
Vue.createApp({
data() {
return {
data: {}
}
},
methods: {
changeData() {
this.data = data
}
}
}).mount('#app')
</script>
Vue3 性能
可以看到耗费时间大约 0.1s,因为没有使用脚手架创建工程,所以这个 0.1s 中有很多时间用在了模板编译上,实际上的时间就更少了。
Vue3 和 Vue2 响应式数据最大的区别就是,Vue3 是用到哪个数据,才会去对这个数据做劫持,我下面贴一段 Vue3 响应式处理的代码。
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!isTracking()) {
return
}
// 判断数据是否响应式处理过。
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 判断数据的这个 key 是否做过响应式劫持。
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
// 收集依赖
trackEffects(dep, eventInfo)
}
确实有点意思,我觉得可以叫做惰性劫持。
编译
Vue3 利用编译做了一些优化,而且对性能的提升是非常高的,现在贴一段代码,看看在 Vue2,Vue3 编译过后有什么不同。
<ul>
<li>
我是静态数据1
<span>123</span>
</li>
<li>我是静态数据2</li>
<li>我是静态数据3</li>
<li>我是静态数据4</li>
<li>我是静态数据5</li>
<li>{{ name }}</li>
</ul>
Vue2
在 Vue2 编译中主要的优化是:找到静态节点 => 找到值得标记的静态根节点,静态节点就是不会变化的节点,而静态根节点就是静态节点最大的祖先,也不会变化。这样每次组件更新的时候就可以跳过这些节点,从而提升性能。
Vue3
Vue3 将所有可以提升的节点全部提升,从而每次 render 的时候不会重复创建静态节点。
patch
接着上面 Vue3 的编译继续讲,你可以看到 Vue3 编译的时候调用了 _openBlock 这个方法,下面来讲讲这个方法做了什么。
// 这个就是 _openBlock 函数
// 我们可以看到创建了一个数组,然后放到了 blockStack 中。
export function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
}
现在我们回顾刚才 render 函数里面创建 VNode 的方法,创建 VNode 的方法都会调用 createBaseVNode 这个方法,我们在下面可以看到他把当前这个 VNode 放到了下面的 currentBlock 数组里面,Vue3 是把动态节点和所有能提升的节点隔离开来,然后把当前组件所创建的 VNode 放到了一个数组里面。
function createBaseVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag = 0,
dynamicProps: string[] | null = null,
shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
isBlockNode = false,
needFullChildrenNormalization = false
) {
// 省略代码。。。
// track vnode for block tree
if (
isBlockTreeEnabled > 0 &&
// avoid a block node from tracking itself
!isBlockNode &&
// has current parent block
currentBlock &&
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
// component doesn't need to update, it needs to persist the instance on to
// the next vnode so that it can be properly unmounted later.
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching.
vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
) {
// 我在这里
currentBlock.push(vnode)
}
return vnode
}
现在我们继续接着 openBlock 挨着的这个方法,我们看他做了什么。
// 它调用了 setupBlock 方法
export function createElementBlock(
type: string | typeof Fragment,
props?: Record<string, any> | null,
children?: any,
patchFlag?: number,
dynamicProps?: string[],
shapeFlag?: number
) {
return setupBlock(
createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
true /* isBlock */
)
)
}
function setupBlock(vnode: VNode) {
// 给当前组件根节点挂载一个数组,存放所有创建的 VNode
vnode.dynamicChildren = isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
// 弹出
closeBlock()
// a block is always going to be patched, so track it as a child of its
// parent block
if (isBlockTreeEnabled > 0 && currentBlock) {
currentBlock.push(vnode)
}
return vnode
}
// 弹出
export function closeBlock() {
blockStack.pop()
currentBlock = blockStack[blockStack.length - 1] || null
}
接下来我们去看看这个 dynamicChildren 到底是要做什么。
代码太多,下面以图片示例,组件 patch 的时候会调用一些 patch 的子方法,其中会对 dynamicChildren 做判断,如果有,直接用新旧两个 dynamicChildren 去对比,Vue2 中你不论怎么办,你都得从头比到脚,而在 Vue3 中,可以直接对会变化的节点进行对比,这个是之前不敢想像的。
静态插槽的优化
我们平常项目开发中经常会这样使用组件。
<!-- 组件内 -->
<div>
<!-- 某个子组件,我们会直接插进来一些内容 -->
<Child>
{{ name }}
</Child>
</div>
如上所示,在 Vue2 和 Vue3 中的效果是截然不同的。
Vue2
如图所示,子组件插槽的编译和 VNode 的生成都是在父组件,在 Vue2 中这种情况,只要父组件更新,子组件就会伴随着强制更新,如果我们这个子组件很大,那么就会带来不必要的性能开销。
Vue3
如图所示,静态插槽被归类成了作用域插槽,也就意味着子组件插槽的编译是在父组件,但是这个生成 VNode 的函数会在子组件内调用生成,也就意味着子组件会被所使用的响应式数据收集到依赖中,也就只有这些数据发生变化时,才会通知子组件更新,子组件也不会伴随父组件更新而强制更新。
插槽的影响可以看我之前的这篇文章:理解Vue内部渲染机制(避免页面性能瓶颈)
总结
Vue3 确实有点意思!!!
转载自:https://juejin.cn/post/7074766983853506596