大白話Vue源碼系列(05):運行時鳥瞰圖

来源:https://www.cnblogs.com/iovec/archive/2018/01/02/vue_05.html
-Advertisement-
Play Games

初看runtime源碼,如入迷宮,小模塊間跳來跳去,我是誰,我在哪,我為什麼要打開它;再看runtime,眉目初現,繪出調用棧,如坐時光機,骨架漸漸明晰。再再看,炳如觀火,代碼層次結構已瞭然於胸。Vue 運行時模塊主要是圍繞 Vue 實例的生命周期展開的,它涵蓋了 Vue 實例生命周期內所需要的全部... ...


閱讀目錄

研究 runtime
一邊 Vue
一邊源碼

初看 Vue 是 Vue
源碼是源碼

再看 Vue 不是 Vue
源碼不是源碼

再再看
Vue 是調用棧
源碼也是調用棧

—— By DOM哥

Vue 運行時這一塊是非常有意思的,不像 Vue 編譯器那麼枯燥,這裡面有大量的實用技巧和設計思想可以學習。使用過 Vue 的小伙伴應該對 Vue 【響應的數據綁定】(也叫雙向綁定)的印象非常深刻,在修改了數據之後,視圖就會實時得到相應更新,這無疑極大地減輕了開發者的負擔,使得開發人員可以專註於處理業務邏輯和操作數據,也就是聞名遐邇的【數據驅動開發】。至於操作 DOM 更新視圖這件苦臟累的活,Vue 已經幫你妥善處理完畢並且對你完全透明(意思是它就像空氣一樣你完全註意不到它,卻又深度依賴它,離不開它)。

Vue 運行時模塊主要是圍繞 Vue 實例的生命周期展開的,它涵蓋了 Vue 實例生命周期內所需要的全部設施,包括實例創建,響應的數據綁定,掛載到 DOM 節點以及數據變化時自動更新視圖等關鍵部分。本篇也將沿著 Vue 實例的生命周期路線,結合運行時關鍵實現偽代碼,一步步清晰地描繪出 Vue 運行時的空中鳥瞰圖。

Vue 實例的生命周期

本段的部分內容參考自 Vue 官網的生命周期描述

就像每個人的生命周期有 幼年童年少年青年中年老年,每個 Vue 實例的生命周期也有 beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedactivateddeactivatedbeforeDestroydestroyed 等多個階段。

Vue 實例生命周期代碼示例:

<div id="index">{{msg}}</div>
 
new Vue({
    el: '#index',
    data: {
        msg: 'lifecycle',
    },

    beforeCreate(){ console.log('beforeCreate')},
    created(){ console.log('created')},
    beforeMount(){ console.log('beforeMount')},
    mounted(){ console.log('mounted')},
})

// Console output:
//      beforeCreate
//      created
//      beforeMount
//      mounted
 

每個 Vue 實例在被創建時都要經過一系列的初始化過程,例如設置數據監聽,編譯 HTML 模板,將實例掛載到 DOM 等。在這個初始化的過程中會在特定的地方運行一些叫做【生命周期鉤子】的函數,這些鉤子其實就是開發者可以自定義的回調函數,如上面傳入的 created 函數就會在 Vue 實例 created 時被調用。

下麵一張圖可以非常清晰地說明 Vue 各個生命周期鉤子的調用時機(圖片來自 Vue 官網生命周期圖示):

Vue 的生命周期圖示

你不需要立馬弄明白圖上所有的東西,不過隨著你的不斷學習和使用,它的參考價值會越來越高。

實例創建

眾所周知 Vue 是通過 new Vue() 的方式進行使用的,也就是說 Vue 內部將自己封裝成了一個類。然而 Vue 並沒有使用 ES6 最新的 class 方式進行實現,而是用了原來 prototype 那一套,這是讓寶寶有些傷心的。閑話待會再敘,先看一下源碼:

// vue/src/core/instance/index.js
function Vue (options) {
  this._init(options)
}
 

Vue 將初始化工作全部放在了 Vue.prototype._init() 方法里。去偽存真,_init 方法主代碼如下:

