vue中的双向数据绑定原理与常见操作技巧详解


Posted in Javascript onMarch 16, 2020

本文实例讲述了vue中的双向数据绑定原理与常见操作技巧。分享给大家供大家参考,具体如下:

什么是双向数据绑定?

vue是一个mvvm框架,即数据双向绑定,即当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化。这也是算是vue的精髓之处了。值得注意的是,我们所说的数据双向绑定,一定是对于UI控件来说的,非UI控件不会涉及到数据双向绑定。单向数据绑定是使用状态管理工具的前提,如果我们使用vuex,那么数据流也是单向的,这时就会和双向数据绑定有冲突,我们可以这么解决。

为什么要实现数据的双向绑定?

在vue中,如果使用vuex,实际上数据还是单向的,之所以说是数据双向绑定,这是用的UI控件来说,对于我们处理表单,vue的双向数据绑定用起来就特别舒服了。即两者并不互斥,在全局性数据流使用单项,方便跟踪,局部性数据流使用双向,简单易操作。

1.访问器属性

Object.defineProperty()函数可以定义对象的属性相关描述符,其中的set和get函数对于完成数据双向绑定起到了至关重要的作用,下面,我们看看这个函数的基本使用方式。

var obj = {
   foo: 'foo'
  }

  Object.defineProperty(obj, 'foo', {
   get: function () {
    console.log('将要读取obj.foo属性');
   }, 
   set: function (newVal) {
    console.log('当前值为', newVal);
   }
  });

  obj.foo; // 将要读取obj.foo属性
  obj.foo = 'name'; // 当前值为 name

上面代码中,get即为我们访问属性时调用,set为我们设置属性值时调用。

2.简单的数据双向绑定实现方法

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>forvue</title>
</head>
<body>
 <input type="text" id="textInput">
 输入:<span id="textSpan"></span>
 <script>
  var obj = {},
    textInput = document.querySelector('#textInput'),
    textSpan = document.querySelector('#textSpan');

  Object.defineProperty(obj, 'foo', {
   set: function (newValue) {
    textInput.value = newValue;
    textSpan.innerHTML = newValue;
   }
  });

  textInput.addEventListener('keyup', function (e) {
    obj.foo = e.target.value;
  });

 </script>
</body>
</html>

可以看到,实现一个简单的数据双向绑定还是不难的,使用Object.defineProperty()来定义属性的set函数,属性被赋值的时候,修改input的value值以及span中的innerHTML,然后监听input的keyup事件,修改对象的属性值,即可以实现这样一个简单的数据双向绑定。

3. 实现任务的思路

上面我们只是实现了一个简单的数据双向绑定,而我们真正希望实现的是下面这种方式:

<div id="app">
    <input type="text" v-model="text">
    {{ text }}
  </div> 

  <script>
    var vm = new Vue({
      el: '#app', 
      data: {
        text: 'hello world'
      }
    });
  </script>

即和vue一样的方式来实现数据的双向绑定,那么我们可以把整个实现过程分为下面几步:

输入框以及文本节点与data中的数据绑定

输入框内容变化时,data中的数据同步变化。即view => model的变化。

data中的数据变化 时,文本节点的内容同步变化。即model => view的变化。

4.DocumentFragment

如果希望实现任务,我们还需要使用到DocumentFragment文档片段,可以把它看做一个容器,如下所示:

<div id="app">
    
  </div>
  <script>
    var flag = document.createDocumentFragment(),
      span = document.createElement('span'),
      textNode = document.createTextNode('hello world');
    span.appendChild(textNode);
    flag.appendChild(span);
    document.querySelector('#app').appendChild(flag)
  </script>

使用文档片段的好处在于:在文档片段上进行操作DOM,而不会影响到真实的DOM,操作完成后,我们就可以添加到真实的DOM上,这样的效率比直接在正式DOM上修改要高很多。

vue在进行编译时,就是将挂载目标的所有子节点劫持到DocumentFragment中,经过一番处理之后,再将DocumentFragment整体返回插入挂载目标。

5.初始化数据绑定

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 
    node.value = vm.data[name]
    node.removeAttribute('v-model')
   }
   
  }
 }
 
 if (node.nodeType === 3) {
  if (reg.test(node.nodeValue)) {
   var name = RegExp.$1
   name = name.trim()
   node.nodeValue = vm.data[name]
  }
 }
}

function nodeToFragment(node, vm) {
 var flag = document.createDocumentFragment()
 var child 
 while(child = node.firstChild) {
  compile(child, vm)
  flag.appendChild(child)
 }
 return flag
}

