Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

watch.js 源码解读 #3

Open
zmmbreeze opened this issue Aug 31, 2016 · 0 comments
Open

watch.js 源码解读 #3

zmmbreeze opened this issue Aug 31, 2016 · 0 comments

Comments

@zmmbreeze
Copy link
Owner

zmmbreeze commented Aug 31, 2016

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

用麻雀虽小五脏俱全来描述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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant