中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

怎么在Vue3 中偵測數據

發布時間:2021-04-17 17:43:37 來源:億速云 閱讀:182 作者:Leah 欄目:web開發

這篇文章給大家介紹怎么在Vue3 中偵測數據,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

實現可響應對象的方式

通過可響應對象,實現對數據的偵測,從而告知外界數據變化。實現可響應對象的方式:

  1. getter 和 setter

  2. defineProperty

  3. Proxy

關于前兩個 API 的使用方式不多贅述,單一的訪問器 getter/setter 功能相對簡單,而作為 Vue2.x 實現可響應對象的 API -defineProperty ,API 本身存在較多問題。

Vue2.x 中,實現數據的可響應,需要對 Object 和 Array 兩種類型采用不同的處理方式。

Object 類型通過 Object.defineProperty 將屬性轉換成 getter/setter ,這個過程需要遞歸偵測所有的對象 key,來實現深度的偵測。
為了感知 Array 的變化,對 Array 原型上幾個改變數組自身的內容的方法做了攔截,雖然實現了對數組的可響應,但同樣存在一些問題,或者說不夠方便的情況。同時,defineProperty 通過遞歸實現 getter/setter 也存在一定的性能問題。

更好的實現方式是通過 ES6 提供的 Proxy API。

Proxy API 的一些細節

Proxy API 具有更加強大的功能,相比舊的 defineProperty API ,Proxy 可以代理數組,并且 API 提供了多個 traps ,可以實現諸多功能。

這里主要說兩個trap: get 、 set , 以及其中的一些比較容易被忽略的細節。

細節一:trap 默認行為

let data = { foo: 'foo' }
let p = new Proxy(data, {
 get(target, key, receiver) {
  return target[key]
 },
 set(target, key, value, receiver) {
  console.log('set value')
  target[key] = value // ?
 }
})

p.foo = 123

// set value

通過 proxy 返回的對象 p 代理了對原始數據的操作,當對 p 設置時,便可以偵測到變化。但是這么寫實際上是有問題,
當代理的對象數據是數組時,會報錯。

let data = [1,2,3]
let p = new Proxy(data, {
 get(target, key, receiver) {
  return target[key]
 },
 set(target, key, value, receiver) {
  console.log('set value')
  target[key] = value
 }
})

p.push(4) // VM438:12 Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'

將代碼更改為:

let data = [1,2,3]
let p = new Proxy(data, {
 get(target, key, receiver) {
  return target[key]
 },
 set(target, key, value, receiver) {
  console.log('set value')
  target[key] = value
  return true
 }
})

p.push(4)

// set value // 打印2次

實際上,當代理對象是數組,通過 push 操作,并不只是操作當前數據,push 操作還觸發數組本身其他屬性更改。

let data = [1,2,3]
let p = new Proxy(data, {
 get(target, key, receiver) {
  console.log('get value:', key)
  return target[key]
 },
 set(target, key, value, receiver) {
  console.log('set value:', key, value)
  target[key] = value
  return true
 }
})

p.push(1)

// get value: push
// get value: length
// set value: 3 1
// set value: length 4

先看 set 操作,從打印輸出可以看出,push 操作除了給數組的第 3 位下標設置值 1 ,還給數組的 length 值更改為 4。
同時這個操作還觸發了 get 去獲取 push 和 length 兩個屬性。

我們可以通過 Reflect 來返回 trap 相應的默認行為,對于 set 操作相對簡單,但是一些比較復雜的默認行為處理起來相對繁瑣得多,Reflect 的作用就顯現出來了。

let data = [1,2,3]
let p = new Proxy(data, {
 get(target, key, receiver) {
  console.log('get value:', key)
  return Reflect.get(target, key, receiver)
 },
 set(target, key, value, receiver) {
  console.log('set value:', key, value)
  return Reflect.set(target, key, value, receiver)
 }
})

p.push(1)

// get value: push
// get value: length
// set value: 3 1
// set value: length 4

相比自己處理 set 的默認行為,Reflect 就方便得多。

細節二:多次觸發 set / get

從前面的例子中可以看出,當代理對象是數組時,push 操作會觸發多次 set 執行,同時,也引發 get 操作,這點非常重要,vue3 就很好的使用了這點。

我們可以從另一個例子來看這個操作:

