vue2源码学习 (9).响应式原理-7.queueWatcher

91 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 13 天,点击查看活动详情

9.响应式原理-7.queueWatcher

start

上一个章节查看了 Watcher 的源码,了解到 dep 中会存储 Watcher。那么当我们 set 数据的时候,dep 和 Watcher 是如何工作的呢?

从set到update

/* 1. defineReactive中的set */
dep.notify()

/* 2.dep的notify */
for (let i = 0, l = subs.length; i < l; i++) {
  subs[i].update()
}

/* 3.Watcher实例的update*/
update() {
  /* istanbul ignore else */
  if (this.lazy) {
    // 懒
    this.dirty = true;
  } else if (this.sync) {
    // 同步
    this.run();
  } else {
    // 主要是执行 queueWatcher
    queueWatcher(this);
  }
}

/* 4. queueWatcher */

看上述的逻辑,可以了解到当修改数据的时候,触发了以下操作:

  1. 触发 defineReactive 中的 set;
  2. Dep 实例的 notify;
  3. Watcher 实例的 update
  4. queueWatcher

queueWatcher

  • 看一下 queueWatcher 英文单词的释义

    • queue 队列
    • Watcher 观察者
    • scheduler 时间调度员;程序机,调度机;调度程序; (scheduler是存放 queueWatcher 方法的 js 文件名)
  • 好,开始研究queueWatcher以及其相关的逻辑

\src\core\observer\scheduler.js 中的 queueWatcher

const queue: Array<Watcher> = []
let index = 0

let has: { [key: number]: ?true } = {}
let flushing = false

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 * 将一个观察者推入观察者队列。
 * 有重复id的工作将被跳过,除非它是
 * 当队列被刷新时推送。
 */

export function queueWatcher(watcher: Watcher) {
  // 1. 拿到 watcher的id
  const id = watcher.id

  // 2. has 是一个对象, 用来存储 watcher,同一个 watcher不用重复推入
  if (has[id] == null) {
    has[id] = true

    // flushing 冲洗
    if (!flushing) {
      // queue是一个数组,存储watcher
      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.
      // 如果已经在刷新,则根据监视程序的id拼接它
      // 如果已经超过了它的id,接下来将立即运行它。
      let i = queue.length - 1 // 拿到最后一个watcher的索引
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    // 排队刷新
    if (!waiting) {
      waiting = true

      nextTick(flushSchedulerQueue)
    }
  }
}

function flushSchedulerQueue() {
  currentFlushTimestamp = getNow()

  flushing = true
  let watcher, id

  queue.sort((a, b) => a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]

    // watcher上有 before ,先执行对应的 before
    if (watcher.before) {
      watcher.before()
    }

    // 拿到当前 watcher id
    id = watcher.id

    // 清空
    has[id] = null

    // 执行watcher的 run
    watcher.run()
  }

  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  resetSchedulerState()
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

\src\core\observer\next-tick.js 中的 nextTick

// 定义一个数组,存储用户注册的回调
const callbacks = []
// 声明一个变量标记,标记是否已经向任务队列中添加了一个任务
let pending = false

// 其实就是 下次微任务执行时 更新 dom
export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise((resolve) => {
      _resolve = resolve
    })
  }
}

\src\core\observer\next-tick.js 中的 timerFunc

let timerFunc // 定义一个变量

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true,
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

/* 
通过下面的方法
promise
MutationObserver
setImmediate
setTimeout
*/
function flushCallbacks() {
  // 1. 重置为false
  pending = false

  // 2. 浅拷贝一份函数
  const copies = callbacks.slice(0)
  callbacks.length = 0 // 清空数组???有点意思 var a=[1,2,3]; a.length = 0;    打印a []

  // 3. for 循环执行callbacks中存储的每一项
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

首先,上方的代码块展示了 queueWatcher 相关的主要代码。 \src\core\observer\scheduler.js \src\core\observer\next-tick.js

这里统一梳理一下相关逻辑。

前面提到,当我们触发 data 中一个属性的 set,会触发queueWatcher

  1. queueWatcher 的主要逻辑

    • 在 has,queue 存储我们的 watcher 的 id;
    • nextTick(flushSchedulerQueue)
  2. flushSchedulerQueue

    • for 循环遍历 queue,依次执行 watcher.run
  3. nextTick

    • 将传入的 flushSchedulerQueue,存储在 callbacks 数组中。
    • 如果 !pending,执行timerFunc
  4. timerFunc

    • 异步执行 flushCallbacks
    • 这里的异步实现优先级依次为 Promise,MutationObserver,setImmediate,setTimeout
  5. flushCallbacks

    • 遍历并执行 callbacks 的每一项(flushSchedulerQueue)。

watcher.run

  /**
   * 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);
        }
      }
    }
  }

watcher.run的逻辑也很简单,调用 this.get(),调用 vm._update(vm._render(), hydrating); _update,_render后续会去学习,这里简单理解,视图更新的方法。

思考

  1. 异步队列

阅读了上述内容,我了解到当修改数据,其实最终还是执行 watcher.run。 但是为什么要引入异步队列? 先看看官方文档对这里的解释。 vue2_异步队列

image.png

结合现有我们了解到的

并不是当我修改数据,就马上更新 dom,而是开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

举个例子,我点击一个按钮,分别修改a,b,c三个属性的属性值,代码如下所示

{
 data () {
   return {
     a: 1,
     b: 1,
     c: 1
   }
 },
 methods: {
   say () {
     this.a = "111"
     this.b = "222"
     this.c = "333"
   }
 }
}
  • 此时会分别触发 a,b,c三个属性的 set。如果没有异步队列,会执行三次 watcehr.run , 重新渲染三次页面?
  • 加入了异步队列,在一次事件循环中,收集去重后的 watcher,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

这么做,可以避免不必要的性能消耗。但是也会有一些影响。正如官网所提到的

如果你想基于更新后的 DOM 状态做点什么?

this.$nextTick(() => {
  // 这里写你的逻辑
})

end

  • 本节主要是了解了 通知更新实际是 执行 watcher.run,然后触发视图更新。
  • Vue 在更新 DOM 时是异步执行的。