vue3 快速入門系列 —— 其他API

彭加李發表於2024-04-22

其他章節請看:

vue3 快速入門 系列

他API

前面我們已經學習了 vue3 的一些基礎知識,本篇將繼續講解一些常用的其他api,以及較完整的分析vue2 和 vue3 的改變。

淺層響應式資料

shallowRef

shallow 中文:“淺層的”

shallowRef:淺的 ref()。

先用 ref 寫個例子:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>a: {{ a }}</p>
    <p>o: {{ o }}</p>
    <p><button @click="change1">change1</button></p>
    <p><button @click="change2">change2</button></p>
    <p><button @click="change3">change3</button></p>
    <p><button @click="change4">change4</button></p>
</template>

<script lang="ts" setup name="App">
import {ref, shallowRef} from 'vue'

let a = ref(0)
let o = ref({
    name: 'p',
    age: 18
})

function change1 (){
    a.value = 1
}
function change2 (){
    o.value.name = 'p2'
}
function change3 (){
    o.value.age = 19
}
function change4 (){
    o.value = {name: 'p3', age: 20}
}
</script>

這4個按鈕都會觸發頁面資料的變化。

現在將 ref 改成 shallowRef,其他都不變。你會發現只有 change1 和 change4 能觸發頁面資料的變化:

<!-- ChildA.vue -->
<template>
   // 不變
</template>

<script lang="ts" setup name="App">
import {ref, shallowRef} from 'vue'

let a = shallowRef(0)
let o = shallowRef({
    name: 'p',
    age: 18
})

function change1 (){
    a.value = 1
}
function change2 (){
    o.value.name = 'p2'
}
function change3 (){
    o.value.age = 19
}
function change4 (){
    o.value = {name: 'p3', age: 20}
}
</script>

這是因為 change1 中的 a.value 是淺層,而 change2 中的 o.value.name 是深層。

對於大型資料結構,如果只關心整體是否被替換,就可以使用 shallowRef,避免使用 ref 將大型資料結構所有層級都轉成響應式,這對底層是很大的開銷。

shallowReactive

知曉了 shallowRef,shallowReactive也類似。

shallowReactive:淺的 reactive()。

請看示例:

現在3個按鈕都能修改頁面資料:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>o: {{ o }}</p>
    <p><button @click="change2">change2</button></p>
    <p><button @click="change3">change3</button></p>
    <p><button @click="change4">change4</button></p>
</template>

<script lang="ts" setup name="App">
import {reactive} from 'vue'

let o = reactive({
    name: 'p',
    options: {
        age: 18,
    }
})

function change2 (){
    o.name = 'p2'
}
function change3 (){
    o.options.age = 19
}
function change4 (){
    o = Object.assign(o, {name: 'p3', options: {age: 20}})
}

</script>

將 reactive 改為 shallowReactive:

import {shallowReactive} from 'vue'

let o = shallowReactive({
    name: 'p',
    options: {
        age: 18,
    }
})

現在只有 change2 和 change4 能修改頁面資料,因為 change3 是多層的,所以失效。

只讀資料

readonly

readonly : Takes an object (reactive or plain) or a ref and returns a readonly proxy to the original.

readonly 能傳入響應式資料,並返回一個只讀代理

請看示例:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>name: {{ name }}</p>

    <p><button @click="change1">change name</button></p>

    <p>copyName: {{ copyName }}</p>

    <p><button @click="change2">change copyName</button></p>
</template>

<script lang="ts" setup name="App">
import {ref, readonly} from 'vue'
let name = ref('p')
// 傳入一個響應式的資料,返回一個只讀代理
// reactive 資料也可以
// name 資料的修改,也會同步到 copyName
let copyName = readonly(name)

// 型別“number”的引數不能賦給型別“object”的引數。ts
// let copyName = readonly(2)

function change1(){
    name.value = 'p2'
}

function change2(){
    // 透過代理修改資料
    // vscode 報錯:無法為“value”賦值,因為它是隻讀屬性。ts
    copyName.value = 'p3'
}
</script>

瀏覽器呈現:

# 元件A

