Vue3 简介

v3

Vue3 带来了什么

  1. 性能的提升

    • 打包大小减少41%
    • 初次渲染快 55%, 更新渲染快 133%
    • 内存减少 54%
  2. 源码的升级

    • 使用 Proxy 代替 defineProperty 实现响应式
    • 重写虚拟 DOM 的实现和 Tree-Shaking
  3. 拥抱 TypeScript

    • Vue3 可以更好的支持 TypeScript
  4. 新的特性

    1. Composition API(组合 API)

      • setup 配置
      • ref 与 reactive
      • watch 与 watchEffect
      • provide 与 inject
      • ···
    2. 新的内置组件

      • Fragment
      • Teleport
      • Suspense
    3. 其他改变

      • 新的生命周期钩子
      • data 选项应始终被声明为一个函数
      • 移除 keyCode 支持作为 v-on 的修饰符
      • ···

创建 vue3 项目

1
2
3
4
5
6
7
8
9
## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
## 安装或者升级你的@vue/cli
npm install -g @vue/cli
## 创建
vue create vue3_study
## 启动
cd vue3_study
npm run serve

Options API 的弊端

在 Vue2 中,我们编写组件的方式是 OptionsAPI:

Options API 的一大特点就是在对应的属性中编写对应的功能模块;

比如 data 定义数据、methods 中定义方法、computed 中定义计算属性、watch 中监听属性改变,也包括生命 周期钩子;

但是这种代码有一个很大的弊端:

当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中;

当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散;

尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人);

下面我们来看一个非常大的组件,其中的逻辑功能按照颜色进行了划分:

这种碎片化的代码使用理解和维护这个复杂的组件变得异常困难,并且隐藏了潜在的逻辑问题;

并且当我们处理单个逻辑关注点时,需要不断的跳到相应的代码块中;

OptionsAPI Composition API
OptionsAPI Composition API

如果我们能将同一个逻辑关注 点相关的代码收集在一起会更好。

这就是Composition API想要做的事情,以及可以帮助我 们完成的事情。

也有人把 Vue Composition API 简称为VCA

认识 Composition API

那么既然知道 Composition API 想要帮助我们做什么事情,接下来看一下到底是怎么做呢?

为了开始使用 Composition API,我们需要有一个可以实际使用它(编写代码)的地方;

在 Vue 组件中,这个位置就是 setup 函数;

setup其实就是组件的另外一个选项:

只不过这个选项强大到我们可以用它来替代之前所编写的大部分其他选项;

比如 methods、computed、watch、data、生命周期等等;

接下来我们一起学习这个函数的使用:

函数的参数

函数的返回值

setup 函数的参数

我们先来研究一个 setup 函数的参数,它主要有两个参数:

第一个参数:props

第二个参数:context

props 非常好理解,它其实就是父组件传递过来的属性会被放到 props 对象中,我们在setup 中如果需要使用,那么就可以直接通过 props 参数获取:

对于定义 props 的类型,我们还是和之前的规则是一样的,在 props 选项中定义;

并且在 template 中依然是可以正常去使用 props 中的属性,比如 message;

如果我们在 setup 函数中想要使用 props,那么不可以通过 this 去获取(后面我会讲到为什么);

因为 props 有直接作为参数传递到 setup 函数中,所以我们可以直接通过参数来使用即可;

另外一个参数是context,我们也称之为是一个SetupContext,它里面包含三个属性:

  • attrs:所有的非 prop 的 attribute;
  • slots:父组件传递过来的插槽(这个在以渲染函数返回时会有作用,后面会讲到);
  • emit:当我们组件内部需要发出事件时会用到 emit(因为我们不能访问 this,所以不可以通过 this.$emit 发出事件);

setup 函数的返回值

  1. 若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注!)
  2. 若返回一个渲染函数:则可以自定义渲染内容。(了解)

