性能利器,經過Vue3深度解析webpack熱更新原理

原文發佈於BestVue3社區vue

最近在解決 Vue3 的 JSX 不支持熱更新的問題,因此較爲深度地研究了 Webpack 的熱更新的原理,以及應該如何實現 Vue3 的組件熱更新, 本文就來深度分析一下關於 Webpack 熱更新的原理和實現。須要注意的是熱更新不是 Webpack 的專利,其餘的打包工具也是有的,而且會有一些區別。 本文主要關注 Webpack。react

我已經給 Vue3 的 babel-jsx 插件提了PR,有興趣能夠看一下我是如何實現的。webpack

參考資料:git

Webpack 的熱更新

具體的 webpack 熱更新流程以下:github

  • 應用讓 HRM 運行時來檢查是否具備熱更新
  • 運行時同步下載代碼而且通知應用
  • 應用告訴運行時須要執行更新
  • 運行時同步地執行更新

咱們在啓動 webpack-dev-server 以後,應該都有看到過在瀏覽器命令行或者 network 區域有一些websocket相關的內容, 曾經我不止一次奇怪爲何我好像沒有加入任何 websocket 相關代碼,哪來的這些提醒呢? 這就是由於 webpack 的熱更新的須要。web

在咱們修改了項目代碼以後,webpack 會監聽到文件內容的變化,而且從新進行編譯等工做,而後會把新的代碼經過 websocket 發送給瀏覽器。 瀏覽器獲取到新的代碼以後會從新執行模塊代碼,而且替換模塊的內容。須要注意咱們本文不討論 webpack 如何替換模塊內容瀏覽器

咱們須要在 webpack 的配置中開啓熱更新,纔會讓 webpack 可以執行以上的操做,如何開啓:微信

{
    devServer: {
        hot: true
    }
}
複製代碼

在開啓了熱更新以後,咱們代碼中的module上會有熱更新相關的屬性,最多見的就是這樣的代碼:babel

if (module.hot) {
    // 關於如何處理這個模塊的代碼
    module.hot.accept()
}
複製代碼

大部分熱更新的插件都會經過這種方式來判斷當前是否開啓了熱更新。websocket

假如咱們有以下的 Vue3 組件文件代碼:

export const Comp1 = defineComponent({
    setup() {
        return () => <div>Hello Comp1</div>
    },
})

export const Comp2 = defineComponent({
    setup() {
        return () => <div>Hello World</div>
    },
})
複製代碼

咱們通常會使用一些插件來添加熱更新的代碼,好比咱們這裏會用 Vue3 的 babel-jsx 插件,這個插件在編譯代碼的時候會往這個文件增長相似以下代碼:

// ... 組件代碼

if (module.hot) {
    module.hot.accept()
}
複製代碼

咱們這裏執行module.hot.accept()來通知 webpack 的 HMR 咱們接收了這個模塊,HMR 並不須要再從新執行模塊的替換。 若是咱們執行了這句代碼,咱們就須要自行替換這個文件的模塊代碼,來達到運行的應用更新模塊的目的。 若是在這裏你沒有去執行一些其餘更新模塊功能的代碼,那麼這個模塊並不會被更新。 而若是你不執行這句代碼,那麼這個模塊的全部代碼都會被更新。在這裏例子裏面,咱們若是把Comp2的內容改成Hello Vue3, 咱們若是不執行module.hot.accept()那麼Comp1Comp2都會被從新渲染。

這勉強可以達到熱更新的目的,可是追求精益求精的咱們,確定不知會知足於此。

Vue3 的 HMR

Vue3 專門實現了熱更新的功能,Vue3 在 window 上會掛載一個__VUE_HMR_RUNTIME__對象,來提供組件從新掛載渲染的功能。 其源碼在runtime-core/src/hmr.ts,你們有興趣能夠去看一下實現。

咱們能夠經過__VUE_HMR_RUNTIME__.reload來從新渲染掛載一個組件,經過__VUE_HMR_RUNTIME__.createRecord來記錄一個組件, 還有__VUE_HMR_RUNTIME__.rerender來從新渲染某個組件。

而後咱們來看看,咱們但願中的 HMR 的最終目的是啥呢?

  • 只有代碼更新了的組件纔會被從新渲染
  • 從新渲染的組件可以保持組件以前的狀態(state)
  • 若是當前文件並非只 export 組件,那麼須要徹底地更新全部模塊

只從新渲染更新的組件

第一條不少同窗可能不太好理解,尤爲是沒有用過 JSX 來進行開發的同窗,若是你以前都是用 SFC 來寫組件的,你可能認爲一個文件只能 export 一個組件。 可是若是咱們使用 JSX 來進行開發,就像上面的例子,一個文件 export 多個組件是很正常的。 在這種狀況下,若是咱們只改了一個組件的代碼,可是全部組件都從新渲染,這其實就變得沒有必要。

那麼咱們如何實現這個功能呢?在使用 babel 插件進行代碼編譯的時候,咱們給全部的組件計算一個 ID,而且根據組件的源碼計算其hash值,編譯以後的代碼大體以下:

Comp1.__id = 'comp1'
Comp1.__hash = 'xxxxx'
複製代碼

而後咱們聲明一個全局對象,來存儲組件的 Id 和 hash 的映射:

const $VueCompHashMap$ = (window.$VueCompHashMap$ =
    window.$VueCompHashMap$ || {})
複製代碼

接下去,在每次模塊更新以後,咱們會執行如下代碼來接收模塊的更新:

if (module.hot) {
    if ($VueCompHashMap$[Comp1.__id] !== Comp1.__hash) {
        __VUE_HMR_RUNTIME__.reload(Comp1.__id, Comp1)
    }
}
複製代碼

那麼只要組件的源碼不變,他的hash也不會改變,在模塊從新執行以後,組件也就不會被從新渲染。

保持組件的 state

Vue3 是提供了方法讓咱們可以保持組件的 state 的,前面提到的__VUE_HMR_RUNTIME__.rerender就是用來實現這個目的的。

可是並非全部狀況都可以實現保持狀態的,好比我我的更喜歡的開發方式就沒法實現:

export const Comp1 = defineComponent({
    setup() {
        const state = reactive({
            count: 1,
        })
        return () => <div>{state.count}</div>
    },
})
複製代碼

對於這樣一個組件,其render函數是來自於setup的返回值的,而其對於state的引用是來自於閉包,這裏的state並無掛載到任何的對象上。 若是Comp1更新了,咱們須要從新獲取其render函數,就須要從新執行setup,那麼閉包就會從新生成。因此目前沒有什麼好的辦法來保持這種類型組件的 state。

可是咱們能夠改個寫法:

export const Comp1 = defineComponent({
    setup() {
        const state = reactive({
            count: 1,
        })

        return {
            state,
        }
    },
    render() {
        return <div>{this.state.count}</div>
    },
})
複製代碼

對於這樣的組件,咱們能夠直接經過Comp1.render來獲取其render函數,而statesetup返回以後,會被 Vue3 掛載到組件的this上, 那麼對於這樣一個組件,咱們只須要獲取其render函數,而後經過__VUE_HMR_RUNTIME__.rerender來執行從新渲染,而保持this上的全部 state。

事實上,SFC 的狀態得以保持,就是由於 SFC 組件狀態是必然保持在this上。並且由於script部分是單獨分離的,對於 SFC 的狀態代碼是否有更新, 能夠直觀的根據script部分代碼是否有修改來判斷。而在 JSX 寫的組件上,則相對難以判斷setup中的代碼是否有更新。

我以前跟尤老師討論過這個問題,在尤老師關於他完成了 Vite 的 JSX 熱更新功能的 twitter 上。

跟尤老師的討論

他也提到了 Composition API 實現的 state 很難保持,而且他提到了 React hooks 的狀態在熱更新過程當中能夠被保持的緣由, 主要是由於 React hooks 的狀態其實在組件的實例上是有保存的,並且是根據 hooks 執行的順序和類型能夠判斷狀態代碼是否有改變。 我在React 源碼解析中寫過 hooks 可以保持狀態的緣由。

尤老師專門提了這一點,果真 Vue 和 React 的對比是永遠逃不過的話題。

須要更新整個模塊的代碼

有些狀況下即使只有某個組件更新了,咱們仍是須要讓整個模塊被更新。主要的狀況就是若是這個文件向外 export 了非組件代碼,咱們就須要更新整個模塊。

由於咱們 export 出去的代碼必然是會被其餘地方調用的,若是咱們執行了module.hot.accept(), 那麼 HMR 運行時並不會更新其餘引用了這個文件的模塊的代碼,這就會有問題了,其餘模塊在執行的時候可能會使用老的當前模塊的代碼。 咱們處理了組件的更新,由於 Vue3 提供了這個功能。

而其餘的 export 的內容就沒有這麼幸運,有框架來提供這些功能。這種狀況下,你能夠實現本身的熱更新邏輯來更新這些功能,固然這會比較麻煩。 那麼最簡單的方式,天然就是讓 HMR 運行時直接更新整個模塊,因此在這種狀況下咱們就不該該執行module.hot.accept()

咱們在 babel-jsx 插件中增長了以下代碼:

if (module.hot) {
    if ($isVueHMRAcceptable(module)) {
        module.hot.accept()
    }
}
複製代碼

$isVueHMRAcceptable這個函數就是來判斷當前模塊向外導出的是否都是組件的,只有在都是的狀況下才accept

總結

以上就簡單明瞭地向你們講解了 webpack 的 HMR 的原理。咱們藉由實現 Vue3 的熱更新的過程,來展現了一次熱更新中會經歷哪些過程,以及須要考慮哪些問題。

核心的模塊更新能力,其實 webpack 已經幫助咱們實現了,咱們更多的實際上是須要考慮咱們使用的框架該如何更小代價地執行更新。

React fast Refresh 也是去年才真正成爲官方的熱更新方案,以前的 React-Hot-Loader 一直存在一些問題,卻也一直活躍在 React 生態中。

此次對於 Vue3 熱更新的實現,也是更多參考了 React fast Refresh 的設計。可是很遺憾目前沒有找到辦法來保持組件狀態,指望將來能找到方法解決這個問題吧。

BestVue3社區專一於提供Vue3最新鮮最優質的學習內容,你能夠搜索微信公衆號 BestVue3 進行關注。

相關文章
相關標籤/搜索