name: p2
// 按鈕1
change name

copyName: p2
// 按鈕2
change copyName

點選第一個按鈕,發現 copyName 的值也跟著變化了(說明不是一錘子買賣),但是點選第二個按鈕,頁面資料不會變化。瀏覽器控制檯也會警告:

[Vue warn] Set operation on key "value" failed: target is readonly. RefImpl {__v_isShallow: false, dep: Map(1), __v_isRef: true, _rawValue: 'p2', _value: 'p2'}

readonly 只讀代理是深的:任何巢狀的屬性訪問也將是隻讀的。對比 shallowReadonly 就知道了。

Tip:使用場景,比如同事A定義了一個很重要的資料,同事B需要讀取該資料,但又擔心誤操作修改了該資料,就可以透過 readonly 包含資料。

shallowReadonly

readonly 只讀代理是深層的,而 shallowReadonly 是淺層的。也就是深層的 shallowReadonly 資料不是隻讀的。

請看示例:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>obj: {{ obj }}</p>

    <p><button @click="change1">change1</button></p>
    <p><button @click="change2">change2</button></p>
</template>

<script lang="ts" setup name="App">
import {ref, reactive, shallowReadonly} from 'vue'
let obj = reactive({
    name: 'p',
    options: {
        age: 18,
    }
})

let copyObj = shallowReadonly(obj)

function change1(){
    // vscode 會提示:無法為“name”賦值,因為它是隻讀屬性。ts
    copyObj.name = 'p2'
}

function change2(){
    copyObj.options.age = 19
}

</script>

透過 shallowReadonly 建立一個備份資料,點選第一個按鈕沒反應,點選第二個按鈕,頁面變成:

# 元件A

obj: { "name": "p", "options": { "age": 19 } }

shallowReadonly 只處理淺層次的只讀。深層次的不管,也就是可以修改。

疑惑:筆者的開發者工具中, copyObj -> options 中的 age 屬性沒有表示能修改的鉛筆圖示。應該要有,這樣就能保持和程式碼一致

原始資料

toRaw

toRaw() can return the original object from proxies created by reactive(), readonly(), shallowReactive() or shallowReadonly().

用於獲取一個響應式物件的原始物件。修改原始物件,不會在觸發檢視。

const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo) // true

比如這個使用場景:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>obj: {{ obj }}</p>

    <p><button @click="handle1(toRaw(obj))">處理資料</button></p>
</template>

<script lang="ts" setup name="App">
import {reactive, toRaw} from 'vue'
let obj = reactive({
    name: 'p',
    age: 18,
})

// 不用擔心修改了資料從而影響到使用 obj 的地方
function handle1(o: any){
    // 修改資料
    o.age += 1
    // o: {name: 'p', age: 19}
    console.log('o: ', o)

    // 例如傳送請求
}

</script>

markRaw

Marks an object so that it will never be converted to a proxy. Returns the object itself.

標記一個物件,使其永遠不會被轉換為proxy。返回物件本身。

  • 有些值不應該是響應式的,例如一個複雜的第三方類例項,或者一個Vue元件物件。
import {reactive} from 'vue'
let o = {
    getAge() {
        console.log(18)
    }
}
// Proxy(Object) {getAge: ƒ}
let o2 = reactive(o)
  • 當使用不可變資料來源呈現大型列表時,跳過代理轉換可以提高效能。

請問輸出什麼:

import {reactive} from 'vue'
let o = {
    name: 'p',
    age: 18,
}
let o2 = reactive(o)

console.log(o);
console.log(o2);

答案是:

{name: 'p', age: 18}
Proxy(Object) {name: 'p', age: 18}

透過 reactive 會將資料轉為響應式。

請看 markRaw 示例:

import {reactive, markRaw} from 'vue'
// 標記 o 不能被轉成響應式
let o = markRaw({
    getAge() {
        console.log(18)
    }
})
let o2 = reactive(o)

// {__v_skip: true, getAge: ƒ}
console.log(o2);

比如中國的城市,資料是固定不變的,我不做成響應式的,別人也不許做成響應式的。我可以這麼寫:

