Vue监听数据对象变化源码


Posted in Javascript onMarch 09, 2017

监听数据对象变化,最容易想到的是建立一个需要监视对象的表,定时扫描其值,有变化,则执行相应操作,不过这种实现方式,性能是个问题,如果需要监视的数据量大的话,每扫描一次全部的对象,需要的时间很长。当然,有些框架是采用的这种方式,不过他们用非常巧妙的算法提升性能,这不在我们的讨论范围之类。

Vue 中数据对象的监视,是通过设置 ES5 的新特性(ES7 都快出来了,ES5 的东西倒也真称不得新)Object.defineProperty() 中的 set、get 来实现的。

目标

与官方文档第一个例子相似,不过也有简化,因为这篇只是介绍下数据对象的监听,不涉及文本解析,所以文本解析相关的直接舍弃了:

<div id="app"></div>
var app = new Vue({
 el: 'app',
 data: {
 message: 'Hello Vue!'
 }
});

浏览器显示:

Hello Vue!

在控制台输入诸如:

app.message = 'Changed!'

之类的命令,浏览器显示内容会跟着修改。

Object.defineProperty

引用 MDN 上的定义:

Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。
与此相生相伴的还有一个 Object.getOwnPropertyDescriptor():

Object.getOwnPropertyDescriptor() 返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

下面的例子用一种比较简单、直观的方式来设置 setter、getter:

var dep = [];

function defineReactive(obj, key, val) {
 // 有自定义的 property,则用自定义的 property
 var property = Object.getOwnPropertyDescriptor(obj, key);
 if(property && property.configurable === false) {
 return;
 }

 var getter = property && property.get;
 var setter = property && property.set;

 Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get: function() {
  var value = getter ? getter.call(obj) : val;
  dep.push(value);
  return value;
 },
 set: function(newVal) {
  var value = getter ? getter.call(obj) : val;
  // set 值与原值相同,则不更新
  if(newVal === value) {
  return;
  }
  if(setter) {
  setter.call(obj, newVal);
  } else {
  val = newVal;
  }
  console.log(dep);
 }
 });
}
var a = {};
defineReactive(a, 'a', 12);
// 调用 getter,12 被压入 dep,此时 dep 值为 [12]
a.a;
// 调用 setter,输出 dep ([12])
a.a = 24;
// 调用 getter,24 被压入 dep,此时 dep 值为 [12, 24]
a.a;

Observer

简单说过 Object.defineProperty 之后,就要开始扯 Observer 了。observer,中文解释为“观察者”,观察什么东西呢?观察对象属性值的变化。故此,所谓 observer,就是给对象的所有属性加上 getter、setter,如果对象的属性还有属性,比如说 {a: {a: {a: 'a'}}},则通过递归给其属性的属性也加上 getter、setter:

function Observer(value) {
 this.value = value;
 this.walk(value);
}
Observer.prototype.walk = function(obj) {
 var keys = Object.keys(obj);
 for(var i = 0; i < keys.length; i++) {
 // 给所有属性添加 getter、setter
 defineReactive(obj, keys[i], obj[keys[i]]);
 }
};

var dep = [];

function defineReactive(obj, key, val) {
 // 有自定义的 property,则用自定义的 property
 var property = Object.getOwnPropertyDescriptor(obj, key);
 if(property && property.configurable === false) {
 return;
 }

 var getter = property && property.get;
 var setter = property && property.set;

 // 递归的方式实现给属性的属性添加 getter、setter
 var childOb = observe(val);
 Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get: function() {
  var value = getter ? getter.call(obj) : val;
  dep.push(value);
  return value;
 },
 set: function(newVal) {
  var value = getter ? getter.call(obj) : val;
  // set 值与原值相同,则不更新
  if(newVal === value) {
  return;
  }
  if(setter) {
  setter.call(obj, newVal);
  } else {
  val = newVal;
  }
  // 给新赋值的属性值的属性添加 getter、setter
  childOb = observe(newVal);
  console.log(dep);
 }
 });
}

function observe(value) {
 if(!value || typeof value !== 'object') {
 return;
 }
 return new Observer(value);
}

Watcher

