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 相关文章推荐
jQuery html() in Firefox (uses .innerHTML) ignores DOM changes
Mar 05 Javascript
eval的两组性能测试数据
Aug 17 Javascript
JavaScript创建对象的写法
Aug 29 Javascript
js调试工具console.log()方法查看js代码的执行情况
Aug 08 Javascript
编写高质量JavaScript代码的基本要点
Mar 02 Javascript
Bootstrap自定义文件上传下载样式
May 26 Javascript
JS 判断某变量是否为某数组中的一个值的3种方法(总结)
Jul 10 Javascript
微信小程序实现带参数的分享功能(两种方法)
May 17 Javascript
vue引入微信sdk 实现分享朋友圈获取地理位置功能
Jul 04 Javascript
JavaScript this使用方法图解
Feb 04 Javascript
微信jssdk踩坑之签名错误invalid signature
May 19 Javascript
vue使用Sass时报错问题的解决方法
Oct 14 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
一个查看session内容的函数
2006/10/09 PHP
php列出一个目录下的所有文件的代码
2012/10/09 PHP
mysqli_set_charset和SET NAMES使用抉择及优劣分析
2013/01/13 PHP
php可扩展的验证类实例(可对邮件、手机号、URL等验证)
2015/07/09 PHP
PHP实现文件上传与下载实例与总结
2016/03/13 PHP
PHP中Laravel 关联查询返回错误id的解决方法
2017/04/01 PHP
PHPMailer使用QQ邮箱实现邮件发送功能
2017/08/18 PHP
PHP中in_array的隐式转换的解决方法
2018/03/06 PHP
JavaScript 异步调用框架 (Part 1 - 问题 &amp; 场景)
2009/08/03 Javascript
实现只能输入数字的input不用replace方法
2013/09/12 Javascript
一行命令搞定node.js 版本升级
2014/07/20 Javascript
jQuery获得子元素个数的方法
2015/04/14 Javascript
最简单的JavaScript验证整数、小数、实数、有效位小数正则表达式
2015/04/17 Javascript
javascript中select下拉框的用法总结
2016/01/07 Javascript
bootstrap table小案例
2016/10/21 Javascript
Bootstrap里的文件分别代表什么意思及其引用方法
2017/05/01 Javascript
JavaScript中最常用的10种代码简写技巧总结
2017/06/28 Javascript
详解设置Webstorm 利用babel将ES6自动转码成ES5
2017/12/20 Javascript
vue+element-ui集成随机验证码+用户名+密码的form表单验证功能
2018/08/05 Javascript
nodeJS进程管理器pm2的使用
2019/01/09 NodeJs
ES11新增的这9个新特性,你都掌握了吗
2020/10/15 Javascript
JavaScript 防盗链的原理以及破解方法
2020/12/29 Javascript
python学生信息管理系统(初级版)
2018/10/17 Python
pymongo中group by的操作方法教程
2019/03/22 Python
Python实现 PS 图像调整中的亮度调整
2019/06/28 Python
python3.7简单的爬虫实例详解
2019/07/08 Python
Django如何使用第三方服务发送电子邮件
2019/08/14 Python
PyCharm2018 安装及破解方法实现步骤
2019/09/09 Python
使用pytorch实现可视化中间层的结果
2019/12/30 Python
Python容器类型公共方法总结
2020/08/19 Python
详解android与HTML混合开发总结
2018/06/06 HTML / CSS
生产部岗位职责范文
2014/02/07 职场文书
学生会竞选演讲稿怎么写
2014/08/26 职场文书
在职证明格式样本
2015/06/15 职场文书
AJAX引擎原理以及XmlHttpRequest对象的axios、fetch区别详解
2022/04/09 Javascript
JS前端监控采集用户行为的N种姿势
2022/07/23 Javascript