vue雙向數據綁定實現原理學習筆記

參考鏈接:https://www.cnblogs.com/kidney/p/6052935.html
黃軼的源碼解讀:https://github.com/DDFE/DDFE-blog/issues/7

一、雙向數據綁定和單向數據綁定概念
????????雙向數據綁定就是在單向數據綁定的基礎上給可輸入元素(input、textare等)添加了change(input)事件,來動態修改model(js)和 view(視圖),在單向數據綁定中,input輸入元素中輸入的內容可以通過js操作dom動態獲取,js中改變的數據也需要再次操作dom反映到視圖中。雙向數據綁定通過watcher方法自動更新視圖中的數據,省去了煩瑣的dom操作;
二、訪問器屬性
  var obj = {}
  // 為obj對象定義一個名為hello的訪問器屬性
  // 訪問器屬性是對象中的一種特殊屬性,不能直接在對象中定義,只能通defineProperty方法定義
  // 讀取或設置訪問器屬性的值,實際上是調用其內部函數get或set方法
  Object.defineProperty(obj, "hello", {
    get: function() {},
    set: function() {}
  })
  obj.hello // 調用get方法,并返回get方法的返回值
  obj.hello = "123" // 賦值傳參,調用set方法,參數是123
  // 訪問器屬性會被優先訪問,即訪問器屬性會覆蓋同名屬性
三、雙向數據綁定的簡化版
var obj = {}
  Object.defineProperty(obj, "hello", {
    get: function() {},
    set: function(newVal) {
      document.getElementById('a').value = newVal
      document.getElementById('b').innerHTML = newVal
    }
  })
  // 模擬watcher
  document.addEventListener('keyup', function(e) {
    obj.hello = e.target.value
  })
四、將vue中的值單向綁定到dom中

1)DocumentFragment文檔片斷
????????可以看做是節點容器,它可以包含多個子節點,將其插入到dom中時,只有它的子節點會插入到目標節點;
????????使用DocumentFragment處理節點,速度和性能遠遠優于直接操作dom;
????????vue進行編譯時,就是將掛載目標的所有子節點劫持(通過append方法,dom中的所有節點會被自動刪除)到DocumentFragment中,處理后再將DocumentFragment整體返回插入掛載目標;

// html代碼
<div id="app">
    <input type="text" id="a">
    <span id="b"></span>
  </div>
// js操作
var dom = nodeToFragment(document.getElementById('app'))
  console.log(dom)
  function nodeToFragment(node) {
    var flag = document.createDocumentFragment()
    var child
    while (child = node.firstChild) {
      flag.appendChild(child) // 將子節點劫持到文檔片斷中
    }
    return flag
  }
  document.getElementById('app').appendChild(dom) // 返回到app中
屏幕快照 2018-07-23 下午4.16.01.png

2)dom編譯和數據綁定

// html代碼
  <div id="app">
    <input type="text" v-model="text">
    {{ text }}
  </div>
// js代碼
// 對dom進行編譯,將輸入框以及文本節點與data中的數據綁定
  function compile(node, vm) {
    var reg = /\{\{(.*)\}\}/
    // 節點類型為元素
    if (node.nodeType === 1) {
      var attr = node.attributes
      // 解析屬性
      for (var i = 0; i < attr.length; i++) {
        if (attr[i].nodeName == 'v-model') {
          var name = attr[i].nodeValue // 獲取v-model綁定的屬性名
          node.value = vm.data[name] // 將data的值賦給該node
          node.removeAttribute('v-model')
        }
      }
    }
    // 節點類型為text
    if (node.nodeType === 3) {
      if (reg.test(node.nodeValue)) {
        var name = RegExp.$1 // 獲取匹配到的字符串
        name = name.trim()
        node.nodeValue = vm.data[name] // 將data的值賦給該node
      }
    }
  }
