菜鸡手写vue(四)-依赖收集

110 阅读2分钟

1、挂载元素

初次渲染页面的时候会实例化的一个watcher,在这个watcher里面通过vm._render()函数和vm._update()函数实现页面渲染。 _render()函数生成虚拟dom,update()函数再将虚拟dom生成真实dom。

export function mountComponent(vm, el){
    // 实现页面的挂载流程
    vm.$el = el;
    const updateComponent = () => {
        // 调用render函数,获取虚拟节点,生成真实dom
        vm._update(vm._render())
    };
    new Watcher(vm, updateComponent, () => {}, true);
}

2、保存当前watcher

watcher会在构造函数里面调用get(),然后再调用外部传进来的updateComponent方法,在调用updateComponent之前先记录下当前watcher。

class Watcher{
    constructor(vm, exprOrfn, cb, options){
        this.vm = vm;
        this.getter = exprOrfn;
        this.cb = cb;
        this.options = options;
        this.id = id++;
        this.deps = [];             // 记录dep
        this.depsId = new Set();

        this.get();
    }

    get(){
        // 在对属性取值之前先把watcher记录一下
        pushTarget(this);
        // 这个方法中会对属性进行取值操作
        this.getter();
        popTarget();
    }

    addDep(dep){
        let id = dep.id;
        if(!this.depsId.has(id)){
            this.depsId.add(id);
            this.deps.push(dep);
            dep.addSub(this);
        }
        this.deps.push(dep);
    }

    update(){
        this.get();
    }
}

直接将当前watcher保存在了Dep.target中。

export function pushTarget(watcher){
    Dep.target = watcher
}

3、给属性添加dep

watcher中调用this.getter()实际上就是调用了vm._update(vm._render()),在render()函数时会访问属性(例如_s(name)访问name属性),所以可以在数据劫持中做一下处理。在遍历属性添加数据劫持时,给每一个属性加了一个dep,当存在Dep.target时就会进行依赖收集。

walk(data){
    // 循环遍历data的key值进行观测
    Object.keys(data).forEach(key => {
        defineReactive(data, key, data[key]);
    })
}

function defineReactive(obj, key, value){
    observe(value);   
    let dep = new Dep()     // 每次都给属性创建一个dep
    Object.defineProperty(obj, key, {
        get(){
            if(Dep.target){
                dep.depend();   // 只有存在watcher才进行依赖收集,避免外部其他地方访问属性也进行了依赖收集。让这个属性的dep记住watcher,也要让watcher记住dep
            }
            return value;   
        },
        set(newValue){
            if(value === newValue) return;
            observe(newValue);      
            value = newValue;

            dep.notify();           // 当值发生变化时,让dep通知watcher去执行
        }
    })
}

4、watcher和dep互相记录

调用dep.denpend()让watcher记录dep

class Dep{
    constructor(){
        this.id = id++;
        this.subs = [];     // 让属性记住watcher
    }
    depend(){
        // 让watcher记住dep
        Dep.target.addDep(this);
    }
    addSub(watcher){
        this.subs.push(watcher);
    }

    notify(){
        // 通知watcher去更新
        this.subs.forEach(sub => sub.update())
    }
}


// watcher中的addDep()会有一个去重的过程,避免记录重复dep,记录完dep后也要让dep记录watcher,这样就可以保证watcher和dep互相都有记录
addDep(dep){
    let id = dep.id;
    if(!this.depsId.has(id)){
        this.depsId.add(id);
        this.deps.push(dep);
        dep.addSub(this);
    }
    this.deps.push(dep);
}

5、更新页面

当数据发生变化时,就会调用dep.notify()通知watcher更新渲染

notify(){
    // 通知watcher去更新
    this.subs.forEach(sub => sub.update())
}

image.png

疑问:这样做的优势在哪?通知watcher更新的时候不也还是调用_update(_render())方法吗?不也还是重新生成虚拟dom再生成真实dom吗?