Vue3 深度解析

前言

距離尤雨溪首次公開 Vue3 (vue-next)源碼有一個多月了。青筆觀察到,剛發佈國慶期間,出現很多解讀 Vue3 源碼的文章。固然很多有追風蹭熱之嫌,文章草草講講響應式原理,或者只是作了一些上層的導讀,告訴讀者應該先看哪再看哪。不能說這些文章就沒有一點價值,它確實可以讓你在短期內,不用過多思考就能瞭解到一些 Vue3 重中之重的「乾貨」。可是過於乾貨的未必就是好的。由於乾貨一般是通過做者咀嚼事後的產物,大部分養分其實只被做者消化了。留給讀者的只是一些看似頗有料,實則沒有養分的殘渣。就像一塊啃到只剩骨頭的排骨。這樣的文章一般適合於媒體傳播,僅用於快速捕獲眼球。可是對於想更細緻瞭解 Vue3 的專業前端開發,這顯然遠遠不夠。javascript

事實上,這不是青筆第一篇關於 Vue3 的文章。在 Vue3 公佈後的第五天,也就是10月10號。青筆沒有直接解讀源碼,而是從一個想要本身開發或參與 Vue3 項目的角度,講到了構建 Vue3 所用到的構建工具和相關技術。文章不只僅是給出最後的「乾貨」,而是把青筆在實踐過程當中的用到的方式方法,包括獲得結果的每一行shell命令,git 技巧等。讀者徹底能夠按照文章的脈絡獲得和青筆同樣的結果。這樣作是青筆自身多年軟件開發經驗,所堅持的一個觀點,那就是**「技術不是用來看的,而是用來實操的」**。只有當你能親自實踐才能真正理解其中的內涵,而且這也是最簡單和行之有效的學習方式。那些讀起來晦澀難懂,繁雜抽象的概念術語,其實最怕的就是被實操,由於一旦遇到一個身懷實操大法的讀者,它的全部「江湖禁術」將被見招拆招,一一破解。html

可是想要少走彎路,高效率地實踐,前提是有一篇相關的文章。前端

從零開始構建 vue3vue

本文依然堅持這樣的一個準則,帶你從實踐角度來探祕 Vue3 的源碼。你也能夠理解爲「授之以魚不如授之以漁」。java

1. 準備工做

爲了順利完成後面的實踐。請先確保你的電腦已經安裝瞭如下工具。node

  1. git
  2. node 10 及以上版本 (LTS版)
  3. yarn
  4. lerna
  5. typescript

其中 lernatypescript 使用 npm 進行全局安裝。安裝方式以下:react

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)。linux

在經典的 Options API 中,咱們使用一個具備 data, methods 等「選項」的 JS 對象來定義一個頁面或者組件。這種簡單直接的方式,在應用早期階段,代碼和業務邏輯較簡單時,很是的友好親民,這也是 Vue 以學習門檻較低而廣受開發者親暱的的一個因素。可是,有過開發大型 Vue 應用的開發者應該心有體會。當頁面的邏輯愈來愈多時,咱們的組件將變得冗長複雜。不少本能夠複用的邏輯和代碼,你很難有一種使用起來很是溫馨的方式來複用。親筆自身實踐,在 Vue2 中,組件邏輯和代碼複用最經常使用的方式是混入 mixin ,這雖然是一種可行的方式,可是這種方式顯然從出生和 Vue 框架緊密耦合。當你想要將一個框架無區別的普通 JS 函數或者對象複用到 Vue2 開發的組件中時,你發現一切都是那麼的不優雅。git

基於知足在開發大型 Vue 應用中更優雅地複用已有代碼的需求催生下,Vue3 Composition API 彷佛是順勢而爲,而且勢在必得。github

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 即 reactivitycompiler-coreruntime-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-coreruntime-core 的平臺級實現。可是要實現全部平臺的視圖渲染,可不是一個小的工做量,前提你要會相關平臺界面開發,例如 IOS APP 或 Android APP 的界面開發。可溪,尤同窗只學過 Web 前端。因而,尤同窗先從本身熟悉的入手,添加了兩個用於在瀏覽器下環境的模塊渲染和應用運行時實現,即 compiler-domruntime-dom 。 "...不知不覺,又過了一個秋"。尤同窗終於將一年前那個設想在瀏覽器環境下實現,可是,距離最終目標顯然還有一段路要走。尤同窗接下來首要任務是先實現單文件組件 package 和 服務端渲染 pacakge ,來知足在 Webpack 環境更好開發 Vue3 應用,以及須要 SEO 場景的服務端渲染應用。

舒適提示:本劇情純屬虛構,甚至有點可笑 ^^!

看完這段虛構劇情,想必你已經對當前 Vue3 中 5 個最重要的模塊有了一個比較清晰的理解。最後,用一張圖來總結它們之間的關係。圖片中箭頭表明依賴。事實上,咱們最終使用的 vue package 就是在瀏覽器下運行的,所以,vue 直接依賴於 compiler-domruntime-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-corecompiler-dom 中用於將模版字符串編譯成渲染函數的抽象語法樹及 dom 渲染實現;綠色部分是 reactivity 導出的兩個基本的響應式 API,reactive 函數用於傳入一個非響應式普通 JS 對象,返回一個響應式數據對象,而 isReactive 函數用於判斷一個對象是不是一個響應式對象。

5. Typescript

咱們知道 Vue3 使用 Typescript 編寫。可是,這不併意味着咱們必須從頭至尾先把 Typescript 學習一遍,才能看懂 Vue3 的源碼。衆所周知,Javascript 是一門弱類型的語言,這樣帶來的好處是減小代碼「噪聲」(與要實現功能無關的語法成分),讓開發者專一於業務邏輯的實現,寫出更加簡潔易懂的代碼;但凡事皆有利弊,當編寫對穩定性和安全性有更高要求的大型軟件時,類型靈活多變反而成了滋生疑難 BUG 的溫牀。 所以就有了 Typescript 這樣的強類型的語言,不過它僅僅是 Javascript 的超集,就是說任何合法的 Javascript 代碼同時也是合法的 TypescriptTypescript 的核心就是在 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('ast\n----')
console.log(ast)
console.log('code\n----')
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 前端技術交流羣",和做者一塊兒深刻探討學習。


關注做者微信

相關推薦:《從零開始構建 vue3》

相關文章
相關標籤/搜索