當談及Javascript時, 咱們經常聽到 單線程、異步非阻塞、事件循環這樣的關鍵詞,然而它們是什麼? 爲何單線程還能夠實現異步?怎麼實現?相信這些問題都曾今或正在困擾着許多前端愛好者。經過這篇文章咱們將對它們一一梳理。文章將講解:javascript
若是你對它們感興趣,就請繼續往下讀吧。前端
籠統抽象地說:java
回到編程的世界裏具體地說:web
用圖表達:編程
因爲步驟2 的執行時間較長,在同步執行過程當中他會阻塞步驟3的執行一段時間,反之在異步的機制中,若是咱們標明瞭步驟2是異步的, 那麼在完成步驟1以後咱們只會開始執行步驟二並讓他在另外一個世界裏執行,而後馬上開始執行步驟3.
promise
用JavaScript來模擬上面的過程:瀏覽器
const step1 = () => console.log(1); const step3 = () => console.log(3); const step4 = () => console.log(4); // 用 Promise 簡單模擬一個執行時間爲1秒,並在結束時候打印2的函數 // 若是你不瞭解Promise,不要緊,蓋住函數的內容,只要記得它執行時間很長,會在結束時打印2,並會告訴你」我執行完了「 const step2 = () => new Promise((resolve, _) => { setTimeout(() => { console.log(2); resolve(); }, 1000); }); // 異步機制執行 // JS引擎支持返回Promise的函數異步執行,因此咱們不須要作額外的包裝,直接執行step2,它即是異步的) const asyncExecute = () => { step1(); step2(); step3(); step4(); }; // 同步機制執行 // 若是你不瞭解async、await沒關係,只要知道 await 就是要等到它後面的語句執行完了纔會進行下一步 const syncExecute = async () => { step1(); await step2(); step3(); step4(); }; asyncExecute(); // 打印 1 3 4 2 syncExecute(); // 打印 1 2 3 4
同步與異步各有優勢,同步能夠保證執行的順序,異步能夠保證程序的非阻塞。 在一個web應用中,若是咱們把向服務端請求一個資源當作一個時間很長的步驟,那麼處理這個請求返回信息就須要咱們去同步執行。然而在若是咱們在請求資源的同時還想讓用戶能夠繼續使用咱們的應用,那這就須要異步地去實現。 Javascript 和他的引擎給咱們提供豐富的資源去實現這兩種機制。數據結構
講到這裏,也許還有些抽象,但相信在下面的章節裏,這一切會變得愈來愈清晰。異步
首先咱們來理解幾個概念:async
在一個JS運行時環境中,JS 代碼只在一個線程中執行, 因此咱們說 JS 是單線程的。然而運行時環境自己(好比在瀏覽器中)並非單線程的,他包含了JS引擎的運行、一系列的web API 調用、以及咱們後面要講到的事件循環機制的運行等。
若是說線程是程序自我分割、並行執行的最小單元,那在單個線程裏執行的JS代碼又怎麼可能實現並行,也就是異步呢?
假設咱們在Chrome瀏覽器中,事件循環的機制能夠用這樣一張圖來解釋。
在這裏咱們須要記住五個模塊:
接下來讓咱們一一解釋
當 JS 引擎解析JS代碼的過程當中遇到一些變量或者函數申明的時候,它會將它們存儲到裏面。
undefined
),引擎就會把這個調用從stack頂端刪除, 而後繼續執行它下面的函數。舉個例子:
const func2 = ()=> { console.log("我是 func2 ") } const func1 = () => { console.log("func1 開始了") func2(); console.log("func1 結束了") } func1(); // 打印: // func1 開始了 // 我是 func2 // func1 結束了
執行這段代碼時,引擎就會先調用 func1
, 將它的調用放到stack裏,而後執行func1
中第一行打印。而後執行第二行 調用 func2
。這時引擎會把 func2
調用放到stack的頂端(如大圖中所示),而後執行 func2
的內容也就是打印。結束以後,由於func2
中沒有更多的內容,引擎會刪除stack頂端的func2
的調用,而後繼續執行func1
第三行,當第三行結束完畢,引擎刪除stack中func1
的調用。最後咱們會看到這段程序的打印如代碼最後的註釋中所示。
callback1 = () => console.log("我是 callback1"); callback2 = () => console.log("我是 callback2"); const func2 = () => { console.log("func2 開始"); // setTimeout 就是一個異步調用的 Timer API, 他會讓 Timer 計時必定的時間,好比這裏是1秒,而後觸發計時結束,隨後callback將會被放入 callback queue setTimeout(callback2, 1000); console.log("func2 結束"); }; const func1 = () => { console.log("func1 開始"); setTimeout(callback1, 0); func2(); console.log("func1 結束"); }; func1(); // 打印 // func1 開始 // func2 開始 // func2 結束 // func1 結束 // 我是 callback1 // 我是 callback2
咱們來講說這段代碼是怎麼在在剛纔解釋的機制下執行的(超長!若是你已經理解了能夠跳過這段 ^^):
因爲4和5是在兩個線程裏執行的,因此咱們能夠把它們當作幾乎是同時執行的。
因爲9和10是在兩個線程裏執行的,因此咱們能夠把它們當作幾乎是同時執行的
文字表現比較侷限,咱們能夠按步驟動手畫一畫,就很是清晰了。
Web應用的性能是個很大的話題,在這裏咱們只討論性能中與 JS 的單線程和異步相關的部分。
首先提幾個概念做爲準備:
那若是Stack中有一個function執行時間超過16.66ms 會怎麼樣?答案是它會致使下一個render的推遲執行。在這個function結束前,頁面是停在一個靜止的狀態的,用戶在頁面上點擊也不會有什麼反應。這就是咱們有時會感覺到的 「頁面有點卡」。因此爲了防止這種性能差的表現,咱們不建議將耗時的function放到 JS 的主線程裏執行。
其實因爲render自己的執行也須要消耗時間,因此咱們還要給它留出空間。根據谷歌的官方文檔,咱們最好是將本身的邏輯保證在10ms如下,甚至是3-4ms。
然而因爲業務的須要,在開發中一些耗時的邏輯是沒法避免的,例如排序、搜索等。在這樣的狀況下咱們能夠將邏輯分紅小塊,而後使用requestAnimationFrame,或者將耗時的邏輯放到service worker中進行。 具體如何使用在這裏不作細說,咱們能夠參照谷歌的這篇文檔 Optimize JavaScript Execution,上面有詳細的解說。
長話短說:
setTimeout
只是在給定的時間以後將它的 callback function 放入 callback queue 但並不能保證function 的準時執行。setInterval
也是,只是每隔固定的時間放入一次callback function. 因此它們是否能準時執行都取決於當時stack 和 callback queue 的狀態。但咱們仍是能夠粗略地認爲它們是準時的,由於大部分狀況下這些不許時只是毫秒級的,但也須要理解它們其中的原理來處理和解釋那些小部分的狀況。
詳細解釋:
看了 MDN 或 w3schools 對 setTimeout
的解釋,咱們容易簡單地認爲它的做用是在必定的時間後執行一個callback function。然而這並不徹底正確。根據咱們在第二節中解釋的 stack 和 callback queue 的概念, setTimeout 只能保證將它的callback function在必定時間以後放入callback queue 而不是執行。 若是此時callback queue 中只有這個function 且stack是空的,固然它就會被準時執行。但若是此時stack中還有還沒有執行完的內容,或者在callback queue 中還有好幾個callback在排隊,顯然咱們的function會被推後執行,這個推後的時間取決與stack中的內容 和 callback queue 中排在前面的 callback 要執行多久。
但咱們仍是能夠粗略地認爲它是準時的。 只要咱們不在JS的線程裏放入一個十分耗時的function, 或者在callback queue裏瞬間塞入一大堆callback, 那麼stack是時常會被空出來執行咱們 setTimeout
扥 callback 的。在這樣的狀況下不許時的誤差也就只是毫秒級的。
細心的你也許發現了在第二節的例子中咱們使用了 setTimeout(callback,0)
, 也就是在 0 毫秒後將callback
放入 callback queue。 因爲queue 中的 callback 會在stack 空了以後在執行,那麼這個用法其實能夠做爲一種控制執行順序的工具。 讓咱們來看一個簡單地例子:
setTimeout(() => console.log("我想後執行"), 0); console.log("我想先執行"); // 打印 // 我想先執行 // 我想後執行
setInterval
和 setTimeout
的實現原理是類似的。咱們粗略地理解它爲每隔必定的時間執行一次callback。實際上是每隔必定的時間在 callback queue 中加入一次 callback, 因此它先後兩次執行callback 的間隔時間也是不能保證的, 它們可長可短, 取決於stack和callback queue 的狀態。
謝謝你一直讀到如今。這是個人第一篇博文,它記錄了我對前端知識學習和思考的過程。但願你在閱讀過程當中有所收穫。我會繼續堅持下去分享我在學習工做中的心得和體會。
最後,感謝這些幫助我學習文章相關內容的資料:
Asynchronous JavaScript: Promises, Callbacks, Async Await
The Javascript Runtime Environment
What the heck is the event loop anyway? | Philip Roberts | JSConf EU