其他章節請看: vue3 快速入門 系列 他API 前面我們已經學習了 vue3 的一些基礎知識,本篇將繼續講解一些常用的其他api,以及較完整的分析vue2 和 vue3 的改變。 淺層響應式數據 shallowRef shallow 中文:“淺層的” shallowRef:淺的 ref()。 先 ...
其他章節請看:
他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。基於代理的變化檢測已經不再需要它們了。
其他章節請看:
出處:https://www.cnblogs.com/pengjiali/p/18150115
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接。