Vue2源碼解析-源碼調試與核心流程梳理圖解

来源:https://www.cnblogs.com/flytree/archive/2022/07/14/16448646.html
-Advertisement-
Play Games

現在VUE3已經有一段時間了,也慢慢普及起來了。不過因為一直還在使用VUE2的原因還是去瞭解和學了下它的源碼,畢竟VUE2也不會突然就沒了是吧,且VUE3中很多原理之類的也是類似的。然後就準備把VUE3搞起來了是吧。VUE2源碼使用的是roullup進行打包的,還使用了Flow進行靜態類型檢測(該庫 ...


現在VUE3已經有一段時間了,也慢慢普及起來了。不過因為一直還在使用VUE2的原因還是去瞭解和學了下它的源碼,畢竟VUE2也不會突然就沒了是吧,且VUE3中很多原理之類的也是類似的。然後就準備把VUE3搞起來了是吧。VUE2源碼使用的是roullup進行打包的,還使用了Flow進行靜態類型檢測(該庫使用的已經不多了,且VUE3已經使用TypeScript進行開發了,有類型檢測了)。若是沒怎麼接觸過Vue2,直接Vue3會更划算些,結構之類的也更清晰了

篇幅有限只探討了核心的一些過程。


VUE2項目結構與入口

主要目錄結構:

vue2源碼倉庫:https://github.com/vuejs/vue
clone後可以看到大概如下結構:

|----benchmarks 性能測試
|----scripts 腳本文件
|----scr 源碼
|  |----compiler 模板編譯相關
|  |----core vue2核心代碼
|  |----platforms 平臺相關
|  |----server 服務端渲染
|  |----sfc 解析單文件組件
|  |----shared 模塊間共用屬性和方法

package.json入口:

// package.json 中指定了roullup的配置文件及打包參數
"scripts": {
    "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
    "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
    "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
}
// 在/scripts/config.js 可以看到在接受到參數後 打包入口最終在 /src/platforms下文件中

構建參數與版本的說明

可以看到rollup打包或者調試的時候後面更了很多參數,不同參數就能生成不同內容的版本,參數說明如下:

  • web-runtime: 運行時,無法解析傳入的template
  • web-full:運行時 + 模板編譯
  • web-compiler:僅模板編譯
  • web-runtime-cjs web-full-cjs:cjsCommonJS打包
  • web-runtime-esm web-full-esm :esm 語法(支持import export)
  • web-full-esm-browser:瀏覽器中使用
  • web-server-renderer:服務端渲染

註:在使用CLI腳手架開發時,一般都是選擇web-runtime是因為,腳手架中有vue-loader會將模板轉為render函數了,所以不需要再模板編譯了。

入口深入與源碼的構建,調試

我們可以在/platforms目錄下找到,最外層的入口。但這個入口有經過層層包裝,添加了些方法後,最後才會到創建VUE實例的入口。以entry-runtime-with-compiler.js為例,
entry-runtime-with-compiler 重寫了$mount,主要增加了對模板的處理方法。:

  • 沒有template則嘗試從el中取dom作template
  • template則直接使用傳入的template
  • 沒則將template轉化為render函數,放在$options

它的Vue又是從./runtime/index導進來的。runtime/index.js有公共的$mount方法,還增加了:

  • directives (全局指令:model,show)
  • components (全局組件:transition,transitionGroup)
  • patch(瀏覽器環境)

詳細流程如下圖:
image

開啟調試:

package.json項中增加sourcemap配置,如:

"scripts": {
    "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
	.......
 }

然後npm run dev就可以在源碼中debugger進行調試了。


VUE基本代碼的執行在源碼中的核心流程

比如在頁面中有如下代碼,它主要涉及到Vue中的技術有:模板語法,數據雙向綁定,計算屬性,偵聽器。

點擊查看主要代碼
<div id="app">
  <p>{{fullName}}:{{fullName}}-{{formBY}}</p>
</div>

const vm = new Vue({
    el: "#app",
    data() {
        return {
            firstName: "Shiina",
            lastName: "Mashiro",
            formBY: "flytree-cnblogs",
            arr: [1, 2, 3, ["a"]],
        };
    },
    computed: {
        fullName() {
            return this.firstName + this.lastName;
        }
    },
    watch: {
        firstName(newValue, oldValue) {
            console.log(newValue, oldValue)
        }
    }
});

setTimeout(() => {
    vm.firstName = 'flytree'
}, 1000);

我們可以把核心(細節後面再展開,先有個整體把握)的執行流程梳理下如下圖:
image

創建響應式數據

要實現數據的雙向綁定,就要創建響應式數據,原理就是重寫了data中每項數據的gettersetter,這樣就可以攔截到每次的取值或者改值的操作了,取值的時候收集依賴,改值的時候通知notify:

點擊查看代碼
// 路徑 /scr/core/observer/index.js
export function defineReactive() {
    const dep = new Dep()

    const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
        return
    }

    // cater for pre-defined getter/setters
    const getter = property && property.get
    const setter = property && property.set
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }

    let childOb = !shallow && observe(val)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    childOb.dep.depend()
                    if (Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val
            /* eslint-disable no-self-compare */
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            /* eslint-enable no-self-compare */
            if (process.env.NODE_ENV !== 'production' && customSetter) {
                customSetter()
            }
            // #7981: for accessor properties without setter
            if (getter && !setter) return
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            childOb = !shallow && observe(newVal)
            dep.notify()
        }
    })
}