// 中國就這些地方,不會變。我自己不做成響應式的,別人也不許做成響應式的
let citys = markRow([
    {name: '北京'},
    {name: '上海'},
    {name: '深圳'},
    ...
])

customRef

自定義 ref 可用於解決內建 ref 不能解決的問題。

ref 用於建立響應式資料,資料一變,檢視也會立刻更新。比如要1秒後更新檢視,這個 ref 辦不到。

先用ref寫個例子:input 輸入字元,msg 立刻更新:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>msg: {{ msg }}</p>
    <input v-model="msg"/>
</template>

<script lang="ts" setup name="App">
import {ref} from 'vue'

let msg = ref('')

</script>

現在要求:input輸入字元後,等待1秒msg才更新。

我們可以用 customRef 解決這個問題。

實現如下:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>msg: {{ msg }}</p>
    <input v-model="msg"/>
</template>

<script lang="ts" setup name="App">
import {ref, customRef, } from 'vue'

let initValue = ''

// customRef 傳入函式,裡面又兩個引數
let msg = customRef((track, trigger) => {
    return {
      get() {
        // 告訴 vue 這個資料很重要,要持續關注,資料一旦變化,更新檢視
        track()
        return initValue
      },
      set(newValue) {
        setTimeout(() => {
            initValue = newValue
            // 告訴vue我更新資料了,你更新檢視去吧
            trigger()
        }, 1000)
      }
    }
  })
</script>

customRef() 接收一個工廠函式作為引數,這個工廠函式接受 track 和 trigger 兩個函式作為引數,並返回一個帶有 get 和 set 方法的物件。

track()trigger() 缺一不可,需配合使用:

  • 缺少 track,即使通知vue 更新了資料,但不會更新檢視
  • 缺少 trigger,track 則一直在等著資料變,快變,我要更新檢視。但最終沒人通知它資料變了

實際工作會將上述功能封裝成一個 hooks。使用起來非常方便。就像這樣:

// hooks/useMsg.ts
import { customRef, } from 'vue'

export function useMsg(value: string, delay = 1000) {

  // customRef 傳入函式,裡面又兩個引數
  let msg = customRef((track, trigger) => {
    // 防抖
    let timeout: number
    return {
      get() {
        // 告訴 vue 這個資料很重要,要持續關注,資料一旦變化,更新檢視
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          // 告訴vue我更新資料了,你更新檢視去吧
          trigger()
        }, delay)
      }
    }
  })

  return msg
}

使用起來和 ref 一樣方便。就像這樣:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>msg: {{ msg }}</p>
    <input v-model="msg"/>
</template>

<script lang="ts" setup name="App">
import {useMsg} from '@/hooks/useMsg'

let msg = useMsg('hello', 1000)

</script>

Teleport

Teleport 中文“傳送”

Teleport 將其插槽內容渲染到 DOM 中的另一個位置。

比如 box 內的內容現在在 box 元素中:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <div class="box">
        <p>我是元件A內的彈框</p>
    </div>
</template>

我可以利用 Teleport 新增元件將其移到body下面。

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p><button @click="handle1">change msg</button></p>
    <div class="box">
        <Teleport to="body">
            <p>{{ msg }}</p>
        </Teleport>
    </div>
</template>

<script lang="ts" setup name="App">
import {ref} from 'vue'
let msg = ref('我是元件A內的彈框')

function handle1(){
    msg.value += '~'
}
</script>

現在這段ui內容就移到了 body 下,並且資料鏈還是之前的,也就是 msg 仍受 button 控制。

Tip:to 必填,語法是選擇器或實際元素

<Teleport to="#some-id" />
<Teleport to=".some-class" />
<Teleport to="[data-teleport]" />

Suspense

suspense 官網說是一個實驗性功能。用來在元件樹中協調對非同步依賴的處理。

我們首先在子元件中非同步請求,請看示例:

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <hr>
    <ChildA/>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'
</script>
<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
</template>

<script lang="ts" setup name="App">
import axios from 'axios';
// https://api.uomg.com/ 免費的 API 介面服務
let {data} = await axios.get('https://api.uomg.com/api/rand.music?sort=熱歌榜&format=json')
console.log('data: ', data);
</script>

