之前寫了兩篇vue2.0的響應式原理,鏈接在此,對響應式原理不清楚的請先看下麵兩篇 和尤雨溪一起進階vue 和尤雨溪一起進階vue(二) 現在來寫一個簡單的3.0的版本吧 大家都知道,2.0的響應式用的是Object.defineProperty,結合發佈訂閱模式實現的,3.0已經用Proxy改寫了 ...
之前寫了兩篇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瀏覽器不支持
本文的文字及圖片來源於網路加上自己的想法,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處理