Vue雙向數據綁定簡易實現

1、vue中的雙向數據綁定主要使用到了Object.defineProperty(新版的使用Proxy實現的)對Model層的數據進行getter和setter進行劫持,修改Model層數據的時候,在setter中能夠知道對那個屬性進行修改了,而後修改View的數據。javascript

2、簡易版雙向數據綁定html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Proxy雙向數據綁定大概原理</title>
</head>
<body>
  <div id="app">
    <input type="text" id="inpt"/>
    <span id="txt"></span>
  </div>
  <script>
    var inputDom = document.getElementById("inpt"),
        spanDom = document.getElementById("txt"),
        data = {}
    
    // 更新DOM
    function notifyToUpdateDOM (newVal) {
      inputDom.value = newVal
      spanDom.innerHTML = newVal
    }

    var proxyHandler = {
      get: function(target, property){
        return target[property]
      },
      set: function(target, property, value){
        target[property] = value
        notifyToUpdateDOM(value)
      }
    }
    
    // 建立代理
    var dataProxy = new Proxy(data, proxyHandler)

    // 監聽input的input事件
    inputDom.addEventListener("input", function(e){
      // 設置data中的inputModel屬性,會觸發set方法的調用
      dataProxy.inputModel = e.target.value
    })
  </script>
</body>
</html>

以上簡易代碼比較適合Model層沒有默認數據的時候,若是Model層的inputModel默認有值爲:「雙向數綁定」;那麼如何在頁面初始化完成的時候就把Model層的數據顯示到View上呢?所以在進行數據綁定以前,須要把View模板進行編譯,和Model層的數據進行關聯。vue

3、實現View數據變化映射到Model數據上,初始化的Model數據映射到View上java

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Proxy雙向數據綁定大概原理</title>
</head>
<body>
  <div id="app">
    <input type="text" v-model="text"/>{{text}}
  </div>

  <!-- 這次完成了UI到Model的數據綁定,尚未實現Model到UI的綁定({{xxx}}處理沒有實現) -->
  <script>

    // 參數el:最外層的dom元素,此處案例是app
    // 參數vm:表示Vue的實例
    // 主要處理v-model指令和{{xxx}},把裏面的變量和Vue中的進行映射
    function CompileTemplate(el, vm){
      this.$ele = el
      this.$vm = vm
      this.$fragment = null // 保存從新編譯以後的dom結構
    }

    CompileTemplate.prototype = {
      // 修正構造函數的指向
      constructor: CompileTemplate,
      // 返回fragment
      getDocumentFragment: function(){
        if (this.$fragment) {
          return this.$fragment
        } else {
          this.$fragment = this.nodeToFragment()
          return this.$fragment
        }
      },
      nodeToFragment: function(){
        var node = document.getElementById(this.$ele),
            fragment = document.createDocumentFragment(),
            child = null
        
        while(child = node.firstChild){
          this.compileElement(child)
          /*若是被插入的節點已經存在於當前文檔的文檔樹中,則那個節點會首先從原先的位置移除,
          而後再插入到新的位置;若是你須要保留這個子節點在原先位置的顯示,則你須要先用Node.cloneNode
          方法複製出一個節點的副本,而後在插入到新位置.
          */
          fragment.appendChild(child)
        }
        return fragment
      },
      // 處理節點信息以及綁定事件
      compileElement: function(node){
        // 匹配{{}}
        var reg = /\{\{(.*)\}\}/g,
            _this = this

        // 元素
        if (node.nodeType === 1) {
          var attributes = node.attributes
          for (var len = attributes.length - 1; len > 0; len--) {
            // 獲取v-model綁定的變量
            if (attributes[len].nodeName === 'v-model') {
              // 獲取v-model="txt"中的txt
              var name = attributes[len].nodeValue
              // 爲input元素綁定input事件,當事件發生時,設置Vue對象中的$data的值
              node.addEventListener("input", function(e){
                // 設置vue對象中data的text中值
                _this.$vm.$data[name] = e.target.value
              })
              // 初始化的時候,須要把Vue對象中的數據賦值給input元素的value
              node.value = _this.$vm.$data[name];
              node.removeAttribute('v-model')
            }
          }
        }
        
        // text
        if (node.nodeType === 3) {
          if(reg.test(node.nodeValue)) {// 獲取v-model綁定的屬性名 {{text}}
            var name = RegExp.$1.trim(); // 獲取匹配到的字符串
            // 初始化的時候,把vue對象data的數據賦值給{{text}}
            node.nodeValue = _this.$vm.$data[name];
          }
        }
      }
    }

    function Vue(options){
      this.$el = options.el
      this.$data = options.data
      // 解析DOM模板,如v-model指令改成input事件,{{xxx}}改成對象中的數據
      var fragmentDOM = new CompileTemplate(this.$el, this).getDocumentFragment()
      // 更新DOM
      document.getElementById(this.$el).appendChild(fragmentDOM)
    }

    var vm = new Vue({
      el: "app",
      data: {
        text: '雙向數據綁定'
      }
    })
  </script>

</body>
</html>

