编译优化
patch过程优化
Block tree
- Vue2中的diff算法进行全量对比的,数据更新并触发重新渲染的粒度是组件级的,但是在组件内部依旧需要去遍历该组件内部的vNode树,比如我们需要去更新这个组件
<template>
<div>
<p>static text</p>
<p>static text</p>
<p>{{message}}</p>
<p>static text</p>
<p>static text</p>
</div>
</template>
整个diff过程会将所有标签都遍历,但是只有第三个p标签是动态标签,所以这里很多的diff和遍历都是不需要的,这就会导致Vnode如果整个模块动态节点很少时,这些遍历都是性能的浪费,而对于上诉这个例子,只需要遍历绑定的动态message p标签即可
- Vue3 通过编译阶段对静态模板的分析,编译生成Block tree,block tree的根节点记录该树的动态节点,直接跳过静态节点的对比。所以该处理不用深度遍历和广度遍历,在静态节点更多的情况下,其性能远远领先vue2的diff。
PatchFlag
静态标记
vue2在新旧树对比时,需要对比属性是否变化,内容是否变化等,而vue3会通过patchFlag标记这些节点类型、节点属性、节点内容等,在新旧树对比中只对比某一属性。
它与上次虚拟节点进行对比的时候,只对比带有静态标记的节点,并且可以通过flag得知当前节点需要对比的内容,如下图所示
vue3.0在创建虚拟DOM的时候,会根据DOM中的内容,会不会发生变化,来添加静态标记,因为<p>我是段落</p>
它是不会写死的,所以也不需要多浪费资源去对它进行对比,然后对会变化的<p>{{msg}}</p>
就自动添加上静态标记,这里面的1 指的是文本的一个静态标记,flag它是一个枚举,因为会改变的有时候不止会有文本,也会有class,style。
静态提升(hoistStatic)
- Vue2中无论元素是否参与更新,每次都会重新创建,然后再渲染
- Vue3中对于不参与更新的元素(比如上文的
<p>我是段落</p>
),会做静态提升(提升到全局变量里面),只会被创建一次,在渲染的时候直接复用即可。
事件侦听缓存(cacheHandlers)
vue2 onClick会被视为动态绑定,所以每次都会去追踪它的变化,但是因为是同一个函数。
所以vue3认为事件的处理一般是不会变化的,所以其对事件处理函数进行缓存。
//vue2
render(ctx){
return createVNode('button',{
onClick:function($event){
ctx.count++
}
})
}
//vue3
render(ctx,_cache){
return createVNode('button',{
onClick:cache[0] || (cache[0] = ($event) => ctx.count++)
})
}
源码体积优化
引入tree-shaking技术
依赖ES6模块语法的静态结构(即import和export)
通过编译阶段的静态分析,找到没有引用的模块并打上静态标记
如果在项目中如果没有引入transition、keepalive等组件,那么他们对应的代码就不会被打包,这样也间接的起到了减少项目引入vuejs包体积的目的
数据劫持优化
-
vue2是通过使用Object.defaultPrototype这个API来劫持数据的getter和setter,但这个API有一些缺陷,
1、需要预先知道拦截的key是什么,所以并不能检测对象属性的新增和删除,即使提供了$set和$delete等方法,但是还是新增了使用负担
2、如果遇到嵌套层级过多的对象,那么就需要对这个对象进行递归遍历,对每一层对象进行数据监听,如果定义的数据复杂,会对性能造成一定的影响 -
vue3使用的是Proxy去劫持整个对象,自然对对象的属性新增和删除都能检测到,不过需要注意的是,proxy不能监听到对象内部深层次的变化,因为vue3处理方式是在getter中去递归响应式,这样子的好处是真是访问到内部对象才会变成响应式,而不是无脑递归,这无疑很大程度提升了性能
语法方面
vue2 本质就是编写一个包含了描述组件选项的对象 我们成为option API ,按照data、methods、props、watch、computed选项分类,
如果处于比较小型的项目中,可能逻辑还是可以分清的,但是如果处于大型的项目中一个组件可能有多个逻辑关注点,非常分散,如果需要修改一个逻辑关注点的时候,就需要上下来回的切换代码,
vue3 提供了Componsition api
优化逻辑复用
当我们开发的项目比较复杂的时候,免不了需要抽离出来一些复用的代码,在vue2.x中我们通常会使用混入(mixins)去复用逻辑,举一个鼠标监听的例子,我们会编写如下代码。
const mousePositionMixin = {
data() {
return {
x: 0,
y: 0
}
},
mounted() {
window.addEventListener('mousemove', this.update)
},
destroyed() {
window.removeEventListener('mousemove', this.update)
},
methods: {
update(e) {
this.x = e.pageX
this.y = e.pageY
}
}
}
export default mousePositionMixin
然后在组件中使用:
<template>
<div>
Mouse position: x {{ x }} / y {{ y }}
</div>
</template>
<script>
import mousePositionMixin from './mouse'
export default {
mixins: [mousePositionMixin]
}
</script>
使用单个 mixin 似乎问题不大,但是当我们一个组件混入大量不同的 mixins 的时候,会存在两个非常明显的问题:命名冲突 和 数据来源不清晰。
- 1、首先每个 mixin 都可以定义自己的 props、data,它们之间是无感的,所以很容易定义相同的变量,导致命名冲突。
- 2、另外对组件而言,如果模板中使用不在当前组件中定义的变量,那么就会不太容易知道这些变量在哪里定义的,这就是数据来源不清晰。
Vue.js 3.0 设计的 Composition API,就很好地帮助我们解决了 mixins 的这两个问题。
在vuejs3.x中使用hook来解决
import { ref, onMounted, onUnmounted } from 'vue'
export default function useMousePosition() {
const x = ref(0)
const y = ref(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
然后进行使用。
<template>
<div>
Mouse position: x {{ x }} / y {{ y }}
</div>
</template>
<script>
import useMousePosition from './mouse'
export default {
setup() {
const { x, y } = useMousePosition()
return { x, y }
}
}
</script>
我们可以看到,数据来源变得清晰了,即使去编写更多的hooks,也不会出现命名冲突的问题。
Componsition api除了在逻辑复用方面存在一些优势,也会有更好的类型支持,因为它们都是一些函数,在调用函数时,自然所有的类型就被推导出来了,不像 Options API 所有的东西使用 this。另外,Composition API 对 tree-shaking 友好,代码也更容易压缩。这里还需要说明的是,Composition API 属于 API 的增强,它并不是 Vue.js 3.0 组件开发的范式,如果你的组件足够简单,你还是可以使用 Options API。