这篇文章主要讲一下nextTick()
的使用,event loop,和vue中nextTick()
的原理,以及在使用nextTick()
的时候踩到的坑。作为我学习的记录。
首先,nextTick()
的用法有两种:
- Vue.nextTick([callback, context])
- vm.$nextTick([callback])
两个方法的作用都是在DOM更新循环结束之后执行延迟回调。当我们改变了数据的时候,DOM的渲染需要时间,然而我们希望去操作DOM元素,就需要等待渲染完成后再去操作。就需要用到nextTick
,将等待DOM渲染完成后需要的操作放在回调函数里。
不同的是,Vue.nextTick([callback, context])
是全局的,使用vm.$nextTick([callback])
时的回调会自动绑定到调用它的实例上。而这里文档中并没有说明全局的Vue.nextTick([callback, context])
的context
参数是用来做什么的,后面我将通过源码的分析告诉大家这个参数的用法。
好,现在大家应该都知道nextTick
是用来做什么的了。这个方法是怎么实现的呢?首先,需要理解一下Event loop。
Event loop
很多时候我们看到别人的代码里有这么一句setTimeout(fn, 0)
。额,作为前端小白的我,觉得这段代码很神奇。延时0毫秒,不就是不用延时么,为什么还要这么写一句呢?这里其实就是Event loop的知识点。
首先,JavaScript是一个单线程的语言。
也就是说,在特定的时间只能是特定的代码被执行,要等待上一步的代码执行完成后在执行下一段代码。那么问题来了,如果上一段代码的请求需要等待很长时间,那么后面的代码就得给我等着,用户也得给我等着。最终,用户就会关掉浏览器走人。那我们今天的表演就结束了,欢迎收看,下期再见。
呵呵,其实,JavaScript除了主线程以外,还有一个叫做任务队列的东东。他会把一些需要一定等待时间的操作,放进任务队列里。
JavaScript的执行依靠函数调用栈和任务队列。
首先我们弄懂栈和队列的区别:
栈是先进后出,后进先出。
队列则相反,是先进先出。
函数执行栈
我们的js代码从上到下的执行,当一个函数被执行的时候,都会有一个执行上下文,全局环境也有一个执行上下文,就是全局的上下文。JavaScript将以栈的形式来存储他们。每执行一个函数,就把它上下文存入栈。栈的最底层就是全局上下文,栈顶就是当前正在执行的函数。每当一个函数执行结束,他的执行上下文就从栈中被弹出,释放。最底层的全局上下文,在浏览器关闭的时候才被弹出。
任务队列
任务队列有两种:macro-task(task)和micro-task(job)
macro-task(task):
- setTimeout/setInterval
- setImmediate
- I/O操作
- UI rendering
micro-task(job):
- process.nextTick
- Promise
- MutationObserve
注意:以上的方法的回调函数会被分发到执行队列中,而他们自身会被直接执行,比如Promise
只有then()
会被加入到执行队列中,而Promise
本身会被直接执行。
JavaScript执行的机制是:首先执行调用栈中的函数,当调用栈中的执行上下文全部被弹出,只剩下全局上下文的时候,就开始执行job的执行队列,job的执行完以后就开始执行task的队列中的。先进入的先执行,后进入的后执行。无论是task还是job都是通过函数调用栈来执行。task执行完成一个,js代码会继续检查是否有job需要执行。就形成了task-job-task-job的循环(其实这里可以将第一次的函数调用栈也看成一个task)。这就形成了event loop.
好了,现在可以来看nextTick
的实现原理了
var nextTick = (function () {
// 这里存放的是回调函数的队列
var callbacks = [];
var pending = false;
var timerFunc;
//这个函数就是DOM更新后需要执行的
function nextTickHandler () {
pending = false;
//这里将回调函数copy给copies
var copies = callbacks.slice(0);
callbacks.length = 0;
//进行循环执行回调函数的队列
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
})()
vue用了三个方法来执行nextTickHandler
函数,分别是:
Promise
//当浏览器支持Promise的时候就是用Promise
p.then(nextTickHandler).catch(logError);
MutationObserver
//当浏览器支持MutationObserver的时候就是用MutationObserver
var observer = new MutationObserver(nextTickHandler);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
setTimeout
//当以上都不支持的时候就用setTimeout
setTimeout(nextTickHandler, 0);
那么Vue.nextTick([callback, context])的第二个参数是什么呢?来看下面的代码。
return function queueNextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
//看这里,其实是可以给cb指定一个对象环境,来改变cb中this的指向
if (cb) { cb.call(ctx); }
if (_resolve) { _resolve(ctx); }
});
if (!pending) {
pending = true;
timerFunc();
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
看到代码后,我开心的这么写道
Vue.nextTick(()=>{
this.text()
}, {
text(){
console.log('test')
}
})
结果报错了,这是为什么呢?
源码中使用的是if (cb) { cb.call(ctx) }
所以不能使用箭头函数,箭头函数的this
是固定的,是不可用apply
,call
,bind
来改变的。改成这样:
Vue.nextTick(function () {
this.text()
}, {
text(){
console.log('test')
}
})
OK