尤雨溪:重頭來過的 Vue 3 帶來了什么?
重寫 Vue 新的主要版本的兩個關鍵的因素:
- 主流瀏覽器對新的 JavaScript 語言特性的普遍支持。
- 當前 Vue 代碼庫隨着時間的推移而暴露出來的設計和體系架構問題。
重寫帶來的技術性變化:
雙向數據綁定
vue2
通過 Object.defineProperty
將對象屬性轉化為 getter/setter
, 該屬性是 ES5 中無法被 shim 的特性,也是 vue 不支持 IE8 及以下版本瀏覽器的原因。
該方法存在的問題:
在 vue 中, Object.defineProperty
無法監控到數據的下標變化,導致直接通過數組下標給數組設置新值時,無法做到實時響應。目前 vue 只針對數組的變異方法 push/pop/shift/unshift/splice/sort/reverse
做了 hack 處理,存在響應局限。
vue3
Proxy
是 ES6
中新增的一個特性,翻譯過來意思是"代理",用在這里表示由它來“代理”某些操作。 Proxy
讓我們能夠以簡潔易懂的方式控制外部對對象的訪問。其功能非常類似於設計模式中的代理模式。
Proxy
可以理解成,在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。
使用 Proxy 的核心優點是可以交由它來處理一些非核心邏輯(如:讀取或設置對象的某些屬性前記錄日志;設置對象的某些屬性值前,需要驗證;某些屬性的訪問控制等)。 從而可以讓對象只需關注於核心邏輯,達到關注點分離,降低對象復雜度等目的。
通過 es2015 中的新特性 Proxy
使用,在框架中攔截針對對象(屬性)的操作.
const arr = [1, 2, 3]
const handler = {
get(target, property) {
console.log(`${property}被讀取了`)
return target[property]
},
set(target, property, value) {
console.log(`${property} 被設置為 ${value}`)
target[property] = value
}
}
let a = new Proxy(arr, handler)
a[2] // 2被讀取了
a[3] = 4 // 3 被設置為 4
arr // [1, 2, 3, 4]
對 ts 的支持
vue2 最初是使用純 ES(Javascript)
寫成的,而沒有引入類型檢查系統。類型檢查能有效減少重構過程中引入錯誤的機會,雖然后續采用了 Facebook
的 Flow type checker
, 但沒有明顯的改觀,相比較 TypeScript
與 Visual Studio Code
集成開發工具的深度集成,Flow type checker
對集成開發環境的支持也不理想。切換到 TypeScript
將允許我們自動生成聲明文件,從而減輕維護負擔。
虛擬 dom 渲染
Vue 有一個相當獨特的渲染策略:它提供類似於 HTML 的模板語法,但是,它是將模板編譯成渲染函數來返回虛擬 DOM 樹。Vue 框架通過遞歸遍歷兩個虛擬 DOM 樹,並比較每個節點上的每個屬性,來確定實際 DOM 的哪些部分需要更新。由於現代 JavaScript 引擎執行的高級優化,這種有點暴力的算法通常非常快速,但是 DOM 的更新仍然涉及許多不必要的 CPU 工作。當你看到一個基本上是靜態內容、只有少量動態綁定的模板時,效率低下的情況尤其明顯,因為這時候仍然需要遞歸地遍歷整個虛擬 DOM 樹,以找出需要更改的內容。
幸運的是,模板編譯步驟使我們有機會對模板執行靜態分析並提取有關動態部分的信息。Vue 2 在某種程度上是通過跳過靜態子樹來實現的,但是過於簡單的編譯器體系架構使得更高級的優化很難實現。在 Vue 3 中,我們使用適當的 AST 轉換管道重寫編譯器,這允許我們以轉換插件的形式將編譯時(compile-time)優化組合進來。
隨着新的體系架構的出現,我們希望找到一種能夠盡可能減少開銷的渲染策略。一種選擇是拋棄虛擬 DOM 並直接生成命令式 DOM 操作,但這樣做會消除直接編寫虛擬 DOM 渲染函數的能力,而我們發現這種能力對於高級用戶和庫的編寫者非常有價值。另外,這將是一個巨大的突破性改變。
另一個更好的辦法是去掉不必要的虛擬 DOM 樹遍歷和屬性比較,這在更新期間往往會產生最大的性能開銷。為了實現這一點,編譯器和運行時需要協同工作:編譯器分析模板並生成帶有優化提示的代碼,而運行時盡可能獲取提示並采用快速路徑。這里有三個主要的優化:
首先,在 DOM 樹級別。我們注意到,在沒有動態改變節點結構的模板指令(例如 v-if 和 v-for)的情況下,節點結構保持完全靜態。如果我們將一個模板分成由這些結構指令分隔的嵌套“塊”,則每個塊中的節點結構將再次完全靜態。當我們更新塊中的節點時,我們不再需要遞歸遍歷 DOM 樹 - 該塊內的動態綁定可以在一個平面數組中跟蹤。這種優化通過將需要執行的樹遍歷量減少一個數量級來規避虛擬 DOM 的大部分開銷。
其次,編譯器積極地檢測模板中的靜態節點、子樹甚至數據對象,並在生成的代碼中將它們提升到渲染函數之外。這樣可以避免在每次渲染時重新創建這些對象,從而大大提高內存使用率並減少垃圾回收的頻率。
第三,在元素級別。編譯器還根據需要執行的更新類型,為每個具有動態綁定的元素生成一個優化標志。例如,具有動態類綁定和許多靜態屬性的元素將收到一個標志,提示只需要進行類檢查。運行時將獲取這些提示並采用專用的快速路徑。
綜合起來,這些技術大大改進了我們的渲染更新基准,Vue 3 有時占用的 CPU 時間不到 Vue 2 的十分之一。
注:CPU 時間指的是執行 JavaScript 計算所花費的時間,不包括瀏覽器 DOM 操作。