// vue/src/core/instance/init.js
Vue.prototype._init = function (options) {
    const vm = this

    vm.$options = mergeOptions(options || {})

    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initState(vm)
    callHook(vm, 'created')

    if (vm.$options.el) {
        vm.$mount(vm.$options.el)
    }
}
 

initEventsinitRender 函數主要用來初始化 Vue 實例的一些容器欄位,現在可暫時忽略它們。接下來重點來了,在 initState 函數中封裝了實現【響應的數據綁定】的關鍵代碼,雖然這不是 Vue 最流弊的部分,但卻是咱對 Vue 最好奇的地方,也是咱開始本源碼系列的最初動力。在 initState 之前和之後分別調用了 Vue 的生命周期鉤子函數 beforeCreatecreated,接下來看看 Vue 是如何實現響應的數據綁定的。

響應的數據綁定

響應的數據綁定並不是 Vue 獨創的,而是 MVVVM 模式理論的一部分,它是 View 層和 ViewModel 層的連接方式。如下圖所示:

MVVM 分層示意圖

Vue 通過【觀察者模式】實現了一套響應式系統。觀察者模式(也叫發佈/訂閱模式)會將觀察者和被觀察的對象嚴格分離開,當被觀察對象的狀態發生變化時,所有依賴於它的觀察者都將得到通知並自動刷新。舉個慄子,用戶界面可以作為一個觀察者,業務數據是被觀察者,用戶界面觀察業務數據的變化,當數據發生變化時,用戶界面就會自動更新。

該模式必須包含兩個角色:觀察者和被觀察對象。Vue 定義了一個 Watcher 類來創建觀察者,定義了一個 Dep 類來創建被觀察對象。 Dep 是 Dependent 的縮寫,意思是作為觀察者的依賴存在,也就是被觀察對象。

首先看一下【觀察者】 Watcher 的定義:

// vue/src/core/observer/watcher.js
import Dep from './dep'
export default class Watcher {
    constructor(vm) {
        this.vm = vm
        this.newDeps = []
        Dep.target = this
    }

    // 添加一個觀察者,或者說註冊一個依賴
    addDep(dep) {
        this.newDeps.push(dep)
        // 在【觀察者】收集【被觀察者】的同時,【被觀察者】也會收集【觀察者】
        // 這好比王八看綠豆對眼兒了,遂互存了電話號碼,就有了後來的相識相知
        dep.addSub(this)
    }

    // 在被觀察對象狀態發生變化時調用此方法
    update() {
        let {vm} = this
        // 更新視圖
        vm._update(vm._render())
    }
}
 

每一個【觀察者】都會收集自己要觀察的數據對象(Dep),當【被觀察對象】發生變化時,【被觀察對象】會通知【觀察者】,【觀察者】收到通知後執行 update 方法更新視圖。

接下來看一下【被觀察者】 Dep

export default class Dep {
    constructor () {
        this.subs = []
    }
    addSub (sub) {
        this.subs.push(sub)
    }
    depend () {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }
    // 通知所有對自己有依賴的觀察者
    notify () {
        const subs = this.subs
        for (let i = 0; i < subs.length; i++) {
            subs[i].update()
        }
    }
}
Dep.target = null
 

每個【被觀察對象】同樣會收集依賴自己的【觀察者】,當自己發生變化時,就會通知(notify)這些觀察者 update

那麼問題來了,這兩個角色是如何收集對方的呢?又如何得知【被觀察者】發生變化了呢? 這就用到了並不常用的 Object.defineProperty() 方法,通過在 JavaScript 對象每個屬性描述符的 settergetter 里做文章,就能實時捕捉 JavaScript 對象的變化。

需要註意的是,Object.defineProperty() 是 JS 語言本身的一個 API 而不是 Vue 實現的,Object.defineProperty 是 ES5 中一個無法 shim 的特性,這也是為什麼 Vue 不支持 IE8 以及更低版本瀏覽器的原因。如果想支持 IE8 以及更低版本瀏覽器怎麼辦呢?那就只有放棄 Vue,選擇 Knockout。更好的解決方案就是直接讓 IE8 以及更 low 的家伙見鬼去吧。不過基本上不用擔心這個問題了,因為據最新瀏覽器使用調查報告,IE8 以及更低版本瀏覽器的市場份額已經微不足道,直接忽略不計就行了。

