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 服務端渲染。