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 字符串操作函数
Jul 25 Javascript
半角全角相互转换的js函数
Oct 16 Javascript
让firefox支持IE的一些方法的javascript扩展函数代码
Jan 02 Javascript
解决Jquery向页面append新元素之后事件的绑定问题
Mar 16 Javascript
基于BootStrap的图片轮播效果展示实例代码
May 23 Javascript
使用grunt合并压缩js和css文件的方法
Mar 02 Javascript
微信小程序 sha1 实现密码加密实例详解
Jul 06 Javascript
详解从新建vue项目到引入组件Element的方法
Aug 29 Javascript
基于Vue的SPA动态修改页面title的方法(推荐)
Jan 02 Javascript
关于ligerui子页面关闭后,父页面刷新,重新加载的方法
Sep 27 Javascript
keep-alive不能缓存多层级路由菜单问题解决
Mar 10 Javascript
Node.js文本文件BOM头的去除方法
Nov 22 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缓存技术介绍
2006/11/25 PHP
Zend Framework动作助手Json用法实例分析
2016/03/05 PHP
js 判断checkbox是否选中的实现代码
2010/11/23 Javascript
JS中把字符转成ASCII值的函数示例代码
2013/11/21 Javascript
基于jquery实现省市联动效果
2015/11/23 Javascript
基于javascript实现图片左右切换效果
2016/01/25 Javascript
React Native预设占位placeholder的使用
2017/09/28 Javascript
vue2里面ref的具体使用方法
2017/10/27 Javascript
微信小程序实现打卡日历功能
2020/09/21 Javascript
详解基于Vue,Nginx的前后端不分离部署教程
2018/12/04 Javascript
JavaScript函数式编程(Functional Programming)高阶函数(Higher order functions)用法分析
2019/05/22 Javascript
vue-router 按需加载 component: () =&gt; import() 报错的解决
2020/09/22 Javascript
Python装饰器的函数式编程详解
2015/02/27 Python
Python类的定义、继承及类对象使用方法简明教程
2015/05/08 Python
matplotlib subplots 设置总图的标题方法
2018/05/25 Python
python写入并获取剪切板内容的实例
2018/05/31 Python
使用Python处理BAM的方法
2018/09/28 Python
Python 迭代,for...in遍历,迭代原理与应用示例
2019/10/12 Python
通过字符串导入 Python 模块的方法详解
2019/10/27 Python
Pytorch 实现冻结指定卷积层的参数
2020/01/06 Python
django admin 根据choice字段选择的不同来显示不同的页面方式
2020/05/13 Python
查看keras的默认backend实现方式
2020/06/19 Python
Python中用xlwt制作表格实例讲解
2020/11/05 Python
pytorch加载语音类自定义数据集的方法教程
2020/11/10 Python
预订全球最佳旅行体验:Viator
2018/03/30 全球购物
澳洲国民品牌乡村路折扣店:Country Road & Trenery Outlet
2018/04/19 全球购物
Fox Racing官方网站:越野摩托车和山地自行车装备和服装
2019/12/23 全球购物
运动会闭幕式解说词
2014/02/21 职场文书
《梅兰芳学艺》教学反思
2014/02/24 职场文书
幼儿教师暑期培训方案
2014/08/27 职场文书
出差报告范文
2014/11/06 职场文书
2015年试用期工作总结
2014/12/12 职场文书
2015年检验员工作总结范文
2015/04/30 职场文书
导游词之山西祁县乔家大院
2019/10/14 职场文书
Nginx使用X-Accel-Redirect实现静态文件下载的统计、鉴权、防盗链、限速等
2021/04/04 Servers
Python中的 enumerate和zip详情
2022/05/30 Python