既然 JS 已經支持在對象屬性變化時添加自定義處理,Vue 需要做的事就是遍歷傳入的 data 選項,為 data 的每個屬性設置 settergetter。這就解決瞭如何得知【被觀察者】發生了變化這個問題。

接下來說說這兩者是如何收集對方的。【觀察者】和【被觀察者】就好比單身男和單身女,得有人安排相親才能建立起聯繫呵,Vue 就是這個牽線搭橋的媒婆。下麵是相親源碼:

// vue/src/core/observer/index.js
import Dep from './dep'
export function observe (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
        let key = keys[i], value = obj[key];
        // 深度優先遍歷
        observe(value)

        const dep = new Dep()
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                // 【觀察者】收集【被觀察者】
                // 同時【被觀察者】也會收集【觀察者】
                if (Dep.target) {
                    Dep.target.addDep(dep)
                }
                return value
            },
            set(newVal) {
                value = newVal
                // 【被觀察者】通知【觀察者】
                dep.notify()
            }
        })
    }
}
 

可以看到,Vue 在遍歷 data 對象時完成了【觀察者】和【被觀察對象】彼此之間的收集工作。並且在 data 的某欄位發生變化時,相應的依賴就會通知【觀察者】自己發生了變化,【觀察者】就可以做出反應。

Vue 接下來就會在 initState() 中調用 observe(vm.$options.data),執行之後實例化 Vue 時傳入的 data 對象就會成為響應式的,當你修改 data 對象的數據時(通常是根據用戶操作執行對應的業務邏輯),【被觀察者】就會通知已收集的所有【觀察者】,觀察者就會調用自己的 update 方法,從而更新視圖。這基本上就是 Vue 所實現的響應的數據綁定的工作原理。

掛載到 DOM 節點

在構建完響應式系統之後,Vue 接下來會檢查用戶是否傳入了 el 選項,因為 Vue 在將包含指令的 HTML 模板編譯成最終的朴素的 HTML 之後會執行 DOM 替換操作,最終展示在頁面上,如果沒有 el 選項,Vue 就不知道要把產出的 HTML 放到哪裡去展示。

掛載到 DOM 節點並非替換一下 DOM 那麼簡單,它包括將模板編譯成 render 函數,執行 render 函數生成虛擬DOM,計算出新舊虛擬DOM之間的最小變更,打補丁式地更新頁面視圖等幾大步。

將模板編譯成 render 函數

這個編譯過程在前幾篇的 Vue 編譯器模塊里已經講得很清楚了,主要分為根據模板生成 AST,對 AST 進行優化,根據 AST 生成 render 函數這三步,這裡不再贅述,感興趣的可前往查看

執行 render 函數生成虛擬DOM

【虛擬DOM】並非 Vue 提出的概念,而是老早就被髮掘出來的新型DOM操作方式,MVVM 框架在引入虛擬DOM之後如虎添翼。之所以叫做虛擬DOM,是相對於真實DOM而言的。直接操作DOM很慢,因為真實的DOM對象很重,操作真實DOM對象(HTMLElement)花銷很大,而且操作完之後往往會引起瀏覽器對頁面的重繪和重排。如果頻繁的進行DOM操作,頁面性能會急劇下降。於是聰明的 Jser 決定使用簡單的 JS 對象格式來表示真實 DOM,也就是虛擬DOM。先執行對虛擬DOM的操作(這會執行的很快,因為是純 JS 操作),最後對比操作前後的新舊虛擬DOM樹,找出最小變更,一次性地應用到真實DOM上。雖然還是要對真實DOM操作,但次數卻大大減少,從而在更新視圖的同時可有效保證頁面性能。

Vue 的虛擬DOM系統是在開源虛擬DOM庫 Snabbdom 的基礎上做了適當的改進。

下麵是 Vue 的 VNode 定義(正是一個個這樣的 VNode 組成了一棵虛擬DOM樹):

// vue/src/core/vdom/vnode.js
export default class VNode {
    constructor (tag, data, children, text, elm) {
        this.tag = tag
        this.data = data
        this.children = children
        this.text = text
        this.elm = elm  // 此欄位存放真實DOM
    }
}
 

計算出新舊虛擬DOM之間的最小變更