4、在案例三的基礎上,使用訂閱發佈模式實現{{xxx}}node

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Proxy雙向數據綁定大概原理(最終版)</title>
</head>
<body>
  <div id="app">
    <input type="text" v-model="text"/>{{text}}
  </div>

  <!-- 這次實現Model到UI的綁定 -->
  <script>

    // 參數el:最外層的dom元素,此處案例是app
    // 參數vm:表示Vue的實例
    // 主要處理v-model指令和{{xxx}},把裏面的變量和Vue中的進行映射
    function CompileTemplate(el, vm){
      this.$ele = el
      this.$vm = vm
      this.$fragment = null // 保存從新編譯以後的dom結構
    }

    CompileTemplate.prototype = {
      // 修正構造函數的指向
      constructor: CompileTemplate,
      // 返回fragment
      getDocumentFragment: function(){
        if (this.$fragment) {
          return this.$fragment
        } else {
          this.$fragment = this.nodeToFragment()
          return this.$fragment
        }
      },
      nodeToFragment: function(){
        var node = document.getElementById(this.$ele),
            fragment = document.createDocumentFragment(),
            child = null
        
        while(child = node.firstChild){
          this.compileElement(child)
          /*若是被插入的節點已經存在於當前文檔的文檔樹中,則那個節點會首先從原先的位置移除,
          而後再插入到新的位置;若是你須要保留這個子節點在原先位置的顯示,則你須要先用Node.cloneNode
          方法複製出一個節點的副本,而後在插入到新位置.
          */
          fragment.appendChild(child)
        }
        return fragment
      },
      // 處理節點信息以及綁定事件
      compileElement: function(node){
        // 匹配{{}}
        var reg = /\{\{(.*)\}\}/g,
            _this = this

        // 元素
        if (node.nodeType === 1) {
          var attributes = node.attributes
          for (var len = attributes.length - 1; len > 0; len--) {
            // 獲取v-model綁定的變量
            if (attributes[len].nodeName === 'v-model') {
              // 獲取v-model="txt"中的txt
              var name = attributes[len].nodeValue
              // 爲input元素綁定input事件,當事件發生時,設置Vue對象中的$data的值
              node.addEventListener("input", function(e){
                // 設置vue對象中data的text中值
                _this.$vm.$data[name] = e.target.value
              })
              // 初始化的時候,須要把Vue對象中的數據賦值給input元素的value
              // node.value = _this.$vm.$data[name];
              new Watcher(_this.$vm, node, name, "value")
              node.removeAttribute('v-model')
            }
          }
        }
        
        // text
        if (node.nodeType === 3) {
          if(reg.test(node.nodeValue)) {// 獲取v-model綁定的屬性名 {{text}}
            var name = RegExp.$1.trim(); // 獲取匹配到的字符串
            // 初始化的時候,把vue對象data的數據賦值給{{text}}
            // node.nodeValue = _this.$vm.$data[name];
            new Watcher(_this.$vm, node, name, "nodeValue")
          }
        }
      }
    }

    /* 對model層的數據進行劫持,model改變時,須要通知修改UI的數據,此處使用Proxy處理(兼容性很差),
    也可使用Object.defineProperty處理;Proxy雖然能夠動態對被代理的對象進行屬性劫持,可是對Model到
    UI這條路徑仍是沒法進行雙向數據綁定,由於模板先編譯了;因此在真正的Vue.js中須要經過this.$set()設置
    動態屬性,這樣才能作到響應式
    */
    function observe (obj) {
      var publish = new Publish()
      var dataProxy = new Proxy(obj, {
        // 在首次編譯模板的時候,建立觀察者時,觸發vm.$data中屬性的get方法
        get: function(target, property){
          // 把觀察者放入發佈者中,Publish類的屬性
          if (Publish.target) {
            publish.addSubscribe(Publish.target)
          }
          return target[property]
        },
        set: function(target, property, value){
          if (target[property] === value) {
            return
          }
          target[property] = value
          // 通知更新UI
          publish.notify()
        }
      })
      return dataProxy;
    }
    
    // 發佈者
    function Publish () {
      // 保存觀察者
      this.subscribes = []
    }
    Publish.prototype = {
      constructor: Publish,
      // 保存觀察者
      addSubscribe: function (sub){
        // 不存在
        if (this.subscribes.indexOf(sub) === -1) {
          this.subscribes.push(sub)
        }
      },
      // Model改變了須要更新UI
      notify: function () {
        this.subscribes.forEach(function(sub) {
          sub.update()
        })
      }
    }
    
    // 觀察者:綁定Model的數據到UI中對應的DOM節點屬性中
    // 參數vm:Vue對象的實例
    // 參數node:須要進行數據綁定的DOM對象
    // 參數name:Vue對象$data的屬性名稱
    // 參數type:DOM元素須要設置數據的屬性。如:value(input元素),nodeValue(text元素的內容)
    function Watcher (vm, node, name, type) {
      // 在發佈者身上綁定一個當前惟一的觀察者對象(相似class中的static,屬於類屬性)
      Publish.target = this
      this.vm = vm
      this.node = node
      this.name = name
      this.type = type
      this.update() // 關鍵點
      Publish.target = null
    }

    Watcher.prototype = {
      constructor:  Watcher,
      // 把Model中的數據綁定到UI中
      update: function(){
        // 此處會觸發vm對象中的get方法
        this.node[this.type] = this.vm.$data[this.name]
      }
    }

    function Vue(options){
      this.$el = options.el
      this.$data = observe(options.data)
      // 解析DOM模板,如v-model指令改成input事件,{{xxx}}改成對象中的數據
      var fragmentDOM = new CompileTemplate(this.$el, this).getDocumentFragment()
      // 更新DOM
      document.getElementById(this.$el).appendChild(fragmentDOM)
    }

    var vm = new Vue({
      el: "app",
      data: {
        text: '雙向數據綁定'
      }
    })
  </script>

</body>
</html>

 5、參考的文章:https://blog.csdn.net/tangxiujiang/article/details/79594860app

相關文章
相關標籤/搜索