剖析$nextTick原理与特性

本文详细解析了Vue.js中的$nextTick方法,包括其作用、原理以及在数据变化后如何获取最新DOM。$nextTick用于在DOM更新后执行回调,通过异步更新队列确保在所有状态修改完成后一次性渲染。它优先使用微任务,如Promise,若不支持则降级至宏任务。此外,还介绍了$nextTick在无回调情况下的Promise处理以及完整的内部实现代码。
摘要由CSDN通过智能技术生成

1. $nextTick的作用是什么?

$nextTick的作用是将回调延迟到下次DOM更新周期之后执行。

new Vue({
    // ...
    methods:{
      example: function(){
          this.message = 'change';
          this.$nextTick(function(){
              // DOM更新了,可以拿到最新的DOM
          })
      }  
    }
})

2.为什么要在$nextTick中才能拿到数据改变后的最新DOM?

这时由于Vue.js采用了异步更新队列来进行更新DOM。我们知道Vue.js 2.0开始使用虚拟DOM进行渲染,变化侦测的通知只发送到组件,组件内用到的所有状态都会通知到同一个watcher,然后虚拟DOM会对整个组件进行对比并更新DOM。也就是说,如果在同一轮事件循环中有两个数据发生了变化,那么组件的watcher会受到两份通知,从而进行两次渲染。事实上,并不需要渲染两次,虚拟DOM会对整个组件进行渲染,所以只需要等所有状态都修改完毕后,一次性将整个组件的DOM渲染到最新即可。

要解决整个问题,Vue.js的实现方式是将受到通知的watcher实例添加到队列中缓存起来,并且在添加到队列之前检查其中是否已经存在相同的watcher,只有不存在时,才将watcher实例添加到队列中。然后在下一次事件循环中,Vue.js会让队列中的watcher触发渲染流程并清空队列。这样就可以即便在同一事件中有两个状态发生改变,watcher最后也执行一次渲染流程。

下次DOM更新周期的意思是下次微任务执行时更新DOM,而vm.$nextTick其实是将回调添加到微任务中(只有在特殊情况下才降级到宏任务),所以我们要在$nextTick中才能拿到数据改变后的最新DOM。

3. $nextTick的原理

3.1. 一次事件循环中多次调动$nextTick只会将回调添加到任务队列中一次

$nextTick将回调延迟到下次DOM更新周期之后执行,但是在一次事件循环中多次调动$nextTick,Vue只会将回到添加到任务队列中一次。vue中使用pending来判断是否已经将回到添加到任务队列中,

const callbacks = [] // 用来存储vm.$nextTick参数中提供的回调
let pending = false // 标记是否已经向任务队列中添加了一个任务(每当向任务队列中插入任务时,将pending设置为true,每当任务被执行时将pending设置为false)

// 被注册的任务
function flushCallbacks () {
  pending = false 
  const copies = callbacks.slice(0)
  callbacks.length = 0 // 清除callbacks
  for (let i = 0; i < copies.length; i++) {
    copies[i]() // 依次触发
  }
}

const p = Promise.resolve() // 微任务队列
timerFunc = () => {
    p.then(flushCallbacks)
}

export function nextTick (cb?: Function, ctx?: Object) {
  // 将回调函数添加到callbacks中
  callbacks.push(() => {
    if (cb) {
      cb.call(ctx)
  })
  // 只推一次(在一次事件循环中调用了两次nextTick,只有第一次才会将任务推进任务队列,第二次只会改变callbacks,因为中执行的是callbacks)
  if (!pending) {
    pending = true // 标记已经推进
    timerFunc()
  }
}

在上面的代码中我们可以看到,当第一次调用$nextTick时,我们会将回调函数注册到callbacks队列中,同时会调用timerFunc将flushCallbacks使用Promise微任务进行包装,这时候相当于将flushCallbacks添加到微任务任务队列中,同时将pending设置为true。在同一次事件循环中再次调用$nextTick时,pending为true,将不会再次执行timerFunc,也就是不会再将flushCallbacks添加到微任务任务队列中。但是回调函数会注册到callbacks队列中。由于callbacks是引用类型,所以这两次的回调函数在flushCallbacks中都会被一一触发。

3.2 $nextTick添加到任务队列的类型

在$nextTick优先使用微任务(Promise)进行包装,但是如果Promise不支持的情况下,将会降级使用宏任务(setImmediate、setTimeout)

let timerFunc 
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve() // 微任务队列
  timerFunc = () => {
    p.then(flushCallbacks)
  }
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 宏任务
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 宏任务
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

在上面的代码中我们可以看到Vue在$nextTick使用任务队列优先级。

3.3 使用$nextTick没有指定回调函数的情况

当使用$nextTick没有指定回调函数并且支持Promise时,$nextTick将会返回一个Promise,同时将当前实例传出去。

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
   * this.$nextTick().then((ctx)=>{
   *  // dom更新了
   * })
   */
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

在以上代码中,我们先定义_resolve,当没有传入回调函数并且Promise部位空的情况下,将返回一个Promise,同时将传入的回调函数赋值给_resolve,并将ctx(当前Vue.js实例)传出去。

3.4 完整代码

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

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

export let isUsingMicroTask = false // 是否使用微任务

const callbacks = [] // 用来存储vm.$nextTick参数中提供的回调
let pending = false // 标记是否已经向任务队列中添加了一个任务(每当向任务队列中插入任务时,将pending设置为true,每当任务被执行时将pending设置为false)

// 被注册的任务:将callbacks中的所有函数依次执行(一轮事件循环中flushCallbacks只会执行一次)
function flushCallbacks () {
  pending = false // 清除
  const copies = callbacks.slice(0)
  callbacks.length = 0 // 清除callbacks
  for (let i = 0; i < copies.length; i++) {
    copies[i]() // 依次触发
  }
}

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc // 作用是将flushCallbacks添加到异步任务队列中(微任务或宏任务)

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve() // 微任务队列
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 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)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  // 宏任务
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  // 宏任务
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 将回调函数添加到callbacks中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 只推一次(在一次事件循环中调用了两次nextTick,只有第一次才会将任务推进任务队列,第二次只会改变callbacks,因为中执行的是callbacks)
  if (!pending) {
    pending = true // 标记已经推进
    timerFunc()
  }
  /**
   * 如果没有提供回调且在支持Promise的环境中,则返回一个Promise
   * this.$nextTick().then((ctx)=>{
   *  // dom更新了
   * })
   */
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值