專欄分享:vue2源碼專欄,vue3源碼專欄,vue router源碼專欄,玩具項目專欄,硬核??推薦??
歡迎各位ITer關(guān)注點贊收藏??????
背景
以下是柏成根據(jù)Vue3官方課程整理的響應(yīng)式書面文檔 - 第二節(jié),課程鏈接在此:Proxy and Reflect - Vue 3 Reactivity | Vue Mastery
本篇文章將解決 上一篇文章 結(jié)尾遺留的問題:如何讓代碼自動實現(xiàn)響應(yīng)性? 換句話說就是,如何讓我們的 effect
自動保存 & 自動重新運行?
在 上一篇文章 中,我們最終運行的代碼長這樣
聰明的你會立馬發(fā)現(xiàn),我們現(xiàn)在仍要手動調(diào)用 track()
來保存 effect
;手動調(diào)用 trigger()
來運行 effects
,這不是脫褲子放屁么
我們想讓我們的響應(yīng)性引擎自動調(diào)用 track()
和 trigger()
。那么問題就來了,何時才是調(diào)用它們的最好時機呢?
從邏輯上來說,如果訪問了對象的屬性,就是我們調(diào)用 track()
去保存 effect
的最佳時機;如果對象的屬性改變了,就是我們調(diào)用 trigger()
來運行 effects
的最佳時機
所以問題變成了,我們該如何攔截對象屬性的訪問和賦值操作?
Proxy(代理)
在 MDN 上的 Proxy 對象是這樣定義的
Proxy 對象用于創(chuàng)建一個對象的代理,從而實現(xiàn)基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)。
也可以理解為在操作目標對象前架設(shè)一層代理,將所有本該我們手動編寫的程序交由代理來處理,生活中也有許許多多的“proxy”, 如代購,中介,因為他們所有的行為都不會直接觸達到目標對象
語法
-
target: 要使用 Proxy 包裝的目標對象(可以是任何類型的對象,包括原生數(shù)組,函數(shù),甚至另一個代理)
-
handler: 一個通常以函數(shù)作為屬性的對象,用來定制攔截行為;它包含有 Proxy 的各個捕獲器(trap),例如 handler.get() / handler.set()
const p = new Proxy(target, handler)
常用方法
比較常用的兩個方法就是 get()
和 set()
方法
方法 | 描述 |
---|---|
handler.get(target, key, ?receiver) | 屬性讀取操作的捕捉器 |
handler.set(target, key, value, ? receiver) | 屬性設(shè)置操作的捕捉器 |
handler.get
用于代理目標對象的屬性讀取操作,其接受三個參數(shù) handler.get(target, propKey, ?receiver)
- target: 目標對象
- key: 屬性名
- receiver: Proxy 本身或者繼承它的對象,后面會重點介紹
舉個栗子
const origin = {}
const obj = new Proxy(origin, {
get: function (target, key, receiver) {
return 10
}
})
obj.a // 10
obj.b // 10
origin.a // undefined
origin.b // undefined
在這個栗子中,我們給一個空對象 origin
的 get 架設(shè)了一層代理,所有 get 操作都會直接返回我們定制的數(shù)字10
需要注意的是,代理只會對 proxy 對象生效,如訪問上方的 origin 對象就沒有任何效果
handler.set
用于代理目標對象的屬性設(shè)置操作,其接受四個參數(shù) handler.set(target, key, value, ?receiver)
- target: 目標對象
- key: 屬性名
- value: 新屬性值
- receiver: Proxy 本身或者繼承它的對象,后面會重點介紹
const obj = new Proxy({}, {
set: function(target, key, value, receiver) {
target[key] = value
console.log('property set: ' + key + ' = ' + value)
return true
}
})
'a' in obj // false
obj.a = 10 // "property set: a = 10"
'a' in obj // true
obj.a // 10
Reflect(反射)
在 MDN 上的 Reflect 對象是這樣定義的
Reflect 是一個內(nèi)建的對象,用來提供方法去攔截 JavaScript的操作。Reflect 不是一個函數(shù)對象,所以它是不可構(gòu)造的,也就是說你不能通過 new操作符去新建一個 Reflect對象或者將 Reflect對象作為一個函數(shù)去調(diào)用。Reflect的所有屬性和方法都是靜態(tài)的(就像Math對象)
常用方法
Reflect對象掛載了很多靜態(tài)方法,所謂靜態(tài)方法,就是和 Math.round() 這樣,不需要 new 就可以直接使用的方法。
比較常用的兩個方法就是 get()
和 set()
方法:
方法 | 描述 |
---|---|
Reflect.get(target, key, ?receiver) | 和 target[key] 類似,從對象中讀取屬性值 |
Reflect.set(target, key, value, ? receiver) | 和 target[key] = value 類似,給對象的屬性設(shè)置一個新值 |
Reflect.get()
Reflect.get方法允許你從一個對象中取屬性值,返回值是這個屬性值
Reflect.set()
Reflect.set 方法允許你在對象上設(shè)置屬性,返回值是 Boolean 值,代表是否設(shè)置成功
- target: 目標對象
- key: 屬性名
- value: 新屬性值
- receiver: 后面會重點介紹
Reflect.get(target, key[, receiver])
// 等同于
target[key]
Reflect.set(target, key, value[, receiver])
// 等同于
target[key] = value
舉個栗子
let product = {price: 5, quantity: 2}
// 以下三種方法是等效的
product.quantity
product['quantity']
Reflect.get(product, 'quantity')
// 以下三種方法是等效的
product.quantity = 3
product['quantity'] = 3
Reflect.set(product, 'quantity', 3)
關(guān)于receiver參數(shù)
在 Proxy 和 Reflect 對象中 get/set()
方法的最后一個參數(shù)都是 receiver
,它到底是個什么玩意?
receiver 是接受者的意思,譯為接收器
- 在 Proxy trap 的場景下(例如 handler.get() / handler.set()),
receiver
永遠指向 Proxy 本身或者繼承它的對象,比方說下面這個例子
let origin = { a: 1 }
let p = new Proxy(origin, {
get(target, key, receiver) {
return receiver
},
})
let child = Object.create(p)
p.getReceiver // Proxy {a: 1}
p.getReceiver === p // true
child.getReceiver // {}
child.getReceiver === child // true
- 在 Reflect.get / Reflect.set() 的場景下,
receiver
可以改變計算屬性中this
的指向
let target = {
firstName: 'li',
lastName: 'baicheng',
get a() {
return `${this.firstName}-${this.age}`
},
set b(val) {
console.log('>>>this', this)
this.firstName = val
},
}
Reflect.get(target, 'a') // li-undefined
Reflect.get(target, 'a', { age: 24 }) // undefined-24
Reflect.set(target, 'b', 'huawei', { age: 24 })
// >>>this {age: 24}
// true
搭配Proxy
在 Proxy 里使用 Reflect,我們會有一個附加參數(shù),稱為 receiver
(接收器),它將傳遞到我們的 Reflect調(diào)用中。它保證了當我們的對象有繼承自其它對象的值或函數(shù)時, this
指針能正確的指向?qū)ο螅@將避免一些我們在 vue2 中有的響應(yīng)式警告
let origin = { a: 1 }
let p = new Proxy(origin, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver)
},
})
Reflect對象經(jīng)常和Proxy代理一起使用,原因有三點:
-
Reflect提供的所有靜態(tài)方法和Proxy第2個handle對象中的方法參數(shù)是一模一樣的,例如Reflect的 get/set() 方法需要的參數(shù)就是Proxy get/set() 方法的參數(shù)
-
Proxy get/set() 方法需要的返回值正是Reflect的 get/set() 方法的返回值,可以天然配合使用,比直接對象賦值/獲取值要更方便和準確
-
receiver 參數(shù)具有不可替代性?。?!
在下面示例中,我們在頁面中訪問了 alias 對應(yīng)的值,稍后 name 變化了,要重新渲染么?
target[key] 方式訪問 proxy.alias 時,獲取到 this.name,此時 this 指向 target,無法監(jiān)控到 name ,不能重新渲染
Reflect 方式訪問 proxy.alias 時,獲取到 this.name,此時 this 指向 proxy,可監(jiān)控到 name ,可以重新渲染
const target = {
name: '柏成',
get alias() {
console.log('this === target', this === target)
console.log('this === proxy', this === proxy)
return this.name
},
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
console.log('key:', key)
return target[key]
// return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver)
},
})
proxy.alias
使用 target[key] 打印結(jié)果:
使用 Reflect 打印結(jié)果:
如何用(How)
讓我們創(chuàng)建一個稱為 reactive
的函數(shù),如果你使用過Composition API,你會感覺很熟悉。然后再封裝一下我們的 handler
方法,讓它長得更像 Vue3 的源代碼,最后我們將創(chuàng)建一個新的 Proxy對象
代碼如下
function reactive(target) {
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver)
// 保存effect
track(target, key)
return result
},
set(target, key, value, receiver) {
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
// 運行effect
trigger(target, key)
}
return result
},
}
return new Proxy(target, handler)
}
let product = reactive({ price: 5, quantity: 2 })
現(xiàn)在我們已經(jīng)不再需要手動調(diào)用 track()
和 trigger()
了
讓我們分析一下上圖內(nèi)容
-
現(xiàn)在我們的響應(yīng)式函數(shù)返回一個 product 對象的代理,我們還有變量 total ,方法 effect()。
-
當我們運行 effect() ,試圖獲取 product.price 時,它將運行
track(product, 'price')
-
在 targetMap 里,它將為 product 對象創(chuàng)建一個新的映射,它的值是一個新的 depsMap ,這將映射 price 屬性得到一個新的 dep ,這個 dep就是一個 effects集(Set),把我們 total 的 effect加到這個集(Set)中
-
我們還會訪問 product.quantity ,這是另一個get請求。我們將會調(diào)用
track(product, 'quantity')
。這將訪問我們 product 對象的 depsMap,并添加一個 quantity 屬性到一個新的 dep 對象的映射 -
然后我們把 total 打印到控制臺是 10
-
然后我們運行
product.quantity = 3
,它會調(diào)用trigger(product, 'quantity')
,然后運行被存儲的所有 effect -
調(diào)用 effect() , 就會訪問到 product.price ,觸發(fā)
track(product, 'price')
;訪問到 product.quantity ,則觸發(fā)track(product, 'quantity')
ActiveEffect
我們每訪問一次Proxy實例屬性,都將會調(diào)用一次 track
函數(shù)。然后它會去歷遍 targetMap、depsMap,以確保當前 effect
會被記錄下來,這不合理,不需要多次添加 effect
這不是我們想要的,我們只應(yīng)該在 effect()
里調(diào)用 track
函數(shù)
console.log('Update quantity to = '+ product.quantity)
console.log('Update price to = '+ product.price)
為此,我們引入了 activeEffect
變量,它代表現(xiàn)在正在運行中的 effect
, Vue3 也是這樣做的,代碼如下
let activeEffect = null
...
// 負責收集依賴
function effect(eff){
activeEffect = eff
activeEffect() // 運行
activeEffect = null //復位
}
// 我們用這個函數(shù)來計算total
effect(() => {
total = product.price * product.quantity
})
現(xiàn)在我們需要新的 track()
函數(shù),讓它去使用這個新的 activeEffect
變量
function track(target, key){
// 關(guān)鍵?。?!
// 我們只想在我們有activeEffect時運行這段代碼
if(!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
//當我們添加依賴(dep)時我們要添加activeEffect
dep.add(activeEffect)
}
這樣就保證了,如果不是通過 effect()
函數(shù)去訪問Proxy實例屬性,則這時的 activeEffect
為 null ,進入 track()
函數(shù)立即就被 return 掉了
完整代碼
這樣一來,我們就實現(xiàn)了 Vue3 基本的響應(yīng)性了。完整代碼如下
// The active effect running
let activeEffect = null
// For storing the dependencies for each reactive object
const targetMap = new WeakMap()
// 負責收集依賴
function effect(eff) {
activeEffect = eff
activeEffect() // 運行
activeEffect = null //復位
}
// Save this code
function track(target, key) {
// 關(guān)鍵?。?!
// 我們只想在我們有activeEffect時運行這段代碼
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
console.log('>>>track', target, key)
//當我們添加依賴(dep)時我們要添加activeEffect
dep.add(activeEffect)
}
// Run all the code I've saved
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
let dep = depsMap.get(key)
if (dep) {
console.log('>>>trigger', target, key)
dep.forEach(eff => {
eff()
})
}
}
// 響應(yīng)式代理
function reactive(target) {
// 如果不是對象或數(shù)組
// 拋出警告,并返回目標對象
if (!target || typeof target !== 'object') {
console.warn(`value cannot be made reactive: ${String(target)}`)
return target
}
const handler = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver)
track(target, key)
// 遞歸創(chuàng)建并返回
if (typeof target[key] === 'object' && target[key] !== null) {
return reactive(target[key])
}
return result
},
set(target, key, value, receiver) {
let oldValue = target[key]
let result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key)
}
return result
},
}
return new Proxy(target, handler)
}
let product = reactive({ price: 5, quantity: 2, rate: { value: 0.9 } })
let total = 0
effect(() => {
total = product.price * product.quantity * product.rate.value
})
控制臺打印結(jié)果如下
參考資料
-
ES6的代理模式 | Proxy | Vue3
-
Proxy是代理,Reflect是干嘛用的? ? 張鑫旭-鑫空間-鑫生活
-
Proxy和Reflect中的receiver到底是個什么東西 - 掘金文章來源:http://www.zghlxwxcb.cn/news/detail-745221.html
-
Proxy 和 Reflect 中的 receiver 到底是什么? · Issue #52 · sl1673495/notes文章來源地址http://www.zghlxwxcb.cn/news/detail-745221.html
到了這里,關(guān)于【Vue3響應(yīng)式原理#02】Proxy and Reflect的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!