簡單梳理下 Vue3 的新特性

lliiooiill發表於2021-02-24

在 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 來處理。

其他新特性

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

相關文章