vue3.0 初探

本文主要是简单了解下vue3.0的新特性,vue2.x和vue3.0差异,以及如何使用方面进行学习,如有问题,请大佬们多多指教。

vue3.0 新特性

1.更快更省

  • 压缩包体积更小
    当前最小化并被压缩的vue运行时大小约为 20kB(2.6.10 版为 22.8kB)。vue3.0捆绑包的大小大约会减少一半,即大概有10kB。
  • 重构 Virtual DOM
    传统Virtual DOM的性能瓶颈问题,vue3.0采用动静结合减少性能损耗。
  • 响应式底层从 Object.defineProperty 变成 Proxy
    Object.defineProperty 是一个相对比较昂贵的操作,因为它直接操作对象的属性,颗粒度比较小,将它替换为ES6Proxy,在目标对象之上架了一层拦截,代理的是对象而不是对象的属性,这样可以将原本对对象属性的操作变为对整个对象的操作,颗粒度变大。
    javascript 引擎在解析的时候希望对象的结构越稳定越好,如果对象一直在变,可优化性降低,proxy 不需要对原始对象做太多操作。

2.完全 TypeScript 重构

  • 团队开发更轻松
  • 架构更灵活,阅读源码更轻松
  • 可以独立使用 Vue 内部模块

3.Composition API(组合式API)

vue2.x 中与组件逻辑相关的选项以 API 函数的形式重新设计

  • 一组低侵入式的、函数式的 API
  • 更好的逻辑复用与代码组织
  • 更好的类型推导

4.选用Function_based API

vue3.0将组件的逻辑都写在了函数内部,setup()会取代vue2.xdata()函数,返回一个对象,暴露给模板,而且只在初始化的时候调用一次,因为值可以被跟踪。

