在兩場vue conf後談談vue3.0要更新的內容相關筆記

從去年就一直說的沸沸揚揚的vue3.0發佈,雖然到現在還沒有正式發佈的事件,但做爲前端開發,我們還是要去了解的,那麼vue3.0會更新什麼內容呢,早在2018年11月尤大就在遠程演講中說到了,vue3.0會帶來

  • 易於維護
  • 更好的多端渲染支持
  • 新功能(新的API)

下面就這些內容來說一下vue3.0的更新,這些內容基本上都是我在看了2018年和2019年的中國Vue conf視頻後整理出來的,如果哪裏寫錯了,請路過大佬幫忙指出,十分感謝
後續如果有其他的更新我也會回來更新這篇博客,那麼開始今天的博客分享之旅

一、 更快

1. 基於模板編譯和Vitrual DOM性能方面的優化

使用typescript重寫Virtual DOM

我們知道,vue是使用Vitrual DOM內部渲染機制,在vue3.0中,Vitrual DOM進行了一次完全的重構,結合了模板編譯等小技巧來提高性能,使得初始渲染的提速最高可以達到翻倍。

在之前的Vitrual DOM渲染機制中,即使是靜態數據的內容節點,渲染機制仍然會重新生成這些節點,然而這實際上是不必要的,在vue2.x中,已經對這部分內容做了一定的優化,通過對模板的分析,對一些靜態數據的內容不再重複生成,但還不夠徹底,而在vue3.0中,對這些做了更多的優化

在vue2.0中,對一個標籤,無論是組件還是原生的標籤,都會先以字符串的形式,傳入到h這個函數中,h是用來生成虛擬node的函數,而判斷一個標籤是否爲組件,實際上是在運行時判斷的,這樣就會導致在編譯時會做重複的工作,而實際上,這個工作可以在編譯的時候完成,如果我們知道它是一個原生的元素,那就生成相應的原生元素對應的Vitrual DOM的代碼,同樣的,如果判斷爲一個組件時,就生成組件的代碼

(1)一些底層的優化

此外,還有一些比較底層的優化,像比如

  • 在調用函數處理時,函數的參數儘可能一樣多,這樣有益於js引擎去做一些性能優化
  • 在模板中直接靜態地分析一個元素所包含的子元素的類型,給運行時留下一些提示,這樣可能會減少一些判斷,舉個例子,像下面的結構
<div>
    <span></span>
</div>

div中只有一個span標籤,在生成虛擬node的時候,標明其只有一個子元素,那麼算法裏面可以直接跳入只有一個元素的分支,這樣就可以跳過很多的判斷

(2)優化slots生成

<Comp>
    <div>{{ hello }}</div>
</Comp>

在之前的vue2.0中,我們像上面這樣給一個組件傳入一個slot值,在這個值更新的時候,父子組件都會更新,即使在父組件其他位置沒有用到這個值,而實際上我們只是要更新這一個值而已

在vue3.0的新編譯機制中,把所有的slot統一編譯一個函數,當我們要將一個slot傳給子組件的時候,將這個函數傳給子組件,而調用這個函數,是子組件的行爲,所以這個“hello”的依賴,就成爲了子組件的而不是父組件的,所以當hello變動時,我們只需要重新渲染子組件,而不必再去渲染父組件了

(3)靜態內容提取

在vue2.0裏面已經有做過了一些處理,如果檢測到一部分模板不變,可以先提取出來,在之後的更新中,這一部分模板不僅可以直接使用原來的Vitrual DOM,甚至連整個比對過程都能直接跳過這整個樹

vue2.0沒做到的是,只要一個元素裏面任意深度包含着一個動態數據,那就無法將其靜態化,但還是可以做一些優化的,像下面這個元素,雖然其內含有動態數據text,但是這個div的屬性是靜態的

<div id="foo" class="bar">
    {{ text }}
</div>

所以我們在比對時,就不需要再去比對這個元素了,可以跳過它,直接去比對它的children即可

(4)內聯事件函數提取

<Comp @event="count++">

