zoukankan      html  css  js  c++  java
  • Vue双向数据绑定简易实现

    一、vue中的双向数据绑定主要使用到了Object.defineProperty(新版的使用Proxy实现的)对Model层的数据进行getter和setter进行劫持,修改Model层数据的时候,在setter中可以知道对那个属性进行修改了,然后修改View的数据。

    二、简易版双向数据绑定

    <!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层的数据进行关联。

    三、实现View数据变化映射到Model数据上,初始化的Model数据映射到View上

    <!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>
    

    四、在案例三的基础上,使用订阅发布模式实现{{xxx}}

    <!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>
    

     五、参考的文章:https://blog.csdn.net/tangxiujiang/article/details/79594860

     

  • 相关阅读:
    模块添加页代码
    模块登录页代码
    列表登录页代码
    不在让你为你写代码头疼的链接页代码
    最新最全产品删除页代码
    网站的产品页后台代码
    Windows修改账户名称和任务管理器中服务对应的用户名称
    nginx处理HTTP header问题
    Maven报错:Missing artifact jdk.tools:jdk.tools:jar:1.6
    linux上传、下载文件rz、sz命令
  • 原文地址:https://www.cnblogs.com/llcdxh/p/10412880.html
Copyright © 2011-2022 走看看