偵聽器
用于偵聽指定變量,當其響應式狀態變化時觸發回調函數。
watch()
watch() 需明確指定偵聽的數據源,并且僅當數據源變化時,才會執行回調,在創建偵聽器時,不會執行回調,可以獲取到數據源變化前后的值。
- 第一個參數為“數據源”,可以是一個 ref (包括計算屬性)、一個響應式對象、一個 getter 函數、或多個數據源組成的數組
- 第二個參數為回調函數
偵聽–響應式變量 / 計算屬性
const x = ref(0)
watch(x, (newX) => {console.log(`x is ${newX}`)
})
偵聽–響應式對象
會隱式地創建一個深層偵聽器,對象的屬性和嵌套屬性發生變化時,都會觸發回調
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {// 此處 `newValue` 和 `oldValue` 是相等的,因為它們是同一個對象!
})
若用getter 函數返回響應式對象 ,則只有在返回不同的對象時,才會觸發回調:
watch(() => state.someObject,() => {// 僅當 state.someObject 被替換時觸發}
)
通過添加 deep 選項,可以將其強制轉成深層偵聽器(即當對象的屬性和嵌套屬性發生變化時觸發回調)
watch(() => state.someObject,(newValue, oldValue) => {// 此處 `newValue` 和 `oldValue` 是相等的,除非 state.someObject 被整個替換了},{ deep: true }
)
偵聽–對象的屬性
方案1:用一個返回該屬性的 getter 函數
// 偵聽obj對象的count屬性
watch(() => obj.count,(count) => {console.log(`count is: ${count}`)}
)
方案2:使用 toRefs
import { ref, toRefs, watch } from "vue";
let obj = ref({ count: 30 });
let { count } = toRefs(obj.value);
watch(count, (newValue, oldValue) => {});
不能直接偵聽響應式對象的屬性值,因為屬性值非響應式
const obj = reactive({ count: 0 })// 錯誤,因為 watch() 得到的參數是一個 number
watch(obj.count, (count) => {console.log(`count is: ${count}`)
})
偵聽-- getter 函數
const x = ref(0)
const y = ref(0)watch(() => x.value + y.value,(sum) => {console.log(`sum of x + y is: ${sum}`)}
)
偵聽-- 多個數據源
// 多個來源組成的數組
watch([x, () => y.value], ([newX, newY]) => {console.log(`x is ${newX} and y is ${newY}`)
})
watchEffect()
watchEffect()在創建偵聽器時,會立即執行一遍回調,并從中自動分析出依賴的數據源(其響應性依賴關系不那么明確),當數據源發生改變時,再次觸發回調。無法獲取到數據源變化前的值。
watchEffect(async () => {const response = await fetch(url.value)data.value = await response.json()
})
上例中,在頁面創建時會先請求 url.value 接口獲得初始數據,并自動追蹤 url.value
當 url.value
變化時,會再次執行回調,訪問新的接口獲取數據。
改變回調的觸發時機
默認情況下,偵聽器回調會在 Vue 組件更新之前被調用(在偵聽器回調中訪問的 DOM 是被 Vue 更新之前的狀態)
添加 flush: 'post'
選項可以讓偵聽器回調在 Vue 組件更新之后再調用,這樣就能在偵聽器回調中訪問被 Vue 更新之后的 DOM 啦!
watch(source, callback, {flush: 'post'
})watchEffect(callback, {flush: 'post'
})
后置刷新的 watchEffect() 可以直接用 watchPostEffect()
import { watchPostEffect } from 'vue'watchPostEffect(() => {/* 在 Vue 更新后執行 */
})
停止偵聽器
同步語句創建的偵聽器,會在組件卸載時自動停止。
異步回調創建的偵聽器,必須手動停止它,以防內存泄漏。
setTimeout(() => {watchEffect(() => {})
}, 100)
手動停止偵聽器的方法是調用 watch 或 watchEffect 返回的函數
const unwatch = watchEffect(() => {})
unwatch()
異步創建偵聽器的情況很少,如果需要等待一些異步數據,可以使用條件式的偵聽邏輯:
// 需要異步請求得到的數據
const data = ref(null)watchEffect(() => {if (data.value) {// 數據加載后執行某些操作...}
})
模板引用 ref
用于直接訪問底層 DOM 元素,即 vue2 中的 $refs
<input ref="input" />
import { ref, onMounted } from 'vue'// 聲明一個ref變量來存放該元素的引用,變量名必須和模板里的 ref 屬性值相同
const input = ref(null)onMounted(() => {// 頁面加載后,輸入框自動獲得焦點input.value.focus()
})
只可以在組件掛載后才能訪問模板引用
若偵聽模板引用 ref 的變化,需考慮到其值為 null 的情況:
watchEffect(() => {if (input.value) {input.value.focus()} else {// 此時還未掛載,或此元素已經被卸載(例如通過 v-if 控制)}
})
ref 綁定函數
<input :ref="(el) => { /* 將 el 賦值給一個數據屬性或 ref 變量 */ }">
- 綁定函數需使用
:ref
- 每次Dom更新時函數都會被調用
- Dom被卸載時,函數也會被調用一次,此時 el 參數的值是 null
子組件上的 ref
若子組件使用的是選項式 API 或沒有使用 <script setup>
,則對子組件的模板引用即子組件的 this,可以直接訪問子組件的屬性和方法。(但仍推薦用標準的 props 和 emit 接口來實現父子組件交互)
使用了 <script setup>
的子組件是默認私有的:父組件無法訪問私有子組件中的任何東西,除非子組件通過 defineExpose 宏顯式暴露。
<script setup>
import { ref } from 'vue'const a = 1
const b = ref(2)defineExpose({a,b
})
</script>
父組件通過模板引用獲取到的實例類型為 { a: number, b: number } (ref 都會自動解包,和一般的實例一樣)。
父子組件
父組件中使用子組件 import
vue3 中導入后就能直接使用,無需像 vue2 中進行注冊
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
<template><ButtonCounter />
</template>
子組件接收父組件傳入的數據 props
子組件用 defineProps() 接收父組件傳入的數據
<script setup>
defineProps(['title'])
</script><template><h4>{{ title }}</h4>
</template>
defineProps() 的參數和 props 選項的值相同
defineProps({title: String,likes: Number
})
搭配 TypeScript 使用類型標注來聲明 props
<script setup lang="ts">
defineProps<{title?: stringlikes?: number
}>()
</script>
選項式風格中,props 對象會作為 setup() 函數的第一個參數被傳入:
export default {props: ['title'],setup(props) {console.log(props.title)}
}
子組件觸發自定義事件 emits
組件觸發的事件不會冒泡,父組件只能監聽直接子組件觸發的事件。
父組件–在引入的子組件上綁定事件
<BlogPost @enlarge-text="postFontSize += 0.1"/>
子組件–用 defineEmits() 聲明事件
<button @click="$emit('enlarge-text')">Enlarge text</button>
<script setup>
defineEmits(['enlarge-text']) // 多個事件則為 defineEmits(['inFocus', 'submit'])
</script>
選項式風格中,通過 emits 選項定義組件會拋出的事件,并用 setup() 的第二個參數(上下文對象)訪問 emit 函數:
export default {emits: ['enlarge-text'],setup(props, ctx) {ctx.emit('enlarge-text')}
}
事件傳參
子組件
<button @click="$emit('increaseBy', 1)"></button>
父組件
<MyButton @increase-by="(n) => count += n" />
或
<MyButton @increase-by="increaseCount" />
function increaseCount(n) {count.value += n
}
子組件繼承樣式
vue2 中限定只能有一個根節點,父組件中給子組件添加的樣式,都會渲染在子組件的根節點上,如:
<!-- 子組件 -->
<p class="child">你好</p>
<!-- 父組件使用子組件時,添加了新的樣式 father -->
<MyComponent class="father" />
最終渲染的效果為:
<p class="child father">你好</p>
vue3 中支持多個根節點,所以需要通過 $attrs
指定具體哪些節點繼承父組件添加的樣式。
<!-- 子組件:在需要繼承樣式的元素上,添加 :class="$attrs.class" -->
<p :class="$attrs.class">你好</p>
<span>我是朝陽</span>
<!-- 父組件使用子組件時,添加了新的樣式 father -->
<MyComponent class="father" />
最終渲染的效果為:
<p class="father">你好</p>
<span>我是朝陽</span>