Vue2依賴收集原理

来源:https://www.cnblogs.com/burc/archive/2023/04/03/17254663.html
-Advertisement-
Play Games

我們只會在 Observer 類 和 defineReactive 函數中實例化 dep。在 getter 方法中依賴收集,在 setter 方法中派發更新通知 ...


觀察者模式定義了對象間一對多的依賴關係。即被觀察者狀態發生變動時,所有依賴於它的觀察者都會得到通知並自動更新。解決了主體對象和觀察者之間功能的耦合。

Vue中基於 Observer、Dep、Watcher 三個類實現了觀察者模式

  • Observer類 負責數據劫持,訪問數據時,調用dep.depend()進行依賴收集;數據變更時,調用dep.notify() 通知觀察者更新視圖。我們的數據就是被觀察者
  • Dep類 負責收集觀察者 watcher,以及通知觀察者 watcher 進行 update 更新操作
  • Watcher類 為觀察者,負責訂閱 dep,併在訂閱時讓 dep 同步收集當前 watcher。當接收到 dep 的通知時,執行 update 重新渲染視圖

dep 和 watcher 是一個多對多的關係。每個組件都對應一個渲染 watcher,每個響應式屬性都有一個 dep 收集器。一個組件可以包含多個屬性(一個 watcher 對應多個 dep),一個屬性可以被多個組件使用(一個 dep 對應多個 watcher)

Dep

我們需要給每個屬性都增加一個 dep 收集器,目的就是收集 watcher。當響應式數據發生變化時,更新收集的所有 watcher

  1. 定義 subs 數組,當劫持到數據訪問時,執行 dep.depend(),通知 watcher 訂閱 dep,然後在 watcher內部執行dep.addSub(),通知 dep 收集 watcher
  2. 當劫持到數據變更時,執行dep.notify() ,通知所有的觀察者 watcher 進行 update 更新操作

Dep有一個靜態屬性 target,全局唯一,Dep.target 是當前正在執行的 watcher 實例,這是一個非常巧妙的設計!因為在同一時間只能有一個全局的 watcher

註意:
渲染/更新完畢後我們會立即清空 Dep.target,保證了只有在模版渲染/更新階段的取值操作才會進行依賴收集。之後我們手動進行數據訪問時,不會觸發依賴收集,因為此時 Dep.target 已經重置為 null

let id = 0

class Dep {
  constructor() {
    this.id = id++
    // 依賴收集,收集當前屬性對應的觀察者 watcher
    this.subs = []
  }
  // 通知 watcher 收集 dep
  depend() {
    Dep.target.addDep(this)
  }
  // 讓當前的 dep收集 watcher
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // 通知subs 中的所有 watcher 去更新
  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

// 當前渲染的 watcher,靜態變數
Dep.target = null

export default Dep

Watcher

不同組件有不同的 watcher。我們先只需關註渲染watcher。計算屬性watcer和監聽器watcher後面會單獨講!

watcher 負責訂閱 dep ,併在訂閱的同時執行dep.addSub(),讓 dep 也收集 watcher。當接收到 dep 發佈的消息時(通過 dep.notify()),執行 update 重新渲染

當我們初始化組件時,在 mountComponent 方法內會實例化一個渲染 watcher,其回調就是 vm._update(vm._render())

import Watcher from './observe/watcher'

// 初始化元素
export function mountComponent(vm, el) {
  vm.$el = el

  const updateComponent = () => {
    vm._update(vm._render())
  }

  // true用於標識是一個渲染watcher
  const watcher = new Watcher(vm, updateComponent, true)
}

當我們實例化渲染 watcher 的時候,在構造函數中會把回調賦給this.getter,並調用this.get()方法。
這時!!!我們會把當前的渲染 watcher 放到 Dep.target 上,併在執行完回調渲染視圖後,立即清空 Dep.target,保證了只有在模版渲染/更新階段的取值操作才會進行依賴收集

import Dep from './dep'

let id = 0

class Watcher {
  constructor(vm, fn) {
    this.id = id++
    this.getter = fn
    this.deps = []  // 收集當前 watcher 對應被觀察者屬性的 dep
    this.depsId = new Set()
    this.get()
  }
  // 收集 dep
  addDep(dep) {
    let id = dep.id
    // 去重,一個組件 可對應 多個屬性 重覆的屬性不用再次記錄
    if (!this.depsId.has(id)) {
      this.deps.push(dep)
      this.depsId.add(id)
      dep.addSub(this) // watcher已經收集了去重後的 dep,同時讓 dep也收集 watcher
    }
  }
  // 執行 watcher 回調
  get() {
    Dep.target = this // Dep.target 是一個靜態屬性

    this.getter() // 執行vm._render時,會劫持到數據訪問,調用 dep.depend() 進行依賴收集

    Dep.target = null // 渲染完畢置空,保證了只有在模版渲染階段的取值操作才會進行依賴收集
  }
  // 重新渲染
  update() {
    this.get()
  }
}

我們是如何觸發依賴收集的呢?

在執行this.getter()回調時,我們會調用vm._render() ,在_s()方法中會去 vm 上取值,這時我們劫持到數據訪問走到 getter,進而執行dep.depend()進行依賴收集

流程:vm._render() ->vm.$options.render.call(vm) -> with(this){ return _c('div',null,_v(_s(name))) } -> 會去作用域鏈 this 上取 name

MDN 中是這樣描述 with 的

JavaScript 查找某個未使用命名空間的變數時,會通過作用域鏈來查找,作用域鏈是跟執行代碼的 context 或者包含這個變數的函數有關。'with'語句將某個對象添加到作用域鏈的頂部,如果在 statement 中有某個未使用命名空間的變數,跟作用域鏈中的某個屬性同名,則這個變數將指向這個屬性值

Observer

我們只會在 Observer 類 和 defineReactive 函數中實例化 dep。在 getter 方法中執行dep.depend()依賴收集,在 setter 方法中執行dep.notity()派發更新通知

依賴收集

依賴收集的入口就是在Object.defineProperty的 getter 中,我們重點關註2個地方,一個是在我們實例化 dep 的時機,另一個是為什麼遞歸依賴收集。我們先來看下代碼

class Observer {
  constructor(data) {
    // 給數組/對象的實例都增加一個 dep
    this.dep = new Dep()

    // data.__ob__ = this 給數據加了一個標識 如果數據上有__ob__ 則說明這個屬性被觀測過了
    Object.defineProperty(data, '__ob__', {
      value: this,
      enumerable: false, // 將__ob__ 變成不可枚舉
    })
    if (Array.isArray(data)) {
      // 重寫可以修改數組本身的方法 7個方法
      data.__proto__ = newArrayProto
      this.observeArray(data) 
    } else {
      this.walk(data)
    }
  }

