Vue原理:依赖收集

208 阅读3分钟

这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

前文已经将Vue的初次渲染流程完成,浏览器中可以将数据进行展示,在日常开发过程中数据是不断变化的,数据变化之后如何驱动视图将最新数据更新至浏览器呢?

初始化Vue,定义数据

const vm = new Vue({
  el: '#app',
  data() {
    return {
      name: 'nordon'
    }
  }
})

初次渲染流程核心

vm._update(vm._render());

此时修改数据且手动调用初次渲染流程

vm.name = 'wy';
vm._update(vm._render());

页面也是可以将新的数据渲染至浏览器中,这样就会导致一个问题,频繁修改数据就需要自己不断的调用vm._update(vm._render());,这个过程明显是不符合期望的,我们所期望的是当数据变化时,可以自动的触发渲染Watcher,驱动浏览器渲染最新的数据,这样才符合数据驱动视图的理念

每个组件都有自己的渲染Watcher,其作为数据和视图的枢纽存在,当数据变化时需要触发渲染Watcher,进而驱动视图更新,渲染Watcher生成的地方


export function mountComponent(vm, el) {
  // 渲染页面
  // 无论渲染还是更新 都会执行
  let updateComponent = () => {
    // vm._render() 返回的是虚拟DOM
    vm._update(vm._render());
  };

  // 渲染 watcher, 每一个组件都有一个watcher
  // true 表示他是一个渲染watcher
  new Watcher(vm, updateComponent, () => {}, true);
}

在初始化new Watcher时会自动执行updateComponent进行页面渲染或者更新

class Watcher {
  constructor(vm, exprOrFn, callback, options) {
    this.getter = exprOrFn; // 将内部传过来的回调函数 放到 getter 属性上
    this.get(); // 调用get方法, 会让渲染 watcher 执行
  }

  get() {
    this.getter(); // 渲染 watcher 执行
  }
}

可以看到初始化会先将exprOrFn存储,在get函数中执行,现在需要在渲染Watcher执行前将其存储下来,当渲染完成之后再将其移除,这个过程就是依赖收集get函数改造为

class Watcher {
  get() {
    pushTarget(this); // 存储watcher, Dep.target
    this.getter(); // 渲染 watcher 执行
    popTarget(); // 移除 watcher
  }
}

将两个函数实现

// 将watcher 保留起来
let stack = [];

/**
 * 存储 渲染Watcher
 */
export function pushTarget(watcher) {
  Dep.target = watcher;
  stack.push(watcher);
}

/**
 * 移除 渲染Watcher
 */
export function popTarget() {
  stack.pop();
  Dep.target = stack[stack.length - 1];
}

收集依赖使用Dep进行,代码如下

let id = 0;

/** 
 * Watcher 和 Dep 是多对多的关系
*/
export default class Dep {
  constructor() {
    this.id = id++;
    this.subs = [] // name: [watcher, watcher]
  }

  depend() {
    // 让这个 watcher 记住当前的 dep
    // 如果 watcher 没有存过 dep, dep 肯定不能存过watcher
    Dep.target.addDep(this)
  }

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

  addSub(watcher) {
    this.subs.push(watcher)
  }
}

渲染Wtcher渲染页面时,需要进行取值操作,会触发数据劫持的get方法,这个时候需要进行依赖收集

function defineReactive(data, key, value) {
  // 这个dep 是给对象使用的, 数组是不能使用的
  let dep = new Dep();

  Object.defineProperty(data, key, 
    get() {
      /**
       * 每个属性 都对应着 自己的 watcher,
       * 需要给每个属性都增加 watcher
       */
      if (Dep.target) {
        // 有值  代表渲染watcher 已经放上去了
        // 如果当前存在 watcher, 将watcher 和  dep 建立一个双向的关系
        dep.depend(); // 我要将 watcher 存起来
      }

      return value;
    },
  });
}

梳理大致流程:

创建渲染Watcher时会执行Watchget函数,其内部做了三件事情:存储渲染Watcher、执行updateComponent函数和移除渲染Watcher,在进行渲染的过程中存在取值操作,触发Object.defineProperty get函数,此时Dep.target值为当前的渲染Watcher,调用dep.depend()将依赖进行收集

当数据变化时,只需要将之前数据对应的watcher触发,便可以触发渲染更新视图