面试过程中被问到,但不是很清楚它的原理,本质就想考察js的event loop,以此记录
$nextTick的原理
为什么有它:
官方说法:Vue异步执行DOM更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个watcher多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作上非常重要,
简答:就是说vue不会对每次数据变化都重新渲染,比如连续的三次改变,就保留最后一次;尤雨溪是定义了vue是在本次事件循环的最后 进行一次DOM更新,我们知道获取DOM的操作是一个同步的,但vue的dom更新是异步的就会造成我们经常遇到的改了数据但页面上还是原来的旧值,是因为还没来得及重新渲染,那怎么办呢 ,我就想要拿到最新的dom,其实$nextTick并不是去改变vue进行dom更新的本质,而是把我们的请求放到dom更新完成后去了
定义:
官方:在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新之后的DOM
简单来说:就是你放在$nextTick
当中的操作不会立即执行,而是等数据更新、DOM更新完成之后再执行,这样我们拿到的肯定就是最新的了,总结一下:就是$nextTick
将回调函数放到微任务或者宏任务当中以延迟它地执行顺序
原理:
tip:能监听到DOM改动的API:MutationObserver;但vue并不是用MutationObserver来监听DOM是否更新完毕的
用队列控制的方式达到目的,把nextTick要执行的代码当作下一个task放入队列末尾,根据event loop的微任务都执行完才开始下一轮循环的宏任务,想在更新DOM的那个microtask的后面追加我们的回调函数就能立刻拿到dom更新了;
常见的microtask有:Promise、MutationObserver、Object.observe(废弃),以及nodejs中的 process.nextTick.
看到了MutationObserver,vue用MutationObserver是想利用它的microtask特性,而不是想做DOM 监听
降级策略:
事实上,vue在2.5版本中已经删去了 MutationObserver相关的代码,因为它是HTML5新增的特性,在iOS上尚有bug。
那么最优的microtask策略就是Promise了,而令人尴尬的是,Promise是ES6新增的东西,也存在兼容问题呀。所以vue就面临一个降级策略。
上面我们讲到了,队列控制的最佳选择是microtask(微任务),而microtask的最佳选择是Promise.但如果当前环境不支持Promise,vue就不得不降级为macrotask(宏任务)来做队列控制了。
macrotask有哪些可选的方案呢?前面提到了setTimeout是一种,但它不是理想的方案。因为 setTimeout执行的最小时间间隔是约4ms的样子,略微有点延迟。
在vue2.5的源码中,macrotask降级的方案依次是:setImmediate、MessageChannel、setTimeout. setImmediate是最理想的方案了,可惜的是只有IE和nodejs支持。
MessageChannel的onmessage回调也是microtask,但也是个新API,面临兼容性的尴尬。
所以最后的兜底方案就是setTimeout了,尽管它有执行延迟,可能造成多次渲染,算是没有办法的办法了。
源码
理解三个参数的含义:
- callback:我们要执行的操作,可以放在这个函数当中,我们没执行一次$nextTick就会把回调函数放到一个异步队列当中;
- pending:标识,用以判断在某个事件循环中是否为第一次加入,第一次加入的时候才触发异步执行的队列挂载
- timerFunc:用来触发执行回调函数,也就是Promise.then或MutationObserver或setImmediate
或setTimeout的过程
callbacks新增回调函数后又执行了timerFunc函数,pending是用来标识同一个时间只能执行一次
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
// cb 回调函数会经统一处理压入 callbacks 数组
callbacks.push(() => {
if (cb) {
// 给 cb 回调函数执行加上了 try-catch 错误处理
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
// 执行异步延迟函数 timerFunc
if (!pending) {
pending = true;
timerFunc();
}
// 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve;
});
}
}
timerFunc函数定义,这里是根据当前环境支持什么方法则确定调用哪个,分别有:
Promise.then、MutationObserver、setImmediate、setTimeout
通过上面任意一种方法,进行降级操作
export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//判断1:是否原生支持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]'
)) {
//判断2:是否原生支持MutationObserver
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)) {
//判断3:是否原生支持setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
//判断4:上面都不行,直接用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
无论是微任务还是宏任务,都会放到flushCallbacks使用
这里将callbacks里面的函数复制一份,同时callbacks置空
依次执行callbacks里面的函数
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}