Vue 3 響應(yīng)式原理二 - Proxy and Reflect

在上一篇【Vue 3 響應(yīng)式原理一 - Vue 3 Reactivity】
中奢人,我們知道了 Vue 3 如何跟蹤effects,以便在需要時(shí)重新運(yùn)行它們。然而,我們?nèi)匀恍枰謩?dòng)調(diào)用tracktrigger×构洌現(xiàn)在我們將學(xué)習(xí)如何使用ReflectProxy來自動(dòng)調(diào)用它們遥昧。

Hooking onto Get and Set

我們需要一種方法來 hook (或偵聽) 我們的響應(yīng)式對(duì)象上的getset
GET property (訪問屬性) => 調(diào)用track去保存當(dāng)前 effect
SET property (修改了屬性) => 調(diào)用trigger來運(yùn)行屬性的 dependencies (effects)

如何做到這些绣檬?在 Vue 3 中我們使用 ES6 的ReflectProxy攔截 GET 和 SET 調(diào)用。Vue 2 中是使用 ES5 的Object.defineProperty實(shí)現(xiàn)這一點(diǎn)的嫂粟。

理解 ES6 Reflect

要打印出一個(gè)對(duì)象的某屬性可以像這樣做:

let product = { price: 5, quantity: 2 }
console.log('quantity is ' + product.quantity)
// or 
console.log('quantity is ' + product['quantity'])

然而娇未,也可以使用ReflectGET 對(duì)象上的值。 Reflect 允許你用另一種方式獲取對(duì)象的屬性:

console.log('quantity is ' + Reflect.get(product, 'quantity'))

為什么使用reflect星虹?因?yàn)樗哂形覀兩院笮枰奶匦粤闾В诶斫?ES6 Proxy 之后再來展示。

理解 ES6 Proxy

Proxy 是另一個(gè)對(duì)象的占位符宽涌,默認(rèn)情況下對(duì)該對(duì)象進(jìn)行委托平夜。 如果我運(yùn)行如下代碼:

let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {})
console.log(proxiedProduct.quantity)

注意到 Proxy 的第二個(gè)參數(shù){}了嗎?這是一個(gè)handler卸亮,可用于定義代理對(duì)象(Proxy)上的自定義行為忽妒,例如攔截 get 和 set 調(diào)用。這些攔截器方法稱為traps(捕捉器),可以幫助我們攔截一些基本操作段直,如屬性查找吃溅、枚舉或函數(shù)調(diào)用。下面是如何在handler上設(shè)置 get traps

let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
  get() {
    console.log('Get was called')
    return 'Not the value'
  }
})
console.log(proxiedProduct.quantity)

我們應(yīng)該返回實(shí)際的值鸯檬,像這樣:

let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
  get(target, key) {  // <--- The target (代理的對(duì)象) and key (屬性名)
    console.log('Get was called with key = ' + key)
    return target[key]
  }
})
console.log(proxiedProduct.quantity)
image.png

get 函數(shù)有兩個(gè)參數(shù)决侈,target是我們的對(duì)象(product)和我們?cè)噲D獲取的key(屬性),在本例中是quantity喧务。

當(dāng)我們?cè)?Proxy 中使用Reflect赖歌,可以添加一個(gè)額外參數(shù),可以被傳遞到Reflect調(diào)用中功茴。

let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
  get(target, key, receiver) {  // <--- notice the receiver
    console.log('Get was called with key = ' + key)
    return Reflect.get(target, key, receiver) // <----
  }
})

這能確保當(dāng)我們的對(duì)象有從其他對(duì)象繼承的值/函數(shù)時(shí)庐冯,this 值能正確地指向調(diào)用對(duì)象。使用 Proxy 的一個(gè)難點(diǎn)就是this綁定坎穿。我們希望任何方法都綁定到這個(gè) Proxy肄扎,而不是target對(duì)象。這就是為什么我們總是在Proxy內(nèi)部使用Reflect赁酝,這樣我們就能保留我們正在自定義的原始行為。

現(xiàn)在讓我們添加一個(gè)setter方法:

let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
  get(target, key, receiver) {  
    console.log('Get was called with key = ' + key)
    return Reflect.get(target, key, receiver) 
  }
  set(target, key, value, receiver) {
    console.log('Set was called with key = ' + key + ' and value = ' + value)
    return Reflect.set(target, key, value, receiver)
  }
})
proxiedProduct.quantity = 4
console.log(proxiedProduct.quantity)

