之前有寫一篇文章在討論 promise 的使用,所以之前提到的這邊就不多講了,現在將帶大家進一步窺探 promise ~

實作 Promise.all

Let’s step by step!

Promise.all 的運作方式是,全部的 promise 都 resolve 則通過。
所以我們可以得出一個簡單的公式:

裡面全部的 promise 都 resolve,則這個 promise 就會 resolve
所以我在自己設計的 promiseAll,裡面會 return 一個 promise
大概的架構會長這樣:

1
2
3
4
5
const promiseAll = (promises) => {
return new Promise((resolve, reject) => {
// ...
}
}

我需要把所有已經被 resolve 的 response 累積起來,以及計數被 resolve 的 promise,所以會這樣設計:

1
2
3
4
5
6
7
const promiseAll = (promises) => {
const resolvedPromiseResponses = [];
let numOfSolvedPromises = 0;
return new Promise((resolve, reject) => {
// ...
});
};

再來我們需要把所有的 promise 一起執行
這邊請注意,你要做的是 concurence 而不是 sequence
如果做成序列化的話,就失去意義了,所以這邊要做的是把所有 promise 並行執行:

1
2
3
4
5
6
7
8
9
const promiseAll = (promises) => {
const resolvedPromiseResponses = [];
let numOfSolvedPromises = 0;
return new Promise((resolve, reject) => {
promises.forEach((promise) => {
// ...
});
});
};

再來請思考一下 Promise.all 的特性,只要有一個 reject 就會整個 reject
所以在跑 promise 的時候,只要 reject,就使用最外層的 reject:

1
2
3
4
5
6
7
8
9
10
11
12
13
const promiseAll = (promises) => {
const resolvedPromiseResponses = [];
let numOfSolvedPromises = 0;
return new Promise((resolve, reject) => {
promises.forEach((promise) => {
promise
.then((result) => {
// ...
})
.catch((err) => reject(err));
});
});
};

Promise.all 會回傳一個 response array,所以我們直接 push 到 resolvedPromiseResponses
並且把 counter 變數 numOfSolvedPromises + 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const promiseAll = (promises) => {
const resolvedPromiseResponses = [];
let numOfSolvedPromises = 0;
return new Promise((resolve, reject) => {
promises.forEach((promise) => {
promise
.then((result) => {
resolvedPromiseResponses.push(result);
numOfSolvedPromises += 1;
// ...
})
.catch((err) => reject(err));
});
});
};

最後,在裡面加上一個判斷式讓最後一個完成的 promise 執行最外層的 resolve
只要最後一個任務被 resolve,整個 promises 就算是 resolve 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const promiseAll = (promises) => {
const resolvedPromiseResponses = [];
let numOfSolvedPromises = 0;
return new Promise((resolve, reject) => {
promises.forEach((promise) => {
promise
.then((result) => {
resolvedPromiseResponses.push(result);
numOfSolvedPromises += 1;

// 最後一個完成的promise會執行這個
if (promises.length === numOfSolvedPromises) {
resolve(resolvedPromiseResponses);
}
})
.catch((err) => reject(err));
});
});
};

測試實作的 Promise

resolve 的例子

這邊實作簡單的 pending function 來測試:

1
2
3
4
5
6
7
const sleep = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(time);
}, time);
});
};

開始測試,最後會回傳一個被 resolve 的 response array:

1
2
3
4
5
6
7
8
9
const promises = [sleep(2000), sleep(3000), sleep(1000)];
const result = promiseAll(promises);
result
.then((results) => {
console.log(results);
})
.catch((error) => {
console.log(error);
});

輸出:
[1000, 2000, 3000]

reject 的例子

測試 reject 的例子只要把某一個 case reject 就能測試囉:

1
2
3
4
5
6
7
8
9
10
const sleep = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (time === 2000) {
reject("I am wake up early!!");
}
resolve(time);
}, time);
});
};

一樣的程式測試,理論上會回傳 I am wake up early!! :

1
2
3
4
5
6
7
8
9
const promises = [sleep(2000), sleep(3000), sleep(1000)];
const result = promiseAll(promises);
result
.then((results) => {
console.log(results);
})
.catch((error) => {
console.log(error);
});

如果是實作 Promise.allsettled 呢

Promise.allsettled 的特性是,不管裡面的 promise 如何,這個 Promise 一定會被 resolve
所以我們可以把上面 catch 裏面的程式稍微調整一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const promiseAllSettled = (promises) => {
const resolvedPromiseResponses = [];
let numOfSolvedPromises = 0;
return new Promise((resolve, reject) => {
promises.forEach((promise) => {
promise
.then((result) => {
resolvedPromiseResponses.push(result);
numOfSolvedPromises += 1;

// 最後一個完成的promise會執行這個
if (promises.length === numOfSolvedPromises) {
resolve(resolvedPromiseResponses);
}
})
.catch((error) => {
resolvedPromiseResponses.push(error);
numOfSolvedPromises += 1;

// 最後一個完成的promise可能也會被reject跑到catch
if (promises.length === numOfSolvedPromises) {
resolve(resolvedPromiseResponses);
}
});
});
});
};