Tip:我們現在用了 setup 語法糖,沒有機會寫 async,之所以能這麼寫,是因為底層幫我們做了。

瀏覽器檢視,發現子元件沒有渲染出來。控制檯輸出:

// main.ts:14 [Vue 警告]: 元件 <App>: setup 函式返回了一個 Promise,但在父元件樹中未找到 <Suspense> 邊界。帶有非同步 setup() 的元件必須巢狀在 <Suspense> 中才能被渲染。
main.ts:14 [Vue warn]: Component <App>: setup function returned a promise, but no <Suspense> boundary was found in the parent component tree. A component with async setup() must be nested in a <Suspense> in order to be rendered. 

data: {code: 1, data: {…}}

vue 告訴我們需要使用 Suspense。

假如我們將 await 用 async 方法包裹,子元件能正常顯示。

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>data: {{ data }}</p>
</template>

<script lang="ts" setup name="App">
import {ref} from 'vue'
import axios from 'axios';

let data = ref({})
async function  handle1(){
    // https://api.uomg.com/ 免費的 API 介面服務
    // 先安裝:npm install axios
    let response = await axios.get('https://api.uomg.com/api/rand.music?sort=熱歌榜&format=json')
    data.value = response.data
    console.log('data: ', data);

}
handle1()
</script>

繼續討論非同步的 setup()的解決方案。在父元件中使用 Suspense 元件即可。請看程式碼:

<!-- Father.vue -->
<template>
    <p># 父親</p>
    <hr>
    // <Suspense> 元件有兩個插槽:#default 和 #fallback。兩個插槽都只允許一個直接子節點。
    <Suspense>
        <template #fallback>
            Loading...
        </template>
        <ChildA/>
    </Suspense>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'
</script>

子元件也稍微調整下:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <p>data: {{ data }}</p>
</template>

<script lang="ts" setup name="App">
import axios from 'axios';
// https://api.uomg.com/ 免費的 API 介面服務
let {data} = await axios.get('https://api.uomg.com/api/rand.music?sort=熱歌榜&format=json')
console.log('data: ', data);
</script>

利用開發者工具將網速跳到 3G,再次重新整理頁面,發現先顯示Loading...,然後在顯示

# 元件A

data: { "code": 1, "data": { "name": "阿普的思念", "url": "http://music.163.com/song/media/outer/url?id=2096764279", "picurl": "http://p1.music.126.net/Js1IO7cwfEe6G6yNPyv5FQ==/109951169021986117.jpg", "artistsname": "諾米麼Lodmemo" } }

:資料是一次性出來的,不是先展示 {} 在展示 {...}。所以我們再看官網,就能理解下面這段內容:

<Suspense>
└─ <Dashboard>
   ├─ <Profile>
   │  └─ <FriendStatus>(元件有非同步的 setup())
   └─ <Content>
      ├─ <ActivityFeed> (非同步元件)
      └─ <Stats>(非同步元件)

在這個元件樹中有多個巢狀元件,要渲染出它們,首先得解析一些非同步資源。如果沒有 <Suspense>,則它們每個都需要處理自己的載入、報錯和完成狀態。在最壞的情況下,我們可能會在頁面上看到三個旋轉的載入態,在不同的時間顯示出內容。

有了 <Suspense> 元件後,我們就可以在等待整個多層級元件樹中的各個非同步依賴獲取結果時,在頂層展示出載入中或載入失敗的狀態。

Tip: 在 React 中可以使用 Suspense 元件和 React.lazy() 函式來實現元件的延遲載入。就像這樣:

import React, {Suspense} from 'react'
// 有當 OtherComponent 被渲染時,才會動態載入 ‘./math’ 元件
const OtherComponent = React.lazy(() => import('./math'))

function TestCompoment(){
    return <div>
                <Suspense fallback={<div>loading</div>}>
                    <OtherComponent/>
                </Suspense>
        </div>
}

全域性 api 轉移到應用物件