注意点:

  1. 尽量不要与 Vue2.x 配置混用

    • Vue2.x 配置(data、methos、computed…)中可以访问到 setup 中的属性、方法。
    • 但在 setup 中不能访问到 Vue2.x 配置(data、methos、computed…)。
    • 如果有重名, setup 优先。
  2. setup 不能是一个 async 函数,因为返回值不再是 return 的对象, 而是 promise, 模板看不到 return 对象中的属性。(后期也可以返回一个 Promise 实例,但需要 Suspense 和异步组件的配合)

另外注意:setup 不可以使用 this

官方关于 this 有这样一段描述

表达的含义是 this 并没有指向当前组件实例;

并且在 setup 被调用之前,data、computed、methods 等都没有被解析;

所以无法在 setup 中获取 this;

OptionsAPI

其实在之前的这段描述是和源码有出入的:

之前的描述大概含义是不可以使用 this 是因为组件实例还没有被创建出来;

通过阅读源码发现,代码是按照如下顺序执行的:

调用 createComponentInstance 创建组件实 例;

调用 setupComponent 初始化 component 内 部的操作;

调用 setupStatefulComponent 初始化有状态的组件;

在 setupStatefulComponent 取出了 setup 函 数;

通过 callWithErrorHandling 的函数执行 setup;

从上面的代码我们可以看出, 组件的 instance 肯定是在执行 setup 函数之前就创建出来了。

OptionsAPI

响应式原理

Vue2

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// vue2.x 双向数据绑定原理是通过Objcet.defineProperty来实现的
// 这种方式有缺点: 数组的长度 数组的内容发生变化检测不到

let obj = {
name: "zs",
age: 18,
phone: {
name: "iphone",
},
};
// let obj = [1, 2, 3, 4, 5]
// 视图更新的方法
function render() {
console.log("视图更新了 ~");
}

let methods = ["push", "pop", "shift", "unshift", "sort", "reverse", "splice"];
// 将数组的原型保存一份
let arrProto = Array.prototype;
// 创建原型 将数组原型拷贝一份
let proto = Object.create(arrProto);
// 重写数组的方法
methods.forEach((method) => {
proto[method] = function () {
// AOP 面向切面编程
// 改变了数组的数据
arrProto[method].call(this, ...arguments);
// 重新渲染视图
render();
};
});

// 观察者模式
function observe(obj) {
// 判断一个对象是不是数组
// if(Object.prototype.toString.call(obj) === '[object Array]'){
if (Array.isArray(obj)) {
// 让观测的对象的原型和我们自己重写的原型建立关系
obj.__proto__ = proto;
return;
}
// 如果观察的是一个对象的话 对其属性进行响应式监听(set、get)
if (Object.prototype.toString.call(obj) === "[object Object]") {
// 取出对象中的每一个键和值
for (let key in obj) {
// 调用响应式处理函数
defineReactive(obj, key, obj[key]);
}
}
}
// 观察obj对象
observe(obj);

// 响应式处理
// Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
function defineReactive(obj, key, val) {
// 检测对象的属性值是否还是对象
observe(val);
Object.defineProperty(obj, key, {
// 获取
get() {
return val;
},
// 设置
set(newVal) {
// 检测设置的值是不是对象
observe(newVal);
// 新值和旧值不相等的时候
if (newVal !== val) {
// 覆盖掉原来的val值
val = newVal;
// 通知render函数重新渲染视图
render();
}
},
});
}

// obj.name = 'lisi'
// obj.age = 20
// console.log(obj.name, obj.age);
// 至此 基本可以实现数据发生变化 视图更新的效果
// 但是 如果数据有多层呢 也就是属性对应的值也是对象?
// obj.phone.name = 'huawei'
// console.log(obj.phone.name);

// 修改的值和原来的值一样 不需要重新渲染视图
// obj.phone.name = 'iphone'

// 如果重新为对象obj的phone赋值一个新的对象呢? 视图要重新渲染
// obj.phone = {name: 'huawei'}

// 并且当修改新的对象的属性值时 视图也要重新渲染
// obj.phone.name = 'zs'

// 为对象新增属性值呢? 也是没有办法监测到的
// obj.sex = 'man'
// $set来解决这个
function $myset(obj, key, val) {
if (Array.isArray(obj)) {
return obj.splice(key, val);
}
defineReactive(obj, key, val);
}
// $myset(obj, 'sex', 'man')
// 修改新增的属性值 视图也能更新
// obj.sex = 'woman'

// obj.phone = Object.assign(obj.phone, {price: '666'})
// obj.phone.price = 888

// obj.phone = {...obj.phone, ...{price: '666'}}
// obj.phone.price = '888'
// 以后vue涉及到给data中的对象新增属性时 有三种方式
// 1、this.$set() || Vue.set()
// 2、obj = Object.assign(原对象, 新对象(新增的属性:值))
// 2、obj = {...原对象, ...新对象(新增的属性:值)}

// =================== 数组 ======================
// 数组的响应式数据处理依赖的并不是Object.defineProperty 而是对数组的能够引起数据变化的方法进行重写
// obj.push(6)
// obj.length ++
// obj[0] = 88
// $myset(obj, '0', 66)

Vue3

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
// vue3响应式原理利用了es6提供的proxy Api
// 可以直接监听到对象和数组内容及长度的变化
// 缺点: 兼容性差 支持就用proxy 不支持就用Objcet.defineProperty
let obj = {
name: "zs",
girlFriend: { name: "刘亦菲" },
arr: [1, 2, 3],
};

function render() {
console.log("视图重新渲染 ~");
}

let handler = {
get(target, key) {
// Reflect.get(target, propertyKey[, receiver])
// 获取对象身上某个属性的值,类似于 target[name]。
// 如果属性的值是一个对象的话 则重新进行代理 设置set&get
if (typeof target[key] == "object" && target[key] != null) {
return new Proxy(target[key], handler);
}
return Reflect.get(target, key);
},
set(target, key, val) {
// if(target[key] == 'length') return true
Reflect.set(target, key, val);
render();
// return true表示赋值成功 如果不返回true 在严格模式下可能会报TypeError
return true;
},
};

let proxy = new Proxy(obj, handler);
// 使用代理后的对象
// console.log(proxy.name);
// console.log(proxy.grilFriend);

// proxy.name = 'lisi'
// 修改深层次的对象的属性 无法监听到
// proxy.grilFriend.name = '黄圣依'
// console.log(proxy.name);
// console.log(proxy.grilFriend);
// console.log(obj.name);
// console.log(obj.grilFriend);

proxy.arr[0] = "11";
proxy.arr.length++;
// console.log(proxy.arr.length);
console.log(obj.arr.length);

认识ref全家桶

ref : 接受一个内部值并返回一个响应式且可变的ref对象。ref对象仅有一个.value property,指向该内部值。

isRef : 判断是不是一个ref对象

shallowRef : 创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的

triggerRef : 强制更新页面DOM — 这样也是可以改变值的

customRef : 自定义ref — customRef 是个工厂函数要求我们返回一个对象 并且实现 get 和 set 适合去做防抖之类的

reactive 函数

  • 作用: 定义一个对象类型的响应式数据(基本类型不要用它,控制台会报警告,要用 ref 函数)
  • 语法:const 代理对象= reactive(源对象)
    • 接收一个对象(或数组),返回一个代理对象(Proxy 的实例对象,简称 proxy 对象)
  • reactive 定义的响应式数据是“深层次的”。
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作。

为什么就可以变成响应式的呢?

这是因为当我们使用 reactive 函数处理我们的数据之后,数据再次被使用时就会进行依赖收集;

当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面);

事实上,我们编写的 data 选项,也是在内部交给了 reactive 函数将其变成响应式对象的;