Vue 异步更新源码解析

先补充一下 Vue 实现双向数据绑定的原理:
  通过Object.defineproperty 拦截对数据的访问 get 和设置 set,当拦截到数据的访问时进行依赖收集,拦截到数据的设置时则执行dep.notify 通知 watcher 进行更新。

notify

/src/core/observer/dep.js

/**
   * 通知该依赖收集的所有 watcher 执行 update 方法,进行异步更新
   */
  notify () {
    // subs 保存的是该依赖收集的所有 watcher
    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()
    }
  }


update

  调用 queueWatcher 方法,将 watcher 加入队列

/src/core/observer/watcher.js

/**
   * 根据 watcher 配置,决定接下来怎么走,一般是 queueWatcher
   */
  update () {
    /* istanbul ignore else */
    // 如果是懒执行,设置 dirty 为 true
    // computed 就是懒执行,可以让 computedGetter 执行时重新计算 computed 回调函数的执行结果
    // if (watcher.dirty) {
    //   watcher.evaluate()
    // }
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      // 同步执行,在使用 vm.$watch 或者 watch 选项时可以传一个 sync 选项,
      // 当为 true 时在数据更新时该 watcher 就不走异步更新队列,直接执行 this.run 
      this.run()
    } else {
      // 将 watcher 放入 watcher 队列,一般走这里
      queueWatcher(this)
    }
  }


queueWatcher

  1. 每个 watcher 都有自己的 id,如果 has 记录到对应的 watcher,则跳过,不会重复入队,这在 官网 有提到:如果同一个 watcher 被多次触发,只会被推入到队列中一次
  2. watcher 加入到队列中,等待执行。
  3. 当队列没有在刷新的时候,执行nextTick(flushSchedulerQueue)。这里的 waiting 的作用就是一个锁,防止 nextTick 重复执行。

ps:flushSchedulerQueue 作为回调传入 nextTick 异步执行。

/src/core/observer/scheduler.js

/**
 * 将 watcher 放入 watcher 队列
 * 具有重复 id 的 watcher 会跳过
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 如果 watcher 已经存在,则跳过,不会重复入队
  if (has[id] == null) {
    has[id] = true
    // 不管是否刷新, watcher 都能立即入队 queue
    if (!flushing) {
      // 如果当前没有处于刷新队列状态,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.
      // 如果已经在刷新队列,把 watcher 放入正确的位置
      // 从队列末尾开始倒序遍历,根据当前 watcher.id 找到它大于的 watcher.id 的位置,然后将自己插入到该位置之后的下一个位置
      // 即将当前 watcher 放入已排序的队列中,且队列仍是有序的
      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) {
        // 直接刷新调度队列, 一般不会走这儿,Vue 默认是异步执行,如果改为同步执行,性能会大打折扣
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}


nextTick

  1. cb 即传入的回调,它被 push 进一个 callbacks 数组,等待调用。
  2. pending 的作用和 queueWatcher 方法中的 waiting一样,也是一个锁。防止后续的 nextTick 重复执行 timerFunctimerFunc 内部创建会一个微任务或宏任务,等待所有的 nextTick 同步执行完成后,再去执行 callbacks 内的回调。
  3. 如果没有传入回调,用户可能使用的是 Promise 形式,返回一个 Promise_resolve 被调用时进入到 then

/src/core/util/next-tick.js

/**
 * 1、用 try catch 获取 cb ,确保拿到的是函数,然后将其放入 callbacks 数组
 * 2、pending = false:表示现在浏览器的任务队列没有 flushCallbacks 函数
 *  pending = true:表示浏览器的任务队列中有 flushCallbacks 函数
 *   待执行 flushCallbacks 函数时,pending 会被再次置为 false,表示下一个 flushCallbacks 函数可以进入
 *   浏览器的任务队列
 * @param {*} cb 接收一个回调函数 => flushSchedulerQueue
 * @param {*} ctx 上下文
 * @returns 
 */
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 因为用户可以采用 Vue.nextTick 和 this.$nextTick 调用 nextTick ,
  // 因此在对 callbacks 数组存储 cb 回调之前,做了捕获,确保拿到 cb
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    // 执行 timerFunc,在浏览器的任务队列中(首选微任务队列)放入 flushCallbacks 函数
    pending = true
    timerFunc()
  }
  // 处理采用 promise 进行调用的情况: Vue.nextTick().then(function () { })
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

