vue2.x源码解读:依赖收集、派发更新

1,252 阅读6分钟

依赖收集

讲依赖收集之前我们需要先了解三个点:

Observe类:用于将响应式对象的属性转换成可以被检测的属性(为其属性添加getter和setter)

Dep类:用于收集当前响应式对象的依赖

Watcher类:作为一个中介(观察者),当数据发生变化时,通过watcher中转通知组件。watcher实例分为渲染watcher、计算watcher、侦听器watcher

vue2.x,中等粒度依赖,用到数据的组件是依赖

在getter中收集依赖,在setter中出发依赖(diff)

Dep

整个getter依赖收集的核心是Dep,将依赖收集的代码封装成一个Dep类,用它来专门管理依赖,每个Observer的实例成员中都有一个Dep的实例;

import type Watcher from './watcher'
import { remove } from '../util/index'

let uid = 0

//dep是一个可观察对象,可以有多个指令订阅它
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++   //Dep实例的id是为了方便去重
    this.subs = []    //subs是为了存储需要依赖收集的watcher
  }

  addSub (sub: Watcher) {//添加当前的观察者对象
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {//移除当前的观察者对象
    remove(this.subs, sub)
  }

  depend () {//依赖收集
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {...}
    
}

Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

wather

watcher是一个中介,数据发生变化时通过watcher中转,通知组件

比较巧妙的点是:watcher把自己设置到全局的一个指定位置,然后读取了数据,所以会触发这个数据的getter.在getter中就能得到当前正在读取的watcher,并把这个watcher收集到Dep中。

export default class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean      // 是否是渲染watcher
  ) {
    this.getter = expOrFn                // 在get方法中执行
    /*是否是计算属性,是的话并不会立刻求值,而是实例化一个dep*/
    if (this.computed) {           
      this.value = undefined                
      this.dep = new Dep()              
    } else {    
    /* 不是计算属性会立刻求值*/
      this.value = this.get()
    }
  }

  /* 获取getter的值并且重新进行依赖收集 */
  get() {
    // 设置Dep.target = this
    pushTarget(this)                
    let value
    //this.getter对应的就是updateComponent函数
    value = this.getter.call(vm, vm)
    // 将观察者实例从target栈中取出并设置给Dep.target
    popTarget()                      
    this.cleanupDeps()
    return value
  }

  addDep(dep: Dep) { ... }  // 添加一个依赖关系到Deps集合中
  cleanupDeps() { ... }  // 清除newDeps中无用watcher依赖
  update() { ... }  //当依赖发生变化进行回调
  run() { ... }    //在update被调用时会回调
  getAndInvoke(cb: Function) { ... }
  evaluate() { ... }  // 收集该watcher的所有deps依赖
  depend() { ... }  // 收集该watcher的所有deps依赖,只有计算属性使用
  teardown() { ... }  //把自身从所有依赖收集订阅列表删除
}

我们知道当访问响应式数据是会触发它们的getter方法,那这些对象什么时候被访问?

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

