記錄--六道題理解Vue2 和 Vue3 的響應式原理比對

来源:https://www.cnblogs.com/smileZAZ/archive/2022/07/27/16525663.html
-Advertisement-
Play Games

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 技術棧是 Vue 的同學,在面試中難免會被問到 Vue2 和 Vue3 的相關知識點的實現原理和比較,面試官是步步緊逼,一環扣一環。 Vue2 的響應式原理是怎麼樣的? Vue3 的響應式原理又是怎麼樣的? Vue2 中是怎麼監測數 ...


這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

前言

技術棧是 Vue 的同學,在面試中難免會被問到 Vue2 和 Vue3 的相關知識點的實現原理和比較,面試官是步步緊逼,一環扣一環。

Vue2 的響應式原理是怎麼樣的?

Vue3 的響應式原理又是怎麼樣的?

Vue2 中是怎麼監測數組的變化的?

Vue3 中又是怎麼監測數組的變化的?

在問完你 Vue2 的數組的響應式原理之後,接著可能會補上一句,為什麼要通過重寫數組原型的 7 個方法來對數組進行監測?是因為 defineProperty 真的不能監測數組變化嗎?Vue3 真的只使用 Proxy 就可以實現對數組的代理了嗎?還需要進行什麼設置呢?

Vue2 和 Vue3 的響應式實現原理具體是非常複雜和細節非常繁瑣的,但我們需要在面試中去說清楚其中的原理,這就需要我們進行巨集觀和高度的概括總結。本文主要從面試的角度去講解相關的實現原理,相關代碼只是一個輔助理解。

問題1:Vue2 的響應式原理是怎麼樣的?

所謂響應式就是首先建立響應式數據和依賴之間的關係,當這些響應式數據發生變化的時候,可以通知那些綁定這些數據的依賴進行相關操作,可以是 DOM 更新,也可以是執行一個回調函數。

我們知道 Vue2 的對象數據是通過 Object.defineProperty 對每個屬性進行監聽,當對屬性進行讀取的時候,就會觸發 getter,對屬性進行設置的時候,就會觸發 setter。

/**
* 這裡的函數 defineReactive 用來對 Object.defineProperty 進行封裝。
**/
function defineReactive(data, key, val) {
   // 依賴存儲的地方
   const dep = new Dep()
   Object.defineProperty(data, key, {
       enumerable: true,
       configurable: true,
       get: function () {
           // 在 getter 中收集依賴
           dep.depend()
           return val
       },
       set: function(newVal) {
           val = newVal
           // 在 setter 中觸發依賴
           dep.notify()
       }
   }) 
}

那麼是什麼地方進行屬性讀取呢?就是在 Watcher 裡面,Watcher 也就是所謂的依賴。在 Watcher 裡面讀取數據的時候,會把自己設置到一個全局的變數中。

/**
* 我們所講的依賴其實就是 Watcher,我們要通知用到數據的地方,而使用這個數據的地方有很多,類型也不一樣,有* 可能是組件的,有可能是用戶寫的 watch,我們就需要抽象出一個能集中處理這些情況的類。
**/
class Watcher {
    constructor(vm, exp, cb) {
        this.vm = vm
        this.getter = exp
        this.cb = cb
        this.value = this.get()
    }

    get() {
        Dep.target = this
        let value = this.getter.call(this.vm, this.vm)
        Dep.target = undefined
        return value
    }

    update() {
        const oldValue = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
    }
}
在 Watcher 讀取數據的時候也就觸發了這個屬性的監聽 getter,在 getter 裡面就需要進行依賴收集,這些依賴存儲的地方就叫 Dep,在 Dep 裡面就可以把全局變數中的依賴進行收集,收集完畢就會把全局依賴變數設置為空。將來數據發生變化的時候,就去 Dep 中把相關的 Watcher 拿出來執行一遍。
/**
* 我們把依賴收集的代碼封裝成一個 Dep 類,它專門幫助我們管理依賴。
* 使用這個類,我們可以收集依賴、刪除依賴或者向依賴發送通知等。
**/
class Dep {
    constructor() {
        this.subs = []
    }
    
    addSub(sub) {
        this.subs.push(sub)
    }
    
    removeSub(sub) {
        remove(this.subs, sub)
    }

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