在 Vue 3 中,一些全域性 API 被轉移到了應用物件(app)中。

app就是這個:

import { createApp } from 'vue'

const app = createApp({
  /* 根元件選項 */
})

這些 API 以前在 Vue 2 中是全域性可用的,但在 Vue 3 中,出於更好的模組化和靈活性考慮,許多 API 被轉移到了應用物件中。

app.component

對應 vue2 中 Vue.component,用於註冊和獲取全域性元件。

例如定義一個元件:

<template>
    <p>我的Apple元件</p>
</template>

在 main.ts 中註冊:

import Apple from '@/views/Apple.vue'
app.component('Apple', Apple)

現在在任何地方都能直接使用,例如在 ChildA.vue 中:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <Apple/>
</template>

<script lang="ts" setup name="App">

</script>

app.config

vue2 中有 Vue.prototype. 比如 Vue.prototype.x = 'hello',在任意模板中 {{x}} 都會輸出 hello

這裡有 app.config。

比如在 main.ts 中增加:app.config.globalProperties.x = 'hello',在任意元件中就可以獲取:

<template>
    <p># 元件A</p>
    x: {{ x }}
    <Apple/>
</template>

但是 ts 會報錯,因為找不到 x。

解決方法在官網中有提供。建立一個 ts:

// test.ts
// 官網:https://cn.vuejs.org/api/application.html#app-config-globalproperties
// 正常工作。
export {}

declare module 'vue' {
  interface ComponentCustomProperties {
    x: string,
  }
}

然後在 main.ts 中引入:

import '@/utils/test'
app.config.globalProperties.x = 'hello'

不要隨便使用,否則你一下定義100個,以後出問題不好維護。

app.directive

Vue.directive() - 註冊或獲取全域性指令。

我們用函式形式的指令,就像這樣:

// https://v2.cn.vuejs.org/v2/guide/custom-directive.html#函式簡寫
Vue.directive('color-swatch', function (el, binding) {
  el.style.backgroundColor = binding.value
})

比如我寫一個這樣的指令:

// main.ts 註冊一個全域性指令
app.directive('green', (element, {value}, vnode) => {
    element.innerText += value
    element.style.color = 'green'
})

接著使用指令:

<!-- ChildA.vue -->
<template>
    <p># 元件A</p>
    <h4 v-green="msg">你好</h4>
    <Apple/>
</template>

<script lang="ts" setup name="App">
import {ref} from 'vue'
let msg = ref('兄弟')
</script>

頁面呈現:

# 元件A
// 綠色文字
你好兄弟

其他

app.mount - 掛載
app.unmount - 解除安裝
app.use - 安裝外掛。例如路由、pinia

非相容性改變

非相容性改變Vue 2 遷移中的一章,列出了 Vue 2 對 Vue 3 的所有非相容性改變

Tip:強烈建議詳細閱讀該篇。

全域性 API 應用例項

Vue 2.x 有許多全域性 API 和配置,它們可以全域性改變 Vue 的行為。例如,要註冊全域性元件,可以使用 Vue.component API

雖然這種宣告方式很方便,但它也會導致一些問題。從技術上講,Vue 2 沒有“app”的概念,我們定義的應用只是透過 new Vue() 建立的根 Vue 例項。從同一個 Vue 建構函式建立的每個根例項共享相同的全域性配置

全域性配置使得在同一頁面上的多個“應用”在全域性配置不同時共享同一個 Vue 副本非常困難

為了避免這些問題,在 Vue 3 中我們引入了...

一個新的全域性 API:createApp

全域性和內部 API 都經過了重構,現已支援 TreeShaking (搖樹最佳化)

如果你曾經在 Vue 中手動操作過 DOM,你可能會用過這種方式:

import Vue from 'vue'

Vue.nextTick(() => {
  // 一些和 DOM 有關的東西
})

但是,如果你從來都沒有過手動操作 DOM 的必要,或者更喜歡使用老式的 window.setTimeout() 來代替它,那麼 nextTick() 的程式碼就會變成死程式碼。

