成人午夜视频全免费观看高清-秋霞福利视频一区二区三区-国产精品久久久久电影小说-亚洲不卡区三一区三区一区

怎么在Vue3中偵測(cè)數(shù)據(jù)-創(chuàng)新互聯(lián)

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

劍閣網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)建站!從網(wǎng)頁設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、APP開發(fā)、響應(yīng)式網(wǎng)站建設(shè)等網(wǎng)站項(xiàng)目制作,到程序開發(fā),運(yùn)營維護(hù)。創(chuàng)新互聯(lián)建站公司2013年成立到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)建站

實(shí)現(xiàn)可響應(yīng)對(duì)象的方式

通過可響應(yīng)對(duì)象,實(shí)現(xiàn)對(duì)數(shù)據(jù)的偵測(cè),從而告知外界數(shù)據(jù)變化。實(shí)現(xiàn)可響應(yīng)對(duì)象的方式:

  1. getter 和 setter

  2. defineProperty

  3. Proxy

關(guān)于前兩個(gè) API 的使用方式不多贅述,單一的訪問器 getter/setter 功能相對(duì)簡單,而作為 Vue2.x 實(shí)現(xiàn)可響應(yīng)對(duì)象的 API -defineProperty ,API 本身存在較多問題。

Vue2.x 中,實(shí)現(xiàn)數(shù)據(jù)的可響應(yīng),需要對(duì) Object 和 Array 兩種類型采用不同的處理方式。

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

更好的實(shí)現(xiàn)方式是通過 ES6 提供的 Proxy API。

Proxy API 的一些細(xì)節(jié)

Proxy API 具有更加強(qiáng)大的功能,相比舊的 defineProperty API ,Proxy 可以代理數(shù)組,并且 API 提供了多個(gè) traps ,可以實(shí)現(xiàn)諸多功能。

這里主要說兩個(gè)trap: get 、 set , 以及其中的一些比較容易被忽略的細(xì)節(jié)。

細(xì)節(jié)一:trap 默認(rèn)行為

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 返回的對(duì)象 p 代理了對(duì)原始數(shù)據(jù)的操作,當(dāng)對(duì) p 設(shè)置時(shí),便可以偵測(cè)到變化。但是這么寫實(shí)際上是有問題,
當(dāng)代理的對(duì)象數(shù)據(jù)是數(shù)組時(shí),會(huì)報(bào)錯(cuò)。

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次

實(shí)際上,當(dāng)代理對(duì)象是數(shù)組,通過 push 操作,并不只是操作當(dāng)前數(shù)據(jù),push 操作還觸發(fā)數(shù)組本身其他屬性更改。

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 操作除了給數(shù)組的第 3 位下標(biāo)設(shè)置值 1 ,還給數(shù)組的 length 值更改為 4。
同時(shí)這個(gè)操作還觸發(fā)了 get 去獲取 push 和 length 兩個(gè)屬性。

我們可以通過 Reflect 來返回 trap 相應(yīng)的默認(rèn)行為,對(duì)于 set 操作相對(duì)簡單,但是一些比較復(fù)雜的默認(rèn)行為處理起來相對(duì)繁瑣得多,Reflect 的作用就顯現(xiàn)出來了。

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 的默認(rèn)行為,Reflect 就方便得多。

細(xì)節(jié)二:多次觸發(fā) set / get

從前面的例子中可以看出,當(dāng)代理對(duì)象是數(shù)組時(shí),push 操作會(huì)觸發(fā)多次 set 執(zhí)行,同時(shí),也引發(fā) get 操作,這點(diǎn)非常重要,vue3 就很好的使用了這點(diǎn)。

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

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

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

而這對(duì)于 通知外部操作 顯然是不利,我們假設(shè) set 中的 console 是觸發(fā)外界渲染的 render 函數(shù),那么這個(gè) unshift 操作會(huì)引發(fā) 多次  render 。

我們后面會(huì)講述如何解決相應(yīng)的這個(gè)問題,繼續(xù)。

細(xì)節(jié)三: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

執(zhí)行代碼,可以看到并沒有觸發(fā) set 的輸出,反而是觸發(fā)了 get ,因?yàn)?set 的過程中訪問了 bar  這個(gè)屬性。

由此可見,proxy 代理的對(duì)象只能代理到第一層,而對(duì)象內(nèi)部的深度偵測(cè),是需要開發(fā)者自己實(shí)現(xiàn)的。同樣的,對(duì)于對(duì)象內(nèi)部的數(shù)組也是一樣。

