源码分析:深入Vue响应式原理 依赖收集

220 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Vue 响应式原理 依赖收集

Vue的响应式的核心是利用了Object.defineProperty API(不清楚响应式对象逻辑可以点击这里),这样我们在获取定义的响应式数据的时候,就会触发数据的get方法,改变响应式数据的时候,就会触发数据的set方法。那这两个方法内部又实现了哪些逻辑呢? 本节我们去分析Vue内部,获取响应式数据的值,触发get方法时,Vue的内部做了哪些事情:

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 创建Dep实例
  const dep = new Dep()

  // 获取obj描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 不可配置则直接返回,不做响应式处理
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 存储预先定义的get方法
  const getter = property && property.get
  ......

  // 深层次对象,则递归执行observe
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 计算值
      const value = getter ? getter.call(obj) : val
      // 如果Dep.target存在,进行依赖收集的操作
      if (Dep.target) {
        // 收集依赖
        dep.depend()
        // childOb存在
        if (childOb) {
          // child收集依赖
          childOb.dep.depend()
          // 如果是数组,数组中的值也需要收集依赖
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    ......
  })
}

上面的代码中,有个Dep的类,我们继续来看:

let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 * dep是一个可监听对象,可以有多个指令订阅它。
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    // 收集依赖的数组
    this.subs = []
  }

  addSub (sub: Watcher) {
    // 将订阅者Watcher推入依赖收集数组
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    // 在收集依赖数组移除订阅者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()
}

  • Dep.target是Dep上的一个静态属性,起始时候是一个null值,
  • pushTarget这个函数,是将当前的目标观察者Watcher推入targetStack栈数组中,并将传入的最新的Watcher,赋值给Dep.target
  • popTarget这个函数,取出targetStack栈数组中的最后一个Watcher,赋值给Dep.target

那么这个Dep.target是什么时候赋值的呢? 我们在之前的文章中有过分析,页面渲染的时候会初始化一个updateComponent方法,随后实例化了一个render Watcher,实例化的过程中,执行了Watcher上的get函数(不清楚这段逻辑可以点击这里),这里我们继续以render Watcher来分析:

/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
  // this指向当前的render Watcher(后续还会分析自定义Watcher与计算属性Watcher)
  pushTarget(this)
  let value
    
  // vm指向当前实例
  const vm = this.vm
  try {
    // 获取当前的值,在取值的过程中,就会触发响应式数据的get函数,而这时的Dep.target就是当前的render Watcher
    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)
    }
    // 取值完毕,依赖收集完成,释放当前render Watcher
    popTarget()
    // 清除依赖,后面会分析这段逻辑
    this.cleanupDeps()
  }
  return value
}

执行this.get的时候,会将Dep.target赋值为当前的render Watcher,而执行this.getter,就是执行updateComponent函数,会调用render方法,这个过程中会访问到渲染当前页面的数据,有响应式数据的话,就会进入get方法:

get: function reactiveGetter () {
      ......
      // Dep.target为render Watcher进入逻辑
      if (Dep.target) {
        // 进入逻辑,收集依赖
        dep.depend()
        // 下面逻辑我们之后的文章分析
        if (childOb) {
          // child收集依赖
          childOb.dep.depend()
          // 数组中的值依次收集依赖
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },

接下来执行dep.depend()

// Dep类上的方法
depend () {
  // 通知依赖更新,Dep.target为render Watcher
  if (Dep.target) {
    // 执行Watcher上的addDep方法
    Dep.target.addDep(this)
  }
}
  // Watcher类上的方法
/**
* Add a dependency to this directive.
* 添加一个依赖到这个指令
*/
addDep (dep: Dep) {
  // 这儿id的作用是防止重复订阅
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    // (上面的逻辑先忽略,之后的文章分析)这儿判断如果这个Watcher没有订阅过这个dep
    if (!this.depIds.has(id)) {
      // 将Watcher推入这个dep,也就是Watcher订阅这个dep了
      dep.addSub(this)
    }
  }
}
// Dep类上的方法
addSub (sub: Watcher) {
  // 将这个Watcher推入Dep的subs中
  this.subs.push(sub)
}

通过以上的分析,我们清楚了依赖收集的整个过程了,首先是实例化render Watcher,将当前的Dep.target更改,执行取值方法,获取响应式数据的值,触发响应式数据中的get方法,这样当前的render Watcher就订阅了响应式数据中的dep实例。

收集依赖流程

graph TD
new Watcher(render Watcher) --> Dep.target = render Watcher --> gain value(触发get:reactiveGetter) --> dep.subs.push(Watcher)

那依赖收集完成后,数据改变的时候怎么更新页面呢?下一节我们继续分析派发更新的过程,此外,本节也遗留了几个小问题,之后的文章也会去分析,敬请期待。