set 除了使用Reflect.set接收值來設(shè)置 target 之外旭等,看起來與 get 非常相似酌呆。輸出也符合我們的預(yù)期。

我們可以通過另一種方式封裝這段代碼搔耕,就像在 Vue 3 源碼中看到的那樣隙袁。首先,我們將這個(gè)代理委托代碼包裝在一個(gè)返回proxy的響應(yīng)式函數(shù)中弃榨,如果你用過 Vue 3 Composition API菩收,它應(yīng)該看起來很熟悉。然后將包含 getset traps 的handler常量發(fā)送到我們的proxy中鲸睛。

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      console.log('Get was called with key = ' + key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      console.log('Set was called with key = ' + key + ' and value = ' + value)
      return Reflect.set(target, key, value, receiver)
    }
  }
  return new Proxy(target, handler) // 創(chuàng)建一個(gè) Proxy 對(duì)象
}
let product = reactive({ price: 5, quantity: 2 }) // <-- Returns a proxy object
product.quantity = 4
console.log(product.quantity)

這會(huì)返回與上面相同的結(jié)果娜饵,但現(xiàn)在我們可以輕松地利用reactive方法創(chuàng)建多個(gè)響應(yīng)式對(duì)象。

結(jié)合 Proxy + Effect 存儲(chǔ)

回到最初的起點(diǎn):
GET property (訪問屬性) => 我們需要調(diào)用track去保存當(dāng)前 effect
SET property (修改了屬性) => 我們需要調(diào)用trigger來運(yùn)行屬性的 dependencies (effects)

track 將檢查當(dāng)前運(yùn)行的是哪個(gè)副作用(effect)官辈,并將其與 target 和 property 記錄在一起箱舞。這就是 Vue 如何知道這個(gè) property 是該副作用的依賴項(xiàng)。

我們可以想象一下上面的reactive代碼拳亿,需要調(diào)用 tracktrigger的地方晴股。

思路整理:

  1. 當(dāng)一個(gè)值被讀取時(shí)進(jìn)行追蹤:proxy 的get處理函數(shù)中track函數(shù)記錄了該 property 和當(dāng)前副作用。
  2. 當(dāng)某個(gè)值改變時(shí)進(jìn)行檢測(cè):在 proxy 上調(diào)用set處理函數(shù)肺魁。
  3. 重新運(yùn)行代碼來讀取原始值trigger函數(shù)查找哪些副作用依賴于該 property 并執(zhí)行它們电湘。

直接整上:

const targetMap = new WeakMap() // targetMap stores the effects that each object should re-run when it's updated
function track(target, key) {
  // We need to make sure this effect is being tracked.
  let depsMap = targetMap.get(target) // Get the current depsMap for this target
  if (!depsMap) {
    // There is no map.
    targetMap.set(target, (depsMap = new Map())) // Create one
  }
  let dep = depsMap.get(key) // Get the current dependencies (effects) that need to be run when this is set
  if (!dep) {
    // There is no dependencies (effects)
    depsMap.set(key, (dep = new Set())) // Create a new Set
  }
  dep.add(effect) // Add effect to dependency map
}
function trigger(target, key) {
  const depsMap = targetMap.get(target) // Does this object have any properties that have dependencies (effects)
  if (!depsMap) {
    return
  }
  let dep = depsMap.get(key) // If there are dependencies (effects) associated with this
  if (dep) {
    dep.forEach(effect => {
      // run them all
      effect()
    })
  }
}
function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver)
      // Track
      track(target, key) // If this reactive property (target) is GET inside then track the effect to rerun on SET
      return result
    },
    set(target, key, value, receiver) {
      let oldValue = target[key]
      let result = Reflect.set(target, key, value, receiver)
      if (oldValue != result) {
        // Trigger
        trigger(target, key) // If this reactive property (target) has effects to rerun on SET, trigger them.
      }
      return result
    }
  }
  return new Proxy(target, handler)
}
let product = reactive({ price: 5, quantity: 2 })
let total = 0
let effect = () => {
  total = product.price * product.quantity
}
effect()
console.log('before updated quantity total = ' + total)
product.quantity = 3
console.log('after updated quantity total = ' + total)

這段代碼輸出:

before updated quantity total = 10
after updated quantity total = 15