Observer 通过设置数据对象的 getter、setter 来达到监听数据变化的目的。数据被获取,被设置、被修改,都能监听到,且能做出相应的动作。

现在还有一个问题就是,谁让你监听的?

这个发出指令的就是 Watcher,只有 Watcher 获取数据才触发相应的操作;同样,修改数据时,也只执行 Watcher 相关操作。

那如何讲 Observer、Watcher 两者关联起来呢?全局变量!这个全局变量,只有 Watcher 才做修改,Observer 只是读取判断,根据这个全局变量的值不同而判断是否 Watcher 对数据进行读取,这个全局变量可以附加在 dep 上:

dep.target = null;

根据以上所述,简单整理下,代码如下:

function Watcher(data, exp, cb) {
 this.data = data;
 this.exp = exp;
 this.cb = cb;
 this.value = this.get();
}
Watcher.prototype.get = function() {
 // 给 dep.target 置值,告诉 Observer 这是 Watcher 调用的 getter
 dep.target = this;
 // 调用 getter,触发相应响应
 var value = this.data[this.exp];
 // dep.target 还原
 dep.target = null;
 return value;
};
Watcher.prototype.update = function() {
 this.cb();
};
function Observer(value) {
 this.value = value;
 this.walk(value);
}
Observer.prototype.walk = function(obj) {
 var keys = Object.keys(obj);
 for(var i = 0; i < keys.length; i++) {
 // 给所有属性添加 getter、setter
 defineReactive(obj, keys[i], obj[keys[i]]);
 }
};

var dep = [];
dep.target = null;

function defineReactive(obj, key, val) {
 // 有自定义的 property,则用自定义的 property
 var property = Object.getOwnPropertyDescriptor(obj, key);
 if(property && property.configurable === false) {
 return;
 }

 var getter = property && property.get;
 var setter = property && property.set;

 // 递归的方式实现给属性的属性添加 getter、setter
 var childOb = observe(val);
 Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get: function() {
  var value = getter ? getter.call(obj) : val;
  // 如果是 Watcher 监听的,就把 Watcher 对象压入 dep
  if(dep.target) {
  dep.push(dep.target);
  }
  return value;
 },
 set: function(newVal) {
  var value = getter ? getter.call(obj) : val;
  // set 值与原值相同,则不更新
  if(newVal === value) {
  return;
  }
  if(setter) {
  setter.call(obj, newVal);
  } else {
  val = newVal;
  }
  // 给新赋值的属性值的属性添加 getter、setter
  childOb = observe(newVal);
  // 按序执行 dep 中元素的 update 方法
  for(var i = 0; i < dep.length; i++) {
  dep[i].update(); 
  }
 }
 });
}

function observe(value) {
 if(!value || typeof value !== 'object') {
 return;
 }
 return new Observer(value);
}
var data = {a: 1};
new Observer(data);
new Watcher(data, 'a', function(){console.log('it works')});
data.a =12;
data.a =14;

上面基本实现了数据的监听,bug 肯定有不少,不过只是一个粗糙的 demo,只是想展示一个大概的流程,没有扣到非常细致。

Dep

上面几个例子,dep 是个全局的数组,但凡 new 一个 Watcher,dep 中就要多一个 Watcher 实例,这时候不管哪个 data 更新,所有的 Watcher 实例的 update 都会执行,这是不可接受的。

Dep 抽象出来,单独搞一个构造函数,不放在全局,就能解决了:

function Dep() {
 this.subs = [];
}
Dep.prototype.addSub = function(sub) {
 this.subs.push(sub);
};
Dep.prototype.notify = function() {
 var subs = this.subs.slice();
 for(var i = 0; i < subs.length; i++) {
 subs[i].update();
 }
}

利用 Dep 将上面的代码改写下就好了(当然,此处的 Dep 代码也不完全,只是一个大概的意思罢了)。

Vue 实例代理 data 对象

官方文档中有这么一句话:

每个 Vue 实例都会代理其 data 对象里所有的属性。

var data = { a: 1 };
var vm = new Vue({data: data});

vm.a === data.a // -> true

// 设置属性也会影响到原始数据
vm.a = 2
data.a // -> 2

// ... 反之亦然
data.a = 3
vm.a // -> 3

