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類型支持友好是兩件完全不同的事