如 webpack 和 Rollup (Vite 基於它) 這樣的模組打包工具支援 tree-shaking,遺憾的是,由於之前的 Vue 版本中的程式碼編寫方式,如 Vue.nextTick() 這樣的全域性 API 是不支援 tree-shake 的,不管它們實際上是否被使用了,都會被包含在最終的打包產物中。

Tip:Vite 基於 Rollup

在 Vue 3 中,全域性和內部 API 都經過了重構,並考慮到了 tree-shaking 的支援。因此,對於 ES 模組構建版本來說,全域性 API 現在透過具名匯出進行訪問。例如,我們之前的程式碼片段現在應該如下所示:

import { nextTick } from 'vue'

nextTick(() => {
  // 一些和 DOM 有關的東西
})

透過這一更改,如果模組打包工具支援 tree-shaking,則 Vue 應用中未使用的全域性 API 將從最終的打包產物中排除,從而獲得最佳的檔案大小。

v-model 指令在元件上的使用已經被重新設計,替換掉了 v-bind.sync

  • 非相容:用於自定義元件時,v-model prop 和事件預設名稱已更改:
    • prop:value -> modelValue;
    • 事件:input -> update:modelValue;
  • 非相容:v-bind 的 .sync 修飾符和元件的 model 選項已移除,可在 v-model 上加一個引數代替;
  • 新增:現在可以在同一個元件上使用多個 v-model 繫結;
  • 新增:現在可以自定義 v-model 修飾符。

sync 和 model 選項已廢除

在<template v-for> 和沒有 v-for 的節點身上使用 key 發生了變化

  • 新增:對於 v-if/v-else/v-else-if 的各分支項 key 將不再是必須的,因為現在 Vue 會自動生成唯一的 key。
  • 非相容:如果你手動提供 key,那麼每個分支必須使用唯一的 key。你將不再能透過故意使用相同的 key 來強制重用分支。
  • 非相容<template v-for> 的 key 應該設定在 <template> 標籤上 (而不是設定在它的子節點上)。

v-if 和 v-for 在同一個元素身上使用時的優先順序發生了變化

  • 非相容:兩者作用於同一個元素上時,v-if 會擁有比 v-for 更高的優先順序。

2.x 版本中在一個元素上同時使用 v-if 和 v-for 時,v-for 會優先作用。

3.x 版本中 v-if 總是優先於 v-for 生效。

v-bind="object" 現在是順序敏感的

  • 不相容:v-bind 的繫結順序會影響渲染結果。

在 2.x 中,如果一個元素同時定義了 v-bind="object" 和一個相同的獨立 attribute,那麼這個獨立 attribute 總是會覆蓋 object 中的繫結。

<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 結果 -->
<div id="red"></div>

在 3.x 中,如果一個元素同時定義了 v-bind="object" 和一個相同的獨立 attribute,那麼繫結的宣告順序將決定它們如何被合併

<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 結果 -->
<div id="blue"></div>

<!-- 模板 -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- 結果 -->
<div id="red"></div>

移除 v-on.native 修飾符

v-on 的 .native 修飾符已被移除。

2.x 語法: 預設情況下,傳遞給帶有 v-on 的元件的事件監聽器只能透過 this.$emit 觸發。要將原生 DOM 監聽器新增到子元件的根元素中,可以使用 .native 修飾符

<my-component
  v-on:close="handleComponentEvent"
  v-on:click.native="handleNativeClickEvent"
/>

3.x 語法: 對於子元件中未被定義為元件觸發的所有事件監聽器,Vue 現在將把它們作為原生事件監聽器新增到子元件的根元素中。強烈建議使用 emits 記錄每個元件所觸發的所有事件。

函式式元件只能透過純函式進行建立

概覽

對變化的總體概述:

  • 2.x 中函式式元件帶來的效能提升在 3.x 中已經可以忽略不計,因此我們建議只使用有狀態的元件
  • 函式式元件只能由接收 props 和 context (即:slots、attrs、emit) 的普通函式建立
  • 非相容:functional attribute 已從單檔案元件 (SFC) 的 <template> 中移除
  • 非相容:{ functional: true } 選項已從透過函式建立的元件中移除
介紹

在 Vue 2 中,函式式元件主要有兩個應用場景:

  • 作為效能最佳化,因為它們的初始化速度比有狀態元件快得多
  • 返回多個根節點

然而,在 Vue 3 中,有狀態元件的效能已經提高到它們之間的區別可以忽略不計的程度。此外,有狀態元件現在也支援返回多個根節點。

因此,函式式元件剩下的唯一應用場景就是簡單元件,比如建立動態標題的元件。否則,建議你像平常一樣使用有狀態元件。

非同步元件現在需要透過 defineAsyncComponent 方法進行建立

非同步元件的主要作用是延遲元件的載入,只有在元件需要被渲染時才會進行載入和例項化,而不是在頁面載入時就載入所有的元件

概覽

以下是對變化的總體概述:

  • 新的 defineAsyncComponent 助手方法,用於顯式地定義非同步元件
  • component 選項被重新命名為 loader
  • Loader 函式本身不再接收 resolve 和 reject 引數,且必須返回一個 Promise
介紹

以前,非同步元件是透過將元件定義為返回 Promise 的函式來建立的,例如:

const asyncModal = () => import('./Modal.vue')

const asyncModal = {
  component: () => import('./Modal.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
}

現在,在 Vue 3 中,由於函式式元件被定義為純函式,因此非同步元件需要透過將其包裹在新的 defineAsyncComponent 助手方法中來顯式地定義:

import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'

// 不帶選項的非同步元件
const asyncModal = defineAsyncComponent(() => import('./Modal.vue'))

// 帶選項的非同步元件
const asyncModalWithOptions = defineAsyncComponent({
  // component 重新命名為 loader
  loader: () => import('./Modal.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})

與 2.x 不同,loader 函式不再接收 resolve 和 reject 引數,且必須始終返回 Promise。

// 2.x 版本
const oldAsyncComponent = (resolve, reject) => {
  /* ... */
}

// 3.x 版本
const asyncComponent = defineAsyncComponent(
  () =>
    new Promise((resolve, reject) => {
      /* ... */
    })
)

元件事件現在應該使用 emits 選項進行宣告

Vue 3 現在提供一個 emits 選項(也就是上文的 defineEmits),和現有的 props 選項類似。這個選項可以用來定義一個元件可以向其父元件觸發的事件。

行為

在 Vue 2 中,你可以定義一個元件可接收的 prop,但是你無法宣告它可以觸發哪些事件:

<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {
    props: ['text']
  }
</script>

在 vue 3.x 中,和 prop 類似,現在可以透過 emits 選項來定義元件可觸發的事件:

<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {
    props: ['text'],
    emits: ['accepted']
  }
</script>
遷移策略

強烈建議使用 emits 記錄每個元件所觸發的所有事件。

這尤為重要,因為我們移除了 .native 修飾符。任何未在 emits 中宣告的事件監聽器都會被算入元件的 $attrs,並將預設繫結到元件的根節點上。

渲染函式

渲染函式 API 更改

此更改不會影響 <template> 使用者。

以下是更改的簡要總結:

  • h 現在是全域性匯入,而不是作為引數傳遞給渲染函式
  • 更改渲染函式引數,使其在有狀態元件和函式元件的表現更加一致
  • VNode 現在有一個扁平的 prop 結構
$listeners 被移除或整合到 $attrs
$attrs 現在包含 class 和 style attribute

其他小改變

destroyed 生命週期選項被重新命名為 unmounted
beforeDestroy 生命週期選項被重新命名為 beforeUnmount
Props 的 default 工廠函式不再可以訪問 this 上下文
自定義指令的 API 已更改為與元件生命週期一致,且 binding.expression 已移除
data 選項應始終被宣告為一個函式

在 2.x 中,開發者可以透過 object 或者是 function 定義 data 選項。

<!-- Object 宣告 -->
<script>
  const app = new Vue({
    data: {
      apiKey: 'a1b2c3'
    }
  })
</script>

<!-- Function 宣告 -->
<script>
  const app = new Vue({
    data() {
      return {
        apiKey: 'a1b2c3'
      }
    }
  })
</script>

在 3.x 中,data 選項已標準化為只接受返回 object 的 function。

此外,當來自元件的 data() 及其 mixin 或 extends 基類被合併時,合併操作現在將被淺層次地執行:

Tip:mixin 的深度合併非常隱式,這讓程式碼邏輯更難理解和除錯。

const Mixin = {
  data() {
    return {
      user: {
        name: 'Jack',
        id: 1
      }
    }
  }
}

const CompA = {
  mixins: [Mixin],
  data() {
    return {
      user: {
        id: 2
      }
    }
  }
}

在 Vue 2.x 中,生成的 $data 是:

{
  "user": {
    "id": 2,
    "name": "Jack"
  }
}

在 3.0 中,其結果將會是:

{
  "user": {
    "id": 2
  }
}
來自 mixin 的 data 選項現在為淺合併
Attribute 強制策略已更改

這是一個底層的內部 API 更改,絕大多數開發人員不會受到影響。

Transition 的一些 class 被重新命名

過渡類名 v-enter 修改為 v-enter-from、過渡類名 v-leave 修改為 v-leave-from。

<TransitionGroup> 不再預設渲染包裹元素

<transition-group> 不再預設渲染根元素,但仍然可以用 tag attribute 建立根元素。

當偵聽一個陣列時,只有當陣列被替換時,回撥才會觸發,如果需要在變更時觸發,則必須指定 deep 選項

非相容: 當偵聽一個陣列時,只有當陣列被替換時才會觸發回撥。如果你需要在陣列被改變時觸發回撥,必須指定 deep 選項。

沒有特殊指令的標記 (v-if/else-if/else、v-for 或 v-slot) 的 <template> 現在被視為普通元素,並將渲染為原生的 <template> 元素,而不是渲染其內部內容。

這種變化主要是為了更好地與 Web 標準保持一致,並提高 Vue 在靜態分析和工具支援方面的表現。雖然在 Vue 2 中,沒有用於 Vue 指令的 <template> 會被視為特殊的 Vue 模板標記,但在 Vue 3 中,它們被認為是普通的 HTML 元素。

已掛載的應用不會替換它所掛載的元素

在 Vue 2.x 中,當掛載一個具有 template 的應用時,被渲染的內容會替換我們要掛載的目標元素。在 Vue 3.x 中,被渲染的應用會作為子元素插入,從而替換目標元素的 innerHTML。

生命週期的 hook: 事件字首改為 vue:

被移除的 API

keyCode 作為 v-on 修飾符的支援
  • 非相容:不再支援使用數字 (即鍵碼) 作為 v-on 修飾符
  • 非相容:不再支援 config.keyCodes
$on、$off 和 $once 例項方法

$on,$off 和 $once 例項方法已被移除,元件例項不再實現事件觸發介面。

vue2 中用於實現事件匯流排的可以用外部的庫替代,例如 mitt。

在絕大多數情況下,不鼓勵使用全域性的事件匯流排在元件之間進行通訊。雖然在短期內往往是最簡單的解決方案,但從長期來看,它維護起來總是令人頭疼。根據具體情況來看,有多種事件匯流排的替代方案

過濾器 (filter)

在 3.x 中,過濾器已移除,且不再支援。取而代之的是,我們建議用方法呼叫或計算屬性來替換它們。

$children 例項 property

$children 例項 property 已從 Vue 3.0 中移除,不再支援。如果你需要訪問子元件例項,我們建議使用模板引用(即 ref)。

propsData 選項

propsData 選項已經被移除。如果你需要在例項建立時向根元件傳入 prop,你應該使用 createApp 的第二個引數

$destroy 例項方法。使用者不應該再手動管理單個 Vue 元件的生命週期。

完全銷燬一個例項。

vue2:在大多數場景中你不應該呼叫這個方法。最好使用 v-if 和 v-for 指令以資料驅動的方式控制子元件的生命週期。

全域性函式 set 和 delete 以及例項方法 $set 和 $delete。基於代理的變化檢測已經不再需要它們了。

其他章節請看:

vue3 快速入門 系列

相關文章