theydy / notebook

记录读书笔记 + 知识整理,vuepress 迁移中 https://theydy.github.io/notebook/
0 stars 0 forks source link

Watcher 更新队列的实现? #26

Open theydy opened 3 years ago

theydy commented 3 years ago

Watcher 更新队列的实现主要是两个函数,queueWatcherflushSchedulerQueue

// src/core/observer/scheduler.js

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

function flushSchedulerQueue () {
  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')
  }
}

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
      nextTick(flushSchedulerQueue)
    }
  }
}

queueWatcher

先来看 queueWatcher,这是主要暴露出来的调度函数。

首先会对传进来的 watcher 使用 has 数组保存 watcher.id 的方法做一个重复过滤,保证在进入 flushSchedulerQueue 之前 queue 中的 watcher 是不重复的。

接着判断如果不是 flushing 则加入队列中,否则会执行这一段代码。

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

能够进入这段代码说明当前的这个 watcher 是在 flushing 期间加入的,也就是在 flushSchedulerQueue 函数执行期间加入,在 flushSchedulerQueue 中,会把 queue 根据 watcher.id 从小到大排序,接着从头到尾遍历执行 watcher.run 方法,执行 get 获取新值,执行回调。这个 watcher.run 的过程中就存在又触发了其他响应式数据 setter → dep.notify → watcher.update → queueWatcher(watcher) 的这个过程,举个例子,比如死循环更新。

<template>
  <p>{{msg}}</p>
  <button @click="change">Add</button>
</template>
<script>
export default {
  data () {
    return {
      msg: 'msg'
    }
  },
  methods: {
    change () {
      this.msg = Math.random()
    }
  },
  watch: {
    msg () {
      this.msg = Math.random()
    }
  }
}
</script>

而且注意一点,flushSchedulerQueue 中遍历 queue 执行 watcher.run 方法之前,会把当前的 has[id] = null 置回 null,这也是死循环会出现的原因之一,就是因为这里会置回 null,所以在 flushing 期间加入的 watcher 都可以通过queueWatcher 中的 has[id] 的重复检查,即使是相同的 watcher。

回到这段代码

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

可以很容易理解,它是一个从后往前比较的插入排序操作,将当前 watcher 根据 id 插入已经排好序的 queue 中。

queueWatcher 的最后判断是否是 waiting 状态,在一个 event loop 中第一次走进 queueWatcher 时,会走这块逻辑执行 nextTick(flushSchedulerQueue) ,这里可以简单的理解为在微任务中调用 flushSchedulerQueue

flushSchedulerQueue

flushSchedulerQueue 函数主要做了这五件事

  1. 设置 flushing 状态
  2. queue 排序
  3. 遍历 queue 执行 watcher.run()
  4. resetSchedulerState() 还原状态变量
  5. 触发组件钩子函数

第一点 flushing = true 很好理解,整个遍历 queue 期间都是 flushing 状态。

第二点 queue 根据 watcher 的 id 从小到大排列,之所以从小到大有以下三点:

第三点遍历 queue ,如果是组件并且 _isMounted,则调用 watcher.before() 的结果是执行 beforeUpdate 钩子函数,接下来的步骤在 queueWatcher 中其实说过了has[id] = nullwatcher.run() ,加上死循环的判断,死循环的判断就是在一个 flushing 期间同一个 watcher 执行的次数超过 MAX_UPDATE_COUNT 就断定为死循环。

第四点重置 indexhascircularwaitingflushing 这些状态变量。

第五点调用 callActivatedHookscallUpdatedHooks 执行钩子函数