Vue源码分析之Watcher队列

1,288 阅读3分钟

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

前言

在前边学习Watcher class的update方法时,看到里边调用了一个queueWatcher的方法。

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

接下来就来看下queueWatcher具体是做了什么??

queueWatcher

queueWatcher定义在src/core/observer/scheduler.js中

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let flushing = false
let waiting = false
let index = 0

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)
    }
  }
}

总体来看,这里引入了一个Watcher队列。被监听的数据发生改变时并不会立即去执行它的回调(除非设置sync值为true),而是将它push到队列中,在下一个Tick去执行flushSchedulerQueue

  • 首先根据Watcher id来判断这个Watcher是否存在has对象中,这里主要是避免将Watcher重复添加。

  • 接下来判断flushing的值(flushing代表当前Watcher对列是不是正在执行),如果flushing为false,则将当前Watcher添加到队列中。

  • flushing为true,则从队尾开始根据id大小(队列是从小到大的顺序)将这个Watcher插入到队列对应的位置。如果没有找到合适的位置,则直接插入队列头部。index值代表队列中当前正在执行的Watcher的位置。

  • 接下来判断waiting的值,如果为false,则将waiting置为true,并执行nextTick函数。waiting变量是为了保证当前只执行一次flushSchedulerQueue逻辑。

  • 最后调用nextTick来执行flushSchedulerQueue。nextTick的实现前边也看到过,在现代浏览器中它就是一个微任务。把所有的回调函数(这里就是这个Watcher的回调)放到一个callbacks数组中,然后在当前事件循环结束后,去执行这个微任务。

flushSchedulerQueue

const activatedChildren: Array<Component> = []


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 (watcher.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')
  }
}
  • 将当前时间赋值给currentFlushTimestamp变量,然后将flushing置为true,代表正在刷新队列。

  • 给队列按照从小到大的顺序排列。

  • for循环遍历Watcher队列。首先判断当前Watcher项是否设置了before方法(渲染Watcher会把beforeUpdate钩子函数传给before方法),如果设置了则执行before方法;接下来调用该Watcher的run方法去执行对应的监听回调,同时将has表中记录的当前Watcher id对应的值置为null。

  • 开发环境下判断circular[id]是否超过最大限制的更新值(100),如果超过,抛出警告可能有循环更新。

  • 在重置队列状态之前拷贝activedChildren和queue队列数据,用来传递给后边的钩子函数。

  • 最后重置初始化参数,并且执行activated与updated钩子函数。

这里有一大段注释来解释为什么给Watcher队列要从小到大排序,具体来看一下:

  1. 组件的更新是从父到子,因为父组件的创建是在子组件之前的。
  2. 组件的user Watcher要在render Watcher之前执行,因为用户自定义的Watcher是在渲染Watcher之前创建的。
  3. 如果一个组件在父组件的Watcher执行期间被销毁,那么它的Watcher可以被跳过,所以父组件要先执行。

resetSchedulerState

Watcher队列更新完毕后,调用resetSchedulerState函数来重置初始参数

function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

queueActivatedComponent

从注释中看出这个函数是为keep-alive组件服务的。

activatedChildren队列保存了要被激活的keep-alive组件。

/**
 * Queue a kept-alive component that was activated during patch.
 * The queue will be processed after the entire tree has been patched.
 */
export function queueActivatedComponent (vm: Component) {
  // setting _inactive to false here so that a render function can
  // rely on checking whether it's in an inactive tree (e.g. router-view)
  vm._inactive = false
  activatedChildren.push(vm)
}

queueActivedComponent函数是在componentVNodeHooks被调用的,componentVNodeHooks是一个定义组件Vnode钩子函数的对象,代码在src/core/vdom/create-component.js中。

具体组件的创建部分这里也先不看,大概看一下这个hooks函数:

const componentVNodeHooks = {
  ...
 
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
  
  ...
}

从代码来看,大概就是说如果<keep-alive>的组件已经mounted,则将这个组件添加到activatedChildren数组中。

callActivedHooks

我们已经知道queueActivatedComponent函数是为了将当前vm实例添加到activatedChildren数组中,而callActivedHooks函数就是通过遍历该数组来将被keep-alive缓存的组件激活。

function callActivatedHooks (queue) {
  for (let i = 0; i < queue.length; i++) {
    queue[i]._inactive = true
    activateChildComponent(queue[i], true /* true */)
  }
}

activateChildComponent函数定义在src/core/instance/lifecycle.js,整体来看就是递归调用,执行所有子组件的activated钩子函数。

export function activateChildComponent (vm: Component, direct?: boolean) {
  ...
  
  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')
  }
}

callUpdatedHooks

生命周期部分后边会专门看,这里大概了解下,updated钩子函数的执行时机就是在flushSchedulerQueue函数中的callUpdatedHooks

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')
    }
  }
}

在看Watcher的构造函数的时候,会判断当前Watcher实例是否是渲染Watcher,如果是会赋值给vm._watcher

if (isRenderWatcher) {
  vm._watcher = this
}

callUpdatedHooks函数遍历Watcher队列,满足当前watcher是vm._watcher(渲染watcher),且该Vue实例已经mounted,同时没有被destroyed,则执行updated钩子函数。