vue作者尤雨溪在開發 vue3.0 的時候開發的一個基於瀏覽器原生 ES imports 的開發伺服器(開發構建工具)。那麼我們先來了解一下vite
Vite
Vite,一個基於瀏覽器原生 ES imports 的開發伺服器。利用瀏覽器去解析 imports,在伺服器端按需編譯返回,完全跳過了打包這個概念,伺服器隨起隨用。同時不僅有 Vue 檔案支援,還搞定了熱更新,而且熱更新的速度不會隨著模組增多而變慢。針對生產環境則可以把同一份程式碼用 rollup 打。雖然現在還比較粗糙,但這個方向我覺得是有潛力的,做得好可以徹底解決改一行程式碼等半天熱更新的問題。它做到了本地快速開發啟動, 用 vite 文件上的介紹,它具有以下特點:
- 快速的冷啟動,不需要等待打包操作;
- 即時的熱模組更新,替換效能和模組數量的解耦讓更新飛起;
- 真正的按需編譯,不再等待整個應用編譯完成;
使用 npm:
# npm 7+,需要加上額外的雙短橫線
$ npm init vite@latest <project-name> -- --template vue
$ cd <project-name>
$ npm install
$ npm run dev
或者 yarn:
$ yarn create vite <project-name> --template vue
$ cd <project-name>
$ yarn
$ yarn dev
概覽
- 速度更快
- 體積減少
- 更易維護
- 更接近原生
- 更易使用
- 重寫了虛擬Dom實現
diff演算法優化
<div>
<span/>
<span>{{ msg }}</span>
</div>
被編譯成:
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("span", null, "static"),
_createVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
首先靜態節點進行提升,會提升到 render 函式外面,這樣一來,這個靜態節點永遠只被建立一次,之後直接在 render 函式中使用就行了。
Vue在執行時會生成number(大於0)值的PatchFlag,用作標記,僅帶有PatchFlag標記的節點會被真正追蹤,無論層級巢狀多深,它的動態節點都直接與Block根節點繫結,無需再去遍歷靜態節點,所以處理的資料量減少,效能得到很大的提升。
- 事件監聽快取:cacheHandlers
<div>
<span @click="onClick">
{{msg}}
</span>
</div>
優化前:
import { toDisplayString as _toDisplayString, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("span", { onClick: _ctx.onClick }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["onClick"])
]))
}
onClick會被視為PROPS動態繫結,後續替換點選事件時需要進行更新。
優化後:
import { toDisplayString as _toDisplayString, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("span", {
onClick: _cache[1] || (_cache[1] = $event => (_ctx.onClick($event)))
}, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
會自動生成一個行內函數,這個行內函數裡面再去引用當前元件最新的onclick,然後把這個行內函數cache起來,第一次渲染的時候會建立行內函數並且快取,後續的更新就直接從快取裡面讀同一個函式,既然是同一個函式就沒有再更新的必要,就變成了一個靜態節點
3. SSR速度提高
當有大量靜態的內容時,這些內容會被當做純字串推進一個buffer裡面,即使存在動態的繫結,會通過模板 插值嵌入進去,這樣會比通過虛擬dom來渲染的快很多。vue3.0 當靜態檔案大到一定量的時候,會用_ceratStaticVNode方法在客戶端去生成一個static node, 這些靜態node,會被直接innerHtml,就不需要建立物件,然後根據物件渲染
- tree-shaking
tree-shakinng 原理
主要依賴es6的模組化的語法,es6模組依賴關係是確定的,和執行時的狀態無關,可以進行可靠的靜態分析,
分析程式流,判斷哪些變數未被使用、引用,進而刪除對應程式碼
前提是所有的東西都必須用ES6 module的import來寫
按照作者的原話解釋,Tree-shaking其實就是:把無用的模組進行“剪枝”,很多沒有用到的API就不會打包到最後的包裡
在Vue2中,全域性 API 如 Vue.nextTick() 是不支援 tree-shake 的,不管它們實際是否被使用,都會被包含在最終的打包產物中。
而Vue3原始碼引入tree shaking特性,將全域性 API 進行分塊。如果你不使用其某些功能,它們將不會包含在你的基礎包中
5. compositon Api
沒有Composition API之前vue相關業務的程式碼需要配置到option的特定的區域,中小型專案是沒有問題的,但是在大型專案中會導致後期的維護性比較複雜,同時程式碼可複用性不高
compositon api提供了以下幾個函式:
-
setup (入口函式,接收兩個引數(props,context))
-
ref (將一個原始資料型別轉換成一個帶有響應式特性)
-
reactive (reactive 用來定義響應式的物件)
-
watchEffect
-
watch
-
computed
-
toRefs (解構響應式物件資料)
-
生命週期的hooks
如果用ref處理物件或陣列,內部會自動將物件/陣列轉換為reactive的代理物件
ref內部:通過給value屬性新增getter/setter來實現對資料的劫持
reactive內部:通過使用proxy來實現對物件內部所有資料的劫持,並通過Reflect反射操作物件內部資料
ref的資料操作:在js中使用ref物件.value獲取資料,在模板中可直接使用
import { useRouter } from 'vue-router'
import { reactive, onMounted, toRefs } from 'vue'
// setup在beforeCreate 鉤子之前被呼叫
// setup() 內部,this是undefined,因為 setup() 是在解析其它元件選項之前被呼叫的,所以 setup() 內部的 this 的行為與其它選項中的 this 完全不同。這在和其它選項式 API 一起使用 setup() 時可能會導致混淆
// props 是響應式的,當傳入新的 prop 時,它將被更新(因為props是響應式的,所以不能使用 ES6 解構,因為它會消除 prop 的響應性。)
// props引數:包含元件props配置宣告且傳入了的所有props的物件
// attrs引數:包含沒有在props配置中宣告的屬性物件,相當於this.$attrs
// slots引數:包含所有傳入的插槽內容的物件,相當於this.$slots
// emit引數:可以用來分發一個自定義事件,相當於this.$emit
setup (props, {attrs, slots, emit}) {
const state = reactive({
userInfo: {}
})
const getUserInfo = async () => {
state.userInfo = await GET_USER_INFO(props.id)
}
onMounted(getUserInfo) // 在 `mounted` 時呼叫 `getUserInfo`
// setup的返回值
// 一般都是返回一個物件,為模板提供資料,就是模板中可以直接使用此物件中所有屬性/方法
// 返回物件中的屬性會與data函式返回物件的屬性合併成為元件物件的屬性
// 返回物件中的方法會與methods中的方法合併成元件物件的方法
// 若有重名,setup優先
return {
...toRefs(state),
getUserInfo
}
}
靈活的邏輯組合與複用
可與現有的Options API一起使用
與選項API最大的區別的是邏輯的關注點
選項API這種碎片化使得理解和維護複雜元件變得困難,在處理單個邏輯關注點時,我們必須不斷地上下翻找相關程式碼的選項塊。
compositon API將同一個邏輯關注點相關程式碼收集在一起
6. Fragment(碎片)
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
Vue 3不再限於模板中的單個根節點,它正式支援了多根節點的元件,可純文字,多節點,v-for等
render 函式也可以返回陣列
7. Teleport(傳送門)
這個元件的作用主要用來將模板內的 DOM 元素移動到其他位置。
允許我們控制在 DOM 中哪個父節點下渲染了 HTML
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
-
更好的Typescript支援
vue3是基於typescipt編寫的,可以享受到自動的型別定義提示 -
自定義渲染 API
vue官方實現的 createApp 會給我們的 template 對映生成 html 程式碼,但是要是你不想渲染生成到 html ,而是要渲染生成到 canvas 之類的不是html的程式碼的時候,那就需要用到 Custom Renderer API 來定義自己的 render 渲染生成函式了。
意味著以後可以通過 vue, Dom 程式設計的方式來進行canvas、webgl 程式設計
預設的目標渲染平臺
自定義目標渲染平臺
-
響應原理的變化
vue2物件響應化:遍歷每個key,通過 Object.defineProperty API定義getter,setter 進而觸發一些檢視更新
陣列響應化:覆蓋陣列的原型方法,增加通知變更的邏輯
vue2響應式痛點
遞迴,消耗大
新增/刪除屬性,需要額外實現單獨的API
陣列,需要額外實現
Map Set Class等資料型別,無法響應式
修改語法有限制
vue3響應式方案: 使用ES6的Proxy進行資料響應化,解決上述vue2所有痛點,Proxy可以在目標物件上加一層攔截/代理,外界對目標物件的操作,都會經過這層攔截。Proxy可以在目標物件上加一層攔截/代理,外界對目標物件的操作,都會經過這層攔截,相比 Object.defineProperty ,Proxy支援的物件操作十分全面
一, 全域性api
1. 全域性 Vue API 已更改為使用應用程式例項
vue2使用全域性api 如 Vue.component, Vue.mixin, Vue.use等,缺點是會導致所建立的根例項將共享相同的全域性配置(從相同的 Vue 建構函式建立的每個根例項都共享同一套全域性環境。這樣就導致一個問題,只要某一個根例項對 全域性 API 和 全域性配置做了變動,就會影響由相同 Vue 建構函式建立的其他根例項。)
vue3 新增了createApp,呼叫createApp返回一個應用例項,擁有全域性API的一個子集,任何全域性改變 Vue 行為的 API 現在都會移動到應用例項上
2. 元件掛載
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
createApp初始化後會返回一個app物件,裡面包含一個mount函式
mount函式是被重寫過的
- 處理傳入的容器並生成節點;
- 判斷傳入的元件是不是函式元件,元件裡有沒有render函式,template屬性,沒有就用容器的innerHTML作為元件的template;
- 清空容器內容
- 執行快取的mount函式實現掛載元件;
二, 模板指令
- 元件上 v-model 用法更改,替換 v-bind.sync
vue2預設會利用名為 value 的 prop 和名為 input 的事件
// ParentComponent
<ChildComponent v-model="pageTitle" />
<!-- 是以下的簡寫: -->
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
// ChildComponent
<input type="text" :value="value" @input="$emit('input', $event.target.value)">
如果想要更改 prop 或事件名稱,則需要在元件中新增 model 選項:
model選項,允許元件自定義用於 v-model 的 prop 和事件
// ChildComponent
<input type="text" :value="title" @input="$emit('change', $event.target.value)">
export default {
model: {
prop: 'title',
event: 'change'
},
props: {
title: String
}
}
使用 title
代替 value
作為 model 的 prop
vue2.3 新增.sync (對某一個 prop 進行“雙向繫結”,是update:title 事件的簡寫)
// ParentComponent
<ChildComponent :title.sync="name" />
<!-- 是以下的簡寫 -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
// ChildComponent
<input type="text" :value="title" @input="$emit('update:title', $event.target.value)">
在 3.x 中,自定義元件上的 v-model 相當於傳遞了 modelValue prop 並接收丟擲的 update:modelValue 事件
prop:value -> modelValue;
event:input -> update:modelValue
v-bind 的 .sync 修飾符和元件的 model 選項已移除,可用 v-model加引數 作為代替
vue3 可以將一個 argument 傳遞給 v-model:
<ChildComponent v-model:title="pageTitle" />
等價於
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
可使用多個model
- 可以在template元素上新增 key
<template v-for="item in list" :key="item.id">
<div>...</div>
</template>
- 同一節點v-if 比 v-for 優先順序更高
- v-bind="object" 現在排序敏感(繫結相同property,vue2單獨的 property 總是會覆蓋 object 中的繫結。vue3按順序決定如何合併)
<div id="red" v-bind="{ id: 'blue' }" ></div>
// vue2 id="red"
// vue3 id="blue"
- 移除 v-on.native 修飾符
Vue 2 如果想要在一個元件的根元素上直接監聽一個原生事件,需要使用v-on 的 .native 修飾符
Vue3 現在將所有未在元件emits 選項中定義的事件作為原生事件新增到子元件的根元素中(除非子元件選項中設定了 inheritAttrs: false)。
(強烈建議元件中使用的所有通過emit觸發的event都在emits中宣告)
<my-component @close="handleComponentEvent" @click="handleNativeClickEvent"/>
// mycomponent
<template>
<div>
<button @click="$emit('click')">click</button>
<button @click="$emit('close')">close</button>
</div>
</template>
<script>
export default {
emits: ['close']
}
</script>
- v-for 中的 ref 不再註冊 ref 陣列
vue2在 v-for 語句中使用ref屬性時,會生成refs陣列插入$refs屬性中。由於當存在巢狀的v-for時,這種處理方式會變得複雜且低效。
vue3在 v-for 語句中使用ref屬性 將不再會自動在$refs中建立陣列。而是,將 ref 繫結到一個 function 中,在 function 中可以靈活處理ref。
<div v-for="item in list" :ref="setItemRef"></div>
export default {
setup() {
let itemRefs = []
const setItemRef = el => {
itemRefs.push(el)
}
return {
setItemRef
}
}
}
三, 元件
-
函式式元件
在 Vue 2 中,函式式元件有兩個主要應用場景:
作為效能優化,因為它們的初始化速度比有狀態元件快得多
返回多個根節點
然而Vue 3對有狀態元件的效能進行了提升,與函式式元件的效能相差無幾。此外,有狀態元件現在還包括返回多個根節點的能力。所以,建議只使用有狀態元件。結合<template>的函式式元件:
- functional 移除
- 將 props 的所有引用重新命名為 $props,attrs 重新命名為 $attrs。
<template>
<component :is=`h${$props.level}` v-bind='$attrs' />
</template>
<script>
export default {
props: ['level']
}
</script>
函式寫法:
相較於 Vue 2.x 有三點變化:
- 所有的函式式元件都是用普通函式建立的,換句話說,不需要定義 { functional: true } 元件選項。
- export default匯出的是一個函式,函式有兩個引數:
props
context(上下文):context是一個物件,包含attrs、slot、emit屬性 - h函式需要全域性匯入
import { h } from 'vue'
const DynamicHeading = (props, context) => {
return h(`h${props.level}`, context.attrs, context.slots)
}
DynamicHeading.props = ['level']
export default DynamicHeading
- 非同步元件需要 defineAsyncComponent 方法來建立
非同步元件的匯入需要使用輔助函式defineAsyncComponent來進行顯式宣告
import { defineAsyncComponent } from 'vue'
const child = defineAsyncComponent(() => import('@/components/async-component-child.vue'))
帶選項非同步元件,component 選項重新命名為 loader
const asyncPageWithOptions = defineAsyncComponent({
loader: () => import('./NextPage.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
})
- (新增)元件事件需要在 emits 選項中宣告()
強烈建議使用 emits 記錄每個元件所觸發的所有事件。
因為移除了 v-on.native 修飾符。任何未宣告 emits 的事件監聽器都會被算入元件的 $attrs 並繫結在元件的根節點上。
如果emit的是原生的事件(如,click),就會存在兩次觸發。
一次來自於$emit的觸發;
一次來自於根元素原生事件監聽器的觸發;
(emits 1.更好的記錄已發出的事件,2.驗證丟擲的事件)
export default {
props: ['text'],
emits: ['accepted']
}
emits: {
click: null,
submit: payload => {
if (payload.email && payload.password) {
return true
} else {
console.warn(`Invalid submit event payload!`)
return false
}
}
}
四, 渲染函式
- 渲染函式API
h是全域性匯入,而不是作為引數傳遞給渲染函式
在 2.x 中,render 函式會自動接收 h 函式作為引數
在 3.x 中,h 函式需要全域性匯入。由於 render 函式不再接收任何引數,它將主要在 setup() 函式內部使用。可以訪問在作用域中宣告的響應式狀態和函式,以及傳遞給 setup() 的引數
import { h, reactive } from 'vue'
export default {
setup(props, { slots, attrs, emit }) {
const state = reactive({
count: 0
})
function increment() {
state.count++
}
// 返回render函式
return () =>
h(
'div',
{
onClick: increment
},
state.count
)
}
}
- 移除$listeners整合到 $attrs
包含了父作用域中的(不含emits的) v-on 事件監聽器。它可以通過 v-on="$listeners" 傳入內部元件
{{$attrs}}
<grand-son v-bind="$attrs"></grand-son>
- $attrs包含class&style
在vue2中,關於父元件使用子元件有這樣一個原則:
預設情況下父作用域的不被認作 props 的 attribute 繫結 (attribute bindings) 將會“回退”且作為普通的 HTML attribute 應用在子元件的根元素上
這句話的意思是,父元件呼叫子元件時,給子元件錨點標籤新增的屬性中,除了在子元件的props中宣告的屬性,其他屬性會自動新增到子元件根元素上。
為此,vue新增了inheritAttrs = false,這些預設行為將會被去掉,通過例項 property $attrs 可以讓這些 attribute 生效,且可以通過 v-bind 顯性的繫結到非根元素上。
五, 自定義元素
- 自定義元素檢測在編譯時執行
自定義元素互動
Vue 2中,通過 Vue.config.ignoredElements 配置自定義元素
Vue.config.ignoredElements = ['plastic-button']
Vue 3 通過app.config.isCustomElement
const app = Vue.createApp({})
app.config.isCustomElement = tag => tag === 'plastic-button'
- Vue 3.x 對 is做了新的限制
當在 Vue 保留的 component標籤上使用is時,它的行為將與 Vue 2.x 中的一致
當在不同元件標籤上使用is時,is會被當做一個不同的prop;
當在普通的 HTML 元素上使用is,is將會被當做元素的屬性。
新增了v-is,專門來實現在普通的 HTML 元素渲染元件。
六, 其他
- destroyed 生命週期選項被重新命名為 unmounted
- beforeDestroy 生命週期選項被重新命名為 beforeUnmount
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-awWIzouv-1637307558259)(assets/vue3/img.png)]
整體來看其實變化不大,使用setup代替了之前的beforeCreate和created,其他生命週期名字有些變化,功能都是沒有變化的 - Props 的預設值函式不能訪問this
替代方案:
把元件接收到的原始 prop 作為引數傳遞給預設函式;
inject API 可以在預設函式中使用。
import { inject } from 'vue'
export default {
props: {
theme: {
default (props) {
// `props` 是傳遞給元件的原始值。
// 也可以使用 `inject` 來訪問注入的屬性
return inject('theme', 'default-theme')
}
}
}
}
- 自定義指令 API 與元件生命週期一致
const MyDirective = {
created(el, binding, vnode, prevVnode) {}, // 新增
beforeMount() {},
mounted() {},
beforeUpdate() {}, // 新增
updated() {},
beforeUnmount() {}, // 新增
unmounted() {}
}
繫結元件的例項從 Vue 2.x 的vnode.context移到了binding.instance中
- data 選項應始終被宣告為一個函式
data 元件選項宣告不再接收 js 物件,只接受函式形式的宣告。
<script>
import { createApp } from 'vue'
createApp({
data() {
return {
apiKey: 'a1b2c3'
}
}
}).mount('#app')
</script>
當合並來自 mixin 或 extend 的多個 data 返回值時,data現在變為淺拷貝形式(只合並根級屬性)。
const Mixin = {
data() {
return {
user: {
name: 'Jack',
id: 1
}
}
}
}
const CompA = {
mixins: [Mixin],
data() {
return {
user: {
id: 2
}
}
}
}
vue2
{
"user": {
"id": 2,
"name": "Jack"
}
}
vue3
{
"user": {
"id": 2
}
}
- 過渡的 class 名更改(過渡類名 v-enter 修改為 v-enter-from、過渡類名 v-leave 修改為 v-leave-from。)
- transition-group 不再需要設定根元素(
不再預設渲染根元素,但仍可以使用 tag prop建立一個根元素。) - 偵聽陣列(當偵聽一個陣列時,只有當陣列被替換時才會觸發回撥。如果你需要在陣列改變時觸發回撥,必須指定 deep 選項。)
- 已掛載的應用不會取代它所掛載的元素(在vue2中,當掛載一個具有 template 的應用時,被渲染的內容會替換我們要掛載的目標元素。在 Vue 3.x 中,被渲染的應用會作為子元素插入,從而替換目標元素的 innerHTML)
- 生命週期 hook: 事件字首改為 vnode-(監聽子元件和第三方元件的生命週期)
移除API
- 不再支援使用數字 (即鍵碼) 作為 v-on 修飾符,vue3建議使用按鍵alias(別名)作為v-on的修飾符。
<input v-on:keyup.delete="confirmDelete" />
- vue3將移除且不再支援 filters,如果需要實現過濾功能,建議通過method或computed屬性來實現(如果需要使用全域性過濾器vue3提供了globalProperties。我們可以藉助globalProperties來註冊全域性過濾, 全域性過濾器裡面定義的只能是method。)
const app = createApp(App)
app.config.globalProperties.$filters = {
currencyUSD(value) {
return '$' + value
}
}
<template>
<p>{{ $filters.currencyUSD(accountBalance) }}</p>
</template>
- 內聯模板 (inline-template attribute移除)
- $children(如果需要訪問子元件例項,建議使用 $refs)
- propsData 選項之前用於在建立 Vue 例項的過程中傳入 prop,現在它被移除了。如果想為 Vue 3 應用的根元件傳入 prop,使用 createApp 的第二個引數。
- 全域性函式 set 和 delete 以及例項方法 $set 和 $delete。基於代理的變化檢測不再需要它們了。
用於遷移的構建版本
@vue/compat (即“遷移構建版本”) 是一個 Vue 3 的構建版本,提供了可配置的相容 Vue 2 的行為。
該構建版本預設執行在 Vue 2 的模式下——大部分公有 API 的行為和 Vue 2 一致,僅有一小部分例外。使用在 Vue 3 中發生改變或被廢棄的特性時會丟擲執行時警告。一個特性的相容性也可以基於單個元件進行開啟或禁用。
已知的限制:
-
基於vue2內部API或文件中未記載行為的依賴。最常見的情況就是使用 VNodes 上的私有 property。如果你的專案依賴諸如 Vuetify、Quasar 或 Element UI 等元件庫,那麼最好等待一下它們的 Vue 3 相容版本。
-
對IE11的支援:Vue 3 已經官方放棄對 IE11 的支援。如果仍然需要支援 IE11 或更低版本,那你仍需繼續使用 Vue 2。
-
服務端渲染:該遷移構建版本可以被用於服務端渲染,但是遷移一個自定義的服務端渲染設定有更多工作要做。大致的思路是將 vue-server-renderer 替換為 @vue/server-renderer。Vue 3 不再提供一個包渲染器,推薦使用 Vite 以支援 Vue 3 服務端渲染。