這樣輸出就會包含所有結果了:
[1000, “I am wake up early!!”, 3000]

針對 Promise 的添加方法

要在 Promise 添加方法,你有這兩種選擇:

  1. instance method
  2. static method / class method

這兩種我都來介紹怎麼使用,最後我們再來評估該選哪個

添加 instance method

JavaScript 是 prototype based 的程式語言,比起其他程式語言特別的地方是,他們具有 function 這個型別
我們會稱呼他為原型物件,但是我們還是會直接稱它為物件

copyright by Giamir

每個原型物件也會有自己的原型而形成原型鏈,最後直到接到 null 為止

copyright by pjchender

當我們要在特定的原型添加屬性或是函數的話,只需要直接呼叫 prototype 來實作即可:

1
2
3
4
5
6
7
8
9
function Example() {
// ...
}

Example.prototype.value = 123;

Example.prototype.myFunction = function () {
console.log("hey myFunction");
};

如此一來,基於原型實現的物件就可以擁有這些原型方法可以用了:

1
2
3
4
const a = new Example();

console.log(a.value);
a.myFunction();

static method

新增靜態方法的話,不用產生實體就可以使用方法
而新增的方法很簡單,只需要直接設置即可:

1
2
3
4
5
6
7
function Example() {
// ...
}

Example.myFunction = function () {
console.log("hey myFunction");
};

直接使用:

1
Example.myFunction();

開始評估與添加

基於以上知識之後,我們再來看看實際的 Promise.all
他是直接使用靜態方法來使用的,所以我們的選擇就會是 static method

所以我們可以這麼做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Promise.promiseAll = (promises) => {
const resolvedPromiseResponses = [];
let numOfSolvedPromises = 0;
return new Promise((resolve, reject) => {
promises.forEach((promise) => {
promise
.then((result) => {
resolvedPromiseResponses.push(result);
numOfSolvedPromises += 1;

// 最後一個完成的promise會執行這個
if (promises.length === numOfSolvedPromises) {
resolve(resolvedPromiseResponses);
}
})
.catch((err) => reject(err));
});
});
};

測試跟之前一樣,稍微改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const sleep = (time) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(time);
}, time);
});
};

const promises = [sleep(2000), sleep(3000), sleep(1000)];
const result = Promise.promiseAll(promises);
result
.then((results) => {
console.log(results);
})
.catch((error) => {
console.log(error);
});

promiseAllSettled 的實作方法也差不多,輪到你試試看吧

補充一些知識

JavaScript 是單執行緒的程式語言,在 browser 上運行則是會以主執行緒來執行
JS 在執行的 Priority 為:Sync > Nanotask > Microtask > Task / Macrotask
如果要像多執行緒一樣能夠非同步執行程式得仰賴 event loop,JS 基於以上的優先順序執行任務來執行非同步
如果時常開發 Node.js 的人,可能比較會知道這方面的知識與效能議題

Macro task

當我們在前端討論 Macro task 的時候,通常都是在講 Web API 與 V8 engine 的互動
V8 engine 擁有的 call stack 執行到 Macro task 時,就會搭配 Macro task queue 跑 event loop
我們先拿最小的 Macro task 來看,常見的 setTimeout, setInterval, I/O… 皆屬於這個
當 engine 處於忙碌狀態的時候,會把任務放到 task queue 裏面,再來依序執行
在執行的過程不會 trigger render,如果 task 執行太久也會導致任務被 block
例如:alert 被呼叫了,但是如果不關閉 alert 就不會執行接下來的 task 了

Micro task

Micro task 包含:Promise, process.nextTick, queueMicrotask…
當我們希望以非同步的方式執行同步的時候就會需要用到它們
只是在瀏覽器上,我們大部分都是使用 Promise,其他比較會在 Node.js 上看到
這也是為什麼我們在討論前端 event loop 時,直接以 Macro task 的當例子討論了

Promise 時常使用 then / catch / finally 處理程式
儘管 Promise 被 resolve,依然會繼續執行 then / catch / finally 內的程式

以下面的例子來說,你覺得順序是什麼:

1
2
3
4
5
6
7
8
// 1
setTimeout(() => alert("setTimeout"), 0);

// 2
Promise.resolve().then(() => alert("Promise"));

// 3
alert("outer");

1 是 Macrotask,所以 Web API 執行完會丟到 Macrotask queue。
2 的 Promise 始於 Microtask,會把 then 丟入 Microtask queue。
3 會先優先被執行,再來剛剛提到 Microtask 會優先被執行,所以顯示順序會是:outer, Promise, setTimeout

參考資料

Event loop: microtasks and macrotasks
Microtasks
JS 原力覺醒 Day15 - Macrotask 與 MicroTask