这种代理看起来很麻烦,其实也是可以通过 Object.defineProperty 来实现的:

function Vue(options) {
 var data = this.data = options.data;

 var keys = Object.keys(data);
 var i = keys.length;
 while(i--) {
 proxy(this, keys[i];
 }
}
function proxy(vm, key) {
 Object.defineProperty(vm, key, {
 configurable: true,
 enumerable: true,
 // 直接获取 vm.data[key] 的值
 get: function() {
  return vm.data[key];
 },
 // 设置值的时候直接设置 vm.data[key] 的值
 set: function(val) {
  vm.data[key] = val;
 }
 };
}

捏出一个 Vue,实现最初目标

var Vue = (function() {
 var Watcher = function Watcher(vm, exp, cb) {
  this.vm = vm;
  this.exp = exp;
  this.cb = cb;
  this.value = this.get();
 };
 Watcher.prototype.get = function get() {
  Dep.target = this;
  var value = this.vm._data[this.exp];
  Dep.target = null;
  return value;
 };
 Watcher.prototype.addDep = function addDep(dep) {
  dep.addSub(this);
 };
 Watcher.prototype.update = function update() {
  this.run();
 };
 Watcher.prototype.run = function run() {
  this.cb.call(this.vm);
 }

 var Dep = function Dep() {
  this.subs = [];
 };
 Dep.prototype.addSub = function addSub(sub) {
  this.subs.push(sub);
 };
 Dep.prototype.depend = function depend() {
  if(Dep.target) {
   Dep.target.addDep(this);
  }
 };
 Dep.prototype.notify = function notify() {
  var subs = this.subs.slice();
  for(var i = 0; i < subs.length; i++) {
   subs[i].update();
  }
 };

 Dep.target = null;

 var Observer = function Observer(value) {
  this.value = value;
  this.dep = new Dep();

  this.walk(value);
 };
 Observer.prototype.walk = function walk(obj) {
  var keys = Object.keys(obj);

  for(var i = 0; i < keys.length; i++) {
   defineReactive(obj, keys[i], obj[keys[i]]);
  }
 };

 function defineReactive(obj, key, val) {
  var dep = new Dep();

  var property = Object.getOwnPropertyDescriptor(obj, key);
  if(property && property.configurable === false) {
   return;
  }

  var getter = property && property.get;
  var setter = property && property.set;

  var childOb = observe(val);
  Object.defineProperty(obj, key, {
   enumerable: true,
   configurable: true,
   get: function reactiveGetter() {
    var value = getter ? getter.call(obj) : val;

    if(Dep.target) {
     dep.depend();
     if(childOb) {
      childOb.dep.depend();
     }
    }
    return value;
   },
   set: function reactiveSetter(newVal) {
    var value = getter ? getter.call(obj) : val;
    if(newVal === value) {
     return;
    }
    if(setter) {
     setter.call(obj, newVal);
    } else {
     val = newVal;
    }
    childOb = observe(newVal);
    dep.notify();
   }
  });
 }
 function observe(value) {
  if(!value || typeof value !== 'object') {
   return;
  }
  return new Observer(value);
 }

 function Vue(options) {
  var vm = this;
  this._el = options.el;
  var data = this._data = options.data;

  var keys = Object.keys(data);
  var i = keys.length;
  while(i--) {
   proxy(this, keys[i]);
  }
  observe(data);

  var elem = document.getElementById(this._el);
  elem.innerHTML = vm.message;

  new Watcher(this, 'message', function() {
   elem.innerHTML = vm.message;
  });

 }
 function proxy(vm, key) {
  Object.defineProperty(vm, key, {
   configurable: true,
   enumerable: true,
   get: function proxyGetter() {
    return vm._data[key];
   },
   set: function proxySetter(val) {
    vm._data[key] = val;
   }
  });
 }
 return Vue;
})();
<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <title>Document</title>
 <script type="text/javascript" src="vue.js"></script>
</head>
<body>
 <div id="app"></div>
 <script type="text/javascript">
  var app = new Vue({
   el: 'app',
   data: {
    message: 'aaaaaaaaaaaaa'
   }
  });
 </script>
</body>
</html>

参考资料:

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

Javascript 相关文章推荐
javascript中方便增删改cookie的一个类
Oct 11 Javascript
jQuery中获取Radio元素值的方法
Jul 02 Javascript
jquery 获取表单元素里面的值示例代码
Jul 28 Javascript
纯JS实现本地图片预览的方法
Jul 31 Javascript
javascript伸缩型菜单实现代码
Nov 16 Javascript
Jq通过td获取同行其它列td的方法
Oct 05 Javascript
网页爬虫之cookie自动获取及过期自动更新的实现方法
Mar 06 Javascript
聊聊JS动画库 Velocity.js的使用
Mar 13 Javascript
vue如何将v-for中的表格导出来
May 07 Javascript
JS实现图片旋转动画效果封装与使用示例
Jul 09 Javascript
JS中的算法与数据结构之字典(Dictionary)实例详解
Aug 20 Javascript
vue项目实现多语言切换的思路
Sep 17 Javascript
html+javascript+bootstrap实现层级多选框全层全选和多选功能
Mar 09 #Javascript
Node.js常用工具之util模块
Mar 09 #Javascript
js遍历json对象所有key及根据动态key获取值的方法(必看)
Mar 09 #Javascript
jQuery插件HighCharts实现的2D回归直线散点效果示例【附demo源码下载】
Mar 09 #Javascript
js实现简单的二级联动效果
Mar 09 #Javascript
jquery表单提交带错误信息提示效果
Mar 09 #Javascript
AngularJS 防止页面闪烁的方法
Mar 09 #Javascript
You might like
PHP绕过open_basedir限制操作文件的方法
2018/06/10 PHP
PDO::getAttribute讲解
2019/01/28 PHP
JavaScript 组件之旅(一)分析和设计
2009/10/28 Javascript
JS实现多物体缓冲运动实例代码
2013/11/29 Javascript
jquery map方法使用示例
2014/04/23 Javascript
流量统计器如何鉴别C#:WebBrowser中伪造referer
2015/01/07 Javascript
介绍JavaScript的一个微型模版
2015/06/24 Javascript
apply和call方法定义及apply和call方法的区别
2015/11/15 Javascript
通过点击jqgrid表格弹出需要的表格数据
2015/12/02 Javascript
JavaScript基础知识之方法汇总结
2016/01/24 Javascript
jQuery on()绑定动态元素出现的问题小结
2016/02/19 Javascript
原生JavaScript实现Ajax的方法
2016/04/07 Javascript
使用jquery提交form表单并自定义action的方法
2016/05/25 Javascript
javascript实现简易计算器的代码
2016/05/31 Javascript
详解AngularJS 路由 resolve用法
2017/04/24 Javascript
基于node搭建服务器,写接口,调接口,跨域的实例
2018/05/13 Javascript
教你如何用Node实现API的转发(某音乐)
2019/09/20 Javascript
使用JavaScript实现贪吃蛇游戏
2020/09/29 Javascript
[39:11]DOTA2上海特级锦标赛C组资格赛#2 LGD VS Newbee第二局
2016/02/28 DOTA
[01:38]DOTA2第二届亚洲邀请赛中国区预选赛出线战队晋级之路
2017/01/17 DOTA
Python读取ini文件、操作mysql、发送邮件实例
2015/01/01 Python
Centos Python2 升级到Python3的简单实现
2016/06/21 Python
Ubuntu16.04/树莓派Python3+opencv配置教程(分享)
2018/04/02 Python
python logging重复记录日志问题的解决方法
2018/07/12 Python
Python 串口读写的实现方法
2019/06/12 Python
Python的log日志功能及设置方法
2019/07/11 Python
python爬虫用scrapy获取影片的实例分析
2020/11/23 Python
战友聚会邀请函
2014/01/18 职场文书
办公室员工岗位工作职责
2014/03/10 职场文书
大学生就业策划书范文
2014/04/04 职场文书
法学专业毕业生求职信
2014/06/12 职场文书
家具商场的活动方案
2014/08/16 职场文书
群众路线个人对照检查材料
2014/09/23 职场文书
写给领导的感谢信
2015/01/22 职场文书
跳高加油稿
2015/07/21 职场文书
Python爬取科目四考试题库的方法实现
2021/03/30 Python