  // 迴圈對象"重新定義屬性",對屬性依次劫持,性能差
  walk(data) {
    Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
  }

  // 觀測數組
  observeArray(data) {
    data.forEach(item => observe(item))
  }
}

// 深層次嵌套會遞歸處理,遞歸多了性能就差 
function dependArray(value) {
  for (let i = 0; i < value.length; i++) {
    let current = value[i]
    current.__ob__ && current.__ob__.dep.depend()
    if (Array.isArray(current)) {
      dependArray(current)
    } 
  }
}

export function defineReactive(target, key, value) {
  // 深度屬性劫持;給所有的數組/對象的實例都增加一個 dep,childOb.dep 用來收集依賴
  let childOb = observe(value)

  let dep = new Dep() // 每一個屬性都有自己的 dep

  Object.defineProperty(target, key, {
    get() {
      // 保證了只有在模版渲染階段的取值操作才會進行依賴收集
      if (Dep.target) {   
        dep.depend() // 依賴收集
        if (childOb) {
          childOb.dep.depend() // 讓數組/對象實例本身也實現依賴收集,$set原理
          if (Array.isArray(value)) { // 數組需要遞歸處理
            dependArray(value)
          }
        }
      }
      return value
    },
    set(newValue) { ... },
  })
}

實例化 dep 的時機

我們只會在 Observer 類 和 defineReactive 函數中實例化 dep

  1. Observer類:在 Observer 類中實例化 dep,可以給每個數組/對象的實例都增加一個 dep
  2. defineReactive函數:在 defineReactive 方法中實例化 dep,可以讓每個被劫持的屬性都擁有一個 dep,這個 dep 是被閉包讀取的局部變數,會駐留到記憶體中且不會污染全局

我們為什麼要在 Observer 類中實例化 dep?

