這篇文章主要講一下nextTick()
的使用七冲,event loop亲配,和vue中nextTick()
的原理蔓涧,以及在使用nextTick()
的時候踩到的坑构灸。作為我學(xué)習(xí)的記錄上渴。
首先,nextTick()
的用法有兩種:
- Vue.nextTick([callback, context])
- vm.$nextTick([callback])
兩個方法的作用都是在DOM更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào)喜颁。當我們改變了數(shù)據(jù)的時候稠氮,DOM的渲染需要時間,然而我們希望去操作DOM元素半开,就需要等待渲染完成后再去操作隔披。就需要用到nextTick
,將等待DOM渲染完成后需要的操作放在回調(diào)函數(shù)里寂拆。
不同的是奢米,Vue.nextTick([callback, context])
是全局的抓韩,使用vm.$nextTick([callback])
時的回調(diào)會自動綁定到調(diào)用它的實例上。而這里文檔中并沒有說明全局的Vue.nextTick([callback, context])
的context
參數(shù)是用來做什么的鬓长,后面我將通過源碼的分析告訴大家這個參數(shù)的用法谒拴。
好,現(xiàn)在大家應(yīng)該都知道nextTick
是用來做什么的了涉波。這個方法是怎么實現(xiàn)的呢英上?首先,需要理解一下Event loop啤覆。
Event loop
很多時候我們看到別人的代碼里有這么一句setTimeout(fn, 0)
苍日。額,作為前端小白的我城侧,覺得這段代碼很神奇易遣。延時0毫秒彼妻,不就是不用延時么嫌佑,為什么還要這么寫一句呢?這里其實就是Event loop的知識點侨歉。
首先屋摇,JavaScript是一個單線程的語言。
也就是說幽邓,在特定的時間只能是特定的代碼被執(zhí)行炮温,要等待上一步的代碼執(zhí)行完成后在執(zhí)行下一段代碼。那么問題來了牵舵,如果上一段代碼的請求需要等待很長時間柒啤,那么后面的代碼就得給我等著,用戶也得給我等著畸颅。最終担巩,用戶就會關(guān)掉瀏覽器走人。那我們今天的表演就結(jié)束了没炒,歡迎收看涛癌,下期再見。
呵呵送火,其實拳话,JavaScript除了主線程以外,還有一個叫做任務(wù)隊列的東東种吸。他會把一些需要一定等待時間的操作弃衍,放進任務(wù)隊列里。
JavaScript的執(zhí)行依靠函數(shù)調(diào)用棧和任務(wù)隊列坚俗。
首先我們弄懂棧和隊列的區(qū)別:
棧是先進后出笨鸡,后進先出姜钳。
隊列則相反,是先進先出形耗。
函數(shù)執(zhí)行棧
我們的js代碼從上到下的執(zhí)行哥桥,當一個函數(shù)被執(zhí)行的時候,都會有一個執(zhí)行上下文激涤,全局環(huán)境也有一個執(zhí)行上下文拟糕,就是全局的上下文。JavaScript將以棧的形式來存儲他們倦踢。每執(zhí)行一個函數(shù)送滞,就把它上下文存入棧。棧的最底層就是全局上下文辱挥,棧頂就是當前正在執(zhí)行的函數(shù)犁嗅。每當一個函數(shù)執(zhí)行結(jié)束,他的執(zhí)行上下文就從棧中被彈出晤碘,釋放褂微。最底層的全局上下文,在瀏覽器關(guān)閉的時候才被彈出园爷。
任務(wù)隊列
任務(wù)隊列有兩種:macro-task(task)和micro-task(job)
macro-task(task):
- setTimeout/setInterval
- setImmediate
- I/O操作
- UI rendering
micro-task(job):
- process.nextTick
- Promise
- MutationObserve
注意:以上的方法的回調(diào)函數(shù)會被分發(fā)到執(zhí)行隊列中宠蚂,而他們自身會被直接執(zhí)行,比如Promise
只有then()
會被加入到執(zhí)行隊列中童社,而Promise
本身會被直接執(zhí)行求厕。
JavaScript執(zhí)行的機制是:首先執(zhí)行調(diào)用棧中的函數(shù),當調(diào)用棧中的執(zhí)行上下文全部被彈出扰楼,只剩下全局上下文的時候呀癣,就開始執(zhí)行job的執(zhí)行隊列,job的執(zhí)行完以后就開始執(zhí)行task的隊列中的弦赖。先進入的先執(zhí)行项栏,后進入的后執(zhí)行。無論是task還是job都是通過函數(shù)調(diào)用棧來執(zhí)行腾节。task執(zhí)行完成一個忘嫉,js代碼會繼續(xù)檢查是否有job需要執(zhí)行。就形成了task-job-task-job的循環(huán)(其實這里可以將第一次的函數(shù)調(diào)用棧也看成一個task)案腺。這就形成了event loop.
好了庆冕,現(xiàn)在可以來看nextTick
的實現(xiàn)原理了
var nextTick = (function () {
// 這里存放的是回調(diào)函數(shù)的隊列
var callbacks = [];
var pending = false;
var timerFunc;
//這個函數(shù)就是DOM更新后需要執(zhí)行的
function nextTickHandler () {
pending = false;
//這里將回調(diào)函數(shù)copy給copies
var copies = callbacks.slice(0);
callbacks.length = 0;
//進行循環(huán)執(zhí)行回調(diào)函數(shù)的隊列
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
})()
vue用了三個方法來執(zhí)行nextTickHandler
函數(shù),分別是:
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])的第二個參數(shù)是什么呢?來看下面的代碼劈榨。
return function queueNextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
//看這里访递,其實是可以給cb指定一個對象環(huán)境,來改變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')
}
})
結(jié)果報錯了拷姿,這是為什么呢惭载?
源碼中使用的是if (cb) { cb.call(ctx) }
所以不能使用箭頭函數(shù),箭頭函數(shù)的this
是固定的响巢,是不可用apply
,call
,bind
來改變的描滔。改成這樣:
Vue.nextTick(function () {
this.text()
}, {
text(){
console.log('test')
}
})
OK