都会用 nextTick,也都晓得 nextTick 作用是在下次 DOM 更新循环完结之后,执行提早回调,就能够拿到更新后的 DOM 相干信息

那么它到底是怎么实现的呢,在 Vue2 和 Vue3 中又有什么区别呢?本文将联合案例介绍执行原理再深刻源码,全副正文,包你一看就会

在进入 nextTick 实现原理之前先略微回顾一下 JS 的执行机制,因为这与 nextTick 的实现非亲非故

JS 执行机制

咱们都晓得 JS 是单线程的,一次只能干一件事,即同步,就是说所有的工作都须要排队,前面的工作须要等后面的工作执行完能力执行,如果后面的工作耗时过长,前面的工作就须要始终等,这是十分影响用户体验的,所以才呈现了异步的概念

同步工作:指排队在主线程上顺次执行的工作
异步工作:不进入主线程,而进入工作队列的工作,又分为宏工作和微工作
宏工作: 渲染事件、申请、script、setTimeout、setInterval、Node中的setImmediate 等
微工作: Promise.then、MutationObserver(监听DOM)、Node 中的 Process.nextTick等

当执行栈中的同步工作执行完后,就会去工作队列中拿一个宏工作放到执行栈中执行,执行完该宏工作中的所有微工作,再到工作队列中拿宏工作,即一个宏工作、所有微工作、渲染、一个宏工作、所有微工作、渲染...(不是所有微工作之后都会执行渲染),如此造成循环,即事件循环(EventLoop)

nextTick 就是创立一个异步工作,那么它天然要等到同步工作执行实现后才执行

咱们先联合例子弄懂执行原理,再深刻源码

Vue2

nextTick 用法

看例子,比方当 DOM 内容扭转后,咱们须要获取最新的高度