// 將節點轉換為文檔片斷
  function nodeToFragment(node, vm) {
    var flag = document.createDocumentFragment()
    var child
    while (child = node.firstChild) {
      compile(child, vm)
      flag.appendChild(child) // 將子節點劫持到文檔片斷中
    }
    return flag
  }
// vue綁定的完整操作
  function Vue(options) {
    this.data = options.data
    var id = options.el
    var dom = nodeToFragment(document.getElementById(id), this)
    // 編譯完成后,將dom返回到app中
    document.getElementById(id).appendChild(dom)
  }

  var vm = new Vue({
    el: 'app',
    data: {
      text: 'hello world'
    }
  })

最終結果:


屏幕快照 2018-07-23 下午5.03.33.png
五、實現數據與dom雙向綁定

??????? 在輸入框中輸入數據的時候,首先會觸發input或者keyup事件,在相應的事件處理程序中,我們獲取輸入框的value并賦值給vm實例的text屬性,利用defineProperty將data中的text設置為vm的訪問器屬性,會觸發set方法更新屬性的值;

// html代碼
<div id="app">
    <input type="text" v-model="text">
    {{ text }}
  </div>
// js代碼
var obj = {}

  function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
      get: function() {
        return val
      },
      set: function(newVal) {
        if (newVal === val) return
        val = newVal
        console.log(val)
      }
    })
  }

  // watcher
  function observe(obj, vm) {
    Object.keys(obj).forEach(function(key) {
      defineReactive(vm, key, obj[key])
    })
  }

  function Vue(options) {
    this.data = options.data
    var data = this.data
    observe(data, this)
    var id = options.el
    var dom = nodeToFragment(document.getElementById(id), this)
    // 編譯完成后,將dom返回到app中
    document.getElementById(id).appendChild(dom)
  }

  // 對dom進行編譯,將輸入框以及文本節點與data中的數據綁定
  function compile(node, vm) {
    var reg = /\{\{(.*)\}\}/
    // 節點類型為元素
    if (node.nodeType === 1) {
      var attr = node.attributes
      // 解析屬性
      for (var i = 0; i < attr.length; i++) {
        if (attr[i].nodeName == 'v-model') {
          var name = attr[i].nodeValue // 獲取v-model綁定的屬性名
          node.addEventListener('input', function(e) {
            // 給相應的data屬性賦值,進而觸發該屬性的set方法
            vm[name] = e.target.value
          })
          node.value = vm[name] // 將data的值賦給該node
          node.removeAttribute('v-model')
        }
      }
    }
    // 節點類型為text
    if (node.nodeType === 3) {
      if (reg.test(node.nodeValue)) {
        var name = RegExp.$1 // 獲取匹配到的字符串
        name = name.trim()
        node.nodeValue = vm[name] // 將data的值賦給該node
      }
    }
  }

  function nodeToFragment(node, vm) {
    var flag = document.createDocumentFragment()
    var child
    while (child = node.firstChild) {
      compile(child, vm)
      flag.appendChild(child) // 將子節點劫持到文檔片斷中
    }
    return flag
  }

  var vm = new Vue({
    el: 'app',
    data: {
      text: 'hello world'
    }
  })

結果如下:


屏幕快照 2018-07-23 下午5.44.28.png
六、實現數據與dom雙向綁定

????????text 文本變化了,set方法觸發了,使用訂閱發布模式將綁定到text的文本節點同步變化,訂閱發布模式是一種一對多的關系,即多個觀察者同時監聽一個主題對象,這個主題對象的狀態發生變化時會通知所有觀察者對象;
????????流程:發布者發出通知=》主題對象收到通知并推送給觀察者=》訂閱者執行相應操作

 //  一個發布者publisher
  var pub = {
    publish: function() {
      dep.notify()
    }
  }
  // 三個訂閱者subscribers
  var sub1 = {
    update: function() {
      console.log(1)
    }
  }
  var sub2 = {
    update: function() {
      console.log(2)
    }
  }
  var sub3 = {
    update: function() {
      console.log(3)
    }
  }

  // 一個主題對象
  function Dep() {
    this.subs = [sub1, sub2, sub3]
  }
  Dep.prototype.notify = function() {
    this.subs.forEach(function(sub) {
      sub.update()
    })
  }
  // 發布者發布消息,主題對象執行notif方法,進而觸發訂閱者執行update方法
  var dep = new Dep()
  pub.publish() // 1,2,3