    notify() {
        const subs = this.subs.slice()
        for(let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
        }
    }
}

// 刪除依賴
function remove(arr, item) {
    if(arr.length) {
        const index = arr.indexOf(item)
        if(index > -1){
            return arr.splice(index, 1)
        } 
    }
}

總的來說就是通過 Object.defineProperty 監聽對象的每一個屬性,當讀取數據時會觸發 getter,修改數據時會觸發 setter。

然後我們在 getter 中進行依賴收集,當 setter 被觸發的時候,就去把在 getter 中收集到的依賴拿出來進行相關操作,通常是執行一個回調函數。

我們收集依賴需要進行存儲,對此 Vue2 中設置了一個 Dep 類,相當於一個管家,負責添加或刪除相關的依賴和通知相關的依賴進行相關操作。

在 Vue2 中所謂的依賴就是 Watcher。值得註意的是,只有 Watcher 觸發的 getter 才會進行依賴收集,哪個 Watcher 觸發了 getter,就把哪個 Watcher 收集到 Dep 中。當響應式數據發生改變的時候,就會把收集到的 Watcher 都進行通知。

由於 Object.defineProperty 無法監聽對象的變化,所以 Vue2 中設置了一個 Observer 類來管理對象的響應式依賴,同時也會遞歸偵測對象中子數據的變化。

問題2:為什麼 Vue2 新增響應式屬性要通過額外的 API?

這是因為 Object.defineProperty 只會對屬性進行監測,而不會對對象進行監測,為了可以監測對象 Vue2 創建了一個 Observer 類。Observer 類的作用就是把一個對象全部轉換成響應式對象,包括子屬性數據,當對象新增或刪除屬性的時候負債通知對應的 Watcher 進行更新操作。

// 定義一個屬性
function def(obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    })
}

class Observer {
    constructor(value) {
        this.value = value
        // 添加一個對象依賴收集的選項
        this.dep = new Dep()
        // 給響應式對象添加 __ob__ 屬性,表明這是一個響應式對象
        def(value, '__ob__', this)
        if(Array.isArray(value)) {
           
        } else {
            this.walk(value)
        }
    }
    
    walk(obj) {
        const keys = Object.keys(obj)
        // 遍歷對象的屬性進行響應式設置
        for(let i = 0; i < keys.length; i ++) {
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }
}

vm.$set 的實現原理

function set(target, key, val) {
    const ob = target.__ob__
    defineReactive(ob.value, key, val)
    ob.dep.notify()
    return val
}

當向一個響應式對象新增屬性的時候,需要對這個屬性重新進行響應式的設置,即使用 defineReactive 將新增的屬性轉換成 getter/setter。

我們在前面講過每一個對象是會通過 Observer 類型進行包裝的,併在 Observer 類裡面創建一個屬於這個對象的依賴收集存儲對象 dep, 最後在新增屬性的時候就通過這個依賴對象進行通知相關 Watcher 進行變化更新。

vm.$delete 的實現原理

function del(target, key) {
    const ob = target.__ob__
    delete target[key]
    ob.dep.notify()
}

我們可以看到 vm.$delete 的實現原理和 vm.$set 的實現原理是非常相似的。

通過 vm.$deletevm.$set 的實現原理,我們可以更加清晰地理解到 Observer 類的作用,Observer 類就是給一個對象也進行一個監測,因為 Object.defineProperty 是無法實現對對象的監測的,但這個監測是手動,不是自動的。

問題3:Object.defineProperty 真的不能監聽數組的變化嗎?

面試官一上來可能先問你 Vue2 中數組的響應式原理是怎麼樣的,這個問題你也許會覺得很容易回答,Vue2 對數組的監測是通過重寫數組原型上的 7 個方法來實現,然後你會說具體的實現,接下來面試官可能會問你,為什麼要改寫數組原型上的 7 個方法,而不使用 Object.defineProperty,是因為 Object.defineProperty 真的不能監聽數組的變化嗎?

其實 Object.defineProperty 是可以監聽數組的變化的。

const arr = [1, 2, 3]
arr.forEach((val, index) => {
  Object.defineProperty(arr, index, {
    get() {
      console.log('監聽到了')
      return val
    },
    set(newVal) {
      console.log('變化了:', val, newVal)
      val = newVal
    }
  })
})

其實數組就是一個特殊的對象,它的下標就可以看作是它的 key。

所以 Object.defineProperty 也能監聽數組變化,那麼為什麼 Vue2 棄用了這個方案呢?

首先這種直接通過下標獲取數組元素的場景就比較少,其次即便通過了 Object.defineProperty 對數組進行監聽,但也監聽不了 push、pop、shift 等對數組進行操作的方法,所以還是需要通過對數組原型上的那 7 個方法進行重寫監聽。所以為了性能考慮 Vue2 直接棄用了使用 Object.defineProperty 對數組進行監聽的方案。

問題4:Vue2 中是怎麼監測數組的變化的?

通過上文我們知道如果使用 Object.defineProperty 對數組進行監聽,當通過 Array 原型上的方法改變數組內容的時候是無發觸發 getter/setter 的, Vue2 中是放棄了使用 Object.defineProperty 對數組進行監聽的方案,而是通過對數組原型上的 7 個方法進行重寫進行監聽的。

原理就是使用攔截器覆蓋 Array.prototype,之後再去使用 Array 原型上的方法的時候,其實使用的是攔截器提供的方法,在攔截器裡面才真正使用原生 Array 原型上的方法去操作數組。

攔截器

// 攔截器其實就是一個和 Array.prototype 一樣的對象。
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
].forEach(function (method) {
    // 緩存原始方法
    const original = arrayProto[method]
    Object.defineProperty(arrayMethods, method, {
        value: function mutator(...args) {
            // 最終還是使用原生的 Array 原型方法去操作數組
            return original.apply(this, args)
        },
        eumerable: false,
        writable: false,
        configurable: true
    })
})

