Composition API
# 一、认识Mixin
目前我们是使用组件化的方式在开发整个Vue的应用程序,但是组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。
在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成:
Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能;
一个Mixin对象可以包含任何组件选项;
当组件使用Mixin对象时,所有Mixin对象的选项将被 混合 进入该组件本身的选项中;
# 1、Mixin的合并规则
如果Mixin对象中的选项和组件对象中的选项发生了冲突,那么Vue会如何操作呢?这里分成不同的情况来进行处理;
情况一:如果是data函数的返回值对象
返回值对象默认情况下会进行合并;
如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据;
情况二:如何生命周期钩子函数
生命周期的钩子函数会被合并到数组中,都会被调用;
情况三:值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。
比如都有methods选项,并且都定义了方法,那么它们都会生效;
但是如果对象的key相同,那么会取组件对象的键值对;
# 2、全局混入Mixin
如果组件中的某些选项,是所有的组件都需要拥有的,那么这个时候我们可以使用全局的mixin:
全局的Mixin可以使用 应用app的方法 mixin 来完成注册;
一旦注册,那么全局混入的选项将会影响每一个组件;
import { demoMixin } from './mixins/demoMixin';
export default {
mixins: [demoMixin], ...
}
2
3
4
5
# 二、extends
另外一个类似于Mixin的方式是通过extends属性:
- 允许声明扩展另外一个组件,类似于Mixins;
在开发中extends用的非常少,在Vue2中比较推荐大家使用Mixin,而在Vue3中推荐使用Composition API。
# 三、认识Composition API
# 1、Options API的弊端
在Vue2中,我们编写组件的方式是Options API:
Options API的一大特点就是在对应的属性中编写对应的功能模块;
比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变,也包括生命周期钩子;
但是这种代码有一个很大的弊端:
当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中;
当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散;
尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人);
下面我们来看一个非常大的组件,其中的逻辑功能按照颜色进行了划分:
这种碎片化的代码使用理解和维护这个复杂的组件变得异常困难,并且隐藏了潜在的逻辑问题;
并且当我们处理单个逻辑关注点时,需要不断的跳到相应的代码块中;
那么既然知道Composition API想要帮助我们做什么事情,接下来看一下到底是怎么做呢?
为了开始使用Composition API,我们需要有一个可以实际使用它(编写代码)的地方; 在Vue组件中,这个位置就是 setup 函数;
setup其实就是组件的另外一个选项:
只不过这个选项强大到我们可以用它来替代之前所编写的大部分其他选项; 比如methods、computed、watch、data、生命周期等等;
接下来我们一起学习这个函数的使用:
函数的参数
函数的返回值
# 2、setup函数的参数
我们先来研究一个setup函数的参数,它主要有两个参数:
第一个参数:props:
props
是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。如果需要解构 prop,可以在setup
函数中使用toRefs
(opens new window) 函数来完成此操作第二个参数:context :
context
是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对context
使用 ES6 解构。
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发出事件);
# 3、setup函数的返回值
setup既然是一个函数,那么它也可以有返回值,它的返回值用来做什么呢?
setup的返回值可以在模板template中被使用;
也就是说我们可以通过setup的返回值来替代data选项;
甚至是我们可以返回一个执行函数来代替在methods中定义的方法:
/**
* 参数一: props, 父组件传递过来属性
*/
// setup函数有哪些参数?
// setup函数有什么样的返回值
// setup(props, context) {
setup(props, {attrs, slots, emit}) { // 解构出来
console.log(props.message);
console.log(attrs.id, attrs.class);
console.log(slots);
console.log(emit);
return {
title: "Hello Home",
counter: 100
}
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 4、setup不可以使用this
在 setup()
内部,this
不是该活跃实例的引用,因为 setup()
是在解析其它组件选项之前被调用的,所以 setup()
内部的 this
的行为与其它选项中的 this
完全不同。这使得 setup()
在和其它选项式 API 一起使用时可能会导致混淆。
Setup | Vue.js (vuejs.org) (opens new window)
# 5、setup中使用ref
<div>
<h2 ref="title">哈哈哈</h2>
</div>
setup() {
const title = ref(null);
watchEffect(() => {
console.log(title.value);
}, {
flush: "post" // 默认为pre, post为挂载后执行
})
return {
title
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 四、Reactive API
如果想为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive的函数
那么这是什么原因呢?为什么就可以变成响应式的呢?
- 这是因为当我们使用reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集;
- 当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面);
- 事实上,我们编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的;
# Reactive判断的API
isProxy
检查对象是否是由 reactive 或 readonly创建的 proxy。
isReactive
检查对象是否是由 reactive创建的响应式代理:
如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true;
isReadonly
检查对象是否是由 readonly 创建的只读代理。
toRaw
返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。
shallowReactive
创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。
shallowReadonly
创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。
# 五、Ref API
reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型:
- 如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告
这个时候Vue3给我们提供了另外一个API:ref API
ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源;
它内部的值是在ref的 value 属性中被维护的;
这里有两个注意事项:
在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用;
但是在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式
# 1、Ref自动解包
模板中的解包是浅层的解包,如果我们的代码是下面的方式:
如果我们将ref放到一个reactive的属性当中,那么在模板中使用时,它会自动解包
<template>
<div>
Home Page
<h2>{{message}}</h2>
<!-- 当我们在template模板中使用ref对象, 它会自动进行解包 -->
<h2>当前计数: {{counter}}</h2>
<!-- ref的解包只能是一个浅层解包(info是一个普通的JavaScript对象) -->
<h2>当前计数: {{info.counter.value}}</h2>
<!-- 当如果最外层包裹的是一个reactive可响应式对象, 那么内部的ref可以解包 -->
<h2>当前计数: {{reactiveInfo.counter}}</h2>
<button @click="increment">+1</button>
</div>
</template>
<script>
import { ref, reactive } from 'vue';
export default {
props: {
message: {
type: String,
required: true
}
},
setup() {
let counter = ref(100);
const info = {
counter
}
const reactiveInfo = reactive({
counter
})
// 局部函数
const increment = () => {
counter.value++;
console.log(counter.value);
}
return {
counter,
info,
reactiveInfo,
increment
}
}
}
</script>
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
# 2、toRefs
如果我们使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改reactive返回的state对象**,**数据都不再是响应式的:
那么有没有办法让我们解构出来的属性是响应式的呢?
Vue为我们提供了一个
toRefs
的函数,可以将reactive返回的对象中的属性都转成ref;那么我们再次进行结构出来的 name 和 age 本身都是 ref的;
这种做法相当于已经在state.name和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化
# 3、toRef
如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法:
import { reactive, toRefs, toRef } from 'vue';
export default {
setup() {
const info = reactive({name: "why", age: 18});
// 1.toRefs: 将reactive对象中的所有属性都转成ref, 建立链接
// let { name, age } = toRefs(info);
// 2.toRef: 对其中一个属性进行转换ref, 建立链接
let { name } = info;
let age = toRef(info, "age");
const changeAge = () => {
age.value++;
}
return {
name,
age,
changeAge
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 4、ref其他的API
# unref
如果我们想要获取一个ref引用中的value,那么也可以通过unref方法:
如果参数是一个 ref,则返回内部值,否则返回参数本身;
这是 val = isRef(val) ? val.value : val 的语法糖函数;
# isRef
判断值是否是一个ref对象。
# shallowRef
创建一个浅层的ref对象;
# triggerRef
手动触发和 shallowRef 相关联的副作用:
# 5、customRef
创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制:
它需要一个工厂函数,该函数接受 track 和 trigger 函数作为参数;
并且应该返回一个带有 get 和 set 的对象;
这里我们使用一个的案例:
对双向绑定的属性进行debounce(节流)的操作;
import { customRef } from 'vue';
// 自定义ref
export default function(value, delay = 300) {
let timer = null;
return customRef((track, trigger) => {
return {
get() {
track(); // 收集依赖
return value;
},
set(newValue) {
clearTimeout(timer);
timer = setTimeout(() => {
value = newValue;
trigger(); // 触发依赖更新
}, delay);
}
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 六、认识readonly
我们通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用,但是不能被修改,这个时候如何防止这种情况的出现呢?
Vue3为我们提供了readonly的方法;
readonly会返回原生对象的只读代理(也就是它依然是一个Proxy,这是一个proxy的set方法被劫持,并且不能对其进行修改);
在开发中常见的readonly方法会传入三个类型的参数:
类型一:普通对象;
类型二:reactive返回的对象;
类型三:ref的对象;
在readonly的使用过程中,有如下规则:
readonly返回的对象都是不允许修改的;
但是经过readonly处理的原来的对象是允许被修改的;
比如 const info = readonly(obj),info对象是不允许被修改的;当obj被修改时,readonly返回的info对象也会被修改;但是我们不能去修改readonly返回的对象info;
- 其实本质上就是readonly返回的对象的setter方法被劫持了而已;
那么这个readonly有什么用呢?
在我们传递给其他组件数据时,往往希望其他组件使用我们传递的内容,但是不允许它们修改时,就可以使用readonly了
# couputed
在前面我们讲解过计算属性computed:当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理
在前面的Options API中,我们是使用computed选项来完成的;
在Composition API中,我们可以在 setup 函数中使用 computed 方法来编写一个计算属性;
如何使用computed呢?
方式一:接收一个getter函数,并为 getter 函数返回的值,返回一个不变的 ref 对象;
方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象;
# 侦听数据的变化
在前面的Options API中,我们可以通过watch选项来侦听data或者props的数据变化,当数据变化时执行某一些操作。
在Composition API中,我们可以使用watchEffect和watch来完成响应式数据的侦听;
watchEffect用于自动收集响应式数据的依赖;
watch需要手动指定侦听的数据源;
# watchEffect自动收集依赖
当侦听到某些响应式数据变化时,我们希望执行某些操作,这个时候可以使用 watchEffect。
n 我们来看一个案例:
首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖;
其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行;
watchEffect的停止侦听
如果在发生某些情况下,我们希望停止侦听,这个时候我们可以获取watchEffect的返回值函数,调用该函数即可。
比如在上面的案例中,我们age达到20的时候就停止侦听:
watchEffect清除副作用
什么是清除副作用呢?比如在开发中我们需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数被再次执行了。那么上一次的网络请求应该被取消掉,这个时候我们就可以清除上一次的副作用;
在我们给watchEffect传入的函数被回调时,其实可以获取到一个参数:onInvalidate
当副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数;
我们可以在传入的回调函数中,执行一些清楚工作
const stop = watchEffect((onInvalidate) => {
const timer = setTimeout(() => {
console.log("网络请求成功~");
}, 2000)
// 根据name和age两个变量发送网络请求
onInvalidate(() => {
// 在这个函数中清除额外的副作用
// request.cancel()
clearTimeout(timer);
console.log("onInvalidate");
})
console.log("name:", name.value, "age:", age.value); // 自动搜集依赖进行监听
});
2
3
4
5
6
7
8
9
10
11
12
13
14
调整watchEffect的执行时机
如果我们希望在第一次的时候就打印出来对应的元素呢?
这个时候我们需要改变副作用函数的执行时机; 它的默认值是pre
,它会在元素 挂载 或者 更新 之前执行;
p所以我们会先打印出来一个空的,当依赖的title发生改变时,就会再次执行一次,打印出元素;
setup() {
const title = ref(null);
watchEffect(() => {
console.log(title.value);
}, {
flush: "post" // 默认为pre, post为挂载后执行
})
return {
title
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# Watch的使用
watch的API完全等同于组件watch选项的Property,源码见: apiWatch.ts
watch需要侦听特定的数据源,并在回调函数中执行副作用;
默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调;
n 与watchEffect的比较,watch允许我们:
懒执行副作用(第一次不会直接执行);
更具体的说明当哪些状态发生变化时,触发侦听器的执行;
访问侦听状态变化前后的值;
# 侦听单个数据源
watch侦听函数的数据源有两种类型:
一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref);
直接写入一个可响应式的对象,reactive或者ref(比较常用的是ref);
# 侦听多个数据源
# 侦听响应式对象
# watch的选项
如果我们希望侦听一个深层的侦听,那么依然需要设置 deep
为true,(传入reactive时默认deep为true) 也可以传入immediate
立即执行
setup() {
const info = reactive({name: "why", age: 18});
// 1.侦听watch时,传入一个getter函数
watch(() => info.name, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
// 2.传入一个可响应式对象: reactive对象/ref对象
// 情况一: reactive对象获取到的newValue和oldValue本身都是reactive对象
// watch(info, (newValue, oldValue) => {
// console.log("newValue:", newValue, "oldValue:", oldValue);
// })
// 如果希望newValue和oldValue是一个普通的对象
watch(() => {
return {...info}
}, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
},{ deep: true, immediate: true})
// 情况二: ref对象获取newValue和oldValue是value值的本身
// const name = ref("why");
// watch(name, (newValue, oldValue) => {
// console.log("newValue:", newValue, "oldValue:", oldValue);
// })
const changeData = () => {
info.name = "kobe";
}
return {
changeData,
info
}
}
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
# 生命周期钩子
我们前面说过 setup 可以用来替代 data 、 methods 、 computed 、watch 等等这些选项,也可以替代 生命周期钩子。
那么setup中如何使用生命周期函数呢?
- 可以使用直接导入的 onX 函数注册生命周期钩子;
- onXX函数可以调用多次
选项式 API | Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated | onActivated |
deactivated | onDeactivated |
因为 setup
是围绕 beforeCreate
和 created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup
函数中编写。
setup() {
const counter = ref(0);
const increment = () => counter.value++
onMounted(() => {
console.log("App Mounted1");
})
onMounted(() => {
console.log("App Mounted2");
})
onUpdated(() => {
console.log("App onUpdated");
})
onUnmounted(() => {
console.log("App onUnmounted");
})
return {
counter,
increment
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Provide函数
事实上我们之前还学习过Provide和Inject,Composition API也可以替代之前的 Provide 和 Inject 的选项。
我们可以通过 provide来提供数据:
可以通过 provide 方法来定义每个 Property;
pprovide可以传入两个参数:
name:提供的属性名称;
value:提供的属性值;
# Inject函数
在后代组件 中可以通过 inject 来注入需要的属性和对应的值:
p可以通过 inject 来注入需要的内容;
inject可以传入两个参数:
要 inject 的 property 的 name;
默认值
import { provide, ref, readonly, inject } from 'vue';
setup() {
const name = ref("coderwhy");
let counter = ref(100);
provide("name", readonly(name)); // 子组件只能获取,不能修改 inject('name')
provide("counter", readonly(counter));
const increment = () => counter.value++;
return {
increment,
counter
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# script setup
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 Typescript 声明 props 和抛出事件。
- 更好的运行时性能 (其模板会被编译成与其同一作用域的渲染函数,没有任何的中间代理)。
- 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。
<script setup>
import { defineProps, defineEmit } from 'vue';
const props = defineProps({
message: {
type: String,
default: "哈哈哈"
}
})
const emit = defineEmit(["increment", "decrement"]);
const emitEvent = () => {
emit('increment', "100000")
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
单文件组件 | Vue.js (vuejs.org) (opens new window)
# 认识h函数
Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时候你可以使用 渲染函数 ,它比模板更接近编译器;
前面我们讲解过VNode和VDOM的改变:
Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM(VDOM);
事实上,我们之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode;
那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode;
n 那么我们应该怎么来做呢?使用 h()函数:
h() 函数是一个用于创建 vnode 的一个函数;
其实更准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数;
接收三个参数:
type
(String | Object | Function),props
(Object) 和children
(String | Array | Object)
h函数可以在两个地方使用:
render函数选项中;
setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode);
全局 API | Vue.js (vuejs.org) (opens new window)
<script>
import { ref, h } from 'vue';
export default {
setup() {
const counter = ref(0);
return () => {
return h("div", {class: "app"}, [
h("h2", null, `当前计数: ${counter.value}`),
h("button", {
onClick: () => counter.value++
}, "+1"),
h("button", {
onClick: () => counter.value--
}, "-1"),
])
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# jsx
如果我们希望在项目中使用jsx,那么我们需要添加对jsx的支持:
jsx我们通常会通过Babel来进行转换(React编写的jsx就是通过babel转换的);
对于Vue来说,我们只需要在Babel中配置对应的插件即可;
安装Babel支持Vue的jsx插件:
npm install @vue/babel-plugin-jsx -D
render() {
const increment = () => this.counter++;
const decrement = () => this.counter--;
return (
<div>
<h2>当前计数: {this.counter}</h2>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<HelloWorld>
</HelloWorld>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 案例
# useTitle.js
import { ref, watch } from 'vue';
export default function(title = "默认的title") {
const titleRef = ref(title);
watch(titleRef, (newValue) => {
document.title = newValue
}, {
immediate: true
})
return titleRef
}
2
3
4
5
6
7
8
9
10
11
12
13
# useLocalStorage.js
import { ref, watch } from 'vue';
export default function(key, value) {
const data = ref(value);
if (value) {
window.localStorage.setItem(key, JSON.stringify(value));
} else {
data.value = JSON.parse(window.localStorage.getItem(key));
}
watch(data, (newValue) => {
window.localStorage.setItem(key, JSON.stringify(newValue));
})
return data;
}
// 一个参数: 取值
// const data = useLocalStorage("name");
// // 二个参数: 保存值
// const data = useLocalStorage("name", "coderwhy");
// data.value = "kobe";
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