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

 

原文地址:https://www.cnblogs.com/llcdxh/p/10412880.html

时间: 2024-10-16 02:15:22

Vue双向数据绑定简易实现的相关文章

vue双向数据绑定简易demo

1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <meta http-equiv="X-UA-Comp

angular和vue双向数据绑定

angular和vue双向数据绑定的原理(重点是vue的双向绑定) 我在整理javascript高级程序设计的笔记的时候看到面向对象设计那章,讲到对象属性分为数据属性和访问器属性,我们平时用的js对象90%以上都只是用到数据属性;我们向来讲解下数据属性和访问器属性到底是什么? 数据属性:数据属性包含一个数据值的位置,在这个位置可以读取和写入值. 访问器属性:访问器属性不包含数据值;他们包含一对getter和setter函数在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值,在写

vue 双向数据绑定的实现学习(二)- 监听器的实现

废话:上一篇https://www.cnblogs.com/adouwt/p/9928278.html 提到了vue实现的基本实现原理:Object.defineProperty() -数据劫持 和 发布订阅者模式(观察者),下面讲的就是数据劫持在代码中的具体实现. 1.先看如何调用 new一个对象,传入我们的参数,这个Myvue ,做了啥? 上面看到了在实例化一个Myvue 对象的时候,会执行init方法, init 方法做了两个事,调用了observer 方法,和 实例化调用了 compil

React 事件对象、键盘事件、表单事件、ref获取dom节点、react实现类似Vue双向数据绑定

1.案例实现代码 import React, { Component } from 'react'; /** * 事件对象.键盘事件.表单事件.ref获取dom节点.react实现类似Vue双向数据绑定 * 事件对象: 在触发DOM上的某个事件时,会产生一个事件对象event,这个对象包含着所有与事件有关的信息 * 表单事件: 获取表单的值 * 1.监听表单的改变事件 ---onChange * 2.在改变的事件里面获取表单输入的值 ---event * 3.把表单输入的值赋值给username

vue双向数据绑定原理探究(附demo)

昨天被导师叫去研究了一下vue的双向数据绑定原理...本来以为原理的东西都非常高深,没想到vue的双向绑定真的很好理解啊...自己动手写了一个. 传送门 双向绑定的思想 双向数据绑定的思想就是数据层与UI层的同步,数据再两者之间的任一者发生变化时都会同步更新到另一者. 双向绑定的一些方法 目前,前端实现数据双向数据绑定的方法大致有以下三种: 1.发布者-订阅者模式(backbone.js) 思路:使用自定义的data属性在HTML代码中指明绑定.所有绑定起来的JavaScript对象以及DOM元

Vue双向数据绑定的手动实现

class Asarua { // 传入一个对象,也就是实例化的时候的参数 constructor(options) { this.$data = options.data; // 获取挂载的dom this.$el = document.querySelector(options.el); this.$methods = options.methods; this.$options = options; // 定义一个容器,来存放所有的订阅者 this.observable = {}; thi

vue双向数据绑定

父组件引用子组件时,可以使用 v-model="params"  绑定一个变量, 在子组件中,需要使用props去接收一个value变量,即父组件绑定到的params的值 在子组件中,使用this.$emit("input" , someData); 可以触发父组件的input事件,someData是参数,它的值会赋给父组件的params的变量. 这种写法是一种简化的写法,是vue的语法糖.他也可以写成 <v-children v-bind:value=&qu

vue 双向数据绑定原理

采用defineProperty的两个方法get.set 示例 1 <!-- 表单 --> 2 <input type="text" id="input"> 3 <!-- 展示 --> 4 <p id="desc"></p> 1 let obj = {}; 2 let temp = {};//采用临时变量代理obj 3 Object.defineProperty(obj,'name',{

vue双向数据绑定原理

1.实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值通知订阅者2.实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,以及绑定相应的更新函数3.实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图4.mvvm入口,整合以上三者 原文地址:https://blog.51cto.com/13550695/2467904