首先当我们实例化一个渲染Watcher时,进入Watcher构造函数,执行它的get()方法,进入get方法会执行pushTarget()将当前的渲染watcher赋值给Dep.target,并进行压栈操作。在这里又执行value = this.getter.call(vm, vm)(实际上就是在执行vm._update(vm._render(),hydrating)

这个函数首先执行vm.render生成渲染VNode,从而在这个过程中会完成对当前vm上数据的访问,触发数据对象的getter,而每个对象值的getter都有一个dep ,触发了getter就会触发dep.depend(),也就会执行Dep.target.addDep(this)

将当前的watcher订阅到这个数据的dep的subs中,目的是为后续数据变化时能知道通知那些订阅者

addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

到这里基本上依赖收集的过程就完了。

依赖清空

但是依赖收集完后,如何依赖清空呢?

 cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

在Watcher构造函数中,我们发现它定义了这几个属性depIds和deps、newDepIds和newDeps。

deps和newDeps表示Watcher实例持有的Dep实例数组。deps表示上一次添加的Dep实例数组。newDeps表示新添加的Dep实例数组。

在执行cleanupDeps时,我们发现遍历了debs,移除了newDepIds中不存在的deps的watcher订阅,然后交换deps和newDeps、depIds和newDepIds。并且将newDepIds和newDeps清空。

那为什么做deps订阅的移除呢?

为了避免这种场景:v-if 已不需要的模板依赖的数据发生变化时就不会通知watcher去 update

当我们根据v-if渲染a和b模版,当条件满足我们渲染a,会访问a中的数据,对a的数据添加getter进行依赖收集,修改了a的数据会通知那些订阅者。当时改变了条件渲染b后,我们如果没有进行依赖移除,修改到a的模版数据时,又会触发a数据的订阅的回调,这显然有浪费的。

派发更新

收集的目的就是修改数据后对相关的依赖派发更新。

当数组中的响应数据被修改就会触发setter逻辑,然后调用dep.notify(),然遍历数组调用sub[i].update(即调用每一个watcher的update)

class Dep {
  // ...
  notify () {
  // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
class Watcher {
  /* 当依赖发生变化进行回调 */
  update () {
    /*
    *计算属性监视器有两种模式:惰性模式和激活模式。
    *默认情况下,它初始化为lazy;
    *只有当至少有一个订阅者依赖它时才会被激活
    */
    if (this.computed) { //computed watcher
      if (this.dep.subs.length === 0) {
        this.dirty = true
      } else {
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {  //sync watcher
     // sync为true,就可以在当前Tick中同步执行watcher的回调函数
      this.run()
    } else {          
      /*这里引入了watcher队列,也是派发更新的优化点
       *不是每次数据变化都触发watcher的回调,而是添加到队列,
       *在nextTick中执行flushSchedulerQueue
       */
      queueWatcher(this)
    }
  }
}  

queueWatcher中通过nextTick将flushSchedulerQueue方法,放入全局的callback数组中。

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  //has确保同一个watcher仅添加一次
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // 已经刷新,根据它的id连接观察器如果已经超过了它的id,它将立即运行
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // 刷新队列
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

flushSchedulerQueue中对队列进行排序后,遍历队列执行watcher.run

function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  // 队列排序从小到大
  // 确保以下几点:
  // 1.组件从父组件更新到子组件,watcher也是从到子
  // 2.用户的自定义watcher要优先于渲染watcher执行
  // 3.一个组件在父组件的watcher运行期间被破坏,则它对应的watcher都可以被跳过。
  queue.sort((a, b) => a.id - b.id)

  // 遍历队列,拿到对应的watcher,执行watcher.run()
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    ......
    }
  }
}

watcher.run()实际上实在在行getAndInvoke

class Watcher {
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      this.getAndInvoke(this.cb)
    }
  }

  getAndInvoke (cb: Function) {
    const value = this.get()
    if (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      // 即使值相同,深层watcher和对象/数组上的watcher也应该触发,因为值可能已经发生了变化。
      
      //设置新值
      const oldValue = this.value
      this.value = value
      this.dirty = false
      if (this.user) {
        try {
         /* 回调函数传入了value 和旧值 oldValue
         *这就是为什么我们自定义watcher可以拿到新旧值
         */
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }
}

flushSchedulerQueue:负责刷新wather队列,即执行queue数组中的每个watcher的run方法,从而进入更新阶段。比如执行组件更新函数或者执行用户watcher的回调函数

vue做派发更新的一个优化点:不是每次数据改变都出发watcher回调,而是把watcher添加到一个队列里,然后再nextTick后执行flushSchedulerQueue

到这里大概就讲完了vue2.x依赖收集和派发更新的过程,博主也是在学习中,🈶️不正确的地方请各位友友指正