vue2依赖收集原理也太绕绕绕了

2,985 阅读5分钟

相信看完这篇文章的你,跟我的想法是一样的。

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

为了实现数据变化影响视图,vue采用了观察者模式,将数据和页面渲染关联起来。通过dep收集依赖,当数据变化时,通知对应watcher更新视图。

具体是怎么实现的呢?

1.创建渲染 watcher 并初始化

我们将更新视图的功能封装了一个watcher

export function lifecycleMixin() {
    Vue.prototype._update = function (vnode) {}
}
export function mountComponent(vm, el) {
    vm.$el = el;
    let updateComponent = () => {
        // 将虚拟节点 渲染到页面上
        vm._update(vm._render());
    }
    new Watcher(vm, updateComponent, () => {}, true);
}

class Watcher {
    // vm,updateComponent,()=>{ console.log('更新视图了')},true
    constructor(vm,exprOrFn,cb,options){
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        this.cb = cb;
        this.options = options;
        this.id = id++;

        // 默认应该让exprOrFn执行  exprOrFn 方法做了什么是? render (去vm上了取值)

        this.getter = exprOrFn; 
        this.deps = []; 
        this.depsId = new Set();

        this.get(); // 默认初始化 要取值
    }
    get(){ // 稍后用户更新 时 可以重新调用getter方法
        // defineProperty.get, 每个属性都可以收集自己的watcher
        // 我希望一个属性可以对应多个watcher,同时一个watcher可以对应多个属性
        pushTarget(this); // Dep.target = watcher
        this.getter(); // render() 方法会去vm上取值 vm._update(vm._render)
        popTarget(); // Dep.target = null; 如果Dep.target有值说明这个变量在模板中使用了
    }
    update(){ // vue中的更新操作是异步的
       // 每次更新时 this
       queueWatcher(this); // 多次调用update 我希望先将watcher缓存下来,等一会一起更新
    }
    run(){ // 后续要有其他功能
        this.get();
    }
    addDep(dep){
        let id = dep.id;
        if(!this.depsId.has(id)){
            this.depsId.add(id);
            this.deps.push(dep);
            dep.addSub(this)
        }
    }
}

2.在渲染时存储 watcher

调用 dep 的 pushTarget 方法存储 watcher

class Watcher{
    // ...
    get(){
        pushTarget(this);//调用dep的pushTarget方法 Dep.target = watcher储存当前的watcher
        this.getter();
        popTarget();
    }
}
let id = 0;
class Dep{
    constructor(){
        this.id = id++;
    }
}
let stack = [];
export function pushTarget(watcher){
    Dep.target = watcher;
    stack.push(watcher);
}
export function popTarget(){
    stack.pop();
    Dep.target = stack[stack.length-1];
}
export default Dep;

3.对象的依赖收集

  1. 在vue中页面渲染时使用的属性,需要进行依赖收集 ,收集对象的渲染watcher
  2. 取值时,给每个属性都加了个dep属性,用于存储这个渲染watcher (同一个watcher会对应多个dep)。
    • watcherget 方法调用 render, render 方法去 vm 上取值,取值就调用了 definePropertyget 方法。
  3. dep.depend() => 通知dep存放watcher => Dep.target.addDep() => 通知watcher存放dep 实现双向存储。
let dep = new Dep();
Object.defineProperty(data, key, {
    get() {
        if(Dep.target){ // 如果取值时有watcher
            dep.depend(); // 让watcher保存dep,并且让dep 保存watcher
        }
        return value
    },
    set(newValue) {
        if (newValue == value) return;
        observe(newValue);
        value = newValue;
        dep.notify(); // 通知渲染watcher去更新
    }
});

Dep 实现

每个 dep 有一个 id 唯一标示,向watcher中添加dep可以通过id去重。 用 dep 做关系的收集,每个属性都分配一个 depdep 可以来存放 watcher

let id = 0;
class Dep{ // 每个属性我都给他分配一个dep,dep可以来存放watcher, watcher中还要存放这个dep
    constructor(){
        this.id = id++;
        this.subs = []; // 用来存放watcher的
    }
    depend(){
        // Dep.target  dep里要存放这个watcher,watcher要存放dep  多对多的关系
        if(Dep.target){
            Dep.target.addDep(this);
        }
    }
    addSub(watcher){
        this.subs.push(watcher);
    }
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
}
Dep.target = null; // 一份

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

export function popTarget(){
    Dep.target = null
}


export default Dep

watcher实现