所以通過攔截器之後,我們就可以追蹤到數組的變化了,然後就可以在攔截器裡面進行依賴收集和觸發依賴了。

接下來我們就使用攔截器覆蓋那些進行了響應式處理的 Array 原型,數組也是一個對象,通過上文我們可以知道 Vue2 是在 Observer 類裡面對對象的進行響應式處理,並且給對象也進行一個依賴收集。所以對數組的依賴處理也是在 Observer 類裡面。

class Observer {
    constructor(value) {
        this.value = value
        // 添加一個對象依賴收集的選項
        this.dep = new Dep()
        // 給響應式對象添加 __ob__ 屬性,表明這是一個響應式對象
        def(value, '__ob__', this)
        // 如果是數組則通過覆蓋數組的原型方法進來攔截操作
        if(Array.isArray(value)) {
          value.__proto__ = arrayMethods 
        } else {
            this.walk(value)
        }
    }
    // ...
}

在這個地方 Vue2 會進行一些相容性的處理,如果能使用 __proto__ 就覆蓋原型,如果不能使用,則直接把那 7 個操作數組的方法直接掛載到需要被進行響應式處理的數組上,因為當訪問一個對象的方法時,只有這個對象自身不存在這個方法,才會去它的原型上查找這個方法。

數組如何收集依賴呢?

我們知道在數組進行響應式初始化的時候會在 Observer 類裡面給這個數組對象的添加一個 __ob__ 的屬性,這個屬性的值就是 Observer 這個類的實例對象,而這個 Observer 類裡面有存在一個收集依賴的屬性 dep,所以在對數組裡的內容通過那 7 個方法進行操作的時候,會觸發數組的攔截器,那麼在攔截器裡面就可以訪問到這個數組的 Observer 類的實例對象,從而可以向這些數組的依賴發送變更通知。

// 攔截器其實就是一個和 Array.prototype 一樣的對象。
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
].forEach(function (method) {
    // 緩存原始方法
    const original = arrayProto[method]
    Object.defineProperty(arrayMethods, method, {
        value: function mutator(...args) {
            // 最終還是使用原生的 Array 原型方法去操作數組
            const result = original.apply(this, args)
            // 獲取 Observer 對象實例
            const ob = this.__ob__
            // 通過 Observer 對象實例上 Dep 實例對象去通知依賴進行更新
            ob.dep.notify()
        },
        eumerable: false,
        writable: false,
        configurable: true
    })
})

因為 Vue2 的實現方法決定了在 Vue2 中對數組的一些操作無法實現響應式操作,例如:

this.list[0] = xxx

由於 Vue2 放棄了 Object.defineProperty 對數組進行監聽的方案,所以通過下標操作數組是無法實現響應式操作的。

又例如:

this.list.length = 0

這個動作在 Vue2 中也是無法實現響應式操作的。

問題5:Vue3 的響應式原理是怎麼樣的?

Vue3 是通過 Proxy 對數據實現 getter/setter 代理,從而實現響應式數據,然後在副作用函數中讀取響應式數據的時候,就會觸發 Proxy 的 getter,在 getter 裡面把對當前的副作用函數保存起來,將來對應響應式數據發生更改的話,則把之前保存起來的副作用函數取出來執行。

具體是副作用函數裡面讀取響應式對象的屬性值時,會觸發代理對象的 getter,然後在 getter 裡面進行一定規則的依賴收集保存操作。

簡單代碼實現:

// 使用一個全局變數存儲被註冊的副作用函數
let activeEffect
// 註冊副作用函數
function effect(fn) {
    activeEffect = fn
    fn()
}
const obj = new Proxy(data, {
    // getter 攔截讀取操作
    get(target, key) {
        // 將副作用函數 activeEffect 添加到存儲副作用函數的全局變數 targetMap 中
        track(target, key)
        // 返回讀取的屬性值
        return Reflect.get(target, key)
    },
    // setter 攔截設置操作
    set(target, key, val) {
        // 設置屬性值
        const result = Reflect.set(target, key, val)
        // 把之前存儲的副作用函數取出來並執行
        trigger(target, key)
        return result
    }
})
// 存儲副作用函數的全局變數
const targetMap = new WeakMap()
// 在 getter 攔截器內追蹤依賴的變化
function track(target, key) {
    // 沒有 activeEffect,直接返回
    if(!activeEffect) return
    // 根據 target 從全局變數 targetMap 中獲取 depsMap
    let depsMap = targetMap.get(target)
    if(!depsMap) {
       // 如果 depsMap 不存,那麼需要新建一個 Map 並且與 target 關聯
       depsMap = new Map()
       targetMap.set(target, depsMap)
    }
    // 再根據 key 從 depsMap 中取得 deps, deps 裡面存儲的是所有與當前 key 相關聯的副作用函數
    let deps = depsMap.get(key)
    if(!deps) {
       // 如果 deps 不存在,那麼需要新建一個 Set 並且與 key 關聯
       deps = new Set()
       depsMap.set(key, deps)
    }
    // 將當前的活動的副作用函數保存起來
    deps.add(activeEffect)
}
// 在 setter 攔截器中觸發相關依賴
function trgger(target, key) {
    // 根據 target 從全局變數 targetMap 中取出 depsMap
    const depsMap = targetMap.get(target)
    if(!depsMap) return
    // 根據 key 取出相關聯的所有副作用函數
    const effects = depsMap.get(key)
    // 執行所有的副作用函數
    effects && effects.forEach(fn => fn())
}

通過上面的代碼我們可以知道 Vue3 中依賴收集的規則,首先把響應式對象作為 key,一個 Map 的實例做為值方式存儲在一個 WeakMap 的實例中,其中這個 Map 的實例又是以響應式對象的 key 作為 key, 值為一個 Set 的實例為值。而且這個 Set 的實例中存儲的則是跟那個響應式對象 key 相關的副作用函數。

來看看圖表表示的結構:

那麼為什麼 Vue3 的依賴收集的數據結構這裡採用 WeakMap 呢?

所以我們需要解析一下 WeakMap 和 Map 的區別,首先 WeakMap 是可以接受一個對象作為 key 的,而 WeakMap 對 key 是弱引用的。所以當 WeakMap 的 key 是一個對象時,一旦上下文執行完畢,WeakMap 中 key 對象沒有被其他代碼引用的時候,垃圾回收器 就會把該對象從記憶體移除,我們就無法該對象從 WeakMap 中獲取內容了。

另外副作用函數使用 Set 類型,是因為 Set 類型能自動去除重覆內容。

