用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 相关文章推荐
表单提交验证类
Jul 14 Javascript
JS写的数字拼图小游戏代码[学习参考]
Oct 29 Javascript
jQuery 数据缓存模块进化史详细介绍
Nov 19 Javascript
深入分析原生JavaScript事件
Dec 29 Javascript
JavaScript中的slice()方法使用详解
Jun 06 Javascript
JQuery查找DOM节点的方法
Jun 11 Javascript
微信小程序 for 循环详解
Oct 09 Javascript
TableSort.js表格排序插件使用方法详解
Feb 10 Javascript
基于input框覆盖掉数字英文的实例讲解
Jul 21 Javascript
vue-resource拦截器设置头信息的实例
Oct 27 Javascript
Vue.js实现大屏数字滚动翻转效果
Nov 29 Javascript
详解js中的原型,原型对象,原型链
Jul 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实现监听事件
2013/11/06 PHP
深入理解PHP中的empty和isset函数
2016/05/26 PHP
PHP空值检测函数与方法汇总
2017/11/19 PHP
PHP将整数数字转换为罗马数字实例分享
2019/03/17 PHP
php 使用expat方式解析xml文件操作示例
2019/11/26 PHP
php设计模式之正面模式实例分析【星际争霸游戏案例】
2020/03/24 PHP
深入聊聊Array的sort方法的使用技巧.详细点评protype.js中的sortBy方法
2007/04/12 Javascript
用js实现的自定义的对话框的实现代码
2010/03/21 Javascript
jquery选择器-根据多个属性选择示例代码
2013/10/21 Javascript
cocos2dx骨骼动画Armature源码剖析(一)
2015/09/08 Javascript
jquery轮播的实现方式 附完整实例
2016/07/28 Javascript
JS简单获取及显示当前时间的方法
2016/08/03 Javascript
使用Javascript监控前端相关数据的代码
2016/10/27 Javascript
jQuery解析返回的xml和json方法详解
2017/01/05 Javascript
JavaScript实现审核流程状态的动态显示进度条
2017/03/15 Javascript
vue中路由参数传递可能会遇到的坑
2017/12/07 Javascript
vue 双向数据绑定的实现学习之监听器的实现方法
2018/11/30 Javascript
js中Generator函数的深入讲解
2019/04/07 Javascript
浅谈layui分页控件field参数接收对象的问题
2019/09/20 Javascript
[57:09]DOTA2-DPC中国联赛 正赛 Phoenix vs Dynasty BO3 第一场 1月26日
2021/03/11 DOTA
Python中常用操作字符串的函数与方法总结
2016/02/04 Python
python机器学习案例教程——K最近邻算法的实现
2017/12/28 Python
Python线性回归实战分析
2018/02/01 Python
详解Python中的动态属性和特性
2018/04/07 Python
python爬虫_实现校园网自动重连脚本的教程
2018/04/22 Python
使用python自动追踪你的快递(物流推送邮箱)
2020/03/17 Python
django列表筛选功能的实现代码
2020/03/27 Python
使用Python和百度语音识别生成视频字幕的实现
2020/04/09 Python
Python进行特征提取的示例代码
2020/10/15 Python
HTML5 Canvas绘制文本及图片的基础教程
2016/03/14 HTML / CSS
先进事迹演讲稿
2014/09/01 职场文书
办公室领导干部作风整顿个人整改措施
2014/09/17 职场文书
2015年三好一满意工作总结
2015/07/24 职场文书
Python中异常处理用法
2021/11/27 Python
java协程框架quasar和kotlin中的协程对比分析
2022/02/24 Java/Android
Go归并排序算法的实现方法
2022/04/06 Golang