Vue3 深度解析
- 2019 年 11 月 20 日
- 筆記
前言
距離尤雨溪首次公開 Vue3 (vue-next)源碼有一個多月了。青筆觀察到,剛發佈國慶期間,出現不少解讀 Vue3 源碼的文章。當然不少有追風蹭熱之嫌,文章草草講講響應式原理,或者只是做了一些上層的導讀,告訴讀者應該先看哪再看哪。不能說這些文章就沒有一點價值,它確實能夠讓你在短時間內,不用過多思考就能了解到一些 Vue3 重中之重的「乾貨」。但是過於乾貨的未必就是好的。因為乾貨通常是經過作者咀嚼過後的產物,大部分營養其實只被作者消化了。留給讀者的只是一些看似很有料,實則沒有營養的殘渣。就像一塊啃到只剩骨頭的排骨。這樣的文章通常適合於媒體傳播,僅用於快速捕獲眼球。但是對於想更細緻了解 Vue3 的專業前端開發,這顯然遠遠不夠。
事實上,這不是青筆第一篇關於 Vue3 的文章。在 Vue3 公布後的第五天,也就是10月10號。青筆沒有直接解讀源碼,而是從一個想要自己開發或參與 Vue3 項目的角度,講到了構建 Vue3 所用到的構建工具和相關技術。文章不僅僅是給出最後的「乾貨」,而是把青筆在實踐過程中的用到的方式方法,包括得到結果的每一行shell
命令,git 技巧等。讀者完全可以按照文章的脈絡得到和青筆一樣的結果。這樣做是青筆自身多年軟件開發經驗,所堅持的一個觀點,那就是「技術不是用來看的,而是用來實操的」。只有當你能親自實踐才能真正理解其中的內涵,並且這也是最簡單和行之有效的學習方式。那些讀起來晦澀難懂,繁雜抽象的概念術語,其實最怕的就是被實操,因為一旦遇到一個身懷實操大法的讀者,它的所有「江湖禁術」將被見招拆招,一一破解。
但是想要少走彎路,高效率地實踐,前提是有一批相關的文章。
本文依然堅持這樣的一個準則,帶你從實踐角度來探秘 Vue3 的源碼。你也可以理解為「授之以魚不如授之以漁」。
1. 準備工作
為了順利完成後面的實踐。請先確保你的電腦已經安裝了以下工具。
- git
- node 10 及以上版本 (LTS版)
- yarn
- lerna
- typescript
其中 lerna
和 typescript
使用 npm 進行全局安裝。安裝方式如下:
npm install -g lerna npm install -g typescript
2. 先人一步 體驗 Vue3 搭建下一代網頁應用
2.1 Composition API
事實上早在 Vue3 源碼公布之前,Vue 官方已經透露了代表下一代 Vue 技術的 Vue3 將採取的新的接口使用方式。這種新的方式叫做 Composition API
(組合式 API)。而與之相對應的經典 API 也是我們所熟知的 Vue 使用方式叫做 Options API
(選項式 API)或 Options-based API
(基於選項的 API)。
在經典的 Options API
中,我們使用一個具有 data
, methods
等「選項」的 JS 對象來定義一個頁面或者組件。這種簡單直接的方式,在應用早期階段,代碼和業務邏輯較簡單時,非常的友好親民,這也是 Vue 以學習門檻較低而廣受開發者親昵的的一個因素。但是,有過開發大型 Vue 應用的開發者應該心有體會。當頁面的邏輯越來越多時,我們的組件將變得冗長複雜。很多本可以復用的邏輯和代碼,你很難有一種使用起來非常舒適的方式來複用。親筆自身實踐,在 Vue2 中,組件邏輯和代碼復用最常用的方式是混入 mixin
,這雖然是一種可行的方式,但是這種方式顯然從出生和 Vue 框架緊密耦合。當你想要將一個框架無區別的普通 JS 函數或者對象復用到 Vue2 開發的組件中時,你發現一切都是那麼的不優雅。
基於滿足在開發大型 Vue 應用中更優雅地復用已有代碼的需求催生下,Vue3 Composition API
似乎是順勢而為,並且勢在必得。
vue-composition-api-rfc
2.2 第一個 Composition API 應用
據官方介紹,Vue3 正式發佈將在明年第一季度。但這並不影響我們提前使用 Composition API
開發一個簡單的響應式 WEB 應用。
並且作為解讀 Vue3 源碼的前戲,我們將直接在最新源碼上進行實操(你很快就會發現這樣做的好處)。
2.2.1 克隆源碼與初始化
為了精簡篇幅,這裡直接整個給出所有命令。想了解更多細節,推薦青筆另一篇專欄文章 《從零開始構建 vue3》 ,裏面有對相關細節的詳細講解。
# 克隆源碼 git clone https://github.com/vuejs/vue-next.git # 進入源碼目錄 cd vue-next # 安裝依賴 yarn # 初次構建 yarn build # 建立項目內部 packages 軟鏈 lerna bootstrap
這裡需要特別講到的是最後一步 lerna bootstrap
,這裡實際就是在項目根目錄的 node_modules
創建了一個符號鏈接(或軟鏈) vue
和一個 scope 目錄@vue
。
在 macOS 或其他 linux 發行版上可以通過如下命令查看鏈接指向。
ls -l node_modules/ | grep vue ls -l node_modules/@vue