模板編譯

compileToFunctions進行模板編譯,主要流程就是:

  1. 使用正則解析模板,然後將其轉化衛AST抽象語法樹。
  2. 然後根據AST抽象語法樹拼裝render函數。

比如上面的代碼

template: "<div id=\"app\">\n      <p>{{fullName}}:{{fullName}}-{{formBY}}</p>\n

image

生成的render函數:
image

"with(this){
  return _c('div',{attrs:{"id ":"app "}},
        [_c('p',[_v(_s(fullName)+": "+_s(fullName)+" - "+_s(formBY))])])
}"

使用with,vue實列執行到這個方法時,則會去找當前實例的屬性。
_c,_s,_v等函數是用來將對應類型節點轉換位虛擬dom的,render執行後就能生成對應的虛擬dom樹了。

依賴收集

在看依賴收集前,可以想下以下問題:

什麼時候進行依賴收集? data中項被取值(其getter執行)
什麼時候執行getter? _render函數執行
什麼時候執行_render? _update函數執行
什麼時候執行_update? data項中getter執行
什麼時候執行data項中get方法? 模板中取值

這時我們再看下get的來源和去處,看下具體的流程:
image

可以看到:
1.取值:在模板中取值的時候它就會進行依賴收集,執行dep.depend(), 最後會去重的watcher存在依賴的subs[]中。去重是,如果模板中重覆取了兩次值,那也不會重覆收集watcher
2.改值:在值發生變更的時候,就會觸發dep.notify(),會遍歷執行其dep.subs中的所有watcher.update(),最後還是會執行到watcher.get(),那麼就執行了_update(_render())把變化更新到dom上了。

Dep類源碼:

點擊查看代碼
export default class Dep {
  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub) {
    this.subs.push(sub)
  }

  removeSub (sub) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Watcher類源碼:

點擊查看主要代碼
export default class Watcher {
    constructor(vm, expOrFn, cb, options, isRenderWatcher) {
        this.vm = vm
        if (isRenderWatcher) {
            vm._watcher = this
        }
        vm._watchers.push(this)
        // options
        if (options) {
            this.deep = !!options.deep
            this.user = !!options.user
            this.lazy = !!options.lazy
            this.sync = !!options.sync
            this.before = options.before
        } else {
            this.deep = this.user = this.lazy = this.sync = false
        }
        this.cb = cb
        this.id = ++uid // uid for batching
        this.active = true
        this.dirty = this.lazy // for lazy watchers
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
        this.expression = process.env.NODE_ENV !== 'production'
            ? expOrFn.toString()
            : ''
        // parse expression for getter
        if (typeof expOrFn === 'function') {
            // 渲染watcher時就gettr就傳入了 _update(_render())
            this.getter = expOrFn
        } else {
            this.getter = parsePath(expOrFn)
            if (!this.getter) {
                this.getter = noop
                process.env.NODE_ENV !== 'production' && warn(
                    `Failed watching path: "${expOrFn}" ` +
                    'Watcher only accepts simple dot-delimited paths. ' +
                    'For full control, use a function instead.',
                    vm
                )
            }
        }
        // 在計算屬性創建watcher的時候lazy為true
        this.value = this.lazy
            ? undefined
            : this.get()
    }

