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 相关文章推荐
5个最佳的Javascript日期处理类库分享
Apr 15 Javascript
详解JavaScript数组的操作大全
Oct 19 Javascript
Bootstrap插件全集
Jul 18 Javascript
Vue.js每天必学之内部响应式原理探究
Sep 07 Javascript
Javascript中call,apply,bind方法的详解与总结
Dec 12 Javascript
使用openSpeDiv方法实现Ecshop登录弹窗框效果
Mar 13 Javascript
巧用weui.topTips验证数据的实例
Apr 17 Javascript
Node.js npm命令运行node.js脚本的方法
Oct 10 Javascript
小程序实现左滑删除功能
Oct 30 Javascript
详细讲解如何创建, 发布自己的 Vue UI 组件库
May 29 Javascript
vue 使用element-ui中的Notification自定义按钮并实现关闭功能及如何处理多个通知
Aug 17 Javascript
vue实现修改图片后实时更新
Nov 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
web方式ftp
2006/10/09 PHP
php 什么是PEAR?(第三篇)
2009/03/19 PHP
PHP实现手机归属地查询API接口实现代码
2012/08/27 PHP
PHP终止脚本运行三种实现方法详解
2020/09/01 PHP
javascript验证身份证完全方法具体实现
2013/11/18 Javascript
javascript中数组的冒泡排序使用示例
2013/12/18 Javascript
jquery uploadify 在FF下无效的解决办法
2014/09/26 Javascript
jquery插件jquery.confirm弹出确认消息
2015/12/22 Javascript
WordPress中利用AJAX异步获取评论用户头像的方法
2016/01/08 Javascript
jQuery实现简单的tab标签页效果
2016/09/12 Javascript
读Javascript高性能编程重点笔记
2016/12/21 Javascript
angularjs实现的前端分页控件示例
2017/02/10 Javascript
JavaScript简介_动力节点Java学院整理
2017/06/26 Javascript
详解webpack性能优化——DLL
2017/10/20 Javascript
javascript按钮禁用和启用的效果实例代码
2017/10/29 Javascript
TypeScript基础入门教程之三重斜线指令详解
2018/10/22 Javascript
vue使用el-upload上传文件及Feign服务间传递文件的方法
2019/03/15 Javascript
基于VSCode调试网页JavaScript代码过程详解
2020/07/20 Javascript
微信小程序实现modal弹出框遮罩层组件(可带文本框)
2020/12/20 Javascript
python爬虫入门教程之点点美女图片爬虫代码分享
2014/09/02 Python
详解appium+python 启动一个app步骤
2017/12/20 Python
python实现归并排序算法
2018/11/22 Python
python的range和linspace使用详解
2019/11/27 Python
Pytorch中的VGG实现修改最后一层FC
2020/01/15 Python
Python几种常见算法汇总
2020/06/02 Python
Python decimal模块使用方法详解
2020/06/08 Python
解决keras backend 越跑越慢问题
2020/06/18 Python
小学语文国培感言
2014/03/04 职场文书
治安消防安全责任书
2014/07/23 职场文书
高中学生自我评价范文
2014/09/23 职场文书
教师四风问题整改措施
2014/09/25 职场文书
业务员岗位职责
2015/02/03 职场文书
公司内部升职自荐信
2015/03/27 职场文书
vue二维数组循环嵌套方式 循环数组、循环嵌套数组
2022/04/24 Vue.js
SQL Server中锁的用法
2022/05/20 SQL Server
Spring Cloud OAuth2实现自定义token返回格式
2022/06/25 Java/Android