Vue.js響應(yīng)式原理

數(shù)據(jù)驅(qū)動

在我們學(xué)習(xí)Vue.js的過程中,我們經(jīng)??吹饺齻€概念

  • 數(shù)據(jù)驅(qū)動
  • 數(shù)據(jù)響應(yīng)式
  • 雙向數(shù)據(jù)綁定

核心原理分析

  • Vue 2.x版本與Vue 3.x版本的響應(yīng)式實現(xiàn)有所不同,我們可以進(jìn)行分別講解
    • Vue 2.x響應(yīng)式基于ES5的Object.defineProperty實現(xiàn)
    • Vue 3.x響應(yīng)式基于ES6的Proxy實現(xiàn)

回顧defineProperty

我們先定義一個對象

    var obj = {
      name: 'willam',
      age: 18
    }

在defineProperty中,第一個參數(shù)為需要進(jìn)行操作的對象,第二個參數(shù)為屬性,第三個為對應(yīng)的操作

    Object.defineProperty(obj, 'gender', {
      // 值
      value: '男',
      // 是否可寫
      writable: true,
      // 控制是否可以枚舉(遍歷
      enumerable: true,
      // 本次定義之后,再次進(jìn)行重新配置
      configurable: true
    })

    Object.defineProperty(obj, 'gender', {
      enumerable: false
    })

解釋一下代碼:
賦予值:value
是否可以編輯:writable(這條屬性默認(rèn)值為false,表示只可以讀,不可以寫入)


如圖,打印結(jié)果并未發(fā)生改變

是否可以枚舉(遍歷):enumerable(這條屬性默認(rèn)值也為false)

    for (var k in obj) {
      console.log(k, obj[k])
    }
在遍歷中,如果設(shè)置的是false值,我們是無法讀取到他的值的

在本次定義之后,可否再次進(jìn)行重新配置:configurable:默認(rèn)值為false,true時可以進(jìn)行再次的配置

false值,再次進(jìn)行配置會報錯

