[转] js对象监听实现
前言
随着前端交互复杂度的提升,各类框架如angular,react,vue等也层出不穷,这些框架一个比较重要的技术点就是数据绑定。数据的监听有较多的实现方案,本文将粗略的描述一番,并对其中一个兼容性较好的深入分析。
实现方案简介
目前对象的监听可行的方案:
脏检查: 需要遍历scope对象树里的$watch数组,使用不当容易造成性能问题
ES5 object.defineproperty: 除ie8部分支持 其他基本都完全支持
ES7 object.observe : 已经移除(缘由)出ES7草案
gecko object.watch :目前只有基于gecko的浏览器如火狐支持,官方建议仅供调试用
ES6 Proxy: 目前支持较差,babel也暂不支持转化
ES5现代浏览器基本都支持了,OK,本文将介绍目前支持度最好的object.defineproperty 的Setters 和 Getters方式
object.defineproperty介绍
简洁的介绍
它属于es5规范,有两种定义属性:
一种是 数据属性 包含Writable,Enumerable,Configurable
一种是 访问器属性 包含get 和set
数据属性的例子
obj.key='static';
//等效于
Object.defineProperty(obj, "key", {
enumerable: true,
configurable: true,
writable: true,
value: "static"
});
访问器属性例子
var obj = {
temperature:'test'
};
var temperature='';
Object.defineProperty(obj, 'temperature', {
get: function() {
return temperature+'-----after';
},
set: function(value) {
temperature = value;
}
})
obj.temperature='Test';
//Test-----after
console.log(obj.temperature);
详细的介绍
实现监听的思路
将需要监听对象/数组 obj和回调函数callback传入构造函数,this.callback = callback 存储回调函数
遍历对象/数组obj,通过Object.defineProperty将属性全部定义一遍。在set函数里面添加callback函数,设置val值。get函数返回val。
判断对应的obj[key]是否为对象,是则进入第二步,否则继续遍历
遍历结束之后判断该对象是否为数组,是则对操作数组函数如push,pop,shift,unshift等进行封装,操作数组前调用callback函数
数组的封装
比较复杂的是数组的封装,结构如下:
新建一个对象newProto,继承Array的原型,并在newProto上面封装push,pop等数组操作方法,再将传入的array对象的原型设置为newProto。
对应图
路径的定位
在获取数据变化的同时,定位该变化数据在原始根对象的位置,以数组表示如:
如[ 'a', 'dd', 'ddd' ] 表示对象obj.a.dd.ddd的属性改变
实现:每个遍历对象属性都通过path.slice(0)的方式复制入参数组path,生成新数组tpath,给tpath数组push对应的对象属性key,最后在执行set的回调函数时候将tpath当参数传入
带注释代码
watch.js
/**
*
* @param obj 需要监听的对象或数组
* @param callback 当对应属性变化的时候触发的回调函数
* @constructor
*/
function Watch(obj, callback) {
this.callback = callback;
//监听_obj对象 判断是否为对象,如果是数组,则对数组对应的原型进行封装
//path代表相应属性在原始对象的位置,以数组表示. 如[ 'a', 'dd', 'ddd' ] 表示对象obj.a.dd.ddd的属性改变
this.observe = function (_obj, path) {
var type=Object.prototype.toString.call(_obj);
if (type== '[object Object]'||type== '[object Array]') {
this.observeObj(_obj, path);
if (type == '[object Array]') {
this.cloneArray(_obj, path);
}
}
};
//遍历对象obj,设置set,get属性,set属性能触发callback函数,并将val的值改为newVal
//遍历结束后再次调用observe函数 判断val是否为对象,如果是则在对val进行遍历设置set,get
this.observeObj = function (obj, path) {
var t = this;
Object.keys(obj).forEach(function (prop) {
var val = obj[prop];
var tpath = path.slice(0);
tpath.push(prop);
Object.defineProperty(obj, prop, {
get: function () {
return val;
},
set: function (newVal) {
t.callback(tpath, newVal, val);
val = newVal;
}
});
t.observe(val, tpath);
});
};
//通过对特定数组的原型中间放一个newProto原型,该原型继承于Array的原型,但是对push,pop等数组操作属性进行封装
this.cloneArray = function (a_array, path) {
var ORP = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
var arrayProto = Array.prototype;
var newProto = Object.create(arrayProto);
var t = this;
ORP.forEach(function (prop) {
Object.defineProperty(newProto, prop, {
value: function (newVal) {
path.push(prop);
t.callback(path, newVal);
arrayProto[prop].apply(a_array, arguments);
},
enumerable: false,
configurable: true,
writable: true
});
});
a_array.__proto__ = newProto;
};
//开始监听obj对象,初始path为[]
this.observe(obj, []);
}
index.html
<body>
<ul>
<li>
<a href="javascript:void(0)" onClick="dataOne()">
将obj b属性改变
</a>
</li>
<li>
<a href="javascript:void(0)" onClick="dataTwo()">
将obj a属性的dd属性的ddd属性改变
</a>
</li>
<li>
<a href="javascript:void(0)" onClick="dataThree()">
将obj a属性的g属性数组第一个值的a属性改变
</a>
</li>
<li>
<a href="javascript:void(0)" onClick="dataFour()">
将obj a属性的g属性数组push新的值
</a>
</li>
</ul>
<div id="path">
</div>
<div id="old-val">
</div>
<div id="new-val">
</div>
</body>
<script src="../src/watch.js"></script>
<script>
var obj = {
a: {e: 4, f: 5, g: [{a: 1, b: 2}, [3, 4]], dd: {ddd: 1}},
b: 2,
c: 3
};
new Watch(obj, call);
function call(path, newVal, oldVal) {
document.getElementById('path').innerHTML='路径:'+path;
document.getElementById('old-val').innerHTML='新的值:'+newVal;
document.getElementById('new-val').innerHTML='老的值:'+oldVal;
}
function dataOne() {
obj.b = Math.floor(Math.random()*10);
}
function dataTwo() {
obj.a.dd.ddd = Math.floor(Math.random()*10);
}
function dataThree() {
obj.a.g[0].a=Math.floor(Math.random()*10);
}
function dataFour() {
obj.a.g.push(Math.floor(Math.random()*10));
}
</script>
效果图
代码地址
流程图
具体流程的复杂度基于监听对象的深度,所以下图只对父对象做流程分析
归纳
通过定义对象内部属性的setter和getter方法,对将要变化的属性进行拦截代理,在变化前执行预设的回调函数来达到对象监听的目的。
数组则在对象监听之外额外在数组对象上的原型链上加一层原型对象,来拦截掉push,pop等方法,然后在执行预设的回调函数
[转] js对象监听实现的更多相关文章
- js事件监听机制(事件捕获)总结
在前端开发过程中我们经常会遇到给页面元素添加事件的问题,添加事件的js方法也很多,有直接加到页面结构上的,有使用一些js事件监听的方法,由于各个浏览器对事件冒泡事件监听的机制不同,le浏览器只有事件冒 ...
- js动态监听dom变化
原生js 动态监听dom变化,根据不同的类型绑定不同的处理逻辑 // Firefox和Chrome早期版本中带有前缀 var MutationObserver = window.MutationO ...
- js事件监听机制(事件捕获)
在前端开发过程中我们经常会遇到给页面元素添加事件的问题,添加事件的js方法也很多,有直接加到页面结构上的,有使用一些js事件监听的方法,由于各个浏览器对事件冒泡事件监听的机制不同,le浏览器只有事件冒 ...
- Vue.js:监听属性
ylbtech-Vue.js:监听属性 1.返回顶部 1. Vue.js 监听属性 本章节,我们将为大家介绍 Vue.js 监听属性 watch,我们可以通过 watch 来响应数据的变化: 实例 & ...
- js 实时监听input中值变化
注意:用到了jquery需要引入jquery.min.js. 需求: 1.每个地方需要分别打分,总分为100; 2.第一个打分总分为40; 3.第二个打分总分为60. 注意:需要判断null.&quo ...
- js 事件监听 冒泡事件
js 事件监听 冒泡事件 的取消 [自己写框架时,才有可能用到] <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitiona ...
- js 事件监听 兼容浏览器
js 事件监听 兼容浏览器 ie 用 attachEvent w3c(firefox/chrome) 用 addEventListener 删除事件监听 ie 用 detachEven ...
- ANGULAR JS WATCH监听使用(详)
ANGULAR 监听使用: 当angular数据模型发生变化时,我们需要如果需要根据他的变化触发其他的事件. $watch是一个scope函数,用于监听模型变化,当你的模型部分发生变化时它会通知你. ...
- js 事件监听封装
var eventUtil={//添加句柄 //element,节点 //type,事件类型 //handler,函数 addHandler:function(element,type,handler ...
随机推荐
- sudo su 和 sudo -s【转】
sudo su 和 sudo -s 都是切换到root用户,不同的是 sudo su 环境用的是目标用户 (root)的环境 sudo -s 环境用的是当前用户本身的环境 转自 sudo su 和 s ...
- vc++基础班[23]---文件夹的基本操作
①.文件夹的创建:CreateDirectory ※※※ 注意:此函数只能创建一层目录,比如想在 C 盘下的 Temp 目录下创建新目录为:123 那么前提是 Temp 这个目录存在才可以! ...
- 安装mongo php拓展
下载php_mongo.dll文件 下载地址:https://s3.amazonaws.com/drivers.mongodb.org/php/index.html(注意对应版本及是否线程安全)需要注 ...
- 034_nginx报错总结
一.nginx: [emerg] "client_header_timeout" directive is not allowed here in /opt/nginx/conf/ ...
- 020_iPhone救命稻草
一.如何对iPhone强制恢复出厂设置 1.在"通用"->"设置"->"还原全部设置",但是我的不知道为啥除了设置完开机密码后, ...
- web@HTML常用标签分类,标签嵌套规则
一.html标签又叫做html元素,它分为块级元素和内联元素(也可以叫做行内元素),都是html规范中的概念.1.块级元素块级元素是指本身属性为display:block;的元素.因为它自身的特点,我 ...
- object oriented programming : class application
class Thread_Sync; class Critical; class Info; class Info{Info(std::string str):m_info(str){} privat ...
- tp5 设置layui分页
\thinkphp\library\think\paginator\driver 添加 Layui.php <?php namespace think\paginator\driver; use ...
- Modbus库开发笔记:Modbus ASCII Slave开发
与Modbus RTU在串行链路上分为Slave和Master一样,Modbus ASCII也分为Slave和Master,这一节我们就来开发Slave.对于Modbus ASCII从站来说,需要实现 ...
- 设置外部查找工具来索引 Confluence 6
任何网页的 crawler 工具都可以被用来索引你的 Confluence 站点中的内容.如果你希望注册用户才能够查看的内容也被索引的话,你需要为你的 Confluence 创建一个只被 crawl ...