Skip to content

六、依赖收集

  • 每个属性都要有一个dep,用来收集watcher
  • 每个dep中存放着多个watcher
  • 同一个watcher会被多个dep所记录
  • dep与watcher是多对多的关系

1、定义Dep类

js
// src\observer\dep.js

// 唯一标识
let id = 0;

class Dep {
    constructor() {
        this.subs = []; // 用于存储watcher
        this.id = id++; // 标识dep的唯一性,用于防止重复取值添加dep
    }
    depend() {
        // watcher也可以存放dep,实现双向记忆,让watcher记住dep的同时,让dep也记住watcher
        Dep.target.addDep(this);
    }
    addSub(watcher) {
        this.subs.push(watcher);
    }
    notify() {
        this.subs.forEach(watcher => watcher.update());
    }
}

// 静态属性
Dep.target = null;
export function pushTarget(watcher) {
    // 保留watcher
    Dep.target = watcher;
}

export function popTarget() {
    // 将变量删除掉
    Dep.target = null;
}

export default Dep;

2、定义Watcher类

js
// src\observer\watcher.js

import { pushTarget, popTarget } from "./dep";

let id = 0;
class Watcher {
    constructor(vm, exprOrFn, cb, options) {
        this.vm = vm;
        this.exprOrFn = exprOrFn; // 渲染函数
        this.cb = cb; // 回调函数
        this.options = options;
        this.id = id++; //watcher的唯一标识
        this.deps = []; // watcher记录有多少dep依赖
        this.depsId = new Set(); // 用于去重dep

        if (typeof exprOrFn == 'function') {
            this.getter = exprOrFn;
        }

        // 默认调用get方法
        this.get();
    }

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

	// 利用JS的单线程机制,先存储watcher,然后更新,最后删除watcher
    get() {
        // 存储当前watcher实例
        pushTarget(this);
        // 调用exprOrFn,渲染页面 > 取值(执行了get方法)
        this.getter();
        // 渲染完成后,将watcher删掉
        popTarget();
    }

    run() {
        // 渲染逻辑
        this.get();
    }
    update() {
        // 重新渲染
        this.get();
    }
}

export default Watcher;

3、对象依赖收集

js
// src\observer\index.js

// 每个属性都有一个dep
let dep = new Dep();
// 当页面取值时,说明这个值用来渲染了,将这个watcher和这个属性对应起来

Object.defineProperty(data, key, {
	get() {
		// 如果取值时有watcher
		if (Dep.target) {
			// 让watcher保存dep,并且让dep 保存watcher
			dep.depend();
		}
		return value
	},
	set(newValue) {
		if (newValue == value) return;
		observe(newValue);
		value = newValue;

		// 通知渲染watcher去依赖更新
		dep.notify();
	}
})

4、数组依赖收集

  1. 获取arr的值,会调用get方法,就让当前数组记住渲染watcher
  2. 给所有的对象类型都增加一个dep属性
  3. 当页面对arr取值时,让这个数组的dep记住这个watcher
  4. 当操作push, shift等更新数组的方法时,就找到数组对应的watcher来更新
js
// src\observer\index.js

// 1、构造函数中添加一个dep
class Observer {
    constructor(value) {
        this.dep = new Dep(); // value = {} / []
		
		......
    }
}

// 2、定义响应式中进行添加依赖
function defineReactive(data, key, value) {
    // 获取到数组对应的dep
    let childDep = observe(value);

    // 每个属性都有一个dep
    let dep = new Dep();
    // 当页面取值时,说明这个值用来渲染了,将这个watcher和这个属性对应起来

    Object.defineProperty(data, key, {
        get() {
           // 如果取值时有watcher
			if (Dep.target) {
				// 让watcher保存dep,并且让dep 保存watcher
				dep.depend();

                // 可能是数组,可能是对象
                if (childDep) {
                    // 默认给数组增加了一个dep属性,当对数组这个对象取值的时候
                    childDep.dep.depend(); // 数组存起来了渲染watcher
                }
            }
            return value
        },
        set(newValue) {
            if (newValue == value) return;
            observe(newValue);
            value = newValue;

            // 通知渲染watcher去依赖更新
            dep.notify();
        }
    })
}

// src\observer\array.js
// 3、重写方法中进行调用更新
arrayMethods[method] = function (...args) {
    	// ...
        ob.dep.notify()
        return result;
}

5、调用渲染函数进行依赖更新

渲染流程

  1. 先把这个渲染watcher放到Dep.target属性上
  2. 开始渲染,取值会调用get方法,需要让这个属性的dep 存储当前的watcher
  3. 页面上所属需要的属性都会将这个watcher存在自己的dep中
  4. 等会属性更新了,就重新调用渲染逻辑,通知自己存储的watcher来更新
js
// src\lifecycle.js

export function mountComponent(vm, el) {
    vm.$el = el;
    callHook(vm, 'beforeMount');

	// 定义更新函数
    let updateComponent = () => {
        vm._update(vm._render());
    };
    // 初始化就会创建watcher
    new Watcher(vm, updateComponent, () => {
        callHook(vm, 'updated');
    }, true);

    callHook(vm, 'mounted');
}

Released under the MIT License.