可以看到 vue
和 @vue
下的符號鏈接分別指向了源碼目錄 packages/
下對應的目錄(文件夾)。
這樣,我們就可以在 Vue3 正式發佈到 npm 前,直接使用源碼里的各個 package ,等效於使用從 npm 安裝的其他依賴。並且,由於 Vue3 使用 Typescript 編寫,裏面已經安裝和提供編寫 Typescript 所有需要開發依賴和配置。因此,我們可以在源碼項目里使用和 Vue3 源碼一樣的方式書寫 Typescript 程序。不用擔心,即使還不熟悉 Typescript 也不影響繼續閱讀本文。
2.2.2 編寫第一個 Vue3 Composition API 網頁
為了不污染了 Vue3 源碼目錄結構。我們可以創建一個新的分支。
git checkout -b examples
在根目錄下創建 examples
目錄,用於存放示例代碼。
mkdir examples
新建文件 ./examples/composition.html
,添加如下內容:
<html> <head><title>vue3 - hello composition!</title></head> <body> <div id="app"><p>{{ state.text }}</p></div> <script src="../node_modules/vue/dist/vue.global.js"></script> <script> const { createApp, reactive, onMounted } = Vue const state = reactive({ text: 'hello world!' }) const App = { setup () { onMounted(() => { console.log('onMounted: hello world!') }) return { state } } } createApp().mount(App, '#app') </script> </body> </html>
使用 Chrome 瀏覽器打開這個 html 文件。在控制台可以訪問我們定義的全局變量 state
。可以任意修改 state.text
的值,你會看到網頁顯示的文本會隨着新的賦值而變化。

