Vue3源碼解析 —— 編譯器(一)入口

Vue3 發佈已經有一段時間了,最近也有機會在公司項目中用上了 Vue3 + TypeScript + Vite 的技術棧,所以閒暇之餘抽空也在抽空閱讀 Vue3 的源碼。本着好記性不如爛筆頭的想法,在閱讀源碼時順便記錄了一些筆記,也希望能爭取寫一些源碼閱讀筆記,幫助每個想看源碼但可能存在困難的同學減少理解成本。

Vue2.x 的源碼我也有過一些簡單的閱讀,自 Vue3 重構後,Vue 項目的目錄結構也發生了很大的變化,各個功能模塊被分別放入了 packages 目錄下,職責更加清晰,通過目錄名就可以一目瞭然。今天將從 Vue 的入口文件開始,看看聲明瞭一個 Vue 的單文件之後是如何被 compile-core 編譯核心模塊編譯成渲染函數的。

爲了大家的閱讀方便,以及控制文章篇幅,我會把閱讀源碼時不太需要在意的邏輯進行摺疊,或者通過註釋 /* 忽略邏輯 */ 這樣的標識進行忽略處理。

我個人是不太喜歡在看源碼分析文章時一上來就懟出一大段代碼,這容易讓沒閱讀的同學有點懵逼。所以這個系列的文章我會盡量對關鍵的代碼畫出一張流程圖。目的還是一個,幫助大家降低理解成本,同時也讓各位同學在下次自主閱讀時有張流程圖能參考。

我們會先從一個 vue 文件創建的入口來開始我們的源碼閱讀, packages/vue/index.ts 。這個入口文件的代碼比較簡單,只有一個 compileToFunction 函數,但函數體內的內容卻又比較關鍵,所以先看一張圖,來理解這個函數體究竟完成了哪些事情。

在看完流程圖之後,我們來對照代碼一起看,我相信大部分同學在此時可能對下發圖片中的代碼一目瞭然了。

直接跳過所有代碼,看文件的末尾 35 行,調用了 registerRuntiomCompiler 函數,將 compileToFunction 函數作爲參數傳入,這行代碼即對應流程圖的起始,通過依賴注入的方式,將 compile 函數注入至 runtime 運行時中,依賴注入是一種比較巧妙的解耦方式,此時運行時再調用 compile 編譯函數,就是在調用當前的 compileToFunction 函數了。

再看代碼中的第 17 行,調用了 compile-dom 庫提供的 compile 函數,從返回值中解構出了 code 變量。這個就是編譯器執行之後生成的編譯結果,code 是編譯結果的其中一個參數,是一個代碼字符串。比如

<template>
  <div>
    Hello World
  </div>
</template>

這個簡單的模板,在經過編譯後,code 返回的字符串爲

const _Vue = Vue return function render(_ctx, _cache) {  with (_ctx) {    const { openBlock: _openBlock, createBlock: _createBlock } = _Vue     return (_openBlock(), _createBlock("div", null, "Hello World"))  } }

這個神奇的 compile 函數內部的奧妙在之後我會詳細講解。

在拿到這個這個代碼字符串的結果後,我們再順着代碼往下看,第 25 行聲明瞭一個 render 變量,並且將生成的代碼字符串 code 作爲參數傳入了 new Function 構造函數。這就是流程圖中的倒數第二步,生成了 render 函數。可以將我放在上面的 code 字符串格式化,能夠發現 render 函數是一個柯里化的函數,返回了一個函數,函數內部通過 with 來擴展作用域鏈。

而最後入口文件返回了 render 變量,並且順手緩存了 render 函數。

上方源碼的第 1 行,我們看到入口文件創建了一個 compileCache 對象,用以緩存 compileToFunction 函數生成的 render 函數,將 template 參數作爲緩存的 key, 並在 11 行的位置有一個 if 分支做緩存的判斷,如果該模板之前被緩存過,則不再進行編譯,直接返回緩存中的 render 函數,以此提高性能。

至此 package/vue/index.ts 的入口文件就解讀完了。相信大家也都看出來了,最有意思的部分就是調用 compile 函數編譯出了代碼字符串,所以接下來我將圍繞 compile 函數來接着嘮。compile 函數牽扯到 compile-dom 和 compile-core 兩個模塊,本篇文章我只會解讀關鍵流程。細節分析的話會放在後續文章中。一起來看一下 compile 的運行流程:

compile 函數內部直接返回 baseCompile 函數的結果,而 baseCompile 函數在執行過程中會生成 AST 抽象語法樹,並調用 transform 對 每個 AST 節點進行處理,例如轉換vOn、v-if、v-for 等指令,最後將處理後的 AST 抽象語法樹通過 generate 函數生成之前提及的代碼字符串,並返回編譯結果,至此 compile 函數執行完畢。明白了大體的流程後,接着來看源碼。

compile 函數的源碼路徑是 packages/compiler-dom/src/index.ts, 我們看到在 compile 的函數體內,直接 return 了 baseCompile 的處理結果。而 baseCompile 的源碼路徑是 packages/compiler-core/src/compile.ts 。爲什麼會有 baseCompile 這樣的命名呢?因爲 compile-core 是編譯的核心模塊,接受外部的參數來按照規則完成編譯,而 compile-dom 是專門處理瀏覽器場景下的編譯,在這個模塊下導出的 compile 函數是入口文件真正接收的編譯函數。而 compile-dom 中的 compile 函數相對 baseCompile 也是更高階的一個編譯器。例如當 Vue 在 weex 在 iOS 或者 Android 這些 Native App 中工作時,compile-dom 可能會被相關的移動端編譯庫來取代。

順着往下一起看一下 baseCompile 函數:

先從函數聲明中來看,baseCompile 接收 template 模板以及上層高階編譯器中處理過 options 編譯選項,最終返回一個 CodegenResult 類型的編譯結果。

export interface CodegenResult {
  code: string
  preamble: string
  ast: RootNode
  map?: RawSourceMap
}

通過 CodegenResult 的接口聲明能清晰的看到返回結果中存在 code 代碼字符串、處理後的 AST 抽象語法樹,以及 sourceMap。

看上方源碼的第 12 行,判斷 template 模板是否爲字符串,如果是的話則會對字符串進行解析,否則直接將 template 作爲 AST 。其實我們平時在寫的單文件 vue 代碼,都是以字符串的形式傳遞進去的。

接下來源碼是 16 行調用了 transform 函數,以及傳入了指令轉換、節點轉換等工具函數,對由模板生成的 AST 進行轉換。

最終的 32 行位置,我們將轉換好的 AST 傳入 generate,生成 CodegenResult 類型的返回結果。

在 compile-core 模塊中,AST 解析、transform、codegen、compile、parse 這些函數都是一個單獨的小模塊,內部的實現都非常精妙,在編譯器的後續文章中,會逐個進行介紹。

本文通過從入口文件開始,對編譯的大體流程進行解釋,希望可以幫助大家在閱讀編譯器這個模塊的代碼時能有一個清晰的流程概念,配合流程圖食用更香喲。

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