Vue3.0新特性之Composition API

Vue3.0发布了很多新的特性和一些语法上的变更。其中 Composition APIVue3.0版本中主要特色语法,这是一个全新的逻辑重用和代码组织的方法。这边文章就带你看看为啥会有Composition API以及如何使用Composition API

Option API 和 Composition API 的比较

我们知道Vue2.0(选项)所有数据都定义在data中,方法定义在methods中。所以给组件添加逻辑, 我们可能需要填充(选项)属性data, methods,computed等。但是Vue3.0我们可以不这么写了。具体怎么写, 我们先看看Vue2.0的写法有什么缺陷。

Option API

使用Vue2.0option API实现如下功能:

我们实现一个加减法,之后数值显示出来,就需要分别在data,computed,methods添加代码,如果还有其他功能,可能还得在生命周期选项等添加代码。那如果需要添加其他的需求。我们的代码结构可能就变成这样了:

Composition API

Vue3.0中, 我们可以使用Composition API的方式来实现:

代码按照逻辑(Composition API的方式来实现)来分割,上面的图变成下面的图(相同业务逻辑的代码颜色),是不是就清晰了?

可以看的出来: Option API相同业务的代码分散在各处,这样后期维护起来就很麻烦。而Composition API就解决了这个问题。那么下面就来讲解Composition API怎么使用。

Composition API 的入口及API

关于Composition API有哪些API呢? 先总结一下:

下面根据这些知识点分别来讲解。

setup

setup功能是新的组件选项,是Composition API使用的入口。

执行的的时机

setup是在创建vue组件实例并完成props的初始化之后执行(在beforeCreate钩子之前执行)。这就直接限制了在setup中无法使用其他的选项(option)中的数据;如:data,methodscomputed等。 但是其他的选项(option)可以使用setup中返回的变量。

由于在执行 setup函数的时候,还没有执行 Created 生命周期方法,所以在 setup 函数中,无法使用 data methods 的变量和方法

1
2
3
4
5
6
7
8
9
10
11
export default {
beforeCreate () {
console.log("------beforeCreate------")
},
created () {
console.log("------created------")
},
setup () {
console.log("------setup------");
}
}

打印的结果为:

setup中的上下文

setup中是没有this上下文的.为什么呢? javascript函数中都是应该有this。但是由于 执行的的时机的原因,setup中的thisVue2.x中的this 已经不是一个东西了。所以为了防止错误的使用,直接将this改成了 undefined

由于我们不能在 setup函数中使用datamethods,所以 Vue 为了避免我们错误的使用,直接将 setup函数中的this修改成了 undefined

1
2
3
4
5
export default {
setup () {
console.log("this", this); // undefined
}
}

与模板的使用

setup如果返回的是一个对象的话,那么这个对象的所有属性会合并到template的渲染上下文中(也就是说可以在template中使用setup的返回的对象的属性)。

template中,vue已经帮我们自动获取value属性, 所有我们只需要{{ people.name }},{{ people.age }},{{ sex }}

参数

setup是一个函数,它接受两个参数:propscontext

下面我们一个具体的例子项目来讲解。假如我们有这样的项目:

props

props是父组件传给子组件或者vue-router传给页面的参数。setup中的props 是响应式,当传入新的props,就会被更新。

1
2
3
4
5
6
7
8
9
export default  {
props: {
name: String,
age: Number
},
setup(props) {
console.log(props): // { name, age }
}
}

注意: 因为props是响应式的, 所以不能使用ES6解构,如果这样做将会失去响应性。

如果需要ES6解构并且需要数据的响应性的的话, 可以使用toRefs来完成。下面也会讲到。

1
2
3
4
5
6
7
8
9
10
import { toRefs } from "vue"
export default {
props: {
name: String,
age: Number
},
setup(props) {
const { name, age } = toRefs(props);
}
}

context

context是一个javascript对象,它暴露了3 个propertyattrsslotemit。分别对应Vue2.x$attr属性、slot插槽 和$emit

1
2
3
4
5
6
export default  {
emits: ["updateName"],
setup(props, { emit }) {
emit("updateName", "shuliqi");
}
}

propscontext具体的代码如下:

ref

ref 函数接受一个值,用于初始化的值,然后返回一个响应式且可修改的ref对象。该对象有一个value属性。valus保存ref对象的值。所以修改变量的话需要修改变量的value值。

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

import { ref } from "vue";
export default {
setup() {
// ref 函数返回一个响应式
const sex = ref("女"); // { value: "女"}
console.log("sex", sex);

const isOk = ref(false); // { value: false }
console.log("isOk", isOk);

const tag = ref(null); // { value: null }
console.log("tag", tag);

const people = ref({ // { value: { name: "shuliqi", age: 12 }}
name: "shuliqi",
age: 12
});
console.log("people", people);
setTimeout(() => {
// 修改变量需要修改对象的value值
people.value.name = "zhangdada";
console.log("更新后的people", people)
}, 1000)
}
}

我们看看这段代码打印的结果:

说明:

  • 使用ref初始化的变量都是一个对象(ref 函数接受一个值,用于初始化的值,然后返回一个响应式且可修改的ref对象)。valus保存ref对象的值。
  • setTimeout时候, 想要修改people这个响应式对象的值,则需要通过赋值操作 people.value.name = "zhangdada"来实现。

再继续看视图页面

## reactive

reactiveref 很相似。 也是一个函数,但是只接受一个对象。并返回一个对这个对象的响应式代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { reactive, toRefs }  from "vue";
export default {
setup() {
// 使用 reactive 初始化一个变量
const state = reactive({
name: "shuliqi",
age: 12,
sex: "女"
})
setTimeout(() => {
// reactive 中的变量 的取值和赋值不需要 取其 value 属性
state.name = "更改之后的名字";
console.log("更新后的state的name值", state.name)
}, 2000);

return {
// ...state 会失去响应性
...toRefs(state) // 会保留响应性
}
}

我们看打印的结果:

再看看template中的使用:

最后可得出结论:

  • reactive函数返回一个对这个对象的响应式代理
  • reactive 可以将零散的变量聚焦在一个对象
  • reactive中的变量的取值和赋值不需要取其 value

注意的点:使用reactive时记得使用toRefs保证reactive对象属性保持响应性。

isRef

isRef用于判断变量是为ref对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ref, reactive, isRef } from "vue";
export default {
setup (){
const name = ref("shuliqi");
const people = reactive({
age: 12,
sex: "女"
});
console.log(isRef(name)); // true
console.log(isRef(name) ? name.value : name); // shuliqi
console.log(isRef(people)) // false
console.log(isRef(people) ? people.value.age : people.age) // 12
}
}

toRefs

toRefs 用于将一个reactive对象转化为属性为ref对象的普通对象。

1
2
3
4
5
6
7
8
9
10
11
export default {
setup (){
const people = reactive({
age: 12,
sex: "女"
});
const { age, sex } = toRefs(people);
console.log(isRef(age), isRef(sex)) // true true
console.log(age.value, sex.value) // 12 shuliqi
}
}

watch

watch与选项式API(this.$watch/watch选项)完全等效的。watch需要监听特定的data数据源,并且在单独的回调函数中副作用。默认的情况下,是惰性的,也就是说回调函数仅在监听源数据发生变更时才会回调。

语法:

1
watch(source, callback, [options])

参数说明:

  • source:要监听的响应式变量。支持String,Object,Function, Array
  • callback: 要执行的回调函数。回调函数的第一个参数是监听的数据更新后的值, 第二个参数之前监听的数据之前得值。
  • options: 支持deep、immediate 和 flush 选项。
    • 当我们监听复杂的嵌套数据对象的时候,需要传入第三个参数的deep:true。这个参数的意思是进行深拷贝,目的是为了真正的监听复杂数据对象
    • 希望watch 不是惰性的(立即执行回调函数)可以设置immediate: true

监听ref定义的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ref, watch } from "vue";
export default {
setup () {
const name = ref("shuliqi");
setTimeout(() => {
name.value = "张大大";
}, 1000);
watch(name, (newName,oldName) => {
console.log("发生了变化:", "newName:", newName, "oldName:", oldName )
})
}
}

// 发生了变化: newName: 张大大 oldName: shuliqi

监听reactive定义的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

import { watch, reactive} from "vue";
export default {
setup () {
// 监听`reactive`定义的数据
const obj = reactive({
name: "shuliqi",
age: 12
})
setTimeout(() => {
obj.name = "张大大"
}, 500)
watch(() => obj.name, (newName,oldName) => {
console.log("发生了变化:", "newName:", newName, "oldName:", oldName)
})

}
}
// 发生了变化: newName: 张大大 oldName: shuliqi

监听多个数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ref, watch } from "vue";
export default {
setup () {
// 监听多个数据源
const age = ref(12);
const sex = ref("女");
setTimeout(() => {
sex.value = "男";
})
watch([age, sex], (newName,oldName) => {
console.log("发生了变化:", "newName", newName, "oldName", oldName)
})
}
}

监听复杂的嵌套数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { watch, reactive} from "vue";
export default {
setup () {
// 监听复杂的嵌套数据
const state = reactive({
total: 12,
data: {
titile: "标题",
result: {
nane: "shuliqi",
age: 12
}
}
});
setTimeout(() => {
state.data.result.name = "张大大";
}, 500)
watch(() => state.data, (newName,oldName) => {
console.log("发生了变化:", "newName:", newName, "oldName:", oldName)
}, {
deep: true
})
}
}

注意:当我们监听复杂的嵌套数据对象的时候,需要传入第三个参数的deep:true。这个参数的意思是进行深拷贝,目的是为了真正的监听复杂数据对象。

设置watch为立即执行

设置watch的第三个参数:immediate: true。 则watch不是惰性的,即立即执行回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
  import { watch, ref } from "vue"; 
export default {
setup () {
// 设置`watch`为立即执行
const sex = ref("女");
watch(sex, (newName, oldName) => {
console.log("我不是惰性的:", "newName:", newName, "oldName:", oldName)
}, { immediate: true })
}

}
// 我不是惰性的: newName: 女 oldName: undefined

停止监听

组件中创建的watch监听,会在组件被销毁的时候自动停止,如果在组件销毁之前我们想停止某个监听,可以调用watch()函数的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { watch, ref } from "vue";
export default {
setup () {
// 停止监听示例
const age = ref(12);
const stop = watch(age, (newName, oldName) => {
console.log("我不是惰性的:", "newName:", newName, "oldName:", oldName)
});
// 停止监听
stop();
setTimeout(() => {
age.value = 27;
}, 3000)
}
}

结果: 不会监听到变化了

不能监听非响应式的值

1
2
3
4
5
6
7
8
9
10
11
// 不能监听非响应式的值
let age = 0;
setTimeout(() => {
age++;
console.log("改变值", age);

}, 1000);
watch(() => age, () => {
console.log("asdas");
})
// 改变值 1

watchEffect

watchEffectwatch类似,不过watchEffect函数会自动收集依赖。 只需要指定一个回调函数。在组件初始化的时候,会先执行一次手机依赖。然后当收集到的依赖中数据发生变化时,会再次执行回调函数。有以下特点:

  • watchEffect不需要手动传入依赖(不需要手动传入需要监听的值);
  • watchEffect会先执行一次用来自动收集依赖;
  • watchEffect无法获取到变化前的值,只能获取到变化后的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { ref, watchEffect } from "vue";
export default {
setup () {
const name = ref("shuliqi");
const age = ref(0);
setTimeout(() => {
name.value = "张大大";
age.value = 20;
}, 1000);
watchEffect(() => {
console.log("name值:", name.value, "age值:", age.value);
});

}
}
// name值: shuliqi age值: 0
// watchEffect.vue?25bf:15 name值: 张大大 age值: 20

结果可以看出来:组件初始化的时候先执行一次回调函数,收集依赖。1秒之后,依赖发生变化。回调函数会再次被调用。

停止监听

watch的停止监听一样。组件中创建的watchEffect监听,会在组件被销毁的时候自动停止,如果在组件销毁之前我们想停止某个监听,可以调用watchEffect()函数的返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { ref, watchEffect } from "vue";
export default {
setup () {
const name = ref("shuliqi");
const age = ref(0);
setTimeout(() => {
name.value = "张大大";
age.value = 20;
}, 1000);
const stop = watchEffect(() => {
console.log("name值:", name.value, "age值:", age.value);
});
// 停止监听
stop();
}
}
// name值: shuliqi age值: 0

回调函数只会在组件初始化的时候回调一次。因为停止了监听stop();所以依赖发生了变化也不会再触发回调了。

不能监听非响应式的值

1
2
3
4
5
6
7
8
9
10
11
12
13
{
// 不能监听非响应式的值
let age = 0;
setTimeout(() => {
age++;
console.log("改变值", age);

}, 1000);
watchEffect(() => age, () => {
console.log("asdas");
})
}
// 改变值 1

computed

computed函数与Vue.x的中的computed功能一样。computed接受一个函数并返回一个value值为getter返回值不可改变的响应式ref对象。

1
2
3
4
5
6
7
8
9
10
11
12
import { ref, computed, isRef } from "vue";
export default {
setup () {
const age = ref(0);
// computed函数的返回值是响应式的ref对象
const myComputedAge = computed(() => age.value + 1);
console.log(isRef(myComputedAge), myComputedAge.value); // true 1

// omputed函数的返回值是不可改变的
myComputedAge.value = 3; // Write operation failed: computed value is readonly
}
}

