JavaScript執行順序分析
前言
被問到了事件執行順序的問題,想起來之前看《深入淺出Node.js》時看到這一章就忽略了,這次來分析一下JavaScript的事件執行順序。廢話少說,正題開始。
單執行緒JavaScript
首先我們要知道JavaScript
是一門單執行緒解釋型語言。這就意味著在同一個時間下,我們只能執行一條命令。之所以它是一門單執行緒語言,和它的用途有關。
JavaScript
設計出來的初衷是為了增強瀏覽器與使用者的互動,尤其是表單的互動,而之後的Ajax
技術也是為了使表單的互動更加人性化而發明出來的。因為JavaScript
是一門解釋型的語言,而直譯器內嵌於瀏覽器,這個直譯器是單執行緒的。
之所以不設計成多執行緒是因為渲染網頁的時候多執行緒容易引起死鎖或者資源衝突等問題。但是瀏覽器本身是多執行緒的,比如解釋執行JavaScript
的同時還在載入網路資源。
Why doesn’t JavaScript support multithreading?
事件迴圈
單執行緒就意味著如果你要執行很多命令,那麼這些命令需要排序,一般情況下,這些命令是從上到下排序執行(因為直譯器是從檔案頂部開始)。比如以下程式碼是按照順序執行的。
console.log("1");
console.log("2");
console.log("3");
//1
//2
//3
但是我們還有知道在JavaScript
裡有非同步程式設計的說法,比如Ajax
,setTimeout
,setInterval
或者ES6
中的Promise
,async
,await
。
那麼什麼是同步和非同步呢?
一條命令的執行在計算機裡的意思就是它此時在使用CPU等資源,那麼因為想要獲得CPU資源的命令有很多,而CPU執行命令也需要時間去運算獲得結果,於是就有了同步非同步的概念。
同步就是在發出一個CPU請求時,在沒有得到結果之前,該CPU請求就不返回。但是一旦呼叫返回,就得到返回值了。
非同步表示CPU請求在發出之後,這個呼叫就直接返回了,所以沒有返回結果。在執行結束後,需要通過一系列手段來獲得返回值
這時候就要引入程式和執行緒的概念。
程式與執行緒
程式
概念:程式是一個具有一定獨立功能的程式在一個資料集上的一次動態執行的過程,是作業系統進行資源分配和排程的一個獨立單位,是應用程式執行的載體。
執行緒
由於程式對於CPU的使用是輪流的,那麼就存在程式的切換,但是由於現在的程式都比較大,切換的開銷很大會浪費CPU的資源,於是就發明了執行緒,把一個大的程式分解成多個執行緒共同執行。
區別
- 程式是作業系統分配資源的最小單位,執行緒是程式執行的最小單位。
- 一個程式由一個或多個執行緒組成,執行緒是一個程式中程式碼的不同執行路線;
- 程式之間相互獨立,但同一程式下的各個執行緒之間共享程式的記憶體空間(包括程式碼段、資料集、堆等)及一些程式級的資源(如開啟檔案和訊號)。
- 排程和切換:執行緒上下文切換比程式上下文切換要快得多。
舉個例子
假如我是鳴人,我想吃很多拉麵,如果我一個人吃10碗的話,那我就是一個程式一個執行緒完成吃拉麵這件事情。
但是如果我用9個分身和我一起吃10碗拉麵,那我就是一個程式用9個執行緒去完成吃拉麵這件事情。
而多程式這表示名人在一樂拉麵裡面吃拉麵的同時,好色仙人在偷看妹子洗澡~ ~。好色仙人是單程式單執行緒去偷看的哦!
瀏覽器的執行緒
瀏覽器的核心是多執行緒的,在核心控制下各執行緒相互配合以保持同步,一個瀏覽器通常由一下執行緒組成:
- GUI 渲染執行緒
- JavaScript引擎執行緒
- 事件觸發執行緒
- 非同步http請求執行緒
EventLoop
輪詢的處理執行緒
這些執行緒的作用:
- UI執行緒用於渲染頁面
- js執行緒用於執行js任務
- 瀏覽器事件觸發執行緒用於控制互動,響應使用者
- http執行緒用於處理請求,ajax是委託給瀏覽器新開一個http執行緒
EventLoop
處理執行緒用於輪詢訊息佇列
JavaScript
事件迴圈和訊息佇列(瀏覽器環境)
因為JavaScript
是單執行緒的,而瀏覽器是多執行緒的,所以為了執行不同的同步非同步的程式碼,JavaScript
執行的環境採用裡事件迴圈和訊息佇列來達到目的。
每個執行緒的任務執行順序都是FIFO(先進先出)
在JavaScript
執行的環境中,有一個負責程式本身的執行,作為主執行緒;另一個負責主執行緒與其他執行緒的通訊,被稱為Event Loop 執行緒。
每當主執行緒遇到非同步的任務,把他們移入到Event Loop 執行緒,然後主執行緒繼續執行,等到主執行緒完全執行完之後,再去Event Loop 執行緒拿結果。
而每個非同步任務都包含著與它相關聯的資訊,比如執行狀態,回撥函式等。
由此我們可以知道,同步任務和非同步任務會被分發到不同的執行緒去執行。
現在我們就可以分析一下一下程式碼的執行結果了。
setTimeout(()=>{console.log("我才是第一");},0);
console.log("我是第一");
- 因為
setTimeout
是非同步的事件,所以主執行緒把它調入Event Loop執行緒進行註冊。 - 主執行緒繼續執行
console.log("我是第一");
- 主執行緒執行完畢,從Event Loop 執行緒讀取回撥函式。再執行
console.log("我才是第一");;
setTimeout
和 setInterval
setTimeout
這裡值得一提的是,setTimeout(callback,0)
指的是主執行緒中的同步任務執行完了之後立刻由Event Loop 執行緒調入主執行緒。
而計時是在調入Event Loop執行緒註冊時開始的,此時setTimeout
的回撥函式執行時間與主執行緒執行結束的時間相關。
關於setTimeout
要補充的是,即便主執行緒為空,0毫秒實際上也是達不到的。根據HTML的標準,最低是4毫秒。
setInterval
需要注意的是,此函式是每隔一段時間將回撥函式放入Event Loop執行緒。
一旦setInterval
的回撥函式fn執行時間超過了延遲時間ms,那麼就完全看不出來有時間間隔了
micro-task(微任務)
與 macro-task(巨集任務)
Event Loop執行緒中包含任務佇列(用來對不同優先順序的非同步事件進行排序),而任務佇列又分為macro-task(巨集任務)與micro-task(微任務),在最新標準中,它們被分別稱為task
與jobs
。
macro-task
大概包括:script(整體程式碼),setTimeout
,setInterval
,setImmediate
, I/O, UI rendering。micro-task
大概包括:process.nextTick
,Promise
,Object.observe
(已廢棄),
MutationObserver
(html5新特性)setTimeout/Promise
等我們稱之為任務源。而進入任務佇列的是他們指定的具體執行任務(回撥函式)。
來自不同的任務源的任務會進入到不同的任務佇列中,而不同的任務佇列執行過程如下:
執行過程如下:
JavaScript引擎首先從macro-task
中取出第一個任務,
執行完畢後,將micro-task
中的所有任務取出,按順序全部執行;
然後再從macro-task
中取下一個,
執行完畢後,再次將micro-task
中的全部取出;
迴圈往復,直到兩個佇列中的任務都取完。
舉個大例子
console.log("start");
var promise = new Promise((resolve) => {
console.log("promise start..");
resolve("promise");
}); //3
promise.then((val) => console.log(val));
setTimeout(()=>{console.log("setTime1")},0);
console.log("test end...")
這裡我們按順序來分析。
第一輪
- 整體script程式碼作為一個巨集任務進入主執行緒,執行
console.log("start");
。 - 然後遇到
Promises
直接執行console.log("promise start..")
。 - 然後遇到
promise.then
,存入到micro-task
佇列中。 - 然後遇到
setTimeout
,存入到macro-task
佇列中。 - 於然後執行
console.log("test end...")
; - 在這一輪中,巨集任務執行結束,執行
micro-task
佇列中的promise.then
,輸出promise
第二輪
- 取出
macro-task
佇列中的setTimeout
,執行console.log("setTime1")
;
結果
輸出的順序就是
// start
// promise start
// test end...
// promise
//setTime1
留一個案例你們去分析
async function testSometing() {
console.log("執行testSometing");
return "testSometing";
}
async function testAsync() {
console.log("執行testAsync");
return Promise.resolve("hello async");
}
async function test() {
console.log("test start...");
const v1 = await testSometing();
console.log(v1);
const v2 = await testAsync();
console.log(v2);
console.log(v1, v2);
}
test();
var promise = new Promise((resolve) => {
console.log("promise start..");
resolve("promise");
}); //3
promise.then((val) => console.log(val));
setTimeout(()=>{console.log("setTime1")},3000);
console.log("test end...")
相關文章
- JavaScript的執行順序JavaScript
- JavaScript for迴圈 執行順序JavaScript
- JavaScript 執行順序淺析JavaScript
- javascript執行機制之執行順序詳解JavaScript
- javascript執行順序簡單介紹JavaScript
- Sql執行順序SQL
- 任務執行順序
- for語句執行順序
- laravel Event執行順序Laravel
- mySQL 執行語句執行順序MySql
- JavaScript程式碼執行順序和資料型別JavaScript資料型別
- Javascript在頁面載入時的執行順序JavaScript
- JavaScript for迴圈語句的執行順序和優化JavaScript優化
- sql mysql 執行順序 (4)MySql
- SQL語句執行順序SQL
- js執行順序Event LoopJSOOP
- Linux c多執行緒執行順序Linux執行緒
- 【JavaScript】JS引擎中執行上下文如何順序執行程式碼JavaScriptJS行程
- 關於 Promise 的執行順序Promise
- Spring Aop的執行順序Spring
- js解惑-函式執行順序JS函式
- SQL 語句的執行順序SQL
- Select語句執行順序
- 命令執行順序控制與管道
- java執行緒優先順序Java執行緒
- CSS規則的執行順序CSS
- Java類的基本執行順序Java
- SQL 執行順序 你懂的SQL
- JUnit 標籤執行順序解析
- pipeline的執行順序
- mysql 語句的執行順序MySql
- 執行緒執行順序——CountDownLatch、CyclicBarrier 、join()、執行緒池執行緒CountDownLatch
- JavaScript同步、非同步、回撥執行順序之經典閉包setTimeout面試題分析JavaScript非同步面試題
- Java中如何保證執行緒順序執行Java執行緒
- Pytest 順序執行,依賴執行,引數化執行
- unittest--TestCase 按宣告順序執行
- Go包中程式碼執行順序Go
- 路由的中介軟體執行順序路由