亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

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

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

defineProperty和Proxy如何實現的響應式系統

發布時間:2021-09-09 15:43:13 來源:億速云 閱讀:175 作者:小新 欄目:web開發

這篇文章主要介紹了defineProperty和Proxy如何實現的響應式系統,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

一、極簡雙向綁定

首先從最簡單的雙向綁定入手:

// html
<input type="text" id="input">
<span id="span"></span>
// js
let input = document.getElementById('input')
let span = document.getElementById('span')
input.addEventListener('keyup', function(e) {
 span.innerHTML = e.target.value
})

以上似乎運行起來也沒毛病,但我們要的是數據驅動,而不是直接操作dom:

// 操作obj數據來驅動更新
let obj = {}
let input = document.getElementById('input')
let span = document.getElementById('span')
Object.defineProperty(obj, 'text', {
 configurable: true,
 enumerable: true,
 get() {
  console.log('獲取數據了')
  return obj.text
 },
 set(newVal) {
  console.log('數據更新了')
  input.value = newVal
  span.innerHTML = newVal
 }
})
input.addEventListener('keyup', function(e) {
 obj.text = e.target.value
})

以上就是一個簡單的雙向數據綁定,但顯然是不足的,下面繼續升級。

二、以defineProperty實現響應系統

在Vue3版本來臨前以defineProperty實現的數據響應,基于發布訂閱模式,其主要包含三部分:Observer、Dep、Watcher。

1. 一個思路例子

// 需要劫持的數據
let data = {
 a: 1,
 b: {
  c: 3
 }
}

// 劫持數據data
observer(data)

// 監聽訂閱數據data的屬性
new Watch('a', () => {
  alert(1)
})
new Watch('a', () => {
  alert(2)
})
new Watch('b.c', () => {
  alert(3)
})

以上就是一個簡單的劫持和監聽流程,那對應的observer和Watch該如何實現?

2. Observer

observer的作用就是劫持數據,將數據屬性轉換為訪問器屬性,理一下實現思路:

①Observer需要將數據轉化為響應式的,那它就應該是一個函數(類),能接收參數。
②為了將數據變成響應式,那需要使用Object.defineProperty。
③數據不止一種類型,這就需要遞歸遍歷來判斷。

// 定義一個類供傳入監聽數據
class Observer {
 constructor(data) {
  let keys = Object.keys(data)
  for (let i = 0; i < keys.length; i++) {
   defineReactive(data, keys[i], data[keys[i]])
  }
 }
}
// 使用Object.defineProperty
function defineReactive (data, key, val) {
 // 每次設置訪問器前都先驗證值是否為對象,實現遞歸每個屬性
 observer(val)
 // 劫持數據屬性
 Object.defineProperty(data, key, {
  configurable: true,
  enumerable: true,
  get () {
   return val
  },
  set (newVal) {
   if (newVal === val) {
    return
   } else {
    data[key] = newVal
    // 新值也要劫持
    observer(newVal)
   }
  }
 })
}

// 遞歸判斷
function observer (data) {
 if (Object.prototype.toString.call(data) === '[object, Object]') {
  new Observer(data)
 } else {
  return
 }
}

// 監聽obj
observer(data)

3. Watcher

根據new Watch('a', () => {alert(1)})我們猜測Watch應該是這樣的:

class Watch {
 // 第一個參數為表達式,第二個參數為回調函數
 constructor (exp, cb) {
  this.exp = exp
  this.cb = cb
 }
}

那Watch和observer該如何關聯?想想它們之間有沒有關聯的點?似乎可以從exp下手,這是它們共有的點:

class Watch {
 // 第一個參數為表達式,第二個參數為回調函數
 constructor (exp, cb) {
  this.exp = exp
  this.cb = cb
  data[exp]  // 想想多了這句有什么作用
 }
}

data[exp]這句話是不是表示在取某個值,如果exp為a的話,那就表示data.a,在這之前data下的屬性已經被我們劫持為訪問器屬性了,那這就表明我們能觸發對應屬性的get函數,那這就與observer產生了關聯,那既然如此,那在觸發get函數的時候能不能把觸發者Watch給收集起來呢?此時就得需要一個橋梁Dep來協助了。

4. Dep

思路應該是data下的每一個屬性都有一個唯一的Dep對象,在get中收集僅針對該屬性的依賴,然后在set方法中觸發所有收集的依賴,這樣就搞定了,看如下代碼:

class Dep {
 constructor () {
  // 定義一個收集對應屬性依賴的容器
  this.subs = []
 }
 // 收集依賴的方法
 addSub () {
  // Dep.target是個全局變量,用于存儲當前的一個watcher
  this.subs.push(Dep.target)
 }
 // set方法被觸發時會通知依賴
 notify () {
  for (let i = 1; i < this.subs.length; i++) {
   this.subs[i].cb()
  }
 }
}

Dep.target = null

class Watch {
 constructor (exp, cb) {
  this.exp = exp
  this.cb = cb
  // 將Watch實例賦給全局變量Dep.target,這樣get中就能拿到它了
  Dep.target = this
  data[exp]
 }
}

此時對應的defineReactive我們也要增加一些代碼:

function defineReactive (data, key, val) {
 observer()
 let dep = new Dep() // 新增:這樣每個屬性就能對應一個Dep實例了
 Object.defineProperty(data, key, {
  configurable: true,
  enumerable: true,
  get () {
   dep.addSub() // 新增:get觸發時會觸發addSub來收集當前的Dep.target,即watcher
   return val
  },
  set (newVal) {
   if (newVal === val) {
    return
   } else {
    data[key] = newVal
    observer(newVal)
    dep.notify() // 新增:通知對應的依賴
   }
  }
 })
}

至此observer、Dep、Watch三者就形成了一個整體,分工明確。但還有一些地方需要處理,比如我們直接對被劫持過的對象添加新的屬性是監測不到的,修改數組的元素值也是如此。這里就順便提一下Vue源碼中是如何解決這個問題的:

對于對象:Vue中提供了Vue.set和vm.$set這兩個方法供我們添加新的屬性,其原理就是先判斷該屬性是否為響應式的,如果不是,則通過defineReactive方法將其轉為響應式。

對于數組:直接使用下標修改值還是無效的,Vue只hack了數組中的七個方法:pop','push','shift','unshift','splice','sort','reverse',使得我們用起來依舊是響應式的。其原理是:在我們調用數組的這七個方法時,Vue會改造這些方法,它內部同樣也會執行這些方法原有的邏輯,只是增加了一些邏輯:取到所增加的值,然后將其變成響應式,然后再手動出發dep.notify()

三、以Proxy實現響應系統

Proxy是在目標前架設一層"攔截",外界對該對象的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫,我們可以這樣認為,Proxy是Object.defineProperty的全方位加強版。

依舊是三大件:Observer、Dep、Watch,我們在之前的基礎再完善這三大件。

1. Dep

let uid = 0 // 新增:定義一個id
class Dep {
 constructor () {
  this.id = uid++ // 新增:給dep添加id,避免Watch重復訂閱
  this.subs = []
 }
 depend() { // 新增:源碼中在觸發get時是先觸發depend方法再進行依賴收集的,這樣能將dep傳給Watch
  Dep.target.addDep(this);
 }
 addSub () {
  this.subs.push(Dep.target)
 }
 notify () {
  for (let i = 1; i < this.subs.length; i++) {
   this.subs[i].cb()
  }
 }
}

2. Watch

class Watch {
 constructor (exp, cb) {
  this.depIds = {} // 新增:儲存訂閱者的id,避免重復訂閱
  this.exp = exp
  this.cb = cb
  Dep.target = this
  data[exp]
  // 新增:判斷是否訂閱過該dep,沒有則存儲該id并調用dep.addSub收集當前watcher
  addDep (dep) { 
   if (!this.depIds.hasOwnProperty(dep.id)) {
    dep.addSub(this)
    this.depIds[dep.id] = dep
   }
  }
  // 新增:將訂閱者放入待更新隊列等待批量更新
  update () {
   pushQueue(this)
  }
  // 新增:觸發真正的更新操作
  run () {
   this.cb()
  }
 }
}

3. Observer

與Object.defineProperty監聽屬性不同,Proxy可以監聽(實際是代理)整個對象,因此就不需要遍歷對象的屬性依次監聽了,但是如果對象的屬性依然是個對象,那么Proxy也無法監聽,所以依舊使用遞歸套路即可。

function Observer (data) {
 let dep = new Dep()
 return new Proxy(data, {
  get () {
   // 如果訂閱者存在,進去depend方法
   if (Dep.target) {
    dep.depend()
   }
   // Reflect.get了解一下
   return Reflect.get(data, key)
  },
  set (data, key, newVal) {
   // 如果值未變,則直接返回,不觸發后續操作
   if (Reflect.get(data, key) === newVal) {
    return
   } else {
    // 設置新值的同時對新值判斷是否要遞歸監聽
    Reflect.set(target, key, observer(newVal))
    // 當值被觸發更改的時候,觸發Dep的通知方法
    dep.notify(key)
   }
  }
 })
}

// 遞歸監聽
function observer (data) {
 // 如果不是對象則直接返回
 if (Object.prototype.toString.call(data) !== '[object, Object]') {
  return data
 }
 // 為對象時則遞歸判斷屬性值
 Object.keys(data).forEach(key => {
  data[key] = observer(data[key])
 })
 return Observer(data)
}

// 監聽obj
Observer(data)

至此就基本完成了三大件了,同時其不需要hack也能對數組進行監聽。

四、觸發依賴收集與批量異步更新

完成了響應式系統,也順便提一下Vue源碼中是如何觸發依賴收集與批量異步更新的。

1. 觸發依賴收集

在Vue源碼中的$mount方法調用時會間接觸發了一段代碼:

vm._watcher = new Watcher(vm, () => {
 vm._update(vm._render(), hydrating)
}, noop)

這使得new Watcher()會先對其傳入的參數進行求值,也就間接觸發了vm._render(),這其實就會觸發了對數據的訪問,進而觸發屬性的get方法而達到依賴的收集。

2. 批量異步更新

Vue在更新DOM時是異步執行的。只要偵聽到數據變化,Vue將開啟一個隊列,并緩沖在同一事件循環中發生的所有數據變更。如果同一個watcher被多次觸發,只會被推入到隊列中一次。這種在緩沖時去除重復數據對于避免不必要的計算和DOM操作是非常重要的。然后,在下一個的事件循環“tick”中,Vue刷新隊列并執行實際 (已去重的) 工作。Vue在內部對異步隊列嘗試使用原生的Promise.then、MutationObserver和setImmediate,如果執行環境不支持,則會采用setTimeout(fn, 0)代替。

根據以上這段官方文檔,這個隊列主要是異步和去重,首先我們來整理一下思路:

  1. 需要有一個隊列來存儲一個事件循環中的數據變更,且要對它去重。

  2. 將當前事件循環中的數據變更添加到隊列。

  3. 異步的去執行這個隊列中的所有數據變更。

// 使用Set數據結構創建一個隊列,這樣可自動去重
let queue = new Set()

// 在屬性出發set方法時會觸發watcher.update,繼而執行以下方法
function pushQueue (watcher) {
 // 將數據變更添加到隊列
 queue.add(watcher)
 // 下一個tick執行該數據變更,所以nextTick接受的應該是一個能執行queue隊列的函數
 nextTick('一個能遍歷執行queue的函數')
}

// 用Promise模擬nextTick
function nextTick('一個能遍歷執行queue的函數') {
 Promise.resolve().then('一個能遍歷執行queue的函數')
}

以上已經有個大體的思路了,那接下來完成'一個能遍歷執行queue的函數':

// queue是一個數組,所以直接遍歷執行即可
function flushQueue () {
 queue.forEach(watcher => {
  // 觸發watcher中的run方法進行真正的更新操作
  watcher.run()
 })
 // 執行后清空隊列
 queue = new Set()
}

還有一個問題,那就是同一個事件循環中應該只要觸發一次nextTick即可,而不是每次添加隊列時都觸發:

// 設置一個是否觸發了nextTick的標識
let waiting = false
function pushQueue (watcher) {
 queue.add(watcher)
 if (!waiting) {
  // 保證nextTick只觸發一次
  waiting = true
  nextTick('一個能遍歷執行queue的函數')
 }
}

完整代碼如下:

// 定義隊列
let queue = new Set()

// 供傳入nextTick中的執行隊列的函數
function flushQueue () {
 queue.forEach(watcher => {
  watcher.run()
 })
 queue = new Set()
}

// nextTick
function nextTick(flushQueue) {
 Promise.resolve().then(flushQueue)
}

// 添加到隊列并調用nextTick
let waiting = false
function pushQueue (watcher) {
 queue.add(watcher)
 if (!waiting) {
  waiting = true
  nextTick(flushQueue)
 }
}

感謝你能夠認真閱讀完這篇文章,希望小編分享的“defineProperty和Proxy如何實現的響應式系統”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!

向AI問一下細節

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

AI

鹤壁市| 开鲁县| 敦煌市| 永善县| 云霄县| 茶陵县| 贵南县| 修武县| 合作市| 淮南市| 华亭县| 崇文区| 沾化县| 汉川市| 名山县| 永仁县| 娱乐| 探索| 甘孜| 横山县| 历史| 洛川县| 始兴县| 嘉禾县| 满洲里市| 兴隆县| 华坪县| 宜昌市| 安溪县| 张北县| 中山市| 闻喜县| 吴川市| 翁牛特旗| 屯昌县| 衡山县| 阿拉尔市| 绥中县| 陇川县| 社会| 鄂伦春自治旗|