在說 瀏覽器事件環 以前,先說幾組概念:node
堆中存的是引用數據類型,是動態分配的內存,大小不定也不會自動釋放。git
棧中存的是基本數據類型,會自動分配內存空間,自動釋放;ajax
給張圖片瞭解一下:數組
使用JS代碼實現隊列和棧的功能(就是用數組的增刪方法):promise
let arr = new Array();
arr.push(1);
arr.push(2);
arr.shift();
複製代碼
let arr = new Array();
arr.push(1);
arr.push(2);
arr.pop();
複製代碼
首先,進程確定要比線程大,一個程序至少要有一個進程,一個進程至少要有一個線程。
下面看一張瀏覽器的工做機制:瀏覽器
因而可知,瀏覽器就是多進程的,當一個網頁崩潰時不會影響其餘網頁的正常運行。每一個進程管理着瀏覽器不一樣的部分,主要分爲如下幾種:bash
其中渲染引擎內部有三個線程是咱們着重須要關注的網絡
其中js線程和ui線程是互斥的,數據結構
當js執行的時候可能ui還在渲染,那麼這時ui線程會把更改放到隊列中 當js線程空閒下來 ui線程再繼續渲染併發
除此以外還有一些其它的線程,這也是咱們分發異步任務時用到的線程
說一個老生常談的問題:
JS是單線程的,任務是須要一個一個按順序執行的,可是說的是 JS的主線程是單線程 的,他能夠建立子線程,來幫他完成任務。
同步和異步關注的是消息通知機制
任務隊列:
同步任務 是指在主線程上執行的任務,只有前一個任務執行完畢,下一個任務才能執行。
異步任務 是指不進入主線程,而是進入任務隊列(task queue)的任務,只有主線程任務執行完畢,任務隊列的任務纔會進入主線程執行。
實現過程:
1.全部同步任務都在主線程上執行,造成一個執行棧;
2.只要異步任務有了運行結果,就在任務隊列(task queue)(隊列是一個先進先出的數據結構,而棧是一個先進後出的數據結構)之中放置一個事件;
3.一旦執行棧中的全部同步任務執行完畢,系統就會讀取任務隊列,又將隊列中的事件放到stack中依次執行,就是執行異步任務中的回調函數。這個過程是循環不斷的,這就是Event Loop(事件循環);
關於線程進程,同步異步,想了解更多請參考個人文章《進程與線程、同步與異步、阻塞與非阻塞、併發與並行》
在上面的異步任務中又分爲兩種:宏任務 和 微任務
常見的宏任務和微任務:
macro-task
(宏任務,優先級低,先定義的先執行): ajax,setTimeout, setInterval, setImmediate, I/O,事件,postMessage,MessageChannel(用於消息通信)
micro-task
(微任務,優先級高,而且能夠插隊,不是先定義先執行):process.nextTick, 原生 Promise(有些實現的promise將then方法放到了宏任務中),Object.observe(已廢棄), MutationObserver
Promise自己是同步的,Promise.then是異步的
宏任務和微任務的區別:微任務是會被加入本輪循環的,而宏任務都是在次輪循環中被執行。簡單就是說,微任務會比宏任務提早執行
簡單的說就是:由於微任務的優先級較高,因此會先將微任務的異步任務取出來進行執行,當微任務的任務都執行完畢以後,會將宏任務中的任務取出來執行。
本輪循環是指什麼呢?JS主線程會從任務隊列中提取任務到執行棧中執行,每一次執行均可能會再產生一個新的任務,對於這些任務來講此次執行到下一次從任務隊列中提取新的任務到執行棧以前就是這些新生任務的本輪。
給出一張網上很火的一張圖:
從上圖看出:
1.主線程運行的時候產生堆(heap)和棧(stack)
2.棧中的代碼調用各類外部API,它們在"任務隊列"中加入各類事件(例如:click,load,done)
3.只要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",將隊列中的事件放到執行棧中依次執行。
4.主線程繼續執行,當再調用外部API時又加入到任務隊列中,等主線程執行完畢又會接着將任務隊列中的事件放到主線程中。
5.上面整個過程是循環不斷的。
例題(執行順序):
JS代碼本質上仍是從上往下執行的
//例題1
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
複製代碼
輸出結果順序:script start --> script end --> promise1 --> promise2 --> setTimeout
1.先執行同步任務,輸出script star
script end
2.而後執行異步任務,先執行異步任務的微任務,輸出 promise1
3.接着返回了一個Promise
,而後又.then
,仍是微任務,接着執行,輸出 promise2
4.最後執行異步任務中的宏任務,輸出 setTimeout
//例題2
console.log(1);
setTimeout(function(){
console.log(2);
new Promise(function(resolve,reject){
console.log(3);
resolve();
}).then(res=>{
console.log(4);
})
});
setTimeout(function(){
console.log(5);
})
console.log(6);
複製代碼
輸出結果順序:1 6 2 3 4 5
1.執行棧中同步任務先執行,先走console.log(1)
和console.log(6)
;
2.接着是遇到setTimeout
將它們的回調函數放入MacroTask
(宏任務隊列);
3.而後將任務隊列中的回調函數依次放入主執行棧中執行,console.log(2)
,接着console.log(3)
;是當即執行,console.log(4)
;是微任務放入MicroTask
中先執行;
4.最後執行第二個setTimeout
的回調函數console.log(5)
;
//例題3
setTimeout(() => {
console.log('setTimeout1');
Promise.resolve().then(data => {
console.log('then3');
});
},1000);
Promise.resolve().then(data => {
console.log('then1');
});
Promise.resolve().then(data => {
console.log('then2');
setTimeout(() => {
console.log('setTimeout2');
},1000);
});
console.log(2);
複製代碼
輸出結果順序:2 then1 then2 setTimeout1 then3 setTimeout2
1.先執行棧中的內容,也就是同步代碼,因此2
被輸出出來;
2.而後清空微任務,因此依次輸出的是 then1
then2
;
3.因代碼是從上到下執行的,因此1s後 setTimeout1
被執行輸出;
4.接着再次清空微任務,then3
被輸出;
5.最後執行輸出setTimeout2
下面例題就不一一分析了,能夠本身嘗試運行並分析一下
例題4
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(6);
});
}, 0);
Promise.resolve(3).then((data) => {
console.log(data);
return data + 1;
}).then((data) => {
console.log(data)
setTimeout(() => {
console.log(data + 1)
return data + 1;
}, 1000)
}).then((data) => {
console.log(data);
});
複製代碼
輸出結果順序:1 3 4 undefined 2 6 5
//例題5
setTimeout(() => {
console.log('A');
}, 0);
var obj = {
func: function () {
setTimeout(function () {
console.log('B')
}, 0);
return new Promise(function (resolve) {
console.log('C');
resolve();
})
}
};
obj.func().then(function () {
console.log('D')
});
console.log('E');
複製代碼
輸出結果順序:C E D A B
這裏內容不作過多解釋
1. setTimeout,setImmediate誰先誰後?
setImmediate
的回調永遠先執行。2. nextTick和promise.then誰快?
3. nextTick和其它的定時器嵌套
setImmediate(function(){
console.log(1);
process.nextTick(function(){
console.log(4);
})
})
process.nextTick(function(){
console.log(2);
setImmediate(function(){
console.log(3);
})
})
2 1 3 4
複製代碼
緣由在於nextTic
k在node
中的執行實際和瀏覽器中不徹底同樣,雖然它們在第一次進入事件環時都會先執行,但若是後續還有nextTick
加入,node
中只會在階段轉換時纔會去執行,而瀏覽器中則是一有nextTick
加入就會當即執行。
形成這樣區別的緣由在於,node
中的事件環是有6種狀態的,每種狀態都是一個callbcak queue
,只有當一個狀態的callback queue
中存放的回調都清空後纔會執行nextTick
。
4. 定時器指定的回調函數必定會在指定的時間內執行嗎?
不必定,先不說node中事件環的六種狀態之間轉化時的貓膩,光是瀏覽器中的事件環也可能由於本輪循環的執行時間過長,長的比定時器指定的事件還長從而致使定時器的回調觸發被延誤。
node11.x版本以後,node事件環就慢慢和瀏覽器的事件環同樣了,這裏就先不做過多解釋