七、雙向數據綁定完整代碼

????????監聽數據的過程中,會為data中的每一個屬性生成一個主題對象dep;
????????在編譯html過程中,會為每個與數據綁定相關的節點生成一個訂閱者watcher,watcher會將自己添加到相應屬性的dep中;
????????發出通知dep.notify()=>觸發訂閱者的update方法=>更新視圖;

  function defineReactive(obj, key, val) {
    var dep = new Dep()
    Object.defineProperty(obj, key, {
      get: function() {
        // 添加訂閱者watcher到主題對象Dep
        if (Dep.target) dep.addSub(Dep.target)
        return val
      },
      set: function(newVal) {
        if (newVal === val) return
        val = newVal
        // 作為發布者發出通知
        dep.notify()
      }
    })
  }

  // watcher
  function observe(obj, vm) {
    Object.keys(obj).forEach(function(key) {
      defineReactive(vm, key, obj[key])
    })
  }

  // 一個主題對象
  function Dep() {
    this.subs = []
  }
  Dep.prototype = {
    addSub: function(sub) {
      this.subs.push(sub)
    },
    notify: function() {
      this.subs.forEach(function(sub) {
        sub.update()
      })
    }
  }

  function Vue(options) {
    this.data = options.data
    var data = this.data
    observe(data, this)
    var id = options.el
    var dom = nodeToFragment(document.getElementById(id), this)
    // 編譯完成后,將dom返回到app中
    document.getElementById(id).appendChild(dom)
  }

  // 對dom進行編譯,將輸入框以及文本節點與data中的數據綁定
  function compile(node, vm) {
    var reg = /\{\{(.*)\}\}/
    // 節點類型為元素
    if (node.nodeType === 1) {
      var attr = node.attributes
      // 解析屬性
      for (var i = 0; i < attr.length; i++) {
        if (attr[i].nodeName == 'v-model') {
          var name = attr[i].nodeValue // 獲取v-model綁定的屬性名
          node.addEventListener('input', function(e) {
            // 給相應的data屬性賦值,進而觸發該屬性的set方法
            vm[name] = e.target.value
          })
          node.value = vm[name] // 將data的值賦給該node
          node.removeAttribute('v-model')
        }
      }
    }
    // 節點類型為text
    if (node.nodeType === 3) {
      if (reg.test(node.nodeValue)) {
        var name = RegExp.$1 // 獲取匹配到的字符串
        name = name.trim()
        // node.nodeValue = vm[name] // 將data的值賦給該node
        new Watcher(vm, node, name)
      }
    }
  }

  function Watcher(vm, node, name) {
    Dep.target = this
    this.name = name
    this.node = node
    this.vm = vm
    this.update()
    Dep.target = null
  }

  Watcher.prototype = {
    update: function() {
      this.get()
      this.node.nodeValue = this.value
    },
    // 獲取data中的屬性值
    get: function() {
      this.value = this.vm[this.name] // 觸發相應屬性的get
    }
  }

  function nodeToFragment(node, vm) {
    var flag = document.createDocumentFragment()
    var child
    while (child = node.firstChild) {
      compile(child, vm)
      flag.appendChild(child) // 將子節點劫持到文檔片斷中
    }
    return flag
  }

  var vm = new Vue({
    el: 'app',
    data: {
      text: 'hello world'
    }
  })

結果如下:


屏幕快照 2018-07-23 下午6.42.45.png
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容