不知啥时候起,对nextTick
的理解出现了偏差,感觉变成了setTimeout
一样的延时操作了,好像就一定会在代码执行完成之后执行。有多少人和我一样,当需要等到dom
渲染完成之后进行js
操作时,就想着使用nextTick
。殊不知,离真相越来越远,现在,让我们来梳理一下它的真正用法。
先来看下官网的介绍:在下次
DOM
更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM
。敲黑板划重点,DOM
更新之后执行延迟回调,它确实是延迟回调,但是是涉及到DOM
更新之后才会执行的。我们先来欣赏一下nextTick的源码。
/**
* Defer a task to execute it asynchronously.
* 异步更新队列
*/
var nextTick = (function() {
var callbacks = [];
var pending = false; // 标记位,是否在执行回调函数
var timerFunc; // 延时操作
function nextTickHandler() {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
// 只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。
// 如果同一个 watcher 被多次触发,只会被推入到队列中一次。
// 这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。
// 然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
// Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
if (isIOS) setTimeout(noop)
}
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else {
// fallback to setTimeout
/* istanbul ignore next */
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
return function queueNextTick(cb, ctx) {
var _resolve;
callbacks.push(function() {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
}
})();
Vue.prototype.$nextTick = function(fn) {
return nextTick(fn, this)
};
从源码我们能得到下面这张图,主要的关键就在于timeFunc
方法中。
timeFunc
()一共有三种实现方式:
- promise
- MutationObserver
- setTimeout
promise
和setTimeout
很好理解,都是异步任务,在同步任务执行完以及更新DOM
的异步任务之后再执行。MutationObserver
则是h5
的新标准,用来监视DOM
变动的接口,它能监听一个DOM
对象上发生的子节点删除、属性修改、文本内容修改等等。
理解了源码,也就理解了官网的描述,我们来分析一下nextTick
的使用场景。
场景一:
生命周期created
里要进行dom
操作时需使用nextTick
。原因是created
时dom
还未渲染
场景二:
js
更改了视图后,想基于新的视图进行操作时,需要将操作代码放在nextTick
中
场景三:
使用其他插件时,希望在dom
动态改变后重新应用该插件,也需要在nextTick
中重新应用该插件。如使用better-scroll
组件,组件内部数据刷新了,高度变化了,这时我们需要在nextTick
中主动调用它的refresh
方法以重新计算高度并流畅滚动。
。。。
那么是不是nextTick
就是万能的呢?它一定会在dom
执行完成之后执行?也不一定。
比如下面这个场景:
当存在折叠面板时,我们经常会使用watch
去监听绑定的值,然后希望绑定的值改变时,动态去改变scroll
的高度,这时候我们想用nextTick
去实现在dom
更新完成之后触发refresh
,你会发现,不行。。。why
??? 黑人问号脸。。。
watch:{
activeName(newVal, oldVal){
console.log(document.getElementById('scroll').scrollHeight, '---')
this.$nextTick(() => {
console.log(document.getElementById('scroll').scrollHeight, '---')
console.log('nextTick', new Date().getTime())
this.$refs.scroll.refresh();
console.log(document.getElementById('scroll').scrollHeight, '===')
console.log('nextTick之后', new Date().getTime())
})
}
},
我们从打印的结果会发现,nextTick
里的scrollHeight
高度并不为真实高度。那么这是为什么呢?是nextTick
的问题,还是watch
的问题?我比较了一下,既不是watch
的问题,也不是nextTick
的问题,而是我们watch
的对象并不是真实dom
所关联的数据,它改变了,还要驱动着其他真正控制着dom
改变的数据,这时候,nextTick
就失效了。如果我们监听的是真正控制dom
的对象,我们会发现我们在nextTick
中得到的是准确的dom
渲染完成后的。那这时,要解决这种监听并不直接操作dom
的data
时,我们只能通过 setTimeout
来实现dom
渲染完成后的操作了,并且dom
渲染也需要一定的时间。
还有些关于nextTick
在父子组件间的更新机制,推荐看另外一篇文章,写的挺好。https://www.jianshu.com/p/cd299e4d0221