用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 相关文章推荐
增强的 JavaScript 的 trim 函数的代码
Aug 13 Javascript
JavaScript的单例模式 (singleton in Javascript)
Jun 11 Javascript
JavaScript利用正则表达式去除日期中的“-”
Jul 01 Javascript
jQuery实现购物车计算价格功能的方法
Mar 25 Javascript
jstree创建无限分级树的方法【基于ajax动态创建子节点】
Oct 25 Javascript
深入理解Node.js 事件循环和回调函数
Nov 02 Javascript
javascript设置文本框光标的方法实例小结
Nov 04 Javascript
vue页面使用阿里oss上传功能的实例(一)
Aug 09 Javascript
详解JS浏览器事件循环机制
Mar 27 Javascript
JavaScript 函数用法详解【函数定义、参数、绑定、作用域、闭包等】
May 12 Javascript
通过实例了解JS执行上下文运行原理
Jun 17 Javascript
原生Javascript+HTML5一步步实现拖拽排序
Jun 12 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
如何将数据从文本导入到mysql
2006/10/09 PHP
php cli 方式 在crotab中运行解决
2010/02/08 PHP
两个开源的Php输出Excel文件类
2010/02/08 PHP
学习php设计模式 php实现观察者模式(Observer)
2015/12/09 PHP
PHP+swoole实现简单多人在线聊天群发
2016/01/19 PHP
Yii+upload实现AJAX上传图片的方法
2016/07/13 PHP
thinkphp配置文件路径的实现方法
2016/08/30 PHP
passwordStrength 基于jquery的密码强度检测代码使用介绍
2011/10/08 Javascript
Js 时间函数getYear()的使用问题探讨
2013/04/01 Javascript
使用ImageMagick进行图片缩放、合成与裁剪(js+python)
2013/09/16 Javascript
浅谈jQuery中 wrap() wrapAll() 与 wrapInner()的差异
2014/11/12 Javascript
JavaScript编程中的Promise使用大全
2015/07/28 Javascript
Angularjs 与 bower安装和使用详解
2017/05/11 Javascript
vue指令只能输入正数并且只能输入一个小数点的方法
2018/06/08 Javascript
详解javascript中的babel到底是什么
2018/06/21 Javascript
JS简单判断是否在微信浏览器打开的方法示例
2019/01/08 Javascript
Vue Router history模式的配置方法及其原理
2019/05/30 Javascript
[06:57]DOTA2-DPC中国联赛 正赛 Ehome vs PSG.LGD 选手采访
2021/03/11 DOTA
python读取html中指定元素生成excle文件示例
2014/04/03 Python
python通过装饰器检查函数参数数据类型的方法
2015/03/13 Python
Python fileinput模块使用实例
2015/06/03 Python
matplotlib 输出保存指定尺寸的图片方法
2018/05/24 Python
Python自动重新加载模块详解(autoreload module)
2020/04/01 Python
keras中epoch,batch,loss,val_loss用法说明
2020/07/02 Python
在pycharm创建scrapy项目的实现步骤
2020/12/01 Python
德国最大的设计师鞋网上商店:Budapester
2017/12/07 全球购物
美国最古老的精致书写工具制造商:A.T. Cross(高仕)
2018/01/30 全球购物
高中毕业生自我鉴定范文
2013/09/26 职场文书
企业后勤岗位职责
2014/02/28 职场文书
小学中等生评语
2014/12/29 职场文书
满月酒邀请函
2015/01/30 职场文书
辞职信的写法
2015/02/27 职场文书
SQL Server——索引+基于单表的数据插入与简单查询【1】
2021/04/05 SQL Server
Python异常类型以及处理方法汇总
2021/06/05 Python
Javascript webpack动态import
2022/04/19 Javascript
Windows11 Insider Preview Build 25206今日发布 更新内容汇总
2022/09/23 数码科技