在 Vue3 測試版剛剛釋出的時候,我就學習了下 Composition API,但沒想到正式版時隔一年多才出來,看了一下發現還是增加了不少新特性的,在這裡我就將它們一一梳理一遍。
本文章只詳細闡述 Vue3 中重要或常用的新特性,如果想了解全部的特性請轉:Vue3 響應性基礎 API
Composition API
這是一個非常重要的改變,我認為 Composition API 最大的用處就是將響應式資料和相關的業務邏輯結合到一起,便於維護(這樣做的優點在處理龐大元件的時候顯得尤為重要)。
之所以叫做 Composition API(或組合式 API) 是因為所有的響應式資料和業務邏輯程式碼都可以放在 setup 方法中進行處理,我們通過程式碼看一下 Vue2 的 Options API 和 Composition API 的區別:
/* Options API */
export default {
props: {},
data(){},
computed: {},
watch: {},
methods: {},
created(),
components:{}
// ...other options
}
/* Composition API */
export default {
props: {},
setup(),
components:{}
}
這就是兩種 API 在大致結構上的不同,雖然 Composition API 提倡使用 setup
來暴露元件的 data
、computed
、watch
、生命週期鉤子... 但並不意味著強制使用,在 Vue3 中同樣可以選擇 Options API 或者兩種寫法混用。
接下來我們看看在 setup 的使用。
setup
執行時機
setup
在 beforeCreate
之前執行,因此訪問不到元件例項,換句話說 setup 內無法使用 this 訪問元件例項。
引數
setup
方法接受兩個引數 setup(props, context)
,props
是父元件傳給元件的資料,context
(上下文) 中包含了一些常用屬性:
attrs
attrs
表示由上級傳向該元件,但並不包含在 props
內的屬性:
<!-- parent.vue -->
<Child msg="hello world" :name="'child'"></Child>
/* child.vue */
export default {
props: { name: String },
setup(props, context) {
console.log(props) // {name: 'child'}
console.log(context.attrs) // {msg: 'hello world'}
},
}
emit
用於在子元件內觸發父元件的方法
<!-- parent.vue -->
<Child @sayWhat="sayWhat"></Child>
/* child.vue */
export default {
setup(_, context) {
context.emit('sayWhat')
},
}
slots
用來訪問被插槽分發的內容,相當於 vm.$slots
<!-- parent.vue -->
<Child>
<template v-slot:header>
<div>header</div>
</template>
<template v-slot:content>
<div>content</div>
</template>
<template v-slot:footer>
<div>footer</div>
</template>
</Child>
/* child.vue */
import { h } from 'vue'
export default {
setup(_, context) {
const { header, content, footer } = context.slots
return () => h('div', [h('header', header()), h('div', content()), h('footer', footer())])
},
}
生命週期
Vue3 的生命週期除了可以使用傳統的 Options API 形式外,也可以在 setup
中進行定義,只不過要在前面加上 on
:
export default {
setup() {
onBeforeMount(() => {
console.log('例項建立完成,即將掛載')
})
onMounted(() => {
console.log('例項掛載完成')
})
onBeforeUpdate(() => {
console.log('元件dom即將更新')
})
onUpdated(() => {
console.log('元件dom已經更新完畢')
})
// 對應vue2 beforeDestroy
onBeforeUnmount(() => {
console.log('例項即將解除掛載')
})
// 對應vue2 destroyed
onUnmounted(() => {
console.log('例項已經解除掛載')
})
onErrorCaptured(() => {
console.log('捕獲到一個子孫元件的錯誤')
})
onActivated(() => {
console.log('被keep-alive快取的元件啟用')
})
onDeactivated(() => {
console.log('被keep-alive快取的元件停用')
})
// 兩個新鉤子,可以精確地追蹤到一個元件發生重渲染的觸發時機和完成時機及其原因
onRenderTracked(() => {
console.log('跟蹤虛擬dom重新渲染時')
})
onRenderTriggered(() => {
console.log('當虛擬dom被觸發重新渲染時')
})
},
}
Vue3 沒有提供單獨的 onBeforeCreate
和 onCreated
方法,因為 setup
本身是在這兩個生命週期之前執行的,Vue3 建議我們直接在 setup
中編寫這兩個生命週期中的程式碼。
Reactive API
ref
ref
方法用來為一個指定的值(可以是任意型別)建立一個響應式的資料物件,該物件包含一個 value
屬性,值為響應式資料本身。
對於 ref
定義的響應式資料,無論獲取其值還是做運算,都要用 value
屬性。
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
const obj = ref({ a: 2 })
console.log(obj.value.a) // 2
return {
count,
obj,
}
},
}
但是在 template
中訪問 ref
響應式資料,是不需要追加 .value
的:
<template>
<div>
<ul>
<li>count: {{count}}</li>
<li>obj.a: {{obj.a}}</li>
</ul>
</div>
</template>
reactive
和 ref
方法一樣,reactive
也負責將目標資料轉換成響應式資料,但該資料只能是引用型別。
<template>
<div>{{obj.a}}</div>
</template>
<script>
export default {
setup() {
const obj = reactive({ a: 2 })
obj.a++
console.log(obj.a) // 3
return { obj }
},
}
</script>
可以看出 reactive
型別的響應式資料不需要在後面追加 .value
來呼叫或使用。
reactive 和 ref 的區別
看上去 reactive
和 ref
十分相似,那麼這兩個方法有什麼不同呢?
實際上 ref
本質上與 reactive
並無區別,來看看 Vue3 的部分原始碼(來自於 @vue/reactivity/dist/reactivity.cjs.js
):
function ref(value) {
return createRef(value)
}
function createRef(rawValue, shallow = false) {
/**
* rawValue表示呼叫ref函式時傳入的值
* shallow表示是否淺監聽,預設false表示進行深度監聽,也就是遞迴地將物件/陣列內所有屬性都轉換成響應式
*/
if (isRef(rawValue)) {
// 判斷傳入ref函式的資料是否已經是一個ref型別的響應式資料了
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl {
constructor(_rawValue, _shallow = false) {
// 用於儲存未轉換前的原生資料
this._rawValue = _rawValue
// 是否深度監聽
this._shallow = _shallow
// 是否為ref型別
this.__v_isRef = true
// 如果為深度監聽,則使用convert遞迴將所有巢狀屬性轉換為響應式資料
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
track(toRaw(this), 'get' /* GET */, 'value')
return this._value
}
set value(newVal) {
if (shared.hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), 'set' /* SET */, 'value', newVal)
}
}
}
// 如果val滿足:val !== null && typeof val === 'object',則使用reactive方法轉換資料
const convert = (val) => (shared.isObject(val) ? reactive(val) : val)
如果你不明白上面的程式碼做了什麼,假設我現在執行這行程式碼:
const count = ref(0)
那麼實際上 ref
函式返回的是一個 RefImpl
例項,裡面包含如下屬性:
{
_rawValue: 0,
_shallow: false,
__v_isRef: true,
_value: 0
}
通過 RefImpl
類的 get value()
方法可以看出,呼叫 value
屬性返回的其實就是 _value
屬性。
Vue3 建議在定義基本型別的響應式資料時使用
ref
是因為基本型別不存在引用效果,這樣一來在其他地方改變該值便不會觸發響應,因此ref
將資料包裹在物件中以實現引用效果。
Vue3 會判斷 template
中的響應式資料是否為 ref
型別,如果為 ref
型別則會在尾部自動追加 .value
,判斷方式很簡單:
function isRef(r) {
return Boolean(r && r.__v_isRef === true)
}
那麼其實我們是可以用 reactive
來偽裝成 ref
的:
<template>
<div>{{count}}</div>
</template>
<script>
export default {
setup() {
const count = reactive({
value: 0,
__v_isRef: true,
})
return { count }
},
}
</script>
雖然這樣做毫無意義,不過證明了 Vue3 確實是通過 __v_isRef
屬性判斷資料是否為 ref
定義的。
我們再看看 reactive
的實現:
function reactive(target) {
if (target && target['__v_isReadonly' /* IS_READONLY */]) {
return target
}
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
if (!shared.isObject(target)) {
{
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 如果target已經被代理,直接返回target
if (target['__v_raw' /* RAW */] && !(isReadonly && target['__v_isReactive' /* IS_REACTIVE */])) {
return target
}
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const targetType = getTargetType(target)
if (targetType === 0 /* INVALID */) {
return target
}
const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers)
proxyMap.set(target, proxy)
return proxy
}
reactive
方法會呼叫 createReactiveObject
代理物件中的各個屬性來實現響應式,在使用 ref
定義引用型別資料的時候同樣會用到這個方法:
export default {
setup() {
console.log(ref({ a: 123 }))
console.log(reactive({ a: 123 }))
},
}
可以看到 ref
物件的 _value
屬性和 reactive
一樣都被代理了。
綜上所述,我們可以簡單將 ref
看作是 reactive
的二次包裝,只不過多了幾個屬性罷了。
明白了 ref
和 reactive
的大致實現和關係,我們再來看其他的響應式 API。
isRef & isReactive
判斷一個值是否是 ref 或 reactive 型別:
const count = ref(0)
const obj = reactive({ a: 123 })
console.log(isRef(count)) // true
console.log(isRef(obj)) // false
console.log(isReactive(count)) // false
console.log(isReactive(obj)) // true
customRef
自定義 ref,常用來定義需要非同步獲取的響應式資料,舉個搜尋框防抖的例子:
function useDebouncedRef(value, delay = 1000) {
let timeout
return customRef((track, trigger) => {
/**
* customRef回撥接受兩個引數
* track用於追蹤依賴
* trigger用於出發響應
* 回撥需返回一個包含get和set方法的物件
*/
return {
get() {
track() // 追蹤該資料
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger() // 資料被修改,更新ui介面
}, delay)
},
}
})
}
export default {
setup() {
const text = useDebouncedRef('')
const searchResult = reactive({})
watch(text, async (newText) => {
if (!newText) return void 0
const result = await new Promise((resolve) => {
console.log(`搜尋${newText}中...`)
resolve(`${newText}的搜尋結果在這裡`)
})
searchResult.data = result
})
return {
text,
searchResult,
}
},
}
<template>
<input v-model="text" />
<div>{{searchResult.data}}</div>
</template>
在這個例子中我們使用 customRef
和防抖函式,延遲改變 text.value
值,當 watch
監聽到 text
的改變再進行搜尋以實現防抖搜尋。
toRef & toRefs
toRef
可以將一個 reactive
形式的物件的屬性轉換成 ref
形式,並且 ref
物件會保持對源 reactive
物件的引用:
const obj1 = reactive({ a: 1 })
const attrA = toRef(obj1, 'a')
console.log(obj1.a) // 1
console.log(attrA.value) // 1
console.log(obj1 === attrA._object) // true
attrA.value++
console.log(obj1.a) // 2
如果使用 ref
,那麼由於 obj1.a
本身是一個基本型別值,最後會生成一個與原物件 obj1
毫無關係的新的響應式資料。
我們來看一下 toRef
的原始碼:
function toRef(object, key) {
return isRef(object[key]) ? object[key] : new ObjectRefImpl(object, key)
}
class ObjectRefImpl {
constructor(_object, _key) {
this._object = _object
this._key = _key
this.__v_isRef = true
}
get value() {
return this._object[this._key]
}
set value(newVal) {
this._object[this._key] = newVal
}
}
可以看到其涉及的程式碼非常簡單,ObjectRefImpl
類就是為了保持資料與源物件之間的引用關係(設定新 value
值同時會改變原物件對應屬性的值)。
可能你已經注意到 ObjectRefImpl
類並沒有像 ref
方法用到的 RefImpl
類一樣在 get
和 set
時使用 track
追蹤改變和用 trigger
觸發 ui
更新。
因此可以得出一個結論,toRef 方法所生成的資料僅僅是儲存了對源物件屬性的引用,但該資料的改變可能不會直接觸發 ui 更新!,舉個例子:
<template>
<ul>
<li>obj1: {{obj1}}</li>
<li>attrA: {{attrA}}</li>
<li>obj2: {{obj2}}</li>
<li>attrB: {{attrB}}</li>
</ul>
<button @click="func1">addA</button>
<button @click="func2">addB</button>
</template>
<script>
import { reactive, toRef } from 'vue'
export default {
setup() {
const obj1 = { a: 1 }
const attrA = toRef(obj1, 'a')
const obj2 = reactive({ b: 1 })
const attrB = toRef(obj2, 'b')
function func1() {
attrA.value++
}
function func2() {
attrB.value++
}
return { obj1, obj2, attrA, attrB, func1, func2 }
},
}
</script>
可以看到,點選 addA 按鈕不會觸發介面渲染,而點選 addB 會更新介面。雖然 attrB.value
的改變確實會觸發 ui 更新,但這是因為 attrB.value
的改變觸發了 obj2.b
的改變,而 obj2
本身就是響應式資料,所以 attrB.value
的改變是間接觸發了 ui 更新,而不是直接原因。
再來看看 toRefs
,toRefs
可以將整個物件轉換成響應式物件,而 toRef
只能轉換物件的某個屬性。但是 toRefs
生成的響應式物件和 ref
生成的響應式物件在用法上是有區別的:
const obj = { a: 1 }
const refObj = ref(obj)
const toRefsObj = toRefs(obj)
console.log(refObj.value.a) // 1
console.log(toRefsObj.a.value) // 1
toRefs
是將物件中的每個屬性都轉換成 ref
響應式物件,而 reactive
是代理整個物件。
shallowRef & shallowReactive
ref
和 reactive
在預設情況下會遞迴地將物件內所有的屬性無論巢狀與否都轉化為響應式,而 shallowRef
和 shallowReactive
則只將第一層屬性轉化為響應式。
const dynamicObj2 = shallowReactive({ a: 1, b: { c: 2 } })
console.log(isReactive(dynamicObj2)) // true
console.log(isReactive(dynamicObj2.b)) // false
dynamicObj2.a++ // 觸發ui更新
dynamicObj2.b.c++ // 不觸發ui更新
const dynamicObj3 = shallowRef({ a: 1, b: { c: 2 } })
console.log(isRef(dynamicObj3)) // true
// ref函式在處理物件的時候會交給reactive處理,因此使用isReactive判斷
console.log(isReactive(dynamicObj3.value)) // false
我們可以發現,shallowRef
和 shallowReactive
型別的響應式資料,在改變其深層次屬性時候是不會觸發 ui 更新的。
注意:
shallowRef
的第一層是value
屬性所在的那一層,而a
是在第二層,因此只有當value
改變的時候,才會觸發 ui 更新
triggerRef
如果 shallowRef
只有在 value
改變的時候,才會觸發 ui 更新,有沒有辦法在其他情況下手動觸發更新呢?有的:
const dynamicObj3 = shallowRef({ a: 1, b: { c: 2 } })
function func() {
dynamicObj3.value.b.c++
triggerRef(dynamicObj3) // 手動觸發ui更新
}
readonly & isReadonly
readonly
可將整個物件(包含其內部屬性)變成只讀的,並且是深層次的。
isReadonly
通過物件中的 __v_isReadonly
屬性判斷物件是否只讀。
const obj3 = readonly({ a: 0 })
obj3.a++ // warning: Set operation on key "a" failed: target is readonly
obj3.b.c++ // Set operation on key "c" failed: target is readonly
console.log(obj3.a) // 0
console.log(isReadonly(obj3)) // true
console.log(isReadonly(obj3.b)) // true
toRaw
toRaw
可以返回 reactive
或 readonly
所代理的物件。
const obj3 = { a: 123 }
const readonlyObj = readonly(obj3)
const reactiveObj = reactive(obj3)
const refObj = ref(obj3)
console.log(toRaw(readonlyObj) === obj3) // true
console.log(toRaw(reactiveObj) === obj3) // true
console.log(refObj._rawValue === obj3) // true
function toRaw(observed) {
return (observed && toRaw(observed['__v_raw' /* RAW */])) || observed
}
事實上,無論是 reactive
還是 readonly
,都會將源物件儲存一份在屬性 __v_raw
中,而 ref
會將源物件或值儲存在 _rawValue
屬性中。
computed
Vue3 將 computed
也包裝成了一個方法,我們看看 computed
的原始碼:
function computed(getterOrOptions) {
let getter
let setter
// 判斷getterOrOptions是否為函式
if (shared.isFunction(getterOrOptions)) {
// 如果是函式,就作為getter,這種情況下只能獲取值,更改值則會彈出警告
getter = getterOrOptions
setter = () => {
console.warn('Write operation failed: computed value is readonly')
}
} else {
// 如果不是函式,將getterOrOptions中的get和set方法賦給getter和setter
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return new ComputedRefImpl(getter, setter, shared.isFunction(getterOrOptions) || !getterOrOptions.set)
}
我們可以發現,computed
接收兩種不同的引數:
computed(() => {}) // only getter
computed({ get: () => {}, set: () => {} }) // getter and setter
和 Vue2 一樣,computed
既可以單純的用 getter
計算並返回資料,也可以設定 setter
使其變得可寫。
const count = ref(1)
const countCpy = computed(() => count.value * 2)
// 由於computed返回的是ref物件,因此使用value獲取值
console.log(countCpy.value) // 2
const countCpy2 = computed({
get: () => count.value,
set: (newVal) => {
count.value = newVal
},
})
countCpy2.value = 10
console.log(countCpy2.value) // 10
watch & watchEffect
Vue3 的 watch
和 Vue2 的 vm.$watch
效果是相同的。
watch
可以對一個 getter
發起監聽:
const count = ref(2)
watch(
() => Math.abs(count.value),
(newVal, oldVal) => {
console.log(`count的絕對值發生了變化!count=${newVal}`)
}
)
count.value = -2 // 沒有觸發watch
count.value = 1 // count的絕對值發生了變化!count=1
也可以偵聽一個 ref
:
const count = ref(2)
watch(count, (newVal, oldVal) => {
console.log(`count值發生了變化!count=${newVal}`)
})
count.value = -1 // count的絕對值發生了變化!count=-1
watch
不僅可以監聽單一資料,也可以監聽多個資料:
const preNum = ref('')
const aftNum = ref('')
watch([preNum, aftNum], ([newPre, newAft], [oldPre, oldAft]) => {
console.log('資料改變了')
})
preNum.value = '123' // 資料改變了
aftNum.value = '123' // 資料改變了
watchEffect
會在其任何一個依賴項發生變化的時候重新執行,其返回一個函式用於取消監聽。
const count = ref(0)
const obj = reactive({ a: 0 })
const stop = watchEffect(() => {
console.log(`count或obj發生了變化,count=${count.value},obj.a=${obj.a}`)
})
// count或obj發生了變化,count=0,obj.a=0
count.value++ // count或obj發生了變化,count=1,obj.a=0
obj.a++ // count或obj發生了變化,count=1,obj.a=1
stop()
count.value++ // no log
可以看出:與 watch
不同,watchEffect
會在建立的時候立即執行,依賴項改變時再次執行;而 watch
只在監聽物件改變時才執行。
watch
和 watchEffect
都用到了 doWatch
方法處理,來看看原始碼(刪除了部份次要程式碼):
function watchEffect(effect, options) {
return doWatch(effect, null, options)
}
const INITIAL_WATCHER_VALUE = {}
function watch(source, cb, options) {
// 省略部分程式碼...
return doWatch(source, cb, options)
}
function doWatch(
source,
cb,
{ immediate, deep, flush, onTrack, onTrigger } = shared.EMPTY_OBJ, // 預設為Object.freeze({})
instance = currentInstance // 預設為null
) {
if (!cb) {
if (immediate !== undefined) {
warn(
`watch() "immediate" option is only respected when using the ` + `watch(source, callback, options?) signature.`
)
}
if (deep !== undefined) {
warn(`watch() "deep" option is only respected when using the ` + `watch(source, callback, options?) signature.`)
}
}
// 省略部分程式碼...
let getter
let forceTrigger = false
if (reactivity.isRef(source)) {
// 如果監聽的是響應式ref資料
getter = () => source.value
forceTrigger = !!source._shallow
} else if (reactivity.isReactive(source)) {
// 如果監聽的是響應式reactive物件
getter = () => source
deep = true
} else if (shared.isArray(source)) {
// 如果監聽由響應式資料組成的陣列
getter = () =>
source.map((s) => {
// 遍歷陣列再對各個值進行型別判斷
if (reactivity.isRef(s)) {
return s.value
} else if (reactivity.isReactive(s)) {
// 如果是監聽一個reactive型別資料,使用traverse遞迴監聽屬性
return traverse(s)
} else if (shared.isFunction(s)) {
return callWithErrorHandling(s, instance, 2)
} else {
warnInvalidSource(s)
}
})
} else if (shared.isFunction(source)) {
// 如果source是一個getter函式
if (cb) {
getter = () => callWithErrorHandling(source, instance, 2)
} else {
// 如果沒有傳遞cb函式,說明使用的是watchEffect方法
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return callWithErrorHandling(source, instance, 3, [onInvalidate])
}
}
} else {
getter = shared.NOOP
warnInvalidSource(source)
}
if (cb && deep) {
// 如果傳遞了cb函式,並且為深層次監聽,則使用traverse遞迴監聽屬性
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let cleanup
const onInvalidate = (fn) => {
cleanup = runner.options.onStop = () => {
callWithErrorHandling(fn, instance, 4)
}
}
// 省略部分程式碼...
let oldValue = shared.isArray(source) ? [] : INITIAL_WATCHER_VALUE
// 觀察者回撥函式job
const job = () => {
if (!runner.active) {
return
}
if (cb) {
const newValue = runner()
if (deep || forceTrigger || shared.hasChanged(newValue, oldValue)) {
if (cleanup) {
cleanup()
}
callWithAsyncErrorHandling(cb, instance, 3, [
newValue,
// 在監聽資料首次發生更改時將undefined置為舊值
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onInvalidate,
])
oldValue = newValue
}
} else {
// watchEffect
runner()
}
}
// 是否允許自動觸發
job.allowRecurse = !!cb
let scheduler
if (flush === 'sync') {
scheduler = job
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
scheduler = () => {
if (!instance || instance.isMounted) {
queuePreFlushCb(job)
} else {
job()
}
}
}
const runner = reactivity.effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler,
})
recordInstanceBoundEffect(runner, instance)
// initial run
if (cb) {
if (immediate) {
// 如果immediate為true,則可以一開始就執行監聽回撥函式
job()
} else {
oldValue = runner()
}
} else if (flush === 'post') {
queuePostRenderEffect(runner, instance && instance.suspense)
} else {
runner()
}
return () => {
// 返回取消監聽的函式
reactivity.stop(runner)
if (instance) {
shared.remove(instance.effects, runner)
}
}
}
通過上面的程式碼,我們可以發現 watch
和 watchEffect
函式還接收一個 options
引數,這個引數預設為 Object.freeze({})
也就是一個被凍結的,無法新增任何屬性的空物件。如果 options
不為空,那麼它可以包含五個有效屬性:immediate
、deep
、flush
、onTrack
和 onTrigger
,我們來看看這五個屬性的作用。
immediate
表示立即執行,我們之前說過,watch
是惰性監聽,僅在偵聽源發生更改時呼叫,但 watch
也可以主動監聽,即在 options
引數中新增 immediate
屬性為 true
:
const count = ref(2)
watch(
() => count.value,
(newVal, oldVal) => {
console.log(`count發生了變化!count=${newVal}`)
},
{ immediate: true }
)
// log: count發生了變化!count=2
這樣,watch
在一開始就會立即執行回撥。
再說說第二個屬性 deep
,我們通過上面的原始碼可得知,deep
在監聽 reactive
響應式資料的時候會置為 true
,即遞迴地監聽物件及其所有巢狀屬性的變化。如果想要深度偵聽 ref
型別的響應式資料,則需要手動將 deep
置為 true
const obj = ref({ a: 1, b: { c: 2 } })
watch(
obj,
(newVal, oldVal) => {
console.log(`obj發生了變化`)
},
{
deep: true,
}
)
obj.value.b.c++ // obj發生了變化
第三個屬性 flush
有三個有效值:pre
、sync
和 post
。
pre
為預設值,表示在元件更新前執行偵聽回撥;post
表示在更新後呼叫;而 sync
則強制同步執行回撥,因為一些資料往往會在短時間內改變多次,這樣的強制同步是效率低下的,不推薦使用。
<template>
<div>
{{count}}
<button @click="count++">add</button>
</div>
</template>
<script>
import { ref, watch, onBeforeUpdate } from 'vue'
export default {
setup() {
const count = ref(0)
watch(
count,
() => {
console.log('count發生了變化')
},
{
flush: 'post',
}
)
onBeforeUpdate(() => {
console.log('元件更新前')
})
},
}
</script>
在點選 add
按鈕時,先輸出 元件更新前
再輸出 count發生了變化
,如果 flush
為 pre
,則輸出順序相反。
再來看看 onTrack
和 onTrigger
,這兩個一看就是回撥函式。onTrack
將在響應式 property
或 ref
作為依賴項被追蹤的時候呼叫;onTrigger
將在依賴項變更導致 watchEffect
回撥觸發時被呼叫。
const count = ref(0)
watchEffect(
() => {
console.log(count.value)
},
{
onTrack(e) {
console.log('onTrack')
},
onTrigger(e) {
console.log('onTrigger')
},
}
)
count.value++
注意:onTrack
和 onTrigger
只能在開發模式下進行除錯時使用,不能再生產模式下使用。
Fragments
在 Vue2 中,元件只允許一個根元素的存在:
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
在 Vue3 中,允許多個根元素的存在:
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>
這不僅簡化了巢狀,而且暴露出去的多個元素可以受父元件樣式的影響,一定程度上也減少了 css 程式碼。
早在以前,React 就允許 Fragments 元件,該元件用來返回多個元素,而不用在其上面新增一個額外的父節點:
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}
如果想在 Vue2 實現 Fragments,需要安裝 Vue-fragment 包,如今 Vue3 整合了 Vue-fragment,我們可以直接使用這個功能了。
Teleport
Teleport
用來解決邏輯屬於該元件,但從技術角度(如 css 樣式)上看卻應該屬於 app 外部的其他位置。
一個簡單的栗子讓你理解:
<!-- child.vue -->
<template>
<teleport to="#messageBox">
<p>Here are some messages</p>
</teleport>
</template>
<!-- index.html -->
<body>
<div id="app"></div>
<div id="messageBox"></div>
</body>
teleport
接受一個 to
屬性,值為一個 css 選擇器,可以是 id,可以是標籤名(如 body)等等。
teleport
內的元素將會插入到 to 所指向的目標父元素中進行顯示,而內部的邏輯是和當前元件相關聯的,除去邏輯外,上面的程式碼相當於這樣:
<!-- index.html -->
<body>
<div id="app"></div>
<div id="messageBox">
<p>Here are some messages</p>
</div>
</body>
因此 teleport
中的元素樣式,是會受到目標父元素樣式的影響的,這在建立全屏元件的時候非常好用,全屏元件需要寫 css 做定位,很容易受到父元素定位的影響,因此將其插入到 app 外部顯示是非常好的解決方法。
Suspense
Suspense
提供兩個 template
,當要載入的元件不滿足狀態時,顯示 default template
,滿足條件時才會開始渲染 fallback template
。
<!-- AsyncComponent.vue -->
<template>
<div>{{msg}}</div>
</template>
<script>
export default {
setup() {
return new Promise((resolve) => {
setTimeout(() => {
return resolve({ msg: '載入成功' })
}, 1000)
})
},
}
</script>
<!-- parent.vue -->
<template>
<div>
<Suspense>
<template #default>
<AsyncComponent></AsyncComponent>
</template>
<template #fallback>
<h1>Loading...</h1>
</template>
</Suspense>
</div>
</template>
<script>
import AsyncComponent from './AsyncComponent.vue'
export default {
components: { AsyncComponent },
setup() {},
}
</script>
這樣在一開始會顯示 1 秒的 Loading...,然後才會顯示 AsyncComponent
,因此在做載入動畫的時候可以用 Suspense
來處理。
其他新特性
更多比較細小的新特性官網說的很詳細,請看:其他新特性。