nextTick 作用 nextTick 接收一个回调函数作为参数,并将这个回调函数延迟到DOM更新后才执行;
使用场景 :想要操作 基于最新数据生成的DOM 时 ,就将这个操作放在 nextTick 的回调中;
为什么需要nextTick 因为 vue 采用的异步更新策略 ,当监听到数据发生变化的时候不会立即去更新DOM,而是开启一个任务队列,并缓存在同一事件循环中发生的所有数据变更。
这种做法带来的好处就是可以将多次数据更新合并成一次,减少操作DOM的次数 ,如果不采用这种方法,假设数据改变100次就要去更新100次DOM,而频繁的DOM更新是很耗性能的。举个例子,如果同一个 Watcher
被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
nextTick 实现原理 将传入的回调函数包装成异步任务 ,异步任务又分微任务和宏任务,为了尽快执行所以优先选择微任务 ;
nextTick 提供了四种异步方法 Promise.then、MutationObserver、setImmediate、setTimeout(fn,0)
源码解读 vue3 源码位置: CORE/packages/runtime-core/src/scheduler.ts
vue2 源码位置: src/core/util/next-tick.js
nextTick 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import { noop } from 'shared/util' import { handleError } from './error' import { isIE, isIOS, isNative } from './env' export let isUsingMicroTask = false const callbacks = [] let pending = false 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 () } if (!cb && typeof Promise !== 'undefined' ) { return new Promise (resolve => { _resolve = resolve }) } }
timerFunc 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 let timerFunc if (typeof Promise !== 'undefined' && isNative (Promise )) { 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 ) } }
timerFunc
函数就是用各种异步执行的方法调用 flushCallbacks
函数。
flushCallbacks 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function flushCallbacks ( ) { pending = false const copies = callbacks.slice (0 ) callbacks.length = 0 for (let i = 0 ; i < copies.length ; i++) { copies[i]() } }
==流程总结== 1.nextTick 会将传入的函数做一个包装然后放到 callbacks 数组里,这个包装主要是针对没传入回调以及回调错误捕获。
2.如果当前没有其它 nextTick 在执行的话,就会调用 timerFunc 函数,会根据环境,做一个兼容性判断,选择某一种异步执行的方法调用 flushCallbacks 函数。优先级是 promise.then > object.observe > setImmediate > setTimeout。(因为 timerFunc 是异步的,同时只能执行一个)
3.flushCallbacks 遍历 callbacks 执行其中每一个回调。这里注意,源码里对 callbacks 做了一个拷贝,因为执行回调的过程中,可能会触发新的 nextTick,会将新的回调 push 入 callbacks,这样可能就会一直循环下去。因此nextTick 回调中的 nextTick 应该放在下一轮执行。
参考 https://juejin.cn/post/7102750403418128391
https://juejin.cn/post/7087866362785169416