JavaScript的三座大山之單線程和異步
一、進程與線程
1. 進程:
程序的一次執(zhí)行, 它占有一片獨有的內存空間 ---- 可以通過windows任務管理器查看進程;
2. 線程:
是進程內的一個獨立執(zhí)行單元;是程序執(zhí)行的一個完整流程;CPU的基本調度單位;
3. 進程與線程的關系:
* 一個進程中一般至少有一個運行的線程: 主線程 -- 進程啟動后自動創(chuàng)建;
* 一個進程中也可以同時運行多個線程, 我們會說程序是多線程運行的;
* 一個進程內的數(shù)據(jù)可以供其中的多個線程直接共享;
* 多個進程之間的數(shù)據(jù)是不能直接共享的
4. 瀏覽器運行是單進程還是多進程?
- 有的是單進程
- firefox
- 有的是多進程
- chrome
5. 如何查看瀏覽器是否是多進程運行的呢?
任務管理器==>進程
6. 瀏覽器運行是單線程還是多線程?
都是多線程運行的
二、單線程
1、什么是單線程
JavaScript語言的一大特點就是單線程,也就是說同一時間只能做一件事。
//栗子?? console.log(1)console.log(2)console.log(3)//輸出順序 1 2 3
2. JavaScript為什么是單線程
- 首先是歷史原因,在創(chuàng)建 javascript 這門語言時,多進程、多線程的架構并不流行,硬件支持并不好。
- 其次是因為多線程的復雜性,多線程操作需要加鎖,編碼的復雜性會增高。
- 最后與它的用途有關,作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM,如果同時操作 DOM ,在多線程不加鎖的情況下,最終會導致 DOM 渲染的結果不可預期。
為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創(chuàng)建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準并沒有改變JavaScript單線程的本質。
三、同步與異步
1、JS的 同步任務/異步任務
同步任務:
在主線程上排隊執(zhí)行的任務,只有前一個任務執(zhí)行完畢,才能執(zhí)行后一個任務;
所有同步任務都在主線程上執(zhí)行,形成一個執(zhí)行棧(execution context stack)。
異步任務:在主線程外執(zhí)行的任務;在主線程之外還存在一個“任務隊列”(task queue),當異步任務執(zhí)行完成后會以回調函數(shù)的方式放入任務隊列中等待,等主線程空閑時,主線程就會去事件隊列中取出等待的回調函數(shù)放入主線程中進行執(zhí)行。這個過程反復執(zhí)行就形成了js的事件循環(huán)機制(Event Loop)。
//栗子?? // 同步 console.log(1) // 異步 setTimeout(()=>{ console.log(2) },100) // 同步 console.log(3) //輸出順序 1 3 2
2、 JavaScript為什么需要異步
如果在JS代碼執(zhí)行過程中,某段代碼執(zhí)行過久,后面的代碼遲遲不能執(zhí)行,產生阻塞(即卡死),會影響用戶體驗。
3、JavaScript怎么實現(xiàn)異步
1)執(zhí)行棧與任務隊列
其實上面我們已經提到了,JS實現(xiàn)異步時通過 事件循環(huán);
我們先理解幾個概念:
- JS任務 分為同步任務(synchronous)和異步任務(asynchronous)
- 同步任務都在 JS引擎線程(主線程) 上執(zhí)行,形成一個執(zhí)行棧(call stack)
- 事件觸發(fā)線程 管理一個 任務隊列(Task Queue)
- 異步任務 觸發(fā)條件達成,將 回調事件 放到任務隊列(Task Queue)中
- 執(zhí)行棧中所有同步任務執(zhí)行完畢,此時JS引擎線程空閑,系統(tǒng)會讀取任務隊列,將可運行的異步任務回調事件添加到執(zhí)行棧中,開始執(zhí)行
當一個JS文件第一次執(zhí)行的時候,js引擎會 解析這段代碼,并將其中的同步代碼 按照執(zhí)行順序加入執(zhí)行棧中,然后從頭開始執(zhí)行。如果當前執(zhí)行的是一個方法,那么js會向執(zhí)行棧中添加這個方法的執(zhí)行環(huán)境,然后進入這個執(zhí)行環(huán)境繼續(xù)執(zhí)行其中的代碼。當這個執(zhí)行環(huán)境中的代碼 執(zhí)行完畢并返回結果后,js會退出這個執(zhí)行環(huán)境并把這個執(zhí)行環(huán)境銷毀,回到上一個方法的執(zhí)行環(huán)境。這個過程反復進行,直到執(zhí)行棧中的代碼全部執(zhí)行完畢。
栗子 ??
//(1) console.log(1) //(2) setTimeout(()=>{ console.log(2) },100) //(3) console.log(3)
1.先解析整段代碼,按照順序加入到執(zhí)行棧中,從頭開始執(zhí)行
2.先執(zhí)行(1),是同步的,所以直接打印 1
3.執(zhí)行(2),發(fā)現(xiàn)是 setTimeout,于是調用瀏覽器的方法(webApi)執(zhí)行,在 100ms后將 console.log(2) 加入到任務隊列
4.執(zhí)行(3),同步的,直接打印 3
5.執(zhí)行棧已經清空了,現(xiàn)在檢查任務隊列,(執(zhí)行太快的話可能此時任務隊列還是空的,沒到100ms,還沒有將(2)的打印加到任務隊列,于是不停的檢測,直到隊列中有任務),發(fā)現(xiàn)有 console.log(2),于是添加到執(zhí)行棧,執(zhí)行console.log(2),同步代碼,直接打印 2 (如果這里是異步任務,同樣會再走一遍循環(huán):-->任務隊列->執(zhí)行棧)
所以結果是 1 3 2;
注意:setTimeout/Promise等我們稱之為任務源。而進入任務隊列的是他們指定的回調;
2)宏任務(macro task)與微任務(micro task)
上面的循環(huán)只是一個宏觀的表述,實際上異步任務之間也是有不同的,分為 宏任務(macro task) 與 微任務(micro task),最新的標準中,他們被稱為 task與 jobs
- 宏任務有哪些:
script
(整體代碼),setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
(渲染) - 微任務有哪些:
process.nextTick
,Promise
,Object.observe
(已廢棄),MutationObserver
(html5新特性)
下面我們再詳細講解一下執(zhí)行過程:
執(zhí)行棧在執(zhí)行的時候,會把宏任務放在一個宏任務的任務隊列,把微任務放在一個微任務的任務隊列,在當前執(zhí)行棧為空的時候,主線程會 查看微任務隊列是否有事件存在。如果微任務隊列不存在,那么會去宏任務隊列中 取出一個任務 加入當前執(zhí)行棧;如果微任務隊列存在,則會依次執(zhí)行微任務隊列中的所有任務,直到微任務隊列為空(同樣,是吧隊列中的事件加到執(zhí)行棧執(zhí)行),然后去宏任務隊列中取出最前面的一個事件加入當前執(zhí)行棧...如此反復,進入循環(huán)。
注意:
- 宏任務和微任務的任務隊列都可以有多個
- 當前執(zhí)行棧執(zhí)行完畢時會立刻先處理所有微任務隊列中的事件,然后再去宏任務隊列中取出一個事件。同一次事件循環(huán)中,微任務永遠在宏任務之前執(zhí)行。
- 不同的運行環(huán)境 循環(huán)策略可能有不同,這里探討chrome、node環(huán)境
栗子 ??
//(1) setTimeout(()=>{ console.log(1) // 宏任務 },100) //(2) setTimeout(()=>{ console.log(2) // 宏任務 },100) //(3) new Promise(function(resolve,reject){ //(4) console.log(3) // 直接打印 resolve(4) }).then(function(val){ //(5) console.log(val); // 微任務 }) //(6) new Promise(function(resolve,reject){ //(7) console.log(5) // 直接打印 resolve(6) }).then(function(val){ //(8) console.log(val); // 微任務 }) //(9) console.log(7) // 直接打印 //(10) setTimeout(()=>{ console.log(8) // 宏任務,單比(1)(2)宏任務早 },50)
上面的代碼在node和chrome環(huán)境的正確打印順序是 3 5 7 4 6 8 1 2
下面分析一下執(zhí)行過程:
1.全部代碼在解析后加入執(zhí)行棧
2.執(zhí)行(1),宏任務,調用webapi setTimeout,這個方法會在100ms后將回調函數(shù)放入宏任務的任務隊列
3.執(zhí)行(2),同(1),但是會比(1)稍后一點
4.執(zhí)行(3),同步執(zhí)行new Promise,然后執(zhí)行(4),直接打印 3 ,然后resolve(4),然后.then(),把(5)放入微任務的任務隊列
5.執(zhí)行(6),同上,先打印 5 ,再執(zhí)行resolve(6),然后.then()里面的內容(8)加入到微任務的任務隊列
6.執(zhí)行(9),同步代碼,直接打印 7
7.執(zhí)行(10),同(1)和(2),只是時間更短,會在 50ms 后將回調 console.log(8) 加入宏任務的任務隊列
8.現(xiàn)在執(zhí)行棧清空了,開始檢查微任務隊列,發(fā)現(xiàn)(5),加入到執(zhí)行棧執(zhí)行,是同步代碼,直接打印 4
9.任務隊列又執(zhí)行完了,又檢查微任務隊列,發(fā)現(xiàn)(8),打印 6
10.任務隊列又執(zhí)行完了,檢查微任務隊列,沒有任務,再檢查宏任務隊列,此時如果超過了50ms的話,會發(fā)現(xiàn) console.log(8) 在宏任務隊列中,于是執(zhí)行 打印 8
11.依次打印 1 2
注:因為渲染也是宏任務,需要在一次執(zhí)行棧執(zhí)行完后才會執(zhí)行渲染,所以如果執(zhí)行棧中同時有幾個同步的改變同一個樣式的代碼,在渲染時只會渲染最后一個。
總結
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關注腳本之家的更多內容!
相關文章
js split 的用法和定義 js split分割字符串成數(shù)組的實例代碼
關于js split的用法,我們經常用來將字符串分割為數(shù)組方便后續(xù)操作,今天寫一段廣告判斷代碼的時候,竟然忘了split的用法了,特整理下,方便需要的朋友2012-05-05基于JavaScript實現(xiàn)繼承機制之調用call()與apply()的方法詳解
本文將介紹兩種很類似于對象冒充的繼承方式,即使用call()和apply()方法2013-05-05深入理解javascript中的立即執(zhí)行函數(shù)(function(){…})()
這篇文章主要介紹了深入理解javascript中的立即執(zhí)行函數(shù),立即執(zhí)行函數(shù)也叫立即調用函數(shù),通常它的寫法是用(function(){…})()包住業(yè)務代碼,使用jquery時比較常見,需要的朋友可以參考下2014-06-06document.getElementById獲取控件對象為空的解決方法
今天寫個網頁,想在頁面加載onLoad時,動態(tài)顯示由后臺其他程序傳來的數(shù)據(jù)時,用document.getElementById獲取控件對象總是為空。但是檢查了這個id確實是存在的??聪挛牡氖纠徒鉀Q方法2013-11-11