日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > vue >内容正文

vue

【Vue3响应式原理#02】Proxy and Reflect

發布時間:2023/11/16 vue 81 coder
生活随笔 收集整理的這篇文章主要介紹了 【Vue3响应式原理#02】Proxy and Reflect 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

專欄分享:vue2源碼專欄,vue3源碼專欄,vue router源碼專欄,玩具項目專欄,硬核??推薦??

歡迎各位ITer關注點贊收藏??????

背景

以下是柏成根據Vue3官方課程整理的響應式書面文檔 - 第二節,課程鏈接在此:Proxy and Reflect - Vue 3 Reactivity | Vue Mastery

本篇文章將解決 上一篇文章 結尾遺留的問題:如何讓代碼自動實現響應性? 換句話說就是,如何讓我們的 effect 自動保存 & 自動重新運行?

在 上一篇文章 中,我們最終運行的代碼長這樣

聰明的你會立馬發現,我們現在仍要手動調用 track() 來保存 effect;手動調用 trigger() 來運行 effects,這不是脫褲子放屁么

我們想讓我們的響應性引擎自動調用 track()trigger()。那么問題就來了,何時才是調用它們的最好時機呢?

從邏輯上來說,如果訪問了對象的屬性,就是我們調用 track() 去保存 effect 的最佳時機;如果對象的屬性改變了,就是我們調用 trigger() 來運行 effects 的最佳時機

所以問題變成了,我們該如何攔截對象屬性的訪問和賦值操作?

Proxy(代理)

在 MDN 上的 Proxy 對象是這樣定義的

Proxy 對象用于創建一個對象的代理,從而實現基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數調用等)。

也可以理解為在操作目標對象前架設一層代理,將所有本該我們手動編寫的程序交由代理來處理,生活中也有許許多多的“proxy”, 如代購,中介,因為他們所有的行為都不會直接觸達到目標對象

語法

  • target: 要使用 Proxy 包裝的目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理)

  • handler: 一個通常以函數作為屬性的對象,用來定制攔截行為;它包含有 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) 屬性設置操作的捕捉器

handler.get

用于代理目標對象的屬性讀取操作,其接受三個參數 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 架設了一層代理,所有 get 操作都會直接返回我們定制的數字10

需要注意的是,代理只會對 proxy 對象生效,如訪問上方的 origin 對象就沒有任何效果

handler.set

用于代理目標對象的屬性設置操作,其接受四個參數 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 是一個內建的對象,用來提供方法去攔截 JavaScript的操作。Reflect 不是一個函數對象,所以它是不可構造的,也就是說你不能通過 new操作符去新建一個 Reflect對象或者將 Reflect對象作為一個函數去調用。Reflect的所有屬性和方法都是靜態的(就像Math對象)

常用方法

Reflect對象掛載了很多靜態方法,所謂靜態方法,就是和 Math.round() 這樣,不需要 new 就可以直接使用的方法。
比較常用的兩個方法就是 get()set() 方法:

方法 描述
Reflect.get(target, key, ?receiver) 和 target[key] 類似,從對象中讀取屬性值
Reflect.set(target, key, value, ? receiver) 和 target[key] = value 類似,給對象的屬性設置一個新值

Reflect.get()

Reflect.get方法允許你從一個對象中取屬性值,返回值是這個屬性值

Reflect.set()

Reflect.set 方法允許你在對象上設置屬性,返回值是 Boolean 值,代表是否設置成功

  • 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)

關于receiver參數

在 Proxy 和 Reflect 對象中 get/set() 方法的最后一個參數都是 receiver,它到底是個什么玩意?