p.ary.push('c')

// get value: ary

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

我們注意到 get/set 還有一個(gè)參數(shù):receiver ,對(duì)于 receiver ,其實(shí)接收的是一個(gè)代理對(duì)象:

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 輸出的是當(dāng)前代理對(duì)象,注意,這是一個(gè)已經(jīng)代理后的對(duì)象。

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} }

當(dāng)我們嘗試輸出 Reflect.get 返回的值,會(huì)發(fā)現(xiàn),當(dāng)代理的對(duì)象是多層結(jié)構(gòu)時(shí),Reflect.get 會(huì)返回對(duì)象的內(nèi)層結(jié)構(gòu)。
記住這一點(diǎn),Vue3 實(shí)現(xiàn)深度的proxy ,便是很好的使用了這點(diǎn)。

解決 proxy 中的細(xì)節(jié)問題

前面提到了使用 Proxy 來偵測(cè)數(shù)據(jù)變化,有幾個(gè)細(xì)節(jié)問題,包括:

  1. 使用 Reflect 來返回 trap 默認(rèn)行為

  2. 對(duì)于 set 操作,可能會(huì)引發(fā)代理對(duì)象的屬性更改,導(dǎo)致 set 執(zhí)行多次

  3. proxy  只能代理對(duì)象中的一層,對(duì)于對(duì)象內(nèi)部的操作 set 未能感知,但是 get 會(huì)被執(zhí)行

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

setTimeout 解決重復(fù) 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

程序輸出結(jié)果為一個(gè): trigger

這里實(shí)現(xiàn)了 reactive 函數(shù),接收兩個(gè)參數(shù),第一個(gè)是被代理的數(shù)據(jù) data ,還有一個(gè)回調(diào)函數(shù) cb,我們這里先簡單的在 cb 中打印 trigger 操作,來模擬通知外部數(shù)據(jù)的變化。

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

解決數(shù)據(jù)深度偵測(cè)

目前還有一個(gè)問題,那便是深度的數(shù)據(jù)偵測(cè),我們可以使用遞歸代理的方式來實(shí)現(xiàn):

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

對(duì)代理的對(duì)象進(jìn)行遍歷,對(duì)每個(gè) key 都做一次 proxy,這是遞歸實(shí)現(xiàn)的方式。同時(shí),結(jié)合前面提到的 timer 避免重復(fù) set 的問題。

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

怎么在Vue3 中偵測(cè)數(shù)據(jù)

可以看到深度代理后的對(duì)象,都攜帶 proxy 的標(biāo)志。

到這里,我們解決了使用 proxy 實(shí)現(xiàn)偵測(cè)的系列細(xì)節(jié)問題,雖然這些處理方式可以解決問題,但似乎并不夠優(yōu)雅,尤其是遞歸 proxy 是一個(gè)性能隱患,當(dāng)數(shù)據(jù)對(duì)象比較大時(shí),遞歸的 proxy 會(huì)消耗比較大的性能,并且有些數(shù)據(jù)并非需要偵測(cè),我們需要對(duì)數(shù)據(jù)偵測(cè)做更細(xì)的控制。

接下來我們就看下 Vue3 是如何使用 Proxy 實(shí)現(xiàn)數(shù)據(jù)偵測(cè)的。

Vue3 中的 reactivity

Vue3 項(xiàng)目結(jié)構(gòu)采用了 lerna 做 monorepo 風(fēng)格的代碼管理,目前比較多的開源項(xiàng)目切換到了 monorepo 的模式,比較顯著的特征是項(xiàng)目中會(huì)有個(gè) packages/ 的文件夾。

Vue3 對(duì)功能做了很好的模塊劃分,同時(shí)使用 TS 。我們直接在 packages 中找到響應(yīng)式數(shù)據(jù)的模塊:

怎么在Vue3 中偵測(cè)數(shù)據(jù)

其中,reactive.ts 文件提供了 reactive 函數(shù),該函數(shù)是實(shí)現(xiàn)響應(yīng)式的核心。同時(shí)這個(gè)函數(shù)也掛載在了全局的 Vue 對(duì)象上。

這里對(duì)源代碼做一點(diǎn)程度的簡化:

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)
 // 原數(shù)據(jù)已經(jīng)有相應(yīng)的可響應(yīng)數(shù)據(jù), 返回可響應(yīng)數(shù)據(jù)
 if (observed !== void 0) {
  return observed
 }
 // 原數(shù)據(jù)已經(jīng)是可響應(yīng)數(shù)據(jù)
 if (toRaw.has(target)) {
  return target
 }
 observed = new Proxy(target, baseHandlers)
 toProxy.set(target, observed)
 toRaw.set(observed, target)
 return observed
}

