8000 watch.js 源码解读 · Issue #3 · zmmbreeze/blog · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
watch.js 源码解读 #3
Open
Open
@zmmbreeze

Description

@zmmbreeze

以前的旧文章,转移到这里

用麻雀虽小五脏俱全来描述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

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
  5. setter会监听属性值所有的属性变化
  6. 监听数组的操作函数
  7. 将对象转换成字符串之后判断是否有改变
  8. 如果改变则调用监听函数

降级处理

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0