readonly

readonly获取一个对象(响应式/纯对象)并返回原始代理的只读代理,只读代理是深层次的:访问的任何嵌套的property也是只读的。返回的代理对象不可改变。但是传入的原始对象改变时,返回的代理对象也会相应的改变。如果传入的是ref对象或者reactive对象。那么返回的代理对象也是响应式的。

readonly 响应式对象

ref对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ref, readonly } from "vue";
export default {
setup () {
const name = ref("shuliqi");
const readonlyName = readonly(name);
console.log("读取只读代理的值:",readonlyName.value ); // 读取只读代理的值: shuliqi
// 尝试修改会直接警告
readonlyName.value = "张大大"; // Set operation on key "value" failed: target is readonly.
setTimeout(() => {
name.value = "小小舒";
console.log("原始ref对象改变,只读代理的值也会变:",readonlyName.value); // 原始ref对象改变,只读代理的值也会变:小小舒
}, 3000)
}
}

reactive对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { readonly, reactive } from "vue";
export default {
setup () {
const state = reactive({
title: "标题",
subTitle: "二级标题"
})
const readonlyState = readonly(state);
console.log("读取只读代理的值(reactive):",readonlyState.title ); // 读取只读代理的值(reactive):标题
// 尝试修改会直接警告
readonlyState.title = "修改标题"; // Set operation on key "title" failed: target is readonly

setTimeout(() => {
state.title = "标题被修改了";
console.log("原始reactive对象改变,只读代理的值也会变(reactive):",readonlyState.title);
// 原始reactive对象改变,只读代理的值也会变(reactive):标题被修改了
}, 3000)
}
}

重要:readonly 响应式对象.只读代理是具有响应性的:

readonly 普调对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { readonly } from "vue";
export default {
setup () {
const obj = {
size: 12,
isNew: true,
}
const readonlyObj = readonly(obj);
console.log("读取readonly一个普调对象的只读代理的值:",readonlyObj );
// 读取readonly一个普调对象的只读代理的值:{ size: 12, isNew: true }


// 尝试修改 readonlyObj, 直接警告
readonlyObj.size = 100; // Set operation on key "size" failed: target is readonly
// 修改原始数据
obj.size = 3000;
console.log("原始数据obj改变, readonlyObj也会改变", readonlyObj);
// 原始数据obj改变, readonlyObj也会改变 { size: 3000, isNew: true }
}
}

readonly 一个非对象

1
2
3
4
5
6
7
8
9
10
11
12
import { readonly } from "vue";
export default {
setup () {
// readonly 一个非对象: 就不具有只读特性了
const str = "oldDate";
let readonlyStr = readonly(str);
console.log("readonlyStr:",readonlyStr)
// 可以直接修改,说明不具有只读特性
readonlyStr = "newDate";
console.log("newReadonlyStr:",readonlyStr)
}
}

生命周期

Composition API有人提供了组件生命周期的钩子回调。我们先看vue3.0生命周期图:

我们可以看出,vue3.0增加了setup,然后为了更加语义化将beforeDestroy改成了beforeUnmountdestotredy改成了unmounted。其他Vue2中的生命周期仍然保留。这个图没有显示全部的声声明周期。我们看看全部的生命周期的钩子图示。

beforeCreatecreatedsetup替换了(但是Vue3中你仍然可以使用, 因为Vue3是向下兼容的, 也就是你实际使用的是vue2的)。钩子命名都增加了on; Vue3.x还新增用于调试的钩子函数数onRenderTriggeredonRenderTricked

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

import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
onRenderTracked,
onRenderTriggered
} from "vue";
export default {
beforeCreate () {
console.log("beforecreate")
},
created () {
console.log("created")
},
setup () {
onBeforeMount(() => {
console.log("Composition API: onBeforeMount")
});

onMounted(() => {
console.log("Composition API: onMounted")
});

onBeforeUpdate(() => {
console.log("Composition API: onBeforeUpdate")
})
// ...

},
// ...
}

最后

当然关于Composition API的还有很多的钩子,具体可以看中文官网 组合式 API? 英文官网:Composition API?

最后关于以上的例子的github链接: vue3.0–Composition-API

通过修改src/main.js中的不同的模板来看不同的API 示例

文章作者: 舒小琦
文章链接: https://shuliqi.github.io/2021/05/08/Vue3-0新特性之Composition-API/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 舒小琦的Blog