国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 開發(fā) > JS > 正文

這應該是最詳細的響應式系統(tǒng)講解了

2024-05-06 16:53:31
字體:
來源:轉載
供稿:網友

前言

本文從一個簡單的雙向綁定開始,逐步升級到由defineProperty和Proxy分別實現(xiàn)的響應式系統(tǒng),注重入手思路,抓住關鍵細節(jié),希望能對你有所幫助。

一、極簡雙向綁定

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

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

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

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

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

二、以defineProperty實現(xiàn)響應系統(tǒng)

在Vue3版本來臨前以defineProperty實現(xiàn)的數(shù)據(jù)響應,基于發(fā)布訂閱模式,其主要包含三部分:Observer、Dep、Watcher。

1. 一個思路例子

// 需要劫持的數(shù)據(jù)let data = { a: 1, b: {  c: 3 }}// 劫持數(shù)據(jù)dataobserver(data)// 監(jiān)聽訂閱數(shù)據(jù)data的屬性new Watch('a', () => {  alert(1)})new Watch('a', () => {  alert(2)})new Watch('b.c', () => {  alert(3)})

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

2. Observer

observer的作用就是劫持數(shù)據(jù),將數(shù)據(jù)屬性轉換為訪問器屬性,理一下實現(xiàn)思路:

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

// 定義一個類供傳入監(jiān)聽數(shù)據(jù)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.definePropertyfunction defineReactive (data, key, val) { // 每次設置訪問器前都先驗證值是否為對象,實現(xiàn)遞歸每個屬性 observer(val) // 劫持數(shù)據(jù)屬性 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 }}// 監(jiān)聽objobserver(data)

3. Watcher

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

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

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

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

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

4. Dep

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

class Dep { constructor () {  // 定義一個收集對應屬性依賴的容器  this.subs = [] } // 收集依賴的方法 addSub () {  // Dep.target是個全局變量,用于存儲當前的一個watcher  this.subs.push(Dep.target) } // set方法被觸發(fā)時會通知依賴 notify () {  for (let i = 1; i < this.subs.length; i++) {   this.subs[i].cb()  } }}Dep.target = nullclass 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觸發(fā)時會觸發(fā)addSub來收集當前的Dep.target,即watcher   return val  },  set (newVal) {   if (newVal === val) {    return   } else {    data[key] = newVal    observer(newVal)    dep.notify() // 新增:通知對應的依賴   }  } })}

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

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

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

三、以Proxy實現(xiàn)響應系統(tǒng)

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

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

1. Dep

let uid = 0 // 新增:定義一個idclass Dep { constructor () {  this.id = uid++ // 新增:給dep添加id,避免Watch重復訂閱  this.subs = [] } depend() { // 新增:源碼中在觸發(fā)get時是先觸發(fā)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)  }  // 新增:觸發(fā)真正的更新操作  run () {   this.cb()  } }}

3. Observer

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

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) {   // 如果值未變,則直接返回,不觸發(fā)后續(xù)操作   if (Reflect.get(data, key) === newVal) {    return   } else {    // 設置新值的同時對新值判斷是否要遞歸監(jiān)聽    Reflect.set(target, key, observer(newVal))    // 當值被觸發(fā)更改的時候,觸發(fā)Dep的通知方法    dep.notify(key)   }  } })}// 遞歸監(jiān)聽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)}// 監(jiān)聽objObserver(data)

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

四、觸發(fā)依賴收集與批量異步更新

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

1. 觸發(fā)依賴收集

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

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

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

2. 批量異步更新

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

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

  1. 需要有一個隊列來存儲一個事件循環(huán)中的數(shù)據(jù)變更,且要對它去重。
  2. 將當前事件循環(huán)中的數(shù)據(jù)變更添加到隊列。
  3. 異步的去執(zhí)行這個隊列中的所有數(shù)據(jù)變更。
// 使用Set數(shù)據(jù)結構創(chuàng)建一個隊列,這樣可自動去重let queue = new Set()// 在屬性出發(fā)set方法時會觸發(fā)watcher.update,繼而執(zhí)行以下方法function pushQueue (watcher) { // 將數(shù)據(jù)變更添加到隊列 queue.add(watcher) // 下一個tick執(zhí)行該數(shù)據(jù)變更,所以nextTick接受的應該是一個能執(zhí)行queue隊列的函數(shù) nextTick('一個能遍歷執(zhí)行queue的函數(shù)')}// 用Promise模擬nextTickfunction nextTick('一個能遍歷執(zhí)行queue的函數(shù)') { Promise.resolve().then('一個能遍歷執(zhí)行queue的函數(shù)')}

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

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

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

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

完整代碼如下:

// 定義隊列l(wèi)et queue = new Set()// 供傳入nextTick中的執(zhí)行隊列的函數(shù)function flushQueue () { queue.forEach(watcher => {  watcher.run() }) queue = new Set()}// nextTickfunction nextTick(flushQueue) { Promise.resolve().then(flushQueue)}// 添加到隊列并調用nextTicklet waiting = falsefunction pushQueue (watcher) { queue.add(watcher) if (!waiting) {  waiting = true  nextTick(flushQueue) }}

最后

以上就是響應式的一個大概原理,希望對大家的學習有所幫助,也希望大家多多支持VeVb武林網。

 

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VeVb武林網。


注:相關教程知識閱讀請移步到JavaScript/Ajax教程頻道。
發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 和静县| 尉氏县| 宝兴县| 华坪县| 广河县| 武山县| 浮梁县| 大厂| 宁国市| 焦作市| 赤水市| 兴安县| 维西| 鲁甸县| 通河县| 金门县| 蒲江县| 个旧市| 德惠市| 临海市| 商都县| 宿松县| 永定县| 建始县| 纳雍县| 荔波县| 固镇县| 湘潭县| 股票| 浦江县| 武城县| 额尔古纳市| 密云县| 丹江口市| 阳新县| 贺州市| 故城县| 阜新市| 团风县| 棋牌| 磴口县|