function Vue(options) {
 this.data = options.data 
 var el = options.el
 var dom = nodeToFragment(document.querySelector(el), this)
 
 document.querySelector(el).appendChild(dom)
}

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

6.响应式的数据绑定

我们再来看看任务的实现思路,当我们在输入框输入数据的时候,首先触发input事件(或者keyup,change事件),在相应的事件处理程序中,我们获取输入框的value并赋值给vm实例的text属性。我们会利用defineProperty将data中text设置为vm的访问器属性,因此给vm.text赋值,就会触发set方法。在set方法可主要做两件事,第一,更新属性的值,第二后面再说。

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>forvue</title>
</head>
<body>
  <div id="app">
    <input type="text" v-model="text">
    {{ text }}
  </div>
    
  <script>
    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;
    }

    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);
    }

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

    function defineReactive(obj, key, val) {
      // 响应式的数据绑定
      Object.defineProperty(obj, key, {
        get: function () {
          return val;
        },
        set: function (newVal) {
          if (newVal === val) {
            return; 
          } else {
            val = newVal;
            console.log(val); // 方便看效果
          }
        }
      });
    }

    function observe (obj, vm) {
      Object.keys(obj).forEach(function (key) {
        defineReactive(vm, key, obj[key]);
      });
    }
  </script>

</body>
</html>

7. 订阅/发布模式(subscribe & publish)

text属性变化了,set方法触发了,但是文本节点的内容没有变化。如何才能让同样绑定到text的文本节点也同步变化呢?这里有一个知识点:订阅发布模式,订阅发布模式又称为观察者模式,定义一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有的观察者对象。
发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应的操作

// 一个发布者 publisher,功能就是负责发布消息 - publish
    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();
      });
    }

    // 发布者发布消息, 主题对象执行notify方法,进而触发订阅者执行Update方法
    var dep = new Dep();
    pub.publish();

不难看出,这里的思路还是很简单的: 发布者负责发布消息、 订阅者负责接收接收消息,而最重要的是主题对象,他需要记录所有的订阅这特消息的人,然后负责吧发布的消息通知给哪些订阅了消息的人。

所以,当set方法触发后做的第二件事情就是作为发布者发出通知: “我是属性text,我变了”。 文本节点作为订阅者,在接收到消息之后执行相应的更新动作。

8.双向绑定的实现

回顾一下,每当new一个vue,主要做了两件事情 ,第一监听数据:observe(data),第二是编译HTML, nodeToFragment(id)
在监听数据的过程中,会为data中的每一个属性生成一个主题对象dep。
在编译HTML的过程中,会为每一个数据绑定相关的节点生成一个订阅者watcher,watcher会将自己添加到相应属性的dep中。
我们已经实现了:修改输入框内容 => 在事件回调函数中修改属性值 => 触 发属性的set方法。
接下来我们要实现的是:发出通知dep.notify() => 触发订阅者update方法 => 更新视图。
这里的关键逻辑是:如何将watch添加到关联属性的dep中。

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

function defineReactive(obj, key, val) {
 var dep = new Dep()
 Object.defineProperty(obj, key, {
  get: function() {
   if (Dep.target) {
    // 添加订阅者watcher到主题对象Dep
    dep.addsub(Dep.target)
   }
   return val
  },
  set: function(newVal) {
   if (newVal === val) {
    return
   } else {
    val = newVal
    // 作为发布者发出通知
    dep.notify()
    
   }
   
  }
 })
}

function Dep () {
 this.subs = []
}

Dep.prototype = {
 addsub: function(sub) {
  this.subs.push(sub)
 },
 notify: function() {
  this.subs.forEach(function(sub) {
   sub.update()
  })
 }
}

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
    node.addEventListener('input', function(e) {
     vm[name] = e.target.value
    })
    node.value = vm[name]
    node.removeAttribute('v-model')
   }
  }
 }
 
 if (node.nodeType === 3) {
  if (reg.test(node.nodeValue)) {
   var name = RegExp.$1
   name = name.trim()
   // node.nodeValue = vm[name]
   new Watcher(vm, node, name)
  }
 }
}

function nodeToFragment(node, vm) {
 var flag = document.createDocumentFragment()
 var child 
 while (child = node.firstChild) {
  compile(child, vm)
  flag.appendChild(child)
 }
 return flag
}

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

Watcher.prototype = {
 update: function() {
  this.get()
  this.node.nodeValue = this.value
 },
 get: function() {
  this.value = this.vm[this.name]
 }
}

