如何模拟vue实现一个watcher

首先需要了解一下Object.defineProperty,看一个示例。

var obj = {};
Object.defineProperty(obj, 'name', {
    value: 'zhangsan'
});

上述例子,给对象obj添加一个name属性,值为zhangsan

defineProperty的第三个参数中,可以添加多个属性描述:

  • value:属性的值(不用多说了)

  • writable:如果为false,属性的值就不能被重写,只能为只读了

  • configurable:总开关,一旦为false,就不能再设置他的(valuewritableconfigurable

  • enumerable:是否能在for...in循环中遍历出来或在Object.keys中列举出来。

  • get:获取值时执行的函数

  • set:赋值是执行的函数

需要注意的是,第三个参数的对象中,value属性不能和get/set同时存在。实现属性值变动的监测,就是从getset这两个属性上着手,比如如下的例子,设置值和获取值时都会打印一段文本内容。

var obj = {};
Object.defineProperty(obj, 'name', {
    get: function() {
        return '获取值:zhangsan';
    },
    set: function (newVal) {
        console.log('设置值:' + newVal);
        return newVal;
    }
});
obj.name = 'lisi';
console.log(obj.name);

//输出:
//设置值:lisi
//获取值:zhangsan

Object.defineProperty在MDN上的介绍(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)

最终希望实现的效果

var v = new Vue({
    data: {
        //properties...
    }
})
v.$watch('someProp',function() {
    //watch callback
})

$watch的属性值变化时,让其触发watch中的回调。

那么实现思路就有了,在属性值set的时候,让其发送数据发生变化的通知给Watcher,然后Watcher触发$watch中设置的回调,当属性值get的时候,将属性值再返回回来。

  1. 实现一个Observer,让其为对象设置的每一个属性都添加上getset方法,以便于我们获得控制权。
  2. 实现一个Watcher,当数据发生变化时,立即通知Watcher,让Watcher进行相应的操作。

  3. 使用Dep来进行通知处理,它连接WatcherObserver,当Observer监测到数据变化,使用Dep来通知Watcher

首先实现一个Observer,递归遍历对象,为属性添加set/get

export default class Observer {
    constructor(value) {
        this.value = value;
        this.walk(value);
    }

    walk(value) {
        Object.keys(value).forEach(key => {
            this.convert(key, value[key]);
        })
    }

    convert(key, val) {
        defineReactive(this.value, key, val);
    }
}

defineReactive中为属性添加set/get,当设置值时使用Depnotify方法来通知Watcher,当获取值时,判断来源,如果属于watcher监听范围内的属性,将其Dep.target(即当前属性的Watcher对象)添加到Dep实例的订阅数组中,这个数组中维护着当前watch的Watcher对象集合。

export function defineReactive(obj, key, val) {
    let childObj = observe(val);
    const dep = new Dep();
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            if (Dep.target) {
                dep.addSub(Dep.target);
            }
            return val;
        },
        set(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            childObj = observe(newVal);
            dep.notify();
        }
    })
}

export function observe(value) {
    if (!value || typeof value !== 'object') {
        return;
    }
    return (new Observer(value));
}

然后是Dep,可以把它看做是服务于Observer的订阅系统。Watcher订阅某个ObserverDep,当Observer观察的数据发生变化时,通过Dep通知各个已经订阅的Watcher

Dep提供了几个接口:

  • addSub: 接收的参数为Watcher实例,并把Watcher实例存入记录依赖的数组中
  • removeSub: 与addSub对应,作用是将Watcher实例从记录依赖的数组中移除

  • depend: Dep.target上存放这当前需要操作的Watcher实例,调用depend会调用该Watcher实例的addDep方法,addDep的功能可以看下面对Watcher的介绍

  • notify: 通知依赖数组中所有的watcher进行更新操作

export default class Dep {
    constructor() {
        this.subs = [];
    }

    addSub(sub) {
        this.subs.push(sub);
    }

    removeSub(sub) {
        remove(this.subs, sub);
    }

    depend() {
        if(Dep.target) {
            Dep.target.addDep(this);
        }
    }

    notify() {
        this.subs.forEach(sub => sub.update());
    }
}

Dep.target = null;

Watcher中,当获取属性值时,首先将Dep.target设置为当前watcher对象,那么可以在属性get的时候,可以判断这个属性值的获取来源是哪里,如果来自于Watcher,则会带有当前的watcher对象。

export default class Watcher {
    constructor(vm, expOrFn, cb) {
        this.expOrFn = expOrFn;
        this.cb = cb;
        this.vm = vm;
        this.value = this.get();
    }

    get() {
        Dep.target = this;
        const value = this.vm.$data[this.expOrFn];
        Dep.target = null;
        return value;
    }

    update() {
        this.run();
    }

    addDep(dep) {
        dep.addSub(this);
    }

    run() {
        const value = this.get();
        if (value !== this.value) {
            this.value = value;
            this.cb.call(this.vm);
        }
    }
}

最后是vue的实现,将属性值也遍历添加到根节点上,通过vue当前实例.属性这种形式也可以获取到属性值。

export default class Vue {
    constructor(options = {}) {
        this.$options = options;
        let data = this.$data = this.$options.data;
        Object.keys(data).forEach(key => this._proxy(key));
        observe(data);
    }

    $watch(expOrFn, cb) {
        new Watcher(this, expOrFn, cb);
    }

    _proxy(key) {
        const self = this;
        Object.defineProperty(self, key, {
            enumerable: true,
            configurable: true,
            get: function proxyGetter() {
                return self.$data[key];
            },
            set: function proxySetter(newVal) {
                self.$data[key] = newVal;
            }
        })
    }
}

最后测试代码

const vm = new Vue({
    data: {
        a: 1,
        b: {
            c: 2
        }
    }
});

vm.$watch('a', () => console.log(`a当前的值为:${vm.a}`));

setTimeout(() => {
    vm.a = 3;
}, 2000);


setTimeout(() => {
    vm.a = 15;
}, 5000);

在控制台运行可以看到当a的值变化时,能够执行$watch的回调。

完整实现代码:https://github.com/DulJuly/simpleVue

本文参考自:https://segmentfault.com/a/1190000004384515

如果您觉得本文对您有用,欢迎捐赠或留言~
微信支付
支付宝

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注