上述方法只實現了對引用類型的響應式處理,因為 Proxy 的代理目標必須是非原始值。原始值指的是 Boolean、Number、BigInt、String、Symbol、undefined 和 null 等類型的值。在 JavaScript 中,原始值是按值傳遞的,而非按引用傳遞。這意味著,如果一個函數接收原始值作為參數,那麼形參與實參之間沒有引用關係,它們是兩個完全獨立的值,對形參的修改不會影響實參。

Vue3 中是通過對原始值做了一層包裹的方式來實現對原始值變成響應式數據的。最新的 Vue3 實現方式是通過屬性訪問器 getter/setter 來實現的。

class RefImpl{
    private _value
    public dep
    // 表示這是一個 Ref 類型的響應式數據
    private _v_isRef = true
    constructor(value) {
        this._value = value
        // 依賴存儲
        this.dep = new Set()
    }
	// getter 訪問攔截
    get value() {
        // 依賴收集
        trackRefValue(this)
        return this._value
    }
	// setter 設置攔截
    set value(newVal) {
        this._value = newVal
        // 觸發依賴
        triggerEffect(this.dep)   
    }
}

ref 本質上是一個實例化之後的 “包裹對象”,因為 Proxy 無法提供對原始值的代理,所以我們需要使用一層對象作為包裹,間接實現原始值的響應式方案。 由於實例化之後的 “包裹對象” 本質與普通對象沒有任何區別,所以為了區分 ref 與 Proxy 響應式對象,我們需要給 ref 的實例對象定義一個 _v_isRef 的標識,表明這是一個 ref 的響應式對象。

最後我們和 Vue2 進行一下對比,我們知道 Vue2 的響應式存在很多的問題,例如:

  • 初始化時需要遍歷對象所有 key,如果對象層次較深,性能不好
  • 通知更新過程需要維護大量 dep 實例和 watcher 實例,額外占用記憶體較多
  • 無法監聽到數組元素的變化,只能通過劫持重寫了幾個數組方法
  • 動態新增,刪除對象屬性無法攔截,只能用特定 set/delete API 代替
  • 不支持 Map、Set 等數據結構

而 Vue3 使用 Proxy 實現之後,則以上的問題都不存在了。

問題6:Vue3 中是怎麼監測數組的變化?

我們知道在 Vue2 中是需要對數組的監聽進行特殊的處理的,其中在 Vue3 中也需要對數組進行特殊的處理。在 Vue2 是不可以通過數組下標對響應式數組進行設置和讀取的,而 Vue3 中是可以的,數組中仍然有很多其他特別的讀取和設置的方法,這些方法沒經過特殊處理,是無法通過普通的 Proxy 中的 getter/setter 進行響應式處理的。

數組中對屬性或元素進行讀取的操作方法。

  • 通過索引訪問數組的元素值
  • 訪問數組的長度
  • 把數組作為對象,使用 for ... in 迴圈遍歷
  • 使用 for ... of 迭代遍曆數組
  • 數組的原型方法,如 concat、join、every、some、find、findIndex、includes 等

數組中對屬性或元素進行設置的操作方法。

  • 通過索引修改數組的元素值
  • 修改數組的長度
  • 數組的棧方法
  • 修改原數組的原型方法:splice、fill、sort 等

當上述的數組的讀取或設置的操作發生時,也應該正確地建立響應式聯繫或觸發響應。

當通過索引設置響應式數組的時候,有可能會隱式修改數組的 length 屬性,例如設置的索引值大於數組當前的長度時,那麼就要更新數組的 length 屬性,因此在觸發當前的修改屬性的響應之外,也需要觸發與 length 屬性相關依賴進行重新執行。

遍曆數組,使用 for ... in 迴圈遍曆數組與遍歷常規對象是一致的,也可以使用 ownKeys 攔截器進行設置。而影響 for ... in 迴圈對數組的遍歷會是添加新元素:arr[0] = 1 或者修改數組長度:arr.length = 0,其實無論是為數組添加新元素,還是直接修改數組的長度,本質上都是因為修改了數組的 length 屬性。所以在 ownKeys 攔截器內進行判斷,如果是數組的話,就使用 length 屬性作為 key 去建立響應聯繫。

在 Vue3 中也需要像 Vue2 那樣對一些數組原型上方法進行重寫。