恭喜你!你已經成功使用 Vue3 Composition API
編寫了一個響應式 Web 應用。
可以看到不同於 Vue2 選項API醜陋的組件定義。Vue3 Composition API
提供一系列 Api 函數,通過簡單組合(這也是 Composition 的含義所在),就構建了一個 Vue3 響應式 Web 應用,一切看起來那麼自然舒服。可以預見,隨着函數式編程的日趨流行,Vue3 Composition API
勢必成為構建下一代 Vue 應用的首選和主流方式。
3. 源碼探秘
看過青筆專欄《從零開始構建 vue3》的讀者應該知道,Vue3 源碼分為幾個不同的 package
,存放在目錄 ./packages/
下,並使用 lerna
來管理多 package
項目。
packages/ ├── compiler-core ├── compiler-dom ├── compiler-sfc ├── reactivity ├── runtime-core ├── runtime-dom ├── server-renderer ├── shared └── vue
其中 compiler-sfc
是 Vue 單文件組件(也就是我們在 Webpack 下使用的 .vue 文件)的實現,server-renderer
是服務端渲染的源碼,這兩個部分截止本文寫作時,還未完成;shared
是各個 package
共享的實用庫(相當於我們平時使用的 utils),裏面封裝的都是一些例如判斷是否是數組,是否對象和函數等通用函數,因此從理解 Vue3 源碼角度,可以不去關注;而 vue
就是最終要發佈的 Vue3
的包,但是從源碼來看,這僅僅是內部模塊對外的導出出口, 它的源碼也只有一個 index.ts
文件,通過這個文件我們可以知道,最終 Vue3
對外提供了哪些接口,也就是前面我們創建Composition API
網頁裏面使用的全局對象 Vue
里支持的 API 函數。
縮小我們的關注範圍,構成 Vue3
最核心的是以下 5 個 package:
reactivity
compiler-core
runtime-core
compiler-dom
runtime-dom
而這其中前 3 個 package 即 reactivity
,compiler-core
,runtime-core
又是 Vue3
核心中的核心(正如 core 一詞所表示的含義)。可以說這 3 個 package 是構建整個 Vue3
項目乃至整個 Vue3
生態的最底層依賴和基石。為了更加生動的理解這句話的含義。我設想一個這樣的畫面。
在一個秋高氣爽的午後,尤雨溪同學抱着自己 13 英寸的 macBookPro 來到自己最常光顧的咖啡店,點了一杯拿鐵。打開 VSCode 準備擼代碼,冥冥之中看到了一個叫做 AngularJs 的東東。突然一個念頭閃現在尤同學的腦海中。」wokao,這傢伙,得勁啊! 我也弄一個…」。經過一段苦思冥想。」本尤要做一個更屌的,不僅用於構建 WEB 界面,還能使用前端熟悉的 html 模版構建手機 App 等任何客戶端界面」。而要達到這個效果,必須在設計時就要把頁面模版解析(編譯)和渲染輸出進行解耦,於是,尤同學新建一個文件夾,命名為
compiler-core
,用於存放實現將使用html
編寫的模版編譯成抽象語法樹
和渲染橋接函數
(用於解耦渲染函數實現的橋
)的代碼,有了模版編譯解析,僅僅只有渲染層的抽象,但還需針對應用級別進行抽象,來運行應用,於是尤同學新建了第二個文件夾,命名為runtime-core
,用於存放創建應用和應用渲染器的抽象,這其中也包含了構成應用的組件和節點的抽象。到這一步,一個從 html 模版(字符串)構建應用視圖界面的抽象已經完成,但是為了將視圖顯示的內容與數據進行綁定,實現修改數據時,就能響應式地改變視圖內容,還需要一個響應數據變化的模塊,於是尤同學又新建了第三個文件夾,命名為reactivity
,經過技術分析,尤同學認為當前使用 ES6 的新特性Proxy
來實現數據響應是最優雅的方式,於是尤同學決定在這個文件夾里存放管理所有基於Proxy
封裝的響應式模塊。不同於前兩個package
是對平台和環境的抽象,reactivity
是一個具像的實現,正如我們前面使用Composition API
構建的hello world
網頁中使用的reactive
函數就是導出自reactivity
。至此,用於實現構建任何用戶界面的底層抽象和響應式數據模型已經完成。距離將這個視圖設計方式應用到最終的產品中,還差一個將抽象的平台無差別的compiler-core
和runtime-core
的平台級實現。但是要實現所有平台的視圖渲染,可不是一個小的工作量,前提你要會相關平台界面開發,例如 IOS APP 或 Android APP 的界面開發。可溪,尤同學只學過 Web 前端。於是,尤同學先從自己熟悉的入手,添加了兩個用於在瀏覽器下環境的模塊渲染和應用運行時實現,即compiler-dom
和runtime-dom
。 「…不知不覺,又過了一個秋」。尤同學終於將一年前那個設想在瀏覽器環境下實現,但是,距離最終目標顯然還有一段路要走。尤同學接下來首要任務是先實現單文件組件 package 和 服務端渲染 pacakge ,來滿足在 Webpack 環境更好開發 Vue3 應用,以及需要 SEO 場景的服務端渲染應用。溫馨提示:本劇情純屬虛構,甚至有點好笑 ^^!
看完這段虛構劇情,想必你已經對當前 Vue3
中 5 個最重要的模塊有了一個比較清晰的理解。最後,用一張圖來總結它們之間的關係。圖片中箭頭代表依賴。事實上,我們最終使用的 vue package 就是在瀏覽器下運行的,因此,vue 直接依賴於 compiler-dom
和 runtime-dom
。而 vue 到 reactive
依賴使用了虛線,是因為,vue 不是直接依賴於 reactivity
,而是通過導出所有 runtime-dom
的導出,而 runtime-dom
又導出了所有 runtime-core
,其中包含了 reactivity
中創建響應式對象的 reactive
函數,通過這種方式間接導出了 reactive
,也就是前文 hello-world
WEB 應用中使用的函數。