現(xiàn)在我們不再需要調(diào)用triggertrack,因?yàn)樗鼈冊(cè)谖覀兊?code>get和set方法中被合理地調(diào)用。

使用 Proxy 和 Reflect 能帶來什么好處寂呛?

當(dāng)你使用proxies時(shí)怎诫,也就是所謂的響應(yīng)式轉(zhuǎn)換,是懶執(zhí)行的昧谊。
而把對(duì)象傳給 Vue 2 的響應(yīng)式時(shí)刽虹,則必須遍歷所有的 key,并且當(dāng)場(chǎng)轉(zhuǎn)換呢诬,以確保它們被訪問時(shí)都是響應(yīng)式的涌哲。
對(duì)于 Vue3,當(dāng)調(diào)用reactive時(shí)尚镰,返回的是一個(gè)proxy代理對(duì)象阀圾,并且只會(huì)在需要的時(shí)候才去轉(zhuǎn)換嵌套的對(duì)象。有點(diǎn)像"懶加載"狗唉。這樣做的好處打個(gè)比方初烘,當(dāng)你進(jìn)行分頁渲染,那么只有第一頁需要的10個(gè)object需要經(jīng)過響應(yīng)式轉(zhuǎn)化分俯。這對(duì)應(yīng)用程序而言可以節(jié)省很多時(shí)間肾筐,特別是當(dāng)程序擁有龐大的列表對(duì)象時(shí)。

我們已經(jīng)前進(jìn)了一大步缸剪!在此代碼穩(wěn)固之前吗铐,只有一個(gè) bug 需要修復(fù)。具體來說杏节,我們只希望track在 響應(yīng)式對(duì)象有被effect使用 時(shí)才被調(diào)用』I現(xiàn)在只要響應(yīng)式對(duì)象屬性是get,就會(huì)調(diào)用track奋渔。我們將在下一篇中完善這一點(diǎn)镊逝。

Vue 3 響應(yīng)式原理一 - Vue 3 Reactivity
Vue 3 響應(yīng)式原理二 - Proxy and Reflect
Vue 3 響應(yīng)式原理三 - activeEffect & ref
Vue 3 響應(yīng)式原理四 - Computed Values & Vue 3 源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市嫉鲸,隨后出現(xiàn)的幾起案子撑蒜,更是在濱河造成了極大的恐慌,老刑警劉巖玄渗,帶你破解...
    沈念sama閱讀 221,406評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件减江,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡捻爷,警方通過查閱死者的電腦和手機(jī)辈灼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來也榄,“玉大人巡莹,你說我怎么就攤上這事司志。” “怎么了降宅?”我有些...
    開封第一講書人閱讀 167,815評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵骂远,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我腰根,道長(zhǎng)激才,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,537評(píng)論 1 296
  • 正文 為了忘掉前任额嘿,我火速辦了婚禮瘸恼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘册养。我一直安慰自己东帅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評(píng)論 6 397
  • 文/花漫 我一把揭開白布球拦。 她就那樣靜靜地躺著靠闭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪坎炼。 梳的紋絲不亂的頭發(fā)上愧膀,一...
    開封第一講書人閱讀 52,184評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音谣光,去河邊找鬼檩淋。 笑死,一個(gè)胖子當(dāng)著我的面吹牛抢肛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播碳柱,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼捡絮,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了莲镣?” 一聲冷哼從身側(cè)響起福稳,我...
    開封第一講書人閱讀 39,668評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎瑞侮,沒想到半個(gè)月后的圆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,212評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡半火,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評(píng)論 3 340
  • 正文 我和宋清朗相戀三年越妈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钮糖。...
    茶點(diǎn)故事閱讀 40,438評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡梅掠,死狀恐怖酌住,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情阎抒,我是刑警寧澤酪我,帶...
    沈念sama閱讀 36,128評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站且叁,受9級(jí)特大地震影響都哭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜逞带,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評(píng)論 3 333
  • 文/蒙蒙 一欺矫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧掰担,春花似錦汇陆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至勺疼,卻和暖如春教寂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背执庐。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評(píng)論 1 272
  • 我被黑心中介騙來泰國打工酪耕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人轨淌。 一個(gè)月前我還...
    沈念sama閱讀 48,827評(píng)論 3 376
  • 正文 我出身青樓迂烁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親递鹉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子盟步,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容