單線程的JavaScript為什么可以異步執(zhí)行任務(wù)
JavaScript作為一門單線程語(yǔ)言,卻能夠高效處理各種異步操作,這得益于其精巧的事件循環(huán)(Event Loop)機(jī)制。本文將全面剖析JavaScript的異步處理原理,從單線程設(shè)計(jì)的原因到具體實(shí)現(xiàn)機(jī)制,最后通過(guò)偽代碼模擬整個(gè)異步處理流程。
一、JavaScript為何選擇單線程設(shè)計(jì)
JavaScript誕生之初就被設(shè)計(jì)為單線程語(yǔ)言,這并非技術(shù)限制,而是經(jīng)過(guò)深思熟慮的設(shè)計(jì)選擇。這種設(shè)計(jì)與其最初的應(yīng)用場(chǎng)景密切相關(guān):
用途決定設(shè)計(jì):
- JavaScript 最初被設(shè)計(jì)為瀏覽器腳本語(yǔ)言,主要用于與用戶交互和操作 DOM。
- 如果 JavaScript 是多線程的,可能會(huì)出現(xiàn)多個(gè)線程同時(shí)操作同一個(gè) DOM 節(jié)點(diǎn)的情況。例如,一個(gè)線程在添加內(nèi)容,另一個(gè)線程在刪除該節(jié)點(diǎn),瀏覽器將無(wú)法確定以哪個(gè)線程的操作為準(zhǔn)。
// 假設(shè)多線程環(huán)境下可能出現(xiàn)的沖突場(chǎng)景 // 線程1: document.getElementById('node').innerHTML = '新內(nèi)容'; // 線程2: document.body.removeChild(document.getElementById('node'));
- 這種復(fù)雜性會(huì)導(dǎo)致同步問(wèn)題,因此 JavaScript 被設(shè)計(jì)為單線程,確保操作的順序性和一致性。
避免復(fù)雜性:
- 多線程編程需要處理鎖、死鎖、線程同步等問(wèn)題,這會(huì)增加語(yǔ)言的復(fù)雜性和開(kāi)發(fā)難度。
- 單線程模型簡(jiǎn)化了 JavaScript 的設(shè)計(jì),開(kāi)發(fā)者可以專注于業(yè)務(wù)邏輯,而不必?fù)?dān)心線程安全問(wèn)題。
HTML5 的 Web Worker:
- 為了利用多核 CPU 的計(jì)算能力,HTML5 提供了 ??Web Worker?? 標(biāo)準(zhǔn),允許 JavaScript 創(chuàng)建多個(gè)線程。
- 但這些子線程完全受主線程控制,且不能直接操作 DOM,因此 JavaScript 的單線程本質(zhì)并未改變。
二、單線程如何實(shí)現(xiàn)異步處理
單線程意味著所有任務(wù)需要排隊(duì)執(zhí)行,如果前一個(gè)任務(wù)耗時(shí)很長(zhǎng)(比如等待網(wǎng)絡(luò)請(qǐng)求),后續(xù)任務(wù)就會(huì)被阻塞。聰明的JavaScript設(shè)計(jì)者想到了解決辦法:當(dāng)遇到I/O等耗時(shí)操作時(shí),主線程不必傻等,而是先掛起這個(gè)任務(wù),繼續(xù)執(zhí)行后面的代碼。等到I/O操作完成,再將掛起的任務(wù)放入任務(wù)隊(duì)列等待執(zhí)行。
這種機(jī)制將任務(wù)分為兩類:同步任務(wù)和異步任務(wù)。
同步任務(wù)在主線程上順序執(zhí)行,形成所謂的"執(zhí)行棧";
異步任務(wù)則委托給瀏覽器其他模塊處理,完成后將回調(diào)函數(shù)放入"任務(wù)隊(duì)列"。
當(dāng)主線程完成當(dāng)前執(zhí)行棧中的所有任務(wù),就會(huì)查看任務(wù)隊(duì)列,取出等待中的任務(wù)繼續(xù)執(zhí)行。
這就好比一位廚師在準(zhǔn)備多道菜品時(shí),遇到需要長(zhǎng)時(shí)間燉煮的菜,會(huì)先開(kāi)火燉上,然后轉(zhuǎn)身處理其他可以快速完成的菜品,等燉煮完成后再回來(lái)處理后續(xù)步驟。這種工作方式大大提高了整體效率。
下圖就是主線程和任務(wù)隊(duì)列的示意圖。
只要主線程空了,就會(huì)去讀取"任務(wù)隊(duì)列",這就是JavaScript的運(yùn)行機(jī)制。這個(gè)過(guò)程會(huì)不斷重復(fù)。
三、事件循環(huán)(Event Loop)核心原理
事件循環(huán)是JavaScript異步處理的核心機(jī)制,它像一位不知疲倦的調(diào)度員,持續(xù)監(jiān)控著執(zhí)行棧和任務(wù)隊(duì)列的狀態(tài)。其工作流程可以形象地描述為:
- 主線程首先處理執(zhí)行棧中的所有同步任務(wù),就像廚師按順序準(zhǔn)備食材。
- 遇到異步操作(如setTimeout或AJAX請(qǐng)求)時(shí),主線程會(huì)將這些任務(wù)交給對(duì)應(yīng)的瀏覽器模塊處理,就像廚師把需要長(zhǎng)時(shí)間處理的食材交給專門設(shè)備。
- 瀏覽器模塊在后臺(tái)處理這些異步任務(wù),任務(wù)完成后將回調(diào)函數(shù)放入任務(wù)隊(duì)列,就像助手把處理好的食材放在待取區(qū)。
- 當(dāng)執(zhí)行棧清空后,事件循環(huán)會(huì)檢查任務(wù)隊(duì)列,取出最早進(jìn)入隊(duì)列的任務(wù)推入執(zhí)行棧執(zhí)行,就像廚師完成手頭工作后去待取區(qū)拿下一個(gè)要處理的食材。
- 這個(gè)過(guò)程不斷循環(huán),形成所謂的"事件循環(huán)"。
為了更好地理解,讓我們看一個(gè)實(shí)際的Ajax操作示例:
var req = new XMLHttpRequest(); req.open('GET', url); req.onload = function(){ }; // 回調(diào)函數(shù) req.onerror = function(){ }; // 回調(diào)函數(shù) req.send(); // 異步任務(wù)
這段代碼中的req.send()是異步操作,它的回調(diào)函數(shù)(onload/onerror)在代碼中的位置并不重要,因?yàn)橹挥挟?dāng)前腳本的所有同步代碼執(zhí)行完,系統(tǒng)才會(huì)去讀取"任務(wù)隊(duì)列"。因此,它與下面的寫法完全等價(jià):
var req = new XMLHttpRequest(); req.open('GET', url); req.send(); req.onload = function(){ }; req.onerror = function(){ };
需要注意的是,現(xiàn)代瀏覽器將任務(wù)隊(duì)列細(xì)分為宏任務(wù)隊(duì)列和微任務(wù)隊(duì)列。微任務(wù)隊(duì)列中的任務(wù)(如Promise回調(diào))會(huì)在當(dāng)前宏任務(wù)執(zhí)行完畢后立即執(zhí)行,比下一個(gè)宏任務(wù)(如setTimeout回調(diào))擁有更高的優(yōu)先級(jí)。這就像廚師在處理完一道主菜后,會(huì)優(yōu)先完成與之配套的醬汁調(diào)制,然后再開(kāi)始下一道主菜。
四、setTimeout與Promise的運(yùn)行機(jī)制
除了放置異步任務(wù)的事件,"任務(wù)隊(duì)列"還可以放置定時(shí)事件,即指定某些代碼在多少時(shí)間之后執(zhí)行。這叫做"定時(shí)器"(timer)功能,也就是定時(shí)執(zhí)行的代碼。
定時(shí)器功能主要由setTimeout()和setInterval()實(shí)現(xiàn),它們的內(nèi)部機(jī)制相同,區(qū)別在于前者執(zhí)行一次,后者重復(fù)執(zhí)行。看一個(gè)典型例子:
console.log(1); setTimeout(function(){ console.log(2); }, 1000); console.log(3); // 輸出順序:1, 3, 2
上面代碼的執(zhí)行結(jié)果是1,3,2,因?yàn)閟etTimeout()將第二行推遲到1000毫秒之后執(zhí)行。
如果將setTimeout()的第二個(gè)參數(shù)設(shè)為0,就表示當(dāng)前代碼執(zhí)行完(執(zhí)行棧清空)以后,立即執(zhí)行(0毫秒間隔)指定的回調(diào)函數(shù)。
setTimeout(function(){ console.log(1); }, 0); console.log(2); // 輸出順序總是:2, 1
上面代碼的執(zhí)行結(jié)果總是2,1,因?yàn)橹挥性趫?zhí)行完第二行以后,系統(tǒng)才會(huì)去執(zhí)行"任務(wù)隊(duì)列"中的回調(diào)函數(shù)。
總之,setTimeout(fn,0)的含義是,指定某個(gè)任務(wù)在主線程最早可得的空閑時(shí)間執(zhí)行,也就是說(shuō),盡可能早得執(zhí)行。它在"任務(wù)隊(duì)列"的尾部添加一個(gè)事件,因此要等到同步任務(wù)和"任務(wù)隊(duì)列"現(xiàn)有的事件都處理完,才會(huì)得到執(zhí)行。
HTML5標(biāo)準(zhǔn)規(guī)定了setTimeout()的第二個(gè)參數(shù)的最小值(最短間隔),不得低于4毫秒,如果低于這個(gè)值,就會(huì)自動(dòng)增加。在此之前,老版本的瀏覽器都將最短間隔設(shè)為10毫秒。另外,對(duì)于那些DOM的變動(dòng)(尤其是涉及頁(yè)面重新渲染的部分),通常不會(huì)立即執(zhí)行,而是每16毫秒執(zhí)行一次。這時(shí)使用requestAnimationFrame()的效果要好于setTimeout()。
需要注意的是,setTimeout()只是將事件插入了"任務(wù)隊(duì)列",必須等到當(dāng)前代碼(執(zhí)行棧)執(zhí)行完,主線程才會(huì)去執(zhí)行它指定的回調(diào)函數(shù)。要是當(dāng)前代碼耗時(shí)很長(zhǎng),有可能要等很久,所以并沒(méi)有辦法保證,回調(diào)函數(shù)一定會(huì)在setTimeout()指定的時(shí)間執(zhí)行。
注:以上內(nèi)容摘自 阮一峰《JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop》
對(duì)于Promise,情況則稍有不同。
console.log('腳本開(kāi)始'); setTimeout(() => { console.log('setTimeout'); }, 0); Promise.resolve().then(() => { console.log('Promise'); }); console.log('腳本結(jié)束');
以上代碼的輸出順序?yàn)椋?/span>
1. 腳本開(kāi)始
2. 腳本結(jié)束
3. Promise
4. setTimeout
這是因?yàn)镻romise的回調(diào)會(huì)進(jìn)入微任務(wù)隊(duì)列,而setTimeout進(jìn)入宏任務(wù)隊(duì)列。事件循環(huán)會(huì)在一輪循環(huán)中先執(zhí)行所有微任務(wù),再執(zhí)行一個(gè)宏任務(wù)。
五、偽代碼模擬異步處理機(jī)制
為了更直觀地理解JavaScript的異步處理,我們可以用偽代碼模擬整個(gè)事件循環(huán)系統(tǒng):
class EventLoop { constructor() { this.callStack = []; // 執(zhí)行棧,存儲(chǔ)同步任務(wù) this.macroQueue = []; // 宏任務(wù)隊(duì)列 this.microQueue = []; // 微任務(wù)隊(duì)列 this.isRunning = false; // 運(yùn)行狀態(tài)標(biāo)志 } // 啟動(dòng)事件循環(huán) start() { this.isRunning = true; while (this.isRunning) { // 執(zhí)行所有同步代碼 while (this.callStack.length > 0) { const task = this.callStack.pop(); execute(task); // 執(zhí)行當(dāng)前任務(wù) } // 執(zhí)行所有微任務(wù) while (this.microQueue.length > 0) { const microTask = this.microQueue.shift(); this.callStack.push(microTask); // 將微任務(wù)推入執(zhí)行棧 } // 執(zhí)行一個(gè)宏任務(wù) if (this.macroQueue.length > 0) { const macroTask = this.macroQueue.shift(); this.callStack.push(macroTask); // 將宏任務(wù)推入執(zhí)行棧 } // 如果所有隊(duì)列都為空,暫停循環(huán) if (this.callStack.length === 0 && this.macroQueue.length === 0 && this.microQueue.length === 0) { this.isRunning = false; } } } // 模擬setTimeout setTimeout(callback, delay) { // 使用瀏覽器定時(shí)器API externalTimerAPI.set(() => { this.macroQueue.push(callback); // 時(shí)間到后加入宏任務(wù)隊(duì)列 }, delay); } // 模擬Promise Promise(executor) { // 立即執(zhí)行executor executor( value => this.resolve(value), reason => this.reject(reason) ); } resolve(value) { this.microQueue.push(() => { // 這里處理Promise的成功回調(diào) handleThenCallbacks(value); }); } } // 使用示例 const loop = new EventLoop(); // 添加同步任務(wù) loop.callStack.push(() => console.log('開(kāi)始執(zhí)行')); // 添加宏任務(wù) loop.setTimeout(() => console.log('宏任務(wù)執(zhí)行'), 0); // 添加微任務(wù) loop.microQueue.push(() => console.log('微任務(wù)執(zhí)行')); // 啟動(dòng)事件循環(huán) loop.start();
段偽代碼清晰地展示了:
- 同步任務(wù)直接進(jìn)入執(zhí)行棧立即執(zhí)行
- setTimeout回調(diào)進(jìn)入宏任務(wù)隊(duì)列
- Promise回調(diào)進(jìn)入微任務(wù)隊(duì)列
- 事件循環(huán)優(yōu)先處理微任務(wù),再處理宏任務(wù)
- 整個(gè)過(guò)程循環(huán)往復(fù),直到所有任務(wù)完成
通過(guò)這個(gè)模擬,我們可以更深入地理解JavaScript如何在單線程環(huán)境下實(shí)現(xiàn)高效的異步處理,以及各種異步API在事件循環(huán)中的不同表現(xiàn)。
參考資料:
阮一峰《JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop》
到此這篇關(guān)于單線程的JavaScript為什么可以異步執(zhí)行任務(wù)的文章就介紹到這了,更多相關(guān)js單線程異步任務(wù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript觀察者模式實(shí)現(xiàn)自動(dòng)刷新效果
這篇文章主要為大家詳細(xì)介紹了javascript觀察者模式實(shí)現(xiàn)自動(dòng)刷新效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09uniapp手機(jī)號(hào)一鍵登錄實(shí)現(xiàn)保姆級(jí)教程(含前端和后端)
這篇文章主要介紹了uniapp手機(jī)號(hào)一鍵登錄實(shí)現(xiàn)的相關(guān)資料,本文指導(dǎo)如何創(chuàng)建uniapp項(xiàng)目、關(guān)聯(lián)uniCloud云空間,并配置一鍵登錄功能,,整個(gè)過(guò)程涉及創(chuàng)建云開(kāi)發(fā)環(huán)境、關(guān)聯(lián)云服務(wù)空間、配置登錄服務(wù)和編寫云函數(shù),需要的朋友可以參考下2024-10-10JavaScript Set與Map數(shù)據(jù)結(jié)構(gòu)詳細(xì)分析
大家心里是否產(chǎn)生過(guò)這樣的疑問(wèn),JS中既然已經(jīng)有對(duì)象這種數(shù)據(jù)結(jié)構(gòu),我們?yōu)槭裁催€要再單獨(dú)去使用Set或者M(jìn)ap呢?下面這篇文章主要給大家介紹了關(guān)于ES6中Set和Map數(shù)據(jù)結(jié)構(gòu)的相關(guān)資料,需要的朋友可以參考下2022-11-11js中位數(shù)不足自動(dòng)補(bǔ)位擴(kuò)展padLeft、padRight實(shí)現(xiàn)代碼
這篇文章主要介紹了js中位數(shù)不足自動(dòng)補(bǔ)位擴(kuò)展之padLeft、padRight實(shí)現(xiàn)方法,主要是通過(guò)String.prototype擴(kuò)展實(shí)現(xiàn),需要的朋友可以參考下2020-04-04限制復(fù)選框最多選擇項(xiàng)的實(shí)現(xiàn)代碼
下面小編就為大家?guī)?lái)一篇限制復(fù)選框最多選擇項(xiàng)的實(shí)現(xiàn)代碼。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-05-05