Vue3原始碼學習-2 | 框架設計的核心要素

語言: CN / TW / HK

theme: condensed-night-purple highlight: atelier-estuary-light


image.png

Vue3原始碼學習 | 框架設計的核心要素

框架設計要比想象得複雜多,並不是說只把功能開發完成,能用就算大功告成了,這裡面還有很多學問。我們針對框架設計丟擲幾個問題: - 當用戶沒有以預期的方式使用框架時,是否應該打印合適的警告資訊從而提供更好的開發體驗? - 開發版本的構建和生產版本的構建有何區別? - 當框架提供了多個功能,而使用者只需要其中幾個功能時,使用者是否選擇關閉其他功能從而減少最終資源的打包體積?

(建議:閱讀本節內容,大家最好對常用的模組打包工具有一定的經驗,eg:rollup.js / webpack,咳咳,只是建議)

2.1 提升使用者的開發體驗

衡量一個框架是否足夠優秀的指標之一就是看它的開發體驗如何

例項說明

js // Vue3.js createApp(App).mount('#not-exist') 當我們建立一個Vue.js應用並試圖將其掛載到一個不存在的DOM節點時,就會收到一條警告資訊

[Vue warn]:Faild to mount app:mount target selector "#not-exist" returned null.

這條資訊告訴我們掛載失敗了,並說明了失敗的原因:Vue.js根據我們提供的選擇器無法找到相應的DOM元素(返回null),這條資訊讓我們能夠清晰且快速地定位問題。

所以在框架的設計和開發中,提供友好的警告資訊不僅能夠幫助使用者快速定位問題,節省使用者時間,還能夠讓框架收穫良好的口碑,讓使用者認可框架的專業性

Vue3原始碼

在Vue.js原始碼中,經常能夠看到warn函式的呼叫,上述的警告資訊就是由下面這個warn函式呼叫列印的:

js warn( `Faild to mount app:mount target selector "${container}" return null.` ) 對於 warn 函式來說,由於它選喲儘可能提供有用的資訊,因此它需要收集當前發生錯誤的元件棧資訊。如果你去看原始碼,就會發現有些複雜,但其實始終就是呼叫了 console.warn 函式

除了必要的警告資訊外,還有很多其他方面可以作為切入口,進一步提升使用者的開發體驗

2.2 控制框架程式碼的體積

框架的大小也是衡量框架的標準之一

例項說明

如果讓我們去看 Vue3 的原始碼,就會發現每一個 warn 函式的呼叫會配合 __DEV__ 常量的檢查,例如:

js if(__DEV__ && !res) { warn( `Faild to mount app:mount target selector "${container}" return null.` ) } 可以看出,列印警告資訊的前提是:__DEV__這個常量一定要為true,這裡的__DEV__常量就是達到目的的關鍵。 這時候你就會問:

  • 明明這個直接if(!res)就可以直接判斷是否存在錯誤,__DEV__的存在是不是有點多餘,沒錯!我剛開始也是這麼想的,但是!存在即合理!接著看!

這裡先說明__DEV__怎麼來的:Vue.js使用 rollup.js 對專案進行構建,這裡的__DEV__常量實際上是通過rollup.js的外掛配置預定義的,其功能類似於 webpack 中的 DefinePlugin 外掛

開發環境 與 生產環境

可能有的讀者看到這裡會對開發環境生產環境產生疑惑:這兩個環境到底是個什麼玩意兒? - 開發環境:是程式猿們專門用於開發的伺服器,配置可以比較隨意, 為了開發除錯方便,一般開啟全部錯誤報告。(程式設計師接到需求後,開始寫程式碼,開發,執行程式,看看程式有沒有達到預期的功能) - 生產環境:指正式提供對外服務的,一般會關掉錯誤報告,開啟錯誤日誌。(就是線上環境,釋出到對外環境上,正式提供給客戶使用的環境)

Vue.js 在輸出資源的時候,會輸出兩個版本,其中一個用於開發環境,如vue.global.js,另一個用於生產環境,如vue.global.prod.js,通過檔名我們也能夠區分。

程式碼在不同環境的執行

  • 當Vue.js構建使用者開發環境資源時,會把__DEV__常量設定為true,這時上面那段輸出警告的程式碼就等價於: js if(true && !res) { warn( `Faild to mount app:mount target selector "${container}" return null.` ) } 可以看到,這裡是將__DEV__常量替換成字面量true,所以這段程式碼在開發環境中是肯定存在的。

  • 當Vue.js用於構建生產環境的資源時,會把__DEV__常量設定為false,這時上面那段輸出警告資訊的程式碼就等價於: js if(false && !res) { warn( `Faild to mount app:mount target selector "${container}" return null.` ) } 很明顯!這是我們發現這段分支程式碼永遠都不會執行!

