首頁 web前端 js教程 前端進階(十二):詳解事件循環機制

前端進階(十二):詳解事件循環機制

Apr 04, 2017 pm 05:59 PM

前端進階(十二):詳解事件循環機制

Event Loop

JavaScript的學習零散而龐雜,因此很多時候我們學到了一些東西,但是卻沒辦法感受到自己的進步,甚至過了不久,就把學到的東西給忘了。為了解決自己的這個困擾,在學習的過程中,我一直試著在尋找一條核心的線索,只要我根據這條線索,我就能夠一點一點的進步。

前端基礎進階正是圍繞這條線索慢慢展開,而事件循環機制(Event Loop),則是這條線索的最關鍵的知識點。所以,我就馬不停蹄的去深入的學習了事件循環機制,並總結出了這篇文章跟大家分享。

事件循環機制從整體上的告訴了我們所寫的JavaScript程式碼的執行順序。但是在我學習的過程中,找到的許多國內部落格文章對於它的講解淺嚐輒止,不得其法,很多文章在圖中畫個圈就表示循環了,看了之後也沒感覺明白了多少。但他又如此重要,以致於當我們想要面試中高階職位時,事件循環機制總是繞不開的話題。特別是ES6中正式加入了Promise物件之後,對於新標準中事件循環機制的理解就變得更加重要。這就很尷尬了。

最近有兩篇比較火的文章也表達了這個問題的重要性。

這個前端面試在搞事
80% 應徵者都不及格的JS 面試題

但是很遺憾的是,大神們告訴了大家這個知識點很重要,卻並沒有告訴大家為什麼會這樣。所以當我們在面試時遇到這樣的問題時,就算你知道了結果,面試官再進一步問一下,我們依然懵逼。

在學習事件循環機制之前,我默認你已經懂得如下概念,如果仍然有疑問,可以回過頭去看看我以前的文章。

  • 執行上下文(Execution context)

  • #函數呼叫堆疊(call stack)

  • 佇列資料結構(queue)

  • #Promise(我會在下一篇文章專門總結Promise的詳細使用與自訂封裝)

因為chrome瀏覽器中新標準中的事件循環機制與nodejs幾乎一樣,因此此處就以整合nodejs一起來理解,其中會介紹到幾個nodejs有,但是瀏覽器中沒有的API,大家只需要了解就好,不一定要知道她是如何使用。例如process.nextTick,setImmediate

OK,那我就先拋出結論,然後以例子與圖示詳細給大家示範事件循環機制。

  • 我們知道JavaScript的一大特點就是單線程,而這個線程中擁有唯一的一個事件循環。

    當然新標準中的web worker涉及到了多線程,我對它了解也不多,這裡就不討論了。

  • JavaScript程式碼的執行過程中,除了依靠函數呼叫堆疊來搞定函數的執行順序外,還依靠任務佇列(task queue)來搞定另外一些程式碼的執行。

前端進階(十二):詳解事件循環機制

佇列資料結構

  • #一個執行緒中,事件循環是唯一的,但是任務隊列可以擁有多個。

  • 任務佇列又分為macro-task(巨集任務)與micro-task(微任務),在最新標準中,它們分別稱為task與jobs。

  • macro-task大概包含:script(整體程式碼), setTimeout, setInterval, setImmediate, I/O, UI r#endering。

  • micro-task大概包含: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)

  • setTimeout/Promise等我們稱之為任務來源。而進入任務隊列的是他們指定的具體執行任務。

    // setTimeout中的回调函数才是进入任务队列的任务
    setTimeout(function() {
      console.log('xxxx');
    })
    登入後複製
  • 來自不同任務來源的任務會進入到不同的任務佇列。其中setTimeout與setInterval是同源的。

  • 事件迴圈的順序,決定了JavaScript程式碼的執行順序。它從script(整體程式碼)開始第一次循環。之後全域上下文進入函數呼叫堆疊。直到呼叫堆疊清空(只剩下全域),然後執行所有的micro-task。當所有可執行的micro-task執行完畢之後。迴圈再次從macro-task開始,找到其中一個任務佇列執行完畢,然後再執行所有的micro-task,這樣就一直循環下去。

  • 其中每一個任務的執行,無論是macro-task或micro-task,都是藉由函數呼叫堆疊來完成。

純文字表述確實有點乾澀,因此,這裡我們透過2個例子,來逐步理解事件循環的具體順序。

// demo01  出自于上面我引用文章的一个例子,我们来根据上面的结论,一步一步分析具体的执行过程。
// 为了方便理解,我以打印出来的字符作为当前的任务名称
setTimeout(function() {
    console.log('timeout1');
})

new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    console.log('promise2');
}).then(function() {
    console.log('then1');
})

console.log('global1');
登入後複製