let data = [1,2,3]
let p = new Proxy(data, {
 get(target, key, receiver) {
  console.log('get value:', key)
  return Reflect.get(target, key, receiver)
 },
 set(target, key, value, receiver) {
  console.log('set value:', key, value)
  return Reflect.set(target, key, value, receiver)
 }
})

p.unshift('a')

// get value: unshift
// get value: length
// get value: 2
// set value: 3 3
// get value: 1
// set value: 2 2
// get value: 0
// set value: 1 1
// set value: 0 a
// set value: length 4

可以看到,在對數組做 unshift 操作時,會多次觸發 get 和 set 。仔細觀察輸出,不難看出,get 先拿數組最末位下標,開辟新的下標 3 存放原有的末位數值,然后再將原數值都往后挪,將 0 下標設置為了 unshift 的值 a ,由此引發了多次 set 操作。

而這對于 通知外部操作 顯然是不利,我們假設 set 中的 console 是觸發外界渲染的 render 函數,那么這個 unshift 操作會引發 多次  render 。

我們后面會講述如何解決相應的這個問題,繼續。

細節三:proxy 只能代理一層

let data = { foo: 'foo', bar: { key: 1 }, ary: ['a', 'b'] }
let p = new Proxy(data, {
 get(target, key, receiver) {
  console.log('get value:', key)
  return Reflect.get(target, key, receiver)
 },
 set(target, key, value, receiver) {
  console.log('set value:', key, value)
  return Reflect.set(target, key, value, receiver)
 }
})

p.bar.key = 2

// get value: bar

執行代碼,可以看到并沒有觸發 set 的輸出,反而是觸發了 get ,因為 set 的過程中訪問了 bar  這個屬性。

由此可見,proxy 代理的對象只能代理到第一層,而對象內部的深度偵測,是需要開發者自己實現的。同樣的,對于對象內部的數組也是一樣。

p.ary.push('c')

// get value: ary

同樣只走了 get 操作,set 并不能感知到。

我們注意到 get/set 還有一個參數:receiver ,對于 receiver ,其實接收的是一個代理對象:

let data = { a: {b: {c: 1 } } }
let p = new Proxy(data, {
 get(target, key, receiver) {
  console.log(receiver)
  const res = Reflect.get(target, key, receiver)
  return res
 },
 set(target, key, value, receiver) {
  return Reflect.set(target, key, value, receiver)
 }
})

// Proxy {a: {…}}

這里 receiver 輸出的是當前代理對象,注意,這是一個已經代理后的對象。

let data = { a: {b: {c: 1 } } }
let p = new Proxy(data, {
 get(target, key, receiver) {
  const res = Reflect.get(target, key, receiver)
  console.log(res)
  return res
 },
 set(target, key, value, receiver) {
  return Reflect.set(target, key, value, receiver)
 }
})

// {b: {c: 1} }

當我們嘗試輸出 Reflect.get 返回的值,會發現,當代理的對象是多層結構時,Reflect.get 會返回對象的內層結構。
記住這一點,Vue3 實現深度的proxy ,便是很好的使用了這點。

解決 proxy 中的細節問題

前面提到了使用 Proxy 來偵測數據變化,有幾個細節問題,包括:

  1. 使用 Reflect 來返回 trap 默認行為

  2. 對于 set 操作,可能會引發代理對象的屬性更改,導致 set 執行多次

  3. proxy  只能代理對象中的一層,對于對象內部的操作 set 未能感知,但是 get 會被執行

接下來,我們將先自己嘗試解決這些問題,后面再分析 Vue3 是如何解決這些細節的。

setTimeout 解決重復 trigger

function reactive(data, cb) {
 let timer = null
 return new Proxy(data, {
  get(target, key, receiver) {
   return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
   clearTimeout(timer)
   timer = setTimeout(() => {
    cb && cb()
   }, 0);
   return Reflect.set(target, key, value, receiver)
  }
 })
}

let ary = [1, 2]
let p = reactive(ary, () => {
 console.log('trigger')
})
p.push(3)

// trigger

程序輸出結果為一個: trigger

這里實現了 reactive 函數,接收兩個參數,第一個是被代理的數據 data ,還有一個回調函數 cb,我們這里先簡單的在 cb 中打印 trigger 操作,來模擬通知外部數據的變化。

