nextTick源码解读

个人主页:爱吃炫迈
系列专栏:Vue
‍座右铭:道阻且长,行则将至

文章目录

  • nextTick原理
    • nextTick
    • timerFunc
    • flushCallbacks
  • 异步更新流程
    • update
    • queueWatcher
    • flushSchedulerQueue
    • resetSchedulerState


nextTick原理

nextTick

export let isUsingMicroTask = false // 标记 nextTick 最终是否以微任务执行
/*存放异步执行的回调*/
const callbacks = [] 
/*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
let pending = false
/*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
let timerFunc

/*
  推送到队列中下一个tick时执行
  cb 回调函数
  ctx 上下文
*/
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
   // 第一步 传入的cb会被push进callbacks中存放起来
  callbacks.push(() => {
    if (cb) {      
        try {
            cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })

  // 第二步:判断用什么方法
  // 检查上一个异步任务队列(即名为callbacks的任务数组)是否派发和执行完毕了。pending此处相当于一个锁
  if (!pending) {
  // 若上一个异步任务队列已经执行完毕,则将pending设定为true(把锁锁上)
    pending = true
    // 调用判断Promise,MutationObserver,setTimeout的优先级
    timerFunc()
  }

  // 第三步:nextTick 函数会返回一个Promise对象。该Promise对象在异步任务执行完毕后会resolve,可以让用户在异步任务执行完毕后进行处理。
  if (!cb && typeof Promise !== 'undefined') {   
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

解释

第二步pending 的作用就是一个锁,防止后续的 nextTick 重复执行 timerFunc(换句话说:当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次)。timerFunc 内部创建会一个微任务或宏任务,等待所有的 nextTick 同步执行完成后,再去执行 callbacks 内的回调。

timerFunc

timerFunc函数,主要通过一些兼容判断来创建合适的 timerFunc,最优先肯定是微任务,其次再到宏任务。 优先级为 promise.then > MutationObserver > setImmediate > setTimeout

// 判断当前环境是否原生支持 promise
if (typeof Promise !== 'undefined' && isNative(Promise)) { // 支持 promise
  const p = Promise.resolve()
  timerFunc = () => {
    // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  // 标记当前 nextTick 使用的微任务
  isUsingMicroTask = true

  // 如果不支持 promise,就判断是否支持 MutationObserver
} 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 // 标记当前 nextTick 使用的微任务

   // 判断当前环境是否原生支持 setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {

  timerFunc = () => {
    setImmediate(flushCallbacks)
  }

     // 以上三种都不支持就选择 setTimeout
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

我们发现无论那种timerFunc 最终都会执行flushCallbacks 函数

flushCallbacks

flushCallbacks 里做的事情很简单,它就负责执行 callbacks 里的回调。

// flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0) // 拷贝一份 callbacks
  callbacks.length = 0 // 清空 callbacks
  for (let i = 0; i < copies.length; i++) { // 遍历执行传入的回调
    copies[i]()
  }
}

nextTick源码解读_第1张图片

异步更新流程

Vue 使用异步更新,等待所有数据同步修改完成后,再去执行更新逻辑。

update


触发某个数据的setter方法后,它的setter函数会通知闭包中的Dep,Dep则会调用它管理的所有Watch对象。触发Watch对象的update实现。

/*调度者接口,当依赖发生改变的时候进行回调 */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      /*同步则执行run直接渲染视图*/
      this.run()
    } else {
      /*异步推送到观察者队列中,下一个tick时调用。*/
      queueWatcher(this) // this为当前实例watcher
    }
  }

queueWatcher

将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送

export function queueWatcher (watcher: Watcher) {
  /*获取watcher的id*/
  const id = watcher.id
  /*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
  if (has[id] == null) {
    has[id] = true

    // 不是刷新
    if (!flushing) {
      queue.push(watcher)  // 将多个渲染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 >= 0 && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(Math.max(i, index) + 1, 0, watcher)
    }

    // 是刷新
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue) //这里会产生一个nextTick,队列刷新函数(flushSchedulerQueue)
    }
  }
}

从queueWatcher代码中看出Watch对象并不是立即更新视图,而是被push进了一个队列queue,此时状态处于waiting的状态,这时候会继续会有Watch对象被push进这个队列queue,等到下一个tick运行时将这个队列queue全部拿出来run一遍,这些Watch对象才会被遍历取出,更新视图。同时,id重复的Watcher不会被多次加入到queue中去。这也解释了同一个watcher被多次触发,只会被推入到队列中一次。

flushSchedulerQueue

flushSchedulerQueue 内将刚刚加入 queuewatcher 逐个 run 更新。

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // 在刷新之前对队列进行排序。
  // 这确保了:
  // 1. 组件从父级更新到子级。(因为父母总是在子进程之前创建)
  // 2. 组件的用户观察程序在其渲染观察程序之前运行(因为用户观察者是在渲染观察者之前创建的)
  // 3. 如果组件在父组件的观察程序运行期间被销毁,可以跳过它的观察者。
  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()
  }

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

resetSchedulerState

resetSchedulerState 重置状态,等待下一轮的异步更新。

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

要注意此时 flushSchedulerQueue 还未执行,它只是作为回调传入而已。因为用户可能也会调用 nextTick 方法。这种情况下,callbacks 里的内容为 [“flushSchedulerQueue”, “用户的nextTick回调”],当所有同步任务执行完成,才开始执行 callbacks 里面的回调。

由此可见,最先执行的是页面更新的逻辑,其次再到用户的 nextTick 回调执行。这也是为什么我们能在 nextTick 中获取到更新后DOM的原因。

参考文章:

Vue你不得不知道的异步更新机制和nextTick原理 - 掘金

通俗易懂的Vue异步更新策略及 nextTick 原理 - 掘金

你可能感兴趣的:(vue,前端,javascript,html)