<template>  <div>{{ name }}</div></template><script>export default {  data() {    return {      name: ""    }  },  mounted() {    console.log(this.$el.clientHeight) // 0    this.name = "沐华"    console.log(this.$el.clientHeight) // 0    this.$nextTick(() => {      console.log(this.$el.clientHeight) // 18    });  }};</script>

为什么在 nextTick 里就能拿到最新的 DOM 相干信息?是怎么拿到的,咱们来剖析一下原理

原理剖析

在执行 this.name = '沐华' 的时候,就会触发 Watcher 更新,watcher 会把本人放到一个队列

用队列的起因是比方多个数据变更就更新视图屡次的话,性能上就不好了,所以对视图更新做一个异步更新的队列,防止反复计算和不必要的DOM操作,在下一轮事件循环的时候刷新队列,并执行已去重的工作(nextTick的回调函数),更新视图

而后调用 nextTick(),响应式派发更新的源码在这一块是这样的,地址:src/core/observer/scheduler.js - 164行

export function queueWatcher (watcher: Watcher) {  ...  // 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用  nextTick(flushSchedulerQueue)}

这里参数 flushSchedulerQueue 办法就会被放入事件循环,主线程工作的行完后就会执行这个函数,对 watcher 队列排序、遍历、执行 watcher 对应的 run 办法,而后 render,更新视图

也就是说 this.name = '沐华' 的时候,工作队列能够简略了解成这样 [flushSchedulerQueue]

而后下一行 console.log(...),因为会更新视图的工作 flushSchedulerQueue 在工作队列里没有执行,所以无奈拿到更新后的视图

而后执行到 this.$nextTick(fn) 的时候,增加一个异步工作,这时的工作队列能够简略了解成这样 [flushSchedulerQueue, fn]

而后同步工作就执行完了,接着按程序执行工作队列里的工作,第一个工作执行就会更新视图,前面天然能失去更新后的视图了

nextTick 源码分析

源码版本:2.6.14,源码地址:src/core/util/next-tick.js

这里整个源码分为两局部,一是判断以后环境能应用的最合适的 API 并保留异步函数,二是调用异步函数 执行回调队列

环境判断

次要是判断用哪个宏工作或微工作,因为宏工作消耗的工夫是大于微工作的,所以成先应用微工作,判断程序如下

  • Promise
  • MutationObserver
  • setImmediate
  • setTimeout
export let isUsingMicroTask = false // 是否启用微工作开关const callbacks = [] // 回调队列let pending = false // 异步控制开关,标记是否正在执行回调函数// 该办法负责执行队列中的全副回调function flushCallbacks () {  // 重置异步开关  pending = false  // 避免nextTick里有nextTick呈现的问题  // 所以执行之前先备份并清空回调队列  const copies = callbacks.slice(0)  callbacks.length = 0  // 执行工作队列  for (let i = 0; i < copies.length; i++) {    copies[i]()  }}let timerFunc // 用来保留调用异步工作办法// 判断以后环境是否反对原生 Promiseif (typeof Promise !== 'undefined' && isNative(Promise)) {  // 保留一个异步工作  const p = Promise.resolve()  timerFunc = () => {    // 执行回调函数    p.then(flushCallbacks)    // ios 中可能会呈现一个回调被推入微工作队列,然而队列没有刷新的状况    // 所以用一个空的计时器来强制刷新工作队列    if (isIOS) setTimeout(noop)  }  isUsingMicroTask = true} else if (!isIE && typeof MutationObserver !== 'undefined' && (  isNative(MutationObserver) ||  MutationObserver.toString() === '[object MutationObserverConstructor]')) {  // 不反对 Promise 的话,在反对MutationObserver的非 IE 环境下  // 如 PhantomJS, iOS7, Android 4.4  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)  }}

环境判断完结就会失去一个提早回调函数 timerFunc

而后进入外围的 nextTick

nextTick()

咱们用 Vue.nextTick() 或者 this.$nextTick() 都是调用 nextTick() 这个办法

这里代码不多,次要逻辑就是:

  • 把传入的回调函数放进回调队列 callbacks
  • 执行保留的异步工作 timeFunc,就会遍历 callbacks 执行相应的回调函数了
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()  }  // 如果没有提供回调,并且反对 Promise,就返回一个 Promise  if (!cb && typeof Promise !== 'undefined') {    return new Promise(resolve => {      _resolve = resolve    })  }}

能够看到最初有返回一个 Promise 是能够让咱们在不传参的时候用的,如下

this.$nextTick().then(()=>{ ... })

Vue3

nextTick 用法

先看个例子,点击按钮更新 DOM 内容,并获取最新的 DOM 内容

 <template>     <div ref="test">{{name}}</div>     <el-button @click="handleClick">按钮</el-button> </template> <script setup>     import { ref, nextTick } from 'vue'     const name = ref("沐华")     const test = ref(null)     async function handleClick(){         name.value = '掘金'         console.log(test.value.innerText) // 沐华         await nextTick()         console.log(test.value.innerText) // 掘金     }     return { name, test, handleClick } </script>

Vue3 里这一块有大改,不过事件循环的原理还是一样,只是加了几个专门保护队列的办法,以及关联到 effect,不过好在这里源码的代码不多,所以不如间接看源码会更容易了解

nextTick 源码分析

源码版本:3.2.11,源码地址:packages/runtime-core/src/sheduler.ts

const resolvedPromise: Promise<any> = Promise.resolve()let currentFlushPromise: Promise<void> | null = nullexport function nextTick<T = void>(this: T, fn?: (this: T) => void): Promise<void> {  const p = currentFlushPromise || resolvedPromise  return fn ? p.then(this ? fn.bind(this) : fn) : p}

就一个 Promise,没了

就这!!!

好吧,认真点

能够看出 nextTick 承受一个函数为参数,同时会创立一个微工作

在咱们页面调用 nextTick 的时候,会执行该函数,把咱们的参数 fn 赋值给 p.then(fn),在队列的工作实现后,fn 就执行了

因为加了几个保护队列的办法,所以执行程序是这样的:

queueJob -> queueFlush -> flushJobs -> nextTick参数的 fn

当初不晓得都是干嘛的不要紧,几分钟后你就会分明了

咱们按程序来,先看一下入口函数 queueJob 是在哪里调用的,看代码

// packages/runtime-core/src/renderer.ts - 1555行function baseCreateRenderer(){  const setupRenderEffect: SetupRenderEffectFn = (...) => {    const effect = new ReactiveEffect(      componentUpdateFn,      () => queueJob(instance.update), // 当作参数传入      instance.scope    )  }}

ReactiveEffect 这边接管过去的形参就是 scheduler,最终被用到了上面这里,看过响应式源码的这里就相熟了,就是派发更新的中央

// packages/reactivity/src/effect.ts - 330行export function triggerEffects(  ...  if (effect.scheduler) {    effect.scheduler()  } else {    effect.run()  }}

而后是 queueJob 外面干了什么?咱们一个一个的来

queueJob()

该办法负责保护主工作队列,承受一个函数作为参数,为待入队工作,会将参数 pushqueue 队列中,有唯一性判断。会在以后宏工作执行完结后,清空队列

const queue: SchedulerJob[] = []export function queueJob(job: SchedulerJob) {  // 主工作队列为空 或者 有正在执行的工作且没有在主工作队列中  && job 不能和以后正在执行工作及前面待执行工作雷同  if ((!queue.length ||      !queue.includes( job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex )      ) && job !== currentPreFlushParentJob  ) {    // 能够入队就增加到主工作队列    if (job.id == null) {      queue.push(job)    } else {      // 否则就删除      queue.splice(findInsertionIndex(job.id), 0, job)    }    // 创立微工作    queueFlush()  }}

queueFlush()

该办法负责尝试创立微工作,期待工作队列执行

let isFlushing = false // 是否正在执行let isFlushPending = false // 是否正在期待执行const resolvedPromise: Promise<any> = Promise.resolve() // 微工作创立器let currentFlushPromise: Promise<void> | null = null // 当前任务function queueFlush() {  // 以后没有微工作  if (!isFlushing && !isFlushPending) {    // 防止在事件循环周期内屡次创立新的微工作    isFlushPending = true    // 创立微工作,把 flushJobs 推入工作队列期待执行    currentFlushPromise = resolvedPromise.then(flushJobs)  }}

flushJobs()

该办法负责解决队列工作,次要逻辑如下:

  • 先解决前置工作队列
  • 依据 Id 排队队列
  • 遍历执行队列工作
  • 执行结束后清空并重置队列
  • 执行后置队列工作
  • 如果还有就递归继续执行
function flushJobs(seen?: CountMap) {  isFlushPending = false // 是否正在期待执行  isFlushing = true // 正在执行  if (__DEV__) seen = seen || new Map() // 开发环境下  flushPreFlushCbs(seen) // 执行前置工作队列  // 依据 id 排序队列,以确保  // 1. 从父到子,因为父级总是在子级后面先创立  // 2. 如果父组件更新期间卸载了组件,就能够跳过  queue.sort((a, b) => getId(a) - getId(b))  try {    // 遍历主工作队列,批量执行更新工作    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {      const job = queue[flushIndex]      if (job && job.active !== false) {        if (__DEV__ && checkRecursiveUpdates(seen!, job)) {          continue        }        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)      }    }  } finally {    flushIndex = 0 // 队列工作执行完,重置队列索引    queue.length = 0 // 清空队列    flushPostFlushCbs(seen) // 执行后置队列工作    isFlushing = false  // 重置队列执行状态    currentFlushPromise = null // 重置以后微工作为 Null    // 如果主工作队列、前置和后置工作队列还有没被清空,就持续递归执行    if ( queue.length || pendingPreFlushCbs.length || pendingPostFlushCbs.length ) {      flushJobs(seen)    }  }}

flushPreFlushCbs()

该办法负责执行前置工作队列,阐明都写在正文里了

export function flushPreFlushCbs( seen?: CountMap, parentJob: SchedulerJob | null = null) {  // 如果待处理的队列不为空  if (pendingPreFlushCbs.length) {    currentPreFlushParentJob = parentJob    // 保留队列中去重后的工作为以后流动的队列    activePreFlushCbs = [...new Set(pendingPreFlushCbs)]    // 清空队列    pendingPreFlushCbs.length = 0    // 开发环境下    if (__DEV__) { seen = seen || new Map() }    // 遍历执行队列里的工作    for ( preFlushIndex = 0; preFlushIndex < activePreFlushCbs.length; preFlushIndex+ ) {      // 开发环境下      if ( __DEV__ && checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])) {        continue      }      activePreFlushCbs[preFlushIndex]()    }    // 清空以后流动的工作队列    activePreFlushCbs = null    preFlushIndex = 0    currentPreFlushParentJob = null    // 递归执行,直到清空前置工作队列,再往下执行异步更新队列工作    flushPreFlushCbs(seen, parentJob)  }}

flushPostFlushCbs()

该办法负责执行后置工作队列,阐明都写在正文里了

let activePostFlushCbs: SchedulerJob[] | null = nullexport function flushPostFlushCbs(seen?: CountMap) {  // 如果待处理的队列不为空  if (pendingPostFlushCbs.length) {    // 保留队列中去重后的工作    const deduped = [...new Set(pendingPostFlushCbs)]    // 清空队列    pendingPostFlushCbs.length = 0    // 如果以后曾经有流动的队列,就增加到执行队列的开端,并返回    if (activePostFlushCbs) {      activePostFlushCbs.push(...deduped)      return    }    // 赋值为以后流动队列    activePostFlushCbs = deduped    // 开发环境下    if (__DEV__) seen = seen || new Map()    // 排队队列    activePostFlushCbs.sort((a, b) => getId(a) - getId(b))    // 遍历执行队列里的工作    for ( postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++ ) {      if ( __DEV__ && checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])) {        continue      }      activePostFlushCbs[postFlushIndex]()    }    // 清空以后流动的工作队列    activePostFlushCbs = null    postFlushIndex = 0  }}

整个 nextTick 的源码到这就解析完啦

往期精彩

  • 12 个 Vue 开发中的性能优化小技巧
  • Vue3.2 响应式原理源码分析,及与 Vue2 .x响应式的区别
  • 深入浅出 Vue2 响应式原理源码分析
  • 深入浅出虚构 DOM 和 Diff 算法,及 Vue2 与 Vue3 中的区别
  • Vue3的7种和Vue2的12种组件通信,值得珍藏
  • JavaScript进阶知识点
  • 前端异样监控和容灾
  • 20分钟助你拿下HTTP和HTTPS,坚固你的HTTP常识体系

结语

如果本文对你有一丁点帮忙,点个赞反对一下吧,感激感激