之前寫了兩篇vue2.0的回應式原理,鏈接在此,對回應式原理不清楚的請先看下面兩篇
和尤雨溪一起進階vue
和尤雨溪一起進階vue(二)
現在來寫一個簡單的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 = https://www.cnblogs.com/chengxuyuanaa/p/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瀏覽器不支持
本文的文字及圖片來源于網路加上自己的想法,僅供學習、交流使用,不具有任何商業用途,著作權歸原作者所有,如有問題請及時聯系我們以作處理
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/69819.html
標籤:JavaScript
