第8章 組合API與響應性
目錄
8.1 響應性
8.1.1 什么是響應性
8.1.2 響應性原理
8.2 為什么使用組合API
8.3 setup組件選項
8.3.1 setup函數的參數
8.3.2 setup函數的返回值
8.3.3 使用ref創建響應式引用
8.3.4 setup內部調用生命周期鉤子函數
8.4 提供/注入
8.4.1 provide方法
8.4.2 inject方法
8.5 模板引用
8.6 響應式計算與偵聽
8.6.1 響應式計算
8.6.2 響應式偵聽
8.1 響應性
8.1.1 什么是響應性
響應性是一種允許我們以聲明式的方式去適應變化的一種編程范例。Vue.js如何追蹤數據的變化呢?在生成Vue.js實例時,使用帶有getter和setter的處理程序遍歷傳入的data,將其所有property轉換為Proxy對象。Proxy代理對象,顧名思義,在訪問對象前增加一個中間層,通過中間層做一個中轉,通過操作代理對象,實現目標對象的修改。Proxy對象對于用戶來說是不可見的,但在內部,它使Vue.js能夠在property值被訪問或修改的情況下進行依賴跟蹤和變更通知。
【例8-1】property轉換為Proxy對象。
<script>const data = {uname: 'chenheng',age: 90}const handler = {get(target, name, receiver) {alert('執行get方法')//Reflect.get 方法查找并返回 target 對象的 name 屬性,如果沒有該屬性,則返回 undefinedreturn Reflect.get(...arguments)},set(target, name, value, receiver) {alert('執行set方法')//Reflect.set 方法設置 target 對象的 name 屬性等于 value。return Reflect.set(...arguments)}}const proxy = new Proxy(data, handler)alert(proxy.uname) //執行get方法proxy.uname = 'hhhhh' //執行set方法alert(proxy.uname) //執行get方法
</script>
target
要包裝的目標對象Proxy。它可以是任何類型的對象,包括本機數組,函數甚至其他代理
handler
一個對象,其屬性是定義對代理p執行操作時的行為的函數
proxy監聽數組
?proxy可以監聽屬性的新增刪除操作
proxy監聽深層次嵌套對象
8.1.2 響應性原理
reactive()方法和watchEffect()方法是Vue3中響應式的兩個核心方法,reactive()方法負責將數據變成響應式代理對象,watchEffect()方法的作用是監聽數據變化去更新視圖或調用函數。
【例8-2】reactive()方法和watchEffect()方法的應用。
8.2 為什么使用組合API
通過創建Vue.js組件,可以將接口的可重復部分及其功能提取到可重用的代碼段中,從而使應用程序可維護且靈活。然而,當應用程序非常復雜(成百上千組件)時,再使用組件的選項(data、computed、methods、watch)組織邏輯,可能導致組件難以閱讀和理解。如果能夠將與同一個邏輯相關的代碼配置在一起將有效解決邏輯復雜、可讀性差等問題。這正是使用組合API的目的。
8.3 setup組件選項
Vue組件提供setup選項,供開發者使用組合API。setup選項在創建組件前執行,一旦props被解析,便充當組合式API的入口點。由于在執行setup時尚未創建組件實例,因此在setup選項中沒有this。這意味著,除了props之外,無法訪問組件中聲明的任何屬性,包括本地狀態、計算屬性或方法。
setup選項是一個接受props和context參數的函數。此外,從setup返回的所有內容都將暴露給組件的其余部分(計算屬性、方法、生命周期鉤子、模板等等)。
8.3.1 setup函數的參數
(1)setup函數中的第一個參數(props)
setup函數中的props是響應式的,當傳入新的屬性時,它將被更新。
【例8-3】在setup函數中,參數props是響應式的。
但是,因為props是響應式的,不能使用ES6解構,將會消除props的響應性。如果需要解構props,可以在setup函數中使用toRefs函數來完成此操作。
【例8-4】在setup函數中,使用toRefs函數創建props屬性的響應式引用。
toRef是把對象的某個屬性改成響應式的數據,toRefs是把整個對象改成響應式數據
(2)setup函數中的第二個參數(context)
context上下文是一個普通的JavaScript對象,它暴露組件的4個屬性:attrs、slots、emit以及expose。
setup(props, context) {
??? // Attribute (非響應式對象,等同于 $attrs)
??? console.log(context.attrs)可以獲取父組件的傳遞歸來的參數hobby,但是一定需要注釋props
??? // 插槽 (非響應式對象,等同于 $slots)
??? console.log(context.slots)
??? // 觸發事件 (方法,等同于 $emit)
?
?home.vue
<template><Demo @zemit="showemit"></Demo>
</template><script>
import Demo from "@/components/demo.vue";
export default {components: {Demo,},setup() {function showemit(val) {alert(`觸發context.emit事件,收到參數是:${val}`);}return {showemit,};},
};
</script>
?demo.vue
<template><p>個人信息</p><p>姓名:{{ person.name }}</p><p>年齡:{{ person.age }}</p><button @click="zevent">zemit事件</button>
</template><script>
import { reactive } from "vue";
export default {name: "Home12",emits: ["zemit"],setup(props, context) {console.log("1", props);// console.log("context.attrs", context.attrs);console.log("context.attrs", context.emit);const person = reactive({name: "劉巍",age: 18,});function zevent() {context.emit("zemit", '南昌大學');}return { person,zevent,};},
};
</script>
?
8.3.2 setup函數的返回值
(1)對象
如果setup返回一個對象,則可以在組件的模板中訪問該對象的屬性。
【例8-5】在該實例中,setup函數返回一個對象。
<template><h1>一個人的信息</h1><h3>職業:{{ job.type }}</h3><h3>薪水:{{ job.salary }}</h3><h3>愛好:{{ hobby }}</h3><h3>測試數據的值:{{ job.a.b.c }}</h3><button @click="changeInfo">修改人的信息</button>
</template><script>
import {reactive} from 'vue'export default {name: 'App',setup() {//數據let job = reactive({type: 'SAP工程師',salary: '60k',a: {b: {c: 666}}})let hobby = reactive(['籃球', '說泡', '旅游'])//counts changeInfo = ()=>{...}function changeInfo() {job.type = "管理咨詢顧問"job.salary = "100k"job.a.b.c = 999hobby[0] = '學習'}return {job,hobby,changeInfo}}
}
</script>
(2)渲染函數
setup還可以返回一個渲染函數,該函數可以直接使用在同一作用域中聲明的響應式狀態。
【例8-6】實現【例8-5】的功能,要求setup返回渲染函數。
8.3.3 使用ref創建響應式引用
1.聲明響應式狀態
要為JavaScript對象創建響應式狀態,可以使用reactive()方法。reactive()方法接收一個普通對象然后返回該對象的響應式代理。示例代碼如下:
const book = Vue.reactive({ title: '好書' })
reactive()方法響應式轉換是“深層的”即影響對象內部所有嵌套的屬性。基于ES的 Proxy實現,返回的代理對象不等于原始對象。建議使用代理對象,避免依賴原始對象。
2.使用ref創建獨立的響應式值對象
ref接受一個參數值并返回一個響應式且可改變的ref對象。ref對象擁有一個指向內部值的單一屬性.value。示例代碼如下:
const readersNumber = Vue.ref(1000)
console.log(readersNumber.value) //1000
readersNumber.value++
console.log(readersNumber.value) // 1001
當ref作為渲染上下文的屬性返回(即在setup()返回的對象中)并在模板中使用時,它會自動開箱,無需在模板內額外書寫.value。
8.3.4 setup內部調用生命周期鉤子函數
在setup內部,可通過在生命周期鉤子函數前面加上“on”來訪問組件的生命周期鉤子函數。因為setup是圍繞beforeCreate和created生命周期鉤子函數運行的,所以不需要顯式地定義它們。換句話說,在這些鉤子函數中編寫的任何代碼都應該直接在setup函數中編寫。這些on函數接受一個回調函數,當鉤子函數被組件調用時將會被執行。示例代碼如下:
setup() {
?// mounted時執行
?onMounted(() => {
? console.log('Component is mounted!')
? })
}
8.4 提供/注入
通過4.3.4節可知,使用provide和inject可實現組件鏈傳值。也就是說,父組件可以作為其所有子組件的依賴項提供程序,而不管組件層次結構有多深,父組件有一個provide選項來提供數據,子組件有一個inject選項來使用這個數據。(跨組件的數據傳遞)
現在,在組合API中,也可以使用provide方法和inject方法實現傳值,但兩者都只能在當前活動實例的setup()期間調用。
8.4.1 provide方法
首先,從vue顯式導入provide方法;然后,在setup()中使用provide方法定義每個property。
provide方法有兩個參數:
l? name:代表字符串類型的屬性名稱;
l? value:代表任意類型的屬性值。
8.4.2 inject方法
首先,從vue顯式導入inject方法;然后,在setup()中使用inject方法注入每個property值。
inject方法有兩個參數:
l? name:被注入的屬性名稱(字符串類型);
l? defaultValue:默認值(可選)。
假設我們有一個祖先組件?app
,一個中間組件?one
,以及一個后代組件?two
。我們希望從?app
傳遞一個數據到?two
。
app.vue
<template><one></one>
</template><script setup>
// import HelloWorld from './components/HelloWorld.vue'
// import setup from './components/setup.vue'
// import setup1 from './components/setup1.vue'
import one from './components/one.vue'
import { provide } from 'vue';
const hcm = 'payroll';
provide('sap', hcm)
</script><style>
#app {font-family: Avenir, Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;text-align: center;color: #2c3e50;margin-top: 60px;
}
</style>
?one.vue
<template><two/>
</template><script setup>
import two from '../components/two.vue';
</script>
two.vue
<template><div :class="theme">我是SAP {{ theme }} 顧問.</div>
</template><script setup>
import { inject } from 'vue';const theme = inject('sap', 'time'); // 'time' 是默認值,如果 provide 沒有提供 'theme'
</script><style scoped>
.dark {background-color: black;color: white;
}.light {background-color: white;color: black;
}
</style>
8.5 模板引用
在使用組合API時,響應式引用和模板引用的概念是統一的。為了獲得對模板內元素或組件實例的引用,可以聲明一個ref并從setup()返回。
【例8-8】在模板中使用ref引用響應式對象。
8.6 響應式計算與偵聽
8.6.1 響應式計算
使用響應式計算方法computed有兩種方式:傳入一個getter函數,返回一個默認不可手動修改的ref對象;傳入一個擁有get和set函數的對象,創建一個可手動修改的計算狀態。
【例8-9】返回一個默認不可手動修改的ref對象。
<template><div>{{count}}</div>
</template><script>
import{ref,computed} from 'vue';export default {setup() {const count = ref(1)const account = computed(()=> count.value + 1)console.log(account.value)account.value++// 返回值會暴露給模板和其他的選項式 API 鉤子return {count}}
}
</script>
【例8-10】返回一個可手動修改的ref對象。
<template><div>{{count}}</div>
</template><script>
import{ref,computed} from 'vue';export default {setup() {const count = ref(1)const account = computed({get:()=> count.value + 1,set:(val)=> {count.value = val - 1},} )account.value = 1console.log(count.value)return {count}}
}
</script>
8.6.2 響應式偵聽
可使用響應性偵聽watchEffect方法,對響應性進行偵聽。該方法立即執行傳入的一個函數,同時響應式追蹤其依賴,并在其依賴變更時重新運行該函數。(監聽所有屬性)
【例8-11】響應性偵聽watchEffect方法的使用。
監聽屬性變化
<template><div><input type="text" v-model="obj.name"> </div>
</template><script>
import{reactive,watchEffect} from 'vue';export default {setup() {let obj = reactive({name:'vivi'});watchEffect(()=>{console.log('name:',obj.name)})return {obj}}
}
</script>
停止監聽
<template><div><input type="text" v-model="obj.name"> <button @click="stopWatchEffect">停止監聽</button></div>
</template><script>
import{reactive,watchEffect} from 'vue';export default {setup() {let obj = reactive({name:'vivi'});const stop1 = watchEffect(()=>{console.log('name',obj.name)})const stopWatchEffect = ()=>{console.log('停止監聽')stop1(); // ...當該偵聽器不再需要時}return {obj,stopWatchEffect,}}
}
</script>
副作用
使用 onInvalidate 清理計時器,每次 count 變化時,watchEffect 會重新執行,在此之前?onInvalidate 會先清理掉之前的計時器,避免重復創建計時器導致內存泄漏。
注意:如果在?watchEffect 沒有直接使用?count.value ,那么它的變化就不會觸發副作用函數重新執行,從而不會調用?onInvalidate 清理之前的計時器
<template><div><p>當前計數: {{ count }}</p><button @click="count++">增加計數</button></div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {setup() {const count = ref(0);watchEffect((onInvalidate) => {// 在函數內直接讀取 count.value,確保它被追蹤,這一步很重要!!!console.log(`副作用函數執行,count 值為: ${count.value}`);const timer = setInterval(() => {console.log(`計時器中 count 的值: ${count.value}`);}, 1000);onInvalidate(() => {console.log('清除計時器 timer');clearInterval(timer);});});return {count,};},
};
</script>