簡單梳理下 Vue3 的新特性

在 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 來暴露組件的 datacomputedwatch、生命周期鉤子… 但並不意味著強制使用,在 Vue3 中同樣可以選擇 Options API 或者兩種寫法混用。

接下來我們看看在 setup 的使用。

setup

執行時機

setupbeforeCreate 之前執行,因此訪問不到組件實例,換句話說 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 沒有提供單獨的 onBeforeCreateonCreated 方法,因為 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 的區別

看上去 reactiveref 十分相似,那麼這兩個方法有什麼不同呢?

實際上 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 }))
  },
}

1613874843_1_.png

可以看到 ref 對象的 _value 屬性和 reactive 一樣都被代理了。

綜上所述,我們可以簡單將 ref 看作是 reactive 的二次包裝,只不過多了幾個屬性罷了。

明白了 refreactive 的大致實現和關係,我們再來看其他的響應式 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 類一樣在 getset 時使用 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>

GIF.gif

可以看到,點擊 addA 按鈕不會觸發介面渲染,而點擊 addB 會更新介面。雖然 attrB.value 的改變確實會觸發 ui 更新,但這是因為 attrB.value 的改變觸發了 obj2.b 的改變,而 obj2 本身就是響應式數據,所以 attrB.value 的改變是間接觸發了 ui 更新,而不是直接原因

再來看看 toRefstoRefs 可以將整個對象轉換成響應式對象,而 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

refreactive 在默認情況下會遞歸地將對象內所有的屬性無論嵌套與否都轉化為響應式,而 shallowRefshallowReactive 則只將第一層屬性轉化為響應式。

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

我們可以發現,shallowRefshallowReactive 類型的響應式數據,在改變其深層次屬性時候是不會觸發 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 可以返回 reactivereadonly 所代理的對象。

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 只在監聽對象改變時才運行。

watchwatchEffect 都用到了 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)
    }
  }
}

通過上面的程式碼,我們可以發現 watchwatchEffect 函數還接收一個 options 參數,這個參數默認為 Object.freeze({}) 也就是一個被凍結的,無法添加任何屬性的空對象。如果 options 不為空,那麼它可以包含五個有效屬性:immediatedeepflushonTrackonTrigger,我們來看看這五個屬性的作用。

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 有三個有效值:presyncpost

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發生了變化,如果 flushpre,則輸出順序相反。

再來看看 onTrackonTrigger,這兩個一看就是回調函數。onTrack 將在響應式 propertyref 作為依賴項被追蹤的時候調用;onTrigger 將在依賴項變更導致 watchEffect 回調觸發時被調用。

const count = ref(0)
watchEffect(
  () => {
    console.log(count.value)
  },
  {
    onTrack(e) {
      console.log('onTrack')
    },
    onTrigger(e) {
      console.log('onTrigger')
    },
  }
)
count.value++

注意:onTrackonTrigger 只能在開發模式下進行調試時使用,不能再生產模式下使用。

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>

GIF.gif

這樣在一開始會顯示 1 秒的 Loading…,然後才會顯示 AsyncComponent,因此在做載入動畫的時候可以用 Suspense 來處理。

其他新特性

更多比較細小的新特性官網說的很詳細,請看:其他新特性