vue数据响应式原理(二)

434 阅读3分钟

前言

数据被更改之后,vue需要把数据生成虚拟结点,然后渲染到页面上,既分别调用_render和_update方式,但是调用这两个方法的过程(vm._update(vm._render()))不应该由用户去执行,而应该由程序在数据被更改之后自动去调用。这就是依赖收集和更新过程。

封装方法

vue更新策略是以组件为单位的,给每一个组件都增加一个watcher,属性变化后会更新调用这个watcher(渲染watcher)

创建Watcher类,把vm._update(vm._render())封装在Watcher里,只需要通过调用Watcher实例上的get方法就可以重新渲染。

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();
    if (typeof exprOrFn == 'function') {
      this.getter = exprOrFn
    }
    this.get() ;// 默认调用getter方法
  }
  addDep(dep) {
    let id = dep.id
    if(!this.depsId.has(id)){
      this.deps.push(dep)
      this.depsId.add(id)
      dep.addSub(this)
    }
  }
  get() {
    pushTarget(this);
    this.getter() // 调用exprOrFn  渲染页面  取值(执行了get方法)
    popTarget()
  }
  update() {
    this.get();
  }
}
export default Watcher// 默认调用exprOrFn方法

属性与watcher的关系

很显然,一个组件内往往会存在多个属性,也就意味一个watcher与多个属性绑定在一起,这些属性的变化都会引起watcher的渲染;同样地,一个属性也可能会引起多个watcher的变化(如:用户创建了vm.$watcher)。我们创建一个Dep类,一个属性对应一个Dep实例用来收集watcher。因此一个watcher和一个dep存在多对多的关系。

先上代码,然后再分析watcher和dep之间建立关系的过程:

function defineReactive(data,key,value) {
  // 获取到数组对应的dep
  let childDep = observe(value)  // 如果值是对象再进行观测
  let dep = new Dep(); // 每个属性都有一个dep
  Object.defineProperty(data,key,{
    get() { // 依赖收集
      if(Dep.target) { //让这个属性记住这个watcher
        dep.depend();
        if(childDep){
          childDep.dep.depend();
        }
      }
      return value
    },
    set(newValue) { // 依赖更新
      if(newValue == value) return;
      observe(newValue) // 如果用户将值设置为对象仍然需要进行观测
      value = newValue;
      dep.notify();
    }
  })
}
let id=0;
class Dep {
  constructor() {
    this.subs=[]
    this.id= id++
  }
  depend() {
    // watcher和dep双向记忆
    Dep.target.addDep(this)
    // this.subs.push(Dep.target);
  }
  addSub(watcher) {
    this.subs.push(watcher);
  }
  notify() {
    this.subs.forEach(watcher=>watcher.update())
  }
}
Dep.target = null;
export function pushTarget(watcher) {
  Dep.target = watcher // 保留watcher
}
export function popTarget() {
  Dep.target = null;
}
export default Dep;

整个过程如下: 1.在第一次页面渲染的时候,会调用watcher上的get方法,这时就会将当前的渲染watcher挂载到Dep的target静态属性上。 2.然后执行getter方法,渲染页面的时候会取值(执行了get方法),进入到defineReactive中的get方法,给每一个属性创建一个dep。如果当前是渲染过程,就把当前这个数据添加到watcher的deps上(这个watcher上依赖),同时也把这个watcher添加到dep的subs上,实现互相依赖。 3.清除Dep的target属性。并不需要给每个属性加dep,没参与到渲染过程的也就不会出现在页面上。 4.当发生数据变更的时候,执行了defineReactive中的set方法,就会触发dep的notify方法,这个属性上所依赖的watcher就会执行更新操作。

总结

几个重点:

1.在Dep的target上挂载渲染watcher,在渲染完成后,需要清除watcher。

依赖收集只需要收集在页面渲染中用到的属性。而在组件中用到的其他属性,并不需要进行依赖收集。因此在页面渲染完成后,将watcher清除。

2.属性进行依赖收集和依赖更新的过程。

每一个属性都有一个对应dep属性,在dep中使用了发布订阅模式,把当前属性所依赖的watcher收集起来。在属性发生变更时,触发所依赖的watcher上的update方法。