vue源码学习记录--数据响应观察者模式的实现之Dep、Watcher、Observer之间的关系

400 阅读2分钟

Dep、Watcher、Observer主要解决两个问题

  1. 如何实现依赖的收集?
  2. 依赖发生变化时如何通知页面更新?

三者之间的关系:

data中的每一个属性对应一个dep,dep中的subs保存了依赖该属性的watcher,每一个watcher有一个update方法,该方法会在依赖的属性的值发生变化的时候调用。
一个对象或数组对应一个__ob__属性即observer实例。由observer(data)开始,在observer中执行new Observer(value)操作,new Observer中遍历value对象,为每一个值defineReactive,在defineReactive中默认会对观测的data对象进行深度观测,即会递归观测属性值,如果属性值时对象或数组的话。此时的watcher只有一个,即为渲染watcher,所有的dep都会添加到该watcher的deps中。

综上,三者之间的关系为一个渲染watcher对应一个组件,该watcher中的deps保存了data中所有属性值的dep,wacther中保存deps的目的是为了依赖取消时移除dep实例中subs记录的watcher,每个dep实例有一个subs属性,该属性用于保存依赖产生dep实例的data属性的watcher,dep由observer产生,一个对象或数组对应一个observer实例,每一个属性对应一个dep。

随便写点代码来帮助理解:

   /** data */
    const data = {
        name: 'nick',
        age: 25,
        job: 'Front-end Developer'
    };

    /** utils */
    function defineReactive(obj, key, val) {
        const dep = new Dep();

        Object.defineProperty(obj, key, {
            enumerable: false,
            configurable: true,
            get: function reactiveGetter() {
                console.log(`${key}使用到了,需要收集起来`);
                if (Dep.target) {
                    dep.depend();
                }
                return val;
            },
            set: function reactiveSetter(newVal) {
                if (newVal === val) {
                    return;
                }
                console.log(`${key}被设置了,需要通知依赖它的观察者更新哦`);
                val = newVal;
                dep.notify();
            }
        })
    }

    function def(obj, key, val, enumerable) {
        Object.defineProperty(obj, key, {
            value: val,
            enumerable: !!enumerable,
            configurable: true,
            writable: true
        })
    }

    function isObject(value) {
        return Object.prototype.toString.call(value) === '[object Object]';
    }

    function createElement(tag) {
        return document.createElement(tag);
    }


    /** observer */
    class Observer{
        constructor(value) {
            def(value, '__ob__', this);
            this.walk(value);
        }

        walk(obj) {
            const keys = Object.keys(obj);
            let l = keys.length, i = 0;
            for (; i < l; i++) {
                defineReactive(obj, keys[i], obj[keys[i]])
            }
        }
    }

    function observer(obj) {
        if (!isObject(obj)) {
            return;
        }
        let ob = obj.__ob__;
        if (ob) {
            console.log('对象已经被观测过了');
            return ob;
        }
        ob = new Observer(obj);
        return ob;
    }

    /** watcher */
    class Watcher {
        constructor(fn) {
            this.deps = [];
            this.getter = fn;
            this.get();
        }
        get() {
            pushStack(this);
            this.getter(data);
            popStack();
        }
        addDep(dep) {
            this.deps.push(dep);
            dep.addSub(this);
        }
        update() {
            this.getter(data);
        }
    }

    /** dep */
    class Dep {
        static target;
        constructor() {
            this.subs = [];
        }
        addSub(sub) {
            this.subs.push(sub);
        }
        notify() {
            for (let i = 0; i < this.subs.length; i++) {
                this.subs[i].update();
            }
        }
        depend() {
            if (Dep.target) {
                Dep.target.addDep(this);
            }
        }
    }
    Dep.target = null;

    const targetStack = [];
    function pushStack(target) {
        targetStack.push(target);
        Dep.target = target;
    }
    function popStack() {
        targetStack.pop();
        Dep.target = targetStack[targetStack.length - 1];
    }

    /** render */
    observer(data);
    const watcher = new Watcher(_render);
    console.log(watcher);

    function _render(data) {
        const _c = createElement;
        const p1 = _c('p');
        p1.textContent = data.name;
        const p2 = _c('p');
        p2.textContent = String(data.age);
        const div = _c('div');
        div.setAttribute('id', 'app');
        div.appendChild(p1);
        div.appendChild(p2);

        const oldDiv = document.getElementById('app');
        console.log(oldDiv);

        if (oldDiv) {
            document.body.replaceChild(div, oldDiv);
        }
        else {
            document.body.appendChild(div);
        }

    }