記錄--六道題理解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 [[email protected] ~]# wget http://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm [[email protected] ~]# 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
  • Github / Gitee QQ群(1群) : 813100564 / QQ群(2群) : 579033769 視頻教學 介紹 MiniWord .NET Word模板引擎,藉由Word模板和數據簡單、快速生成文件。 Getting Started 安裝 nuget link : https:// ...
  • Array.Sort Array類中相當實用的我認為是Sort方法,相比起冗長的冒泡排序,它的出現讓排序更加的簡化 結果如下: 還可以聲明一個靜態方法用來專門調用指定數組排序,從名為 array 的一維數組中 a 索引處開始,到 b 元素 從小到大排序。 註意: a + b 不能大於 array 的 ...
  • 前言 在上一篇文章CLR類型系統概述里提到,當運行時掛起時, 垃圾回收會執行堆棧遍歷器(stack walker)去拿到堆棧上值類型的大小和堆棧根。這裡我們來翻譯BotR里一篇專門介紹Stackwalking的文章,希望能加深理解。 順便說一句,StackWalker在中文里似乎還沒有統一的翻譯,J ...
  • 使用過 nginx 的小伙伴應該都知道,這個中間件是可以設置跨域的,作為今天的主角,同樣的 反向代理中間件的 YARP 毫無意外也支持了跨域請求設置。 有些小伙伴可能會問了,怎樣才算是跨域呢? 在 HTML 中,一些標簽,例如 img、a 等,還有我們非常熟悉的 Ajax,都是可以指向非本站的資源的 ...
  • 什麼是Git Git 是一個開源的分散式版本控制系統,用於敏捷高效地處理任何或小或大的項目。 Git 是 Linus Torvalds 為了幫助管理 Linux 內核開發而開發的一個開放源碼的版本控制軟體。 Git 與常用的版本控制工具 CVS, Subversion 等不同,它採用了分散式版本庫的 ...
  • 首先CR3是什麼,CR3是一個寄存器,該寄存器內保存有頁目錄表物理地址(PDBR地址),其實CR3內部存放的就是頁目錄表的記憶體基地址,運用CR3切換可實現對特定進程記憶體地址的強制讀寫操作,此類讀寫屬於有痕讀寫,多數驅動保護都會將這個地址改為無效,此時CR3讀寫就失效了,當然如果能找到CR3的正確地址... ...
  • 說明 onlyoffice為一款開源的office線上編輯組件,提供word/excel/ppt編輯保存操作 以下操作均基於centos8系統,officeonly鏡像版本7.1.2.23 鏡像下載地址:https://yunpan.360.cn/surl_y87CKKcPdY4 (提取碼:1f92 ...
  • 二叉樹查找指定的節點 前序查找的思路 1.先判斷當前節點的no是否等於要查找的 2.如果是相等,則返回當前節點 3.如果不等,則判斷當前節點的左子節點是否為空,如果不為空,則遞歸前序查找 4.如果左遞歸前序查找,找到節點,則返回,否繼續判斷,當前的節點的右子節點是否為空,如果不為空,則繼續向右遞歸前 ...
  • ##Invalid bound statement (not found)出現原因和解決方法 ###前言: 想必各位小伙伴在碼路上經常會碰到奇奇怪怪的事情,比如出現Invalid bound statement (not found),那今天我就來分析以下出現此問題的原因。 其實出現這個問題實質就是 ...
  • ###一、背景知識 爬蟲的本質就是一個socket客戶端與服務端的通信過程,如果我們有多個url待爬取,只用一個線程且採用串列的方式執行,那隻能等待爬取一個結束後才能繼續下一個,效率會非常低。 需要強調的是:對於單線程下串列N個任務,並不完全等同於低效,如果這N個任務都是純計算的任務,那麼該線程對c ...