watcher 中存放 dep(一个属性一个 dep,多个属性多个 dep

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

let id = 0;
class Watcher {
    // vm,updateComponent,()=>{ console.log('更新视图了')},true
    constructor(vm,exprOrFn,cb,options){
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        this.cb = cb;
        this.options = options;
        this.id = id++;

        // 默认应该让exprOrFn执行  exprOrFn 方法做了什么是? render (去vm上了取值)

        this.getter = exprOrFn; 
        this.deps = []; 
        this.depsId = new Set();

        this.get(); // 默认初始化 要取值
    }
    get(){ // 稍后用户更新 时 可以重新调用getter方法
        // defineProperty.get, 每个属性都可以收集自己的watcher
        // 我希望一个属性可以对应多个watcher,同时一个watcher可以对应多个属性
        pushTarget(this); // Dep.target = watcher
        this.getter(); // render() 方法会去vm上取值 vm._update(vm._render)
        popTarget(); // Dep.target = null; 如果Dep.target有值说明这个变量在模板中使用了 比如说 我在 vue 的外面可以, vm.xxx 这个值 不需要收集 watcher
    }
    update(){ // vue中的更新操作是异步的
       // 每次更新时 this
       queueWatcher(this); // 多次调用update 我希望先将watcher缓存下来,等一会一起更新
    }
    run(){ // 后续要有其他功能
        this.get();
    }
    addDep(dep){
        let id = dep.id;
        //watcher在添加dep的时候做了去重
        if(!this.depsId.has(id)){
            this.depsId.add(id);
            this.deps.push(dep);
            dep.addSub(this)
        }
    }
}

4.数组的依赖收集

  1. 给数组的__ob__添加一个dep,用来存放watcher
  2. 当调用数组变异方法时,调用dep.notify()来通知watcher更新
  3. 如果是数组嵌套数组,通过dependArray()递归收集watcher
this.dep = new Dep(); // 专门为数组设计的
if (Array.isArray(value)) {
	value.__proto__ = arrayMethods;
	this.observeArray(value);
} else {
	this.walk(value);
}	

function defineReactive(data, key, value) {
    let childOb = observe(value);
    let dep = new Dep();
    Object.defineProperty(data, key, {
        get() {
            if(Dep.target){
                dep.depend();
                if(childOb){ 
                    childOb.dep.depend(); // 收集数组依赖
                }
            }
            return value
        },
        set(newValue) {
            if (newValue == value) return;
            observe(newValue);
            value = newValue;
            dep.notify();
        }
    })
}


arrayMethods[method] = function (...args) {
    	// ...
        ob.dep.notify()
        return result;
}

递归收集数组依赖

if(Dep.target){
    dep.depend();
    if(childOb){
        childOb.dep.depend(); // 收集数组依赖
        if(Array.isArray(value)){ // 如果内部还是数组
            dependArray(value);// 不停的进行依赖收集
        }
    }
}
function dependArray(value) {
    for (let i = 0; i < value.length; i++) {
        let current = value[i];
        current.__ob__ && current.__ob__.dep.depend();
        if (Array.isArray(current)) {
            dependArray(current)
        }
    }
}

Vue异步更新之nextTick

每改一次数据就更新一下视图,这样操作显然是不合理的。所以vue的视图更新是异步的,多次调用update,先将watcher缓存在queue中,根据id去重,再一起更新。

1.实现队列机制

update(){
    queueWatcher(this);
}
import { nextTick } from "../utils";

let queue = [];
let has = {}; // 做列表的 列表维护存放了哪些watcher
function flushSchedulerQueue(){
    for(let i =0 ; i < queue.length; i++){
        queue[i].run(); // vm.name = 123?
    }
    queue = [];
    has = {};
    pending = false;
}

let pending = false;

// 要等待同步代码执行完毕后 才执行异步逻辑
export function queueWatcher(watcher) {
// 当前执行栈中代码执行完毕后,会先清空微任务,在清空宏任务, 我希望尽早更新页面
    const id = watcher.id; 
    if (has[id] == null) {
        queue.push(watcher);
        has[id] = true;
        // 开启一次更新操作  批处理 (防抖)
        if(!pending){
            nextTick(flushSchedulerQueue, 0);
            pending = true;
        }
    }
}

2.nextTick实现原理

const callbacks = [];
function flushCallbacks() {
  callbacks.forEach((cb) => cb());
  waiting = false;
}
//nextTick防抖 异步
//vue2考虑了兼容性问题
//1.支持promise,promise.then
//2.mutationObserver 监控文本的变化,文本内容变化执行flushCallbacks
//3.setImmediate
//4.setTimeout
//vue3不再考虑兼容性问题
let timerFn = () => {};
if (Promise) {
  timerFn = () => {
    Promise.resolve().then(flushCallbacks);
  };
} else if (MutationObserver) {
  let textNode = document.createTextNode(1);
  let observe = new MutationObserver(flushCallbacks);
  observe.observe(textNode, {
    characterData: true,
  });
  timerFn = () => {
    textNode.textContent = 3;
  };
} else if (setImmediate) {
  timerFn = () => {
    setImmediate(flushCallbacks);
  };
} else {
  timerFn = () => {
    setTimeout(flushCallbacks);
  };
}

let waiting = false;
export function nextTick(cb) {
  callbacks.push(cb);
  if (!waiting) {
    timerFn(flushCallbacks, 0);
    waiting = false;
  }
}

小结

到这里,我们就实现了依赖收集的原理。

响应式数据时,对于数组和对象的处理分开两种不同的方式。 现在依赖的收集也是。

你觉得绕吗吗吗?