1. 程式人生 > 實用技巧 >Vue雙向資料繫結實現

Vue雙向資料繫結實現

本篇檔案來記錄Vue雙向資料繫結的實現。

一、知識點

  • 什麼是雙向資料繫結(MVVM)?
    MVVM分別表示Model View View-Model,即模型(資料訪問層)、檢視(介面)、檢視模型(模型和檢視的通訊),是一種軟體架構模式。

    View層接收到互動資訊,通過View-Model更新Model資料,同樣,當Model資料發生變化後(一般是請求後端資料)通知View-Model使得檢視發生更新。從而實現雙向資料監聽,並修改檢視或者模型,這就是MVVM模式。
  • Vue是如何實現雙向資料繫結的?
    實現雙向資料繫結的關鍵在於如何監聽資料發生了變化,Vue2.x及以前版本通過Javascript
    內建標準物件的Object.defineProperty()方法實現。由於Object.defineProperty()無法監聽到陣列更新(準確來說是通過length增加長度監聽不到),所以Vue3.x使用Proxy作為新的資料監聽方案,Proxy可以監聽到整個物件的變更。
  • Object.defineProperty()
    此方法可直接在一個物件上定義一個新的屬性,或者修改物件的現有屬性,並返回此物件,MDN傳送門
    // 新增屬性
    const obj = {}
    Object.defineProperty(obj, 'name', {
      value: 'hua',
      writable: false
    })
    console.log(obj.name) // hua
    
    // getter setter,監聽資料變化就藉助這兩個函式
    const obj = {}
    let age = 18
    Object.defineProperty(obj, 'age', {
      get() {
        console.log('get data')
        return age
      },
      set(newValue) {
        console.log('set data')
        age = newValue
      }
    })
    obj.age = 16 // 輸出:set data
    console.log(obj.age) // 先輸出:get data 再輸出:16
    
  • Proxy
    此物件用於定義基本操作的自定義行為(如屬性查詢、賦值、列舉、函式呼叫等)MDN傳送門
    // 賦值轉發到target
    const target = {}
    const p = new Proxy(target, {})
    p.name = 'hua'
    console.log(target.name) // hua
    
    // 攔截
    const target = {}
    const p = new Proxy(target, {
      get(obj, prop) {
        return obj[prop]
      },
      set(obj, prop, value) {
        if (prop == 'name' && typeof value == 'string') {
          obj[prop] = value
        }
        return true
      }
    })
    
    p.name = 66
    console.log(target.name) // undefined
    p.name = 'hua'
    console.log(target.name) // hua
    

二、實現