解決重復的 cb 調用有很多中方式,比方通過標志,來決定是否調用。而這里是使用了定時器 setTimeout ,每次調用 cb 之前,都清除定時器,來實現類似于 debounce 的操作,同樣可以解決重復的 callback 問題。

解決數據深度偵測

目前還有一個問題,那便是深度的數據偵測,我們可以使用遞歸代理的方式來實現:

function reactive(data, cb) {
 let res = null
 let timer = null

 res = data instanceof Array ? []: {}

 for (let key in data) {
  if (typeof data[key] === 'object') {
   res[key] = reactive(data[key], cb)
  } else {
   res[key] = data[key]
  }
 }

 return new Proxy(res, {
  get(target, key) {
   return Reflect.get(target, key)
  },
  set(target, key, val) {
   let res = Reflect.set(target, key, val)
   clearTimeout(timer)
   timer = setTimeout(() => {
    cb && cb()
   }, 0)
   return res
  }
 })
}

let data = { foo: 'foo', bar: [1, 2] }
let p = reactive(data, () => {
 console.log('trigger')
})
p.bar.push(3)

// trigger

對代理的對象進行遍歷,對每個 key 都做一次 proxy,這是遞歸實現的方式。同時,結合前面提到的 timer 避免重復 set 的問題。

這里我們可以輸出代理后的對象 p :

怎么在Vue3 中偵測數據

可以看到深度代理后的對象,都攜帶 proxy 的標志。

到這里,我們解決了使用 proxy 實現偵測的系列細節問題,雖然這些處理方式可以解決問題,但似乎并不夠優雅,尤其是遞歸 proxy 是一個性能隱患,當數據對象比較大時,遞歸的 proxy 會消耗比較大的性能,并且有些數據并非需要偵測,我們需要對數據偵測做更細的控制。

接下來我們就看下 Vue3 是如何使用 Proxy 實現數據偵測的。

Vue3 中的 reactivity

Vue3 項目結構采用了 lerna 做 monorepo 風格的代碼管理,目前比較多的開源項目切換到了 monorepo 的模式,比較顯著的特征是項目中會有個 packages/ 的文件夾。

Vue3 對功能做了很好的模塊劃分,同時使用 TS 。我們直接在 packages 中找到響應式數據的模塊:

怎么在Vue3 中偵測數據

其中,reactive.ts 文件提供了 reactive 函數,該函數是實現響應式的核心。同時這個函數也掛載在了全局的 Vue 對象上。

這里對源代碼做一點程度的簡化:

const rawToReactive = new WeakMap()
const reactiveToRaw = new WeakMap()

// utils
function isObject(val) {
 return typeof val === 'object'
}

function hasOwn(val, key) {
 const hasOwnProperty = Object.prototype.hasOwnProperty
 return hasOwnProperty.call(val, key)
}

// traps
function createGetter() {
 return function get(target, key, receiver) {
  const res = Reflect.get(target, key, receiver)
  return isObject(res) ? reactive(res) : res
 }
}

function set(target, key, val, receiver) {
 const hadKey = hasOwn(target, key)
 const oldValue = target[key]

 val = reactiveToRaw.get(val) || val
 const result = Reflect.set(target, key, val, receiver)

 if (!hadKey) {
  console.log('trigger ...')
 } else if(val !== oldValue) {
  console.log('trigger ...')
 }

 return result
}

// handler
const mutableHandlers = {
 get: createGetter(),
 set: set,
}

// entry
function reactive(target) {
 return createReactiveObject(
  target,
  rawToReactive,
  reactiveToRaw,
  mutableHandlers,
 )
}

function createReactiveObject(target, toProxy, toRaw, baseHandlers) {
 let observed = toProxy.get(target)
 // 原數據已經有相應的可響應數據, 返回可響應數據
 if (observed !== void 0) {
  return observed
 }
 // 原數據已經是可響應數據
 if (toRaw.has(target)) {
  return target
 }
 observed = new Proxy(target, baseHandlers)
 toProxy.set(target, observed)
 toRaw.set(observed, target)
 return observed
}

rawToReactive 和 reactiveToRaw 是兩個弱引用的 Map 結構,這兩個 Map 用來保存 原始數據 和 可響應數據 ,在函數 createReactiveObject 中,toProxy 和 toRaw 傳入的便是這兩個 Map 。

