专栏首页前端小作坊watch.js 源码解读

watch.js 源码解读

watch.js 源码解读

用麻雀虽小五脏俱全来描述Watch.js比较合适。“观察者”模式是我们在开发的时候经常需要用到的。使用Watch.js那么我们就可以实现在“每当对象属性改变的时候,执行你的函数”。虽然有很多其他的库可以实现相同的功能,但是Watch.js却可以不改变你平时书写代码的方式,并且实现属性改变的监听功能。

API

让我们来看看它的API:

//使用任意一种方式定义一个对象
var ex1 = {
    attr1: "initial value of attr1",
    attr2: "initial value of attr2"
};

//针对其中一个属性“attr1”设置“观察者”
watch(ex1, "attr1", function(){
    alert("attr1 changed!");
});

//当“attr1”修改的时候“观察者”函数会被调用
ex1.attr1 = "other value";`</pre>
[try demo](http://jsfiddle.net/NbJuh/17/)

Watch.js还提供了其他有用的api:监控多个属性、监控整个对象或移除整一个watch。你可能会想到循环调用,例如:

//使用任意一种方式定义一个对象
var ex1 = {
    attr1: "inicial value of attr1",
    attr2: "initial value of attr2"
};

//定义“attr1”属性的监听函数
watch(ex1, "attr1", function(){
    //在此域中阻止监听器的调用
    WatchJS.noMore = true;
    ex1.attr2 = ex1.attr1 + " + 1";
});

//给另一个属性定义“监听函数”
watch(ex1, "attr2", function(){
    alert("attr2 changes");
});

//attr1修改的时候,会修改attr2但不会触发attr2的“监听函数”
ex1.attr1 = "other value to 1";

WatchJS.noMore = true可以确保在此次监听函数的调用中,对属性的修改不会触发新的监听函数。接下来看看它是如何实现它们的!

Object.defineProperty

Object.defineProperty是ECMAScript 5标准提供的方法,它允许你在一个对象上定义一个新属性或是修改原有属性的描述符。这个方法接收一个属性描述符,并用它来初始化(或更新)一个属性。例如:

(function() {
    var obj = {},
        value = 1;

    Object.defineProperty( obj, "value", {
        // value: true,
        // writable: false,
        enumerable: true,
        configurable: true,
        get: function() {
            return value;
        },
        set: function(v) {
            value = v);
        }
    });
})();

defineProperty的第三个参数是描述符,value设置属性值,writable表示属性是否可写(不会报错,只是写操作不生效),enumerable表示属性是否可以通过for in迭代获取,configurable表示属性该属性的描述符无法再被定义,并且属性也无法被deletegetset分别定义属性的access方法,注意不可同时定义属性的accessor和value及writable参数。具体的方法描述参考MDN上的文档

Watch.js利用了属性的accessor方法实现了对属性变化的监听,代码如下:

defineWatcher = function (obj, prop, watcher) {
    var val = obj[prop];

    // 监听数组的操作函数,用于数组变化的监听
    watchFunctions(obj, prop);

    // 初始化watchers对象,用以存储监听者
    if (!obj.watchers) {
        defineProp(obj, "watchers", {});
    }

    // 初始化针对此属性的监听者数组
    if (!obj.watchers[prop]) {
        obj.watchers[prop] = [];
    }

    // 添加监听者
    obj.watchers[prop].push(watcher);

    var getter = function () {
        return val;
    };

    var setter = function (newval) {
        var oldval = val;
        val = newval;

        // 监听对象上所有属性
        if(obj[prop]){
            watchAll(obj[prop], watcher);
        }

        // 监听数组的操作函数
        watchFunctions(obj, prop);

        // 将对象转换成字符串之后判断是否有改变
        if (JSON.stringify(oldval) !== JSON.stringify(newval)) {
            if (!WatchJS.noMore){
                callWatchers(obj, prop, newval, oldval);
                WatchJS.noMore = false;
            }
        }
    };

    // 定义accessor
    defineGetAndSet(obj, prop, getter, setter);

};

defineWatcher函数是定义监听者的主要函数,它做了如下几件事情:

  1. 监听数组的操作函数,用于数组变化的监听
  2. 初始化watchers对象,用于存储监听函数
  3. 添加监听函数
  4. 设置accessor
    1. setter会监听属性值所有的属性变化
    2. 监听数组的操作函数
    3. 将对象转换成字符串之后判断是否有改变
    4. 如果改变则调用监听函数

降级处理

defineGetAndSet是利用了defineProperty来设置监听函数,如果不支持则会尝试如下方法:

Object.prototype.__defineGetter__.call(obj, propName, getter);
Object.prototype.__defineSetter__.call(obj, propName, setter);

有些稍微旧版本的先进浏览器可以使用这个方法来设置accessor。如果浏览器还是不支持那么会fallback到设置定时器监控。如下:

var subjects = [];

defineWatcher = function(obj, prop, watcher){
    // 存储当前状态和监听者
    subjects.push({
        obj: obj,
        prop: prop,
        serialized: JSON.stringify(obj[prop]), // 设置当前属性值的序列化值
        watcher: watcher
    });
};

unwatchOne = function (obj, prop, watcher) {
    for (var i in subjects) {
        var subj = subjects[i];
        if (subj.obj == obj &amp;&amp; subj.prop == prop &amp;&amp; subj.watcher == watcher) {
            subjects.splice(i, 1);
        }
    }
};

callWatchers = function (obj, prop) {
    for (var i in subjects) {
        var subj = subjects[i];

        if (subj.obj == obj &amp;&amp; subj.prop == prop) {
            subj.watcher.call(obj, prop);
        }
    }
};

var loop = function(){
    for(var i in subjects){
        var subj = subjects[i];
        // 新的获取序列化值
        var newSer = JSON.stringify(subj.obj[subj.prop]);
        // 与旧的序列化值判断
        if(newSer != subj.serialized){
            // 不同则调用监听函数
            subj.watcher.call(subj.obj, subj.prop, subj.obj[subj.prop], JSON.parse(subj.serialized));
            // 设置新的值
            subj.serialized = newSer;
        }
    }
};
setInterval(loop, 50);

这个降级处理是存在bug的,它无法正确处理属性设置之间的间隔。原因在于javascript的线程模型,以及setInterval不是立即能知道更新,有50ms的间隔时间。样例如下:

var obj = {
   a: 1
};

function cb() {
    console.log(this.a);
}

watch(obj, 'a', cb);
console.log('set first');
obj.a = 2;
console.log('set second');
obj.a = 3;

上面的代码在支持defineProperty的浏览器中执行结果如下:

set first
2
set second
3

在不支持的浏览器中执行结果如下:

set first
set second
3

可见setInterval的降级处理是异步的,而defineProperty是同步的。这在一些情况下会导致严重的bug,而且难以调试。所以在真正使用的环境中需要注意!

总结

抛开watch.js的bug不谈,它还是有很多可圈可点的地方。使用下来觉得watch.js的优点如下:

  1. API设计的很好,实用
  2. 不需要特殊的修改定义属性的代码
  3. 支持commonjs的模块标准
  4. 属性为object和array的时候也可以监听

缺点如下:

  1. 监听整个对象的时候,只能监听到已有属性。监听不到新属性
  2. 对于不支持Object.defineProperty的浏览器,降级方案有小bug

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • CSS硬件加速的好与坏

    每个人都痴迷于60桢每秒的顺滑动画。为了实现这个顺滑体验现在用的最流行的一个做法就是使用『CSS硬件加速』。在一些极端例子中,强制使用translate3d意味...

    mmzhou
  • CSS3着重符及其fallback

    在中文里面,我们一般会在文字下方加上圆形符号。在日语中会在文字上方加上小顿号。在CSS3中如下属性可以控制着重符号:

    mmzhou
  • 原型链上的DOM Attributes

    Chrome开发小组最近发表声明他们正在將DOM properties移动到原型链中。这个更新将会在Chrome 43(2015年4月发布beta版本)中实现。...

    mmzhou
  • 深入了解VSTS的Unit Test测试属性

    深入的了解一下方法上带有的属性的含义. 每个方法上几乎都带有TestMethod这个属性,我们直觉告诉我们,这肯定是表示被测试函数的意思.事实也正是如此,在Un...

    跟着阿笨一起玩NET
  • SceneKit-你其实不懂模型的物理身体

    1.如何查看几何模型物理身体 2.如何设置几何模型的物体身体形态 3.如何给几何模型自定义物体身体

    酷走天涯
  • 为什么子类引用不能指向父类对象

    在java、C++等面向对象的语言中,实现多态的方式就是使用父类引用指向子类对象,所以父类引用指向子类对象是没有任何为题的,但是,大家有没有想过,子类引用可以指...

    xujjj
  • 动手写个 JSON-Model Mapping 库

    Swift 在 JSON解析方面有个比较有名的第三方库——SwiftyJSON,之前我也一直用的它。虽然用着还不错,但是它主要是为了避免手动解析 JSON 数据...

    Sheepy
  • [日常] Go语言圣经--示例: 并发的Clock服务习题

    练习 8.1: 修改clock2来支持传入参数作为端口号,然后写一个clockwall的程序,这个程序可以同时与多个clock服务器通信,从多服务器中读取时间,...

    陶士涵
  • 你还缺乳腺癌表达量数据集吗

    最近有粉丝求助说他研究乳腺癌做了单细胞转录组数据,定位到了一个稀有细胞亚群,先看它感兴趣的亚群细胞特异性基因的临床意义,问我有没有除了TCGA数据库之外的其它数...

    生信技能树
  • 依赖倒转原则(笔记整理)

    已经是2个月没有写过博客了吧,打开自己的博客,突然有种亲切感。给老板干活的日子很苦,能够有点属于自己的时间真是一种享受。

    卡尔曼和玻尔兹曼谁曼

扫码关注云+社区

领取腾讯云代金券