當數組響應式對象使用 includes、indexOf、lastIndexOf 這方法的時候,它們內部的 this 指向的是代理對象,並且在獲取數組元素時得到的值要也是代理對象,所以當使用原始值去數組響應式對象中查找的時候,如果不進行特別的處理,是查找不到的,所以我們需要對上述的數組方法進行重寫才能解決這個問題。

首先 arr.indexOf 可以理解為讀取響應式對象 arr 的 indexOf 屬性,這就會觸發 getter 攔截器,在 getter 攔截器內我們就可以判斷 target 是否是數組,如果是數組就看讀取的屬性是否是我們需要重寫的屬性,如果是,則使用我們重新之後的方法。

const arrayInstrumentations = {}
;(['includes', 'indexOf', 'lastIndexOf']).forEach(key => {
  const originMethod = Array.prototype[key]
  arrayInstrumentations[key] = function(...args) {
    // this 是代理對象,先在代理對象中查找
    let res = originMethod.apply(this, args)

    if(res === false) {
       // 在代理對象中沒找到,則去原始數組中查找
       res = originMethod.apply(this.raw, args)
    }
    // 返回最終的值
    return res
  }
})

上述重寫方法的主要是實現先在代理對象中查找,如果沒找到,就去原始數組中查找,結合兩次的查找結果才是最終的結果,這樣就實現了在代理數組中查找原始值也可以查找到。

在一些數組的方法中除了修改數組的內容之外也會隱式地修改數組的長度。例如下麵的例子:

我們可以看到我們只是進行 arr.push 的操作卻也觸發了 getter 攔截器,並且觸發了兩次,其中一次就是數組 push 屬性的讀取,還有一次是什麼呢?還有一次就是調用 push 方法會間接讀取 length 屬性,那麼問題來了,進行了 length 屬性的讀取,也就會建立 length 的響應依賴,可 arr.push 本意只是修改操作,並不需要建立 length 屬性的響應依賴。所以我們需要 “屏蔽” 對 length 屬性的讀取,從而避免在它與副作用函數之間建立響應聯繫。

相關代碼實現如下:

const arrayInstrumentations = {}
// 是否允許追蹤依賴變化
let shouldTrack = true
// 重寫數組的 push、pop、shift、unshift、splice 方法
;['push','pop','shift', 'unshift', 'splice'].forEach(method => {
    // 取得原始的數組原型上的方法
    const originMethod = Array.prototype[method]
    // 重寫
    arrayInstrumentations[method] = function(...args) {
        // 在調用原始方法之前,禁止追蹤
        shouldTrack = false
        // 調用數組的預設方法
        let res = originMethod.apply(this, args)
        // 在調用原始方法之後,恢復允許進行依賴追蹤
        shouldTrack = true
        return res
    }
})

在調用數組的預設方法間接讀取 length 屬性之前,禁止進行依賴跟蹤,這樣在間接讀取 length 屬性時,由於是禁止依賴跟蹤的狀態,所以 length 屬性與副作用函數之間不會建立響應聯繫。

總結

本文通過一個一個問題的方式,分別解答 Vue2 和 Vue3 的響應式實現原理。

Vue2 部分

Vue2 是通過 Object.defineProperty 將對象的屬性轉換成 getter/setter 的形式來進行監聽它們的變化,當讀取屬性值的時候會觸發 getter 進行依賴收集,當設置對象屬性值的時候會觸發 setter 進行向相關依賴發送通知,從而進行相關操作。

由於 Object.defineProperty 只對屬性 key 進行監聽,無法對引用對象進行監聽,所以在 Vue2 中創建一個了 Observer 類對整個對象的依賴進行管理,當對響應式對象進行新增或者刪除則由響應式對象中的 dep 通知相關依賴進行更新操作。

Object.defineProperty 也可以實現對數組的監聽的,但因為性能的原因 Vue2 放棄了這種方案,改由重寫數組原型對象上的 7 個能操作數組內容的變更的方法,從而實現對數組的響應式監聽。

Vue3 部分

Vue3 則是通過 Proxy 對數據實現 getter/setter 代理,從而實現響應式數據,然後在副作用函數中讀取響應式數據的時候,就會觸發 Proxy 的 getter,在 getter 裡面把對當前的副作用函數保存起來,將來對應響應式數據發生更改的話,則把之前保存起來的副作用函數取出來執行。

