Vue3笔记(二)了解组合式API的应用与方法
一、组合式API(
官方文档: 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>
二、setup
2.0、setup 是什么?
setup 是 vue3新增的生命周期函数,setup的加入就是为了让vue3使用组合式API(Composition API)。使用组合式API更符合大型项目的开发,通过setup可以将该部分抽离成函数,让其他开发者就不用关心该部分逻辑。
注意:组件中所用到的数据,方法等等,均要配置在setup中
2.1、setup函数的两种返回值:
-
若返回一个函数,则对象中的属性,方法,在模板中均可以直接使用
-
若返回一个渲染函数:则可以自定义渲染内容
2.2、setup 的生命周期
setup 位于 beforeCreated 之前,用于代替 created 和 beforeCreated。由于 setup 函数执行的时候项目还没有进行初始化,所以不能访问 data 或 methods 中的数据,console.log(this)显示为undefind。栗子如下:
<template> </template> <script> export default { setup(){ console.log(this) //undefined } } </script>
2.3、setup 的简单使用
setup 内部可以定义数据和方法,如果想在模板中使用,必须通过 return 进行返回。例如:我想用 setup 的方法,在浏览器中显示 Hello Vue,然后在点击按钮后将Hello Vue变成 hello vue3,栗子如下:
<template> <h3>{{text}}</h3> <button @click="btn">修改</button> </template> <script> import {ref} from 'vue' export default { setup(){ //因为setup里面的数据是没有响应式的,所以这里需要借助ref属性,ref属性需要从vue中按需引入 //简单的来说ref就是将简单数据类型变为响应式数据,从而达到点击改变文字的效果。 //值得注意的是在使用了ref后想要改变属性内容就需要.value(想要更多了解ref的可以自行查询,这里不做过多解释) const text =ref('Hello Vue') const btn=()=>{ text.value='hello vue3' } return{ text, btn } } } </script> <style lang="scss" scoped> </style>
2.4、简化的setup函数:
<template> <h2>{{ username }}</h2> <button @click="update">update</button> </template> <script lang="ts" setup> // 把 setup 直接写在script 标签里 import { h } from "vue"; let username = "tom"; let update = () => { console.log("update2"); }; </script> <style></style>
2.5、如果 setup 返回的是一个渲染函数,注意会替换页面中的内容,很少使用,慎用。
<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>
运行结果:
2.5、setup 的注意点:
-
尽量不要与Vue2.x配置混用
-
Vue2.x配置(data、methos、computed...)中可以访问到setup中的属性、方法。
-
但在setup中不能访问到Vue2.x配置(data、methos、computed...)。
-
如果有重名, setup优先。
-
-
setup一般不能是一个async函数,因为返回值不再是 return 的对象, 而是 promise,模板看不到 return 对象中的属性。(后期也可以返回一个Promise实例,但需要 Suspense 和异步组件的配合)
- setup 执行的时机
- 在 beforeCreate 之前执行一次,this 是 undefined
- setup 的参数
-
props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。
-
context:上下文对象
-
-
attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 相当于
this.$attrs
-
slots: 收到的插槽内容, 相当于
this.$slots
-
emit: 分发自定义事件的函数, 相当于
this.$emit
-
-
2.5.1、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>
运行结果:
2.5.2、setup可以混用vue2,但不建议
<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>
运行结果:
2.5.3、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>
运行结果:
2.5.4、在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>
运行结果:
2.5.5、如果有重名, 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>
运行结果:
2.5.6、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>
运行结果:
三、ref函数
3.1、ref 函数的介绍
-
作用: 定义一个响应式的数据
-
语法:
const 变量名 = ref(initValue)
-
创建一个包含响应式数据的引用对象(reference对象,简称ref对象)。
-
JS中操作数据:
变量名.value
-
模板中读取数据:不需要.value,直接:
<div>{{ 变量名 }}</div>
- ref 对应的接口是 interface Ref<T>
-
-
备注:
-
ref
函数仅能监听基本类型的变化,不能监听复杂类型的变化(比如对象、数组) -
基本类型的数据:响应式依然是靠
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 }
3.2、什么情况下使用ref函数?栗子如下:
<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>
重新运行的结果:
四、reactive函数
-
作用: 定义一个对象类型的响应式数据(基本类型不要用它,要用
ref
函数) -
语法:
const 代理对象= reactive(源对象)
接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象) -
reactive 定义的响应式数据是“深层次的”。
-
内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作。
4.1、基本使用:
<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"); 错误的,因为基本数据类型不能使用reactive,reactive的类型是object console.log(user); </script> <style></style>
运行结果:
4.2、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>
运行结果:
4.3、reactive对比ref
-
从定义数据角度对比:
-
ref用来定义:基本类型数据
-
reactive用来定义:对象(或数组)类型数据
-
备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过
reactive
转为代理对象
-
-
从原理角度对比:
-
ref通过
Object.defineProperty()
的get
与set
来实现响应式(数据劫持) -
reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据
-
-
从使用角度对比:
-
ref定义的数据:操作数据需要
.value
,读取数据时模板中直接读取不需要.value
-
reactive定义的数据:操作数据与读取数据:均不需要
.value
-
五、Vue中的响应式原理
5.1.Vue2.x的响应式
使用 Object 构造函数上的 defineProperty() 实现。
5.1.2、vue2存在的问题
-
新增属性、删除属性,界面不会更新
-
直接通过下标修改数组,界面不会自动更新
问题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>
运行结果:
问题二的栗子(也是用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>{{array}}</h2> <button @click="arrayOperation"> 直接通过下标修改数组,界面不会自动更新。 </button> </div> <script src="../js/vue.js"></script> <script> //直接通过下标修改数组,界面不会自动更新。 var app = new Vue({ el: "#app", data: function () { return { array: [1, 2, 3], }; }, methods: { arrayOperation() { this.array[0] = 100; // 修改数组中第一个元素的值,页面是不会更新的 console.log(this.array); console.log("============================================="); this.array = [5,6,7]; // 直接覆盖这个数组,页面可以更新 console.log(this.array); }, }, }); </script> </body> </html>
运行结果:
注意:如果要解决上述问题
-
① 使用 Vue.set 进行添加或修改,Vue.delete 进行删除
-
② 使用 vue 实例对象上的 $nextTick 进行页面更新
-
③ 使用数组的一些方法对数组进行操作(如 splice() )
5.1.2、Vue2 实现响应式的原理
对象类型:通过 Object.defineProperty() 对属性的读取、修改进行拦截(数据劫持)
数组类型:通过重写更新数组的方法来实现拦截(对数组的变更方法进行了包裹)
5.1.2.1、Object.defineProperty() 的基本使用
Object.defineProperty("对象", "属性", { value: 0, // 属性值 enumerable: true, // 属性是否可被枚举,默认 false writable: true, // 属性是否可被修改,默认 false configurable: true, // 属性是否可被删除,默认 false get() {}, // 获取属性值时调用,此函数需返回属性的属性值 set(value) {}, // 修改属性值时调用,value为修改后的值 })
栗子1:定义属性
var user = {}; //定义属性 Object.defineProperty(user, "name", { value: "tom", }); console.log(user);
栗子2:使用 defineProperty 重新定义属性 name,访问时或修改时更新界面
<script> 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); </script>
运行结果:
栗子3:使用闭包(内部函数访问外部函数的参数叫闭包)调用参数中的 value,不再定义外部变量
<script> // 定义一个对象,对象里面name的值本来为jack var user = { name: "jack" }; //定义属性 /** * 参数的含义: * 参数1.在哪个对象定义 参数2.定义哪个属性 参数3.属性的值是哪个 */ 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); // 重新修改name的值 user.name = "mark"; console.log(user.name); </script>
运行结果:跟上面栗子2的结果一样!!!
栗子3:如果变量比较多,需要一个个去定义的情况下,可以使用循环
<!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: "男" }; //定义属性 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) { // 如果观察的对象就是一个object或者该对象为空的,就返回原来的对象回去,不做操作 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); </script> </body> </html>
运行结果:
栗子4:深层次(对象(浅)里面还包含有对象(深))的对象不能进入监听
<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>
运行结果:
栗子5:解决栗子4的问题(没有重新定义 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>
运行结果:
5.2、Vue3.x的响应式
5.2.1、Vue3-ES6中的 Reflect(反射)
Reflect 是ES6 中新增加的一个对象,并非构造器,该对象中含有多个可完成 “ 元编程 ”(对编程语言进行编程)功能的静态函数,能方便的对对象进行操作,也可以结合Proxy 实现拦截功能
对Reflect的理解:https://www.cnblogs.com/best/p/16291079.html#_lab2_4_0
举栗说明:
5.2.2、Vue3-ES6中的 Proxy(代理)
5.2.2.1、Proxy是什么?
Proxy 本质是一个构造函数,用于创建一个对象的代理,从而实现对被代理对象操作的的拦截和自定义(如属性查找,赋值,枚举,函数调用等)相对于Object.defineProperty 虽功能相似却有着本质的区别,Object.defineProperty 从其本质上来说更像是官方向开发者开放的对于对象本身的一种扩展操作
5.2.2.2、语法
// target:表示被代理的原始对象,handler:代理对象 const proxy = new Proxy(target, handler)
举栗说明:
<!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>Proxy_-ES6</title> </head> <body> <script> let user={name:"rose",age:23}; function updateView(type,target,key,receiver) { console.log(type,"更新界面",target,key,receiver); } let userProxy=new Proxy(user,{ // target表示被代理的原始对象,key访问的属性名,receiver代理对象 get(target,key,receiver){ updateView("get",target,key,receiver); return Reflect.get(target,key,receiver); }, // 要设置的值 set(target,key,receiver) { updateView("set",target,key,receiver); return Reflect.set(target,key,receiver); }, // 当删除属性时被拦截 deleteProperty(target,key,receiver){ updateView("delete",target,key,receiver); return Reflect.deleteProperty(target,key); } }); userProxy.name="mark"; userProxy.age=20; console.log(userProxy.name,userProxy.age); delete userProxy.age; delete userProxy.name; </script> </body> </html>
运行结果:
5.2.3、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) } })
-
5.3、总结:
-
vue2使用 Object.defineProperty() 实现响应式原理,而 vue3 使用 Proxy() 实现
-
虽然 vue2,vue3 面对对象嵌套,都需要递归,但 vue2 是对对象的所有属性进行递归,vue3 是按需递归,如果没有使用到内部对象的属性,就不需要递归,性能更好
-
vue2中,对象不存在的属性是不能被拦截的。而 vue3 可以
-
vue2 对数组的实现是重写数组的所有方法并改变,vue2中,数组靠原型来实现,而 Proxy 则可以轻松实现。而且 vue2 中改变数组的长度是无效的,无法做到响应式,但 vue3 可以做到响应式
5.4、响应式数据的判断
-
isRef: 检查一个值是否为一个 ref 对象
-
isReactive: 检查一个对象是否是由
reactive
创建的响应式代理 -
isReadonly: 检查一个对象是否是由
readonly
创建的只读代理 -
isProxy: 检查一个对象是否是由
reactive
或者readonly
方法创建的代理
六、计算属性
6.1、computed 函数
- 模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。用计算属性来描述依赖响应式状态的复杂逻辑
-
与Vue2.x中 computed 配置功能一致
-
写法:
import {computed} from 'vue' setup(){ ... //计算属性——简写 let fullName = computed(()=>{ return person.firstName + '-' + person.lastName }) //计算属性——完整 let fullName = computed({ get(){ return person.firstName + '-' + person.lastName }, set(value){ const nameArr = value.split('-') person.firstName = nameArr[0] person.lastName = nameArr[1] } }) }
- 栗子1:只用get()方法,拼接姓名
<template lang=""> <p>姓氏:<input type="text" v-model="firstname" /></p> <p>名字:<input type="text" v-model="lastname" /></p> <p>姓名:<input type="text" v-model="fullname" /></p> </template> <script lang="ts"> import { computed, ref } from 'vue'; export default { setup() { let firstname = ref('张'); let lastname = ref('三'); let fullname = computed(() => { return firstname.value + lastname.value; }); return { firstname, lastname, fullname }; }, }; </script> <style lang=""></style>
运行结果:
-
栗子2:用get方法拼接姓名,set方法拆分姓名:
<template lang=""> <p>姓氏:<input type="text" v-model="firstname" /></p> <p>名字:<input type="text" v-model="lastname" /></p> <p> 姓名:{{fullname}} </p> <p>姓名:<input type="text" v-model="fullname" /></p> </template> <script lang="ts"> import { computed, ref } from 'vue'; export default { setup() { let firstname = ref('张'); let lastname = ref('三'); // 2、有get与set操作的计算属性 let fullname = computed({ get(){ // 直接返回值 let result = firstname.value; // 如果这个有值,才去连接符号 if (lastname.value) { result += "," + lastname.value; } return result; }, set(value:any){ // 拆分姓和名 const names = value.split(","); firstname.value = names[0]; lastname.value = names[1] || ""; }, }) return { firstname, lastname, fullname }; }, }; </script> <style lang=""></style>
运行结果:
-
栗子三:价钱保留两位小数并在前面加¥符号
<template> <nav> <router-link to="/">计算属性中get方法</router-link> | <router-link to="/example2">get,set(拆分姓和名)方法</router-link> | <router-link to="/example3">价钱取两位小数并在前面加¥的方法</router-link> </nav> <router-view/> </template> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; } nav { padding: 30px; } nav a { font-weight: bold; color: #2c3e50; } nav a.router-link-exact-active { color: #42b983; } </style>
运行结果:
注意: 两种方式在结果上虽然是完全相同的,但是不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。
6.2、watch函数
6.2.0、Watch概述
一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性
-
与 Vue2.x 中 watch 配置功能一致
-
两个小“ 坑 ”:
-
监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)。
-
监视reactive定义的响应式数据中某个属性时:deep配置有效。
6.2.1、watch()
一共可以接受三个参数
-
侦听数据源
- 它可以是不同形式的“数据源”,它可以是: 一个 ref ,一个计算属性 ,一个 getter 函数(有返回值的函数), 一个响应式对象 , 以上类型的值组成的数组
-
回调函数
- 这个回调函数接受三个参数:新值、旧值,以及一个用于清理副作用的回调函数。
-
配置选项
immediate
:在侦听器创建时立即触发回调。(常用)deep
:深度遍历,以便在深层级变更时触发回调。(常用)flush
:回调函数的触发时机。pre:默认,dom 更新前调用,post: dom 更新后调用,sync 同步调用。onTrack / onTrigger
:用于调试的钩子。在依赖收集和回调函数触发时被调用。
6.2.3、举栗说明
栗子1:监视 ref 值的基本用法
<template lang=""> <h1>{{ msg }}</h1> <button @click="msg += '!' ">修改msg</button> </template> <script lang="ts"> import { watch, ref } from 'vue'; export default { setup() { // 信息 let msg = ref("hello"); // 定义一个监视函数 watch(msg,(newValue,oldValue)=>{ console.log("msg已经修改了!","新值:"+newValue,"旧值:"+oldValue); },{immediate:true}); // {immediate:true}:立即监视,初始化时被调用一次 return { msg }; } }; </script> <style lang=""></style>
运行结果:
栗子2:监视多个 ref 值的基本用法(主要是在watch函数中的第一个参数多加一个 [ 参数1,参数2 ] )
<template lang=""> <!-- 修改a --> <div> <h1>a = {{ a }}</h1> <button @click="a++">修改a</button> </div> <!-- 修改b --> <div> <h1>b = {{ b }}</h1> <button @click="b++">修改b</button> </div> </template> <script lang="ts"> import { watch, ref } from 'vue'; export default { setup() { let a = ref(100), b = ref(200); // 定义一个监视函数 watch([a, b], function (newArrayValue, oldArrayValue) { console.log("a,b已经修改了!", "新值:" + newArrayValue, "旧值:" + oldArrayValue); }, { immediate: true }); // {immediate:true}:立即监视,初始化时被调用一次 return { a,b }; } }; </script> <style lang=""></style>
运行结果:
栗子3:监视reactive定义的响应式数据
出现的问题:
-
问题一:oldValue 被丢失,监视不到它的值
-
问题二:immediate:true 立即监视,初始化时被执行一次
-
问题三:deep:false:深度监视无效
<template lang=""> <h2>姓名:{{ user.name }}</h2> <h2>薪资:{{ user.job.salary }} K</h2> <button @click="user.name+='!'">修改姓名</button> <button @click="user.job.salary++">加薪</button> </template> <script lang="ts"> import { watch, reactive } from 'vue'; export default { setup() { let user = reactive({name:"tom",job:{salary:20}}) // 使用watch监听reactive定义的对象 // 问题一:oldValue被丢失,监视不到它的值 // 问题二:immediate:true立即监视,初始化时被执行一次 // 问题三:deep:false:深度监视无效 watch(user,(newValue,oldValue)=>{ console.log("user修改了","新值:"+newValue,"旧值:"+oldValue); },{immediate:true,deep:false}) // deep:深度监视的意思 return { user }; } }; </script> <style lang=""></style>
运行结果:
栗子4:监视 reactive 对象的单个属性
<template lang=""> <h2>姓名:{{ user.name }}</h2> <h2>薪资:{{ user.job.salary }} K</h2> <button @click="user.name+='!'">修改姓名</button> <button @click="user.job.salary++">加薪</button> </template> <script lang="ts"> import { reactive,watch } from 'vue'; export default { setup() { let user = reactive({name:"tom",job:{salary:20}}); // 修改单属性的: // 使用watch监听reactive定义的对象中的属性 watch(()=>user.name,(newValue,oldValue)=>{ console.log("user中的name被修改了","新值:",newValue,"旧值:",oldValue); },{immediate:true,deep:false}) // deep:深度监视的意思 return { user }; } }; </script> <style lang=""></style>
运行结果:
栗子5:监视 reactive 对象的多个属性
<template lang=""> <h2>姓名:{{ user.name }}</h2> <h2>薪资:{{ user.job.salary }} K</h2> <button @click="user.name+='!'">修改姓名</button> <button @click="user.job.salary++">加薪</button> </template> <script lang="ts"> import { reactive,watch } from 'vue'; export default { setup() { let user = reactive({name:"tom",job:{salary:20}}); // 修改多个属性的: // 也是在属性里面加[] watch([()=>user.name,()=>user.job.salary],(newValue,oldValue)=>{ console.log("user被修改了","新值:",newValue,"旧值:",oldValue); },{immediate:true,deep:false}) // deep:深度监视的意思 return { user }; } }; </script> <style lang=""></style>
运行结果:
栗子6:特殊情况(例如监听对象中的某个属性,deep就有效了)
<template lang=""> <h2>姓名:{{ user.name }}</h2> <h2>薪资:{{ user.job.salary }} K</h2> <button @click="user.name+='!'">修改姓名</button> <button @click="user.job.salary++">加薪</button> </template> <script lang="ts"> import { reactive,watch } from 'vue'; export default { setup() { let user = reactive({name:"tom",job:{salary:20}}); // 特殊情况(例如监听对象中的某个属性,deep就有效了) // 这个栗子监视的是jop watch(()=>user.job,(newValue,oldValue)=>{ console.log("user中的jop被修改了","新值:",newValue,"旧值:",oldValue); },{deep:true}) // deep:深度监视的意思 return { user }; } }; </script> <style lang=""></style>
运行结果:
6.2.3、Watch和computed的区别
-
computed 支持缓存,只有依赖数据发生改变,才会重新进行计算;而 watch不支持缓存,数据变,直接会触发相应的操作 computed 不支持异步,当 computed内有异步操作时无效,无法监听数据的变化,而 watch 支持异步
-
computed 属性值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于 data 中声明过或者父组件传递的 props 中的数据通过计算得到的值;而watch 监听的函数接收两个参数,第一个参数是最新的值,第二个参数是输入之前的值
-
如果一个属性是由其它属性计算而来的,这个属性依赖其它属性,多对一或者一对一,一般用 computed;而当一个属性发生变化时,需要执行对应的操作,一对多,一般用 watch
6.3、watchEffect函数
watch()
是懒执行的:当数据源发生变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。
watchEffect 有点像 computed:
-
但 computed 注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
-
而 watchEffect 更注重的是过程(回调函数的函数体),所以不用写返回值。
6.3.1、watch
与 watchEffect 对比?
watch
和 watchEffect
的主要功能是相同的,都能响应式地执行回调函数。它们的区别是追踪响应式依赖的方式不同:
watch
只追踪明确定义的数据源,不会追踪在回调中访问到的东西;默认情况下,只有在数据源发生改变时才会触发回调;watch
可以访问侦听数据的新值和旧值。watchEffect
会初始化执行一次,在副作用发生期间追踪依赖,自动分析出侦听数据源;watchEffect
无法访问侦听数据的新值和旧值。
简单一句话,watch
功能更加强大,而 watchEffect
在某些场景下更加简洁
6.3.2、举栗说明
1、watchEffect 所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
<template lang=""> <h2>a={{ a }}<button @click="a++">a++</button></h2> <h2>b={{ b }}<button @click="b++">b++</button></h2> <h2>c={{ c.x }}<button @click="c.x++">c++</button></h2> <button @click="updateAB">更新</button> </template> <script lang="ts"> import { reactive, ref, watchEffect } from 'vue'; export default { setup() { let a = ref(100), b = ref(200); let c = reactive({ x: 300 }); watchEffect(() => { // console.log(a.value, b.value); // console.log('a或b发生了变化'); console.log(c.x); console.log('c.x发生了变化'); }); let updateAB = () => { a.value = 1000; b.value = 2000; }; return { a, b, updateAB, c }; }, }; </script> <style lang=""></style>
2、停止监视(定义个变量,然后调用就好了)
<template lang=""> <h4>{{ a }}</h4> <button @click="a++">a++</button> <button @click="stop">停止监视a</button> </template> <script lang="ts"> import { ref, watchEffect } from 'vue'; export default { setup() { let a = ref(100); let stop = watchEffect(() => { console.log('a变了', a.value); }); return { a, stop }; }, }; </script> <style lang=""></style>
6.4、toRef
-
作用:创建一个 ref 对象,其 value 值指向另一个对象中的某个属性。
-
语法:
const name = toRef(person,'name')
-
应用: 要将响应式对象中的某个属性单独提供给外部使用时。
- 直接取出值,是非响应式的:
<template lang=""> <h3>{{ user }}</h3> <h2>姓名:{{ name }}</h2> <h2>薪水:{{ salary }}K</h2> <button @click="name += '@'">改名</button> <!-- <button @click="salary++">加薪</button> --> <button @click="salary++">加薪</button> </template> <script lang="ts"> import { reactive,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 lang=""></style>
运行结果:
-
ref和toRef的区别:
-
(1). ref 本质是拷贝,修改响应式数据不会影响原始数据;toRef 的本质是引用关系,修改响应式数据会影响原始数据
-
(2). ref 数据发生改变,界面会自动更新;toRef 当数据发生改变是,界面不会自动更新
-
(3). toRef 传参与 ref 不同;toRef 接收两个参数,第一个参数是哪个对象,第二个参数是对象的哪个属性
-
6.5、toRefs
-
扩展:
toRefs
与toRef
功能一致,但可以批量创建多个 ref 对象,语法:toRefs(person)
-
将一个响应式对象转为普通对象
-
对象的每一个属性都是对应的 ref
<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, toRef, toRefs } from 'vue'; export default { setup() { let user = reactive({ name: 'tom', job: { salary: 20 } }); // let userRefs=toRefs(user); // console.log(userRefs); // 简写 return { user, ...toRefs(user), }; }, }; </script> <style> </style>
运行结果:
七、其它 Composition API
7.1、shallowReactive 与 shallowRef
-
shallowReactive:只处理对象最外层属性的响应式(浅响应式)
-
shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理。
-
什么情况下使用?
- 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
- 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===> shallowRef
- 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
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 栗子:
<template> <div> <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> <hr /> <button @click="user = { name: 'tom', age: 12, job: { salary: 14 } }"> 修改user对象 </button> </div> </template> <!-- shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理 --> <script lang="ts"> import { shallowRef } from 'vue'; export default { setup() { let user = shallowRef({ name: 'rose', age: 55, job: { salary: 30, }, }); return { user }; }, }; </script>
7.2、readonly 与 shallowReadonly
-
readonly: 让一个响应式数据变为只读的(深只读)
-
shallowReadonly:让一个响应式数据变为只读的(浅只读)
-
应用场景: 不希望数据被修改时
栗子:
<template> <div> <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> </template> <!-- readonly: 让一个响应式数据变为只读的(深只读)。 shallowReadonly:让一个响应式数据变为只读的(浅只读)--> <script lang="ts"> import { reactive, readonly, ref, shallowReadonly } from 'vue'; export default { setup() { let n = ref(0); let user = reactive({ name: 'rose', age: 21, job: { salary: 30, }, }); // 将user转换成一个只读的对象 // user=readonly(user); // readonly可转换refImpl类型数据为只读 // n=readonly(n); // 第一层只读,第二层不只读 // 深拷贝,浅拷贝 user = shallowReadonly(user); n = shallowReadonly(n); console.log(user); let updateName = () => { user.name += '%'; console.log(user); }; return { user, updateName, n }; }, }; </script>
7.3、对象拷贝(深拷贝与浅拷贝)
-
对象的浅拷贝:浅拷贝是对象共用的一个内存地址,对象的变化相互影响。
-
对象的深拷贝:简单理解深拷贝是将对象放到新的内存中,两个对象的改变不会相互影响。
object.assign定义:
-
Object.assign() 方法的第一个参数是目标对象,后面的参数都是源对象.
-
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。 注意:如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
简单栗子:
const obj = {'name': 'qiu', 'children': {'a':"1"}}; const objcopy = Object.assign({}, obj); objcopy.name="liang"; objcopy.children.a="2"; console.log('obj', obj); //name: "qiu" grade: {chi: "2"} console.log('objcopy', objcopy);//name: "liang" grade: {chi: "2"}
官方描述:Object.assign() 拷贝的只是属性值,假设源对象的属性值是一个指向对象的引用,那么它也只拷贝那个引用值
个人简述:如上方当打印结果,拷贝的是引用值时,那么实现到浅拷贝到效果(也就是对象共用的一个内存地址,对象的变化相互影响
).如果是普通类型,如string、number则为深拷贝
7.4、toRaw 与 markRaw
-
toRaw:
-
作用:将一个由
reactive
生成的响应式对象转为普通对象。 -
-
使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
- 栗子:
<template> <div> <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="updteName">修改姓名</button> <button @click="user.age++">修改年龄</button> <button @click="user.job.salary++">加薪</button> <hr> <button @click="updateRawObject">修改raw对象</button> </div> </template> <!-- toRaw: 作用:将一个由reactive生成的响应式对象转为普通对象。 markRaw:作用:标记一个对象,使其永远不会再成为响应式对象--> <script lang="ts"> import { reactive, ref, toRaw } from 'vue'; export default { setup() { let n = ref(0); // 响应式对象 let user = reactive({ name: 'rose', age: 21, job: { salary: 30, }, }); // toRaw可将响应式对象变普通对象 // 普通对象与响应式对象是关联关系,非深拷贝 // 当更新普通对象时,响应式对象也跟着更新,页面不会更新 // 当更新响应式对象时,普通对象也跟着更新,页面会更新 // 普通对象 let u2 = toRaw(user); console.log(user, u2); function updteName() { user.name += '&'; console.log(user, u2); } function updateRawObject() { u2.name += '$'; console.log(user, u2); } return { user, n, updteName, updateRawObject }; }, }; </script>
-
- markRaw:
-
作用:标记一个对象,使其永远不会再成为响应式对象。
-
应用场景:
-
有些值不应被设置为响应式的,例如复杂的第三方类库等。
-
当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。
- 栗子:
<template> <div> <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: lightblue"> <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> </div> </template> <!-- markRaw:作用:标记一个对象,使其永远不会再成为响应式对象--> <script lang="ts"> // 定义用户类型 type UserInfo = { name: string; age: number; job: { salary: number; }; position?: { province: string; city: string; }; }; import { markRaw, reactive, ref } from 'vue'; export default { setup() { let n = ref(0); let user: UserInfo = reactive({ name: 'rose', age: 21, job: { salary: 30, }, }); function addPosition() { // 添加一个新的对象position,将该对象标记为非响应式对象 user.position = markRaw({ province: '广东', city: '珠海', }); console.log(user); } return { user, n, addPosition }; }, }; </script>
-
7.5、限流与防抖
7.5.1、限流(节流)(规定时间内 只触发一次)
在JS中,如果一个事件频繁触发(比如用户疯狂点击按钮)并且处理函数处理耗时还比较长,那么就容易造成性能问题。
限流函数是针对这类问题的优化方式之一,它要求两次事件处理必须大于某个间隔时间,简而言之就是加了一层判断。
限流函数(throttle:节流阀) 的核心在于内部维护了一个“上次执行时间点”,通过比较当前执行时间与上次执行时间的差值判断是否“频繁”,是否执行。限流函数本身是一个装饰函数,修饰了事件处理器之后返回一个新的闭包函数。经过限流函数处理之后,事件触发的频率就被限制为预先传入的 interval 之上了。
栗子1:没有限流的一段登录代码,模拟登录:
<!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>没有节流的模拟登录栗子</title> </head> <body> <button id="btnLogin">登录</button> <script> let u1={name:"mark"}; let u2=u1; u2.name="###"; // console.log(u1.name); document.getElementById("btnLogin").addEventListener( "click", function (event) { setTimeout(()=>{ console.log("向服务器请求登录"); }); } ) </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>有节流的模拟登录栗子</title> </head> <body> <button id="btnLogin">登录</button> <script> // 节流函数 // 在interval时间内将要执行的fu函数 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来判断。例如:懒加载;
-
浏览器播放事件,每个一秒计算一次进度信息等
-
7.5.2、防抖(多次触发,只执行最后一次)
防抖函数也是一种限流函数,但要特殊一些。最典型的场景是表单输入,如果我们要在表单中监听 input 事件(比如远程搜索),那用户在输入的时候也会频繁触发,但这里使用 throttle 函数不行,因为我们需要等待用户停止输入一段时间后才能确认用户输入的值,所以要定义一个新的限流函数,叫做防抖函数。
防抖(防反跳)函数的核心是内部使用定时器并维护定时器返回的 ID 值,如果持续触发则不断 clearTimeout() 并重新发起 setTimeout(),通过这种方式等待事件触发完毕,然后进行延时处理。
栗子1:没有防抖带来的问题栗子:
<!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>
运行结果:
栗子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> 用户名:<input id="username" /> <script> // 防抖函数 ,对函数fn的调用防抖,delay是延迟时间 function debounce(fn, delay) { timer = null; return function () { let ctx = this; // 上下文 let args = arguments; //参数 if (timer) { // 清空时间,重新计时 clearTimeout(timer); } // 延迟delay指定的时间后执行函数 timer = setTimeout(function () { fn.apply(); }, delay); }; } document.getElementById("username").addEventListener( "keyup", debounce((event) => { console.log( "用户名" + ["存在", "不存在"][new Date().getMilliseconds() % 2] ); }, 2000), false ); </script> </body> <ml></ml> </html>
运行结果:
小结:
-
防抖策略(debounce)是当事件被触发后,延迟n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时。
-
作用: 高频率触发的事件,在指定的单位时间内,只响应最后一次,如果在指定的时间内再次触发,则重新计算时间。
-
防抖的应用场景:
-
登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖
-
调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
-
文本编辑器实时保存,当无任何更改操作一秒后进行保存
-
7.6、customRef
-
作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
- customRef 接收一个函数作为参数,这个函数接收两个函数作为参数 track (通知vue需要追踪后续内容的变化) 和 trigger (通知vue重新解析模板)
简单栗子1:自定义 ref
<template> 卡号:<input v-model="num" /> {{ num }} </template> <script lang="ts"> import { customRef } from "vue"; export default { setup() { // 自定义响应式函数,指定值value let myRef = function (value: any) { // 闭包 return customRef((track, trigger) => { return { get() { track(); //通知告诉vue追踪该变量,调用track函数 console.log("取值"); return value; // 返回值 }, set(newValue) { value = newValue; // 修改原来的值为新的值 if (!isNaN(newValue)) { // 如果是数字则赋值 value = newValue; } // 不过有没有值都更新页面 trigger(); //通知vue更新视图界面 }, }; }); }; let num = myRef("hello,大美女!"); return { num }; }, }; </script> <style scoped> </style>
运行结果:
栗子2:用 customRef 实现防抖功能:
<template> 卡号:<input v-model="msg" /> <hr/> {{ msg }} </template> <script lang="ts"> import { customRef, ref } from "vue"; export default { setup() { // 带防抖的自定义响应式函数 let myDebounceRef = function (value: any,delay:number) { // 时间 let time:number = 0; // 闭包,内部函数调用外部函数的time return customRef((track, trigger) => { return { get() { track(); //通知告诉vue追踪该变量,调用track函数 return value; // 返回值 }, set(newValue) { // 如上一种时钟存在。则清除,重新计时 if (time) { clearTimeout(time); } // 计时器,延时赋值 time = setTimeout(() => { value = newValue; // 修改原来的值为新的值 console.log("向服务器发起请求~"); trigger(); //通知vue更新视图界面 },delay); }, }; }); }; let msg = myDebounceRef("",1000); return { msg }; }, }; </script> <style scoped> </style>
运行结果:
栗子三: 自定义一个cardRef,实现限制用户的输入的卡号长度为16位,每隔4位用空格自动隔开,实现防抖功能,2000毫秒内只能向服务器发送一次查询卡号是否激活的状态,要求提供后台服务Spring Boot,模拟检查。
后台代码:
package com.fairy.cardtest.controller; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController @CrossOrigin(origins = "*") public class CardController { @GetMapping("/find") public int find(int card){ System.out.println(card); // 所有的卡号数据 int[] arr = {123,456,789,112233}; // 循环所有的卡号 for (int i = 0; i < arr.length; i++) { // 如果传过来的卡号与上面数组的匹配到了,就说明已激活 if (card == arr[i]){ return 1; } } // 否则就返回0,说明没有此卡号 return 0; } }
前端代码:
<template lang=""> <h1>我是银行</h1> <label for="card_number">请输入您的卡号:</label> <input type="text" id="card_number" v-model="card"> <hr/> 我的卡号:{{ card }} </template> <script lang="ts"> // 导入包 import { customRef } from 'vue'; import axios from 'axios'; export default { setup(){ // 计时器的 let time = 0; // 带防抖的响应式函数 let cardRef = function(value:any){ // 闭包 return customRef((track, trigger)=>{ // 一定要有的get()和set()方法 return{ get(){ track(); // 追踪该变量 return value; }, set(newvalue:any){ clearTimeout(time); // 一进入这个方法之前,先清除计时器,目的是为了关闭计时器,就不用调用多次请求 time = setTimeout(()=>{ // c变量目的是:清除上一次输入该值的空格 let c = newvalue.replaceAll(" ",""); // 判断该值是否是数字或空 if ( c !=null && !isNaN(c) ) { // 如果不为空而且是一个数字 // 限制该值不能超过16个字符 value = c.substring(0,16); // 每隔4个字符加一个空格 value = value.replace(/(.{4})/g, "$1 ") // 测试防抖,发起请求,查询文本框输出的值是否在后台数组里面,如果在,那就是已经激活,如果不在,那就证明没有这个卡号,所以就不存在激活 axios.get('http://localhost:8080/find',{ params :{ card: Number(value.replaceAll(" ","")) // 传一个已经清除了空格的值过去 } }).then(res=>{ if(res.data){ // 如果返回的是1,就已经激活,否则返回0 alert('已激活') }else{ alert('卡号都没有还想输入 傻逼!!!') } }) } trigger() //通知vue更新视图界面 },2000) } } }) } const card = cardRef(""); return {card} } } </script>
运行结果:
7.7、Options选项式API与Composition 组合式Api 的区别
7.7.1、Options Api
又叫选项 API,
以vue为后缀的文件,通过定义 methods
,computed
,watch
,data
等属性与方法,共同处理页面逻辑,如下图:
优缺点:
-
条例清晰,相同的放在相同的地方;但随着组件功能的增大,关联性会大大降低,组件的阅读和理解难度会增加
-
调用使用 this,但逻辑过多时 this 会出现问题,比如指向不明等;
-
其本身并不是有效的 js 代码 我们在使用 options API 的时候,需要确切了解我们具体可以访问到哪些属性,以及我们访问到的当前属性的行为在后台,vue需要将此属性转换为工作代码,因为 我们无法从自动建议和类型检查中受益,因此给我们在使用相关属性时,造成了一定弊端
7.7.2、Composition Api
又叫组合式API,
组件根据逻辑功能来组织的,一个功能所定义的所有 API 会放在一起(更加的高内聚,低耦合)
即使项目很大,功能很多,我们都能快速的定位到这个功能所用到的所有 API
优势 :
-
其代码更易读,更易理解和学习,没有任何幕后操作
-
Composition API的好处不仅仅是以不同的方式进行编码,更重要的是对于代码的重用
-
不受模板和组件范围的限制,也可以准确的知道我们可以使用哪些属性
-
由于幕后没有什么操作,所以编辑器可以帮助我们进行类型检查和建议
7.7.、Options选项式API与Composition 组合式Api对比:
假设一个组件是一个大型组件,其内部有很多处理逻辑关注点(对应下图不用颜色),在进行修改的时候,Composition API
看起来更加的有条理,更加方便阅读
总结:
-
在逻辑组织和逻辑复用方面,Composition API是优于Options API
-
因为Composition API几乎是函数,会有更好的类型推断。
-
Composition API对 tree-shaking 友好,代码也更容易压缩
-
Composition API中没有对this的使用,减少了this指向不明的情况
-
如果是小型组件,可以继续使用Options API,也是十分友好的
来源:https://blog.csdn.net/lhkuxia/article/details/115694267
7.8、VsCode-折叠代码
折叠代码可以让代码更加有序,方便查找与管理
语法: #region 代码 #endregion
//#region 功能数据
// 功能一的数据
// 功能二的数据
// 功能三的数据
//#endregion
//#region 功能方法
// 功能一的方法
// 功能二的方法
// 功能二的方法
//#endregion
//#region 功能计算属性
// 功能一的计算属性
// 功能二的计算属性
// 功能三的计算属性
//#endregion
7.9、provide 与 inject
父子组件传参可以通过 props
和 emit
来实现,但是当组件的层次结构比较深时,props
和 emit
就没什么作用了。vue为了解决这个提出了 Provide / Inject
7.9.1、概念解析
成对出现:provide和 inject 是成对出现的
作用:用于父组件向子孙组件传递数据,父组件有一个 provide
选项来提供数据,后代组件有一个 inject
选项来开始使用这些数据
使用方法:provide 在父组件中返回要传给下级的数据,inject在需要使用这个数据的子辈组件或者孙辈等下级组件中注入数据。
使用场景:由于vue有 $parent 属性可以让子组件访问父组件。但孙组件想要访问祖先组件就比较困难。通过 provide / inject 可以轻松实现跨级访问父组件的数据
栗子:
爷组件:App.vue
<template> <nav> <!-- 引入子组件 --> <router-link to="/example10_1">provide和inject 用法</router-link> | </nav> <router-view/> </template> <script lang="ts"> import { provide } from 'vue'; import { RouterLink, RouterView } from 'vue-router'; // 导出 export default { setup() { // 创建一个方法,把需要的东西导出 function msg(){ alert("你好,大美女!"); } provide("msg",msg); } } </script>
父组件:example10_1.vue:
<!-- 父组件 --> <template lang=""> <h1>父组件</h1> <!-- 组件标签 --> <example10_2Vue/> </template> <script lang="ts"> import example10_2Vue from './example10_2.vue'; export default { // 注册组件 components:{ example10_2Vue } } </script> <style lang=""> </style>
子组件:example10_2.vue:
<!-- 子组件 --> <template lang=""> <h1>子组件</h1> <button @click="msg">我是按钮</button> </template> <script lang="ts"> import { defineComponent, inject } from 'vue'; export default defineComponent({ name: 'example10_2', props: { msg: String, }, setup(){ let msg = inject("msg"); return {msg} } }); </script> <style lang=""> </style>
运行结果:
小结:provider / inject:简单的来说就是在父组件中通过 provider 来提供变量,然后在子组件中通过 inject 来注入变量
需要注意的是这里不论子组件有多深,只要调用了 inject 那么就可以注入 provider 中的数据。而不是局限于只能从当前父组件的 prop 属性来获取数据。
7.10、响应式数据的判断
-
isRef: 检查一个值是否为一个 ref 对象
-
isReactive: 检查一个对象是否是由
reactive
创建的响应式代理 -
isReadonly: 检查一个对象是否是由
readonly
创建的只读代理 -
isProxy: 检查一个对象是否是由
reactive
或者readonly
方法创建的代理