vue3 特點
- vue3 支持 vue2 的大多數特性
- 性能提升:
打包大小減少41%
初次渲染快55%,更新快133%
內存使用減少54% -
Composition API
ref 和 reactive
computed 和 watch
新的生命周期函數
自定義函數——Hooks 函數 - 其他新增特性
Teleport——瞬移組建的位置
Suspense——異步加載組件的新福音
全局 API 的修改和優化
更多的試驗性特性 - 更好的 Typescript 支持
為什么要有 vue3
- vue2 遇到的難題:同一邏輯分類的代碼分散,不利于維護。
- mixins 難點:命名沖突、不清楚暴露出來變量的作用、組件復用時會遇到問題
- vue2 對于 typescript 的支持非常有限
應用和組件
-
創建應用:
- vue3:
const app = Vue.createApp({}) app.component('SearchInput', SearchInputComponent) app.directive('focus', FocusDirective) app.use(LocalePlugin) app.mount('#app') // 或 Vue.createApp({}) .component('SearchInput', SearchInputComponent) .directive('focus', FocusDirective) .use(LocalePlugin) .mount('#app')
- vue2:
new Vue({ el: '#app', data: obj })
組件是可復用的
Vue
實例,且帶有一個名字,如<button-counter>
。我們可以在一個通過new Vue
創建的Vue
根實例中,把這個組件作為自定義元素來使用。
生命周期鉤子函數
// 主要區別在于銷毀時
beforeCreate() { console.log('實例剛剛被創建') },
created() { console.log('實例創建完成') },
beforeMount() { console.log('實例掛載之前') },
// 請求數據,操作dom , 放在這個里面
mounted() { console.log('實例掛載完成') },
// 數據更新時,虛擬 DOM 變化之前調用,這里適合在更新之前訪問現有的 DOM,比如手動移除已添加的事件監聽器。
beforeUpdate() { console.log('數據更新之前') },
// 數據更新和虛擬 DOM 變化之后調用。請不要在此函數中更改狀態,否則會觸發死循環。
updated() { console.log('數據更新完畢') },
// vue3:
// 實例銷毀之前調用,在這一步,實例仍然完全可用。一般在這里移除事件監聽器、定時器等,避免內存泄漏
beforeUnmount() { console.log('實例銷毀之前') },
// Vue 實例銷毀后調用。調用后,Vue 實例指示的所有東西都會解綁,所有的事件監聽器會被移除,所有的子實例也會被銷毀。
unmounted() { console.log('實例銷毀完成') },
// vue2:
beforeDestroy() { console.log('實例銷毀之前') },
destroyed() { console.log('實例銷毀完成') },
不要在選項
property
、回調上或生命周期函數上使用箭頭函數,比如created: () => console.log(this.a)
或vm.$watch('a', newValue => this.myMethod())
。
因為箭頭函數并沒有this
,this
會作為變量一直向上級詞法作用域查找,直至找到為止。經常導致Uncaught TypeError: Cannot read property of undefined
或Uncaught TypeError: this.myMethod is not a function
之類的錯誤。
不常用模板語法
v-once
指令:執行一次性地插值,當數據改變時,插值處的內容不會更新。但請留心這會影響到該節點上的其它數據綁定:-
動態參數(2.6.0 新增):
<a v-bind:[attributeName]="url"> ... </a>
<a v-on:[eventName]="doSomething"> ... </a>
-
對動態參數表達式的約束:
1、動態參數表達式有一些語法約束,因為某些字符,如空格和引號,放在HTML attribute
名里是無效的。例如:<!-- 這會觸發一個編譯警告 --> <a v-bind:['foo' + bar]="value"> ... </a>
變通的辦法是使用沒有空格或引號的表達式,或用計算屬性替代這種復雜表達式。
2、在
DOM
中使用模板時 (直接在一個HTML
文件里撰寫模板),還需要避免使用大寫字符來命名鍵名,因為瀏覽器會把attribute
名全部強制轉為小寫:<!-- 在 DOM 中使用模板時這段代碼會被轉換為 `v-bind:[someattr]`。 除非在實例中有一個名為“someattr”的 property,否則代碼不會工作。 --> <a v-bind:[someAttr]="value"> ... </a>
動態參數預期會求出一個字符串,異常情況下值為
null
。這個特殊的null
值可以被顯性地用于移除綁定。任何其它非字符串類型的值都將會觸發一個警告。
-
計算屬性和偵聽器
計算屬性是基于它們的響應式依賴進行緩存的。只在相關響應式依賴發生改變時它們才會重新求值。這就意味著只要 message
還沒有發生改變,多次訪問 reversedMessage
計算屬性會立即返回之前的計算結果,而不必再次執行函數。
相比之下,每當觸發重新渲染時,調用方法將總會再次執行函數。
通常更好的做法是使用計算屬性而不是命令式的 watch
回調。
Class 與 Style 綁定
自動添加前綴
當v-bind:style
使用需要添加瀏覽器引擎前綴的CSS property
時,如transform
,Vue.js
會自動偵測并添加相應的前綴。多重值
從2.3.0
起你可以為style
綁定中的property
提供一個包含多個值的數組,常用于提供多個帶前綴的值,如:<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
。
這樣寫只會渲染數組中最后一個被瀏覽器支持的值。在本例中,如果瀏覽器支持不帶瀏覽器前綴的flexbox
,那么就只會渲染display: flex
。
條件渲染
用
key
管理可復用的元素
Vue
會盡可能高效地渲染元素,通常會復用已有元素而不是從頭開始渲染。
這樣也不總是符合實際需求,所以Vue
為你提供了一種方式來表達“這兩個元素是完全獨立的,不要復用它們”。只需添加一個具有唯一值的key
即可。-
v-if
vsv-show
1、v-if
是“真正”的條件渲染,因為它會確保在切換過程中條件塊內的事件監聽器和子組件適當地被銷毀和重建。
2、v-if
也是惰性的:如果在初始渲染時條件為假,則什么也不做——直到條件第一次變為真時,才會開始渲染條件塊。
3、相比之下,v-show
就簡單得多——不管初始條件是什么,元素總是會被渲染,并且只是簡單地基于CSS
進行切換。
4、一般來說,v-if
有更高的切換開銷,而v-show
有更高的初始渲染開銷。因此,如果需要非常頻繁地切換,則使用v-show
較好;如果在運行時條件很少改變,則使用v-if
較好。注意,
v-show
不支持<template>
元素,也不支持v-else
。 v-if
與v-for
一起使用
當v-if
與v-for
一起使用時,v-for
具有比v-if
更高的優先級,這意味著v-if
將分別重復運行于每個v-for
循環中。不推薦同時使用v-if
和v-for
。若使用,eslint
會報錯~~
數組更新檢測
Vue
將被偵聽的數組的變更方法進行了包裹,所以它們也將會觸發視圖更新。這些被包裹過的方法包括:push()
、pop()
、shift()
、unshift()
、splice()
、sort()
、reverse()
事件綁定
同時綁定多個事件
<button @click="one($event), two($event)">Submit</button>
methods: {
one(event) {
// first handler logic...
},
two(event) {
// second handler logic...
}
}
修飾符
事件修飾符
.prevent
:等同于JavaScript
中的event.preventDefault()
,用于取消默認事件
.stop
:等同于JavaScript
中的event.stopPropagation()
,防止事件冒泡(由內到外)
.self
:只會觸發自己范圍內的事件,不包含子元素
.capture
:與事件冒泡的方向相反,事件捕獲由外到內
.once
:事件將只會觸發一次
.passive
:設置{passive: true}
,表示處理事件函數中不會調用preventDefault
函數,減少了額外的監聽,從而提高了性能;所以不能和.prevent
修飾符一同使用,否則瀏覽器會報錯。尤其能夠提升移動端的性能
.native
:把一個vue
組件轉化為一個普通的HTML
標簽,并且該修飾符對普通HTML
標簽是沒有任何作用的。按鍵修飾符
.enter
、.tab
、.esc
、.space
、.up
、.down
、.left
、.right
、.delete
: 捕獲“刪除”和“退格”鍵系統修飾鍵
.ctrl
、.alt
、.shift
、.meta
鼠標按鈕修飾符
.left
、.right
、.middle
-
精確修飾符
.exact
:允許你控制由精確的系統修飾符組合觸發的事件。<!-- 即使 Alt 或 Shift 被一同按下時也會觸發 --> <button v-on:click.ctrl="onClick">A</button> <!-- 有且只有 Ctrl 被按下的時候才觸發 --> <button v-on:click.ctrl.exact="onCtrlClick">A</button> <!-- 沒有任何系統修飾符被按下的時候才觸發 --> <button v-on:click.exact="onClick">A</button>
-
表單修飾符
.lazy
:在默認情況下,v-model
在每次input
事件觸發后將輸入框的值與數據進行同步 (除了上述輸入法組合文字時)。你可以添加lazy
修飾符,從而轉為在change
事件之后進行同步。
.number
:自動將用戶的輸入值轉為數值類型。
.trim
:自動過濾用戶輸入的首尾空白字符。修飾符可以串聯,也可以只有修飾符。
使用修飾符時,順序很重要;相應的代碼會以同樣的順序產生。因此,用v-on:click.prevent.self
會阻止所有的點擊,而v-on:click.self.prevent
只會阻止對元素自身的點擊。
組件
全局組件:使用
app.component('search-input', SearchInputComponent)
定義。只要定義了,處處可以使用,性能不高,但是使用起來簡單。建議小寫字母開頭中劃線間隔命名。局部組件:使用
components
注冊。定義了,要注冊之后才能使用,性能較高,使用起來麻煩。建議大寫字母開頭駝峰命名。-
Prop
1、Prop
的大小寫
HTML
中的attribute
名是大小寫不敏感的,所以瀏覽器會把所有大寫字符解釋為小寫字符。這意味著當你使用DOM
中的模板時,camelCase
(駝峰命名法) 的prop
名需要使用其等價的kebab-case
(短橫線分隔命名) 命名:const app = Vue.createApp({}) app.component('blog-post', { // camelCase in JavaScript props: ['postTitle'], template: '<h3>{{ postTitle }}</h3>' })
<!-- 在 HTML 中是 kebab-case 的 --> <blog-post post-title="hello!"></blog-post>
2、傳入一個對象的所有
property
如果你想要將一個對象的所有property
都作為prop
傳入,你可以使用不帶參數的v-bind
(取代v-bind:prop-name
)。例如,對于一個給定的對象post
:post: { id: 1, title: 'My Journey with Vue' }
下面的模板:
<blog-post v-bind="post"></blog-post>
等價于:
<blog-post :id="post.id" :title="post.title"></blog-post>
3、單項數據流
所有的prop
都使得其父子prop
之間形成了一個單向下行綁定:父級prop
的更新會向下流動到子組件中,但是反過來則不行。這樣會防止從子組件意外變更父級組件的狀態,從而導致你的應用的數據流向難以理解。4、Prop 驗證
app.component('my-component', { props: { // 基礎的類型檢查 (`null` 和 `undefined` 會通過任何類型驗證) propA: Number, // 多個可能的類型 propB: [String, Number], // 必填的字符串 propC: { type: String, required: true }, // 帶有默認值的數字 propD: { type: Number, default: 100 }, // 帶有默認值的對象 propE: { type: Object, // 對象或數組默認值必須從一個工廠函數獲取 default: function () { return { message: 'hello' } } }, // 自定義驗證函數 propF: { validator: function (value) { // 這個值必須匹配下列字符串中的一個 return ['success', 'warning', 'danger'].indexOf(value) !== -1 } } } })
5、非 prop 的 attribute
組件可以接受任意的attribute
,而這些attribute
會被添加到這個組件的根元素上。
如果你不希望組件的根元素繼承attribute
,你可以在組件的選項中設置inheritAttrs: false
。(不會影響style
和class
的綁定)app.component('date-picker', { inheritAttrs: false, template: ` <div class="date-picker"> <input type="datetime" v-bind="$attrs" /> </div> ` })
當組件有多個根節點時:
<custom-layout id="custom-layout" @click="changeValue"></custom-layout>
// This will raise a warning app.component('custom-layout', { template: ` <header>...</header> <main>...</main> <footer>...</footer> ` }) // No warnings, $attrs are passed to <main> element app.component('custom-layout', { template: ` <header>...</header> <main v-bind="$attrs">...</main> <footer>...</footer> ` })
自定義事件
不同于組件和 prop
,事件名不存在任何自動化的大小寫轉換。因為 HTML
是大小寫不敏感的,因此推薦你始終使用 kebab-case 的事件名。
- 驗證發出的事件
app.component('custom-form', {
emits: {
// No validation
click: null,
// Validate submit event
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
},
methods: {
submitForm() {
this.$emit('submit', { email, password })
}
}
})
- 自定義組件的
v-model
<my-component v-model:title="bookTitle"></my-component>
app.component('my-component', {
props: {
title: String
},
emits: ['update:title'],
template: `
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)">
`
})
- 同時綁定多個
v-model
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName"
></user-name>
app.component('user-name', {
props: {
firstName: String,
lastName: String
},
emits: ['update:firstName', 'update:lastName'],
template: `
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)">
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)">
`
})
-
v-model
增加自定義修飾符
<my-component v-model.capitalize="myText"></my-component>
app.component('my-component', {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
template: `
<input type="text"
:value="modelValue"
@input="emitValue">
`,
created() {
console.log(this.modelModifiers) // { capitalize: true }
},
methods: {
emitValue(e) {
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
},
})
插槽
編譯作用域:父級模板里的所有內容都是在父級作用域中編譯的;子模板里的所有內容都是在子作用域中編譯的。
后備內容:
// slot 標簽內為后備內容,父組件提供了插槽內容,則展示;否則展示后備內容
<button type="submit">
<slot>Submit</slot>
</button>
- 具名插槽:
v-slot
只能添加在<template>
上(獨占默認插槽的縮寫語法除外)
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<!-- 一個不帶 name 的 <slot> 出口會帶有隱含的名字“default” -->
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<base-layout>
<!-- 可簡寫為 <template #header> -->
<!-- 還可使用動態插槽名:<template v-slot:[dynamicSlotName]> -->
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
- 作用域插槽
為了讓插槽內容能夠訪問子組件中才有的數據,可以將data
作為<slot>
元素的一個attribute
綁定上去。
<!-- 綁定在 <slot> 元素上的 attribute 被稱為插槽 prop -->
<span>
<slot :user="user">
{{ user.lastName }}
</slot>
</span>
<!-- 將包含所有插槽 prop 的對象命名為 slotProps,也可以使用任意你喜歡的名字 -->
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
- 獨占默認插槽的縮寫語法
被提供的內容只有默認插槽時,組件的標簽才可以被當作插槽的模板來使用
<!--
解構插槽 Prop:
<current-user v-slot="{ user: person }"> or <current-user #default="{ user }">
-->
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
</current-user>
provide / inject
使用場景:由于 vue
有 $parent
屬性可以讓子組件訪問父組件。但孫組件想要訪問祖先組件就比較困難。通過 provide / inject
可以輕松實現跨級訪問祖先組件的數據。
provide:Object | () => Object
inject:Array<string> | { [key: string]: string | Symbol | Object }
// TodoList -> TodoListFooter -> TodoListStatistics
app.component('todo-list', {
// ...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})
app.component('todo-list-statistics', {
inject: ['todoLength'],
created() {
console.log(`Injected property: ${this.todoLength.value}`) // > Injected property: 5
}
})
提示:provide
和 inject
綁定并不是可響應的。然而,如果你傳入了一個可監聽的對象,那么其對象的屬性還是可響應的。
動態組件與異步組件
動態組件:
<!-- 失活的組件將會被緩存!-->
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
注意這個
<keep-alive>
要求被切換到的組件都有自己的名字,不論是通過組件的name
選項還是局部 / 全局注冊。
異步組件:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
app.component('async-component', AsyncComp)
// or
import { createApp, defineAsyncComponent } from 'vue'
createApp({
// ...
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
}
})
mixin
混入 mixin
提供了一種非常靈活的方式,來分發 Vue
組件中的可復用功能。一個混入對象可以包含任意組件選項。當組件使用混入對象時,所有混入對象的選項將被“混合”進入該組件本身的選項。
- 例子
// 定義一個混入對象
const myMixin = {
created() {
this.hello()
},
methods: {
hello() {
console.log('hello from mixin!')
}
}
}
// 定義一個使用混入對象的組件
const app = Vue.createApp({
mixins: [myMixin]
})
app.mount('#app') // => "hello from mixin!"
-
選項合并
當組件和混入對象含有同名選項時,這些選項將以恰當的方式進行“合并”。- 數據對象在內部會進行遞歸合并,并在發生沖突時以組件數據優先。
- 同名鉤子函數將合并為一個數組,因此都將被調用。另外,混入對象的鉤子將在組件自身鉤子之前調用。
- 值為對象的選項,例如
methods
、components
和directives
,將被合并為同一個對象。兩個對象鍵名沖突時,取組件對象的鍵值對。
全局混入
混入也可以進行全局注冊。一旦使用全局混入,它將影響每一個之后創建的Vue
實例 (包括第三方組件)。
const app = Vue.createApp({
// 自定義屬性
myOption: 'hello!'
})
// 為自定義的選項 'myOption' 注入一個處理器。
app.mixin({
created() {
// 獲取自定義屬性 this.$options.XXX
const myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
// add myOption also to child component
app.component('test-component', {
myOption: 'hello from component!'
})
app.mount('#app')
// => "hello!"
// => "hello from component!"
- 自定義選項合并策略
自定義選項將使用默認策略,即簡單地覆蓋已有值。如果想讓自定義選項以自定義邏輯合并,可以向app.config.optionMergeStrategies
添加一個函數:
const app = Vue.createApp({})
// 返回合并后的值
app.config.optionMergeStrategies.customOption = (mixinVal, appVal) => mixinVal || appVal
自定義指令
- 注冊一個全局自定義指令
v-focus
:
const app = Vue.createApp({})
app.directive('focus', {
// 當被綁定的元素插入到 DOM 中時……
mounted(el) {
// 聚焦元素
el.focus()
}
})
- 注冊局部指令:
app.directive('focus', {
mounted (el) {
el.focus()
}
})
// 若只有 mounted 和 updated,且邏輯一致,可簡寫
// binding.arg 獲取參數;binding.value 獲取值
app.directive('top', (el, binding) => {
el.style.top = binding.value + 'px'
})
-
鉤子函數
- created: 在綁定元素的屬性或事件偵聽器被應用之前調用,只調用一次
- beforeMount: 當指令第一次綁定到元素上并且父組件掛載之前調用
- mounted: 當綁定元素的父組件掛載完成時調用
- beforeUpdate: 指令所在組件的 VNode 及其子 VNode 更新前調用
- updated: 指令所在組件的 VNode 及其子 VNode 更新后調用
- beforeUnmount: 當綁定元素的父組件銷毀之前調用
- unmounted: 只調用一次,指令與元素解綁時調用
動態指令參數
<div id="dynamicexample">
<h2>Scroll down the page</h2>
<input type="range" min="0" max="500" v-model="pinPadding">
<p v-pin:[direction]="pinPadding">Stick me {{ pinPadding + 'px' }} from the {{ direction }} of the page</p>
</div>
const app = Vue.createApp({
data() {
return {
direction: 'right',
pinPadding: 200
}
}
})
app.directive('pin', {
mounted(el, binding) {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
},
updated(el, binding) {
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
}
})
傳送門 teleport
將插槽內容傳送至指定位置,接受一個 to
的屬性,它接受一個 css query selector
作為參數,這就是代表要把這個組件渲染到哪個 dom
元素中
<body>
<div id="app"></div>
<div id="childBox"></div>
<div id="modals"></div>
<div class="wrapper"></div>
<div data-teleport></div>
</body>
const app = Vue.createApp({
template: `
<h1>Root</h1>
<modal-button />
<parent-component />
<multiple-teleports />
`
})
// 基礎用法
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
I'm a teleported modal! (My parent is "body")
<button @click="modalOpen = false">Close</button>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})
// 與Vue components一起使用,如果<teleport>包含Vue組件,則它仍將是<teleport>父組件的邏輯子組件
app.component('parent-component', {
template: `
<h2>This is a parent component</h2>
<teleport to="#childBox">
<child-component name="John" />
</teleport>
`
})
app.component('child-component', {
props: [ 'name' ],
template: `<div>Hello, {{ name }}</div>`
})
// 在同一目標上使用多個teleport
// disabled 可以用于禁用teleport組件的功能,這意味著它的插槽內容將不會被移動到任何位置,而是在周圍父組件中指定<teleport>的地方渲染。
app.component('multiple-teleports', {
template: `
<teleport to="#modals" disabled>
<div>A</div>
</teleport>
<teleport to="#modals">
<div>B</div>
</teleport>
<teleport to=".wrapper">
<div>C</div>
</teleport>
<teleport to="[data-teleport]">
<div>D</div>
</teleport>
`
})
app.mount('#app')
Suspense
Suspense
組件用于在等待某個異步組件解析時顯示后備內容。
AsyncShow.vue
<template>
<div v-for="(item, index) in list" :key="index">{{item}}</div>
</template>
<script lang="ts">
import axios from 'axios'
import { defineComponent, PropType } from 'vue'
export default defineComponent({
name: 'AsyncShow',
async setup () {
const result = await axios.get('https://XXX')
return {
list: result.data,
}
},
})
</script>
index.vue
<template>
<Suspense>
<template #default>
<async-show />
</template>
<template #fallback>
<h1>Loading !...</h1>
</template>
</Suspense>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import AsyncShow from '@/components/AsyncShow.vue'
export default defineComponent({
name: 'Index',
components: {
AsyncShow,
},
})
</script>