receiver 是接受者的意思,譯為接收器

  1. 在 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
  1. 在 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,我們會有一個附加參數,稱為 receiver (接收器),它將傳遞到我們的 Reflect調用中。它保證了當我們的對象有繼承自其它對象的值或函數時, this 指針能正確的指向對象,這將避免一些我們在 vue2 中有的響應式警告

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對象經常和Proxy代理一起使用,原因有三點:

  1. Reflect提供的所有靜態方法和Proxy第2個handle對象中的方法參數是一模一樣的,例如Reflect的 get/set() 方法需要的參數就是Proxy get/set() 方法的參數

  2. Proxy get/set() 方法需要的返回值正是Reflect的 get/set() 方法的返回值,可以天然配合使用,比直接對象賦值/獲取值要更方便和準確

  3. receiver 參數具有不可替代性!!!

    在下面示例中,我們在頁面中訪問了 alias 對應的值,稍后 name 變化了,要重新渲染么?

    target[key] 方式訪問 proxy.alias 時,獲取到 this.name,此時 this 指向 target,無法監控到 name ,不能重新渲染

    Reflect 方式訪問 proxy.alias 時,獲取到 this.name,此時 this 指向 proxy,可監控到 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] 打印結果:

使用 Reflect 打印結果:

如何用(How)

讓我們創建一個稱為 reactive 的函數,如果你使用過Composition API,你會感覺很熟悉。然后再封裝一下我們的 handler 方法,讓它長得更像 Vue3 的源代碼,最后我們將創建一個新的 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 })

現在我們已經不再需要手動調用 track()trigger()

讓我們分析一下上圖內容

  1. 現在我們的響應式函數返回一個 product 對象的代理,我們還有變量 total ,方法 effect()

  2. 當我們運行 effect() ,試圖獲取 product.price 時,它將運行track(product, 'price')

  3. targetMap 里,它將為 product 對象創建一個新的映射,它的值是一個新的 depsMap ,這將映射 price 屬性得到一個新的 dep ,這個 dep就是一個 effects集(Set),把我們 total 的 effect加到這個集(Set)中

  4. 我們還會訪問 product.quantity ,這是另一個get請求。我們將會調用track(product, 'quantity')。這將訪問我們 product 對象的 depsMap,并添加一個 quantity 屬性到一個新的 dep 對象的映射

  5. 然后我們把 total 打印到控制臺是 10

  6. 然后我們運行product.quantity = 3,它會調用 trigger(product, 'quantity'),然后運行被存儲的所有 effect

  7. 調用 effect() , 就會訪問到 product.price ,觸發track(product, 'price');訪問到 product.quantity ,則觸發track(product, 'quantity')

ActiveEffect

我們每訪問一次Proxy實例屬性,都將會調用一次 track 函數。然后它會去歷遍 targetMap、depsMap,以確保當前 effect 會被記錄下來,這不合理,不需要多次添加 effect

這不是我們想要的,我們只應該在 effect() 里調用 track 函數

console.log('Update quantity to = '+ product.quantity)
console.log('Update price to = '+ product.price)

為此,我們引入了 activeEffect 變量,它代表現在正在運行中的 effect, Vue3 也是這樣做的,代碼如下

let activeEffect = null
...
// 負責收集依賴
function effect(eff){ 
  activeEffect = eff 
  activeEffect() // 運行
  activeEffect = null //復位
}

// 我們用這個函數來計算total
effect(() => {
  total = product.price * product.quantity
})

現在我們需要新的 track() 函數,讓它去使用這個新的 activeEffect 變量

function track(target, key){
  // 關鍵!!!
  // 我們只想在我們有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() 函數去訪問Proxy實例屬性,則這時的 activeEffect 為 null ,進入 track() 函數立即就被 return 掉了

完整代碼

這樣一來,我們就實現了 Vue3 基本的響應性了。完整代碼如下

// 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) {
  // 關鍵!!!
  // 我們只想在我們有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()
    })
  }
}

// 響應式代理
function reactive(target) {
  // 如果不是對象或數組
  // 拋出警告,并返回目標對象
  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)

      // 遞歸創建并返回
      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
})

控制臺打印結果如下

參考資料

  • ES6的代理模式 | Proxy | Vue3

  • Proxy是代理,Reflect是干嘛用的? ? 張鑫旭-鑫空間-鑫生活

  • Proxy和Reflect中的receiver到底是個什么東西 - 掘金

  • Proxy 和 Reflect 中的 receiver 到底是什么? · Issue #52 · sl1673495/notes

總結

以上是生活随笔為你收集整理的【Vue3响应式原理#02】Proxy and Reflect的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。