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

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

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

爲了你們的閱讀方便,以及控制文章篇幅,我會把閱讀源碼時不太須要在乎的邏輯進行摺疊,或者經過註釋 /* 忽略邏輯 */ 這樣的標識進行忽略處理。瀏覽器

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

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

flow1.png

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

compileToFunction.png

直接跳過全部代碼,看文件的末尾 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 的運行流程:

compileFlow.png

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

compile.png

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.png

先從函數聲明中來看,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 這些函數都是一個單獨的小模塊,內部的實現都很是精妙,在編譯器的後續文章中,會逐個進行介紹。

本文經過從入口文件開始,對編譯的大致流程進行解釋,但願能夠幫助你們在閱讀編譯器這個模塊的代碼時能有一個清晰的流程概念,配合流程圖食用更香喲。

相關文章
相關標籤/搜索