一. JavaScript是單線程的
為什么呢 ? 首先JavaScript語言的一大特點就是單線程, 通俗點說就是, 同一個時間只能做一件事.那么會又有新的問題, JavaScript為什么不能有多個線程呢 ?
JavaScript最初被設(shè)計用在瀏覽器中, 那么設(shè)想下, 瀏覽器中的JavaScript是多線程的.例如 : 假定JavaScript同時有兩個線程, 一個線程在某個DOM節(jié)點上添加內(nèi)容, 另外一個線程刪除了這個節(jié)點, 這時瀏覽器應(yīng)該以哪個線程為準(zhǔn)呢 ?
后來, HTML5提出Web Worker標(biāo)準(zhǔn), 允許JavaScript腳本創(chuàng)建多個線程, 但是子線程完全受主線程控制, 且不得操作DOM, 所以, 這個新標(biāo)準(zhǔn)并沒有改變JavaScript單線程本質(zhì)
二. JavaScript為什么需要異步 ?
如果JavaScript中不存在異步, 只能自上而下執(zhí)行, 如果上一行解析時間很長, 那么下面的代碼就會被阻塞.對于用戶而言, 阻塞就意味著 "卡死", 這樣就導(dǎo)致了很差的用戶體驗.所以, JavaScript中存在異步執(zhí)行
三. 那么又是如何實現(xiàn)異步的呢 ?
任務(wù)隊列 :1. 所有同步任務(wù)都在主線程上執(zhí)行, 形成一個執(zhí)行棧(stack)暖璧。2.主線程之外, 還存在一個任務(wù)隊列Event Loop, 異步任務(wù)在event table中注冊函數(shù), 當(dāng)滿足觸發(fā)條件(即DOM,AJAX夜惭,setTimeout水醋,setImmediate有返回結(jié)果了) 后, 被推入任務(wù)隊列(Event Loop)葛碧。3. 一旦執(zhí)行棧(stack) 中所有同步任務(wù)都執(zhí)行完了, 系統(tǒng)就會讀取任務(wù)隊列(Event Loop), 看看里面有哪些事件.那些對應(yīng)的異步任務(wù), 于是結(jié)束等待狀態(tài), 進(jìn)入執(zhí)行棧, 開始執(zhí)行 。4.主線程不斷重復(fù)上面的第三步粪狼。
例子1:
console.log(1)
setTimeout(
function() {
console.log(2)
},
0)
console.log(3)
運(yùn)行的結(jié)果是:1,3任岸,2
代碼分析:
1.console.log(1)是同步任務(wù)再榄,放入主線程里
2.setTimeout是異步任務(wù),被放入event table享潜,0秒之后被推入任務(wù)隊列(Event Loop)里
3.console.log(3)是同步任務(wù)困鸥,放到主線程里.
當(dāng)1,3在控制臺被打印后剑按,主線程去Event Loop(事件隊列)里查看是否有可執(zhí)行的函數(shù)疾就,執(zhí)行setTimeout里的函數(shù),這就是Event Loop
四.Event Loop是什么 ?
主線程從任務(wù)隊列(Event Loop) 中讀取事件, 這個過程是循環(huán)不斷的, 所以整個的這種運(yùn)行機(jī)制又稱為Event Loop(事件循環(huán)).
上圖中, 主線程運(yùn)行的時候, 產(chǎn)生堆(heap) 和棧(stack), 棧中的代碼調(diào)用各種外部API艺蝴, 它們在” 任務(wù)隊列(Event Loop)” 中加入各種事件( click虐译, load, done)吴趴。 只要棧中的代碼執(zhí)行完畢漆诽, 主線程就會去讀取” 任務(wù)隊列(Event Loop)”侮攀, 依次執(zhí)行那些事件所對應(yīng)的回調(diào)函數(shù)。
例子2:
setTimeout(function() {
console.log('定時器開始啦')
});
new Promise(function(resolve) {
console.log('馬上執(zhí)行for循環(huán)啦');
for(var i =0; i <10000; i++) {
i ==99 &&resolve();
}
}).then(function() {
console.log('執(zhí)行then函數(shù)啦')
});
console.log('代碼執(zhí)行結(jié)束');
嘗試按照厢拭,上文我們剛學(xué)到的js執(zhí)行機(jī)制去分析:
1.setTimeout 是異步任務(wù)兰英,被放到event table
2.new Promise是同步任務(wù),被放到主線程里供鸠,直接執(zhí)行打印console.log('馬上執(zhí)行for循環(huán)啦');
3..then里的函數(shù)是異步任務(wù)畦贸,被放到event table
4.console.log('代碼執(zhí)行結(jié)束');是同步代碼,被放到主線程里楞捂,直接執(zhí)行
所以根據(jù)分析的結(jié)果是:馬上執(zhí)行for循環(huán)啦---代碼執(zhí)行結(jié)束---定時器開始啦---執(zhí)行then函數(shù)啦
自己運(yùn)行了下代碼后薄坏,結(jié)果居然不是這樣的,而是:
馬上執(zhí)行for循環(huán)啦---代碼執(zhí)行結(jié)束---執(zhí)行then函數(shù)啦---定時器開始啦
事實上寨闹,按照異步和同步的方式來劃分胶坠,并不準(zhǔn)確,而準(zhǔn)確的劃分方式是:
macro-task(宏任務(wù)):script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering繁堡。
micro-task(微任務(wù)):process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
按照這種分類方式沈善,js的執(zhí)行機(jī)制是:
1.執(zhí)行一個宏任務(wù),過程中如果遇到微任務(wù)椭蹄,就將其放到微任務(wù)的"事件隊列"里
2.當(dāng)前宏任務(wù)執(zhí)行完成后闻牡,會查看微任務(wù)的"事件隊列",并將里面全部的微任務(wù)依次執(zhí)行完
3.重復(fù)以上2步驟绳矩,結(jié)合圖1和圖2就是更為準(zhǔn)確的js執(zhí)行機(jī)制了
那么罩润,去分析例2:
1.首先執(zhí)行script下的宏任務(wù),遇到setTimeout,將其放到宏任務(wù)的“隊列”里
2.遇到 new Promise直接執(zhí)行翼馆,打印"馬上執(zhí)行for循環(huán)啦"
3.遇到then方法割以,是微任務(wù),將其放到微任務(wù)的“隊列”里写妥。
4.遇到console.log('代碼執(zhí)行結(jié)束');是同步任務(wù)拳球,直接打印"代碼執(zhí)行結(jié)束"
5.本輪宏任務(wù)執(zhí)行完畢,查看本輪的微任務(wù)珍特,發(fā)現(xiàn)有一個then方法里的函數(shù)祝峻,打印"執(zhí)行then函數(shù)啦"
6.到此,本輪的event loop 全部完成。
7.下一輪的循環(huán)里扎筒,先執(zhí)行一個宏任務(wù)莱找,發(fā)現(xiàn)宏任務(wù)的“隊列”里有一個setTimeout里的函數(shù),執(zhí)行打印"定時器開始啦"
所以最后的執(zhí)行順序是: 馬上執(zhí)行for循環(huán)啦---代碼執(zhí)行結(jié)束---執(zhí)行then函數(shù)啦---定時器開始啦
五.定時器setTimeout()和setInterval()
定時器指定某些代碼在多少時間之后執(zhí)行這叫做”定時器”(timer)功能,也就是定時執(zhí)行的代碼嗜桌。
例子3:
setTimeout(function(){
console.log('執(zhí)行了')
},3000)
我們一般會說:3秒后奥溺,會執(zhí)行setTimeout里的那個函數(shù),但是這種說法并不嚴(yán)謹(jǐn)骨宠,準(zhǔn)確的解釋是:3秒后浮定,setTimeout里的函數(shù)會被推入事件隊列(Event Loop)相满,而事件隊列(Event Loop)里的任務(wù),只有在主線程空閑時才會執(zhí)行桦卒,所以條件只有同時滿足(ps:3秒后并且主線程空閑)時立美,才會3秒后執(zhí)行函數(shù)
如果主線程執(zhí)行內(nèi)容很多,執(zhí)行時間超過3秒方灾,比如主線程里執(zhí)行棧執(zhí)行了10秒建蹄,那么這個函數(shù)只能10秒后執(zhí)行了
六.Node.js的Event Loop
Node.js也是單線程的Event Loop,但是它的運(yùn)行機(jī)制不同于瀏覽器環(huán)境裕偿。
如圖所示洞慎,Node.js的運(yùn)行機(jī)制如下:
1.V8引擎解析JavaScript腳本。
2.解析后的代碼嘿棘,調(diào)用Node API劲腿。
3.libuv庫負(fù)責(zé)Node API的執(zhí)行。它將不同的任務(wù)分配給不同的線程蔫巩,形成一個Event Loop(事件循環(huán))谆棱,以異步的方式將任務(wù)的執(zhí)行結(jié)果返回給V8引擎快压。
4.V8引擎再將結(jié)果返回給用戶圆仔。
除了setTimeout和setInterval這兩個方法,Node.js還提供了另外兩個與”任務(wù)隊列”有關(guān)的方法:process.nextTick和setImmediate蔫劣。它們可以幫助我們加深對”任務(wù)隊列”的理解坪郭。
nextTick setImmediate 區(qū)別和聯(lián)系
nextTick :把回調(diào)函數(shù)放在當(dāng)前執(zhí)行棧的底部,而多個process.nextTick語句總是在當(dāng)前”執(zhí)行棧”一次執(zhí)行完
setImmediate :把回調(diào)函數(shù)放在事件隊列(event loop)的尾部,而多個setImmediate可能則需要多次loop才能執(zhí)行完
例子4:
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
上面代碼中脉幢,由于process.nextTick方法指定的回調(diào)函數(shù)歪沃,總是在當(dāng)前”執(zhí)行棧”的尾部觸發(fā)嫌松,所以不僅回調(diào)函數(shù)A比setTimeout指定的回調(diào)函數(shù)timeout先執(zhí)行沪曙,而且函數(shù)B也比timeout先執(zhí)行。這說明萎羔,如果有多個process.nextTick語句(不管它們是否嵌套)液走,將全部在當(dāng)前”執(zhí)行棧”執(zhí)行贾陷。所以結(jié)果是:1缘眶,2,'TIMEOUT FIRED'
現(xiàn)在髓废,再來看看setImmediate
例子5:
setImmediate(function A() {
console.log(1);
setImmediate(function B(){
console.log(2);
});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
上面代碼中巷懈,setImmediate與setTimeout(fn,0)各自添加了一個回調(diào)函數(shù)A和timeout,都是在下一次事件隊列(Event Loop)觸發(fā)慌洪。那么顶燕,哪個回調(diào)函數(shù)先執(zhí)行呢凑保?答案是不確定。運(yùn)行結(jié)果可能是1,TIMEOUT FIRED,2涌攻,也可能是TIMEOUT FIRED,1,2愉适。
令人困惑的是,Node.js文檔中稱癣漆,setImmediate指定的回調(diào)函數(shù)维咸,總是排在setTimeout前面。實際上惠爽,這種情況只發(fā)生在遞歸調(diào)用的時候癌蓖。
例子6:
setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});
上面代碼中,setImmediate和setTimeout被封裝在一個setImmediate里面婚肆,它的運(yùn)行結(jié)果總是1租副,TIMEOUT FIRED,2较性,這時函數(shù)A一定在timeout前面觸發(fā)用僧。至于2排在TIMEOUT FIRED的后面(即函數(shù)B在timeout后面觸發(fā)),是因為setImmediate總是將事件注冊到下一輪事件隊列(Event Loop)赞咙,所以函數(shù)A和timeout是在同一輪Loop執(zhí)行责循,而函數(shù)B在下一輪Loop執(zhí)行
另外,由于process.nextTick指定的回調(diào)函數(shù)是在本次”事件循環(huán)”觸發(fā)攀操,而setImmediate指定的是在下次”事件循環(huán)”觸發(fā)院仿,所以很顯然,前者總是比后者發(fā)生得早速和,而且執(zhí)行效率也高(因為不用檢查”任務(wù)隊列”)歹垫。