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 相关文章推荐
js限制textarea每行输入字符串长度的代码
Oct 31 Javascript
使用javascript创建快捷方式的简单实例
Aug 09 Javascript
浅谈JavaScript Math和Number对象
Jan 26 Javascript
在jQuery中处理XML数据的大致方法
Aug 14 Javascript
学习JavaScript设计模式之装饰者模式
Jan 19 Javascript
基于chosen插件实现人员选择树搜索自动筛选功能
Sep 24 Javascript
Jquery针对tr td的一些实用操作方法(必看篇)
Oct 05 Javascript
Javascript 实现微信分享(QQ、朋友圈、分享给朋友)
Oct 21 Javascript
JS仿京东移动端手指拨动切换轮播图效果
Apr 10 Javascript
canvas轨迹回放功能实现
Dec 20 Javascript
Openlayers+EasyUI Tree动态实现图层控制
Sep 28 Javascript
react合成事件与原生事件的相关理解
May 13 Javascript
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将数据导入到Foxmail
2006/10/09 PHP
PHPMailer使用教程(PHPMailer发送邮件实例分析)
2012/12/06 PHP
PHP中的一些常用函数收集
2015/05/26 PHP
各种快递查询--Api接口
2016/04/26 PHP
PHP 将dataurl转成图片image方法总结
2016/10/14 PHP
用window.location.href实现刷新另个框架页面
2007/03/07 Javascript
jQuery学习笔记 操作jQuery对象 文档处理
2012/09/19 Javascript
使用JavaScript 编写简单计算器
2014/11/24 Javascript
JS实现进入页面时渐变背景色的方法
2015/02/25 Javascript
深入学习jQuery Validate表单验证
2016/01/18 Javascript
Angularjs+bootstrap+table多选(全选)支持单击行选中实现编辑、删除功能
2017/03/27 Javascript
vue-cli3跨域配置的简单方法
2019/09/06 Javascript
微信小程序转化为uni-app项目的方法示例
2020/05/22 Javascript
基于javascript实现放大镜特效
2020/12/03 Javascript
[08:47]2018国际邀请赛 OG战队举杯时刻
2018/08/29 DOTA
Python的ORM框架SQLObject入门实例
2014/04/28 Python
Python实现SMTP发送邮件详细教程
2021/03/02 Python
python 垃圾收集机制的实例详解
2017/08/20 Python
python自动查询12306余票并发送邮箱提醒脚本
2018/05/21 Python
对python中array.sum(axis=?)的用法介绍
2018/06/28 Python
python读取xlsx的方法
2018/12/25 Python
Python面向对象总结及类与正则表达式详解
2019/04/18 Python
python加密解密库cryptography使用openSSL生成的密匙加密解密
2020/02/11 Python
Python figure参数及subplot子图绘制代码
2020/04/18 Python
Python爬虫自动化爬取b站实时弹幕实例方法
2021/01/26 Python
css3实现画半圆弧线的示例代码
2017/11/06 HTML / CSS
HTML5新特性之语义化标签
2017/10/31 HTML / CSS
中学生学习生活的自我评价
2013/10/26 职场文书
男方父母婚礼答谢词
2014/01/25 职场文书
项目采购员岗位职责
2014/04/15 职场文书
学生党员批评与自我批评
2014/10/15 职场文书
新郎结婚保证书
2015/02/26 职场文书
城南旧事观后感
2015/06/11 职场文书
广告文案的撰写技巧(实用干货)
2019/08/23 职场文书
导游词之日月潭
2019/11/05 职场文书
python用tkinter开发的扫雷游戏
2021/06/01 Python