javascript
for循环里面有异步操作_JS 线程与异步的那些事
已知,JavaScript 是單線程的,天生異步,適合 IO 密集型,不適合 CPU 密集型,但是,為什么是異步的喃,異步由何而來的喃,我們將在這里逐漸討論實現(xiàn)。
一、進程與線程
1. 瀏覽器是多進程的
它主要包括以下進程:
Browser 進程:瀏覽器的主進程,唯一,負責創(chuàng)建和銷毀其它進程、網(wǎng)絡資源的下載與管理、瀏覽器界面的展示、前進后退等。
GPU 進程:用于 3D 繪制等,最多一個。
第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創(chuàng)建。
瀏覽器渲染進程(瀏覽器內(nèi)核):內(nèi)部是多線程的,每打開一個新網(wǎng)頁就會創(chuàng)建一個進程,主要用于頁面渲染,腳本執(zhí)行,事件處理等。
2. 渲染進程(瀏覽器內(nèi)核)
瀏覽器的渲染進程是多線程的,頁面的渲染,JavaScript 的執(zhí)行,事件的循環(huán),都在這個進程內(nèi)進行:
GUI 渲染線程:負責渲染瀏覽器界面,當界面需要重繪(Repaint)或由于某種操作引發(fā)回流(Reflow)時,該線程就會執(zhí)行。
JavaScript 引擎線程:也稱為 JavaScript 內(nèi)核,負責處理 Javascript 腳本程序、解析 Javascript 腳本、運行代碼等。(例如 V8 引擎)
事件觸發(fā)線程:用來控制瀏覽器事件循環(huán),注意這不歸 JavaScript 引擎線程管,當事件被觸發(fā)時,該線程會把事件添加到待處理隊列的隊尾,等待 JavaScript 引擎的處理。
定時觸發(fā)器線程:傳說中的 setInterval 與 setTimeout 所在線程,注意,W3C 在 HTML 標準中規(guī)定,規(guī)定要求 setTimeout 中低于 4ms 的時間間隔算為 4ms 。
異步 http 請求線程:在 XMLHttpRequest 連接后通過瀏覽器新開一個線程請求,將檢測到狀態(tài)變更時,如果設置有回調(diào)函數(shù),異步線程就產(chǎn)生狀態(tài)變更事件,將這個回調(diào)再放入事件隊列中。再由 JavaScript 引擎執(zhí)行。
注意,GUI 渲染線程與 JavaScript 引擎線程是互斥的,當 JavaScript 引擎執(zhí)行時 GUI 線程會被掛起(相當于被凍結(jié)了),GUI 更新會被保存在一個隊列中等到 JavaScript 引擎空閑時立即被執(zhí)行。所以如果 JavaScript 執(zhí)行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞。
二、單線程的 JavaScript
所謂單線程,是指在 JavaScript 引擎中負責解釋和執(zhí)行 JavaScript 代碼的線程唯一,同一時間上只能執(zhí)行一件任務。
問題:首先為什么要引入單線程喃?
我們知道:
瀏覽器需要渲染 DOM
JavaScript 可以修改 DOM 結(jié)構(gòu)
JavaScript 執(zhí)行時,瀏覽器 DOM 渲染停止
如果 JavaScript 引擎線程不是單線程的,那么可以同時執(zhí)行多段 JavaScript,如果這多段 JavaScript 都修改 DOM,那么就會出現(xiàn) DOM 沖突。
你可能會說,web worker 就支持多線程,但是 web worker 不能訪問 window 對象,document 對象等。
原因:避免 DOM 渲染的沖突
當然,我們可以為瀏覽器引入鎖 的機制來解決這些沖突,但其大大提高了復雜性,所以 JavaScript從誕生開始就選擇了單線程執(zhí)行。
引入單線程就意味著,所有任務需要排隊,前一個任務結(jié)束,才會執(zhí)行后一個任務。這同時又導致了一個問題:如果前一個任務耗時很長,后一個任務就不得不一直等著。
//?實例1let?i,?sum?=?0
for(i?=?0;?i?1000000000;?i?++)?{
????sum?+=?i
}
console.log(sum)
在實例1中,sum 并不能立刻打印出來,必須在 for 循環(huán)執(zhí)行完成之后才能執(zhí)行 console.log(sum) 。
//?實例2console.log(1)
alert('hello')
console.log(2)
在實例2中,瀏覽器先打印 1 ,然后彈出彈框,點擊確定后才執(zhí)行 console.log(2) 。
總結(jié):
優(yōu)點:實現(xiàn)比較簡單,執(zhí)行環(huán)境相對單純
缺點:只要有一個任務耗時很長,后面的任務都必須排隊等著,會拖延整個程序的執(zhí)行。常見的瀏覽器無響應(假死),往往就是因為某一段 Javascript 代碼長時間運行(比如死循環(huán)),導致整個頁面卡在這個地方,其他任務無法執(zhí)行。
為了解決這個問題,JavaScript 語言將任務的執(zhí)行模式分為兩種:同步和異步
三、同步與異步
1. 同步
func(args...)如果在函數(shù) func 返回的時候,調(diào)用者就能夠得到預期結(jié)果(即拿到了預期的返回值或者看到了預期的效果),那么這個函數(shù)就是同步的。
let?a?=?1Math.floor(a)
console.log(a)?//?1
2. 異步
如果在函數(shù) func 返回的時候,調(diào)用者還不能夠得到預期結(jié)果,而是需要在將來通過一定的手段得到,那么這個函數(shù)就是異步的。
fs.readFile('foo.txt',?'utf8',?function(err,?data)?{????console.log(data);
});
總結(jié):
JavaScript 采用異步編程原因有兩點,
一是 JavaScript 是單線程;
二是為了提高 CPU 的利用率。
四、異步過程
fs.readFile('data.json',?'utf8',?function(err,?data)?{????console.log(data)
})
在執(zhí)行這段代碼時,fs.readFile 函數(shù)返回時,并不會立刻打印 data ,只有 data.json 讀取完成時才打印。也就是異步函數(shù) fs.readFile 執(zhí)行很快,但后面還有工作線程執(zhí)行異步任務、通知主線程、主線程回調(diào)等操作,這個過程就叫做異步過程。
主線程發(fā)起一個異步操作,相應的工作線程接受請求并告知主線程已收到(異步函數(shù)返回);主線程繼續(xù)執(zhí)行后面的任務,同時工作線程執(zhí)行異步任務;工作線程完成任務后,通知主線程;主線程收到通知后,執(zhí)行一定的動作(調(diào)用回調(diào)函數(shù))。
工作線程在異步操作完成后通知主線程,那么這個通知機制又是如何顯現(xiàn)喃?答案就是就是消息隊列與事件循環(huán)。
五、消息隊列與事件循環(huán)
工作線程將消息放在消息隊列,主線程通過事件循環(huán)過程去取消息。
消息隊列:消息隊列是一個先進先出的隊列,它里面存放著各種消息。
事件循環(huán):事件循環(huán)是指主線程重復從消息隊列中取消息、執(zhí)行的過程。
1. 事件循環(huán)(eventloop)
主線程不斷的從消息隊列中取消息,執(zhí)行消息,這個過程稱為事件循環(huán),這種機制叫事件循環(huán)機制,取一次消息并執(zhí)行的過程叫一次循環(huán)。
大致實現(xiàn)過程如下:
while(true)?{????var?message?=?queue.get()
????execute(message)
}
例如:
$.ajax({????url:?'xxxx',
????success:?function(result)?{
????????console.log(1)
????}
})
setTimeout(function()?{
????console.log(2)
},?100)
setTimeout(function()?{
????console.log(3)
})
console.log(4)
// output:4321 或 4312
其中,主線程:
//?主線程console.log(4)
異步隊列:
//?異步隊列function?()?{
????console.log(3)
}
function?()?{?//?100ms后
????console.log(2)
}
function()?{?//?ajax加載完成之后
????console.log(1)
}
事件循環(huán)是JavaScript實現(xiàn)異步的具體解決方案,其中同步代碼,直接執(zhí)行;異步函數(shù)先放在異步隊列中,待同步函數(shù)執(zhí)行完畢后,輪詢執(zhí)行 異步隊列 的回調(diào)函數(shù)。
2. 消息隊列
其中,消息就是注冊異步任務時添加的回調(diào)函數(shù)。
$.ajax('XXX',?function(res)?{????console.log(res)
})
...
主線程在發(fā)起 AJAX 請求后,會繼續(xù)執(zhí)行其他代碼,AJAX 線程負責請求 XXX,拿到請求后,會封裝成 JavaScript 對象,然后構(gòu)造一條消息:
//?消息隊列里的消息var?message?=?function?()?{
????callback(response)
}
其中 callback 是 AJAX 網(wǎng)絡請求成功響應時的回調(diào)函數(shù)。
主線程在執(zhí)行完當前循環(huán)中的所有代碼后,就會到消息隊列取出這條消息(也就是 message 函數(shù)),并執(zhí)行它。到此為止,就完成了工作線程對主線程的 通知 ,回調(diào)函數(shù)也就得到了執(zhí)行。如果一開始主線程就沒有提供回調(diào)函數(shù),AJAX 線程在收到 HTTP 響應后,也就沒必要通知主線程,從而也沒必要往消息隊列放消息。
異步過程中的回調(diào)函數(shù),一定不在當前這一輪事件循環(huán)中執(zhí)行。
六、異步與事件
消息隊列中的每條消息實際上都對應著一個事件。
其中一個重要的異步過程就是:DOM事件
var?button?=?document.getElementById('button')button.addEventLister('click',?function(e)?{
????console.log('事件')
})
從異步的角度看,addEventLister 函數(shù)就是異步過程的發(fā)起函數(shù),事件監(jiān)聽器函數(shù)就是異步過程的回調(diào)函數(shù)。事件觸發(fā)時,表示異步任務完成,會將事件監(jiān)聽器函數(shù)封裝成一條消息放在消息隊列中,等待主線程執(zhí)行。
事件的概念實際上并不是必須的,事件機制實際上就是異步過程的通知機制。
另外,所有的異步過程也都可以用事件來描述。例如:
setTimeout(func,?1000)//?可以看成:
timer.addEventLister('timeout',?1000,?func)
其中關于事件的詳細描述,可以看這篇文章:事件綁定、事件監(jiān)聽、事件委托,這里不再深入介紹。
七、生產(chǎn)者與消費者
生產(chǎn)者和消費者問題是線程模型中的經(jīng)典問題:生產(chǎn)者和消費者在同一時間段內(nèi)共用同一個存儲空間,生產(chǎn)者往存儲空間中添加數(shù)據(jù),消費者從存儲空間中取走數(shù)據(jù),當存儲空間為空時,消費者阻塞,當存儲空間滿時,生產(chǎn)者阻塞。
從生產(chǎn)者與消費者的角度看,異步過程是這樣的:
工作線程是生產(chǎn)者,主線程是消費者(只有一個消費者)。工作線程執(zhí)行異步任務,執(zhí)行完成后把對應的回調(diào)函數(shù)封裝成一條消息放到消息隊列中;主線程不斷地從消息隊列中取消息并執(zhí)行,當消息隊列空時主線程阻塞,直到消息隊列再次非空。
那么異步的實現(xiàn)方式有哪些喃?
ES6之前:callback、eventloop、Promise
ES6:Generator
ES7:Async/Await
八、走在最后
1.???玩得開心,不斷學習,并始終保持編程。?
2.??點擊原文,查看更多精彩文章!?
3. 如有任何問題或更獨特的見解,歡迎聯(lián)系瓶子君!(掃碼關注公眾號,回復 123 即可)??
我知道你 “在看”
總結(jié)
以上是生活随笔為你收集整理的for循环里面有异步操作_JS 线程与异步的那些事的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入浅出强化学习_直播 | 深入浅出理解
- 下一篇: gradle idea java ssm