javascript 是一門單線程的語言,在同一個時間只能做完成一件任務,如果有多個任務,就必須排隊,前面一個任務完成,再去執行后面的任務。作為瀏覽器端的腳本語言,javascript 的主要功能是用來和用戶交互以及操作 dom。假設 javascript 不是單線程語言,在一個線程里我們給某個 dom 節點增加內容的時候,另一個線程同時正在刪除這個 dom 節點的內容,則會造成混亂。
由于 js 單線程的設計,假設 js 程序的執行都是同步。如果執行一些耗時較長的程序,例如 ajax 請求,在請求開始至請求響應的這段時間內,當前的工作線程一直是空閑狀態, ajax 請求后面的 js 代碼只能等待請求結束后執行,因此會導致 js 阻塞的問題。
javascript 單線程指的是瀏覽器中負責解釋和執行 javascript 代碼的只有一個線程,即為 js 引擎線程,但是瀏覽器的渲染進程是提供多個線程的,如下:
為解決上述類似上述 js 阻塞的問題,js 引入了同步和異步的概念。
“同步”就是后一個任務等待前一個任務結束后再去執行。
“異步”與同步不同,每一個異步任務都有一個或多個回調函數。webapi 會在其相應的時機里將回調函數添加進入消息隊列中,不直接執行,然后再去執行后面的任務。直至當前同步任務執行完畢后,再把消息隊列中的消息添加進入執行棧進行執行。
異步任務在瀏覽器中一般是以下:
“棧”是一種數據結構,是一種線性表。特點為 LIFO,即先進后出 (last in, first out)。
利用數組的 push 和 shift 可以實現壓棧和出棧的操作。
在代碼運行的過程中,函數的調用會形成一個由若干幀組成的棧。
function foo(b) {
let a = 10;
return a + b + 11;
}
function bar(x) {
let y = 3;
return foo(x * y);
}
console.log(bar(7))
上面代碼最終會在控制臺打印42,下面梳理一下它的執行順序。
對象被分配在堆中,堆是一個用來表示一大塊(通常是非結構化的)內存區域的計算機術語。
首先,stack 是有結構的,每個區塊按照一定次序存放,可以明確知道每個區塊的大小;heap 是沒有結構的,數據可以任意存放。因此,
stack 的尋址速度要快于 heap。
其次,每個線程分配一個 stack,每個進程分配一個 heap,也就是說,stack 是線程獨占的,heap 是線程共用的。
此外,stack 創建的時候,大小是確定的,數據從超過這個大小,就發生 stack overflow 錯誤,而 heap 的大小是不確定的,
需要的話可以不斷增加。
public void Method1()
{
int i=4;
int y=2;
class1 cls1 = new class1();
}
上面代碼這三個變量和一個對象實例在內存中的存放方式如下。
從上圖可以看到,i、y和cls1都存放在stack,因為它們占用內存空間都是確定的,而且本身也屬于局部變量。但是,cls1指向的對象實例存放在heap,因為它的大小不確定。作為一條規則可以記住,所有的對象都存放在heap。
接下來的問題是,當Method1方法運行結束,會發生什么事?
回答是整個stack被清空,i、y和cls1這三個變量消失,因為它們是局部變量,區塊一旦運行結束,就沒必要再存在了。而heap之中的那個對象實例繼續存在,直到系統的垃圾清理機制(garbage collector)將這塊內存回收。因此,一般來說,內存泄漏都發生在heap,即某些內存空間不再被使用了,卻因為種種原因,沒有被系統回收。
隊列是一種數據結構,也是一種特殊的線性表。特點為 FIFO,即先進先出(first in, first out)
利用數組的 push 和 pop 可實現入隊和出隊的操作。
事件循環和事件隊列的維護是由事件觸發線程控制的。
事件觸發線程線程同樣是由瀏覽器渲染引擎提供的,它會維護一個事件隊列。
js 引擎遇到上文所列的異步任務后,會交個相應的線程去維護異步任務,等待某個時機,然后由事件觸發線程將異步任務對應的回調函數加入到事件隊列中,事件隊列中的函數等待被執行。
js 引擎在執行過程中,遇到同步任務,會將任務直接壓入執行棧中執行,當執行棧為空(即 js 引擎線程空閑), 事件觸發線程 會從事件隊列中取出一個任務(即異步任務的回調函數)放入執行在棧中執行。
執行完了之后,執行棧再次為空,事件觸發線程會重復上一步的操作,再從事件隊列中取出一個消息,這種機制就被稱為 事件循環 (Event Loop)機制。
為了更好地理解Event Loop,請看下圖(轉引自Philip Roberts的演講《Help, I'm stuck in an event-loop》)。
例子代碼:
console.log('script start')
setTimeout(() => {
console.log('timer 1 over')
}, 1000)
setTimeout(() => {
console.log('timer 2 over')
}, 0)
console.log('script end')
// script start
// script end
// timer 2 over
// timer 1 over
模擬 js 引擎對其執行過程:
此時,執行棧為空,js 引擎線程空閑。便從事件隊列中讀取任務,此時隊列如下:
注意點:
上面,timer 2 的延時為 0ms,HTML5標準規定 setTimeout 第二個參數不得小于4(不同瀏覽器最小值會不一樣),不足會自動增加,所以 "timer 2 over" 還是會在 "script end" 之后。
就算延時為0ms,只是 time 2 的回調函數會立即加入事件隊列而已,回調的執行還是得等到執行棧為空時執行。
在 ES6 新增 Promise 處理異步后,js 執行引擎的處理過程又發生了新的變化。
看代碼:
console.log('script start')
setTimeout(function() {
console.log('timer over')
}, 0)
Promise.resolve().then(function() {
console.log('promise1')
}).then(function() {
console.log('promise2')
})
console.log('script end')
// script start
// script end
// promise1
// promise2
// timer over
這里又新增了兩個新的概念, macrotask (宏任務)和 microtask (微任務)。
所有的任務都劃分到宏任務和微任務下:
js 引擎首先執行主代碼塊。
執行棧每次執行的代碼就是一個宏任務,包括任務隊列(宏任務隊列)中的。執行棧中的任務執行完畢后,js 引擎會從宏任務隊列中去添加任務到執行棧中,即同樣是事件循環的機制。
當在執行宏任務遇到微任務 Promise.then 時,會創建一個微任務,并加入到微任務隊列中的隊尾。
微任務是在宏任務執行的時候創建的,而在下一個宏任務執行之前,瀏覽器會對頁面重新渲染(task >> render >> task(任務隊列中讀取))。 同時,在上一個宏任務執行完成后,頁面渲染之前,會執行當前微任務隊列中的所有微任務。
所以上述代碼的執行過程就可以解釋了。
js 引擎執行 promise.then 時,promise1、promise2 被認為是兩個微任務按照代碼的先后順序被加入到微任務隊列中,script end執行后,棧空。
此時當前宏任務(script 主代碼塊)執行完畢,并不從當前宏任務隊列中讀取任務。而是立馬清空當前宏任務所產生的微任務隊列。將兩個微任務依次放入執行棧中執行。執行完畢,打印 promise1、promise2。棧空。 此時,第一輪事件循環結束。
緊接著,再去讀取宏任務隊列中的任務,time over 被打印。棧空。
因此,宏任務和微任務的執行機制如下:
因為,async 和 await 本質上還是基于 Promise 的封裝,而 Promise 是屬于微任務的一種。所以使用 await 關鍵字與 Promise.then 效果類似:
setTimeout(_ => console.log(4))
async function main() {
console.log(1)
await Promise.resolve()
console.log(3)
}
main()
console.log(2)
// 1
// 2
// 3
// 4
async 函數在 await 之前的代碼都是同步執行的, 可以理解為 await 之前的代碼都屬于 new Promise 時傳入的代碼,await 之后的所有代碼都是 Promise.then 中的回調,即在微任務隊列中。
參考:
原文作者:大芒果哇
原文地址:https://www.cnblogs.com/shenggao/p/13799566.html
覽器運行過程中會同時面對多種任務,用戶交互事件(鼠標、鍵盤)、網絡請求、頁面渲染等。而這些任務不能是無序的,必須有個先來后到,瀏覽器內部需要一套預定的邏輯來有序處理這些任務,因此瀏覽器事件循環誕生了,再次強調,是瀏覽器事件循環,不是javascript事件循環,js只是瀏覽器事件循環的參與者。
瀏覽器把任務區分成了 宏任務 和 微任務 或者叫 外部任務 和 內部任務 ,內部任務可以理解為js內部處理的任務,外部任務可以認為是瀏覽器處理的任務。
也可以叫宏任務隊列,瀏覽器中的外部事件源包含以下幾種:
這些外部事件源可能很多,為了方便瀏覽器廠商優化,HTML標準中明確指出一個事件循環有一個或多個外部隊列,而每一個外部事件源都有一個對應的外部隊列。不同的時間源之間可以有不同的優先級(例如在網絡時間和用戶交互之間,瀏覽器可以優先處理鼠標行為,從而讓用戶感覺更加流暢)。
也可以叫微任務隊列,指的就是javascript語言內部的事件隊列,在HTML標準中,并沒有明確規定這個隊列的事件源,通常認為有以下幾種:
以上三種除了第一個,其他兩個可以認為沒有,實際上我們js中能夠使用的就只有promise。
先來一張事件循環處理模型的截圖:
可以看出,每一個事件循環,從外部任務隊列中拿出一個來執行,執行完一個外部任務后立即執行內部任務隊列中所有內部任務(清空),然后瀏覽器執行一次渲染,然后再次循環。
了解了兩種隊列和事件循環的執行模型,下面來一段經典代碼:
// 以下代碼會得到什么樣的輸出結果?
console.log('1');
setTimeout(function() {
console.log('2');
Promise.resolve().then(function() {
console.log('3');
});
}, 0);
Promise.resolve().then(function() {
console.log('4');
}).then(function() {
console.log('5');
});
console.log('6');
答案是:164523
執行順序如下:
對于兩者的區別,來張瞟來的截圖:
這個例子的代碼如下:
setTimeout(()=>{
console.log('1');
Promise.resolve().then(function() {
console.log('2');
});
});
setTimeout(()=>{
console.log('3');
Promise.resolve().then(function() {
console.log('4');
});
});
這段代碼在瀏覽器和nodejs中的輸出結果分別是什么呢?
通過前面對瀏覽事件循環的了解,你應該很容易得出在瀏覽器中的輸出結果是: 1234
那在nodejs中的輸出結果是什么呢?結果是在nodejs的 v11.x 之前輸出1324。這之間的原因是瀏覽器有非常多的用戶交互事件,為了用戶體驗更加流暢,必須均勻的處理宏任務和微任務,而在nodejs中由于并沒有用戶交互事件,為了保證異步事件能夠被均等的執行,因此設計的初衷就是先清空宏任務隊列再清空微任務隊列。
不過你應該注意到,我上面只說了在 nodejs的 v11.x 之前輸出1324,但是nodejs這個特性在社區經歷了一波開發者的吐槽之后,node官方在 v11 這個版本緊急修復了這個問題。所以在 v11.x 以上版本執行以上代碼會得到在瀏覽器中一樣的結果。
先來張瞟來的截圖:
我們再來一個例子:
setTimeout(()=>{
console.log('1');
Promise.resolve().then(() => console.log('2'));
});
setTimeout(()=>{
console.log('3');
Promise.resolve().then(() => console.log('4'));
});
setImmediate(() => {
console.log('5');
Promise.resolve().then(() => console.log('6'));
});
setImmediate(() => {
console.log('7');
Promise.resolve().then(() => console.log('8'));
});
以上代碼在nodejsV13.x中的執行結果是12345678,接下來我們把順序調換一下,在第二個位置插入setImmediate
setTimeout(()=>{
console.log('1');
Promise.resolve().then(() => console.log('2'));
});
setImmediate(() => {
console.log('3');
Promise.resolve().then(() => console.log('4'));
});
setTimeout(()=>{
console.log('5');
Promise.resolve().then(() => console.log('6'));
});
setImmediate(() => {
console.log('7');
Promise.resolve().then(() => console.log('8'));
});
執行結果有一定的概率是12347856,也有一定的概率是 12563478
為啥不同的順序會得到不同的結果呢?這是由于setTImeout的精度問題導致的,到了這個級別的時間精度,代碼執行的時間可能都會導致結果的不同。下面這張截圖是nodejs官方文檔對于事件循環順序的展示:
其中timers階段是用于執行setTimeout事件的,check階段是用于執行setImmediate事件的。Nodejs官方這個所謂事件循環過程,其實只是完整的事件循環中Node.js的多個外部隊列相互之間的優先級。setTimeout是由event loop檢測系統時間是否到點然后向時間隊列插入一個事件,然后調用事件的回調方法。而setImmediate是監控UI線程的調用棧,一旦調用棧為空則將回調壓棧。
講了這么多,其實對于上面setTimeout和setImmediate的對比結果還是有點模糊
推測:對于setImmediate的延時有時比setTimeout的要長,由于setImmediate要先監控調用棧,若調用棧為空才壓棧,那么在壓棧之前event loop已經將setTimeout事件的回調函數壓棧了。
好了,以上是這次分享的所有內容,對于后面setTimeout和setImmedate的對比沒有的出一個明確的結果,有興趣的可以一起討論。
編寫程序時,我們經常需要重復執行某些操作,這時候循環結構就顯得非常有用。JavaScript 提供了多種循環結構,以適應不同的編程場景。以下是 JavaScript 中常見的循環結構:
for 循環是最常見的循環結構之一,它允許我們指定循環開始前的初始化代碼、循環繼續的條件以及每次循環結束時要執行的代碼。
for (初始化表達式; 循環條件; 循環后的操作表達式) {
// 循環體代碼
}
for (var i = 1; i <= 10; i++) {
console.log(i);
}
while 循環在給定條件為真時將不斷循環執行代碼塊。與 for 循環不同,while 循環只有循環條件,沒有初始化和迭代表達式。
while (條件) {
// 循環體代碼
}
var i = 1;
while (i <= 10) {
console.log(i);
i++;
}
do...while 循環和 while 循環類似,但它至少會執行一次循環體,無論條件是否為真。
do {
// 循環體代碼
} while (條件);
var i = 1;
do {
console.log(i);
i++;
} while (i <= 10);
for...in 循環用于遍歷對象的屬性。
for (var key in 對象) {
// 使用 key 訪問對象屬性
}
var person = {
name: "張三",
age: 30,
job: "軟件工程師"
};
for (var key in person) {
console.log(key + ": " + person[key]);
}
for...of 循環用于遍歷可迭代對象(如數組、字符串等)的元素。
for (var item of 可迭代對象) {
// 使用 item 訪問元素
}
var fruits = ["蘋果", "香蕉", "橘子"];
for (var fruit of fruits) {
console.log(fruit);
}
JavaScript 的循環結構為我們提供了強大的工具來處理重復任務。for 循環適合于當我們知道循環次數時使用;while 和 do...while 循環適合于循環次數未知,但是循環條件明確的情況;for...in 和 for...of 循環則讓對象和數組的遍歷變得更加簡潔。掌握這些循環結構有助于我們編寫更加高效和可讀性更強的代碼。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。