同步任務和異步任務
同步和異步操作的區別就是是否阻礙后續代碼的執行。
同步任務是那些沒有被引擎掛起、在主線程上排隊執行的任務。只有前一個任務執行完畢,才能執行后一個任務。
異步任務是那些被引擎放在一邊,不進入主線程、而進入任務隊列的任務。只有引擎認為某個異步任務可以執行了(比如 Ajax 操作從服務器得到了結果)【發布訂閱】,該任務(采用回調函數的形式)才會進入主線程執行。排在異步任務后面的代碼,不用等待異步任務結束會馬上運行,也就是說,異步任務不具有“堵塞”效應。
在setTimeout的執行形式上來看,setTimeout是不會阻礙其后續代碼的執行的。所以可以理解為setTimeout是異步操作。
單線程模式
JS是單線程的,但JS運行環境(Chrome瀏覽器)是多線程的。
GUI線程
GUI線程就是渲染頁面的,他解析HTML和CSS,然后將他們構建成DOM樹和渲染樹就是這個線程負責的。
JS引擎線程
這個線程就是負責執行JS的主線程,前面說的"JS是單線程的"就是指的這個線程。大名鼎鼎的Chrome V8引擎就是在這個線程運行的。需要注意的是,這個線程跟GUI線程是互斥的。互斥的原因是JS也可以操作DOM,如果JS線程和GUI線程同時操作DOM,結果就混亂了,不知道到底渲染哪個結果。這帶來的后果就是如果JS長時間運行,GUI線程就不能執行,整個頁面就感覺卡死了。所以我們最開始例子的while(true)
這樣長時間的同步代碼在真正開發時是絕對不允許的。
定時器線程
前面異步例子的setTimeout
其實就運行在這里,他跟JS主線程根本不在同一個地方,所以“單線程的JS”能夠實現異步。JS的定時器方法還有setInterval
,也是在這個線程。
事件觸發線程
定時器線程其實只是一個計時的作用,他並不會真正執行時間到了的回調,真正執行這個回調的還是JS主線程。所以當時間到了定時器線程會將這個回調事件給到事件觸發線程,然后事件觸發線程將它加到任務隊列里面去。最終JS主線程從任務隊列取出這個回調執行。事件觸發線程不僅會將定時器事件放入任務隊列,其他滿足條件的事件也是由他負責放進任務隊列。
異步HTTP請求線程
這個線程負責處理異步的ajax請求,當請求完成后,他也會通知事件觸發線程,然后事件觸發線程將這個事件放入任務隊列給主線程執行。
所以JS異步的實現靠的就是瀏覽器的多線程,當他遇到異步API時,就將這個任務交給對應的線程,當這個異步API滿足回調條件時,對應的線程又通過事件觸發線程將這個事件放入任務隊列,然后主線程從任務隊列取出事件繼續執行。這個流程我們多次提到了任務隊列,這其實就是Event Loop,下面我們詳細來講解下。
任務隊列(Event Loop)
JavaScript 運行時,除了一個正在運行的主線程,引擎還提供多個任務隊列(根據任務的類型,所以有多個)。
首先,主線程會去執行所有的同步任務。等到同步任務全部執行完,就會去看任務隊列里面有沒有事件回調。如果有,則取出就重新進入主線程執行,這時它就變成同步任務了。等到執行完,下一個異步任務再進入主線程開始執行。一旦任務隊列清空,程序就結束執行。
異步任務的寫法通常是回調函數。一旦異步任務重新進入主線程,就會執行對應的回調函數。如果一個異步任務沒有回調函數,就不會進入任務隊列,也就是說,不會重新進入主線程,因為沒有用回調函數指定下一步的操作。
只要同步任務執行完了,引擎就會一遍又一遍地去檢查那些掛起來的異步任務,是不是可以進入主線程了。這種循環檢查的機制,就叫做事件循環(Event Loop)
目前JS的主要運行環境有兩個——瀏覽器和Node.js。相比於V8引擎,node 中增加了兩種異步方式: process.nextTick() 和 setImmediate()。
Node 規定,process.nextTick()
和Promise
的回調函數,追加在本輪循環,即同步任務一旦執行完成,就開始執行它們。
而setTimeout
、setInterval
、setImmediate
的回調函數,追加在次輪循環。
process.nextTick()
追加到本輪循環執行,而且是所有異步任務里面最快執行的,nextTickQueue。
Promise異步
function search(term) {
var url = 'http://example.com/search?q=' + term; var xhr = new XMLHttpRequest(); var result; var p = new Promise(function (resolve, reject) { xhr.open('GET', url, true); xhr.onload = function (e) { if (this.status === 200) { result = JSON.parse(this.responseText); resolve(result); } }; xhr.onerror = function (e) { reject(e); }; xhr.send(); }); return p; } search('Hello World').then(console.log, console.error);
Promise的回調函數不是正常的異步任務,而是微任務(microtask)。它們的區別在於,正常任務追加到下一輪事件循環,微任務隊列追加在process.nextTick
隊列的后面,也屬於本輪循環。
process.nextTick(() => console.log(1)); Promise.resolve().then(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4)); // 1 // 3 // 2 // 4
與正常任務比較:
setTimeout(function() {
console.log(1); }, 0); new Promise(function (resolve, reject) {
console.log(4); resolve(2); }).then(console.log); console.log(3);
// 4 // 3 // 2 // 1
// 注意: Promise構造函數中的代碼為同步執行。
setTimeout與setImmediate
setTimeout和setInterval的運行機制是,將指定的代碼移出本次執行,等到下一輪Event Loop時,再檢查是否到了指定時間。如果到了,就執行對應的代碼;如果不到,就等到再下一輪Event Loop時重新判斷。這意味着,setTimeout指定的代碼,必須等到本次事件循環執行的所有代碼都執行完,才會執行。
setTimeout的作用是將代碼推遲到指定時間執行,如果指定時間為0,即setTimeout(f,0),也不會立刻執行(放到任務隊列后,還要等待主線程空閑且任務隊列中在它之前沒有未執行的回調)。
setTimeout(f,0)將第二個參數設為0,作用是讓 f 在現有的任務(腳本的同步任務和“任務隊列”中已有的事件)一結束就立刻執行。也就是說,setTimeout(f,0)的作用是,盡可能早地執行指定的任務。
var a = 1; setTimeout(function(){ a = 2; console.log(a); }, 0); var a ; console.log(a); a = 3; console.log(a);//輸出//1//3//2 //因為先執行同步代碼
var flag = true; setTimeout(function(){ flag = false; },0) while(flag){ } console.log(flag); //什么都不會輸出,而且瀏覽器會出現卡死狀態 //因為先執行同步代碼,所以相當於一直在做while(true){}的無限循環。因此不會輸出console.log(flag)也不會執行到異步
setTimeout
在 timers 階段執行,而setImmediate
在 check 階段執行(將下方,Nodejs事件循環的六個階段)。但是,setTimeout不一定總是
早於setImmediate
完成。
console.log('outer'); setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); });
原因是,在nodejs中 setTimeout(fn, 0) 會被強制 setTimeout(fn, 1),詳情見官方文檔
1 同步代碼執行完畢,進入Event Loop 2 先進入times階段,檢查當前時間過去了1毫秒沒有。如果過了1毫秒,滿足setTimeout條件,執行回調;如果沒過1毫秒,跳過 3 跳過空的階段,進入check階段,執行setImmediate回調
但如果 setTimeout 和 setImmediate 在同一個 setTimeout 中,因為已經在timers
階段,所以里面的setTimeout
只能等下個循環了,所以setImmediate
肯定先執行。同理的還有其他poll
階段的API也是這樣的。
var fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); }); });
這里setTimeout
和setImmediate
在readFile
的回調里面,由於readFile
回調是I/O操作,屬於poll
階段,所以里面的定時器只能進入下個timers
階段,而setImmediate
卻可以在接下來的check
階段運行。所以setImmediate
先運行,運行完后,再去檢查timers
,才會運行setTimeout
。
Nodejs事件循環的六個階段
首先,事件循環是在主線程上完成的。其次,腳本開始執行時,事件循環只進行了初始化,並沒有開始。只有當下面事件執行完后,事件循環才會開始。
- 同步任務
- 發出異步請求
- 規划定時器生效的時間
- 執行
process.nextTick()
等等
事件循環會無限次地執行,一輪又一輪。只有異步任務的回調函數隊列清空了,才會停止執行。
每一輪的事件循環,分成六個階段。
- timers:定時器階段,處理
setTimeout()
和setInterval()
的回調函數。進入這個階段后,主線程會檢查一下當前時間,是否滿足定時器的條件。如果滿足就執行回調函數,否則就離開這個階段。 - I/O callbacks:除了以下操作的回調函數,其他的回調函數都在這個階段執行。【
setTimeout()
和setInterval()
的回調函數;setImmediate()
的回調函數;用於關閉請求的回調函數,比如socket.on('close', ...)】
- idle, prepare:這個階段是輪詢時間,用於等待還未返回的 I/O 事件,比如服務器的回應、用戶移動鼠標等等。
這個階段的時間會比較長。如果沒有其他異步任務要處理(比如到期的定時器),會一直停留在這個階段,等待 I/O 請求返回結果。
- poll:這個階段是輪詢時間,用於等待還未返回的 I/O 事件,比如服務器的回應、用戶移動鼠標等等。
- check:該階段執行
setImmediate()
的回調函數。 - close callbacks:該階段執行關閉請求的回調函數,比如
socket.on('close', ...)
每個階段都有一個先進先出的回調函數隊列。只有一個階段的回調函數隊列清空了,該執行的回調函數都執行了,事件循環才會進入下一個階段。
轉載自:https://www.imooc.com/article/71081
單線程模型
作者:holdtom
鏈接:https://www.imooc.com/article/71081
來源:慕課網
單線程模型
作者:holdtom
鏈接:https://www.imooc.com/article/71081
來源:慕課網