4. createApp
我們已經知道構成 Vue3 最核心的 5 個 package 的分工和依賴關係。但是它們之間具體如何相互「協作」,來完成一個完整的 WEB APP 的創建呢。我們以前文使用 Composition API
創建的 hello world
網頁應用為例。以下摘取的是 Javascript 代碼部分(這裡使用了 ES6 的語法編寫)。
const { createApp, reactive, onMounted } = Vue const state = reactive({ text: 'hello world!' }) const App = { setup () { onMounted(() => { console.log('onMounted: hello world!') }) return { state } } } createApp().mount(App, '#app')
我們看到最後一行代碼,使用了一個 createApp
工廠函數創建了一個 Vue3 應用實例,然後將使用 Composition API
編寫的應用根組件 App
掛載到 ID 為 app
的 Dom 元素上。這個過程在 Vue3
內部是如何傳遞的,或者說我們前面說的 5 個 package 之間如何協作來完成這個 App 創建的。下面是青筆逐行代碼追蹤後畫出了這樣一個調用關係圖。

圖中添加了背景色的部分是一些比較代表各 package 發揮關鍵作用的部分。其中黃色部分是 Vue3 在應用中導出的 Api ; 橙色部分是 runtime-core
中創建運行時渲染器;青色部分是 compiler-core
及 compiler-dom
中用於將模版字符串編譯成渲染函數的抽象語法樹及 dom 渲染實現;綠色部分是 reactivity
導出的兩個基本的響應式 API,reactive
函數用於傳入一個非響應式普通 JS 對象,返回一個響應式數據對象,而 isReactive
函數用於判斷一個對象是否是一個響應式對象。
5. Typescript
我們知道 Vue3
使用 Typescript
編寫。但是,這不並意味着我們必須從頭到尾先把 Typescript
學習一遍,才能看懂 Vue3
的源碼。眾所周知,Javascript 是一門弱類型的語言,這樣帶來的好處是減少代碼「噪聲」(與要實現功能無關的語法成分),讓開發者專註於業務邏輯的實現,寫出更加簡潔易懂的代碼;但凡事皆有利弊,當編寫對穩定性和安全性有更高要求的大型軟件時,類型靈活多變反而成了滋生疑難 BUG 的溫床。 因此就有了 Typescript
這樣的強類型的語言,不過它僅僅是 Javascript 的超集,就是說任何合法的 Javascript
代碼同時也是合法的 Typescript
。 Typescript
的核心就是在 Javascript
語法的基礎上增加了對數據類型的約束,以及新增一些數據類型(如:元組,枚舉,Any等),接口類型(Interface)。而掌握 Typescript
的真正難點在於掌握在不同場景下限定類型的方式。具體而言就是變量申明,函數傳參,函數返回值,複合(Array,Set, Map,WeakSet,WeakMap)元素類型,接口類型和類型別名。
以下給出了 Typescript
最常用也最基本的類型使用方式。
// 變量申明 let num: number = 1 let str: string = 'hello' // 函數參數類型與返回值類型 function sum(a: number, b: number): number { return a + b } // 複合元素類型 let arr: Array<number> = [1, 2, 3] let set: Set<number> = new Set([1, 2, 2]) let map: Map<string, number> = new Map([['key1', 1], ['key2', 2]]) // 接口類型 interface Point { x: number y: number } const point: Point = { x: 10, y: 20 } // 類型別名 type mathfunc = (a: number, b: number) => number const product: mathfunc = (a, b) => a * b console.log(num, str, arr, set, map, sum(1, 2), product(2, 3), point)
以上的例子,還是比較簡單易懂的。個人覺得最 Typesript
最難理解的類型,也是 Vue3
源碼閱讀起來最大的障礙是泛型(Generics)
。泛型是一種基於類型的組件(這裡的組件是指代碼中可復用單元,如函數等)復用機制,這麼說有些抽象,簡單來說,可以理解為類型變量。通常用於函數,作用類似於面向對象編程里的函數重載。
既然說在 Typescript 里范型就像類型變量,那麼這個變量如何定義和使用,下面舉個例子。
函數 identity()
接受 string 類型參數,並返回自身,也是 string 類型。
function identity(arg: string): string { return arg }
現在不希望參數和返回類型固定為 string ,同時又希望能限定類型,最好的辦法就是使得類型可變,或者說把類型定義為一個變量。這就是所謂的泛型。那麼這個「類型變量」在哪定義,答案是在函數名稱後面,插入一對尖括號」<>」,並在尖括號里定義這個變量,然後就可以將後面參數和返回類型用這個「類型變量替換」。如下:
function identity<T>(arg: T): T { return arg } console.log(identity<string>('hello')) console.log(identity<number>(100)) // 也可省略類型部分 console.log(identity('hello')) console.log(identity(100))
想了解更多范型的使用場景,可參考官方文檔
如果認真掌握以上 Typescript 的類型使用,那麼基本就可以讀懂 Vue3
的源碼了。雖然,這裡列舉的特性並非 Typescript 的全部,但是,剩下的已經不影響正確的理解源碼,並且相比直接看完並掌握所有 Typescript 的特性,通過閱讀 Vue3
源碼能讓你更快速地掌握最重要的特性和最佳實踐方法,可謂一舉兩得。
6. 實踐理解源碼核心部分
說了這麼多,最後通過 3 個示例代碼,實踐總結和加深理解 Vue3
最核心 3 個模塊的作用,作文本文的收尾。
6.1 reactivity
在 ./examples
目錄新建文件 reactivity.ts
,粘貼如下代碼:
import { reactive, isReactive } from '@vue/reactivity' const content = { text: 'hello' } const state = reactive(content) console.log('content is reactive: ', isReactive(content)) console.log('state is reactive: ', isReactive(state)) console.log('state ', state) content.text = 'world' console.log('state ', state)
編譯運行:
tsc reactivity.ts && node reactivity.js

