之前寫了兩篇vue2.0的響應式原理,鏈接在此,對響應式原理不清楚的請先看下面兩篇
現在來寫一個簡單的3.0的版本吧
大家都知道,2.0的響應式用的是Object.defineProperty
,結合發布訂閱模式實現的,3.0已經用Proxy
改寫了
Proxy是es6提供的新語法,Proxy 對象用於定義基本操作的自定義行為(如屬性查找、賦值、枚舉、函數調用等)。
語法:
const p = new Proxy(target, handler)
target 要使用 Proxy 包裝的目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理)。
handler 一個通常以函數作為屬性的對象,各屬性中的函數分別定義了在執行各種操作時代理 p 的行為。
handler的方法有很多, 感興趣的可以移步到MDN,這里重點介紹下面幾個
handler.has()
in 操作符的捕捉器。 handler.get() 屬性讀取操作的捕捉器。 handler.set() 屬性設置操作的捕捉器。 handler.deleteProperty() delete 操作符的捕捉器。 handler.ownKeys() Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。 復制代碼
基於上面的知識,我們來攔截一個對象屬性的取值,賦值和刪除
// version1 const handler = { get(target, key, receiver) { console.log('get', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('set', key, value) let res = Reflect.set(target, key, value, receiver) return res }, deleteProperty(target, key) { console.log('deleteProperty', key) Reflect.deleteProperty(target, key) } } // 測試部分 let obj = { name: 'hello', info: { age: 20 } } const proxy = new Proxy(obj, handler) // get name hello // hello console.log(proxy.name) // set name world proxy.name = 'world' // deleteProperty name delete proxy.name 我是08年出道的前端老鳥,想交流經驗可以進我的扣扣裙 519293536 有問題我都會盡力幫大家
上面已經可以攔截到對象屬性的取值,賦值和刪除了,我們來看看新增一個屬性可否攔截
proxy.height = 20
// 打印 set height 20 復制代碼
成功攔截!! 我們知道vue2.0新增data上不存在的屬性是不可以響應的,需要手動調用$set
的,這就是Proxy
的優點之一
現在來看看嵌套對象的攔截,我們修改info屬性的age屬性
proxy.info.age = 30 // 打印 get info 復制代碼
只可以攔截到info,不可以攔截到info的age屬性,所以我們要遞歸了,問題是在哪里遞歸呢?
因為調用proxy.info.age會先觸發proxy.info的攔截,所以我們可以在get中攔截,如果proxy.info是對象的話,對象需要再被代理一次,我們把代碼封裝一下,寫成遞歸的形式
function reactive(target) { return createReactiveObject(target) } function createReactiveObject(target) { // 遞歸結束條件 if(!isObject(target)) return target const handler = { get(target, key, receiver) { console.log('get', key) let res = Reflect.get(target, key, receiver) // res如果是對象,那么需要繼續代理 return isObject(res) ? createReactiveObject(res): res }, set(target, key, value, receiver) { console.log('set', key, value) let res = Reflect.set(target, key, value, receiver) return res }, deleteProperty(target, key) { console.log('deleteProperty', key) Reflect.deleteProperty(target, key) } } return new Proxy(target, handler) } function isObject(obj) { return obj != null && typeof obj === 'object' } // 測試部分 let obj = { name: 'hello', info: { age: 20 } } const proxy = reactive(obj) proxy.info.age = 30 復制代碼
運行上面的代碼,打印結果
get info
set age 30 復制代碼
Bingo! 嵌套對象攔截到了
vue2.0用的是Object.defineProperty攔截對象的getter和setter,一次將對象遞歸到底, 3.0用Proxy,是惰性遞歸的,只有訪問到某個屬性,確定了值是對象,我們才繼續代理下去這個屬性值,因此性能更好
現在我們來測試數組的方法,看看能否攔截到,以push方法為例, 測試部分代碼如下
let arr = [1, 2, 3] const proxy = reactive(arr) proxy.push(4) 復制代碼
打印結果
get push
get length
set 3 4 set length 4 復制代碼
和預期有點不太一樣,調用數組的push方法,不僅攔截到了push, 還攔截到了length屬性,set被調用了兩次,在set中我們是要更新視圖的,我們做了一次push操作,卻觸發了兩次更新,顯然是不合理的,所以我們這里需要修改我們的handler的set函數,區分一下是新增屬性還是修改屬性,只有這兩種情況才需要更新視圖
set函數修改如下
set(target, key, value, receiver) {
console.log('set', key, value) let oldValue = target[key] let res = Reflect.set(target, key, value, receiver) let hadKey = target.hasOwnProperty(key) if(!hadKey) { // console.log('新增屬性', key) // 更新視圖 }else if(oldValue !== value) { // console.log('修改屬性', key) // 更新視圖 } return res } 復制代碼
至此,我們對象操作的攔截我們基本已經完成了,但是還有一個小問題, 我們來看看下面的操作
let obj = { some: 'hell' } let proxy = reactive(obj) let proxy1 = reactive(obj) let proxy2 = reactive(obj) let proxy3 = reactive(obj) let p1 = reactive(proxy) let p2 = reactive(proxy) let p3 = reactive(proxy) 復制代碼
我們這樣寫,就會一直調用reactive代理對象,所以我們需要構造兩個hash表來存儲代理結果,避免重復代理
function reactive(target) { return createReactiveObject(target) } let toProxyMap = new WeakMap() let toRawMap = new WeakMap() function createReactiveObject(target) { let dep = new Dep() if(!isObject(target)) return target // reactive(obj) // reactive(obj) // reactive(obj) // target已經代理過了,直接返回,不需要再代理了 if(toProxyMap.has(target)) return toProxyMap.get(target) // 防止代理對象再被代理 // reactive(proxy) // reactive(proxy) // reactive(proxy) if(toRawMap.has(target)) return target const handler = { get(target, key, receiver) { let res = Reflect.get(target, key, receiver) // 遞歸代理 return isObject(res) ? reactive(res) : res }, // 必須要有返回值,否則數組的push等方法報錯 set(target, key, val, receiver) { let hadKey = hasOwn(target, key) let oldVal = target[key] let res = Reflect.set(target, key, val,receiver) if(!hadKey) { // console.log('新增屬性', key) } else if(oldVal !== val) { // console.log('修改屬性', key) } return res }, deleteProperty(target, key) { Reflect.deleteProperty(target, key) } } let observed = new Proxy(target, handler) toProxyMap.set(target, observed) toRawMap.set(observed, target) return observed } function isObject(obj) { return obj != null && typeof obj === 'object' } function hasOwn(obj, key) { return obj.hasOwnProperty(key) } 復制代碼
接下來就是修改數據,觸發視圖更新,也就是實現發布訂閱,這一部分和2.0的實現部分一樣,也是在get中收集依賴,在set中觸發依賴
完整代碼如下
class Dep { constructor() { this.subscribers = new Set(); // 保證依賴不重復添加 } // 追加訂閱者 depend() { if(activeUpdate) { // activeUpdate注冊為訂閱者 this.subscribers.add(activeUpdate) } } // 運行所有的訂閱者更新方法 notify() { this.subscribers.forEach(sub => { sub(); }) } } let activeUpdate function reactive(target) { return createReactiveObject(target) } let toProxyMap = new WeakMap() let toRawMap = new WeakMap() function createReactiveObject(target) { let dep = new Dep() if(!isObject(target)) return target // reactive(obj) // reactive(obj) // reactive(obj) // target已經代理過了,直接返回,不需要再代理了 if(toProxyMap.has(target)) return toProxyMap.get(target) // 防止代理對象再被代理 // reactive(proxy) // reactive(proxy) // reactive(proxy) if(toRawMap.has(target)) return target const handler = { get(target, key, receiver) { let res = Reflect.get(target, key, receiver) // 收集依賴 if(activeUpdate) { dep.depend() } // 遞歸代理 return isObject(res) ? reactive(res) : res }, // 必須要有返回值,否則數組的push等方法報錯 set(target, key, val, receiver) { let hadKey = hasOwn(target, key) let oldVal = target[key] let res = Reflect.set(target, key, val,receiver) if(!hadKey) { // console.log('新增屬性', key) dep.notify() } else if(oldVal !== val) { // console.log('修改屬性', key) dep.notify() } return res }, deleteProperty(target, key) { Reflect.deleteProperty(target, key) } } let observed = new Proxy(target, handler) toProxyMap.set(target, observed) toRawMap.set(observed, target) return observed } function isObject(obj) { return obj != null && typeof obj === 'object' } function hasOwn(obj, key) { return obj.hasOwnProperty(key) } function autoRun(update) { function wrapperUpdate() { activeUpdate = wrapperUpdate update() // wrapperUpdate, 閉包 activeUpdate = null; } wrapperUpdate(); } let obj = {name: 'hello', arr: [1, 2,3]} let proxy = reactive(obj) // 響應式 autoRun(() => { console.log(proxy.name) })
我是08年出道的前端老鳥,想交流經驗可以進我的扣扣裙 519293536 有問題我都會盡力幫大家
proxy.name = 'xxx' // 修改proxy.name, 自動執行autoRun的回調函數,打印新值 復制代碼
最后總結下vue2.0和3.0響應式的實現的優缺點:
- 性能 : 2.0用
Object.defineProperty
攔截對象的屬性的修改,在getter中收集依賴,在setter中觸發依賴更新,一次將對象遞歸到底攔截,性能較差, 3.0用Proxy
攔截對象,惰性遞歸,性能好 Proxy
可以攔截數組的方法,Object.defineProperty
無法攔截數組的push
,unshift
,shift
,pop
,slice
,splice
等方法(2.0內部重寫了這些方法,實現了攔截), proxy可以攔截攔截對象的新增屬性,Object.defineProperty
不可以(開發者需要手動調用$set
)- 兼容性 :
Object.defineProperty
支持ie8+,Proxy
的兼容性差,ie瀏覽器不支持
本文的文字及圖片來源於網絡加上自己的想法,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯系我們以作處理