這段永遠不會執行的程式碼稱為dead code,它不會出現在最終產物中,在構建資源的時候就會被移除,因此在vue.global.prod.js 中是不會存在這段程式碼的。

這樣我們就做到了在開發環境中為使用者提供友好警告資訊的同時,不會增加生產環境程式碼的體積。

2.3 Tree-Shaking

為什麼要說這個可能陌生的東西?沒錯,就是因為上面的東西做的不夠好,或者說!這才是真正的主角

吹歸吹,咳咳,那什麼是Tree-Shaking呢?在前端領域,這個概念因 rollup.js 普及。 - 簡單來說,Tree-Shaking指的就是消除那些永遠不會被執行的程式碼,也就是消除dead code,現在無論是rollup.js 還是webpack,都支援 Tree-Shaking。

例項說明

(特殊說明:以下例項以 rollup.js 進行說明Tree-Shaking如何工作)

目錄結構

md |—— demo | └─ package.json | └─ input.js | └─ utils.js 首先安裝 rollup.js ```js yarn add rollup -D

或者 npm install rollup -D

下面是input.js 和 utils.js 檔案內容js // input.js import { foo } from './utils.js' foo() // utils.js export function foo(obj) { obj && obj.foo } export function bar(obj) { obj && obj.bar } `` 在utils.js檔案中定義併到處兩個函式,分別是foo函式和bar函式,然後在input.js中匯入foo函式並執行。注意:我們並沒有匯入bar函式`

接著,我們執行如下命令進行構建 js npx rollup input.js -f esm -o bundle.js 這句命令的意思,以input.js檔案為入口,輸出ESM,輸出的檔案叫做bundle.js。

ESM:實現Tree-Shaking,必須滿足一個條件,即模組必須是ESM(ES Module),因為Three-shaking依賴的靜態結構(至於不知道靜態結構是啥的小夥伴,自己去查!略略略!) 開啟 bundle.js 來檢視內容 js // bundle.js function foo(obj) { obj && obj.foo } 可以看出!其中並不包含bar函式,這說明什麼!說明!Tree-Shaking起了作用!由於我們並沒有使用bar函式,因此它作為dead code被刪除了。

但是咱們仔細看看上面的程式碼會發現,foo函式的執行也沒有什麼意義,僅僅是讀取了物件的值,所以它的執行獅虎沒什麼必要。那麼問題來了: - 既然把這段程式碼刪了不會對我們的應用程式產生影響,那麼為什麼 rollup.js 不把這段程式碼也作為 dead code 移除呢?

接著往下看 副作用

Tree-Shaking 中的 副作用

這就涉及Tree-shaking中的第二個關鍵點——副作用。如果一個函式呼叫會產生副作用,那麼就不能將其移除。 - 那什麼是 副作用 呢?

簡單來說,副作用就是,當呼叫函式的時候會對外部產生影響。eg:修改了全域性變數

  • 這時你可能會說,上面的程式碼明顯是讀取物件的值,怎麼會產生副作用呢?

其實是有可能的,試想一下,如果obj物件是一個通過Proxy建立的代理物件,那麼當我們讀取物件屬性時,就會觸發代理物件的get夾子(trap),在get夾子中是可能產生副作用的,例如我們在get夾子中修改了某個全域性變數。

而到底會不會產生副作用,只有程式碼真正執行的時候才能知道,JavaScript 本身是動態語言,因此想要靜態地分析哪些程式碼是dead code很有難度,上面只是一個例子!

  • 那麼,問題又來了!我們該如何辨別哪些程式碼是 dead code 呢?

像 rollup.js 這類工具都會提供一個機制,讓我們能明確地告訴 rollup.js:“放心吧,這段程式碼不會產生副作用,你可以移除它。” 具體怎麼做呢?那就是 打標識

例項說明

修改 input.js 檔案 ```js import { foo } from './utils'

/PURE/ foo() `` 想必你也發現了,註釋程式碼/PURE/`,其作用就是告訴 rollup.js,對於 foo 函式地呼叫不會產生副作用,你可以放心地對其進行 Tree-shaking。

基於這個案例,我們應該明白,在編寫框架的時候需要合理使用/*__PURE__*/註釋。如果你去搜索Vue3的原始碼,會發現它大量使用了該註釋。例如: js export const isHTMLTag = /*__PURE*/ makeMap(HTML_TAGS) - 有人又會開始問,這樣子做會不會太麻煩了?等會滿螢幕的程式碼都是 /*__PURE__*/?

其實不會,因為通常產生副作用的程式碼都是模組內函式的頂級呼叫

  • 好!問題又來了,頂級呼叫又是什麼玩意兒

直接上程式碼! ```js foo() // 頂級呼叫

function bar() { foo() // 函式內呼叫 } ``` 可以看到,對於頂級呼叫來看,是可能產生副作用的;但是對於函式內呼叫來說,只要函式bar沒有被呼叫,那麼foo函式的呼叫自然不會產生副作用。

在Vue3原始碼中,基本都是在一些頂級呼叫的函式上使用/PURE/註釋,當然,該註釋不僅僅作用於函式,它可以應用於任何語句上。該註釋也不是隻有rollup.js才能識別,webpack以及壓縮工具(如terser)都能識別它

2.4 效能開關

在設計框架時,框架會給使用者提供諸多效能(或功能),例如我們提供A、B、C三個特性給使用者,同時還提供了a、b、c三個對應的特性開關,使用者可以通過設定a、b、c為true/fasle來代表開啟或關閉對應的特性,這將會帶來很多益處。 - 對於使用者關閉的特性,我們可以利用Tree-Shaking機制讓其不包含在最終的資源中。 - 該機制為框架設計帶來了靈活性,可以通過特特性開關任意為框架新增新特性,而不用擔心資源體積過大。同時,當框架升級時,我們也可以通過特性開關來支援遺留API,這樣新使用者可以選擇不使用遺留API,從而使最終打包的資源最小化。

實現特性開關

其實很簡單,原理和上文提到的__DEV__常量一樣,本質上利用 rollup.js 的預定義量外掛來實現。

例項說明

拿Vue3原始碼中的一段rollup.js配置來說 js { _FEATURE_OPTIONS_API_:isBundlerESMBuild ? '_VUE_OPTIONS_API_':true, } 其中_FEATURE_OPTIONS_API_類似於__DEV__。在Vue3原始碼中可以找到很多類似的判斷分支:

特殊說明:看不懂這段程式碼沒關係,注意if的條件,至於這段程式碼幹嘛的下面會說

js // support for 2.x options if(_FEATURE_OPTIONS_API_){ currentInstance = instance pauseTracking() applyOptions(instance,Component) resetTracking() currentInstance = null } 當Vue構建資時,如果構建的資源時供打包工具使用(即帶有-bundler字樣的資源),那麼上面的程式碼在資源中會變成: js // support for 2.x options if(_VUE_OPTIONS_API_){ currentInstance = instance pauseTracking() applyOptions(instance,Component) resetTracking() currentInstance = null } 其中的_VUE_OPTIONS_API_是一個特性開關,使用者通過_VUE_OPTIONS_API_預定義常量的值來控制是否要包含這段程式碼。通常使用者可以使用webpack.DefinePlugin外掛來實現: js // webpack.DefinePlugin 外掛配置 new webpack.DefinePlugin({ _VUE_OPTIONS_API_:JSON.stringfy(true) // 開啟特性 })

_VUE_OPTIONS_API_的作用!
  • 在Vue2中,我們編寫的元件叫做元件選項API:

js export default { data(){}, // data選項 computed:{} // computed 選項 // 其他選項 } - 在Vue3中,推薦使用 Composition API 來編寫程式碼,例如: js export default { setup() { const count = ref(0) const doubleCount = computed(() => count.value * 2) } } 在Vue3中相容Vue2的選項API。但是如果明確知道自己不會使用選項API,使用者就可以使用_VUE_OPTIONS_API_開關來關閉該特性,這樣在打包的時候Vue的這部分程式碼就不會包含在最終的資源中,從而減小資源體積

2.5 總結

  • 提高友好的警告資訊至關重要,這有助於開發者快速定位問題
  • 開發環境生產環境,控制生產環境中不包含開發環境中的一些程式碼,從而實現線上程式碼體積的可控性
  • Tree-Shaking 是一種排除 dead code 的機制,與打包工具配合實現打包程式碼的體積最小
  • 按需引入 Tree-Shaking可以幫助實現

番外

淺提一嘴:使用TS編寫框架和框架對TS型別支援友好是兩件完全不同的事