6.2 compiler-core
在 ./examples
目錄新建文件 compiler-core.ts
,粘貼如下代碼:
import { baseCompile as compile } from '@vue/compiler-core' const template = '<p>{{ state.text }}</p>' const { ast, code } = compile(template) console.log('astn----') console.log(ast) console.log('coden----') console.log(code)
編譯運行:
tsc compiler-core.ts && node compiler-core.js


6.3 runtime-core
在 ./examples
目錄新建文件 runtime-core.ts
,粘貼如下代碼:
import { createRenderer } from '@vue/runtime-core' const patchProp = function (el: Element, key: string, nextValue: any, prevValue: any, isSVG: boolean) {} const nodeOps = { insert: (child: Node, parent: Node, anchor?: Node) => {}, remove: (child: Node) => {}, createElement: (tag: string, isSVG?: boolean) => {}, createText: (text: string) => {}, createComment: (text: string) => {}, setText: (node: Text, text: string) => {}, setElementText: (el: HTMLElement, text: string) => {}, parentNode: (node: Node) => {}, nextSibling: (node: Node) => {}, querySelector: (selector: string) => {} } const { createApp } = createRenderer({ patchProp, ...nodeOps }) console.log(createApp())
編譯運行:
tsc runtime-core.ts && node runtime-core.js

總結
本文從使用 Vue3
組合式API搭建第一個響應式 Web 應用開篇,由淺入深,先後講解了構成 Vue3
最重要的 5 個 package 的分工和依賴,並進一步道出構成 Vue3
及構建 Vue3
生態 3 個最底層的 package ,並通過編造一段有趣的故事來幫助讀者理解 Vue3
的本質。為了掃除讀者深入閱讀 Vue3
源碼的心理障礙,增加了針對 Vue3
源碼所需要掌握的 Typescript 基礎知識。最後,通過動手編寫 3 個示例代碼,分別給出 Vue3
響應式數據,模版編譯和創建運行時應用最重要的接口,引導讀者動手調試 Vue3
核心代碼,來真正吃透 Vue3
的核心原理。
如果你對 Vue3
源碼和最新發展感興趣,可以關注作者微信號,回復:vue
,加入「Vue3 前端技術交流群」,和作者一起深入探討學習。