timerFunc

flushCallbacks函数放入浏览器的异步任务队列中
通过兼容来创建合适的 timerFunc,最优先肯定是微任务,其次再到宏任务。优先级为 promise.then(微任务) > MutationObserver(微任务) > setImmediate(宏任务) > setTimeout(宏任务)

/src/core/util/next-tick.js

let timerFunc
// 首选 Promise.resolve().then() 微任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    // 在 微任务队列 中放入 flushCallbacks 函数
    p.then(flushCallbacks)
    /**
     * 在有问题的UIWebViews中,Promise.then不会完全中断,但是它可能会陷入怪异的状态,
     * 在这种状态下,回调被推入微任务队列,但队列没有被刷新,直到浏览器需要执行其他工作,例如处理一个计时器。
     * 因此,我们可以通过添加空计时器来“强制”刷新微任务队列。
     */
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  // 第二选择 MutationObserver 微任务
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  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)) {
  // 第三选择 MutationObserver 宏任务,但是依然比 setTimeout 好
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 前三种都不支持的情况见,选择 setTimeout 宏任务
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}


flushCallbacks

/src/core/util/next-tick.js

/**
 * 清空 callbacks 数组、pending 设为 false、
 * 执行 callbacks 数组中的每一个函数(比如 flushSchedulerQueue、用户调用 nextTick 传递的回调函数)
 */
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}


nextTick 回调flushSchedulerQueue

将刚刚加入 queuewatcher 逐个 run 更新。resetSchedulerState 重置状态,等待下一轮的异步更新。
要特别注意: 在执行nextTick(flushSchedulerQueue)的时候flushSchedulerQueue 并不是马上执行,它只是作为回调传入而已。因为用户可能还会调用 nextTick 方法。这种情况下,callbacks 里的内容为 ["flushSchedulerQueue", "用户传入的nextTick回调"],当所有 nextTick同步任务执行完才开始执行 callbacks 里面的回调。

/src/core/observer/scheduler.js

/**
 * 刷新 watcher, 运行 watcher.run 方法,触发更新函数
 */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // 刷新队列之前先给队列排序(升序),可以保证:
  // 1. 父组件比子组件先更新. (因为父组件比子组件先创建)
  // 2. 一个组件的用户user watcher 比渲染 watcher 先执行 (用户user watcher 比渲染 watcher 先创建)
  // 3. 如果一个组件在其父组件的 watcher 执行期间被销毁,则它的 watcher 可以被跳过
  queue.sort((a, b) => a.id - b.id)

  // 直接使用了 queue.length,动态计算队列的长度。不要缓存队列的长度,因为在我们运行存在的 watcher 的时候,别的 watcher 可能会 push 进来
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    // 执行 before 钩子,在使用 vm.$watch 或者 watch 选项时可以通过配置项(options.before)传递
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 执行 watcher.run,最终触发更新函数
    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')
  }
}


resetSchedulerState

重置状态,意味着这一'tick' 刷新队列结束,刷新状态改为 false ,等待下一轮的异步更新

/src/core/observer/scheduler.js

/**
 * 重置调度状态
 * 1、has = {}
 * 2、waiting = flushing = false,表示刷新队列结束
 * 可以给 callbacks 数组中放入新的 flushSchedulerQueue 函数
 * 并且可以向浏览器的任务队列放入下一个 flushCallbacks 函数
 */
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false

你可能感兴趣的:(Vue 异步更新源码解析)