博客 / 詳情

返回

初識VUE響應式原理

作者:京東零售 吳靜

自從Vue發佈以來,就受到了廣大開發人員的青睞,提到Vue,我們首先想到的就是Vue的響應式系統,那響應式系統到底是怎麼回事呢?接下來我就給大家簡單介紹一下Vue中的響應式原理。

vue2的響應式原理

儘管Vue2將於2023年12月31日停止維護,但是我們依然有很多項目是基於Vue2.X進行開發的,那麼我們先簡單看一看Vue2.X是基於什麼實現的吧~

Object.defineProperty

Vue2的響應式原理是基於對象的defineProperty()方法進行開發的,那麼這個方法有什麼作用呢?MDN是這樣介紹的:
**

object.defineProperty()方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。

也就是説,我們可以通過對象的這個方法精確的添加或者修改對象的屬性。每個對象都具有get/set屬性,當訪問get屬性時,會調用getter方法,當對象的屬性值被修改時,會調用setter方法,正式基於getter和setter方法,Vue才可以利用Object.defineProperty來實現響應式系統。

Object.defineProperty在Vue中的使用

在vue中,當把一個普通的JavaScript對象傳入Vue實例作為data選項,Vue會遍歷此對象的所有屬性,並使用object.defineProperty將這些屬性轉為getter/setter,

getter/setter可以追蹤依賴,在屬性被訪問的時候通知視圖變更。

Object.defineProperty(obj, 'targetObj', {
   get() {
     // 完成依賴收集
   },
   set() {
      // 發生變更,同時通知相關依賴
   }
})

vue3的響應式原理

vue2.0很好的實現了數據的雙向綁定,但是也遺留了一個很重要的問題:由於Vue會在初始化實例時將property轉化為getter/setter,所以,property必須在data對象上先存在才能讓Vue將其轉換為響應式數據。那麼對於新增加的對象、或者某些需要特殊操作的數組想要轉換為響應式數據就需要使用Vue.set等方法。

Vue3就很好的解決了這個問題。那麼,Vue3是如何解決的呢?讓我們就一起看看吧~

Proxy

提到Vue3的數據攔截,我們首先要了解什麼是proxy?

Proxy 可以理解成,在目標對象之前架設一層“攔截”,外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy 這個詞的原意是代理,用在這裏表示由它來“代理”某些操作,可以譯為“代理器”。

原來,Vue3用了Proxy代理代替了Object.defineProperty方法。同樣的,在proxy中也有get/set方法,舉個例子~

var obj = new Proxy({}, {
  get: function (target, name) {
    return name;
  },
  set: function (target, key, val) {
    target[key] = val
    return target;
  }
});

我們通過給每一個目標對象都建立一個對應的Proxy對象對其代理就可以彌補Object.defineProperty對於新增對象無法監聽的缺陷。

簡單設計一個Vue3的響應系統

實現一個簡單的響應系統的思路:

•讀取(get)時,將副作用函數入棧;

•設置(set)時,將副作用函數出棧,執行副作用函數。

// 存儲副作用函數的棧
const bucket = new Set()

// 存儲被註冊的副作用函數
let activeEffect

// 註冊副作用函數
function effect (fn) {
    // 存儲副作用函數
    activeEffect = fn
    fn()
}

// 副作用函數fn
effect (
    () => {
        document.body.innerText = obj.text
    }
)

執行匿名函數fn方法時,會觸發響應式數據obj.text的讀取操作,進而觸發代理對象Proxy的get攔截函數:

const Proxy = new Proxy(data, {
    get (target, key) {
        if (activeEffect) {
            bucket.add(activeEffect)
        }
        return target[key]
    },
    set (target, key, newVal) {
        target[key] = newVal
        bucket.forEach(fn => fn())
        return true
    }
})

到此,我們會發現,有一個疑問,我們怎樣能保證修改一個屬性之後觸發的副作用函數是我預期想要觸發的副作用函數呢?為了解決這個問題,我們還需要建立副作用函數與目標對象的聯繫:

我們僅需要用WeakMap代替Set數據結構:

const bucket = new WeakMap()

修改Proxy對象:

const Proxy = new Proxy(data, { 
    get (target, key) { 
        if (!activeEffect) return target[key]
        // 先從棧中取出depsMap,depsMap中保存目標對象和其相關副作用函數的一對多的關係        
        let depsMap = bucket.get(target)
        if (!depsMap) {
            bucket.set(target, (depsMap = new Map())
        }
        // 再根據key從depsMap中取得deps,deps保存所有與key相關聯的副作用函數
        let deps = depsMap.get(key)
        if (!deps) {
            depsMap.set(key, (deps = new Set())
        }
        deps.add(activeEffect)
        
        return target[key] 
    }, 
    set (target, key, newVal) { 
        target[key] = newVal 
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const effects = depsMap.get(key)
        effects && effects.forEach(fn => fn())  
    } 
})

這樣,我們就實現了一個簡易的響應系統。那麼為什麼要用weakMap而不是使用Map呢?就交給大家一起思考啦~

參考文獻

《Vue.js設計與實現_霍春陽》

《ECMAScript 6入門》-阮一峯

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.