我們可以通過它們,找到任何代理過的數據是否存在,以及通過代理數據找到原始的數據。

除了保存了代理的數據和原始數據,createReactiveObject 函數僅僅是返回了 new Proxy 代理后的對象。重點在 new Proxy 中傳入的handler參數 baseHandlers。

還記得前面提到的 Proxy 實現數據偵測的細節問題吧,我們嘗試輸入:

let data = { foo: 'foo', ary: [1, 2] }
let r = reactive(data)
r.ary.push(3)

打印結果:

怎么在Vue3 中偵測數據

可以看到打印輸出了一次 trigger ...

問題一:如何做到深度的偵測數據的 ?

深度偵測數據是通過 createGetter 函數實現的,前面提到,當對多層級的對象操作時,set 并不能感知到,但是 get 會觸發,于此同時,利用 Reflect.get() 返回的“多層級對象中內層” ,再對“內層數據”做一次代理。

function createGetter() {
 return function get(target, key, receiver) {
  const res = Reflect.get(target, key, receiver)
  return isObject(res) ? reactive(res) : res
 }
}

可以看到這里判斷了 Reflect 返回的數據是否還是對象,如果是對象,則再走一次 proxy ,從而獲得了對對象內部的偵測。

并且,每一次的 proxy 數據,都會保存在 Map 中,訪問時會直接從中查找,從而提高性能。

當我們打印代理后的對象時:

怎么在Vue3 中偵測數據

可以看到這個代理后的對象內層并沒有代理的標志,這里僅僅是代理外層對象。

輸出其中一個存儲代理數據的 rawToReactive :

怎么在Vue3 中偵測數據

對于內層 ary: [1, 2] 的代理,已經被存儲在了 rawToReactive 中。

由此實現了深度的數據偵測。

問題二:如何避免多次 trigger ?

function hasOwn(val, key) {
 const hasOwnProperty = Object.prototype.hasOwnProperty
 return hasOwnProperty.call(val, key)
}
function set(target, key, val, receiver) {
 console.log(target, key, val)
 const hadKey = hasOwn(target, key)
 const oldValue = target[key]
 
 val = reactiveToRaw.get(val) || val
 const result = Reflect.set(target, key, val, receiver)

 if (!hadKey) {
  console.log('trigger ... is a add OperationType')
 } else if(val !== oldValue) {
  console.log('trigger ... is a set OperationType')
 }

 return result
}

關于多次 trigger 的問題,vue 處理得很巧妙。

在 set 函數中 hasOwn 前打印 console.log(target, key, val) 。

輸入:

let data = ['a', 'b']
let r = reactive(data)
r.push('c')

輸出結果:

怎么在Vue3 中偵測數據

r.push('c') 會觸發 set 執行兩次,一次是值本身 'c' ,一次是 length 屬性設置。

設置值 'c' 時,傳入的新增索引 key 為 2,target 是原始的代理對象 ['a', 'c'] ,hasOwn(target, key) 顯然返回 false ,這是一個新增的操作,此時可以執行 trigger ... is a add OperationType 。

當傳入 key 為 length 時,hasOwn(target, key) ,length 是自身屬性,返回 true,此時判斷 val !== oldValue , val 是 3, 而 oldValue 即為 target['length'] 也是 3,此時不執行 trigger 輸出語句。

所以通過 判斷 key 是否為 target 自身屬性,以及設置val是否跟target[key]相等 可以確定 trigger 的類型,并且避免多余的 trigger。

關于怎么在Vue3 中偵測數據就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

桃园市| 习水县| 策勒县| 上杭县| 兰西县| 松潘县| 长垣县| 扶沟县| 社旗县| 南康市| 什邡市| 唐山市| 修水县| 定结县| 德清县| 玛沁县| 巫溪县| 平原县| 铅山县| 饶河县| 连城县| 阿克| 沂源县| 宣汉县| 依兰县| 石屏县| 穆棱市| 肃宁县| 宜丰县| 镇巴县| 宁德市| 鹤岗市| 平舆县| 吉首市| 台州市| 思茅市| 黄大仙区| 九龙城区| 古丈县| 兴仁县| 隆回县|