進(jìn)行屬性操作時,可以通過getter,setter實現(xiàn),訪問器和設(shè)置器,在訪問和設(shè)置時進(jìn)行相應(yīng)的功能設(shè)置
value,writable和get,set無法共存,邏輯沖突
getter指的是:
當(dāng)我們訪問對象的屬性時,會執(zhí)行這個函數(shù)

    Object.defineProperty(obj, 'gender', {
      get () {
        // 甚至可以進(jìn)行額外的操作
        console.log('任意需要的自定義操作')
        return '男'
      },
屬性訪問時也可以設(shè)置一個事件

setter指的是:
當(dāng)我們設(shè)置某個屬性時觸發(fā)的函數(shù)

      set (newValue) {
        console.log('新的值是',newValue)
        this.gender = newValue
      }

這樣寫是一個誤區(qū),設(shè)置時觸發(fā)setter,就會造成遞歸


造成溢出

解決辦法:
通過第三方數(shù)據(jù),來存取數(shù)據(jù)

    var genderValue = '男'
    Object.defineProperty(obj, 'gender', {
      get () {
        console.log('任意需要的自定義操作')
        return genderValue
      },
      set (newValue) {
        console.log('新的值是',newValue)
        genderValue = newValue
      }
    })
解決問題

模擬Vue2響應(yīng)式原理

  • Vue2.x的數(shù)據(jù)響應(yīng)式就是由Object.defineProperty()實現(xiàn)的
    • 設(shè)置data之后,遍歷所有的屬性,轉(zhuǎn)換為getter和setter,從而在數(shù)據(jù)變化時進(jìn)行視圖更新操作

我們來寫寫模擬代碼:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">原始內(nèi)容</div>
  <script>
    // 聲明一個對象用于進(jìn)行數(shù)據(jù)存儲
    let data = {
      msg: 'hello'
    }
    // 模擬一個vue實例
    let vm = {}
    // 通過數(shù)據(jù)劫持的方式,將data的屬性設(shè)置給getter與setter,并且設(shè)置給vm
    Object.defineProperty(vm, 'msg', {
      // 可遍歷
      enumerable: true,
      // 可配置
      configurable: true,
      // get方法
      get () {
        console.log('訪問數(shù)據(jù)')
        return data.msg
      },
      // set方法
      set (newValue) {
        // 更新數(shù)據(jù)
        data.msg = newValue
        // 數(shù)據(jù)更改,更新視圖中DOM元素內(nèi)容
        document.querySelector('#app').textContent = data.msg
      }
    })
  </script>
</body>
</html>

解釋一下代碼,vm的作用就是通過數(shù)據(jù)劫持將data中的數(shù)據(jù)設(shè)置給get與set,并且設(shè)置給vm,最后更改的還是data


改進(jìn)

  • 操作中只監(jiān)聽了一個屬性,多個屬性無法處理
  • 無法監(jiān)聽數(shù)組變化(Vue里也是同樣存在這個問題)
  • 無法處理屬性也為對象的情況

處理多個屬性的情況

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">原始內(nèi)容</div>
  <script>
    // 聲明一個對象用于進(jìn)行數(shù)據(jù)存儲
    let data = {
      msg1: 'hello',
      msg2: 'world'
    }
    // 模擬一個vue實例
    let vm = {}
    Object.keys(data).forEach(key => {
      // 通過數(shù)據(jù)劫持的方式,將data的屬性設(shè)置給getter與setter,并且設(shè)置給vm
      Object.defineProperty(vm, key, {
        enumerable: true,
        configurable: true,
        // get方法
        get () {
          console.log('訪問數(shù)據(jù)')
          return data[key]
        },
        // set方法
        set (newValue) {
          // 更新數(shù)據(jù)
          data[key] = newValue
          // 數(shù)據(jù)更改,更新視圖中DOM元素內(nèi)容
          document.querySelector('#app').textContent = data[key]
        }
      })
    })
  </script>
</body>
</html>

這里我們使用到了Object.keys()方法,該方法可以返回一個由內(nèi)部參數(shù)對象的自身可枚舉屬性構(gòu)成的一個數(shù)組,然后我們再將其進(jìn)行forEach遍歷,得到每一個屬性,然后進(jìn)行多個屬性的處理,詳細(xì)邏輯可以通過代碼看的一清二楚

檢測數(shù)組的方法

對數(shù)組的操作是無法實現(xiàn)響應(yīng)式數(shù)據(jù)實現(xiàn)的
Vue通過特定的方法處理可以解決這種問題

  • 添加數(shù)組方法支持:
   const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
  • 準(zhǔn)備一個用于存儲處理結(jié)果的對象,準(zhǔn)備替換掉數(shù)組屬性的原型指針
    // 存儲處理結(jié)果的對象,準(zhǔn)備替換到數(shù)組數(shù)組實例的原型指針 _proto_
    const customProto = {}
  • 為了確保原始功能能夠被使用
        // 確保原始功能可以使用,this為數(shù)組實例
        const result = Array.prototype[method].apply(this, arguments)
  • 進(jìn)行其他自定義設(shè)置,比如更新視圖
        // 進(jìn)行其他自定義功能設(shè)置,比如,更新視圖
        document.querySelector('#app').textContent = this
        return result
  • 為了避免數(shù)組實例無法再使用我們處理的方法以外的方法:
    // 為了避免數(shù)組實例無法再使用其他的數(shù)組方法
    customProto.__proto__ = Array.prototype
  • 那么如何將這些設(shè)置與攔截寫在一起呢?
    答案很簡單:判斷一下就行了

    完整代碼
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">原始內(nèi)容</div>
  <script>
    // 聲明一個對象用于進(jìn)行數(shù)據(jù)存儲
    let data = {
      msg1: 'hello',
      msg2: 'world',
      arr: [1, 2, 3]
    }
    // 模擬一個vue實例
    let vm = {}

    // 添加數(shù)組方法的支持
    const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

    // 存儲處理結(jié)果的對象,準(zhǔn)備替換到數(shù)組數(shù)組實例的原型指針 _proto_
    const customProto = {}

    // 為了避免數(shù)組實例無法再使用其他的數(shù)組方法
    customProto.__proto__ = Array.prototype

    arrMethodName.forEach(method => {
      customProto[method] = function () {
        // 確保原始功能可以使用,this為數(shù)組實例
        const result = Array.prototype[method].apply(this, arguments)
        
        // 進(jìn)行其他自定義功能設(shè)置,比如,更新視圖
        document.querySelector('#app').textContent = this
        return result
      }
    })

    Object.keys(data).forEach(key => {
      // 檢測是否為數(shù)組,是的話單獨處理
      if (Array.isArray(data[key])) {
        // 將當(dāng)前數(shù)組實例的__proto__更換為customProto就行了
        data[key].__proto__ = customProto
      }

      // 通過數(shù)據(jù)劫持的方式,將data的屬性設(shè)置給getter與setter,并且設(shè)置給vm
      Object.defineProperty(vm, key, {
        enumerable: true,
        configurable: true,
        // get方法
        get () {
          console.log('訪問數(shù)據(jù)')
          return data[key]
        },
        // set方法
        set (newValue) {
          // 更新數(shù)據(jù)
          data[key] = newValue
          // 數(shù)據(jù)更改,更新視圖中DOM元素內(nèi)容
          document.querySelector('#app').textContent = data[key]
        }
      })
    })
  </script>
</body>
</html>

改進(jìn):封裝與遞歸

使用立即執(zhí)行函數(shù),全部包裹起來,如果對象內(nèi)部還含有對象的話就進(jìn)行遞歸處理,很簡單的邏輯:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">原始內(nèi)容</div>
  <script>
    // 聲明數(shù)據(jù)對象,模擬 Vue 實例的 data 屬性
    let data = {
      msg1: 'hello',
      msg2: 'world',
      arr: [1, 2, 3],
      obj: {
        name: 'jack',
        age: 18
      }
    }
    // 模擬 Vue 實例的對象
    let vm = {}

    // 封裝為函數(shù),用于對數(shù)據(jù)進(jìn)行響應(yīng)式處理
    const createReactive = (function () {
      const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
      const customProto = {}
      customProto.__proto__ = Array.prototype
      arrMethodName.forEach(method => {
        customProto[method] = function () {
          const result = Array.prototype[method].apply(this, arguments)
          document.querySelector('#app').textContent = this
          return result
        }
      })

      // 需要進(jìn)行數(shù)據(jù)劫持的主體功能,也是遞歸時需要的功能
      return function (data, vm) {
        // 遍歷被劫持對象的所有屬性
        Object.keys(data).forEach(key => {
          // 檢測是否為數(shù)組
          if (Array.isArray(data[key])) {
            // 將當(dāng)前數(shù)組實例的 __proto__ 更換為 customProto 即可
            data[key].__proto__ = customProto
          } else if (typeof data[key] === 'object' && data[key] !== null) {
            // 檢測是否為對象,如果為對象,進(jìn)行遞歸操作
            vm[key] = {}
            createReactive(data[key], vm[key])
            return
          }

          // 通過數(shù)據(jù)劫持的方式,將 data 的屬性設(shè)置為 getter/setter
          Object.defineProperty(vm, key, {
            enumerable: true,
            configurable: true,
            get () {
              console.log('訪問了屬性')
              return data[key]
            },
            set (newValue) {
              // 更新數(shù)據(jù)
              data[key] = newValue
              // 數(shù)據(jù)更改,更新視圖中 DOM 元素的內(nèi)容
              document.querySelector('#app').textContent = data[key]
            }
          })
        })
      }

    })()
  
    createReactive(data, vm)
  </script>
</body>
</html>

這就是Vue2版本的響應(yīng)式原理分析

回顧Proxy

ES6提供的一個功能,對一個對象提供代理操作

  <script>
    const data = {
      msg1: '內(nèi)容',
      arr: [1, 2, 3],
      obj: {
        name: 'willam',
        age: 19
      }
    }
    
    const P = new Proxy(data, {
      get (target, property, receiver) {
        console.log(target, property, receiver)
        return target[property]
      },
      set (target, property, value, receiver) {
        console.log(target, property, value, receiver)
        target[property] = value
      }
    })
  </script>

通過代理,訪問P也就是訪問了data的代理,同樣的數(shù)據(jù),get方法中,target參數(shù)表示原數(shù)據(jù)data,property表示訪問的哪條屬性,receiver表示通過代理之后的數(shù)據(jù)
set方法中新添了一個value參數(shù),表示當(dāng)前設(shè)置的數(shù)值
我們來通過控制臺打印一探究竟


Vue3響應(yīng)式原理

與2版本的區(qū)別為數(shù)據(jù)響應(yīng)式是Proxy實現(xiàn)的,其他相同,接下來進(jìn)行演示

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">原始內(nèi)容</div>
  <script>
    const data = {
      msg1: '內(nèi)容',
      arr: [1, 2, 3],
      content: 'world',
      obj: {
        name: 'willam',
        age: 19
      }
    }

    const vm = new Proxy(data, {
      get (target, key) {
        return target[key]
      },
      set (target, key, newValue) {
        // 數(shù)據(jù)更新
        target[key] = newValue
        // 視圖更新
        document.querySelector('#app').textContent = target[key]
      }
    })
  </script>
</body>
</html>

對深層監(jiān)控啊,屬性監(jiān)控啊,遍歷啊都不需要在Vue3進(jìn)行操作了,通過Proxy代理可以輕松解決,但是由于ES6的Proxy方法兼容性不是那么的好,所以市面上Vue3的普及度并不是太高,一切走向都需要根據(jù)市場來確定

相關(guān)設(shè)計模式

設(shè)計模式:針對軟件設(shè)計中普遍存在的各種問題所提出的解決方案

觀察者模式

指的是在對象間定義一個一對多(被觀察者與多個觀察者)的關(guān)聯(lián),當(dāng)一個對象改變了狀態(tài),所有其他相關(guān)的對象會被通知并且自動刷新

就像是超市有一堆顧客,超市出了促銷活動,會通知顧客(觀察者),又因為當(dāng)前是否想要購物,進(jìn)行不同的選擇行動

  • 核心概念:
    • 觀察者Observer
    • 被觀察者(觀察目標(biāo))Subject

      設(shè)計的核心點就是設(shè)置一個被觀察者,設(shè)置一個或者多個的觀察者,在被觀察者中設(shè)置一個遍歷進(jìn)行操作
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    // 被觀察者(觀察目標(biāo))
    // 1.需要能夠添加觀察者
    // 2.通知所有觀察者的功能
    class Subject {
      constructor () {
        // 存儲所有的觀察者
        this.observers = []
      }
      // 添加觀察者功能
      addObserver (observer) {
        // 檢測傳入的參數(shù)是否為觀察者實例
        if (observer && observer.update) {
          this.observers.push(observer)
        }
      }
      // 通知所有的觀察者
      notify () {
        // 調(diào)用觀察者列表中的每個觀察者的更新方法
        this.observers.forEach(observer => {
          observer.update()
        })
      }
    }
    // 觀察者
    // 1.被觀察者發(fā)生狀態(tài)變化時,做一些對應(yīng)的操作“更新”
    class Observer {
      update () {
        console.log('事件發(fā)生了,進(jìn)行一個相應(yīng)的處理...')
      }
    }

    // 功能測試
    const subject = new Subject()
    const ob1 = new Observer()
    const ob2 = new Observer()

    // 將觀察者添加給要觀察的觀察目標(biāo)
    subject.addObserver(ob1)
    subject.addObserver(ob2)

    // 通知觀察者進(jìn)行操作(某些具體的場景下)
    subject.notify()
  </script>
</body>
</html>

通過觀察者模式為不同的數(shù)據(jù)設(shè)置不同的觀察者,監(jiān)視被觀察者的情況,通過特定的方法進(jìn)行更新操作等等

發(fā)布-訂閱模式

可以認(rèn)為是為觀察者模式的解耦的進(jìn)階版本,特點是:

  • 在發(fā)布者和訂閱者之間添加一個消息中心,所有的消息均通過消息中心管理,而發(fā)布者與訂閱者不會直接聯(lián)系,實現(xiàn)了兩張的解耦

核心概念:

  • 消息中心Dep
  • 訂閱者Subscriber
  • 發(fā)布者Publisher


<body>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
  <script>
    // 創(chuàng)建了一個Vue實例(消息中心)
    const eventBus = new Vue()

    // 注冊事件(設(shè)置訂閱者)
    eventBus.$on('dataChange', () => {
      console.log('事件處理功能1')
    })

    eventBus.$on('dataChange', () => {
      console.log('事件處理功能2')
    })

    // 觸發(fā)事件(設(shè)置發(fā)布者)
    eventBus.$emit('dataChange')
  </script>
</body>

設(shè)計模式小結(jié)

  • 觀察者模式是由觀察者和觀察目標(biāo)組成的,適合組件內(nèi)部操作(功能簡單就可以)
    • 特性:特殊事件發(fā)生后,觀察目標(biāo)統(tǒng)一通知所有的觀察者
  • 發(fā)布/訂閱模式由發(fā)布者與訂閱者以及消息中心組成,更加適合消息類型復(fù)雜的情況
    • 特性:特殊事件發(fā)生,消息中心接到發(fā)布指令后,會根據(jù)事件類型給對應(yīng)的訂閱者發(fā)送信息

響應(yīng)式原理模擬

整體分析

要模擬Vue實現(xiàn)響應(yīng)式數(shù)據(jù),首先我們需要觀察一下Vue實例的結(jié)構(gòu),分析要實現(xiàn)哪些屬性和功能


  • Vue:
    • 目標(biāo):將data數(shù)據(jù)注入到Vue實例,便于方法內(nèi)操作
  • Observer(發(fā)布者)
    • 目標(biāo):數(shù)據(jù)劫持,監(jiān)聽數(shù)據(jù)變化,并在變化時通知Dep
  • Dep(消息中心)
    • 目標(biāo):存儲訂閱者以及管理消息的發(fā)送
  • Watcher(訂閱者)
    • 目標(biāo):當(dāng)訂閱數(shù)據(jù)變化,進(jìn)行視圖更新
  • Compiler
    • 目標(biāo):解析模板中的指令與插值表達(dá)式,并替換成相應(yīng)的數(shù)據(jù)

Vue類

  • 功能:
    • 接受配置信息
    • 將data的屬性轉(zhuǎn)換為Getter、setter,并且注入到Vue實例中
    • *監(jiān)聽data中所有屬性的變化,設(shè)置成響應(yīng)式數(shù)據(jù)
    • *調(diào)用解析功能(解析模板內(nèi)的插值表達(dá)式,指令等等)


      1.存儲配置選項,2.掛載元素,3.設(shè)置數(shù)據(jù)屬性,最后通過proxyData將data屬性都設(shè)置到vue實例

      n _proxyData (target, data) {
      Object.keys(data).forEach(key => {
      Object.defineProperty(target, key,{
      enumerable: true,
      configurable: true,
      get () {
      return data[key]
      },
      set (newValue) {
      data[key] = newValue
      }
      })
      })
      }

Observer類

  • 功能:
  • 通過數(shù)據(jù)劫持方式監(jiān)視data中的屬性變化,變化時通知消息中心Dep
  • 需要考慮data的屬性也可能為對象,也要轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)


Dep類

  • Dep是dependency的簡寫,含義是“依賴”,指的是Dep用于收集與管理訂閱者與發(fā)布者之間的依賴關(guān)系
  • 功能:
    • *為每個數(shù)據(jù)收集對應(yīng)的依賴,存儲依賴
    • 添加并存儲訂閱者
    • 數(shù)據(jù)變化時,通知所有的觀察者


Watcher 類

  • 功能:
    • 實例化Watch時,往dep對象中添加自己
    • 當(dāng)數(shù)據(jù)變化觸發(fā)dep,dep通知所有對應(yīng)的Watcher實例更新視圖


Complier類

  • 功能:
    • 進(jìn)行編譯模板,并解析內(nèi)部指令與插值表達(dá)式
    • 進(jìn)行頁面的首次渲染
    • 數(shù)據(jù)變化后,重新渲染視圖


功能回顧與總結(jié)

  • Vue類
    • 把data的屬性注入到Vue實例
    • 調(diào)用Observer實現(xiàn)數(shù)據(jù)響應(yīng)式處理
    • 調(diào)用Compiler編譯模板
  • Observer
    • 將data的屬性轉(zhuǎn)換為Getter/setter
    • 為Dep添加訂閱者Watcher
    • 數(shù)據(jù)變化發(fā)送時通知Dep
  • Dep
    • 收集依賴,添加訂閱者(Watcher)
    • 通知訂閱者
  • Watcher
    • 編譯模板時創(chuàng)建訂閱者,訂閱數(shù)據(jù)變化
    • 接到Dep通知時,調(diào)用Compiler中的模板功能更新視圖
  • Compiler
    • 編譯模板,解析指令與插值表達(dá)式
    • 負(fù)責(zé)頁面首次渲染與數(shù)據(jù)變化后重新渲染


      功能總結(jié)圖
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 今天感恩節(jié)哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開了第一次的黨會,身份的轉(zhuǎn)變要...
    余生動聽閱讀 10,807評論 0 11
  • 彩排完,天已黑
    劉凱書法閱讀 4,467評論 1 3
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來的情緒。表情可以傳達(dá)很多信息。高興了當(dāng)然就笑了,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 129,528評論 2 7

友情鏈接更多精彩內(nèi)容