用vue的双向绑定简单实现一个todo-list的示例代码


Posted in Javascript onAugust 03, 2017

前言

最近在学习vue框架的基本原理,看了一些技术博客以及一些对vue源码的简单实现,对数据代理、数据劫持、模板解析、变异数组方法、双向绑定有了更深的理解。于是乎,尝试着去实践自己学到的知识,用vue的一些基本原理实现一个简单的todo-list,完成对深度复杂对象的双向绑定以及对数组的监听,加深了对vue基本原理的印象。

github地址:todo-list

学习链接

前排感谢以下文章,对我理解vue的基本原理有很大的帮助!

剖析vue实现原理,自己动手实现mvvm by DMQ 

对vue早期源码的理解 by 梁少峰

实现效果

用vue的双向绑定简单实现一个todo-list的示例代码

数据代理

1.简单介绍数据代理

正常情况下,我们都会把数据写在data里面,如下面所示

var vm = new Vue({
  el: '#app',
  data: {
    title: 'hello world'
  }
  methods: {
    changeTitle: function () {
      this.title = 'hello vue'
    }
  }
})
console.log(vm.title) // 'hello world' or 'hello vue'

如果没有数据代理,而我们又要修改data里面的title的话,methods里面的changeTitle只能这样修改成this.data.title = 'hello vue', 下面的console也只能改成console.log(vm.data.title),数据代理就是这样的功能。

2. 实现原理

通过遍历data里面的属性,将每个属性通过object.defineProperty()设置getter和setter,将data里面的每个属性都复制到与data同级的对象里。

(对应上面的示例代码)

 用vue的双向绑定简单实现一个todo-list的示例代码

触发这里的getter将会触发data里面对应属性的getter,触发这里的setter将会触发data里面对应属性的setter,从而实现代理。实现代码如下:

var self = this;  // this为vue实例, 即vm
Object.keys(this.data).forEach(function(key) {
  Object.defineProperty(this, key, {  // this.title, 即vm.title
    enumerable: false,
    configurable: true,
    get: function getter () {
      return self.data[key];  //触发对应data[key]的getter
    },
    set: function setter (newVal) {
      self.data[key] = newVal; //触发对应data[key]的setter
    }
  });
}

对object.defineProperty不熟悉的小伙伴可以在MDN的文档(链接)学习一下

双向绑定

  1. 数据变动 ---> 视图更新
  2. 视图更新(input、textarea) --> 数据变动

视图更新 --> 数据变动这个方向的绑定比较简单,主要通过事件监听来改变数据,比如input可以监听input事件,一旦触发input事件就改变data。下面主要来理解一下数据变动--->视图更新这个方向的绑定。

1. 数据劫持

不妨让我们自己思考一下,如何实现数据变动,对应绑定数据的视图就更新呢?

答案还是object.defineProperty,通过object.defineProperty遍历设置this.data里面所有属性,在每个属性的setter里面去通知对应的回调函数,这里的回调函数包括dom视图重新渲染的函数、使用$watch添加的回调函数等,这样我们就通过object.defineProperty劫持了数据,当我们对数据重新赋值时,如this.title = 'hello vue',就会触发setter函数,从而触发dom视图重新渲染的函数,实现数据变动,对应视图更新。

2. 发布-订阅模式

那么问题来了,我们如何在setter里面触发所有绑定该数据的回调函数呢?

既然绑定该数据的回调函数不止一个,我们就把所有的回调函数放在一个数组里面,一旦触发该数据的setter,就遍历数组触发里面所有的回调函数,我们把这些回调函数称为订阅者。数组最好就定义在setter函数的最近的上级作用域中,如下面实例代码所示。

Object.keys(this.data).forEach(function(key) {
  var subs = []; // 在这里放置添加所有订阅者的数组
  Object.defineProperty(this.data, key, {  // this.data.title
    enumerable: false,
    configurable: true,
    get: function getter () {
      console.log('访问数据啦啦啦')
      return this.data[key];  //返回对应数据的值
    },
    set: function setter (newVal) {
      if (newVal === this.data[key]) {  
        return;  // 如果数据没有变动,函数结束,不执行下面的代码
      }
      this.data[key] = newVal; //数据重新赋值
      
      subs.forEach(function () {
        // 通知subs里面的所有的订阅者
      })
    }
  });
}

那么问题又来了,怎么把绑定数据的所有回调函数放到一个数组里面呢?

我们可以在getter里面做做手脚,我们知道只要访问数据就会触发对应数据的getter,那我们可以先设置一个全局变量target,如果我们要在data里面title属性添加一个订阅者(changeTitle函数),我们可以先设置target = changeTitle,把changeTitle函数缓存在target中,然后访问this.title去触发title的getter,在getter里面把target这个全局变量的值添加到subs数组里面,添加完成后再把全局变量target设置为null,以便添加其他订阅者。实例代码如下:

Object.keys(this.data).forEach(function(key) {
  var subs = []; // 在这里放置添加所有订阅者的数组
  Object.defineProperty(this.data, key, {  // this.data.title
    enumerable: false,
    configurable: true,
    get: function getter () {
      console.log('访问数据啦啦啦')
      if (target) {
        subs.push(target);        
      }
      return this.data[key];  //返回对应数据的值
    },
    set: function setter (newVal) {
      if (newVal === this.data[key]) {  
        return;  // 如果数据没有变动,函数结束,不执行下面的代码
      }
      this.data[key] = newVal; //数据重新赋值
      
      subs.forEach(function () {
        // 通知subs里面的所有的订阅者
      })
    }
  });
}

上面的代码为了方便理解都是通过简化的,实际上我们把订阅者写成一个构造函数watcher,在实例化订阅者的时候去访问对应的数据,触发相应的getter,详细的代码可以阅读DMQ的自己动手实现MVVM

3. 模板解析

通过上面的两个步骤我们已经实现一旦数据变动,就会通知对应绑定数据的订阅者,接下来我们来简单介绍一个特殊的订阅者,也就是视图更新函数,几乎每个数据都会添加对应的视图更新函数,所以我们就来简单了解一下视图更新函数。

假如说有下面这一段代码,我们怎么把它解析成对应的html呢?

<input v-model="title">
<h1>{{title}}</h1>
<button v-on:click="changeTitle">change title<button>

先简单介绍视图更新函数的用途,

比如解析指令v-model="title",v-on:click="changeTitle",还有把{{title}}替换为对应的数据等。

回到上面那个问题,如何解析模板?我们只要去遍历所有dom节点包括其子节点,

  • 如果节点属性含有v-model,视图更新函数就为把input的value设置为title的值
  • 如果节点为文本节点,视图更新函数就为先用正则表达式取出大括号里面的值'title',再设置文本节点的值为data['title']
  • 如果节点属性含有v-on:xxxx,视图更新函数就为先用正则获取事件类型为click,然后获取该属性的值为changeTitle,则事件的回调函数为this.methods['changeTitle'],接着用addEventListener监听节点click事件。

我们要知道视图更新函数也是data对应属性的订阅者,如果不知道如何触发视图更新函数,可以把上面的发布-订阅模式再看一遍。

可能有的小伙伴可能还有个疑问,如何实现input节点的值变化后,下面的h1节点的title值也发生变化?在遍历所有节点后,如果节点含有属性v-model,就用addEventListener监听input事件,一旦触发input事件,改变data['title']的值,就会触发title的setter,从而通知所有的订阅者。

监听数组变化

无法监控每个数组元素

如果让我们自己实现监听数组的变化,我们可能会想到用object.defineProperty去遍历数组每个元素并设置setter,但是vue源码里面却不是这样写的,因为对每一个数组元素defineProperty带来代码本身的复杂度增加和代码执行效率的降低。

变异数组方法

既然无法通过defineProperty监控数组的每个元素,我们可以重写数组的方法(push, pop, shift, unshift, splice, sort, reverse)来改变数组。

vue文档中是这样写的:

Vue 包含一组观察数组的变异方法,所以它们也将会触发视图更新。这些方法如下:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

下面是vue早期源码学习系列之二:如何监听一个数组的变化 中的实例代码

const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = [];

aryMethods.forEach((method)=> {

  // 这里是原生Array的原型方法
  let original = Array.prototype[method];

  // 将push, pop等封装好的方法定义在对象arrayAugmentations的属性上
  // 注意:是属性而非原型属性
  arrayAugmentations[method] = function () {
    console.log('我被改变啦!');

    // 调用对应的原生方法并返回结果
    return original.apply(this, arguments);
  };

});

let list = ['a', 'b', 'c'];
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 别忘了这个空数组的属性上定义了我们封装好的push等方法
list.__proto__ = arrayAugmentations;
list.push('d'); // 我被改变啦! 4

// 这里的list2没有被重新定义原型指针,所以就正常输出
let list2 = ['a', 'b', 'c'];
list2.push('d'); // 4

变异数组方法的缺陷

vue文档中变异数组方法的缺陷

由于 JavaScript 的限制, Vue 不能检测以下变动的数组:

  1. 当你利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如: vm.items.length = newLength

同时文档中也介绍了如何解决上面这两个问题。

最后

以上是自己对vue一些基本原理的理解,当然还有很多不足的地方,欢迎指正。本来自己也是为了应付面试才去学习vue框架的基本原理,但是简单学习了这些vue基本的原理后,让我明白通过深入学习框架原理,可以有效避开一些自己以后会遇到的坑,所以,有时间的话自己以后还是会去看看框架的基本原理。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
对YUI扩展的Gird组件 Part-1
Mar 10 Javascript
JQuery的html(data)方法与&amp;lt;script&amp;gt;脚本块的解决方法
Mar 09 Javascript
javascript中的一些注意事项 更新中
Dec 06 Javascript
Extjs grid添加一个图片状态或者按钮的方法
Apr 03 Javascript
Jquery实现图片预加载与延时加载的方法
Dec 22 Javascript
Javascript中innerHTML用法实例分析
Jan 12 Javascript
JQuery中DOM事件冒泡实例分析
Jun 13 Javascript
.NET微信公众号开发之创建自定义菜单
Jul 16 Javascript
JS的框架Polymer中的dom-if和is属性使用说明
Jul 29 Javascript
jQuery实现table中的tr上下移动并保持序号不变的实例代码
Jul 11 Javascript
JS中使用DOM来控制HTML元素
Jul 31 Javascript
jq给页面添加覆盖层遮罩的实例
Feb 16 Javascript
JavaScript中正则表达式判断匹配规则及常用方法
Aug 03 #Javascript
vue 2.0封装model组件的方法
Aug 03 #Javascript
jQuery实现上传图片前预览效果功能
Aug 03 #jQuery
详解基于vue的移动web app页面缓存解决方案
Aug 03 #Javascript
Bootstrap与Angularjs的模态框实例代码
Aug 03 #Javascript
基于 Bootstrap Datetimepicker 联动
Aug 03 #Javascript
详解react-webpack2-热模块替换[HMR]
Aug 03 #Javascript
You might like
PHP的SQL注入过程分析
2012/01/06 PHP
解决微信授权回调页面域名只能设置一个的问题
2016/12/11 PHP
TNC vs BOOM BO3 第三场2.13
2021/03/10 DOTA
更正确的asp冒泡排序
2007/05/24 Javascript
二级域名或跨域共享Cookies的实现方法
2008/08/07 Javascript
通过js简单实现将一个文本内容转译成加密文本
2013/10/22 Javascript
JS判断对象是否存在的10种方法总结
2013/12/23 Javascript
基于jQuery实现以手风琴方式展开和折叠导航菜单
2016/01/28 Javascript
js 自带的 map() 方法全面了解
2016/08/16 Javascript
微信小程序tabbar不显示解决办法
2017/06/08 Javascript
AngularJS基于provider实现全局变量的读取和赋值方法
2017/06/28 Javascript
JavaScript中防止微信浏览器被整体拖动的方法
2017/08/25 Javascript
vue实现仿淘宝结账页面实例代码
2017/11/08 Javascript
Vue.js实现列表清单的操作方法
2017/11/15 Javascript
详解如何在微信小程序开发中正确的使用vant ui组件
2018/09/13 Javascript
关于单文件组件.vue的使用
2018/09/20 Javascript
vue-router传递参数的几种方式实例详解
2018/11/13 Javascript
express中static中间件的具体使用方法
2019/10/17 Javascript
javascript实现fetch请求返回的统一拦截
2019/12/22 Javascript
解决vue 退出动画无效的问题
2020/08/09 Javascript
python 随机数生成的代码的详细分析
2011/05/15 Python
python利用beautifulSoup实现爬虫
2014/09/29 Python
Python常用的日期时间处理方法示例
2015/02/08 Python
对于Python编程中一些重用与缩减的建议
2015/04/14 Python
python机器学习库常用汇总
2017/11/15 Python
Python实现批量压缩图片
2018/01/25 Python
Python使用matplotlib和pandas实现的画图操作【经典示例】
2018/06/13 Python
Scrapy框架爬取Boss直聘网Python职位信息的源码
2019/02/22 Python
python 实现查询Neo4j多节点的多层关系
2019/12/23 Python
python3中关于excel追加写入格式被覆盖问题(实例代码)
2020/01/10 Python
python实现sm2和sm4国密(国家商用密码)算法的示例
2020/09/26 Python
Blue Nile台湾:钻石珠宝商,订婚首饰、结婚戒指和精品首饰
2017/11/24 全球购物
城市规划毕业生求职信
2013/10/10 职场文书
2014年图书馆个人工作总结
2014/12/18 职场文书
公司酒会致辞
2015/07/30 职场文书
2019年特色火锅店的创业计划书模板
2019/08/28 职场文书