首先,事件循環從巨集任務佇列開始,這個時候,在巨集任務佇列中,只有一個script(整體程式碼)任務。每一個任務的執行順序,都依賴函數呼叫堆疊來搞定,而當遇到任務來源時,則會先分發任務到對應的佇列中去,所以,上面例子的第一步執行如下圖所示。

前端進階(十二):詳解事件循環機制

首先script任務開始執行,全域上下文入堆疊

第二步:script任務執行時首先遇到了setTimeout,setTimeout為一個巨集任務來源,那麼他的作用就是將任務分發到它對應的佇列中。

setTimeout(function() {
    console.log('timeout1');
})
登入後複製

前端進階(十二):詳解事件循環機制

巨集任務timeout1進入setTimeout佇列

第三步:script執行時遇到Promise實例。 Promise建構子中的第一個參數,是在new的時候執行,因此不會進入任何其他的佇列,而是直接在當前任務直接執行了,而後續的.then則會被分發到micro-task的Promise隊列中去。

因此,當建構函式執行時,裡面的參數會進入函式呼叫堆疊執行。 for循環不會進入任何佇列,因此程式碼會依序執行,所以這裡的promise1和promise2會依序輸出。

前端進階(十二):詳解事件循環機制

promise1入棧執行,這時promise1被最先輸出

前端進階(十二):詳解事件循環機制

resolve在在循環中入棧執行

前端進階(十二):詳解事件循環機制

建構子執行完畢的過程中,resolve執行完畢出棧,promise2輸出,promise1頁出棧,then執行時, Promise任務then1進入對應佇列

script任務繼續往下執行,最後只有一句輸出了globa1,然後,全域任務就執行完畢了。

第四步:第一個巨集任務script執行完畢之後,就開始執行所有的可執行的微任務。這時候,微任務中,只有Promise佇列中的一個任務then1,因此直接執行就行了,執行結果輸出then1,當然,他的執行,也是進入函數呼叫堆疊中執行的。

前端進階(十二):詳解事件循環機制

執行所有的微任務

第五步:當所有的micro-tast執行完畢之後,表示第一輪的迴圈就結束了。這時候就得開始第二輪的循環。第二輪循環仍然從巨集任務macro-task開始。

前端進階(十二):詳解事件循環機制

微任務被清空

這個時候,我們發現巨集任務中,只有在setTimeout佇列中還要一個timeout1的任務等待執行。因此就直接執行即可。

前端進階(十二):詳解事件循環機制

timeout1入棧執行

這時候宏任務佇列與微任務佇列都沒有任務了,所以程式碼就不會再輸出其他東西了。

那麼上面這個範例的輸出結果就顯而易見。大家可以自行嘗試體會。

这个例子比较简答,涉及到的队列任务并不多,因此读懂了它还不能全面的了解到事件循环机制的全貌。所以我下面弄了一个复制一点的例子,再给大家解析一番,相信读懂之后,事件循环这个问题,再面试中再次被问到就难不倒大家了。

// demo02
console.log('golb1');

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})

process.nextTick(function() {
    console.log('glob1_nextTick');
})
new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})
登入後複製

这个例子看上去有点复杂,乱七八糟的代码一大堆,不过不用担心,我们一步一步来分析一下。

第一步:宏任务script首先执行。全局入栈。glob1输出。

前端進階(十二):詳解事件循環機制

script首先执行

第二步,执行过程遇到setTimeout。setTimeout作为任务分发器,将任务分发到对应的宏任务队列中。

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})
登入後複製

前端進階(十二):詳解事件循環機制

timeout1进入对应队列

第三步:执行过程遇到setImmediate。setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中。setImmediate的任务队列会在setTimeout队列的后面执行。

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})
登入後複製

前端進階(十二):詳解事件循環機制

进入setImmediate队列

第四步:执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去。

process.nextTick(function() {
    console.log('glob1_nextTick');
})
登入後複製

前端進階(十二):詳解事件循環機制

nextTick

第五步:执行遇到Promise。Promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法会直接执行。因此,glob1_promise会第二个输出。

new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})
登入後複製

前端進階(十二):詳解事件循環機制

先是函数调用栈的变化

前端進階(十二):詳解事件循環機制

然后glob1_then任务进入队列

第六步:执行遇到第二个setTimeout。

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})
登入後複製

前端進階(十二):詳解事件循環機制

timeout2进入对应队列

第七步:先后遇到nextTick与Promise

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})
登入後複製

前端進階(十二):詳解事件循環機制

glob2_nextTick与Promise任务分别进入各自的队列

第八步:再次遇到setImmediate。

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})
登入後複製

前端進階(十二):詳解事件循環機制

nextTick

这个时候,script中的代码就执行完毕了,执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去。接下来,将会执行所有的微任务队列中的任务。

其中,nextTick队列会比Promie先执行。nextTick中的可执行任务执行完毕之后,才会开始执行Promise队列中的任务。

当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了。下一轮循环继续从宏任务队列开始执行。