像上面的這個函數,在我們進行一個重渲染時,會重新去生成一個這樣的函數,而因爲新生成的函數和原來的函數肯定是不一樣的,雖然它們做的事情是一樣的,但JavaScript無法區分,所以爲了安全起見,每次都會觸發組件的重渲染

通過一定程度的優化,每次生成這個函數時都會進行一個cache(緩存),每次都重用一個函數,就可以起到避免子組件無謂更新的一個效果

(5)分區塊對動態節點進行追蹤

在過去的節點diff中,我們是針對一整個組件去做一個diff的,diff算法所消耗的時間,會受模板整體大小的影響,比如下面的內容

<template>
    <div id="content">
        <p class="text">Lorem ipsum</p>
        <p class="text">Lorem ipsum</p>
        <p class="text">{{ message }}</p>
        <p class="text">Lorem ipsum</p>
        <p class="text">Lorem ipsum</p>
    </div>
</template>

這樣的結構,在之前的vue中,會是下面這樣的diff

  • Diff <div>
    • Diff props of <div>
    • Diff children of <div>
      • Diff <p>
        • Diff props of <p>
        • Diff children of <p>
      • Repeat n times…

而實際上,這裏只有個message會發生改變,其他內容都是靜態的,但是這裏會因爲這一個message發生改變,導致整個模板進行一次diff,這樣就做了很多不必要的消耗了

所以在vue3.0中,將vdom更行性能由與模板整體大小相關提升爲與動態內容的數量相關,做到以下幾點

  • 將模板基於動態節點指令切割爲嵌套的區塊
  • 每個區塊內部的節點結構是固定的
  • 每個區塊只需要以一個Array追蹤自身包含的動態節點
    這樣子,當一個數據發生改變時,我們只需要去diff它所在的那個最小區塊即可,所以上面的代碼結構在vue3.0之後,diff會是
  • Diff <p> textContent

這個區塊的大小,甚至可以小到class的名字,因爲實際上爲了改變樣式,更改className是非常常用的,所以如果className是動態內容,那麼會將其劃分爲一個單獨的區塊,在進行diff算法時直接設置一個className即可,這樣又對性能做到了很高的優化

這種優化甚至快了6倍(在2019Vue conf上做的一個測試做到了36ms->6ms)

2. 基於proxy的新數據監聽系統

vue2使用的是Object.defineProperty的set/get

  • 對象屬性增添/刪除
  • 數組index/length更改
  • Map,Set,WeakMap,WeakSet
  • Classes

事實上基於proy的監聽,是所謂的lazy by default,只有當一個數據被使用到的時候,我們纔會真正地去監聽這個數據,所以對於一個大規模數據的監聽,在之前使用Object.defineProperty的監聽中,我們對一個數據監聽,不管這個數據是否使用到,都會對其進行監聽

利用proxy減少組件實例初始化開銷

每個vue組件都會代理它所包含的所有data,computed以及props,而這些代理都是通過Object.defineProperty實現的

在實際的實例化中,大量的Object.defineProperty實際上是一個昂貴的操作,而在vue3.0中,暴露給用戶的this,實際上是真正組件實例的一個Proxy,所以我們實際上是在一個Proxy上獲取數據,而在這個Proxy上獲取數據時,我們再做一些內部的判斷,這樣就徹底避免了Object.defineProperty的使用

二、更小

  1. 讓代碼結構和tree-shaking配合起來
  • 內置組件(keep-alive,transition…)
  • 指令的運行時helper(v-model,v-for…)
  • 各種工具函數(asyncComponent,mixins…)

之前Vue的整個代碼都是直接將整個Vue對象放進來,所以一些沒用到的東西,也無法通過tree-shaking將其扔掉,所以這樣就會造成即使Vue裏面的一些功能我們沒有用到,我們還是會將其放入到項目中,這就造成了不必要的內存消耗(即使這部分內容不大)

而在Vue3.0中,採取了ES module imports按需引入的方式。通過這種做法,如果我們沒有用到一些內置組件時,在編譯時就不會去加載這些內容。所以如果我們只用到所有Vue應用都會用到的一些核心功能,那我們就只會加載相應的這些核心功能相關的代碼,而這一部分代碼在所有無關的東西都使用tree-shaking處理後,只有大概在10kb左右

