官方文档: https://v3.cn.vuejs.org/guide/composition-api-introduction.html
组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API:
-
响应式 API:例如
ref()
和reactive()
,使我们可以直接创建响应式状态、计算属性和侦听器。 -
生命周期钩子:例如
onMounted()
和onUnmounted()
,使我们可以在组件各个生命周期阶段添加逻辑。 -
依赖注入:例如
provide()
和inject()
,使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统。
组合式 API 是 Vue 3 及 Vue 2.7 的内置功能。对于更老的 Vue 2 版本,可以使用官方维护的插件 @vue/composition-api
。在 Vue 3 中,组合式 API 基本上都会配合 <script setup>
语法在单文件组件中使用。下面是一个使用组合式 API 的组件示例:
<script setup> import { ref, onMounted } from 'vue' // 响应式状态 const count = ref(0) // 更改状态、触发更新的函数 function increment() { count.value++ } // 生命周期钩子 onMounted(() => { console.log(`计数器初始值为 ${count.value}。`) }) </script> <template> <button @click="increment">点击了:{{ count }} 次</button> </template>
虽然这套 API 的风格是基于函数的组合,但组合式 API 并不是函数式编程。组合式 API 是以 Vue 中数据可变的、细粒度的响应性系统为基础的,而函数式编程通常强调数据不可变。
如果你对如何通过组合式 API 使用 Vue 感兴趣,可以通过页面左侧边栏上方的开关将 API 偏好切换到组合式 API,然后重新从头阅读指引。
1.1.setup
-
理解:Vue3.0中一个新的配置项,值为一个函数。
-
setup是所有Composition API(组合API)“ 表演的舞台 ”。
-
组件中所用到的:数据、方法等等,均要配置在setup中。
-
setup函数的两种返回值:
-
若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注!)
-
若返回一个渲染函数:则可以自定义渲染内容。(了解)
-
-
注意点:
-
尽量不要与Vue2.x配置混用
-
Vue2.x配置(data、methos、computed...)中可以访问到setup中的属性、方法。
-
但在setup中不能访问到Vue2.x配置(data、methos、computed...)。
-
如果有重名, setup优先。
-
-
setup一般不能是一个async函数,因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)
简化的setup函数:
<template> <h2>{{ username }}</h2> <button @click="update">update</button> </template> <script lang="ts" setup> import { h } from "vue"; let username = "tom"; let update = () => { console.log("update2"); }; </script> <style></style>
返回一个渲染函数,注意会替换页面中的内容,很少使用,慎用。
<template> <h2>123</h2> <button>update</button> </template> <script lang="ts"> import { h } from "vue"; export default { setup() { let username = "tom"; let update = () => { console.log("update"); }; return () => h("h2", "Hello Setup!"); }, }; </script> <style></style>
文本与按钮被替换了。
Vue3兼容多数Vue2的内容
<template> <h2>{{ user }}</h2> <button @click="change">更新</button> </template> <script lang="ts"> import { h } from "vue"; export default { data() { return { user: "tom", }; }, methods: { change() { this.user += "!"; }, }, }; </script> <style></style>
可以混用,但不建议这么做
<template> <h2>{{ user }}</h2> <button @click="change">更新姓名</button> <hr /> <h2>{{ age }}</h2> <button @click="update">更新年龄</button> </template> <script lang="ts"> import { ref } from "vue"; export default { data() { return { user: "tom", }; }, methods: { change() { this.user += "!"; }, }, setup() { let age = ref(18); let update = () => { age.value++; }; return { age, update }; }, }; </script> <style></style>
Vue2.x配置(data、methos、computed...)中可以访问到setup中的属性、方法。
<template> <h2>{{ user }}</h2> <button @click="change">methods中的事件,访问setup中的成员</button> </template> <script lang="ts"> import { ref } from "vue"; export default { data() { return { user: "tom", }; }, methods: { change() { this.user += this.age; }, }, setup() { let age = 18; return { age }; }, }; </script> <style></style>
但在setup中不能访问到Vue2.x配置(data、methos、computed...)。
<template> <h2>{{ user }}</h2> <button @click="update">setup中的事件,访问data中的数据</button> </template> <script lang="ts"> import { ref } from "vue"; export default { data() { return { user: "tom", }; }, setup() { function update(this: any) { this.user += "!!!"; console.log(this.user); } return { update }; }, }; </script> <style></style>
如果有重名, setup优先。
<template> <h2>{{ user }}</h2> </template> <script lang="ts"> import { ref } from "vue"; export default { data() { return { user: "tom", }; }, setup() { return { user: "jack" }; }, }; </script> <style></style>
setup一般不能是一个async函数
<template> <h2>{{ user }}</h2> </template> <script lang="ts"> import { ref } from "vue"; export default { data() { return { user: "tom", }; }, setup() { async function f1() { return { a: 100 }; } console.log(f1()); return { user: "jack" }; }, }; </script> <style></style>
1.2.ref函数
-
作用: 定义一个响应式的数据
-
语法:
const xxx = ref(initValue)
-
创建一个包含响应式数据的引用对象(reference对象,简称ref对象)。
-
JS中操作数据:
xxx.value
-
模板中读取数据: 不需要.value,直接:
<div>{{xxx}}</div>
- ref对应的接口是interface Ref<T>
-
-
备注:
-
接收的数据可以是:基本类型、也可以是对象类型,不建议使用对象数据类型。
-
基本类型的数据:响应式依然是靠
Object.defineProperty()
的get
与set
完成的。 -
对象类型的数据:内部 “ 求助 ” 了Vue3.0中的一个新函数——
reactive
函数。 -
isRef 判断是不是一个ref对象
-
import { ref, Ref,isRef } from 'vue' let message: Ref<string | number> = ref("我是message") let notRef:number = 123 const changeMsg = () => { message.value = "change msg" console.log(isRef(message)); //true console.log(isRef(notRef)); //false }
-
下面的示例运行时没有更新姓名
<template> <h2>姓名:{{ user }}</h2> <button @click="update">更新</button> </template> <script lang="ts"> export default { setup() { let user = "tom"; let update = () => { user += "!"; }; return { user, update }; }, }; </script> <style></style>
其实user的值是修改了,但是没有响应式的更新视图
原因是此时的user是一个非响应式对象,使用ref可以解决问题
<template> <h2>姓名:{{ user }}</h2> <button @click="update">更新</button> </template> <script lang="ts"> import { ref } from "vue"; export default { setup() { let user = ref("tom"); let update = () => { user.value += "!"; console.log(user); }; return { user, update }; }, }; </script> <style></style>
1.3.reactive函数
-
作用: 定义一个对象类型的响应式数据(基本类型不要用它,要用
ref
函数) -
语法:
const 代理对象= reactive(源对象)
接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象) -
reactive定义的响应式数据是“深层次的”。
-
内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作。
基本使用:
<template> <h2>{{ user.name }}</h2> <h2>{{ user.age }}</h2> </template> <script lang="ts" setup> import { reactive } from "vue"; let user = reactive({ name: "tom", age: 19, }); //let name=reactive("abc"); 错误的 console.log(user); </script> <style></style>
reactive定义的响应式数据是“深层次的”。
<template> <h2>{{ user.name }}</h2> <h2>{{ user.age }}</h2> <h2>{{ user.address.city }}</h2> <button @click="update">更新用户的地址</button> </template> <script lang="ts" setup> import { reactive } from "vue"; let user = reactive({ name: "tom", age: 19, address: { city: "New York", country: "US", }, }); let update = () => { user.address.city += " city "; }; </script> <style></style>
1.4.Vue中的响应式原理
1.4.1.vue2.x的响应式
使用 Object 构造函数上的 defineProperty() 实现。
1、vue2存在的问题
新增属性、删除属性,界面不会更新。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="app"> <h2>{{user}}</h2> <button @click="attrOperation">新增属性、删除属性,界面不会更新</button> </div> <script src="./js/vue2.js"></script> <script> //新增属性、删除属性,界面不会更新。 //直接通过下标修改数组,界面不会自动更新。 var app = new Vue({ el: "#app", data: function () { return { user: { name: "tom", age: 19, address: { city: "zhuhai" } }, }; }, methods: { attrOperation() { //添加一个新属性 this.user.address.province = "中国"; //删除属性 delete this.user.address.city; console.log(JSON.stringify(this.user)); }, }, }); </script> </body> </html>
直接通过下标修改数组,界面不会自动更新。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="app"> <h2>{{user}}</h2> <button @click="attrOperation">新增属性、删除属性,界面不会更新</button> <hr /> <h2>{{array}}</h2> <button @click="arrayOperation"> 直接通过下标修改数组,界面不会自动更新。 </button> </div> <script src="./js/vue2.js"></script> <script> //新增属性、删除属性,界面不会更新。 //直接通过下标修改数组,界面不会自动更新。 var app = new Vue({ el: "#app", data: function () { return { user: { name: "tom", age: 19, address: { city: "zhuhai" } }, array: [1, 2, 3], }; }, methods: { attrOperation() { //添加一个新属性 this.user.address.province = "中国"; //删除属性 delete this.user.address.city; console.log(JSON.stringify(this.user)); }, arrayOperation() { this.array[0] = 100; console.log(this.array); }, }, }); </script> </body> </html>
注意:如果要解决上述问题
- ① 使用 vue 实例对象上的 $set 进行添加或修改,$delete 进行删除。
- ② 使用 Vue.set 进行添加或修改,Vue.delete 进行删除。
- ③ 使用 vue 实例对象上的 $nextTick 进行页面更新。
- ④ 使用数组的一些方法对数组进行操作(如 splice() )。
2、实现响应式的原理
对象类型:通过 Object.defineProperty() 对属性的读取、修改进行拦截(数据劫持)。
数组类型:通过重写更新数组的方法来实现拦截(对数组的变更方法进行了包裹)。
(1)Object.defineProperty() 的基本使用
Object.defineProperty("对象", "属性", { value: 0, // 属性值 enumerable: true, // 属性是否可被枚举,默认 false writable: true, // 属性是否可被修改,默认 false configurable: true, // 属性是否可被删除,默认 false get() {}, // 获取属性值时调用,此函数需返回属性的属性值 set(value) {}, // 修改属性值时调用,value为修改后的值 })
var user = {}; //定义属性 Object.defineProperty(user, "name", { value: "tom", }); console.log(user);
使用defineProperty重新定义属性name,访问时更新界面
var user = {}; var name = ""; //定义属性 Object.defineProperty(user, "name", { get() { console.log("更新界面,get"); return name; }, set(value) { console.log("更新界面,set"); name = value; }, }); user.name = "mark"; console.log(user.name);
使用闭包调用参数中的value,不再定义外部变量
var user = { name: "jack" }; //定义属性 function defineProp(target, key, value) { Object.defineProperty(target, key, { get() { console.log("更新界面,get"); return value; }, set(newValue) { console.log("更新界面,set"); value = newValue; }, }); } //重新定义name defineProp(user, "name", user.name); user.name = "mark"; console.log(user.name);
上面的代码还是有缺陷,如果变量比较多,则需要一个个定义,麻烦,可以使用循环
var user = { name: "jack", age: 19, sex: "男" }; //定义属性 function defineProp(target, key, value) { Object.defineProperty(target, key, { get() { console.log("更新界面,get", target, key, value); return value; }, set(newValue) { console.log("更新界面,set", target, key, value); value = newValue; }, }); } function observer(target) { if (typeof target !== "object" || target === null) { return target; } //遍历对象中所有的key for (let key in target) { defineProp(target, key, target[key]); //重新定义属性 } } observer(user); user.name = "rose"; user.age = 17; user.sex = "female"; console.log(user.name); console.log(user.age); console.log(user.sex);
上面的实现还是有问题,不能深层次的对象进入监听,例如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script> var user = { name: "jack", age: 19, sex: "男", address: { city: "zhuhai" }, }; //定义属性 function defineProp(target, key, value) { Object.defineProperty(target, key, { get() { console.log("更新界面,get", target, key, value); return value; }, set(newValue) { console.log("更新界面,set", target, key, value); value = newValue; }, }); } function observer(target) { if (typeof target !== "object" || target === null) { return target; } //遍历对象中所有的key for (let key in target) { console.log(key); defineProp(target, key, target[key]); //重新定义属性 } } observer(user); user.address.city = "珠海"; console.log(user.address.city); </script> </body> </html>
没有重新定义city,set时没有被拦截,使用递归可以解决该问题,完整代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script> //重新定义对象 function observer(target) { //如果被观察的对象不是object类型或为null则直接返回,不能监听 if (typeof target !== "object" || target === null) { return target; } //遍历对象中所有的key for (let key in target) { console.log(key); defineProp(target, key, target[key]); //重新定义属性 } } //定义属性 function defineProp(target, key, value) { //如果value仍然是object类型,则继续拆开监听,递归 if (typeof value === "object") { observer(value); } else { Object.defineProperty(target, key, { get() { console.log("更新界面,get", target, key, value); return value; }, set(newValue) { console.log("更新界面,set", target, key, value, newValue); value = newValue; }, }); } } //原始对象 var user = { name: "jack", age: 19, sex: "男", address: { city: "zhuhai" }, }; //重新定义对象中的所有属性,拦截get与set操作,插入要执行的用户逻辑 observer(user); user.address.city = "珠海"; console.log(user.address.city); </script> </body> </html>
(2)实现代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script src="./js/vue3.js"></script> <script> function updateView(target, key, newValue) { console.log("视图更新了!", target, key, newValue); } function observer(target) { //如果被观察的对象不是对象类,或为null,则直接返回原对象 if (typeof target !== "object" || target === null) { return target; } //遍历出对象的所有的key for (let key in target) { //定义属性,覆盖原有属性 defineReactive(target, key, target[key]); } } //在目标对象上定义属性,目标对象,属性名,值 function defineReactive(target, key, value) { //如果被定义的值是对象类型则继续观察 if (typeof value === "object") { observer(value); } else { //value在这里起到了闭包保留值的作用 Object.defineProperty(target, key, { get() { return value; }, set(newValue) { if (newValue !== value) { updateView(target, key, newValue); value = newValue; } }, }); } } let person = { name: "张果", address: { province: "广东", city: "珠海", }, }; observer(person); person.name = "张三"; person.address.city = "深圳"; console.log(person); </script> </body> </html>
1.4.2.Vue3响应式原理
-
实现原理:
-
通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
-
通过Reflect(反射): 对源对象的属性进行操作。
-
MDN文档中描述的Proxy与Reflect:
-
Proxy:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
-
Reflect:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
new Proxy(data, {
// 拦截读取属性值
get (target, prop) {
return Reflect.get(target, prop)
},
// 拦截设置属性值或添加新属性
set (target, prop, value) {
return Reflect.set(target, prop, value)
},
// 拦截删除属性
deleteProperty (target, prop) {
return Reflect.deleteProperty(target, prop)
}
})
proxy.name = 'tom'
-
-
(1)、反射 Reflect
Reflect是ES6中新增加的一个对象,并非构造器,该对象中含有多个可完成"元编程(对编程语言进行编程)"功能的静态函数,能方便的对对象进行操作,也可以结合Proxy实现拦截功能
https://www.cnblogs.com/best/p/16291079.html#_lab2_4_0
(2)、代理Proxy
浅层代理:
//定义被代理的对象 var shape = { 100 }; //定义代理代理 let proxy = new Proxy(shape, { get: function (target, key, receiver) { console.log("get"); //输出被代理的目标对象,属性名称,receiver为getter调用时的this值(当前对象) console.log(target, key, receiver); //使用get方法从目标对象中获取值,把取得的值加100 return Reflect.get(target, key, receiver) + 100; }, set: function (target, key, value, receiver) { console.log("set"); //输出被代理的目标对象,属性名称,值,receiver为getter调用时的this值(当前对象) console.log(target, key, value, receiver); //在目标对象上设置属性值,设置值时将值加100 return Reflect.set(target, key, value + 100, receiver); }, deleteProperty: function (target, key, receiver) { console.log("delete"); console.log(target, key); return Reflect.deleteProperty(target, key, receiver); }, }); proxy.width = 101; console.log(proxy.width); delete proxy.width;
深层代理
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script> //定义被代理的对象 var shape = { 100, location: { x: 100, y: 200 } }; function reactive(target) { return createReactiveObject(target); } function isObject(target) { return typeof target === "object" && target !== null; } function createReactiveObject(target) { if (!isObject(target)) return target; return new Proxy(target, { get: function (target, key, receiver) { console.log("get"); //输出被代理的目标对象,属性名称,receiver为getter调用时的this值(当前对象) console.log(target, key, receiver); //使用get方法从目标对象中获取值 let result = Reflect.get(target, key, receiver); //递归多层代理,相比于vue2的优势是,vue2默认递归,而vue3中,只要不使用就不会递归。 return isObject(result) ? reactive(result) : result; }, set: function (target, key, value, receiver) { console.log("set"); //输出被代理的目标对象,属性名称,值,receiver为getter调用时的this值(当前对象) console.log(target, key, value, receiver); //在目标对象上设置属性值 return Reflect.set(target, key, value, receiver); }, deleteProperty: function (target, key, receiver) { console.log("delete"); console.log(target, key); return Reflect.deleteProperty(target, key, receiver); }, }); } let proxyShape = reactive(shape); proxyShape.location.x = 500; </script> </body> </html>
Vue3中的响应式方式解决Vue2中没有解决的问题:
//新增加属性是响应式的
userProxy.sex = "女";
//删除属性是响应式的
delete userProxy.sex;
//修改数组中的指定元素是响应式的
userProxy.friends = ["a", "b", "c"];
userProxy.friends[0] = "A";
总结:
- vue2使用Object.defineProperty()实现响应式原理,而vue3使用Proxy()实现。
- 虽然vue2,vue3面对对象嵌套,都需要递归,但vue2是对对象的所有属性进行递归,vue3是按需递归,如果没有使用到内部对象的属性,就不需要递归,性能更好。
- vue2中,对象不存在的属性是不能被拦截的。而vue3可以。
- vue2对数组的实现是重写数组的所有方法,并改变,vue2中,数组的原型来实现,而Proxy则可以轻松实现。而且vue2中改变数组的长度是无效的,无法做到响应式,但vue3可以。
1.5.reactive对比ref
-
从定义数据角度对比:
-
ref用来定义:基本类型数据。
-
reactive用来定义:对象(或数组)类型数据。
-
备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过
reactive
转为代理对象。
-
-
从原理角度对比:
-
ref通过
Object.defineProperty()
的get
与set
来实现响应式(数据劫持)。 -
reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
-
-
从使用角度对比:
-
ref定义的数据:操作数据需要
.value
,读取数据时模板中直接读取不需要.value
。 -
reactive定义的数据:操作数据与读取数据:均不需要
.value
。
-
1.6.setup的两个注意点
-
setup执行的时机
-
在beforeCreate之前执行一次,this是undefined。
-
-
setup的参数
-
props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。
-
context:上下文对象
-
attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 相当于
this.$attrs
。 -
slots: 收到的插槽内容, 相当于
this.$slots
。 -
emit: 分发自定义事件的函数, 相当于
this.$emit
。
-
-
1.7.计算属性与监视
1.7.1.computed函数
- 模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。用计算属性来描述依赖响应式状态的复杂逻辑
-
与Vue2.x中computed配置功能一致
-
写法
import {computed} from 'vue'
setup(){
<template> <p>姓氏:<input v-model="user.lastname" /></p> <p>名字:<input v-model="user.firstname" /></p> <p>姓名:<input v-model="fullname1" /></p> <p>姓名:<input v-model="fullname2" /></p> </template> <script lang="ts"> import { reactive, ref, computed } from "vue"; export default { setup() { let user = reactive({ firstname: "jack", lastname: "ma", }); let fullname1 = computed(() => { return user.firstname + " " + user.lastname; }); let fullname2 = computed({ get() { let result = user.firstname; if (user.lastname) { result += " " + user.lastname; } return result; }, set(value) { if (value) { let users = value.trim().split(" "); user.firstname = users[0]; user.lastname = users[1] || ""; } else { user.firstname = ""; user.lastname = ""; } }, }); return { user, fullname1, fullname2 }; }, }; </script> <style></style>
运行效果
计算属性的第3种写法(类似方法)
<template> <h2> {{ formatPrice(money, 2) }} </h2> <h2> {{ myPrice(money, 2) }} </h2> </template> <script lang="ts"> import { ref, computed } from "vue"; export default { setup() { let money = ref(1995.376); function formatPrice(money: number, length: number) { return "¥" + money.toFixed(length); } let myPrice = computed(() => { return (money: number, length: number) => { return "¥" + money.toFixed(length); }; }); return { money, formatPrice, myPrice }; }, }; </script> <style></style>
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。
1.7.2.watch函数
-
与Vue2.x中watch配置功能一致
-
两个小“坑”:
-
监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)。
-
监视reactive定义的响应式数据中某个属性时:deep配置有效。
//情况一:监视ref定义的响应式数据
watch(sum,(newValue,oldValue)=>{
console.log('sum变化了',newValue,oldValue)
},{immediate:true})
//情况二:监视多个ref定义的响应式数据
watch([sum,msg],(newValue,oldValue)=>{
console.log('sum或msg变化了',newValue,oldValue)
})
/* 情况三:监视reactive定义的响应式数据
若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue!!
若watch监视的是reactive定义的响应式数据,则强制开启了深度监视
*/
watch(person,(newValue,oldValue)=>{
console.log('person变化了',newValue,oldValue)
},{immediate:true,deep:false}) //此处的deep配置不再奏效
//情况四:监视reactive定义的响应式数据中的某个属性
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{immediate:true,deep:true})
//情况五:监视reactive定义的响应式数据中的某些属性
watch([()=>person.job,()=>person.name],(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{immediate:true,deep:true})
//特殊情况
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{deep:true}) //此处由于监视的是reactive素定义的对象中的某个属性,所以deep配置有效 -
1.7.3.watchEffect函数
-
watch的套路是:既要指明监视的属性,也要指明监视的回调。
-
watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
-
watchEffect有点像computed:
-
但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
-
而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
//watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
watchEffect(()=>{
const x1 = sum.value
const x2 = person.age
console.log('watchEffect配置的回调执行了')
}) -
-
let stop = watchEffect(() => {console.log(a.value);console.log("a修改了");});调用stop方法可以停止监视
1.7.4、比较
四者的区别:
计算属性computed:计算属性只能当作属性用,也能带参数(不建议用),有缓存,效率高,,可以直接与v-model绑定。
方法:方法可以直接调用,可带参数,没有缓存,每次调用都会执行,效率不如计算属性高,不可与v-model绑定。
watch:不能显式调用(被监视的对象变化时被动调用),可以对变化的控制更加具体,但应用复杂,可以间接与v-model绑定。
watchEffect:不需要指定监视对象,自动推断出要监视的成员,默认立即监视一次
<template> <button @click="msg = msg ? 'ok' : 'no'"> {{ (msg + "!!!").split("").reverse().join("") }} </button> <h2> {{ `消息内容是:${msg}` }} </h2> <input v-model="msg" /> <h2> {{ showInfo() }} </h2> <h2> {{ showInfoComputed }} </h2> </template> <script lang="ts"> import { computed } from "@vue/reactivity"; import { ref, getCurrentInstance } from "vue"; export default { setup() { let msg = ref("这是一个按钮"); function showInfo() { console.log("showInfo被调用了"); return "showinfo的返回值"; } let showInfoComputed = computed(() => { console.log("showInfoComputed被调用了"); return "showInfoComputed的返回值"; }); return { msg, showInfo, showInfoComputed }; }, }; </script>
1.8.toRef
-
作用:创建一个 ref 对象,其value值指向另一个对象中的某个属性。
-
语法:
const name = toRef(person,'name')
-
应用: 要将响应式对象中的某个属性单独提供给外部使用时。
- 直接取出值,是非响应式的:
-
<template> <h2>姓名:{{ name }}</h2> <h2>薪水:{{ salary }}</h2> <button @click="name += '~'">改名</button> <button @click="salary++">加薪</button> </template> <script lang="ts"> import { ref, reactive, getCurrentInstance, watchEffect } from "vue"; export default { setup() { let user = reactive({ name: "tom", job: { salary: 20 } }); return { name: user.name, salary: user.job.salary }; }, }; </script> <style></style>
再次包装虽然是响应式的,但已不再是同一个对象
-
<template> <h4>{{ user }}</h4> <h2>姓名:{{ name }}</h2> <h2>薪水:{{ salary }}</h2> <button @click="name += '~'">改名</button> <button @click="salary++">加薪</button> </template> <script lang="ts"> import { ref, reactive, getCurrentInstance, watchEffect } from "vue"; export default { setup() { let user = reactive({ name: "tom", job: { salary: 20 } }); return { user, name: ref(user.name), salary: ref(user.job.salary) }; }, }; </script> <style></style>
-
<template> <h4>{{ user }}</h4> <h2>姓名:{{ name }}</h2> <h2>薪水:{{ salary }}</h2> <button @click="name += '~'">改名</button> <button @click="salary++">加薪</button> </template> <script lang="ts"> import { ref, reactive, getCurrentInstance, watchEffect, toRef } from "vue"; export default { setup() { let user = reactive({ name: "tom", job: { salary: 20 } }); return { user, name: toRef(user, "name"), salary: toRef(user.job, "salary"), }; }, }; </script> <style></style>
1.9.toRefs
-
扩展:
toRefs
与toRef
功能一致,但可以批量创建多个 ref 对象,语法:toRefs(person)
-
<template> <h4>{{ user }}</h4> <h2>姓名:{{ name }}</h2> <h2>薪水:{{ job.salary }}</h2> <button @click="name += '~'">改名</button> <button @click="job.salary++">加薪</button> </template> <script lang="ts"> import { ref, reactive, getCurrentInstance, watchEffect, toRef, toRefs, } from "vue"; export default { setup() { let user = reactive({ name: "tom", job: { salary: 20 } }); return { user, ...toRefs(user) }; }, }; </script> <style></style>
二、其它 Composition API
2.1.shallowReactive 与 shallowRef
-
shallowReactive:只处理对象最外层属性的响应式(浅响应式)。
-
<template> <div> <h1>姓名:{{name}}</h1> <h2>年龄:{{age}}</h2> <h3>喜欢的水果:{{likeFood.fruits.apple}}</h3> <button @click="name += '~'">修改姓名</button> <button @click="age++">修改年龄</button> <button @click="likeFood.fruits.apple += '!'">修改水果</button> </div> </template> <script> import {reactive, toRefs, shallowReactive} from 'vue' export default { name: "App", setup() { // 定义了一段数据 let person = shallowReactive({ // 只将第一层数据做了响应式处理 name: '张三', age: 18, likeFood: { fruits:{ apple: '苹果' // 深层次的数据将会是一个普通的对象 } } }) // 将数据返回出去 return { ...toRefs(person) } } }; </script>
-
shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理。
-
... <div> <h1>姓名:{{ sum.n}}</h1> <button @click="sum.n++">点击+</button> </div> ... setup() { // 定义了一段数据 let sum = shallowRef({ // sum将不在是一个响应式对象 n: 0 }); // 将数据返回出去 return { sum, }; }, ...
-
什么时候使用?
-
如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive。
-
如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===> shallowRef。
-
2.2.readonly 与 shallowReadonly
-
readonly: 让一个响应式数据变为只读的(深只读)。
-
shallowReadonly:让一个响应式数据变为只读的(浅只读)。
-
应用场景: 不希望数据被修改时。
-
<template> <h4>当前的sum值是:{{sum}}</h4> <button @click="sum++">点我++</button> <hr> <h2>姓名:{{name}}</h2> <h2>年龄:{{age}}</h2> <h2>薪资:{{job.j1.salary}}</h2> <button @click="person.name+='~'">修改姓名</button> <button @click="person.age++">增长年龄</button> <button @click="person.job.j1.salary++">涨薪</button> </template> <script> import {reactive,ref,toRef,toRefs,readonly,shallowReadonly} from 'vue' export default { name: 'Demo', setup(){ // 数据 let sum=ref(0) let person=reactive({ name:'张三', age:18, job:{ j1:{ salary:20 } } }) sum=readonly(sum) // person=readonly(person) //所有的只读的,包括深层次的 person=shallowReadonly(person) //只有第一层的不让改,深层次的还可以改 // 返回一个对象(常用) return { person, sum, ...toRefs(person) } } } </script>
2.3.toRaw 与 markRaw
-
toRaw:
-
作用:将一个由
reactive
生成的响应式对象转为普通对象。 -
使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
-
<template> <h3>{{ user }}</h3> <h2>姓名:{{ user.name }}</h2> <h2>薪水:{{ user.job.salary }}K</h2> <hr /> <h2>姓名:{{ rawUser.name }}</h2> <h2>薪水:{{ rawUser.job.salary }}K</h2> <button @click="user.name += '@'">改名</button> <button @click="user.job.salary++">加薪</button> <button @click="updateRaw">rawUser改名</button> <button @click="rawUser.job.salary++">rawUser加薪</button> </template> <script lang="ts"> import { reactive, toRaw } from "vue"; export default { setup() { let user = reactive({ name: "tom", job: { salary: 20 } }); let rawUser = toRaw(user); console.log("响应式对象", user); console.log("原始对象", rawUser); function updateRaw() { rawUser.name += "@"; console.log(rawUser); console.log(rawUser.name === user.name); } return { user, rawUser, updateRaw, }; }, }; </script> <style></style>
-
<template> <h2>n={{ n }}</h2> <button @click="n++">n++</button> <h2>姓名:{{ user.name }}</h2> <h2>年龄:{{ user.age }}</h2> <h2>薪水:{{ user.job.salary }}K</h2> <button @click="updateName">修改姓名</button> <button @click="user.age++">修改年龄</button> <button @click="user.job.salary++">修改薪水</button> <hr /> <button @click="updateRawObject">修改raw对象</button> </template> <script lang="ts"> import { reactive, ref, toRaw } from "vue"; export default { setup() { let n = ref(0); let user = reactive({ name: "rose", age: 99, job: { salary: 30, }, }); //toRaw可以将响应式对象变成普通对象 //普通对象与响应式对象是关联关系,非深拷贝 //当更新普通对象时,响应时对象也会更新,但不会更新界面 //当更新响应式对象时,普通对象也会更新,却可以更新界面 let u2 = toRaw(user); console.log(user, u2); function updateName() { user.name += "&"; console.log(user, u2); } function updateRawObject() { u2.name += "$"; console.log(user, u2); } return { user, n, updateName, updateRawObject }; }, }; </script>
-
-
markRaw:
-
作用:标记一个对象,使其永远不会再成为响应式对象。
-
应用场景:
-
有些值不应被设置为响应式的,例如复杂的第三方类库等。
-
当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。
-
<template> <h2>n={{ n }}</h2> <button @click="n++">n++</button> <h2>姓名:{{ user.name }}</h2> <h2>年龄:{{ user.age }}</h2> <h2>薪水:{{ user.job.salary }}K</h2> <button @click="user.name += '~'">修改姓名</button> <button @click="user.age++">修改年龄</button> <button @click="user.job.salary++">修改薪水</button> <div style="margin: 30px; background: #ddeeff"> <fieldset v-if="user.position?.city"> <h3>省:{{ user.position.province }}</h3> <h3>市:{{ user.position.city }}</h3> <button @click="user.position!.province += '#'">修改省</button> <button @click="user.position!.city += '#'">修改市</button> </fieldset> </div> <hr /> <button @click="addPosition">添加位置信息</button> </template> <script lang="ts"> //定义用户类型 type UserInfo = { name: string; age: number; job: { salary: number; }; position?: { province: string; city: string; }; }; import { reactive, ref, toRaw, markRaw } from "vue"; export default { setup() { let n = ref(0); let user: UserInfo = reactive({ name: "rose", age: 99, job: { salary: 30, }, }); function addPosition() { //添加一个新的对象position,将该对象标记为非响应式对象 user.position = markRaw({ province: "广东", city: "珠海", }); console.log(user); } return { n, user, addPosition }; }, }; </script>
-
2.4、限流与防抖
2.4.1、限流(节流)(规定时间内 只触发一次)
在JS中,如果一个事件频繁触发(比如用户疯狂点击按钮)并且处理函数处理耗时还比较长,那么就容易造成性能问题。
限流函数是针对这类问题的优化方式之一,它要求两次事件处理必须大于某个间隔时间,简而言之就是加了一层判断。
限流函数(throttle:节流阀)的核心在于内部维护了一个“上次执行时间点”,通过比较当前执行时间与上次执行时间的差值判断是否“频繁”,是否执行。限流函数本身是一个装饰函数,修饰了事件处理器之后返回一个新的闭包函数。经过限流函数处理之后,事件触发的频率就被限制为预先传入的interval之上了。
没有限流的一段登录代码,模拟登录:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <button id="btnLogin">登录</button> <script> document.getElementById("btnLogin").addEventListener( "click", (event) => { console.log("向服务器请求登录"); }, false ); </script> </body> </html>
写一个限流函数解决限流问题:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <button id="btnLogin">登录</button> <script> //节流函数,在interval时间内将要执行的fn函数 function throttle(fn, interval) { //上一次函数被调用的时间 let last = 0; return function () { //上下文 let ctx = this; //参数 let args = arguments; //当前时间 let now = new Date(); if (now - last > interval) { fn.apply(ctx, args); //执行函数 last = now; //重要计时 } }; } document.getElementById("btnLogin").addEventListener( "click", throttle((event) => { console.log("向服务器请求登录"); }, 2000), false ); </script> </body> </html>
连接点击数十下的效果:
节流策略(throttle),控制事件发生的频率,如控制为1s发生一次,甚至1分钟发生一次。与服务端(server)及网关(gateway)控制的限流 (Rate Limit) 类似。
作用: 高频率触发的事件,在指定的单位时间内,只响应第一次。
节流的应用场景
鼠标连续不断地触发某事件(如点击),单位时间内只触发一次;
监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断。例如:懒加载;
浏览器播放事件,每个一秒计算一次进度信息等
2.4.2、防抖(多次触发,只执行最后一次)
防抖函数也是一种限流函数,但要特殊一些。最典型的场景是表单输入,如果我们要在表单中监听input事件(比如远程搜索),那用户在输入的时候也会频繁触发,但这里使用throttle函数不行,因为我们需要等待用户停止输入一段时间后才能确认用户输入的值,所以要定义一个新的限流函数,叫做防抖函数。
防抖(防反跳)函数的核心是内部使用定时器并维护定时器返回的ID值,如果持续触发则不断clearTimeout()并重新发起setTimeout(),通过这种方式等待事件触发完毕,然后进行延时处理。
没有防抖带来的问题:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> 用户名:<input id="username" /> <script> document.getElementById("username").addEventListener( "keyup", (event) => { console.log( "用户名" + ["存在", "不存在"][new Date().getMilliseconds() % 2] ); }, false ); </script> </body> </html>
防抖函数:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <button id="btnLogin">登录</button> <script> //节流函数,在interval时间内将要执行的fn函数 function throttle(fn, interval) { //上一次函数被调用的时间 let last = 0; return function () { //上下文 let ctx = this; //参数 let args = arguments; //当前时间 let now = new Date(); if (now - last > interval) { fn.apply(ctx, args); //执行函数 last = now; //重要计时 } }; } document.getElementById("btnLogin").addEventListener( "click", throttle((event) => { console.log("向服务器请求登录"); }, 2000), false ); </script> </body> </html>
防抖策略(debounce)是当事件被触发后,延迟n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时。
作用: 高频率触发的事件,在指定的单位时间内,只响应最后一次,如果在指定的时间内再次触发,则重新计算时间。
防抖的应用场景
登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖
调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
文本编辑器实时保存,当无任何更改操作一秒后进行保存
2.5.customRef
-
作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。
- customRef 接收一个函数作为参数,这个函数接收两个函数作为参数 track (通知vue需要追踪后续内容的变化) 和 trigger (通知vue重新解析模板)。
-
<template> <input v-model="num" /> {{ num }} </template> <script lang="ts"> import { customRef } from "vue"; export default { setup() { let myRef = function (value: any) { return customRef((track, trigger) => { return { get() { track(); //告诉vue这个数据是要被追踪的 return value; }, set(newValue) { value = newValue; trigger(); //通知vue更新界面 }, }; }); }; let num = myRef(100); return { num }; }, }; </script> <style scoped></style>
-
实现防抖效果:
-
<template> 卡号:<input v-model="msg" /> <hr /> {{ msg }} </template> <script lang="ts"> import { customRef } from "vue"; export default { setup() { //带防抖功能的自定义响应式函数 let myDebounceRef = function (value: any, delay: number) { let timer: number = 0; return customRef((track, trigger) => { return { get() { track(); //通知vue追踪该变量 return value; }, set(newValue) { if (timer) { //如果上一个时钟存在,则清除,重新计时 clearTimeout(timer); } //延时赋值 timer = setTimeout(() => { value = newValue; trigger(); //更新视图 console.log("向服务器发送处理请求"); }, delay); }, }; }); }; let msg = myDebounceRef("", 1000); return { msg }; }, }; </script>
2.6.provide 与 inject
-
作用:实现祖与后代组件间通信
-
套路:父组件有一个
provide
选项来提供数据,后代组件有一个inject
选项来开始使用这些数据 -
具体写法:
-
祖组件中:
setup(){
-
后代组件中:
setup(props,context){
-
2.7.响应式数据的判断
-
isRef: 检查一个值是否为一个 ref 对象
-
isReactive: 检查一个对象是否是由
reactive
创建的响应式代理 -
isReadonly: 检查一个对象是否是由
readonly
创建的只读代理 -
isProxy: 检查一个对象是否是由
reactive
或者readonly
方法创建的代理
三、Composition API 的优势
3.1.Options API 存在的问题
使用传统OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改 。
3.2.Composition API 的优势
我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。
3.3、折叠代码
折叠代码可以让代码更加有序,方便查找与管理
<template></template> <script lang="ts" setup> import { customRef } from "vue"; //#region 功能部分 //#region 功能1 //功能1的数据 //功能1的方法 //功能1的计算属性 //#endregion //#region 功能2 //功能2的数据 //功能2的方法 //功能2的计算属性 //#endregion //#region 功能3 //功能3的数据 //功能3的方法 //功能3的计算属性 //#endregion //#endregion </script>
七、示例下载
https://gitee.com/zhangguo5/vue3_-chapter1.git
八、视频
【Vue3 + Vuex + Pinia + TypeScript + Router】 https://www.bilibili.com/video/BV1at4y1F75D?share_source=copy_web&vd_source=475a31f3c5d6353a782007cd4c638a8a
九、作业
1、使用方法,computed,watch,watchEffect实现两个数运算的效果,要求4种方法分开完成,加减乘除。
2、完成每一个示例
3、使用React完成每一个示例
4、定义一个对象,如下: