关于vue.js:Vue-源码解读4-异步更新

31次阅读

共计 8430 个字符,预计需要花费 22 分钟才能阅读完成。

当学习成为了习惯,常识也就变成了常识。 感激各位的 点赞 珍藏 评论

新视频和文章会第一工夫在微信公众号发送,欢送关注:李永宁 lyn

文章已收录到 github 仓库 liyongning/blog,欢送 Watch 和 Star。

前言

上一篇的 Vue 源码解读(3)—— 响应式原理 说到通过 Object.defineProperty 为对象的每个 key 设置 getter、setter,从而拦挡对数据的拜访和设置。

当对数据进行更新操作时,比方 obj.key = 'new val' 就会触发 setter 的拦挡,从而检测新值和旧值是否相等,如果相等什么也不做,如果不相等,则更新值,而后由 dep 告诉 watcher 进行更新。所以,异步更新 的入口点就是 setter 中最初调用的 dep.notify() 办法。

目标

  • 深刻了解 Vue 的异步更新机制
  • nextTick 的原理

源码解读

dep.notify

/src/core/observer/dep.js

对于 dep 更加具体的介绍请查看上一篇文章 —— Vue 源码解读(3)—— 响应式原理,这里就不占用篇幅了。

/**
 * 告诉 dep 中的所有 watcher,执行 watcher.update() 办法
 */
notify () {
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  // 遍历 dep 中存储的 watcher,执行 watcher.update()
  for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}
}

watcher.update

/src/core/observer/watcher.js

/**
 * 依据 watcher 配置项,决定接下来怎么走,个别是 queueWatcher
 */
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    // 懒执行时走这里,比方 computed
    // 将 dirty 置为 true,能够让 computedGetter 执行时从新计算 computed 回调函数的执行后果
    this.dirty = true
  } else if (this.sync) {
    // 同步执行,在应用 vm.$watch 或者 watch 选项时能够传一个 sync 选项,// 当为 true 时在数据更新时该 watcher 就不走异步更新队列,间接执行 this.run 
    // 办法进行更新
    // 这个属性在官网文档中没有呈现
    this.run()} else {
    // 更新时个别都这里,将 watcher 放入 watcher 队列
    queueWatcher(this)
  }
}

queueWatcher

/src/core/observer/scheduler.js

/**
 * 将 watcher 放入 watcher 队列 
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 如果 watcher 曾经存在,则跳过,不会反复入队
  if (has[id] == null) {
    // 缓存 watcher.id,用于判断 watcher 是否曾经入队
    has[id] = true
    if (!flushing) {
      // 以后没有处于刷新队列状态,watcher 间接入队
      queue.push(watcher)
    } else {
      // 曾经在刷新队列了
      // 从队列开端开始倒序遍历,依据以后 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 => vm.$nextTick、Vue.nextTick
       *   1、将 回调函数(flushSchedulerQueue)放入 callbacks 数组
       *   2、通过 pending 管制向浏览器工作队列中增加 flushCallbacks 函数
       */
      nextTick(flushSchedulerQueue)
    }
  }
}

nextTick

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

const callbacks = []
let pending = false