function Vue(options) {
 this.data = options.data
 this.methods = options.methods
 var data = this.data 
 var el = options.el
 
 observe(data, this)
 
 var dom = nodeToFragment(document.querySelector(el), this)
 
 document.querySelector(el).appendChild(dom)
}

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

希望本文所述对大家vue.js程序设计有所帮助。

Javascript 相关文章推荐
清空上传控件input file的值
Jul 03 Javascript
解决JQeury显示内容没有边距内容紧挨着浏览器边线
Dec 20 Javascript
javascript初学者常用技巧
Sep 02 Javascript
jQuery实现美观的多级动画效果菜单代码
Sep 06 Javascript
JS动态添加元素及绑定事件造成程序重复执行解决
Dec 07 Javascript
详解为Bootstrap Modal添加拖拽的方法
Jan 05 Javascript
jQuery操作cookie的示例代码
Jun 05 jQuery
js实现多张图片每隔一秒切换一张图片
Jul 29 Javascript
js实现图片跟随鼠标移动效果
Oct 16 Javascript
node.js使用stream模块实现自定义流示例
Feb 13 Javascript
在vue中嵌入外部网站的实现
Nov 13 Javascript
详解Vue的mixin策略
Nov 19 Vue.js
js+canvas实现纸牌游戏
Mar 16 #Javascript
微信小程序利用button控制条件标签的变量问题
Mar 15 #Javascript
JS apply用法总结和使用场景实例分析
Mar 14 #Javascript
javascript事件循环event loop的简单模型解释与应用分析
Mar 14 #Javascript
原生js实现ajax请求和JSONP跨域请求操作示例
Mar 14 #Javascript
js实现的订阅发布者模式简单示例
Mar 14 #Javascript
vue插槽slot的简单理解与用法实例分析
Mar 14 #Javascript
You might like
php 8小时时间差的解决方法小结
2009/12/22 PHP
测试PHP连接MYSQL成功与否的代码
2013/08/16 PHP
ThinkPHP3.1新特性之动态设置自动完成和自动验证示例
2014/06/19 PHP
php检查页面是否被百度收录
2015/10/28 PHP
PHP随手笔记整理之PHP脚本和JAVA连接mysql数据库
2015/11/25 PHP
PHP使用PhpSpreadsheet操作Excel实例详解
2020/03/26 PHP
eclipse如何忽略js文件报错(附图)
2013/10/30 Javascript
防止jQuery ajax Load使用缓存的方法小结
2014/02/22 Javascript
javascript处理表单示例(javascript提交表单)
2014/04/28 Javascript
可编辑下拉框的2种实现方式
2014/06/13 Javascript
JS实现文字向下滚动完整实例
2015/02/06 Javascript
jquery实现鼠标滑过后动态图片提示效果实例
2015/08/10 Javascript
利用JavaScript判断浏览器类型及版本
2016/08/23 Javascript
div实现自适应高度的textarea实现angular双向绑定
2017/01/08 Javascript
微信小程序 Animation实现图片旋转动画示例
2018/08/22 Javascript
vue中使用gojs/jointjs的示例代码
2018/08/24 Javascript
redux.js详解及基本使用
2019/05/24 Javascript
JS中的一些常用的函数式编程术语
2019/06/15 Javascript
js实现简单分页导航栏效果
2019/06/28 Javascript
[05:46]DOTA2英雄梦之声_第18期_陈
2014/06/20 DOTA
一些Python中的二维数组的操作方法
2015/05/02 Python
Python3实现购物车功能
2018/04/18 Python
python3实现zabbix告警推送钉钉的示例
2019/02/20 Python
500行Python代码打造刷脸考勤系统
2019/06/03 Python
python dumps和loads区别详解
2020/02/04 Python
Python 私有属性和私有方法应用场景分析
2020/06/19 Python
通过实例解析python and和or使用方法
2020/11/14 Python
StubHub新西兰:购买和出售你的门票
2019/04/22 全球购物
俄罗斯花园种植材料批发和零售网上商店:Беккер
2019/07/22 全球购物
银行实习的自我鉴定
2013/12/10 职场文书
《小蝌蚪找妈妈》教学反思
2014/02/21 职场文书
怀念母亲教学反思
2014/04/28 职场文书
3的组成教学反思
2014/04/30 职场文书
人间正道是沧桑观后感
2015/06/15 职场文书
MySQL一些常用高级SQL语句
2021/07/03 MySQL
Java 多态分析
2022/04/26 Java/Android