js單線程(線程中擁有唯一的一個事件循環)
- js分為同步任務和異步任務,同步任務都是在主線程上執行。當一個任務執行完畢后,執行后一個任務,形成一個執行棧;
- 主線程之外,事件觸發線程管著一個任務隊列,異步任務會被主線程掛起,不會進入主線程,而是進入任務隊列。只要異步任務有了運行結果,就會在隊列任務中放置一個事件;
- 一旦執行棧中所有的同步任務執行完畢后,系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中,開始執行。
為什么js是單線程的?
JS的主要用途就是與用戶交互,操作DOM,假如JS同時有兩個線程,一個線程中在某個DOM節點上添加內容,另一個線程需要執行刪除該節點操作,就會產生沖突。
事件循環機制告訴我們JavaScript的執行順序。
單線程意味著所有任務都需要排隊,前一任務結束,才會執行后一個任務,如果前一個任務耗時很長,后一個任務就不得不一直等著。
JS引擎執行異步代碼不用等待,是因為有事件隊列和事件循環。
事件循環是指主線程重復從事件隊列中取消息、執行的過程。指整個執行流程。
事件隊列是一個存儲著待執行任務的序列,其中的任務嚴格按照時間先后順序執行,排在隊頭的任務會率先執行,而排在隊尾的任務會最后執行。
事件隊列:
一個線程中,事件循環是唯一的,但是任務隊列可以有多個;
任務隊列又分macro-task(宏任務)和micro-task(微任務);
macro-task包括:script(整體代碼)、setTimeout、setInterval、setImmediate、I/O、UI rendering;
micro-task包括:process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
setTimeout/Promise等稱為任務源,而進入任務隊列的是他們制定的具體執行任務;來自不同任務源的任務會進入到不同的任務隊列,其中setTimeout與setInterval是同源的;
宏任務可以理解成每次執行棧執行的代碼就是一個宏任務。
事件運行機制
(1)執行一個宏任務(棧中沒有就從事件隊列中獲取)
(2)執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中;
(3)宏任務執行完畢后,立即執行當前微任務隊列的所有微任務;
(4)當前微任務執行完畢,開始檢查渲染,然后GUI線程接管渲染;
(5)渲染完畢后,JS線程繼續接管,開始下一個宏任務
代碼實例
async function async1() {
console.log("async1 start"); //(2)
await async2();
console.log("async1 end"); //(6)
}
async function async2() {
console.log( 'async2'); //(3)
}
console.log("script start"); //(1)
setTimeout(function () {
console.log("settimeout"); //(8)
},0);
async1();
new Promise(function (resolve) {
console.log("promise1"); //(4)
resolve();
}).then(function () {
console.log("promise2"); //(7)
});
console.log('script end');//(5)
流程解析
先按順序執行同步代碼 從‘script start‘開始,
執行到setTimeout函數時,將其回調函數加入隊列(此隊列與promise隊列不是同一個隊列,執行的優先級低于promise。
然后調用async1()方法,await async2();//執行這一句后,輸出async2后,await會讓出當前線程,將后面的代碼加到任務隊列中,然后繼續執行test()函數后面的同步代碼
繼續執行創建promise對象里面的代碼屬于同步代碼,promise的異步性體現在then與catch處,所以promise1被輸出,然后將then函數的代碼加入隊列,繼續執行同步代碼,輸出script end。至此同步代碼執行完畢。
開始從隊列中調取任務執行,由于剛剛提到過,setTimeout的任務隊列優先級低于promise隊列,所以首先執行promise隊列的第一個任務,因為在async函數中有await表達式,會使async函數暫停執行,等待表達式中的 Promise 解析完成后繼續執行 async 函數并返回解決結果。
所以先執行then方法的部分,輸出promise2,然后執行async1中await后面的代碼,輸出async1 end。。最后promise隊列中任務執行完畢,再執行setTimeout的任務隊列,輸出settimeout。
setTimeout(fn,0)的含義是指某個任務在主線程最早可得的空閑時間執行。它在“任務隊列”的尾部添加一個事件,因此要等到同步任務和“任務隊列”現有的時間處理完才會得到執行。
按照事件循環機制分析以上代碼運行流程
1. 首先,事件循環從宏任務(macrotask)隊列開始,這個時候,宏任務隊列中,只有一個script(整體代碼)任務;當遇到任務源(task source)時,則會先分發任務到對應的任務隊列中去。
2. 然后我們看到首先定義了兩個async函數,接著往下看,然后遇到了 console
語句,直接輸出 script start
。輸出之后,script 任務繼續往下執行,遇到 setTimeout
,其作為一個宏任務源,則會先將其任務分發到對應的隊列中。
3. script 任務繼續往下執行,執行了async1()函數,前面講過async函數中在await之前的代碼是立即執行的,所以會立即輸出async1 start
。
遇到了await時,會將await后面的表達式執行一遍,所以就緊接著輸出async2
,然后將await后面的代碼也就是console.log('async1 end')
加入到microtask中的Promise隊列中,接著跳出async1函數來執行后面的代碼。
4. script任務繼續往下執行,遇到Promise實例。由于Promise中的函數是立即執行的,而后續的 .then
則會被分發到 microtask 的 Promise
隊列中去。所以會先輸出 promise1
,然后執行 resolve
,將 promise2
分配到對應隊列。
5. script任務繼續往下執行,最后只有一句輸出了 script end
,至此,全局任務就執行完畢了。
根據上述,每次執行完一個宏任務之后,會去檢查是否存在 Microtasks;如果有,則執行 Microtasks 直至清空 Microtask Queue。
因而在script任務執行完畢之后,開始查找清空微任務隊列。此時,微任務中, Promise
隊列有的兩個任務async1 end
和promise2
,因此按先后順序輸出 async1 end,promise2
。當所有的 Microtasks 執行完畢之后,表示第一輪的循環就結束了。
6. 第二輪循環依舊從宏任務隊列開始。此時宏任務中只有一個 setTimeout
,取出直接輸出即可,至此整個流程結束。
事件隊列流程圖
promise對象實現
/**
* 異步解決:
* 此時我們使用一個發布訂閱者模式,在pending狀態的時候將成功的函數和失敗的函數存到各自的回調隊列數組中,等一旦reject或者resolve,就調用它們:
* 在pending態的時候將所有的要在成功態執行的方法都存到onResolveCallbacks數組中
*
鏈式解決: 遞歸處理
根據原生promise的then的用法,我們總結一下:
1.then方法如果返回一個普通的值,我們就將這個普通值傳遞給下一個then
2.then方法如果返回一個promise對象,我們就將這個promise對象執行結果返回到下一個then
普通的值傳遞很好辦,我們將第一次then的onFulfilled函數返回的值存到x變量里面,在然后resolve出去就可以了,
復雜的是then里面返回的是一個promise的時候怎么辦,因為返回的promise的我們要判斷他執行的狀態,來決定是走成功態,還是失敗態,
這時候我們就要寫一個判斷的函數resolvePromise(promise2, x, resolve, reject)來完成這個判斷
這個方法的主要作用是用來判斷x的值,如果x的值是一個普通的值,就直接返回x的值,如果x的值是一個promise,就要返回x.then() 執行的結果,核心代碼如下
*/
const judgePromise = (p2, x, resolve, reject) => {
if (p2 === x) {
return reject(
new TypeError("傳值有誤")
)
}
// 是否為promise對象
if (typeof x === "object" && x != null || typeof x === "function") {
try {
let then = x.then; //去對象的then函數
if (typeof then === "function") {
then.call(x, data => {
resolve(data);
}, err => {
reject(err);
})
} else {
resolve(x);
}
} catch (err) {
reject(err);
}
} else {
resolve(x);
}
}
class Mypromise {
constructor(executor) {
this.state = "pending"; //狀態值
this.value = undefined; //成功返回值
this.reason = undefined; //失敗返回值
this.onResolvedCallbacks = []; // 如果成功的回調函數數組
this.onRejectedCallbacks = []; // 如果失敗的回調函數數組
//成功的
let resolve = value => {
// pending用來屏蔽的,resolve和reject只能調用一個,不能同時調用,這就是pending的作用
if (this.state == "pending") {
this.state = "fullFilled";
this.value = value;
// 發布成功的事件,執行事件(訂閱發布模式--發布過程)
this.onResolvedCallbacks.forEach(fn => fn());
}
}
//失敗
let reject = reason => {
if (this.state == "pending") {
this.state = "rejected";
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFullFilled, onRejected) {
// 既然遞歸解決 第一個回調函數返回一個promise對象,以此類推...
let p2 = new Mypromise((resolve, reject) => {
// 同步的(理解觸發時的下一刻1毫秒立馬有反應,狀態立馬變化)
if (this.state == "fullFilled") {
setTimeout(() => {
try {
let x = onFullFilled(this.value);
judgePromise(p2, x, resolve, reject);
} catch (err) {
reject(err); //只要報錯reject
}
}, 0) //同步無法使用p2,所以借用setiTimeout異步的方式
}
if (this.state == "rejected") {
setTimeout(() => {
try {
let x = onFullFilled(this.reason);
judgePromise(p2, x, resolve, reject);
} catch (err) {
reject(err); //只要報錯reject
}
}, 0) //同步無法使用p2,所以借用setiTimeout異步的方式
}
// 異步(理解觸發時的下一刻1毫秒后沒有反應,狀態沒有變化)
if (this.state == "pending") {
// 在pengding狀態時,先把訂閱者的事件存到數組里(訂閱發布模式--訂閱過程)
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFullFilled(this.value);
judgePromise(p2, x, resolve, reject);
} catch (err) {
reject(err); //只要報錯reject
}
}, 0) //同步無法使用p2,所以借用setiTimeout異步的方式
})
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFullFilled(this.reason);
judgePromise(p2, x, resolve, reject);
} catch (err) {
reject(err); //只要報錯reject
}
}, 0) //同步無法使用p2,所以借用setiTimeout異步的方式
})
}
})
return p2;
}
}