/**
 * 实现两件事:*   1、用 try catch 包装 flushSchedulerQueue 函数,而后将其放入 callbacks 数组
 *   2、如果 pending 为 false,示意当初浏览器的工作队列中没有 flushCallbacks 函数
 *     如果 pending 为 true,则示意浏览器的工作队列中曾经被放入了 flushCallbacks 函数,*     待执行 flushCallbacks 函数时,pending 会被再次置为 false,示意下一个 flushCallbacks 函数能够进入
 *     浏览器的工作队列了
 * pending 的作用:保障在同一时刻,浏览器的工作队列中只有一个 flushCallbacks 函数
 * @param {*} cb 接管一个回调函数 => flushSchedulerQueue
 * @param {*} ctx 上下文
 * @returns 
 */
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 用 callbacks 数组存储通过包装的 cb 函数
  callbacks.push(() => {if (cb) {
      // 用 try catch 包装回调函数,便于谬误捕捉
      try {cb.call(ctx)
      } catch (e) {handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {_resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 执行 timerFunc,在浏览器的工作队列中(首选微工作队列)放入 flushCallbacks 函数
    timerFunc()}
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {_resolve = resolve})
  }
}

timerFunc

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

// 能够看到 timerFunc 的作用很简略,就是将 flushCallbacks 函数放入浏览器的异步工作队列中
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {const p = Promise.resolve()
  // 首选 Promise.resolve().then()
  timerFunc = () => {
    // 在 微工作队列 中放入 flushCallbacks 函数
    p.then(flushCallbacks)
    /**
     * 在有问题的 UIWebViews 中,Promise.then 不会齐全中断,然而它可能会陷入怪异的状态,* 在这种状态下,回调被推入微工作队列,但队列没有被刷新,直到浏览器须要执行其余工作,例如解决一个计时器。* 因而,咱们能够通过增加空计时器来“强制”刷新微工作队列。*/
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // MutationObserver 次之
  // 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)) {
  // 再就是 setImmediate,它其实曾经是一个宏工作了,但依然比 setTimeout 要好
  timerFunc = () => {setImmediate(flushCallbacks)
  }
} else {
  // 最初没方法,则应用 setTimeout
  timerFunc = () => {setTimeout(flushCallbacks, 0)
  }
}

flushCallbacks

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

const callbacks = []
let pending = false

/**
 * 做了三件事:*   1、将 pending 置为 false
 *   2、清空 callbacks 数组
 *   3、执行 callbacks 数组中的每一个函数(比方 flushSchedulerQueue、用户调用 nextTick 传递的回调函数)*/
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  // 遍历 callbacks 数组,执行其中存储的每个 flushSchedulerQueue 函数
  for (let i = 0; i < copies.length; i++) {copies[i]()}
}

flushSchedulerQueue

/src/core/observer/scheduler.js

/**
 * Flush both queues and run the watchers.
 * 刷新队列,由 flushCallbacks 函数负责调用,次要做了如下两件事:*   1、更新 flushing 为 ture,示意正在刷新队列,在此期间往队列中 push 新的 watcher 时须要非凡解决(将其放在队列的适合地位)*   2、依照队列中的 watcher.id 从小到大排序,保障先创立的 watcher 先执行,也配合 第一步
 *   3、遍历 watcher 队列,顺次执行 watcher.before、watcher.run,并革除缓存的 watcher
 */
function flushSchedulerQueue () {currentFlushTimestamp = getNow()
  // 标记当初正在刷新队列
  flushing = true
  let watcher, id

  /**
   * 刷新队列之前先给队列排序(升序),能够保障:*   1、组件的更新程序为从父级到子级,因为父组件总是在子组件之前被创立
   *   2、一个组件的用户 watcher 在其渲染 watcher 之前被执行,因为用户 watcher 先于 渲染 watcher 创立
   *   3、如果一个组件在其父组件的 watcher 执行期间被销毁,则它的 watcher 能够被跳过
   * 排序当前在刷新队列期间新进来的 watcher 也会按程序放入队列的适合地位
   */
  queue.sort((a, b) => a.id - b.id)

  // 这里间接应用了 queue.length,动静计算队列的长度,没有缓存长度,是因为在执行现有 watcher 期间队列中可能会被 push 进新的 watcher
  for (index = 0; index < queue.length; index++) {watcher = queue[index]
    // 执行 before 钩子,在应用 vm.$watch 或者 watch 选项时能够通过配置项(options.before)传递
    if (watcher.before) {watcher.before()
    }
    // 将缓存的 watcher 革除
    id = watcher.id
    has[id] = null

    // 执行 watcher.run,最终触发更新函数,比方 updateComponent 或者 获取 this.xx(xx 为用户 watch 的第二个参数),当然第二个参数也有可能是一个函数,那就间接执行
    watcher.run()}

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  /**
   * 重置调度状态:*   1、重置 has 缓存对象,has = {}
   *   2、waiting = flushing = false,示意刷新队列完结
   *     waiting = flushing = false,示意能够像 callbacks 数组中放入新的 flushSchedulerQueue 函数,并且能够向浏览器的工作队列放入下一个 flushCallbacks 函数了
   */
  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {devtools.emit('flush')
  }
}

/**
 * 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.run

/src/core/observer/watcher.js

/**
 * 由 刷新队列函数 flushSchedulerQueue 调用,如果是同步 watch,则由 this.update 间接调用,实现如下几件事:*   1、执行实例化 watcher 传递的第二个参数,updateComponent 或者 获取 this.xx 的一个函数(parsePath 返回的函数)
 *   2、更新旧值为新值
 *   3、执行实例化 watcher 时传递的第三个参数,比方用户 watcher 的回调函数
 */
run () {if (this.active) {
    // 调用 this.get 办法
    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
    ) {
      // 更新旧值为新值
      const oldValue = this.value
      this.value = value

      if (this.user) {
        // 如果是用户 watcher,则执行用户传递的第三个参数 —— 回调函数,参数为 val 和 oldVal
        try {this.cb.call(this.vm, value, oldValue)
        } catch (e) {handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        // 渲染 watcher,this.cb = noop,一个空函数
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

watcher.get

/src/core/observer/watcher.js

  /**
   * 执行 this.getter,并从新收集依赖
   * this.getter 是实例化 watcher 时传递的第二个参数,一个函数或者字符串,比方:updateComponent 或者 parsePath 返回的函数
   * 为什么要从新收集依赖?*   因为触发更新阐明有响应式数据被更新了,然而被更新的数据尽管曾经通过 observe 察看了,然而却没有进行依赖收集,*   所以,在更新页面时,会从新执行一次 render 函数,执行期间会触发读取操作,这时候进行依赖收集
   */
  get () {
    // 关上 Dep.target,Dep.target = this
    pushTarget(this)
    // value 为回调函数执行的后果
    let value
    const vm = this.vm
    try {
      // 执行回调函数,比方 updateComponent,进入 patch 阶段
      value = this.getter.call(vm, vm)
    } catch (e) {if (this.user) {handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {throw e}
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {traverse(value)
      }
      // 敞开 Dep.target,Dep.target = null
      popTarget()
      this.cleanupDeps()}
    return value
  }

以上就是 Vue 异步更新机制的整个执行过程。

总结

  • 面试官 问:Vue 的异步更新机制是如何实现的?

    Vue 的异步更新机制的外围是利用了浏览器的异步工作队列来实现的,首选微工作队列,宏工作队列次之。

    当响应式数据更新后,会调用 dep.notify 办法,告诉 dep 中收集的 watcher 去执行 update 办法,watcher.update 将 watcher 本人放入一个 watcher 队列(全局的 queue 数组)。

    而后通过 nextTick 办法将一个刷新 watcher 队列的办法(flushSchedulerQueue)放入一个全局的 callbacks 数组中。

    如果此时浏览器的异步工作队列中没有一个叫 flushCallbacks 的函数,则执行 timerFunc 函数,将 flushCallbacks 函数放入异步工作队列。如果异步工作队列中曾经存在 flushCallbacks 函数,期待其执行实现当前再放入下一个 flushCallbacks 函数。

    flushCallbacks 函数负责执行 callbacks 数组中的所有 flushSchedulerQueue 函数。

    flushSchedulerQueue 函数负责刷新 watcher 队列,即执行 queue 数组中每一个 watcher 的 run 办法,从而进入更新阶段,比方执行组件更新函数或者执行用户 watch 的回调函数。

    残缺的执行过程其实就是明天源码浏览的过程。


面试关 问:Vue 的 nextTick API 是如何实现的?

Vue.nextTick 或者 vm.$nextTick 的原理其实很简略,就做了两件事:

  • 将传递的回调函数用 try catch 包裹而后放入 callbacks 数组
  • 执行 timerFunc 函数,在浏览器的异步工作队列放入一个刷新 callbacks 数组的函数

链接

  • 配套视频,关注微信公众号回复:” 精通 Vue 技术栈源码原理视频版 ” 获取
  • 精通 Vue 技术栈源码原理 专栏
  • github 仓库 liyongning/Vue 欢送 Star

感激各位的:点赞 珍藏 评论,咱们下期见。


当学习成为了习惯,常识也就变成了常识。 感激各位的 点赞 珍藏 评论

新视频和文章会第一工夫在微信公众号发送,欢送关注:李永宁 lyn

文章已收录到 github 仓库 liyongning/blog,欢送 Watch 和 Star。

正文完
 0