Code前端首页关于Code前端联系我们

Vue nextTick实现原理

terry 2年前 (2023-09-08) 阅读数 195 #Vue

前言

由于 Vue 是异步更新 DOM 的,如果数据改变后直接执行某些动作,而此时 DOM 尚未更新,则会立即执行。 Vue提供了nextTick,数据改变后立即调用。可以保证更新完成后传入的回调函数会被执行。

然后按照两个问题来探究nextTick的原理

  • 调用 nextTick 时会发生什么?
  • 如何检测DOM更新完成?

下一个Tick

全局API Vue.nextTick和实例方法vm.$nextTick实际上在内部调用了nextTick函数。 nextTick中的逻辑非常简单。它的作用是将传入的回调函数压入callbacks队列(如果没有回调且支持Promise,则传入一个解决方案)然后判断pending为假并调用timerFunc。这就对了。

JavaScript
const callbacks = []
let pending = false

export function nextTick (cb, ctx) {
  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()
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
 

本段代码来自 https://www.codeqd.com/post/202309211.html

可见timerFunc是决定何时执行回调函数的关键。 pending表示当前是否有异步任务正在执行。如果没有,请立即调用timerFunc并将pending更改为true,以防止重复调用导致进程死锁。

调用 timerFunc 时,会创建一个异步任务。当异步任务完成后,会执行回调队列中的函数,并将pending设置为false,等待下一个nextTick被调用。当 timerFunc 结束之前,重复调用 nextTick 只会触发执行一次。

如何在Vue中实现异步任务

看到这里,我迫不及待地想知道timerFunc是如何实现的,回调函数什么时候会被执行。别担心,你需要先了解一些必备知识,即浏览器Event Loop

事件循环事件循环

我们知道js是单线程的,同步任务会顺序执行。我们还知道,带有事件监听回调、setTimeout、Promise 等的异步任务不会立即执行。那么js引擎是如何决定这些异步任务何时被调用的呢?

消息队列

Javascript 运行时包含一个用于处理待处理消息的消息队列。当绑定到事件侦听器的事件被触发或添加 setTimeout 回调时,一条消息将被添加到队列中并等待。被处理。

任务队列

每条消息都有一个与其关联的回调函数,该回调函数被放置在任务队列中。每次都会从消息队列的头部开始处理消息,并执行关联的回调函数,直到回调函数执行完毕。 。

任务和微任务

如上所述,(宏)任务附加到消息中。消息处理完成后,就会执行相应的任务,比如事件触发的回调、使用setTimeout添加的任务等。

微任务独立存储在微任务队列中。当(宏)任务开始执行时,新添加的微任务将被添加到微任务队列中。任务执行完成后,下一次迭代之前,微任务将依次执行,直到微任务队列为空。如果不断添加微任务,则处理将继续,直到微任务队列为空。因此,需要防止重复添加微任务而阻塞进程。

(宏)任务:由事件监听器、setTimeout、setInterval 触发的回调
微任务:Promise、queueMicrotask、MutationObserver

事件循环概述

结合以上三个概念,可以总结出这些步骤

  1. 当消息添加到消息队列时,其对应的任务也会添加到任务队列中。当js引擎空闲时,它会取出一条消息并开始处理。后面的消息必须等待前面的消息执行完成才能处理
  2. 当消息被处理时,其对应的任务也会从任务队列中移除。在此期间,新添加的微任务,例如创建质押,将会被添加到微任务队列中
  3. 等到任务执行完成,会依次执行微任务,直到微任务队列为空
  4. 开始处理下一条消息,重复步骤123,这是事件循环

定时器功能原理

了解了什么是事件循环后,我们再来看看timerFunc的原理以及nextTick回调函数的执行时间。

从源码中可以看到Vue在创建异步任务时做了很多兼容处理。我们尝试使用 PromiseMutationObserversetImmediatesetTimeout 来创建异步任务

JavaScript
let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // ...
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // ...
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // ...
} else {
  // ...
}
 

本段代码来自 https://www.codeqd.com/post/202309211.html

答应.然后

首先确定Promise是否原生支持。 Native Promise.then 是一个比(宏)任务优先级更高的微任务。调用timerFunc会添加一个微任务,并等到DOM更新完成后才会开始下一个事件循环flushCallbacks将被执行

JavaScript
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // iOS 中奇怪的bug,微任务入列了却没有刷新,直到浏览器需要处理其他一些工作,如定时器,这里用来强制刷新队列
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
}
 

本段代码来自 https://www.codeqd.com/post/202309211.html

突变观察者

MutationObserver 接收回调函数,使用 new 关键字创建并返回一个实例对象,当指定的 DOM 更改时将调用该实例对象。

MutationObserver虽然指定了DOM改变时触发,但只是添加了一个微任务,并不会立即执行。相反,直到所有 DOM 更新完成后才会执行。因此,Vue在这里只需要创建一个结构体即可。点击更改内容即可监控。

这里非常巧妙地使用counter = (counter + 1) % 2 来使计数器在 0/1 之间切换。调用timerFunc将会改变textNode的内容。等待监听的textNode发生变化,就会添加一个微任务。当 DOM 更新完成时,调用。

JavaScript
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
 

本段代码来自 https://www.codeqd.com/post/202309211.html

立即设置

此方法用于将一些长时间运行的操作放入回调函数中。该回调函数将在浏览器完成其他后续语句后立即执行。

仅 IE10+ 支持,是一个(宏)任务

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

本段代码来自 https://www.codeqd.com/post/202309211.html

设置超时

是另一种选择。如果以上都不支持,则使用setTimeout创建异步任务,这是一个(宏)任务

JavaScript
timerFunc = () => {
  setTimeout(flushCallbacks, 0)
}
 

本段代码来自 https://www.codeqd.com/post/202309211.html

flushCallbacks

V 上面可以看到,Vue 尝试使用 Promise.thenMutationObserversetImmediatesetTimeout 来创建异步任务, 将在 DOM 完成后执行。

flushCallbacks先将pending改为false,等待下一次调用timerFunc,然后执行回调函数队列,将回调函数一一取出来执行。

JavaScript
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
 

本段代码来自 https://www.codeqd.com/post/202309211.html

总结

由于Vue异步更新DOM,nextTick必须维护一个回调函数队列并等待正确的时间来执行回调函数。这次使用了事件循环机制。当Vue进行异步更新时,Vue更新完成后会处理新增的异步任务,并依次执行回调。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门