1. 程式人生 > >簡單梳理下 Vue3 的新特性

簡單梳理下 Vue3 的新特性

> 在 Vue3 測試版剛剛釋出的時候,我就學習了下 Composition API,但沒想到正式版時隔一年多才出來,看了一下發現還是增加了不少新特性的,在這裡我就將它們一一梳理一遍。 本文章只詳細闡述 Vue3 中重要或常用的新特性,如果想了解全部的特性請轉:[Vue3 響應性基礎 API](https://vue3js.cn/docs/zh/api/basic-reactivity.html) ## Composition API 這是一個非常重要的改變,我認為 Composition API 最大的用處就是**將響應式資料和相關的業務邏輯結合到一起,便於維護**(這樣做的優點在處理龐大元件的時候顯得尤為重要)。 之所以叫做 Composition API(或組合式 API) 是因為所有的響應式資料和業務邏輯程式碼都可以放在 setup 方法中進行處理,我們通過程式碼看一下 Vue2 的 Options API 和 Composition API 的區別: ```javascript /* 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` 內的屬性: ```html
``` ```javascript /* child.vue */ export default { props: { name: String }, setup(props, context) { console.log(props) // {name: 'child'} console.log(context.attrs) // {msg: 'hello world'} }, } ``` ##### emit 用於在子元件內觸發父元件的方法 ```html ``` ```javascript /* child.vue */ export default { setup(_, context) { context.emit('sayWhat') }, } ``` ##### slots 用來訪問被插槽分發的內容,相當於 `vm.$slots` ```html
``` ```javascript /* 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`: ```javascript 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` 屬性。 ```javascript 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` 的: ```html ``` ### reactive 和 `ref` 方法一樣,`reactive` 也負責將目標資料轉換成響應式資料,但該資料只能是**引用型別**。 ```html ``` 可以看出 `reactive` 型別的響應式資料不需要在後面追加 `.value` 來呼叫或使用。 #### reactive 和 ref 的區別 看上去 `reactive` 和 `ref` 十分相似,那麼這兩個方法有什麼不同呢? 實際上 `ref` 本質上與 `reactive` 並無區別,來看看 Vue3 的部分原始碼(來自於 `@vue/reactivity/dist/reactivity.cjs.js`): ```javascript 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) ``` 如果你不明白上面的程式碼做了什麼,假設我現在執行這行程式碼: ```javascript const count = ref(0) ``` 那麼實際上 `ref` 函式返回的是一個 `RefImpl` 例項,裡面包含如下屬性: ```javascript { _rawValue: 0, _shallow: false, __v_isRef: true, _value: 0 } ``` 通過 `RefImpl` 類的 `get value()` 方法可以看出,呼叫 `value` 屬性返回的其實就是 `_value` 屬性。 > Vue3 建議在定義基本型別的響應式資料時使用 `ref` 是因為基本型別不存在引用效果,這樣一來在其他地方改變該值便不會觸發響應,因此 `ref` 將資料包裹在物件中以實現引用效果。 Vue3 會判斷 `template` 中的響應式資料是否為 `ref` 型別,如果為 `ref` 型別則會在尾部自動追加 `.value`,判斷方式很簡單: ```javascript function isRef(r) { return Boolean(r && r.__v_isRef === true) } ``` 那麼其實我們是可以用 `reactive` 來偽裝成 `ref` 的: ```html ``` 雖然這樣做毫無意義,不過證明了 Vue3 確實是通過 `__v_isRef` 屬性判斷資料是否為 `ref` 定義的。 我們再看看 `reactive` 的實現: ```javascript 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` 定義引用型別資料的時候同樣會用到這個方法: ```javascript export default { setup() { console.log(ref({ a: 123 })) console.log(reactive({ a: 123 })) }, } ``` ![1613874843_1_.png](https://i.loli.net/2021/02/21/DSwGQxqmNXgcIkW.png) 可以看到 `ref` 物件的 `_value` 屬性和 `reactive` 一樣都被代理了。 綜上所述,我們可以**簡單將 `ref` 看作是 `reactive` 的二次包裝**,只不過多了幾個屬性罷了。 明白了 `ref` 和 `reactive` 的大致實現和關係,我們再來看其他的響應式 API。 ### isRef & isReactive 判斷一個值是否是 ref 或 reactive 型別: ```javascript 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,常用來定義需要非同步獲取的響應式資料,舉個搜尋框防抖的例子: ```javascript 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, } }, } ``` ```html ``` 在這個例子中我們使用 `customRef` 和防抖函式,延遲改變 `text.value` 值,當 `watch` 監聽到 `text` 的改變再進行搜尋以實現防抖搜尋。 ### toRef & toRefs `toRef` 可以將一個 `reactive` 形式的物件的屬性轉換成 `ref` 形式,並且 `ref` 物件會保持對源 `reactive` 物件的引用: ```javascript 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` 的原始碼: ```javascript 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 更新!**,舉個例子: ```html ``` ![GIF.gif](https://i.loli.net/2021/02/21/RcdSLJEahomsjGz.gif) 可以看到,點選 addA 按鈕不會觸發介面渲染,而點選 addB 會更新介面。雖然 `attrB.value` 的改變確實會觸發 ui 更新,但這是因為 `attrB.value` 的改變觸發了 `obj2.b` 的改變,而 `obj2` 本身就是響應式資料,所以 **`attrB.value` 的改變是間接觸發了 ui 更新,而不是直接原因**。 再來看看 `toRefs`,`toRefs` 可以將整個物件轉換成響應式物件,而 `toRef` 只能轉換物件的某個屬性。但是 `toRefs` 生成的響應式物件和 `ref` 生成的響應式物件在用法上是有區別的: ```javascript 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` 則只將第一層屬性轉化為響應式。 ```javascript 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 更新,有沒有辦法在其他情況下手動觸發更新呢?有的: ```javascript const dynamicObj3 = shallowRef({ a: 1, b: { c: 2 } }) function func() { dynamicObj3.value.b.c++ triggerRef(dynamicObj3) // 手動觸發ui更新 } ``` ### readonly & isReadonly `readonly` 可將整個物件(包含其內部屬性)變成只讀的,並且是深層次的。 `isReadonly` 通過物件中的 `__v_isReadonly` 屬性判斷物件是否只讀。 ```javascript 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` 所代理的物件。 ```javascript 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 ``` ```javascript function toRaw(observed) { return (observed && toRaw(observed['__v_raw' /* RAW */])) || observed } ``` 事實上,無論是 `reactive` 還是 `readonly`,都會將源物件儲存一份在屬性 `__v_raw` 中,而 `ref` 會將源物件或值儲存在 `_rawValue` 屬性中。 ## computed Vue3 將 `computed` 也包裝成了一個方法,我們看看 `computed` 的原始碼: ```javascript 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` 接收兩種不同的引數: ```javascript computed(() => {}) // only getter computed({ get: () => {}, set: () => {} }) // getter and setter ``` 和 Vue2 一樣,`computed` 既可以單純的用 `getter` 計算並返回資料,也可以設定 `setter` 使其變得可寫。 ```javascript 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` 發起監聽: ```javascript 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`: ```javascript const count = ref(2) watch(count, (newVal, oldVal) => { console.log(`count值發生了變化!count=${newVal}`) }) count.value = -1 // count的絕對值發生了變化!count=-1 ``` `watch` 不僅可以監聽單一資料,也可以監聽多個數據: ```javascript const preNum = ref('') const aftNum = ref('') watch([preNum, aftNum], ([newPre, newAft], [oldPre, oldAft]) => { console.log('資料改變了') }) preNum.value = '123' // 資料改變了 aftNum.value = '123' // 資料改變了 ``` `watchEffect` 會在其任何一個依賴項發生變化的時候重新執行,其返回一個函式用於取消監聽。 ```javascript 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` 方法處理,來看看原始碼(刪除了部份次要程式碼): ```javascript 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`: ```javascript 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` ```javascript 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` 則強制同步執行回撥,因為一些資料往往會在短時間內改變多次,這樣的強制同步是效率低下的,不推薦使用。 ```html ``` 在點選 `add` 按鈕時,先輸出 `元件更新前` 再輸出 `count發生了變化`,如果 `flush` 為 `pre`,則輸出順序相反。 再來看看 `onTrack` 和 `onTrigger`,這兩個一看就是回撥函式。`onTrack` 將在響應式 `property` 或 `ref` 作為依賴項被追蹤的時候呼叫;`onTrigger` 將在依賴項變更導致 `watchEffect` 回撥觸發時被呼叫。 ```javascript 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 中,元件只允許一個根元素的存在: ```html ``` 在 Vue3 中,允許多個根元素的存在: ```html ``` 這不僅簡化了巢狀,而且暴露出去的多個元素可以受父元件樣式的影響,一定程度上也減少了 css 程式碼。 早在以前,React 就允許 Fragments 元件,該元件用來返回多個元素,而不用在其上面新增一個額外的父節點: ```javascript render() { return ( ); } ``` 如果想在 Vue2 實現 Fragments,需要安裝 Vue-fragment 包,如今 Vue3 整合了 Vue-fragment,我們可以直接使用這個功能了。 ## Teleport `Teleport` 用來解決邏輯屬於該元件,但從技術角度(如 css 樣式)上看卻應該屬於 app 外部的其他位置。 一個簡單的栗子讓你理解: ```html ``` ```html ``` `teleport` 接受一個 `to` 屬性,值為一個 css 選擇器,可以是 id,可以是標籤名(如 body)等等。 `teleport` 內的元素將會插入到 to 所指向的目標父元素中進行顯示,而內部的邏輯是和當前元件相關聯的,除去邏輯外,上面的程式碼相當於這樣: ```html

Here are some messages

``` 因此 `teleport` 中的元素樣式,是會受到目標父元素樣式的影響的,這在建立全屏元件的時候非常好用,全屏元件需要寫 css 做定位,很容易受到父元素定位的影響,因此將其插入到 app 外部顯示是非常好的解決方法。 ## Suspense `Suspense` 提供兩個 `template`,當要載入的元件不滿足狀態時,顯示 `default template`,滿足條件時才會開始渲染 `fallback template`。 ```html ``` ```html ``` ![GIF.gif](https://i.loli.net/2021/02/23/bWBj13Aney7pdI5.gif) 這樣在一開始會顯示 1 秒的 Loading...,然後才會顯示 `AsyncComponent`,因此在做載入動畫的時候可以用 `Suspense` 來處理。 ## 其他新特性 更多比較細小的新特性官網說的很詳細,請看:[其他新特性](https://vue3js.cn/docs/zh/guide/migration/array-refs.h