    /**
     * Evaluate the getter, and re-collect dependencies.
     */
    get() {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
            value = this.getter.call(vm, vm)
        } catch (e) {
            if (this.user) {
                handleError(e, vm, `getter for watcher "${this.expression}"`)
            } else {
                throw e
            }
        } finally {
            // "touch" every property so they are all tracked as
            // dependencies for deep watching
            if (this.deep) {
                traverse(value)
            }
            popTarget()
            this.cleanupDeps()
        }
        return value
    }

    /**
     * Add a dependency to this directive.
     */
    addDep(dep) {
        const id = dep.id
        if (!this.newDepIds.has(id)) {
            this.newDepIds.add(id)
            this.newDeps.push(dep)
            if (!this.depIds.has(id)) {
                dep.addSub(this)
            }
        }
    }

    /**
     * Clean up for dependency collection.
     */
    cleanupDeps() {
        let i = this.deps.length
        while (i--) {
            const dep = this.deps[i]
            if (!this.newDepIds.has(dep.id)) {
                dep.removeSub(this)
            }
        }
        let tmp = this.depIds
        this.depIds = this.newDepIds
        this.newDepIds = tmp
        this.newDepIds.clear()
        tmp = this.deps
        this.deps = this.newDeps
        this.newDeps = tmp
        this.newDeps.length = 0
    }

    /**
     * Subscriber interface.
     * Will be called when a dependency changes.
     */
    update() {
        /* istanbul ignore else */
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
            this.run()
        } else {
            queueWatcher(this)
        }
    }

    /**
     * Scheduler job interface.
     * Will be called by the scheduler.
     */
    run() {
        if (this.active) {
            const value = this.get()
            if (
                value !== this.value ||
                // Deep watchers and watchers on Object/Arrays should fire even
                // when the value is the same, because the value may
                // have mutated.
                isObject(value) ||
                this.deep
            ) {
                // set new value
                const oldValue = this.value
                this.value = value
                if (this.user) {
                    const info = `callback for watcher "${this.expression}"`
                    invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
                } else {
                    this.cb.call(this.vm, value, oldValue)
                }
            }
        }
    }

    /**
     * Evaluate the value of the watcher.
     * This only gets called for lazy watchers.
     */
    evaluate() {
        this.value = this.get()
        this.dirty = false
    }

    /**
     * Depend on all deps collected by this watcher.
     */
    depend() {
        let i = this.deps.length
        while (i--) {
            this.deps[i].depend()
        }
    }

    /**
     * Remove self from all dependencies' subscriber list.
     */
    teardown() {
        if (this.active) {
            // remove self from vm's watcher list
            // this is a somewhat expensive operation so we skip it
            // if the vm is being destroyed.
            if (!this.vm._isBeingDestroyed) {
                remove(this.vm._watchers, this)
            }
            let i = this.deps.length
            while (i--) {
                this.deps[i].removeSub(this)
            }
            this.active = false
        }
    }
}

更新到dom樹的細節

從上面步驟分析下來,一般情況下,watcher實例的中的get()執行了,就能觸發,dom更新了。就是走了updateComponent

// 此方法在 core/instance/lifecycle.js
updateComponent = () => {
      vm._update(vm._render(), hydrating)
}

_render執行後會生成虛擬dom,而_update就會執行patch(__patch__)更新對比後更新dom了。

_update源碼:

點擊查看代碼
export function lifecycleMixin (Vue) {
  Vue.prototype._update = function (vnode, hydrating) {
    const vm = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }
}

patch進行diff優化

patch導出:

// platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })

最後createPatchFunction的源碼在core/vdom/patch.js
Diff大概的流程:
判斷是否是相同節點:sameVnode判斷標簽和key是否相同。

diff演算法是用來比較兩個虛擬dom的更新情況的,而且是同級比較的
在diff演算法中有四個指針,
在新的虛擬dom中的兩個指針,新前(在前面的指針),新後(在後面的指針)。
在舊的虛擬dom中的兩個指針,舊前(在前面的指針),舊後(在後面的指針)。

前指針的特點:

  1. 初始位置在最前面,也就是說children數組中的第0位。
  2. 前指針只能向後移動。

