响应式原理三:派发更新

《响应式原理一:data 初始化》一文中,分析了 data 是如何将一个普通对象变成响应式对象,核心的实现是在函数 defineReactive 中采用 ES5 Object.defineProperty 定义了 getset 函数,其作用是依赖收集和派发更新。那么,本文将分析派发更新的实现原理。

setter 实现原理

沿着主线将其实现逻辑整理成一张逻辑图如下:

setter.png

当修改 data 属性值,会触发 setter,更新视图;也就是说,会重新执行渲染逻辑,即 renderpatch 过程,接着就一步一步地来分析该过程。setter 实现逻辑如下:

// src/core/observer/index.js
set: function reactiveSetter (newVal) {
  const value = getter ? getter.call(obj) : val
  /* eslint-disable no-self-compare */
  if (newVal === value || (newVal !== newVal && value !== value)) {
    return
  }
  /* eslint-enable no-self-compare */
  if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()
  }
  // #7981: for accessor properties without setter
  if (getter && !setter) return
  if (setter) {
    setter.call(obj, newVal)
  } else {
    val = newVal
  }
  childOb = !shallow && observe(newVal)
  dep.notify()
}
复制代码

首先,获取原始值,与新值做比较,如果值没有发生任何改变,则结束程序;否则继续执行后续逻辑。

接着,判断是否自定义 setter,如果有的话则执行;否则将新值赋值给旧值,即 val = newVal

然后,判断 shallow 的值,如果其值为 false,则执行 observe

最后,调用实例 dep 方法 notify 通知所有订阅者做出相应的改变。

notify 实现原理

// src/core/observer/dep.js
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()
  }
}
复制代码

变量 subswatcher 数组,notity 作用是遍历数组 subs,调用实例 watcher 方法 update ,通知已订阅的 watcher 做出变更。如果当前环境是开发环境,并且是同步执行的,则需要对 subs 进行排序,以确保它们的顺序。那么,来看 update 的实现逻辑:

// src/core/observer/watcher.js
/**
  * Subscriber interface.
  * Will be called when a dependency changes.
  */
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}
复制代码

update 实现逻辑挺简单的,分为三种情况。如果 lazytrue,则执行 this.dirty = true,用于计算属性;如果 synctrue,则执行实例 watcher 方法 run,后续会分析;最后一种情况则执行函数 queueWatcher(this),即执行派发更新逻辑。

至于 lazysync 是如何来的呢?它们的初始化是发生在实例化 Watcher,而对于 Watcher,可分为渲染 WatcherComputed Watcher、用户 Watcher,用户 Watcher 比渲染 Watcher 先被初始化。

queueWatcher 实现原理

// src/core/observer/scheduler.js
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      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)
    }
  }
}
复制代码

Vue 框架在处理 watcher 时,并不是每当有数据变化时,就触发 watcher 的回调,而是会将其先加入到队列 queue,然后等到下一个 tick 才执行队列 queue 里的 watcher 回调,即在 nextTick 后执行 flushSchedulerQueue

这里用对象 has 来保存 watcher,以确保同一个 watcher 只被执行一次;然后通过 flushing 来控制逻辑的执行,如果其值为 false 时,则将 watcher 添加到队列 queue;否则基于 idindex,将 watcher 插入到队列中;最后通过 waiting 来保证 nextTick(flushSchedulerQueue) 只被执行一次。

先来分析 flushSchedulerQueue 的实现,下一节再来分析 nextTick

// src/core/observer/scheduler.js
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  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 (b) {
      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')
  }
}
复制代码

函数的作用是刷新队列和执行 watcher。那么,在刷新队列之前,先对队列做了排序,作用是为了确保:

  • 组件的更新顺序是从父到子,因为组件的创建是先父后子;那么 watcher 的创建也是先父后子,执行顺序也理所当然的是先父后子;
  • 用户自定义 watcher 先于渲染 watcher,因为用户自定义 watcher 先于渲染 watcher 创建;
  • 如果一个组件在其父组件执行 watcher 期间被销毁,那么其 watcher 的执行可以被跳过。

经过一番排序后,queue 队列所保存的 watcher 的顺序是从小到大。

接着,对 queue 队列进行遍历,获取到对应实例 watcher,如果 watcher.before 不为空的话,则执行该回调,该回调是在初始化 watcher 时作为参数传进来的。

然后执行 watcher.run,并且将对象 has 保存的 watcher 设置为空。在遍历的过程中,需要注意两点:

  • 队列 queue 的长度没有被缓存下来,而是每次对其进行求值;原因是 watcher.run 在执行的过程中,用户可能会添加新的 watcher,这样会再次执行到函数 queueWatcher (代码见上)。

    因为此时 flushingtrue,从而执行 else 逻辑,那么又是如何往队列 queue 添加 watcher 呢?

    插入逻辑是从队列后面往前找,找到第一个待插入 watcherid 比当前队列中 watcherid 大的位置,把 watcher 按照 id 插入到队列中,从而使用 queue 的长度发生变化。

  • 在遍历的过程中,有一段 if 逻辑,即当在开发环境时,如果同一个 watcher 执行的次数超过 MAX_UPDATE_COUNT,即 100 时,此时会在控制台抛出告警,提示 watcher 的回调执行陷入死循环。那么这样的场景是如何触发的呢?

    export default {
      watcher: {
        message(oldValue, newValue) {
          this.message = Math.random()
        }
      }
    }
    复制代码

那么 watcher.run 又是如何实现的呢?先来分析其逻辑,再回过头来分析剩下的逻辑。

// src/core/observer/watcher.js
/**
  * Scheduler job interface.
  * Will be called by the scheduler.
  */
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
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      if (this.user) {
        const info = `callback for watcher "${this.expression}"`
        invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}
复制代码

首先通过 this.get() 获取到它当前的值,然后判断是否满足条件。如果满足新旧值不一样、value 其数据类型为对象、deeptrue 中的任何一个,都会执行 if 逻辑。

如果当前是用户自定义 watcher,则调用函数 invokeWithErrorHandling ;否则是渲染 watcher 或者 computed watcher,通过 call 执行其回调函数。

对于渲染 watcher 而言,在执行 this.get() 的过程中,会触发其 getter,即

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
复制代码

也就是说,当我们修改响应式数据时,会触发组件重新渲染,即执行 renderpatch

那么来看下 invokeWithErrorHandling 是如何实现的?

// src/core/utils/error.js
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}
复制代码

其实现逻辑也挺简单的,也就是执行用户在自定义 watcher 时传进来的回调函数,此时会传入 oldValuenewValue 。所以,我们在外部才能拿到这两个值。

至此,watcher.run 实现逻辑分析完,回到 flushSchedulerQueue 分析剩下的逻辑。

在恢复状态之前,分别对 activatedChildrenqueue 拷贝一份,保存到变量 activatedQueueupdatedQueue,然后再执行恢复状态逻辑,即 resetSchedulerState:

// src/core/observer/scheduler.js
/**
 * Reset the scheduler's state.
 */
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}
复制代码

其实现逻辑挺简单的,就是把一些控制状态重置,以及 watcher 清空。

最后,分别触发组件的 updatedactivated 钩子函数。先来看下是如何触发组件 activated 钩子函数的?

// src/core/observer/scheduler.js
function callActivatedHooks (queue) {
  for (let i = 0; i < queue.length; i++) {
    queue[i]._inactive = true
    activateChildComponent(queue[i], true /* true */)
  }
}
复制代码

遍历队列 queue,先对每个 watcher 设置属性 _inactivetrue,再调用 activateChildComponent,实现如下:

// src/core/instance/lifecycle.js
export function activateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = false
    if (isInInactiveTree(vm)) {
      return
    }
  } else if (vm._directInactive) {
    return
  }
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false
    for (let i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i])
    }
    callHook(vm, 'activated')
  }
}
复制代码

最后来看下如何触发组件 updated 钩子函数?

// src/core/observer/scheduler.js

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}
复制代码

至此,派发更新逻辑已分析完。

参考链接

Guess you like

Origin juejin.im/post/7068918363774255141