Vue3 對數組實現代理時,用於代理普通對象的大部分代碼可以繼續使用,但由於對數組的操作與對普通對象的操作存在很多的不同,那麼也需要對這些不同的操作實現正確的響應式聯繫或觸發響應。這就需要對數組原型上的一些方法進行重寫。

比如通過索引為數組設置新的元素,可能會隱式地修改數組的 length 屬性的值。同時如果修改數組的 length 屬性的值,也可能會間接影響數組中的已有元素。另外用戶通過 includes、indexOf 以及 lastIndexOf 等對數組元素進行查找時,可能是使用代理對象進行查找,也有可能使用原始值進行查找,所以我們就需要重寫這些數組的查找方法,從而實現用戶的需求。原理很簡單,當用戶使用這些方法查找元素時,先去響應式對象中查找,如果沒找到,則再去原始值中查找。

另外如果使用 push、pop、shift、unshift、splice 這些方法操作響應式數組對象時會間接讀取和設置數組的 length 屬性,所以我們也需要對這些數組的原型方法進行重新,讓當使用這些方法間接讀取 length 屬性時禁止進行依賴追蹤,這樣就可以斷開 length 屬性與副作用函數之間的響應式聯繫了。

最後,其實在 Vue3 部分還有 WeakMap、Map、Set 這部分的響應式原理還沒進行講解,日後有時間再進行補寫。

本文轉載於:

https://juejin.cn/post/7124351370521477128

如果對您有所幫助,歡迎您點個關註,我會定時更新技術文檔,大家一起討論學習,一起進步。

 


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

-Advertisement-
Play Games
更多相關文章
  • 2022年7月26日,Taier1.2版本正式發佈! 本次版本發佈更新功能: 新增工作流 新增OceanBase SQL 新增Flink jar任務 數據同步、實時採集支持臟數據管理 Hive UDF 控制台UI升級 租戶綁定簡化 新版本的使用文檔已在社區中推送,大家可以隨時下載查閱,歡迎大家體驗新 ...
  • 現如今 Redis 變得越來越流行,幾乎在很多項目中都要被用到,不知道你在使用 Redis 時,有沒有思考過,Redis 到底是如何穩定、高性能地提供服務的? 我使用 Redis 的場景很簡單,只使用單機版 Redis 會有什麼問題嗎? 我的 Redis 故障宕機了,數據丟失了怎麼辦?如何能保證我的... ...
  • 實戰案例 1.搭建mysql服務 下載mysql [root@localhost ~]# wget http://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm [root@localhost ~]# rpm -Uvh my ...
  • 四大組件 Activity 實現步驟 繼承 Activity 或其子類,實現以下方法: //第一次創建時回調 protected void onCreate(Bundle savedInstanceState); //啟動時回調 protected void onStart(); //再次啟動時回調 ...
  • 前言: ​ 從64位開始,iOS引入了Tagged Pointer技術,用於優化NSNumber、NSDate、NSString等小對象的存儲。 Tagged Pointer主要為瞭解決兩個問題: 記憶體資源浪費,堆區需要額外的開闢空間 訪問效率,每次set/get都需要訪問堆區,浪費時間, 而且需要 ...
  • HMS Core音頻編輯服務(Audio Editor Kit)6.6.0版本上線,新增歌聲合成能力。通過歌詞和曲調,結合不同的曲風讓機器也能生成真實度極高的歌聲。支持字級別輸入歌詞進行音素轉換,生成對應歌詞的歌聲,可靈活調整音高、滑音、呼吸音、顫音等細節參數,讓歌聲更真實。 歌聲合成服務可廣泛應用 ...
  • HTML 一、認識HTML 什麼是HTML? HTML 是用來描述網頁的一種語言 HTML 指的是超文本標記語言: HyperText Markup Language HTML 不是一種編程語言,而是一種標記語言 標記語言是一套標記標簽 (markup tag) HTML 使用標記標簽來描述網頁 H ...
  • 本文摘要:主要通過實操講解運用Webpack 5 CSS常用配置的方法步驟 前文已談到可以通過配置 css-loader 和 style-loader,使 webpack5 具有處理 CSS 資源的能力。css-loader 首先會分析出各個 CSS文件之間的關係,把各個CSS文件合併為一大段 CS ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...