  • Vue 無法檢測通過數組索引改變數組的操作,這不是 Object.defineProperty() api 的原因,而是尤大認為性能消耗與帶來的用戶體驗不成正比。對數組進行響應式檢測會帶來很大的性能消耗,因為數組項可能會大,比如10000條
  • Object.defineProperty() 無法監聽數組的新增

如果想要在通過索引直接改變數組成員或對象新增屬性後,也可以派發更新。那我們必須要給數組/對象實例本身增加 dep 收集器,這樣就可以通過 xxx.__ob__.dep.notify() 手動觸發 watcher 更新了

這其實就是 vm.$set 的內部原理!!!

遞歸依賴收集

數組中的嵌套數組/對象沒辦法走到 Object.defineProperty,無法在 getter 方法中執行dep.depend()依賴收集,所以需要遞歸收集

舉個慄子:data: {arr: ['a', 'b', ['c', 'd', 'e', ['f', 'g']], {name: 'libc'}]}

我們可以劫持 data.arr,並觸發 arr 實例上的 dep 依賴收集,然後迴圈觸發 arr 成員的 dep依賴收集。對於深層數組嵌套的['f', 'g'],我們則需要遞歸觸發其實例上的 dep 依賴收集

派發更新

對於對象

在 setter 方法中執行dep.notity(),通知所有的訂閱者,派發更新通知
註: 這個 dep 是在 defineReactive 函數中實例化的。 它是被閉包讀取的局部變數,會駐留到記憶體中且不會污染全局

Object.defineProperty(target, key, {
  get() { ... },

  set(newValue) {
    if (newValue === value) return
    // 修改後重新觀測。新值為對象的話,可以劫持其數據。並給所有的數組/對象的實例都增加一個 dep
    observe(newValue)
    value = newValue

    // 通知 watcher 更新
    dep.notify()
  },
})

對於數組

在數組的重寫方法中執行xxx.__ob__.dep.notify(),通知所有的訂閱者,派發更新通知

註: 這個 dep 是在 Observer 類中實例化的,我們給數組/對象的實例都增加一個 dep。可以通過響應式數據的__ob__獲取到實例,進而訪問實例上的屬性和方法

let oldArrayProto = Array.prototype // 獲取數組的原型
// newArrayProto.__proto__  = oldArrayProto
export let newArrayProto = Object.create(oldArrayProto)

// 找到所有的變異方法
let methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'] // concat slice 都不會改變原數組

methods.forEach(method => {
  // 這裡重寫了數組的方法
  newArrayProto[method] = function (...args) {
    // args reset參數收集,args為真正數組,arguments為偽數組
    const result = oldArrayProto[method].call(this, ...args) // 內部調用原來的方法,函數的劫持,切片編程

    // 我們需要對新增的數據再次進行劫持
    let inserted
    let ob = this.__ob__

    switch (method) {
      case 'push':
      case 'unshift': // arr.unshift(1,2,3)
        inserted = args
        break
      case 'splice': // arr.splice(0,1,{a:1},{a:1})
        inserted = args.slice(2)
      default:
        break
    }

    if (inserted) {
      // 對新增的內容再次進行觀測
      ob.observeArray(inserted)
    }

    // 通知 watcher 更新渲染
    ob.dep.notify()
    return result
  }
})
人間不正經生活手冊
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、安裝虛擬串口軟體 虛擬串口軟體推薦 Virtual Serial Port Driver 官網 和 Virtual Serial Port Kit 官網 都可以免費試用15天。 這裡以Virtual Serial Port Kit為例,打開安裝好的Virtual Serial Port Kit, ...
  • 在接入華為運動健康服務的過程中你是否遇到過許可權申請有困難、功能不會用的情況? 本期超強精華帖,一帖彙總集成華為運動健康服務你可能需要的各類乾貨,還不趕緊收藏起來!開發有困難,隨時可查閱~ 如果你有感興趣或想進一步瞭解的內容,歡迎進行留言,或查看華為運動健康文檔獲取更多詳情! 許可權申請篇 在申請運動健 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 打開游戲界面,看到一個畫面簡潔、卻又富有挑戰性的游戲。屏幕上,有一個白色的矩形框,裡面不斷下落著各種單詞,而我需要迅速地輸入這些單詞。如果我輸入的單詞與屏幕上的單詞匹配,那麼我就可以獲得得分;如果我輸入的單詞錯誤或者時間過長,那麼我就會輸 ...
  • 本博文介紹CSS中常用的文本屬性,包括文本顏色、文本對齊、裝飾文本、文本縮進和行間距。 屬性 說明 屬性值 color 文本顏色 顏色(如red、green)#十六進位(如#ff0000) rgb代碼(如rgb(255,0,0)) text-align 文本對齊 left(預設值,左對齊) righ ...
  • 當我們需要執行動畫或其他高性能操作時,常常會遇到以下問題: - 任務的執行頻率過高,對 CPU 和記憶體造成了大量的壓力。- 任務的優先順序較高,導致其他任務無法及時得到處理。 為瞭解決這些問題,JavaScript 提供了兩個調度 API:requestAnimationFrame 和 request ...
  • 本博文介紹了CSS3中新增的選擇器,包括屬性選擇器、兩類結構偽類選擇器和偽元素選擇器,並對兩類結構偽類選擇器進行了比較。 ...
  • <div class="layui-row layui-col-space15" id="app"></div> 定義vueApp: let vueApp require(['vue'],function(Vue) { vueApp=new Vue({ el: "#app", data: { whe ...
  • 作者:京東科技 孫凱 一、前言 對前端開發者來說,Vite 應該不算陌生了,它是一款基於 nobundle 和 bundleless 思想誕生的前端開發與構建工具,官網對它的概括和期待只有一句話:“下一代的前端工具鏈”。 Vite 最早的版本由尤雨溪發佈於3年前,經歷了3年多的發展,Vite 也已逐 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...