三、更易於維護

vue3.0從Flow遷移到TypeScript,使用TypeScript重寫了

內部模塊解耦

降低源碼閱讀的難度

編譯器重構

  • 插件化設計
  • 帶位置信息的parser(source maps)
  • 爲更好的IDE工具鏈鋪路

四、更好的多端渲染支持

vue2中需要自己去fork源碼

vue3.0引入一個Custom Render API

五、新功能(新API)

響應式數據監聽API

之前沒有顯式地將Vue的響應式功能暴露出來,而在vue3.0中可以通過import引入兩個函數使用

import { observable , effect } from 'vue'

const state = observable({
    count: 0
})

effect(()=>{
    console.log(`count is : ${state.count}`)
}) // count is : 0

state.count++ // count is : 1

上面引入了observable和effect,observable可以用來創建一個顯式的響應式的對象,在effect中依賴的數據,就會被註冊依賴,在之後當這個對象被改動到的時候,effect就會重新執行一遍

暴露這個API的主要目的,是爲了輕鬆地實現跨組件的狀態共享

輕鬆排查組件更新的觸發原因

提供了renderTriggered API,每一次這個組件觸發了更新的時候,可以在這個API中放入一個debugger,如下代碼

const Comp = {
    render(props){
        return h('div',props.count)
    },
    renderTriggered(event){
        debugger
    }
}

我們就可以在瀏覽器中直接地看到究竟是哪一行觸發這個更新,同時event還會提供更具體的更新,包括了觸發更新的對象,更新前的值和更新後的值,以及這個對象中被改動的屬性,觸發更新的操作是什麼(比如set)

更好的typescript支持

使用了原生的classAPI,不再需要去依賴Vue Class Component這個庫了(現在已經被撤銷了)

採用了Function-based API

const App = {
    setup(){
        const count = value(0)
        const plusOne = computed(() => count.value+1)
        const increment = () => { count.value++ }
        watch(() => console.log('mounted!))
        // 暴露給模板或渲染函數
        return { count }
    }
}

對比於Class API

  • 更好的TypeScript類型推導支持
    TypeScript對函數的參數和返回的內容類型支持是很好的,使用這個API,js和ts寫出來是一模一樣的,而且會給出類型補全
  • 更靈活的邏輯複用能力
    使用這個API可以做到
    • 沒有命名空間衝突
      我們將邏輯寫在一個函數中,返回時可以使用解構賦值的方式獲取裏面的值,這樣也能重寫變量名,不會造成命名空間衝突
    • 數據來源清晰
      數據可以清晰地從這個函數中獲得
    • 沒有額外的組件性能消耗
      相對於之前版本需要使用Mixins,高階組件,slot這些方法,使用這個API只需要去寫一個函數即可
  • Tree-shaking友好
    這裏面的方法都是使用import引入的,即按需引入,如果我們不引入的話,就會被tree-shaking丟掉
  • 代碼更容易被壓縮
    在代碼壓縮的時候,一個函數內部的一些變量或者函數名會被壓縮成一個字母,但是出於安全考慮,對象的屬性是不會被壓縮的,所以在代碼壓縮的時候就更容易了

對比React Hooks

  • 同樣的邏輯組合、複用能力
  • 只調用一次
    • 符合js直覺
    • 沒有閉包變量問題
    • 沒有內存/GC壓力
    • 不存在內聯回調導致子組件永遠更新的問題

支持TSX

更好的警告信息

  • 組件堆棧包含函數式組件
  • 可以直接在警告信息中查看組件的props
  • 在更多的警告中提供組件堆棧信息

(Experimental)Time Slicing Support

開啓這個API會將JavaScript的操作切成一部分一部分,每執行一段時間,就會將主線程還給瀏覽器,讓瀏覽器得以去監聽用戶事件並做出相應

在2019年的中國vue conf中,尤大提到了如果做的夠快,可以緩解對時間分片的需求,所以這個功能最後是否會在vue3.0裏出現,現在我也不清楚

關於IE的降級

在IE11中自動降級爲舊的getter/setter機制,並對一些不支持的語法給予警告

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章