Function-based API 对比Class-based API有以下优点

  • typescript更加友好,typescript对函数的参数和返回值都非常好,写Function-based API既是javascript又是typescript,不需要任何的类型声明,typescript可以自己做类型推导。
  • 静态的importexporttreeshaking的前提,Function-based API中的方法都是从全局的vue中import进来的。
  • 函数内部的变量名和函数名都可以被压缩为单个字母,但是对象和类的属性和方法名默认不被压缩(为了防止引用出错。
  • 更灵活的逻辑复用。

5.使用了hooks

React Hook 和 Vue Hook对比

其实 React Hook 的限制非常多,比如官方文档中就专门有一个章节介绍它的限制:

1.不要在循环,条件或嵌套函数中调用 Hook
2.确保总是在你的 React 函数的最顶层调用他们。
3.遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useStateuseEffect 调用之间保持 hook 状态的正确。

而 Vue 带来的不同在于:

1.与 React Hooks 相同级别的逻辑组合功能,但有一些重要的区别。 与 React Hook 不同,setup 函数仅被调用一次,这在性能上比较占优。
2.对调用顺序没什么要求,每次渲染中不会反复调用 Hook 函数,产生的的 GC 压力较小。
3.不必考虑几乎总是需要 useCallback 的问题,以防止传递函数prop给子组件的引用变化,导致无必要的重新渲染。
4.React Hook 有闭包陷阱问题,如果用户忘记传递正确的依赖项数组,useEffectuseMemo 可能会捕获过时的变量,这不受此问题的影响。 Vue 的自动依赖关系跟踪确保观察者和计算值始终正确无误。

我们认可 React Hooks 的创造力,这也是 Vue-Composition-Api 的主要灵感来源。上面提到的问题确实存在于 React Hook 的设计中,我们注意到 Vue 的响应式模型恰好完美的解决了这些问题。

vue2.x 和 vue3.x 的Virtual DOM对比

传统Virtual DOM的性能瓶颈

数据变更之后,会触发对应Dep中的 Watcher 对象,Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比(新的Virtual DOM和旧的Virtual DOM进行 patch(diff算法) 算法比较,并算出二者之间的差异), 然后根据对比结果进行DOM操作来更新视图,但是传统Virtual DOM,进行算法比对时颗粒度是组件,每个组件作为一个颗粒。

虽然Vue能够保证触发更新的组件最小化,但是单个组件内部依然需要遍历该组件的整个Virtual DOM树。如下图所示,template中,只有message插值部分发生改变,整体结构不变,但是数据更新的时候,比对整个template结构,这样就存在性能损耗。

Vue3.0 Virtual DOM

动静结合:找到动态变化的部分,更新时只对比可以变化的部分,减少性能损耗。

1.节点结构不变

  • 节点结构完全不会改变
  • 只有一个动态节点

    这个时候没有必要比较结构顺序。

    2.节点结构变化 v-if

  • v-if 外部:只有v-if是动态节点

  • v-if 内部:只有 是动态节点

    把模板切分成两个部分,各部分相对静态。

    3.节点结构变化 v-for

  • v-for 外部:只有v-for是动态节点 (fragment)

  • 每个v-for循环内部:只有 是动态节点

    区块树Block tree
    以结构性指令为边界,将整个模板切割成一个一个的相对内部静态的块。

  • 将模板基于动态节点指令切割为嵌套的区块。

  • 每个区块内部的节点结构是固定的。
  • 每个区块只需要以一个Array追踪自身包含的动态节点。

响应式底层

Object.defineProperty变成Proxy

vue2 变更检测

vue2中是递归遍历 data 中的所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter,在 getter 中做数据依赖收集处理,在 setter 中监听数据的变化,并通知订阅当前数据的地方。

这么做有什么问题呢?

  • 检测不到对象属性的添加和删除:当你在对象上新加了一个属性newProperty,当前新加的这个属性并没有加入vue检测数据更新的机制(因为是在初始化之后添加的),vue.$set是能让vue知道你添加了属性, 它会给你做处理,$set内部也是通过调用Object.defineProperty()去处理的
  • 无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。
  • 性能问题,当data中数据比较多且层级很深的时候,因为要遍历data中所有的数据并给其设置成响应式的,会导致性能下降

vue3 Proxy

Vue3 进行了全新改进,使用 Proxy代理的作为全新的变更检测,不再使用 Object.defineProperty

为什么使用 Proxy 可以解决上面的问题呢?

主要是因为Proxy是拦截对象,对对象进行一个”拦截”,外界对该对象的访问,都必须先通过这层拦截。无论访问对象的什么属性,之前定义的还是新增的,它都会走到拦截中。

下面分别用Object.defineProperty()Proxy 实现一个简单的数据响应

  • 使用 Object.defineProperty() 实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Observer {
constructor(data) {
// 遍历参数data的属性,给添加到this上
for(let key of Object.keys(data)) {
if(typeof data[key] === 'object') {
data[key] = new Observer(data[key]);
}
Object.defineProperty(this, key, {
get() {
console.log('你访问了' + key);
return data[key];
},
set(newVal) {
console.log('你设置了' + key);
console.log('新的' + key + '=' + newVal);
if(newVal === data[key]) {
return;
}
data[key] = newVal;
}
})
}
}
}
const obj = {
name: 'app',
age: '18'
}
// 实例对象
const app = new Observer(obj);
app.age = 20;
console.log(app.age);
app.newPropKey = '新属性';
console.log(app.newPropKey);

执行结果

1
2
3
4
5
6
7
// 修改 obj原有的属性 age的输出
你设置了age
新的age=20
你访问了age
20
// 设置新属性的输出
新属性

可以看到,给对象新增一个属性,内部并没有监听到,新增的属性需要手动再次使用Object.defineProperty()进行监听。
这就是为什么 vue 2.x 中 检测不到对象属性的添加和删除的原因,内部提供的$set就是通过调用Object.defineProperty()去处理的。

  • 下面我们使用 Proxy 替代 Object.defineProperty() 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const obj = {
name: 'app',
age: '18'
}
const p = new Proxy(obj, {
get(target, propKey, receiver) {
console.log('你访问了' + propKey);
return Reflect.get(target, propKey, receiver);
},
set(target, propKey, value, receiver) {
console.log('你设置了' + propKey);
console.log('新的' + propKey + '=' + value);
Reflect.set(target, propKey, value, receiver);
}
});
p.age = '20';
console.log(p.age);
p.newPropKey = '新属性';
console.log(p.newPropKey);
1
2
3
4
5
6
7
8
9
10
11
// 修改原对象的age属性
你设置了age
新的age=20
你访问了age
20

// 设置新的属性
你设置了newPropKey
新的newPropKey=新属性
你访问了newPropKey
新属性

可以看到,新增的属性,并不需要重新添加响应式处理,因为 Proxy 是对对象的操作,只要你访问对象,就会走到 Proxy 的逻辑中。

Reflect(ES6引入) 是一个内置的对象,它提供拦截 JavaScript 操作的方法。将Object对象一些明显属于语言内部方法(比如Object.defineProperty())放到Reflect对象上。修改某些Object方法的返回结果,让其变得更合理。让Object操作都变成函数行为。具体内容查看MDN

vue2.x项目转换成3.x的方式

  • 1.composition-api
    Github:vuejs/composition-api

    安装:

    1
    2
    3
    npm install @vue/composition-api
    # or
    yarn add @vue/composition-api

    前提:适合还需要使用到基于Vue2.x的第三方插件,比如ElementUI,iview

    使用:通过安装包 @vue/composition-api,然后在main.js中导入,最后使用Vue.use(xxx),就可以使用Vue3.x的语法

  • 2.vue-next
    Github:vuejs/vue-next
    安装:

    1
    vue add vue-next

    完全使用vue3.x的生态,包括最新版的vuevue-routervuex,目前可能会找不到合适的基于Vue3.x的插件

Vue3.0和Vue2.x的差异

通过使用 composition-api 方式简单的对比下

1.语法上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<template>
<div>
<h1>test</h1>
<p>{{msg}}</p>
<p>{{msg1}}</p>
<p>{{msg2}}</p>
<button @click="changeMsg">更改msg1和msg2的值</button>
<p>{{num}}</p>
<p>2倍num: {{doubleNum}}</p>
<p>3倍num: {{threeNum}}</p>
<button @click="add">num++</button>
</div>
</template>

<script>
import { reactive,ref,toRefs,computed,watch,beforeCreate,created} from '@vue/composition-api'
export default {
// vue2.x
data(){
return{
msg:'我的名字叫Jack'
}
},
beforeCreate(){
console.log('---beforeCreate---');

},
created(){
console.log('---created---');
},
// vue3.x
// setup函数是一个新的组件选项。作为在组件内使用Composition API的入口点
setup(){
console.log('---setup---');
// 第一种写法
// reactive:接收一个普通对象然后返回该普通对象的响应式代理
const state = reactive({
msg1:'年龄18',
num:1,
// computed 第一种写法
doubleNum:computed(()=>{
return state.num*2
})
})
// 第一个参数是监听的值,state.num 表示当 state.num 发生变化就会触发监听器的回调函数,即第二个参数,第二个参数可以执行监听时候的回调
watch(() => state.num,val => {
console.log(`count is ${state.num}`)
})
// 第二中写法
//ref: 接受一个参数值并返回一个响应式且可改变的 ref 对象
const msg2=ref('今年高二')
// 方法
const changeMsg=()=>{
state.msg1="年龄20岁"
msg2.value='今年大一'
}
// computed 第二种写法(计算属性 computed 是一个方法,里面需要包含一个回调函数,当我们访问计算属性返回结果时,会自动获取回调函数的值)
const threeNum=computed(()=>{
return state.num*3
})
const add=()=>{
state.num++
}
return {
//toRefs: 从一个组合逻辑函数中返回响应式对象
...toRefs(state),
msg2,
threeNum,
changeMsg,
add
}
}
}
</script>

ref()函数接收一个参数值,返回一个响应式的数据对象。该对象只包含一个指向内部值的 .value 属性
基本用法

  • 在模板中访问时,无需通过.value属性,它会自动展开
  • 当 ref 作为 reactive 对象的 property 被访问或修改时,也将自动解套 value 值,其行为类似普通属性

2.生命周期钩子函数

vue3.0 中的生命周期函数和 vue2.x 相比做了一些调整和变化,对应关系如下:

  • beforeCreate -> 使用 setup()
  • created -> 使用 setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured

    这些生命周期钩子函数只能在 setup() 函数中使用,setup是位于beforeCreatecreated之间调用的

参考

理解Vue3.0中的Proxy
Vue 3.0 有哪些新特性
Vue 组合式 API
Vue Function-based API RFC
Vue 3.0 全家桶抢先体验