按照MVVM模式進行分解實現,即先實現View觸發Model更新,再實現Model變化而更新View。

  • 建立專案
    我們先建立mvvm專案,在專案下建立vue.jsindex.html,分別作為Vue雙向資料繫結“引擎”和Vue的使用場景。

  • 回憶如何使用Vue建立專案
    我們先來看以下程式碼:

    <div id="app">
      {{ message }}
    </div>
    
    const app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue'
      }
    })
    

    是不是很熟悉,這個是Vue官網提供的宣告式渲染,我們也將以此開始我們Vue雙向資料繫結的開始。

  • 建構函式(類)Vue
    我們發現,Vue其實就是一個建構函式,接受一個物件作為引數,然後將message的值賦值給了id為app的div,那麼我們來實現

    function Vue(op) {
      const el = document.querySelector(op.el)
      el.innerHTML = op.data.meaasge
    }
    

    至此,我們可以在頁面上看到效果:
    到這裡,我們以及有了一絲絲進步,但是接下來我們要實現在輸入框輸入之後,輸入資訊在div中顯示出來。

  • input輸入改變div中的值

    • 改造html
    <input />
    <div id="app"></div>
    
    • js操作Dom實現
    const app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue'
      }
    })
    
    const input = document.querySelector('input')
    const el = document.querySelector('#app')
    
    input.oninput = function(e) {
      el.innerHTML = e.target.value
    }
    

    至此,我們實現了input輸入改變div內容的需求,但是會發現,這個改變與Vue並沒有關係。那如果與Vue有關我們應該怎麼做呢?我們發現,在Vue建構函式中,為div初始化內容的時候是這樣一段程式碼el.innerHTML = op.data.meaasge,也就是說,我們可以通過改變op.data.meaasge的值達到我們想要的結果。因此,我們可以這樣改進程式碼:

    const app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue'
      }
    })
    
    const input = document.querySelector('input')
    const el = document.querySelector('#app')
    
    input.oninput = function(e) {
      app.data.message = e.target.value
    }
    

    但是,當我們這樣改之後,並沒有達到我們的效果,為什麼呢?因為最終要改變div中的值,還是的藉助innerHTML實現,那麼我們在什麼時候去呼叫該方法呢?這裡就需要利用對data進行劫持來觸發。

  • Object.defineProperty資料劫持
    我們繼續來改造我們的建構函式Vue,監聽data的改變,然後呼叫innerHTML進行賦值。

    function observe(obj, el) {
      if (typeof obj !== 'object') {
        return
      }
    
      for (let key in obj) {
        observe(obj[key])
    
        let temp = obj[key]
    
        Object.defineProperty(obj, key, {
          get() {
            return temp
          },
          set(newValue) {
            if (temp !== newValue) {
              console.log('data changed', newValue)
              temp = newValue
              el.innerHTML = temp
            }
          }
        })
      }
    }
    
    function Vue(op) {
      const el = document.querySelector(op.el)
    
      this.data = op.data
    
      // 資料監聽
      observe(op.data, el)
    
      // 初始化
      el.innerHTML = op.data.message
    }
    

    至此,我們實現了通過監聽data變化而改變div內容的需求,但是你會發現,這裡只要有資料發生變化了,都會改變div中的內容,如果我們只想在message改變的時候才改變div中的內容,怎麼辦呢?接下來我們來引入我們的監聽者Watcher,完善我們的Vue.

  • 訂閱者Watcher、訂閱器Dep、攔截器observe

    // 攔截器
    function observe(obj, el) {
      if (typeof obj !== 'object') {
        return
      }
      for (let key in obj) {
        observe(obj[key])
    
        let temp = obj[key]
    
        let dep = new Dep()
    
        Object.defineProperty(obj, key, {
          get() {
            // 將watcher新增到訂閱器中
            dep.depend()
            return temp
          },
          set(newValue) {
            if (temp !== newValue) {
              temp = newValue
              dep.notify()
            }
          }
        })
      }
    }
    
    
    // 會有多個訂閱者,所以使用訂閱器進行管理
    class Dep {
      constructor() {
        this.subs = []
      }
    
      // 新增訂閱者
      depend() {
        if (Dep.target) {
          this.addSubs(Dep.target)
        }
      }
    
      // 通知訂閱者更新
      notify() {
        this.subs.forEach(item => {
          item.update()
        })
      }
    
      addSubs(sub) {
        this.subs.push(sub)
      }
    
    }
    
    Dep.target = null
    
    // 訂閱者
    class Watcher {
      constructor(vm, key, cb) {
        this.vm = vm
        this.key = key
        this.cb = cb
        this.value = this.get()
      }
    
      // 訂閱者更新檢視
      update() {
        const newValue = this.vm.data[this.key]
        const oldValue = this.value
        if (newValue !== oldValue) {
          // 這裡相當於將cb新增到vm物件,然後進行呼叫
          this.cb.call(this.vm, newValue, oldValue)
        }
      }
      
      // 初始化value,並利用閉包形式將watcher繫結到訂閱器中
      get () {
        Dep.target = this
        const value = this.vm.data[this.key]
        Dep.target = null
        return value
      }
    }
    
    function Vue(op) {
      const el = document.querySelector(op.el)
    
      this.data = op.data
    
      // 資料監聽
      observe(op.data, el)
    
      // 初始化
      el.innerHTML = op.data.message
    
      // 監聽message變化,只有message變化才能觸發
      new Watcher(this, 'message', function(newValue, ondValue) {
        // 在這裡對我們的div進行內容賦值
        el.innerHTML = newValue
      })
    }
    

    到這裡,我們的Vue雙向資料繫結已經完成了,讓我們一起測試我們的程式碼吧!

  • 測試

    • View改變Model
      輸入框輸入,div內容隨之變化
    • Model改變View
    const app = new Vue({
        el: '#app',
        data: {
          message: 'Hello Vue'
        }
      })
    
      const input = document.querySelector('input')
      const el = document.querySelector('#app')
    
      input.oninput = function(e) {
        app.data.message = e.target.value
      }
    
      setTimeout(function() {
        app.data.message = '過了兩秒執行'
      }, 2000)