Vue源码解析之 依赖收集

295 阅读4分钟

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue 源码解析系列第 8 篇,关注专栏

前言

响应式对象 文章中我们分析了解到 Vue 把普通对象变成响应式对象,响应式对象 getter 部分逻辑就是做依赖收集。我们主要关注两点,一是 const dep = new Dep() 实例化一个 Dep 实例,二是在 get 函数中通过 dep.depend 依赖收集。

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    // ...
  })
}

Dep

Dep 作用主要是对 Watcher 的管理,它是整个 getter 依赖收集的核心,它定义在src/core/observer/dep.js

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

let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
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()
}

Watcher

Watcher 是一个 Class,它定义在 src/core/observer/watcher.js ,我们主要关注依赖收集相关 depsnewDepsdepIdsnewDepIds 属性和 getaddDepcleanupDeps 方法

let uid = 0

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  computed: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  dep: Dep;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = this.get() // 执行 get 方法
    }
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
  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)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  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
  }
  // ...
}

依赖收集过程

之前我们介绍过 Vue 的 mount 过程会执行 mountComponent 函数,里面有这么一段逻辑:

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

当我们实例化一个 Watcher 时候,会执行 this.get() 方法:

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
}

get 方法逻辑首先会执行 pushTarget(this)pushTarget 方法定义在 src/core/observer/dep.js ,该方法主要是把 Dep.target 赋值为当前的渲染 watcher 并压栈。

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

接着会执行:

value = this.getter.call(vm, vm)

this.getter 就是实例化 Watcher 时传入的 updateComponent 函数

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

它会先执行 vm.render() 方法,该方法主要生成 渲染 VNode,并且该过程会进行数据访问,此时会触发数据对象的 getter

每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法:

// 定义在 src/core/observer/index.js
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    // ...
  })
}

// 定义在 src/core/observer/dep.js
depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
}

depend 方法会执行 Dep.target.addDep(this) ,这里的 Dep.target 在执行 pushTarget(this) 时已经被赋值为渲染 watcher , 之后会执行 addDep 方法:

// 定义在 src/core/observer/watcher.js
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)
    }
  }
}

addSub 方法会执行 this.subs.push(sub) 逻辑,就是把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。

所以 vm.render() 过程,会触发所有数据的 getter ,实际上已经完成一个依赖收集的过程。依赖收集完成后,还会执行如下逻辑:

// get 方法中定义
if (this.deep) {
  // 递归访问value, 触发所有子项的 getter
  traverse(value)
}
popTarget()
this.cleanupDeps()

// popTarget 定义在 src/core/observer/dep.js
export function popTarget () {
  /**
   * 把 Dep.target 恢复成上一个状态,因为当前 vm 的数据依赖收集已经完成
   * 那么对应的渲染 Dep.target 也需要改变
   */
  Dep.target = targetStack.pop()
}

// cleanupDeps 定义在 src/core/observer/watcher.js
// 依赖清空
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 的收集
  • 依赖收集的目的是为了当这些响应式数据发生变化,触发它们的 setter 时,能知道应该通知哪些订阅者去做相应的逻辑处理

参考

Vue.js 技术揭秘

Vue 源码解析系列

  1. Vue源码解析之 源码调试
  2. Vue源码解析之 编译
  3. Vue源码解析之 数据驱动
  4. Vue源码解析之 组件化
  5. Vue源码解析之 合并配置
  6. Vue源码解析之 生命周期
  7. Vue源码解析之 响应式对象
  8. Vue源码解析之 依赖收集
  9. Vue源码解析之 派发更新
  10. Vue源码解析之 nextTick