後指針的特點:

  1. 初始位置在最後面,也就是說在children數組中的第length-1位。
  2. 後指針只能向前移動。

每次比較可能進行以下四種比較:

  1. 新前和舊前。匹配則,前指針後移一位,後指針前移一位。
  2. 新後和舊後。匹配則,前指針後移一位,後指針前移一位。
  3. 新後和舊前。匹配則,將所匹配的節點的dom移動到舊後之後,虛擬dom中將其設位undefined,指針移動。
  4. 新前和舊後。匹配則,將所匹配的節點的dom移動到舊前之前,虛擬dom中將其設位undefined,指針移動。
    匹配的步驟是按此順序從一到四進行匹配,但若之中有匹配成功的則不進行之後的匹配,比如第2種情況匹配,則不會進行3,4的匹配了。

上面四種匹配是對push, shift, pop, unshift ,reveres ,sort 操作進行優化,但若以上的四種情況都未曾匹配到,則會以新虛擬dom中為匹配的這項當作查找的目標,在舊虛擬dom中進行遍歷查找:

  1. 若查找到,則將dom中找到這項移動舊前之前,其虛擬dom中位置則設為undefined。然後新前指針移動一位。
  2. 若未找到,則將新前所指的這項(也是查找的目標項),生成dom節點,插入到舊前之前上,而後新前指針移動一位。

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

-Advertisement-
Play Games
更多相關文章
  • 資料庫定義語言(DDL) 資料庫 創建資料庫 CREATE DATABASE IF NOT EXISTS 資料庫名 DEFAULT CHARSET utf8 COLLATE utf8_general_ci; 刪除資料庫 drop database 資料庫名; 切換資料庫 use 資料庫名; 查看所有 ...
  • 聚集函數 count([distinct]列名) 統計某列的值總和 sum([distinct]列名) 統計一列的和(註意必須是數字類型的) avg([distinct]列名) 計算一列的平均值(註意必須是數字類型) max([distinct]列名) 尋找一列的最大值 min([distinct] ...
  • 視圖 視圖(view)是一種虛擬存在的表,是一個邏輯表,本身並不包含數據。作為一個select語句保存在數據字典中的。通過視圖,可以展現基表的部分數據;視圖數據來自定義視圖的查詢中使用的表,使用視圖動態生成。 意義 簡單:方便操作,特別是查詢操作,減少複雜的SQL語句,增強可讀性; 安全:資料庫授權 ...
  • 資料庫規範化 第一範式: 欄位不可再分 1NF(第一範式)是指資料庫表的每一列都是不可分割的基本數據項,同一列中不能有多個值,即實體中的某個屬性不能有多個值或者不能有重覆的屬性。 第二範式: 非主鍵欄位完全依賴主鍵欄位 第二範式(Second Normal Form,2nd NF)是指每個表必須有主 ...
  • 如何在 SQL Server 中使用 Try Catch 處理錯誤? 從 SQL Server 2005 開始,我們在TRY 和 CATCH塊的幫助下提供了結構錯誤處理機制。使用TRY-CATCH的語法如下所示。 BEGIN TRY --這裡寫可能導致錯誤的語句 END TRY BEGIN CATC ...
  • 一年一度的高考結束了,很多學生即將離開父母,一個人踏入大學生活,但由於人生閱歷較少,容易被不法分子盯上。 每年開學季也是大一新生遭受詐騙的高峰期,以下是一些常見的案例。有的騙子會讓新生下載註冊一些惡意金融應用這些應用可能包含有病毒、木馬等程式,也可能是仿冒某些知名軟體的應用,犯罪分子通過惡意應用便可 ...
  • 本文簡介 本文主要講解使用 NodeJS 操作 Redis ,順便會先帶一帶 Redis 基礎用法。 在寫本文時,使用 NPM 安裝的 Redis 依賴包已經到了 4.1.0 版本了。我以前用過 2.8 ,這兩個版本在用法上也是有差別的。可能一些老項目還在用老版本的依賴包。所以我會把2個版本的用法都 ...
  • 手把手教你從空白頁面開始通過拖拉拽可視化的方式製作【立體鍵盤】的靜態頁面,不用手寫一行CSS代碼,全程只用10來行表達式就完成了【盲打練習】的交互邏輯。 整個過程在眾觸應用平臺進行,快速直觀。 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...