在上一步執行 render 函數生成虛擬DOM後,接下來就需要對比新舊虛擬DOM之間的差異,從而獲得DOM的最小變更。比較兩棵DOM樹的差異是虛擬DOM庫最核心的部分,這也是所謂的 Virtual DOM 的 diff 演算法。就像版本控制系統 Git 的 diff 可以計算出兩次提交之間的變更,虛擬DOM的 diff 也可以計算出新舊虛擬DOM之間的差異。計算出來的差異稱為一個 patch,也就是補丁。

打補丁式更新頁面視圖

如果是首次渲染,也就是頁面剛載入進來第一次渲染,Vue 會用模板編譯後的DOM替換掉傳入的 el 元素。請註意這一點,對模板內DOM的操作(綁定事件,引用DOM等)應該始終放在 Vue 的 mounted 之後,否則所有處理都將丟失,因為模板會被替換掉。

如果是後續數據發生變化,Vue 就會用打補丁的方式更新視圖,儘可能重用現有DOM,將真實的DOM操作減到最少。

結論

在上面【觀察者】 Watcher 的定義中 update 方法里執行視圖更新。因此 Vue 運行時的整個工作流程基本上是這樣的:

用戶調用 new Vue(options) 實例化 Vue,Vue 在 _init 方法中初始化相關欄位和事件,最重要的,建立起響應式系統,Vue 實例的後續運行重度依賴於此響應式系統。Vue 會新建一個【觀察者】,該觀察者在創建時會執行 update 方法首次渲染視圖,包含 Vue 指令的模板會被替換成編譯後的朴素 HTML。Vue 會遍歷傳入的 data 選項,通過 Object.defineProperty 設置 settergetter 將其變成【被觀察對象】。當 data 的數據發生變化時,被觀察對象就會通知觀察者,觀察者就會再次調用 update 方法打補丁式地更新視圖。

本篇完,將在下一篇中開始深究運行時實現細節。

大白話 Vue 源碼系列目錄

本系列會以每周一篇的速度持續更新,喜歡的小伙伴記得點關註哦


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [1]數據結構 [2]創建鏈表 [3]雙向鏈表 [4]迴圈鏈表 ...
  • 這是分享按鈕: 這是js調用代碼: 這個就是分享js文件NativeShare.js: !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof def ...
  • 使用combo select完善原始select的功能,當碰到大數據量時,反應很慢,因為數據是一次性載入。 嘗試修改控制項的數據載入方案,變更為伺服器端模糊搜索,降低數據量,降低頁面響應時間。 ...
  • 在js引擎部分,我們可以瞭解到,當渲染引擎解析到script標簽時,會將控制權給JS引擎,如果script載入的是外部資源,則需要等待下載完後才能執行。 所以,在這裡,我們可以對其進行很多優化工作。 放置在BODY底部 為了讓渲染引擎能夠及早的將DOM樹給渲染出來,我們需要將script放在body ...
  • ES6提供了新的數據結構Set,它類似於數組,但是成員的值都是唯一的,沒有重覆的值。 Set 本身是一個數據結構,用來生成Set 數據結構。 const s = new Set(); [2,3,5,4,5,2,2,2].forEach(x=>s.add(x)); for(let i of s) { ...
  • 問題1: 範圍(Scope) 思考以下代碼: 控制台會列印出什麼? 答案 上述代碼會列印出5。 (1)在立即執行函數表達式(IIFE)中,有兩個命名,但是其中變數是通過關鍵詞var來聲明的。這就意味著a是這個函數的局部變數。與此相反,b是在全局作用域下的。 (2)在函數中他沒有使用_“嚴格模式”_ ...
  • 錯誤碼: This dependency was not found: * !!vue-style-loader!css-loader?{"minimize":false,"sourceMap":false}!../../node_modules/vue-loader/lib/style-rewri ...
  • 7.1 模塊的概念 把原本實現在一起的功能離散地分散到每個能實現部分功能的塊,這些塊稱為模塊。模塊具有以下幾個好處: 1 程式小,易理解,易調試測試 2 有助於抽象編程設計和複雜程式的封裝 3 內聚性強,耦合性弱 7.2 模塊的引用方法 1.基於ES2015的語法是:import 語句 2 基於Co ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...