rawToReactive 和 reactiveToRaw 是兩個(gè)弱引用的 Map 結(jié)構(gòu),這兩個(gè) Map 用來保存 原始數(shù)據(jù) 和 可響應(yīng)數(shù)據(jù) ,在函數(shù) createReactiveObject 中,toProxy 和 toRaw 傳入的便是這兩個(gè) Map 。

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

除了保存了代理的數(shù)據(jù)和原始數(shù)據(jù),createReactiveObject 函數(shù)僅僅是返回了 new Proxy 代理后的對(duì)象。重點(diǎn)在 new Proxy 中傳入的handler參數(shù) baseHandlers。

還記得前面提到的 Proxy 實(shí)現(xiàn)數(shù)據(jù)偵測(cè)的細(xì)節(jié)問題吧,我們嘗試輸入:

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

打印結(jié)果:

怎么在Vue3 中偵測(cè)數(shù)據(jù)

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

問題一:如何做到深度的偵測(cè)數(shù)據(jù)的 ?

深度偵測(cè)數(shù)據(jù)是通過 createGetter 函數(shù)實(shí)現(xiàn)的,前面提到,當(dāng)對(duì)多層級(jí)的對(duì)象操作時(shí),set 并不能感知到,但是 get 會(huì)觸發(fā),于此同時(shí),利用 Reflect.get() 返回的“多層級(jí)對(duì)象中內(nèi)層” ,再對(duì)“內(nèi)層數(shù)據(jù)”做一次代理。

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

可以看到這里判斷了 Reflect 返回的數(shù)據(jù)是否還是對(duì)象,如果是對(duì)象,則再走一次 proxy ,從而獲得了對(duì)對(duì)象內(nèi)部的偵測(cè)。

并且,每一次的 proxy 數(shù)據(jù),都會(huì)保存在 Map 中,訪問時(shí)會(huì)直接從中查找,從而提高性能。

當(dāng)我們打印代理后的對(duì)象時(shí):

怎么在Vue3 中偵測(cè)數(shù)據(jù)

可以看到這個(gè)代理后的對(duì)象內(nèi)層并沒有代理的標(biāo)志,這里僅僅是代理外層對(duì)象。

輸出其中一個(gè)存儲(chǔ)代理數(shù)據(jù)的 rawToReactive :

怎么在Vue3 中偵測(cè)數(shù)據(jù)

對(duì)于內(nèi)層 ary: [1, 2] 的代理,已經(jīng)被存儲(chǔ)在了 rawToReactive 中。

由此實(shí)現(xiàn)了深度的數(shù)據(jù)偵測(cè)。

問題二:如何避免多次 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
}

關(guān)于多次 trigger 的問題,vue 處理得很巧妙。

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

輸入:

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

輸出結(jié)果:

怎么在Vue3 中偵測(cè)數(shù)據(jù)

r.push('c') 會(huì)觸發(fā) set 執(zhí)行兩次,一次是值本身 'c' ,一次是 length 屬性設(shè)置。

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

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

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

關(guān)于怎么在Vue3 中偵測(cè)數(shù)據(jù)就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。

分享標(biāo)題:怎么在Vue3中偵測(cè)數(shù)據(jù)-創(chuàng)新互聯(lián)
當(dāng)前URL:http://jinyejixie.com/article6/deciig.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供小程序開發(fā)、網(wǎng)站策劃網(wǎng)站導(dǎo)航、面包屑導(dǎo)航、外貿(mào)網(wǎng)站建設(shè)、標(biāo)簽優(yōu)化

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

搜索引擎優(yōu)化
利川市| 来安县| 安宁市| 周至县| 洪湖市| 高尔夫| 嵊泗县| 威信县| 深水埗区| 门源| 西乌珠穆沁旗| 安庆市| 华池县| 怀来县| 金华市| 谷城县| 建水县| 仲巴县| 聂拉木县| 那曲县| 西充县| 会同县| 郧西县| 连云港市| 凉城县| 平定县| 罗江县| 宁波市| 云龙县| 新干县| 普兰县| 余庆县| 云和县| 梓潼县| 新竹市| 缙云县| 内乡县| 江川县| 滦南县| 虎林市| 旺苍县|