由Event Loop理解vue的nextTick(附源码)

156 阅读4分钟

本文主要讲述vue的异步更新队列(nextTick) 想要了解nextTick,让我们先来简单了解一下JS 运行机制(Event Loop)

JS 运行机制(Event Loop)

先来一张眼熟的事件循环的经典图

大家都知道js是单线程、非阻塞的脚本语言,那么到底是怎么回事呢??

Javascript 只有一个 main thread 主线程和 call-stack调用栈(执行栈),js解析⽅法时,将其中的同步任务按照执⾏顺序排队到调用栈中,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。所有的异步任务放到浏览器所支持的Web Apis中处理,异步任务返回结果之后,将异步事件按照顺序排队到任务队列中等待主线程空闲的时候(前提是调用栈被清空),被读取到栈内等待主线程的执行。

事件循环大致分为以下几个步骤:

(1)所有同步任务都在主线程上执行,形成一个执行栈。

(2)主线程之外,还存在一个任务队列。异步任务经过Web Apis处理有了结果,就依次排列到任务队列等待执行。

(3)一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,任务队列结束等待,进入执行栈,开始执行。

(4)主线程不断重复上面的第3步。

异步任务分为宏任务和微任务

本文只列出接下来在nextTick源码中会用到的异步任务

  • 宏任务(macro task):setTimeout,setImmediate
  • 微任务(micro task):Promise.then,MutationObsever

异步任务执行的顺序

Event Loop小总结

浏览器环境中执行方法时,先将执行栈中的任务清空,再将排列在任务队列的微任务推到执行栈中并清空,之后检查任务队列是否存在宏任务,若存在则取出一个宏任务,执行完成检查是否有微任务,以此循环…

说了这么多,讲道理应该有个小栗子

想必聪明的你已猜到执行结果,没错 打印了 1,3,complete,5,setTimeout 查看效果请点击

异步更新队列解析(nextTick)

罗嗦了这么半天,本文的主角(nextTick)该登场了,当当当...

我们都知道Vue在更新 DOM 时是异步执行的,那么到底之怎么回事呢?

官方解释: 只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

大概理解就是,DOM的更新应该是在当前线程都执行完在更新,所以就不会出现修改数据DOM立即更新的情况,应该在下一个'tick'中,vue刷新队列,更新DOM。

emmmm... 可能会有点疑惑,下面会一点一点解释一下

先大致说一下数据更新后触发的事件:

当我们在组件中对响应的数据做了修改,就会触发 set方法,最后调用 dep.notify() 方法,notify方法触发update()方法,系统收到更新指示就会触发queueWatcher()方法,queueWatcher方法会将get方法所收集的watcher推到一个异步队列,然后在 nextTick 后执行 flushSchedulerQueue()进行watcher的视图更新。

呈上nextTick源码,解释备注了哦(2.6+)

/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
//重置pending,复制一份callback然后将callback重置,遍历执行所有的回调。
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

if (typeof Promise !== 'undefined' && isNative(Promise)) {
//眼熟的微任务:
//promise.then,MutationObsever
//宏任务:
//setImmediate,setTimeout

//没错这部分是判断浏览器的兼容性的来定义timerFunc()方法的,先走微任务Promise.then,不支持的话走微任务MutationObserver,在不支持走接下来宏任务setImmediate,setTimeout。

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

export function nextTick (cb?: Function, ctx?: Object) {
//nextTick接收传过来的回调函数,然后将回调函数push到一个数组里面,如果没有cb,就会走下面的promise对象。
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
  //timerFunc()只执行一次
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

如果看的有点乱,我就再来个小总结吧

nextTick的逻辑大概是这样的:

nextTick把传入的回调函数 cb 压入 callbacks 数组,最后一次性地根据 isUsingMicroTask条件执行 timerFunc,然后在下一个 tick 执行 flushCallbacks,flushCallbacks 对 callbacks 遍历,然后执行相应的回调函数,最后完成watcher的视图更新。 这里使用 callbacks 而不是直接在 nextTick 中执行回调函数的原因是保证在同一个 'tick' 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。

this.$nextTick方法

看到这是不是突然想起来眼熟的this.$nextTick

在那么来结合下面的小栗子在理解一下nextTick和手动的

可见同步打印的DOM数据并没有改变,nextTick()的打印的DOM数据是我们刚刚更新的,所以,为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。

注:数据更新后想要立即获取操作DOM才会用到this.$nextTick(),不要乱用哦!!