學習一波Vue3新特性
前言
去年前端界最轟動的事無非是React Hook
的釋出,上到react-router
、react-redux
等生態庫,下到React
應用開發者都從class component
開發方式積極擁抱Hook
。那今年我認為最值得關注的是Vue3.0
,目前我們可以從vue-next看到Vue3.0
以及vuex
、vue-router
等生態的開發進度,大概7、8月份Vue3.0
正式版本就要落地,到時又會引起前端界的廣泛討論,這裡我整理了一波Vue3
的亮點,供大家參考。
Composition API
什麼是composition api
?先補一波英語知識,composition
是構成、作文
的意思,動詞是compose
compose
估計很多人會想到koa
、redux
等庫的原始碼中有一個compose
核心函式,這個函式像橋樑一樣連線中介軟體(middleware
)的呼叫,是構成這些優秀庫中介軟體機制的關鍵!想想compose
都那麼牛,那composition api
可以帶給我們什麼?能解決什麼問題?這裡我們從下面幾個現象入手,理解composition api
的設計理念。
-
現象一:當你維護前同事長達1000多行的祖傳程式碼,為理解程式碼含義在不斷的
template
、data
、methods
等程式碼中痛苦跳轉時 -
現象二:當你苦苦找不到
this.xxx()
方法定義,卻不敢刪掉,最終在各種mixin
檔案中找到它的真身時
這個時候真的需要composition api
這樣的開發方式來組織你的程式碼了。
功能分割
以組合式 API 徵求意見稿中的Vue CLI UI 檔案瀏覽器為例,這個元件中有這樣幾個功能:
-
追蹤監聽當前資料夾的狀態並展示其中的內容
-
處理資料夾的操作(開啟、關閉、重新整理...)
-
處理新建資料夾的建立
-
是否只展示收藏資料夾
-
是否只展示隱藏資料夾
-
處理當前工作目錄的變化
就單個功能而言,它的程式碼所在位置比較分散(功能所需要的屬性在data
或props
中,處理資料的方法在methods
中),這勢必要求開發者在完成或閱讀程式碼時上下反覆跳轉,同一個功能的程式碼不夠聚合,那使用composition api
建立新資料夾
程式碼例子:
function useCreateFolder(openFolder) {
// 原來的資料 property
const showNewFolder = ref(false)
const newFolderName = ref('')
// 原來的計算屬性
const newFolderValid = computed(() => isValidMultiName(newFolderName.value))
// 原來的一個方法
async function createFolder() {
if (!newFolderValid.value) return
const result = await mutate({
mutation: FOLDER_CREATE,variables: {
name: newFolderName.value,},})
openFolder(result.data.folderCreate.path)
newFolderName.value = ''
showNewFolder.value = false
}
return {
showNewFolder,newFolderName,newFolderValid,createFolder,}
}
複製程式碼
使用composition api
有幾個亮點
- 整個函式就是一個功能
- 函式包含
建立新資料夾
所依賴的資料和邏輯 - 函式完全獨立,功能可以複用
再通過一張圖更直接的與options api
進行對比:
這裡相同的色塊代表相同的功能,composition api
(組合式api)讓開發者就像搭積木一樣將獨立的功能一層一層組合成一個元件,所有的邏輯和功能都一目瞭然,但基於options api
不能讓我們這樣組織程式碼,造成一個功能中邏輯的分散。
邏輯複用
當兩個或多個元件的邏輯相同或相似時,在vue2.x
中我們考慮用mixin
、HOC(高階元件,Vue較少用到)
、slot插槽
來做邏輯複用。但這幾種方式都有各自的弊端:
- 不知道程式碼引用來源
- 與引入的元件屬性或方法命名衝突
-
HOC
和slot
需要額外的有狀態的元件例項,從而使得效能有所損耗。
使用composition api
能做到更清晰的邏輯複用,繼續引用組合式 API 徵求意見稿中追蹤滑鼠位置
的例子:
import { ref,onMounted,onUnmounted } from 'vue'
export function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove',update)
})
onUnmounted(() => {
window.removeEventListener('mousemove',update)
})
return { x,y }
}
複製程式碼
在元件中引入函式
import { useMousePosition } from './mouse'
export default {
setup() {
const { x,y } = useMousePosition()
// 其他邏輯...
return { x,y }
},}
複製程式碼
這裡:
- 所有的資料來源都非常清晰
- 可以通過解構重新命名,不存在命名衝突
- 不再需要僅為邏輯複用而建立的元件例項
告別$set
Vue2.x
使用Object.defineProperty攔截資料實現響應式系統,到了Vue3.0
,響應式系統的核心api使用了Proxy。我們先看Vue2.x
中的一個例子
let vm = new Vue({
data() {
return {
name: 'jay'
}
}
})
vm.age = 20 // 並不會觸發vue的響應系統
複製程式碼
Vue2.x
對新增屬性是無感知的,依賴收集發生在初始化元件的過程中,Vue
不能響應後來新增的屬性,這是Object.defineProperty
天生的特性:
const object1 = {};
// 只能對物件上特定屬性做資料攔截
Object.defineProperty(object1,'property1',{
get() {
return object1['property1'];
}
set(value) {
object1['property1'] = value;
}
});
複製程式碼
Vue2.x
為了讓新增的資料具有響應性,加入$set api
來相容無法響應新增屬性的情況。對於Proxy api
,它天生能夠攔截所有的屬性。
const object1 = {};
let p = new Proxy(object1,{
get(target,prop,receiver) {
},set(target,value) {
console.log(target); // {}
console.log(prop); // property1
console.log(value); // 1
target[prop] = value;
}
})
// 能攔截到新增的屬性
p.property1 = 1;
複製程式碼
Object.defineProperty
與Proxy
的區別:
-
Object.defineProperty
針對物件上特定屬性
(不能攔截新增屬性),Proxy
針對handler
物件(不論你是否為新增屬性) -
Proxy
除了get
、set
外還有其他多種操作符 -
Proxy
不相容IE 11
,相當於IE
家族不能使用Vue3.0
的應用了(也許未來Vue或社群有優雅降級的方案)
有了Proxy
,就不用考慮新增屬性的響應行為了,是時候要跟$set
說聲再見了?。
VDom的效能優化
尤大在此前直播中表示,Vue3.0
在效能優化後VDom
的更新(計算diff)效能提升1.3-2
倍,SSR
的速度提升了2-3
倍。來看看Vue3
在VDom
優化實現。
靜態標記
<div id="app">
<h1>我來組成靜態節點</h1>
<p>{{name}}</p>
</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",{ id: "app" },[
_createVNode("h1",null,"我來組成靜態節點"),_createVNode("p",_toDisplayString(_ctx.name),1 /* TEXT */)
]))
}
// Check the console for the AST
複製程式碼
這裡h1
是靜態節點,p
為動態節點,在Vue2.x
中若name
發生變化,整個模板都需要重新渲染一遍,Vue3.0
在p標籤vdom
多傳了引數1 /* TEXT */
,意味著在diff
時略過靜態節點,只追蹤有這引數的動態節點的更新,Vue3.0
原始碼中還有其他型別的標記位:
export const enum PatchFlags {
TEXT = 1,// 表示具有動態textContent的元素
CLASS = 1 << 1,// 表示有動態Class的元素
STYLE = 1 << 2,// 表示動態樣式(靜態如style="color: red",也會提升至動態)
PROPS = 1 << 3,// 表示具有非類/樣式動態道具的元素。
FULL_PROPS = 1 << 4,// 表示帶有動態鍵的道具的元素,與上面三種相斥
HYDRATE_EVENTS = 1 << 5,// 表示帶有事件監聽器的元素
STABLE_FRAGMENT = 1 << 6,// 表示其子順序不變的片段(沒懂)。
KEYED_FRAGMENT = 1 << 7,// 表示帶有鍵控或部分鍵控子元素的片段。
UNKEYED_FRAGMENT = 1 << 8,// 表示帶有無key繫結的片段
NEED_PATCH = 1 << 9,// 表示只需要非屬性補丁的元素,例如ref或hooks
DYNAMIC_SLOTS = 1 << 10,// 表示具有動態插槽的元素
}
複製程式碼
靜態節點提升
<div id="app">
<h1>我來組成靜態節點</h1>
<p>{{name}}</p>
</div>
複製程式碼
編譯結果:
import { createVNode as _createVNode,createBlock as _createBlock } from "vue"
const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createVNode("h1","我來組成靜態節點",-1 /* HOISTED */)
export function render(_ctx,_hoisted_1,[
_hoisted_2,1 /* TEXT */)
]))
}
// Check the console for the AST
複製程式碼
使用靜態提升時,所有靜態節點被提升到render
方法外,這表明這些節點只會在初始化中建立一次,在更新時進行復用。
新增事件快取
<div id="app">
<p @click="onClick">hello</p>
</div>
複製程式碼
編譯結果:
import { createVNode as _createVNode,[
_createVNode("p",{
onClick: _cache[1] || (_cache[1] = ($event,...args) => (_ctx.onClick($event,...args)))
},"hello")
]))
}
// Check the console for the AST
複製程式碼
編譯後會產生一個行內函數,使用行內函數對繫結事件做快取,更新時事件處理器沒有發生變化,則這個節點被認為一個靜態節點。
Typescript
Vue2.x
是基於options
(選項)的框架,也就是用object
去編寫元件,但typescript
是型別檢查能力範圍限於class/function
,因此Vue2.x
中我們使用Vue + typescript + vue-class-component + vue-property-decorator
的方式開發Vue
應用。但typescript
在Vue3.0
原始碼中佔比90%
以上,Vue3.0
將對tsx
、class component
等有更好的支援。
Tree Shaking
Vue3.0
做到了按需引入,更好支援tree shaking
,有時候並不需要Vue
全部的功能,打包時可以將無用的程式碼剪掉
Fragment
Vue3.0
支援模板新增多個根節點,意味著render
函式也可以返回陣列了
結束
好了,Vue3.0
的新特性還是很多的,當然該學的還是得學,別做Vue2.x釘子戶
了?