Vue 源码分析 -- Data 响应式化

89 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情

Data 响应式化

在开始分析 Data 之前,我们先抛出几个问题,这些问题的会在后面的分析中逐一解答

    1. Dep 作为管理依赖的类,那么 Dep 是在什么时候进行初始化的呢?
    1. Dep 收集了哪些类型的依赖?即 Watcher 作为依赖有哪些分类,分别在什么场景下使用,却别是什么?
    1. Observer 这个类在重写 gettersetter 时具体做了什么?
    1. Vue 选项中定义的 watcher 选项,和页面数据渲染监听的 watcher 同时都监听到数据的变化,优先级时怎样的?
    1. 有进行依赖收集,那么是否存在解除依赖?解除依赖的意义在哪里?

依赖收集

data 在初始化阶段会实例化一个 observe 对象

function initData (vm: Component) {
  // observe data
  observe(data, true /* asRootData */)
}

function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }

  return ob
}

// Observer 类
class Observe{
  constructor(){
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
}

function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

可以看到,在 Observer 实例化时,会往对象上添加一个不可枚举的 __ob__ 属性,并将该属性的值设置为 Observer 实例本身,在调用 observe 方法时, 会判断对象上是否存在 __ob__ 属性,如果已经存在,表明该对象已经时响应式对象,则跳过响应式化的过程。另外, Observer 构造函数中还调用了 walk 方法,在该方法中,会遍历对象属性,对每个属性的 getter setter 方法进行重写,也正是在这一步对添加了数据劫持

class Observer{
   walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

defineReactive 方法时数据响应式化的核心,来看下具体的实现。

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) {
        // 将当前的 watcher 添加到依赖管理中
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      
    }
  })
}

通过上面代码看出, defineReactive 方法在执行时,先会实例化一个 Dep 类,也就是说,数据响应式化过程中,会为数据的每一个属性创建一个依赖管理。之后,使用 Object.defineProperty 重写 getter setter 方法。我们知道,当 data 中的属性值被访问时,会触发 getter 函数的执行,也就是会被 getter 函数拦截,在之前的分析得知,在实例挂载前会创建一个实例 Watcher , 而实例挂载时,会经历将模版解析成 render 函数,由 render 函数转化成虚拟 DOM 的过程,在 render 函数转换成虚拟 DOM 时,会访问袋定义的 data 数据, 那么就会触发 getter 进行依赖收集,而此时收集的依赖就是渲染 Watcher 本身。

class Dep{
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  },
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
}

class Watcher{
  /**
   * 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)
      }
    }
  }
}

派发更新

在数据发生变化时,会执行在 defineReactive 中重写的 setter 方法。来看下 setter 方法的实现

Object.defineProperty(obj, key, {
  set: function reactiveSetter (newVal) {
    const value = getter ? getter.call(obj) : val
    // 新值和旧值相等时,不进行更新
    if (newVal === value || (newVal !== newVal && value !== value)) {
      return
    }
    // 如果新值是一个对象,需要对新值重现进行依赖收集
    childOb = !shallow && observe(newVal)
    dep.notify()
  }
})

派发更新阶段主要完成一下步骤

    1. 判断更改前后的数据是否相等,如果相等则不进行派发更新操作
    1. 新值为对象时,会对该值的属性进行依赖收集的过程
    1. 通知该数据收集的 watcher 依赖,遍历 watcher 进行数据更新
class Dep{
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      // 遍历每个依赖,进行数据更新操作
      subs[i].update()
    }
  }
}

Watcher 上的 update 方法执行时会将 watcher 对象自身添加到队列中,等待下一个 tick 到来时取出每一个 watcher 执行 run 操作

class Watcher{
  update(){
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

queueWatcher 方法的调用,会将数据所收集的依赖一次添加到 queue 数组中, 数组会在下一个 tick 中根据缓冲结果进行视图更新。而在执行视图更新操作时,可能会因为数据的改变而在渲染模版上添加新的依赖,这样又会执行 queueWatcher 过程,所以需要一个标志为判断是否处于异步更新的队列中,如果处于异步更新过程中,则将新的 watcher 添加到 queue 队列中。

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

当下一个 tick 到来时,会执行 flushSchedulerQueue 方法,该方法会拿到一个由 watcher 组成的数组,然后会数据进行排序,源码中对排序进行了说明

    1. 父组件在子组件之前创建,因此需要保证父组件的渲染 watcher 在子组件的渲染 watcher 之前执行
    1. 用户定义的 watch 会在渲染 watcher 之前执行,因此,用户的 watch 会在渲染 watch 之前执行
    1. 如果一个组件在父组件的 watcher 执行阶段被销毁,那么它对应的所有 watcher 都可以跳过
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  // 对 队列中的 watcher 进行排序
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      // 如果 watcher 中定义了 before 的配置,则先执行 before 方法
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

flushSchedulerQueue 阶段,重要的过程可以总结为四点

  • queue 中的 watcher 进行排序
  • 遍历 watcher , 如果 watcherbefore 配置,则执行 before 配置,对应前面的渲染 watcher : 在渲染 watcher 实例化时,我们传递了 before 函数,即在下个 tick 更新视图之前,会先调用 beforeUpdate 生命周期钩子
  • 执行 watcher.run 进行修改的操作
  • 重置恢复状态,这个阶段会将一些流程控制的状态变量恢复成初始值,并清空记录 watcher 的队列。

重点看看 watcher.run 的操作。

class Watcher{
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        //  设置新值
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
}

首先 run 方法会执行 watcher 上的 get 方法,得到变化后的数据值,然后与旧值进行对比,如果满足条件,则执行 cb 回调, cb 为实例化 watcher 时传入的回调。再来看看 get 方法的定义

class Watcher{
  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 方法会调用 this.getter 进行求值,在渲染 watcher 下, this.getter 会进行视图更新操作,这一阶段会重新渲染页面组件。在执行完 getter 操作之后,最后一步会进行依赖的清除,也就是 cleanupDeps 的过程。

关于清楚依赖的作用,列举一个场景: 在开发过程中经常使用 v-if 来进行模版的切换,切换过程中执行不同的模版渲染,不同模版对数据的依赖可能不同,模版切换之后,看你不再对默写数据进行依赖,这时如果数据发生变化,会引起依赖的重新渲染,造成性能的浪费。因此旧依赖的清除在优化阶段是有必要的。

class Watcher{
  // 清除依赖的过程
  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
  }
}

上面派发更新总结成两点:

  • 执行 run 操作会执行 getter 方法,也就是重新计算值,针对渲染 watcher 而言,会执行 updateComponent 进行视图更新
  • 重新计算 getter 之后,会进行依赖的清除