这个时候,script已经执行完毕,所以就从setTimeout队列开始执行。

前端進階(十二):詳解事件循環機制

第二轮循环初始状态

setTimeout任务的执行,也依然是借助函数调用栈来完成,并且遇到任务分发器的时候也会将任务分发到对应的队列中去。

只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。

setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务。

当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了。

大家需要注意这里的循环结束的时间节点。

当我们在执行setTimeout任务中遇到setTimeout时,它仍然会将对应的任务分发到setTimeout队列中去,但是该任务就得等到下一轮事件循环执行了。例子中没有涉及到这么复杂的嵌套,大家可以动手添加或者修改他们的位置来感受一下循环的变化。

OK,到这里,事件循环我想我已经表述得很清楚了,能不能理解就看读者老爷们有没有耐心了。我估计很多人会理解不了循环结束的节点。

当然,这些顺序都是v8的一些实现。我们也可以根据上面的规则,来尝试实现一下事件循环的机制。

// 用数组模拟一个队列
var tasks = [];

// 模拟一个事件分发器
var addFn1 = function(task) {
    tasks.push(task);
}

// 执行所有的任务
var flush = function() {
    tasks.map(function(task) {
        task();
    })
}

// 最后利用setTimeout/或者其他你认为合适的方式丢入事件循环中
setTimeout(function() {
    flush();
})

// 当然,也可以不用丢进事件循环,而是我们自己手动在适当的时机去执行对应的某一个方法

var dispatch = function(name) {
    tasks.map(function(item) {
        if(item.name == name) {
            item.handler();
        }
    })
}

// 当然,我们把任务丢进去的时候,多保存一个name即可。
// 这时候,task的格式就如下
demoTask =  {
    name: 'demo',
    handler: function() {}
}

// 于是,一个订阅-通知的设计模式就这样轻松的被实现了
登入後複製

这样,我们就模拟了一个任务队列。我们还可以定义另外一个队列,利用上面的各种方式来规定他们的优先级

因此,在老的浏览器没有支持Promise的时候,就可以利用setTimeout等方法,来模拟实现Promise,具体如何做到的,下一篇文章我们慢慢分析。


以上是前端進階(十二):詳解事件循環機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

前端熱敏紙小票打印遇到亂碼問題怎麼辦? 前端熱敏紙小票打印遇到亂碼問題怎麼辦? Apr 04, 2025 pm 02:42 PM

前端熱敏紙小票打印的常見問題與解決方案在前端開發中,小票打印是一個常見的需求。然而,很多開發者在實...

神秘的JavaScript:它的作用以及為什麼重要 神秘的JavaScript:它的作用以及為什麼重要 Apr 09, 2025 am 12:07 AM

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

誰得到更多的Python或JavaScript? 誰得到更多的Python或JavaScript? Apr 04, 2025 am 12:09 AM

Python和JavaScript開發者的薪資沒有絕對的高低,具體取決於技能和行業需求。 1.Python在數據科學和機器學習領域可能薪資更高。 2.JavaScript在前端和全棧開發中需求大,薪資也可觀。 3.影響因素包括經驗、地理位置、公司規模和特定技能。

JavaScript難以學習嗎? JavaScript難以學習嗎? Apr 03, 2025 am 12:20 AM

學習JavaScript不難,但有挑戰。 1)理解基礎概念如變量、數據類型、函數等。 2)掌握異步編程,通過事件循環實現。 3)使用DOM操作和Promise處理異步請求。 4)避免常見錯誤,使用調試技巧。 5)優化性能,遵循最佳實踐。

如何實現視差滾動和元素動畫效果,像資生堂官網那樣?
或者:
怎樣才能像資生堂官網一樣,實現頁面滾動伴隨的動畫效果? 如何實現視差滾動和元素動畫效果,像資生堂官網那樣? 或者: 怎樣才能像資生堂官網一樣,實現頁面滾動伴隨的動畫效果? Apr 04, 2025 pm 05:36 PM

實現視差滾動和元素動畫效果的探討本文將探討如何實現類似資生堂官網(https://www.shiseido.co.jp/sb/wonderland/)中�...

如何使用JavaScript將具有相同ID的數組元素合併到一個對像中? 如何使用JavaScript將具有相同ID的數組元素合併到一個對像中? Apr 04, 2025 pm 05:09 PM

如何在JavaScript中將具有相同ID的數組元素合併到一個對像中?在處理數據時,我們常常會遇到需要將具有相同ID�...

JavaScript的演變:當前的趨勢和未來前景 JavaScript的演變:當前的趨勢和未來前景 Apr 10, 2025 am 09:33 AM

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

console.log輸出結果差異:兩次調用為何不同? console.log輸出結果差異:兩次調用為何不同? Apr 04, 2025 pm 05:12 PM

深入探討console.log輸出差異的根源本文將分析一段代碼中console.log函數輸出結果的差異,並解釋其背後的原因。 �...

See all articles