Skip to main content

Promise

Introduction: callbacks

function หลาย ๆ ตัว ถูกกำหนดด้วย host environments ที่จะทำให้เราเขียน asynchronous actions ได้. พูดอีกแบบคือ การกระทำที่เราทำตอนนี้ จะไปจบอีกทีนึง

ตัวอย่างเช่น setTimeout function

และยังมีตัวอย่างในงานจริงของ asynchronous actions เช่น การโหลด script และ modules

ลองมาดูตัวอย่าง loadScript(src) ที่จะโหลด script ตาม src ที่ให้


function loadScript(src) {
// creates a <script> tag and append it to the page
// this causes the script with given src to start loading and run when complete
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}

เป็นการแทรก <script src="…"> เข้าไปใน document และ Browser จะเริ่มโหลด script อัตโนมัติ และ execute เมื่อโหลดเสร็จ

เราสามารถเรียกใช้ function ได้แบบนี้

// load and execute the script at the given path
loadScript('/my/script.js');

script นี้ execute แบบ “asynchronously”, โดยมันเริ่มโหลดตอนนี้ แต่จะรันทีหลังเมื่อโหลดเสร็จ

แต่ถ้ามีโค้ดใต้ loadScript โค้ดข้างล่างจะไม่รอมันโหลดเสร็จ

loadScript('/my/script.js');
// the code below loadScript
// doesn't wait for the script loading to finish
// ...

ลองบอกว่าเราต้องการที่ใช้ script หลังจากที่มันโหลดเสร็จ และภายใน script นั้น ประกาศ function ใหม่ไว้ และต้องการที่จะเรียกใช้ function นั้นหลักจากโค้ดบรรทัด loadScript(…) . มันจะไม่สามารถทำงานได้:

loadScript('/my/script.js'); // the script has "function newFunction() {…}"

newFunction(); // no such function!

แน่นอนว่า Browser อาจไม่สามารถโหลด script ได้ทัน. และเรา ก็ไม่สามารถรู้ได้ว่า loadScript จะโหลดเสร็จตอนไหน และเราต้องการที่จะรู้ว่า script นั้น โหลดเสร็จตอนไหน

เรามาเพิ่ม callback function เป็น argument ตัวที่สองของ loadScript และมันควร execute เมื่อ script โหลดเสร็จ

function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;

script.onload = () => callback(script);

document.head.append(script);
}

onload event ถูกอธิบายไว้ใน Resource loading: onload and onerror, ปกติมันจะทำ function หลังจากที่ script โหลดเสร็จ และถูก execute

และถ้าเราต้องการที่จะเรียกใช้ function เราก็แค่ไปเขียนใน callBack function

loadScript('/my/script.js', function() {
// the callback runs after the script is loaded
newFunction(); // so now it works
...
});

แนวคิดของมัน: argument ตัวที่สอง เป็น function ที่จะทำงานหลังจาก script โหลดเสร็จ

ตัวอย่างการใช้งานจริง:

function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
alert(`Cool, the script ${script.src} is loaded`);
alert( _ ); // _ is a function declared in the loaded script
});

นั่นเรียกว่า “callback-based” เป็นรูปแบบหนึ่งในการเขียน asynchronous


Callback in callback

เราจะโหลด 2 script ตามลำดับได้อย่างไร เช่น โหลดตัวแรกก่อน หลังจากนั้นค่อยโหลดตัวที่สอง

solution โดยปกติเราเรียกใช้ loadScript ภายใน callBack อีกที

loadScript('/my/script.js', function(script) {

alert(`Cool, the ${script.src} is loaded, let's load one more`);

loadScript('/my/script2.js', function(script) {
alert(`Cool, the second script is loaded`);
});

});

และถ้าเราต้องการจะโหลดอีกหลาย ๆ ตัวล่ะ

loadScript('/my/script.js', function(script) {

loadScript('/my/script2.js', function(script) {

loadScript('/my/script3.js', function(script) {
// ...continue after all scripts are loaded
});

});

});

Handling errors

ในตัวอย่างข้างบน เราไม่ได้คำนึงถึง Error ที่จะเกิด จะเกิดอะไรขึ้น ถ้าการโหลด script ของเราเกิดไม่สำเร็จขึ้นมา callBack ของเราควรที่จะตอบสนองกับสิ่งนั้น

นี่คือ loadScript เวอร์ชันปรับปรุงที่สามารถตอบโต้กับ Error ได้:

function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;

script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));

document.head.append(script);
}

เมื่อ Script โหลดสำเร็จมันเรียกใช้ callback(null, script) และ callback(error) เมื่อไม่สำเร็จ

loadScript('/my/script.js', function(error, script) {
if (error) {
// handle error
} else {
// script loaded successfully
}
});

ย้ำอีก 1 ครั้ง โค้ดที่เราใช้กับ loadScript ค่อนข้างจะเป็นพื้นฐาน ซึ่งมันเป็นรูปแบบ “error-first callback”


Pyramid of Doom

เมื่อมองแวบแรก ดูเหมือนว่าการเขียนโค้ดแบบนี้ จะสามารถทำงานได้ดี และมันก็ทำงานได้ดีจริง ๆ สำหรับการโหลดเพียง 1 หรือ 2 script

แต่สำหรับการโหลดหลาย ๆ script นั้น เราจะมีโค้ดที่เป็นแบบนี้

loadScript('1.js', function(error, script) {

if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...continue after all scripts are loaded (*)
}
});

}
});
}
});

ในโค้ดด้านบน:

  1. เราโหลด 1.js, ไปต่อถ้าไม่มี Error
  2. เราโหลด 2.js, ไปต่อถ้าไม่มี Error
  3. เราโหลด 3.js, ไปต่อถ้าไม่มี Error – ทำอย่างอื่นต่อ (*).

เมื่อเรายิ่งมีโค้ดมากขึ้น โค้ดก็จะลึกขึ้นและจัดการได้ยากมากขึ้น

และการเขียนโค้ดแบบนี้เรียกว่า “callBack hell” หรือ “pyramid of doom”

image.png

pyramid ของการเรียกใช้ซ้อนกันไปเรื่อย ๆ จะยาวต่อไปในทางขวา และเมื่อเราเขียนแบบนี้ไปเรื่อย ๆ เราก็จะจัดการโค้ดได้ยากมากขึ้น

ซึ่งในการเขียนโค้ดแบบนี้ ไม่ดีมากนัก

เราสามารถแก้ปัญหานี้ได้โดยการ ทำให้ทุก action เป็น standalone function

loadScript('1.js', step1);

function step1(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', step2);
}
}

function step2(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', step3);
}
}

function step3(error, script) {
if (error) {
handleError(error);
} else {
// ...continue after all scripts are loaded (*)
}
}

เห็นไหม ว่ามันทำงานเหมือนกัน และโค้ดก็อ่านง่ายมากกว่าด้วย


Promise

ลองนึกภาพว่าคุณเป็นนักร้องชั้นนำ แล้วแฟนๆ ก็ถามหาเพลงใหม่ของคุณทั้งวันทั้งคืน

เพื่อทำให้ทุกคนสบายใจ คุณสัญญาว่าจะส่งเพลงไปให้พวกเขาเมื่อมีการเผยแพร่ คุณให้แฟน ๆ ของคุณ กรอกที่อยู่อีเมลของเขา เพื่อว่าเมื่อเพลงพร้อมเผยแพร่ ทุกคนที่สมัครจะได้รับเพลงทันที และแม้ว่าจะมีอะไรผิดพลาดร้ายแรง เช่น ไฟไหม้ในสตูดิโอ ซึ่งคุณไม่สามารถเผยแพร่เพลงได้ พวกเขาจะยังคงได้รับการแจ้งเตือน

ทุกคนมีความสุข เพราะคุณไม่โดดแฟน ๆ รุม และแฟน ๆ ก็จะไม่พลาดเพลงของคุณ

นี่เป็นการเปรียบเทียบชีวิตจริงกับการเขียนโค้ด

  1. producing code ทำบางอย่างที่ใช้เวลา ตัวอย่างเช่น การโหลดข้อมูลบน network เปรียบเป็น “นักร้อง”
  2. consuming code ที่ต้องการผลลัพธ์จาก producing code เมื่อมันพร้อมแล้ว ซึ่ง หลาย ๆ function ต้องการผลลัพธ์นั้น เปรียบเป็น “แฟนคลับ”
  3. promise เป็น Object พิเศษของ JavaScript ที่จะเชื่อม “producing code” และ “consuming code” เข้าด้วยกัน โดยที่ producing code จะใช้เวลาเท่าที่ต้องการ เพื่อส่งผลลัพธ์ ออกมาและ promise จะทำหน้าที่ทำให้ข้อมูลนั้นสามารถใช้งานได้เมื่อมันพร้อมใช้งาน

้ด ในการเปรียบเทียบนี้ ไม่ได้ถูกต้องมากนัก เพราะว่า promise ใน JavaScript นั้น ค่อนข้างซับซ้อนมากกว่าที่กล่าวมา พวกมันยังมีข้อจำกัด และสิ่งเพิ่มเติมอื่น ๆ อีก

syntax ของ promise:

 let promise = new Promise(function(resolve, reject) {
// executor (the producing code, "singer")
});

function ที่ถูกส่งเข้าไปใน new Promise จะถูกเรียกว่า executor. เมื่อ new Promise ถูกสร้างขึ้น executor จะถูกรันโดยอัตโนมัติ. มันเก็บ producing code ไว้ ซึ่งจะส่งผลลัพธ์ออกมาในที่สุด ในการเปรียบเทียบแล้ว โค้ด executor ถูกเปรียบเป็น “นักร้อง”

โดย resolve และ reject คือ callBack ของ JavaScript เอง และโค้ดของเราจะอยู่แค่ภายใน executor

โดยสรุปแล้ว executor จะรันโดยอัตโนมัติ และทำงานของมัน และเมื่อมันทำหน้าที่ของมันเสร็จแล้ว มันจะเรียกใช้ resolve แต่ถ้าไม่สำเร็จ มันจะเรียกใช้ reject

promise จะถูกรีเทิร์นโดย new Promise และมันมี properties ภายในอยู่:

  • state เริ่มแรกจะเป็น "pending" , หลังจากนั้นเมื่อ resolve ถูกใช้ จะเปลี่ยนค่าเป็น "fulfilled" ในทางตรงกันข้าม มันจะเปลี่ยนเป็น "rejected" แทน
  • result เริ่มแรกจะเป็น undefined และจะเปลี่ยนเป็น value เมื่อ resolve(value) ถูกเรียกใช้ และในทางตรงข้ามจะเปลี่ยนเป็น error แทน

image.png

ตัวอย่างของ promise ที่ใช้ producing code ใช้เวลาในการทำงานของมัน (โดยใช้ setTimeout)

let promise = new Promise(function(resolve, reject) {
// the function is executed automatically when the promise is constructed

// after 1 second signal that the job is done with the result "done"
setTimeout(() => resolve("done"), 1000);
});

เราสามารถสองอย่างของโค้ดด้านบน

  1. executor ถูกเรียกใช้โดยทันที

  2. executor รับมาสอง argument resolve และ reject , function สองตัวนี้ถูกประกาศไว้ก่อนแล้ว โดย JavaScript และเราไม่จำเป็นที่จะสร้างมัน

    หลังจาก 1 วินาทีของการประมวลผล, executor เรียกใช้ resolve("done") ในการให้ผลลัพธ์ออกมา มันเปลี่ยน state ของ Promise

image.png

ตัวอย่างของ code เมื่อ executor เจอ Error

let promise = new Promise(function(resolve, reject) {
// after 1 second signal that the job is finished with an error
setTimeout(() => reject(new Error("Whoops!")), 1000);
});

ในการเรียกใช้ reject(...) มันจะเปลี่ยน state ของ Object เป็น "rejected" :

image.png

โดยสรุปแล้ว executor จะทำงานของมัน และเรียกใช้ resolve และ reject ในการเปลี่ยนแปลง state ให้สอดคล้องกับ promise Object


Consumers: then, catch

Promise object ทำหน้าที่เหมือนกับการเชื่อม executor และ consuming function เข้าด้วยกัน โดยที่จะรับ result หรือ error. Consuming functions สามารถติดตามค่าได้โดยการใช้ .then และ .catch.

then

สิ่งพื้นฐานที่สำคัญที่สุดคือ .then

promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
);

argument แรกของ .then คือ เมื่อ promise เป็น resolved และรับค่ามา

argument ตัวที่สองของ .then คือเมื่อ promise เป็น rejected และรับค่า Error มา

ตัวอย่างของการทำงานสำเร็จ

let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});

// resolve runs the first function in .then
promise.then(
result => alert(result), // shows "done!" after 1 second
error => alert(error) // doesn't run
);

function แรก ถูก execute

และในกรณีของ rejection

let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// reject runs the second function in .then
promise.then(
result => alert(result), // doesn't run
error => alert(error) // shows "Error: Whoops!" after 1 second
);

ถ้าเราต้องการแสดงผล แค่เมื่อเวลาที่มันสำเร็จ เราสามารถใส่ argument แค่ตัวเดียวได้

let promise = new Promise(resolve => {
setTimeout(() => resolve("done!"), 1000);
});

promise.then(alert); // shows "done!" after 1 second

catch

ถ้าเราสนใจแค่ Error เราสามารถใช้ null เป็น argument ตัวแรกได้ .then(null, errorHandlingFunction) หรือเราสามารถใช้ .catch(errorHandlingFunction) ซึ่งจะทำหน้าที่เหมือนกัน

let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});

// .catch(f) is the same as promise.then(null, f)
promise.catch(alert); // shows "Error: Whoops!" after 1 second

Cleanup: finally

เหมือนกับ finally ใน try {...} catch {...} มันยังมี finally ใน promise อีกด้วย

ในการเรียกใช้ .finally(f) เหมือนกับการเรียกใช้ .then(f, f) ในแง่ที่ว่าไม่ว่าจะเป็น resolve หรือ reject

function f ก็จะทำงานอยู่เสมอ

แนวคิดของ finally คือการจัดการสำหรับดำเนินการ cleanup/finalizing หลังจากการทำงานก่อนหน้านี้เสร็จสิ้น

ตัวอย่างเช่น หยุดหน้าโหลด, ปิด connections ที่ไม่ต้องการ และ อื่น ๆ

ให้ลองคิดเหมือนกับเวลา party เสร็จ ไม่สำคัญว่า party จะดีหรือไม่ดี สุดท้ายก็ต้องทำความสะอาดมันอยู่ดี

Code จะลักษณะประมาณนี้

new Promise((resolve, reject) => {
/* do something that takes time, and then call resolve or maybe reject */
})
// runs when the promise is settled, doesn't matter successfully or not
.finally(() => stop loading indicator)
// so the loading indicator is always stopped before we go on
.then(result => show result, err => show error)

โปรดจำไว้ว่า finally(f) ไม่ได้เหมือนกับ then(f,f) ซักทีเดียว

ข้อแตกต่างที่สำคัญ

  1. finally ไม่มีการรับ argument. ใน finally เราไม่สามารถรู้ได้ว่า promise สำเร็จหรือไม่

  2. finally จะทำการส่งผ่าน result หรือ Error ไปที่ handler ตัวถัดไป

    ตัวอย่างเช่น finally ส่งผ่าน then ไป

    new Promise((resolve, reject) => {
    setTimeout(() => resolve("value"), 2000);
    })
    .finally(() => alert("Promise ready")) // triggers first
    .then(result => alert(result)); // <-- .then shows "value"

    อย่างที่เราเห็น value ถูก return ด้วย promise ตัวแรก ผ่าน finally ไปที่ then

    เพราะว่า finally ไม่ได้ถูกสร้างมาเพื่อประมวลผลผลลัพธ์ของ promise อย่างที่พูดไป มันทำหน้าที่ cleanup ไม่สำคัญว่า ผลลัพธ์จะออกมาเป็นอะไร

    ตัวอย่างที่ใช้กับ catch :

    new Promise((resolve, reject) => {
    throw new Error("error");
    })
    .finally(() => alert("Promise ready")) // triggers first
    .catch(err => alert(err)); // <-- .catch shows the error
  3. finally handler ไม่ควร return ค่าอะไรออกมา, ค่าที่ถูก return ออกมาจะถูกละเว้น

    ข้อยกเว้นเดียวของมันคือ finally จะได้โยน Error และ Error นั้นจะไปที่ handler ตัวต่อไป

โดยสรุปแล้ว

  • finally handler ไม่ได้รับค่าที่มาจาก handler ตัวก่อนหน้า (มันไม่มี argument). โดย outcome ที่ได้ออกมา จะถูกส่งผ่านไปที่ handler ตัวถัดไป
  • ถ้า finally return ค่าซักอย่างออกมา มันจะถูกละเว้น
  • ถ้า finally โยน Error ออกมา Error จะถูกส่งไปที่ handler ตัวที่ใกล้ที่สุด

Example: loadScript

เรามาดูตัวอย่างในทางปฏิบัติกันดีกว่า ว่าเราสามารถใช้ promises ในการเขียนโค้ดได้อย่างไร

เรามี loadScript function จากบทเรียนที่แล้ว

function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;

script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));

document.head.append(script);
}

เรามาลองเขียนใหม่โดยใช้ Promise กันเถอะ

เราจะสร้าง loadScript โดยที่เราจะไม่ใช้ callBack ในทางกลับกัน เราจะสร้าง และ rerturn Promise object หลังจากที่มันทำงานเสร็จสิ้นแล้ว และเพิ่ม handlers ให้กับมัน

function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;

script.onload = () => resolve(script);
script.onerror = () => reject(new Error(`Script load error for ${src}`));

document.head.append(script);
});
}

การใช้งาน

let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");

promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`)
);

promise.then(script => alert('Another handler...'));

Promises chaining

กลับไปปัญหาที่เรากล่าวถึงในบท Introduction: callbacks เรามีลำดับการทำงานแบบ asynchronous ที่ต้องทำงานทีละงาน — ตัวอย่างเช่นการโหลด script เราจะเขียนโค้ดมันอย่างไร

การใช้ Promise มี 2 วิธีที่จะสามารถทำได้

ในบทนี้ เราจะใช้วิธี Promise chaining

 new Promise(function(resolve, reject) {

setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

alert(result); // 1
return result * 2;

}).then(function(result) { // (***)

alert(result); // 2
return result * 2;

}).then(function(result) {

alert(result); // 4
return result * 2;

});

แนวคิดของมันคือ ผลลัพธ์จะผ่าน Chain ไปเรื่อย ๆ ผ่าน .then handler

หลักการทำงานองมัน:

  1. promise ตัวแรก ส่งค่าให้ภายใน 1 วินาที
  2. .then ถูกเรียกใช้ และรับผลลัพธ์จากตัวแรก หลังจากนั้นสร้าง promise ใหม่ขึ้นมา และส่งต่อผลลัพธ์ต่อไป
  3. .then ถูกเรียกใช้ รับค่าจากตัวก่อนหน้ามา และทำการ processes (เพิ่มค่าคูณ 2) และส่งไปที่ตัวถัดไป
  4. และไปเรื่อย ๆ

image.png

ทุกสิ่งทำงานได้ เพราะว่าทุกครั้งที่เรียกใช้ .then มันจะ return promise ใหม่มาและเราก็เรียกใช้ .then ไปเรื่อย ๆ เมื่อ handler return ค่าออกมา มันจะมาเป็นผลลัพธ์ของ promise ดังนั้น .then ตัวต่อไป จึงเรียกใช้งานได้

ข้อผิดพลาดของมือใหม่: ตามเทคนิคเราสามารถใช้ .then กับ promise ตัวเดียวได้ ที่ไม่ใช่ chaining

ตัวอย่างเช่น

let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});

promise.then(function(result) {
alert(result); // 1
return result * 2;
});

promise.then(function(result) {
alert(result); // 1
return result * 2;
});

promise.then(function(result) {
alert(result); // 1
return result * 2;
});

ที่เราทำคือการที่ เพิ่ม handler หลาย ๆ ตัวไปที่ promise แค่ตัวเดียว. พวกมันไม่ได้ส่งผลลัพธ์ให้กัน แต่มันจะประมวลผลแยกกัน

image.png

.then ทุกตัวจะได้ผลลัพธ์เดียวกัน ดังนั้นโค้ดด้านบนทั้งหมด จะโชว์ผลลัพธ์ alert เหมือนกันหมดคือ 1


Returning promises

handler ที่ใช้ใน .then(handler) มันอาจจะสร้างและ return promise ได้

ในกรณีที่ handlers ต้องรอการที่จะได้ผลลัพธ์ออกมา

new Promise(function(resolve, reject) {

setTimeout(() => resolve(1), 1000);

}).then(function(result) {

alert(result); // 1

return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});

}).then(function(result) { // (**)

alert(result); // 2

return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});

}).then(function(result) {

alert(result); // 4

});

.then ตัวแรกโชว์ค่า 1 ออกมาและ return promise ออกไปในบรรทัด (*) หลังจาก 1 วินาที มันส่งผลลัพธ์ไปที่ handler ตัวถัดไป และ handler ตัวถัดไป ก็โชว์ค่า 2 และเหมือนกัน handler ตัวสุดท้าย ก็โชว์ค่า 4

ดังนั้น ผลลัพธ์ที่เราได้ก็เหมือนกับ ตัวอย่างก่อนหน้า 1 → 2 → 4 แต่เราได้เพิ่ม delay เข้าไป 1 วินาที


Example: loadScript

เรามาลองใช้ feature ของ promise ในการโหลด script กัน โดยโหลดไปเรื่อย ๆ ตามลำดับ

loadScript("/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("/article/promise-chaining/three.js");
})
.then(function(script) {
// use functions declared in scripts
// to show that they indeed loaded
one();
two();
three();
});

เราสามารถเขียนให้สั้นลงได้ โดยการใช้ Arrow function

loadScript("/article/promise-chaining/one.js")
.then(script => loadScript("/article/promise-chaining/two.js"))
.then(script => loadScript("/article/promise-chaining/three.js"))
.then(script => {
// scripts are loaded, we can use functions declared there
one();
two();
three();
});

loadScript อีกตัว คืนค่า promise ออกไป และ .then ตัวต่อไปจะถูกรันเมื่อตัวก่อนหน้าโหลดเสร็จแล้ว

เราสามารถเพิ่ม asynchronous actions เข้าไปอีกได้ เพราะว่าโค้ดที่เราเห็นยังสวยงามอยู่ และโค้ดไม่ได้โตไปทางขวา และไม่มีสัญญาณของการเกิด “pyramid of doom”


Bigger example: fetch

ในการเขียน front-end, promises จะถูกใช้เพื่อ network requests. เรามาดูตัวอย่างเพิ่มเติมกัน

เราจะใช้ method fetch ในการ โหลดข้อมูล จาก server

let promise = fetch(url);

นี่ทำให้ network สร้าง request ไปที่ url และ return promise ออกมา. โดย promise resolve มากับ response Object เมื่อ server responds header มา แต่ว่าก่อนที่ response ทั้งหมดจะถูกโหลด

ในการที่จะอ่านข้อมูล เราจะใช้ response.text() มันจะ return promise resolve เมื่อข้อมูลทั้งหมดถูกโหลดเสร็จแล้ว

โดยโค้ดด้านล้าง จะสร้าง request ไปที่ user.json และโหลดข้อความมาจาก server

fetch('/article/promise-chaining/user.json')
// .then below runs when the remote server responds
.then(function(response) {
// response.text() returns a new promise that resolves with the full response text
// when it loads
return response.text();
})
.then(function(text) {
// ...and here's the content of the remote file
alert(text); // {"name": "iliakan", "isAdmin": true}
});

response object จะถูก return ค่ามาจาก fetch ที่จะมี method response.json() ที่อ่านข้อมูลของ json

ในกรณีของเรานั้นสะดวกกว่า ดังนั้นเรามาเปลี่ยนไปใช้กันดีกว่า

เราจะใช้ Arrow function กัน

fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => alert(user.name)); // iliakan, got user name

เรามาทำบางอย่างกับข้อมูล user กัน

ตัวอย่างเช่น เราจะสร้าง request ไปที่ Github, โหลด profile user ออกมา และแสดง avatar

// Make a request for user.json
fetch('/article/promise-chaining/user.json')
// Load it as json
.then(response => response.json())
// Make a request to GitHub
.then(user => fetch(`https://api.github.com/users/${user.name}`))
// Load the response as json
.then(response => response.json())
// Show the avatar image (githubUser.avatar_url) for 3 seconds (maybe animate it)
.then(githubUser => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);

setTimeout(() => img.remove(), 3000); // (*)
});

เราสามารถเห็นรายละเอียดใน comment ได้ แต่มันอาจมีปัญหาเกิดขึ้น ข้อผิดพลาดนี้สามาถเห็นได้เวลาใช้ promise

ดูที่บรรทัด (*) เราจะสามาถทำอย่างอื่นได้หลังจาก avatar โชว์เสร็จแล้วได้อย่างไร ตัวอย่างเช่น เราต้องการที่จะโชว์ Form ในการแก้ไข user หรืออย่างอื่น ณ ตอนนี้ เราไม่สามารถทำได้

ในการทำสิ่งอื่น ๆ ต่อได้ เราต้อง return ค่า promise ที่ resolve เมื่อ avatar โขว์เสร็จแล้วออกไป

fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise(function(resolve, reject) { // (*)
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);

setTimeout(() => {
img.remove();
resolve(githubUser); // (**)
}, 3000);
}))
// triggers after 3 seconds
.then(githubUser => alert(`Finished showing ${githubUser.name}`));

.then ในบรรทัด (*) **return **new Promise ** จะทำงานได้ก้ต่อเมื่อเรียกใช้ resolve(githubUser) ใน setTimeout() /(**) และ .then ตัวต่อไปจะรอมัน

good practice. asynchronous action ควรจะ return promise ออกมา เพื่อที่จะทำงานอย่างอื่นต่อได้ ถึงแม้เราจะไม่ได้ใช้ผลลัพธ์ของมันต่อ

เราแยกโค้ดเป็น function ที่สามารถใช้ซ้ำได้

function loadJson(url) {
return fetch(url)
.then(response => response.json());
}

function loadGithubUser(name) {
return loadJson(`https://api.github.com/users/${name}`);
}

function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);

setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}

// Use them:
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
// ...

Summary

ถ้า .then handler return ค่าที่เป็น promise ส่วนที่เหลือของ chain จะรอจนกว่าจะได้ค่า เมื่อมันได้แล้ว ผลลัพธ์ของมันจะถูกส่งต่อไป

image.png


Error handling with promises

Promise chains ทำได้ดีมากในเรื่องของการรับมือกับ Error เมื่อ promise เกิดการ reject ขึ้น มันจะกระโดดไปที่ rejection handler ที่ใกล้ที่สุด ซึ่งเป็นเรื่องที่สะดวกสบายมาก

ตัวอย่างเช่น โค้ดบ้านล่าง fetch ไปที่ URL ที่ไม่มีอยู่ และ .catch ทำการรับมือกับ Error

fetch('https://no-such-server.blabla') // rejects
.then(response => response.json())
.catch(err => alert(err)) // TypeError: failed to fetch (the text may vary)

อย่างที่เห็น .catch ไม่จำเป็นต้องทำหน้าที่โดยทันที มันอาจจะเกิดขึ้นหลังจาก 1 หรือ หลาย ๆ .then. หรือบางที ทุกอย่างปกติดี แต่ response ของ JSON นั้น ไม่ถูกต้อง ทางที่ง่ายที่สุดในการจับ Error ก็สามารถใช้ catch ได้เหมือนกัน ณ ที่ล่างสุดของ chain

fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);

setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));

โดยปกติแล้ว .catch จะไม่ทำงานเลย แต่หาก Promise อันใดอันนึง rejecet (หรือ ปัญหา network, invalid JSON หรือ อื่น ๆ) ก็จะไปเข้า .catch เหมือนกัน


Implicit try…catch

โค้ดของ promise executor และ promise handlers มี “try..catch ล่องหน” รอบ ๆ มัน, ถ้ามีข้อผิดพลาดเกิดขึ้น มันจะโดนจับ และจะถูกปฏิบัติเป็นเหมือน rejection

ตัวอย่างของ code:

new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!

ซึ่งจะเหมือนกับ

new Promise((resolve, reject) => {
reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!

try..catch ล่องหน” รอบ ๆ executer จะจับ Error โดยอัตโนมัติ และเปลี่ยนมันเป็น rejection promise

นี่ไม่ได้เกิดแค่ใน executor function แต่ว่าภายใน handler ก็เช่นกัน ถ้าเรา throw ใน .then นั่นก็จะเป็น rejected promise เหมือนกัน ดังนั้น มันจะกระโดดไปที่ Error haandler ที่ใกล้ที่สุด

new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
throw new Error("Whoops!"); // rejects the promise
}).catch(alert); // Error: Whoops!

มันเกิดกับ Error ทุกรูปแบบไม่ใช่แค่กับ throw statement ตัวอย่างเช่น การเขียนโค้ดผิด

new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
blabla(); // no such function
}).catch(alert); // ReferenceError: blabla is not defined

Rethrowing

อย่างที่เรารู้ .catch  ที่จุดท้ายสุดของโค้ดจะเป็นเหมือนกับ try..catch. เราอาจมีหลาย .then เท่าที่เราต้องการและใช้แค่ .catch  ตัวเดียว ในการจัดการกับ Error ท้้งหมด

ใน try..catch ธรรมดา เราสามารถวิเคราะห์ error ได้ และสามารถ rethrow มันได้ ถ้ามันไม่สามารถ handled ได้ และเราก็สามารถทำได้เหมือนกันกับ Promise

ถ้าเรา throw ภายใน .catch ดังนั้น การควบคุม Error จะไปที่ Error handler ที่ใกล้ที่สุด และถ้าการ handle Error เป็นไปได้อย่างปกติ มันจะไปต่อกับ .then handler ที่ใกล้ที่สุด

ตัวอย่างข้างล่าง .catch  สามารถรับมือกับ Error ได้อย่างปกติ

// the execution: catch -> then
new Promise((resolve, reject) => {

throw new Error("Whoops!");

}).catch(function(error) {

alert("The error is handled, continue normally");

}).then(() => alert("Next successful handler runs"));

.catch ทำงานได้ปกติดี ดังนั้น .then ตัวต่อไป ถูกเรียกใช้งาน

ตัวอย่างด้านล่าง เราจะมาดูสถานการณ์ที่ .catch (*) ไม่สามารถจัดการกับ Error ได้ ดังนั้นมันจึงทำการ rethrow

// the execution: catch -> catch
new Promise((resolve, reject) => {

throw new Error("Whoops!");

}).catch(function(error) { // (*)

if (error instanceof URIError) {
// handle it
} else {
alert("Can't handle such error");

throw error; // throwing this or another error jumps to the next catch
}

}).then(function() {
/* doesn't run here */
}).catch(error => { // (**)

alert(`The unknown error has occurred: ${error}`);
// don't return anything => execution goes the normal way

});

Unhandled rejections

จะเกิดอะไรขึ้นหาก Error ไม่ถูกรับมือ ตัวอย่างเช่น เราลืมใส่ .catch  ไปที่สุดท้ายของ chain แบบนี้

new Promise(function() {
noSuchFunction(); // Error here (no such function)
})
.then(() => {
// successful promise handlers, one or more
}); // without .catch at the end!

ในกรณีนี้ promise จะเป็น reject และ execution จะกระโดดไปที่ rejection handler ที่ใกล้ที่สุด แต่มันไม่มี ดังนั้นมันจะเกิดการ “stuck”.

ในทางปฏิบัติ มันเหมือนกับ Error ที่ไม่สามารถรับมือได้ในการเขียนโค้ดแบบธรรมดา นั่นแปลว่าบางอย่างผิดพลาดอย่างร้ายแรง

จะเกิดอะไรขึ้นเมื่อ error ธรรมดาไม่ได้ถูกจับโดย try..catch?  script จะพังและมีข้อความขึ้นใน console ซึ่งจะเหมือนกับ promise rejections ที่ไม่สามารถรับมือได้

JavaScript ติดตาม rejections และสร้าง global error ขึ้นมา ในกรณีนี้ คุณสามารถเห็นมันได้ใน console ถ้าคุณรันโค้ดด้านบน

ใน Browser เราสามารถจับ Error ได้โดยใช้ event unhandledrejection:


window.addEventListener('unhandledrejection', function(event) {
// the event object has two special properties:
alert(event.promise); // [object Promise] - the promise that generated the error
alert(event.reason); // Error: Whoops! - the unhandled error object
});

new Promise(function() {
throw new Error("Whoops!");
}); // no catch to handle the error

ถ้าเกิด Error ขึ้นและไม่มี  .catch ตัว unhandledrejection  handler จะรับ event Object กับข้อมูลของ Error มา เผื่อเราสามารถทำอะไรกับมันได้

ส่วนใหญ่แล้วเมื่อเราเจอปัญหาที่เราไม่สามารถควบคุมได้ ทางที่ดีที่สุดของเราคือแจ้งไปที่ user เกี่ยวกับปัญหาและแจ้งกลับไปที่ server


Summary

  • .catch  ใช้จับ Error ใน promises ทุกรูปแบบ เหมือนกับการเรียกใช้ reject()  และโยน Error ไปที่ handler
  • .then ก็สามารถจับ Error ได้เหมือนกัน ถ้าเราให้ argument ตัวที่สองมา
  • เราควรวาง .catch  ตรงจุดที่เราต้องการจัดการ Error และรู้วิธีการจัดการ Error เหล่านั้น. Error hanlder ควรวิเคราะห์ Error และ rethrow เมื่อเจอ Error ที่เราไม่รู้จัก
  • เป็นสิ่งที่รับได้ถ้าเราไม่มี .catch เลย ถ้าเราไม่มีทางที่จะรับมือกับมันเลย
  • ในทุกกรณี เราควรมี unhandledrejection event handler ในการติดตาม errors และแจ้งไปที่ user เพื่อที่ app ของเราจะได้ไม่ตายไปเฉย ๆ

Promise API

มี 6 API method ของ promise ที่เราจะมาเรียนกันอย่างรวดเร็ว

Promise.all

สมมติว่า เราต้องการ execute promises หลาย ๆ ตัว พร้อม ๆ กัน และรอจนกว่าทุกตัวจะพร้อมใช้งาน

ตัวอย่างเช่น เราต้องการโหลด URL หลาย ๆ ตัวพร้อมกัน และ process content พร้อมกันเมื่อทุกตัวพร้อม

และนั่นคือจุดประสงค์ของ Promise.all

syntax:

let promise = Promise.all(iterable);

Promise.all  จะรับ iterable (โดยปกติจะเป็น array ของ Promise) และ return promise ใหม่ออกไป

promise ตัวใหม่จะเป็น resolve เมื่อ promise ทุกตัวเป็น resolve จะผลลัพธ์ของมันจะออกมาเป็น Array

ตัวอย่างเช่น Promise.all จะเสร็จทั้งหมดเมื่อครบ 3 วินาที และผลลัพธ์ของมันจะออกมาเป็น Array  [1, 2, 3]:

Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
]).then(alert); // 1,2,3 when promises are ready: each promise contributes an array member

โปรดจำไว้ว่า การเรียงลำดับของผลลัพธ์จะเป็นตามเหมือนกับที่เราเรียงใน array ถึงแม้ตัวแรกจะใช้เวลานานที่สุด แต่ผลลัพธ์ของมันจะออกมาเป็นตัวแรกของ Array

เคล็ดลับของการ map Array ของ Job data ไปเป็น Array ของ Promises หลังจากนั้น จับมันใส่เข้าไปใน Promise.all. ตัวอย่างเช่น เรามี Array ของ URL และเราสามารถ fetch พวกมันได้ทั้งหมด แบบนี้

let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://api.github.com/users/jeresig'
];

// map every url to the promise of the fetch
let requests = urls.map(url => fetch(url));

// Promise.all waits until all jobs are resolved
Promise.all(requests)
.then(responses => responses.forEach(
response => alert(`${response.url}: ${response.status}`)
));

ในตัวอย่างที่ใหญ่ขึ้น เราจะ fetch ข้อมูล user ของ Github user โดยใช้ชื่อของ user

**let names = ['iliakan', 'remy', 'jeresig'];

let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));

Promise.all(requests)
.then(responses => {
// all responses are resolved successfully
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // shows 200 for every url
}

return responses;
})
// map array of responses into an array of response.json() to read their content
.then(responses => Promise.all(responses.map(r => r.json())))
// all JSON answers are parsed: "users" is the array of them
.then(users => users.forEach(user => alert(user.name)));**

ถ้า promises บางตัว rejected. promise ที่ return โดย Promise.all จะ reject มันโดยทันที

Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!

promise ตัวที่สองเกิด reject ขึ้น นั่นทำให้เกิดการ rejected ของ Promise.all ดังนั้น .catch ที่จับ rejection จะมาเป็นผลลัพธ์ของทั้ง Promise.all.


Promise.allSettled

Promise.all จะเป็น reject เมื่อมีตัวใดตัวนึงเป็น reject นั่นเป็นสิ่งที่ดีสำหรับกรณีที่ว่า “all or nothing” เมื่อเราต้องการการประมวลผลที่สำเร็จทั้งหมด

Promise.all([
fetch('/template.html'),
fetch('/style.css'),
fetch('/data.json')
]).then(render); // render method needs results of all fetches

Promise.allSettled  จะรอสำหรับ promise ทุกตัวเสร็จสิ้น โดยไม่นึกถึงผลลัพธ์ และผลลัพธ์ของ array เป็น:

  • {status:"fulfilled", value:result} สำหรับ responses ที่สำเร็จ,
  • {status:"rejected", reason:error} สำหรับ Error.

ตัวอย่างเช่น เราต้องการข้อมูลของ user หลาย ๆ คน ถึงแม้จะมี request หนึ่งผิดพลาด เราก็ยังต้องการข้อมูลของคนอื่นอยู่

เรามาลองใช้งานมันกันเถอะ

let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
.then(results => { // (*)
results.forEach((result, num) => {
if (result.status == "fulfilled") {
alert(`${urls[num]}: ${result.value.status}`);
}
if (result.status == "rejected") {
alert(`${urls[num]}: ${result.reason}`);
}
});
});

results ในบรรทัด (*) จะเป็น

[
{status: 'fulfilled', value: ...response...},
{status: 'fulfilled', value: ...response...},
{status: 'rejected', reason: ...error object...}
]

status ที่เราได้ของ promise แต่ละตัวที่เราจะได้เป็น value/error.


Promise.race

เหมือนกับ Promise.all, แต่จะเอาแค่ตัวที่เร็วที่สุดและรับค่าผลลัพธ์หรือ Error มา

syntax:

let promise = Promise.race(iterable);

ตัวอย่างการใช้:

Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

Promise ตัวแรกเป็นตัวที่เร็วสุด ดังนั้นมันจะมาเป็นผลลัพธ์ของโค้ด


Promise.any

เหมือนกับ Promise.race แต่จะรอแค่ตัวแรกที่เป็น fulfilled promise และดึงค่าผลลัพธ์มา ถ้า promises ทั้งหมดเป็น reject ดังนั้นมันจะ return promise เป็น rejected กับ AggregateError — object พิเศษที่เก็บ errors ของ promise ใน errors property

 let promise = Promise.any(iterable);

ตัวอย่างการใช้งาน

Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

promise ตัวแรกเป็นตัวที่เร็วสุดแต่ว่า มันดันเป็น rejected ดังนั้น promise ตัวที่สองจึงมาเป็นผลลัพธ์

ตัวอย่างเมื่อ promise เป็น rejected ทั้งหมด

Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error!")), 2000))
]).catch(error => {
console.log(error.constructor.name); // AggregateError
console.log(error.errors[0]); // Error: Ouch!
console.log(error.errors[1]); // Error: Error!
});

Promise.resolve/reject

Methods Promise.resolve และ Promise.reject อาจไม่ค่อยได้ใช้งานในการเขียนโค้ดสมัยนี้ เพราะ async/await ทำให้มันล้าสมัยไปบ้าง

ซึ่งเราจะมาบอกวิธีใช้ เผื่อมีบางกรณีที่เราไม่สามารถใช้ async/await ได้

Promise.resolve

Promise.resolve(value)  สร้าง resolved promise กับ ผลลัพธ์ value. เหมือนกับ

let promise = new Promise(resolve => resolve(value));

method นี้ใช้เมื่อ function คาดว่าจะ return promise ออกมา

ตัวอย่างเช่น loadCached ข้างล่าง fetches URL และจำ content ของมัน(caches) สำหรับการเรียกใช้ซ้ำในอนาคต กับ URL เดิม โดยมันจะเรียก content ก่อนหน้าจาก cache แต่เราจะใช้ Promise.resolve เพื่อทำให้มันเป็น promise และจะ return ค่าของมันออกมาเป็น promise เสมอ

let cache = new Map();

function loadCached(url) {
if (cache.has(url)) {
return Promise.resolve(cache.get(url)); // (*)
}

return fetch(url)
.then(response => response.text())
.then(text => {
cache.set(url,text);
return text;
});
}

เราสามารถเขียน loadCached(url).then(…),  ได้เพราะว่า function จะรับประกันที่จะ return promise ออกมา

Promise.reject

Promise.reject(error) จะสร้าง rejected promise กับ Error มา

เหมือนกับ

let promise = new Promise((resolve, reject) => reject(error));

Summary

มี 6 static methods ของ promise

  1. Promise.all(promises) – รอจนกว่า promise ทุกตัวจะเป็น resolve. ถ้ามีตัวใดตัวนึงเป็น rejected, มันจะมาเป็น Error ของ Promise.all, และผลลัพธ์ของอันอื่น ๆ จะถูกละเว้น.
  2. Promise.allSettled(promises) (method นี้พึ่งถูกเพิ่มเข้ามา) – รอให้ promise ทุกตัวทำงานเสร็จและ return ค่าของ array ที่เป็น Object โดยจะเก็บค่าเป็น:
    • status"fulfilled" หรือ "rejected"
    • value (ถ้า fulfilled) หรือ reason (ถ้า rejected).
  3. Promise.race(promises) – รอจนกว่าตัวไหนจะเสร็จเป็นตัวแรก, โดยไม่สนว่าผลลัพธ์จะดีหรือเป็น Error
  4. Promise.any(promises) (method นี้พึ่งถูกเพิ่มเข้ามา) – รอจนกว่า promise ตัวไหนจะเป็น fulfill และเสร็จเป็นตัวแรก, และผลลัพธ์ของมันจะออกมาเป็น outcome. ถ้า promise ทุกตัวเป็น rejected, AggregateError จะมาเป็น Error ของ Promise.any เพื่อบอกเหตุผลของ Error
  5. Promise.resolve(value) – สร้าง resolved promise ด้วยค่าที่มอบให้.
  6. Promise.reject(error) – สร้าง rejected promise ด้วย Error ที่มอบให้.

Promisification

Promisification เป็นคำสำหรับการเปลี่ยนแปลง มันเปลี่ยนจาก function ที่รับ callBack ไปเป็น function ที่ return promise ออกไป

เพื่อให้เข้าใจมากขึ้น เรามาดูตัวอย่างกัน

ตัวอย่างเช่น เรามี  loadScript(src, callback) จากบทแรก

function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;

script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));

document.head.append(script);
}

// usage:
// loadScript('path/script.js', (err, script) => {...})

function จะโหลด script ตามที่เราให้ src ไป และเรียกใช้ callback(err) ในกรณีที่เกิด Error หรือใช้ callback(null, script) เมื่อโหลดสำเร็จ

เรามี promisify มันกัน

เราจะสร้าง function ใหม่  loadScriptPromise(src) ที่จะทำงานเหมือนกับ loadScript แต่จะ return เป็น promise ออกมา แทนการใช้ callBack

พูดง่าย ๆ เราจะส่ง src ไปให้มัน จะรับ promise กลับมา

let loadScriptPromise = function(src) {
return new Promise((resolve, reject) => {
loadScript(src, (err, script) => {
if (err) reject(err);
else resolve(script);
});
});
};

// usage:
// loadScriptPromise('path/script.js').then(...)

เห็นได้ว่า function ใหม่นี้คลุมตัว loadScript ไว้ข้างใน โดยมันเรียกใช้ callBack ของมัน และเปลี่ยนเป็น resolve/reject

ตอนนี้ loadScriptPromise สามารถใช้งานได้ตามปกติแล้ว ถ้าเราชอบใช้ promise มากกว่าวิธี callBack เราก็สามารถใช้มันได้

ในการใช้งานจริง เราอาจต้องการ function คล้าย ๆ นี้ หลาย ๆ ตัว ดังนั้นเราจะมาสร้างตัวที่ช่วยสร้าง function แบบนี้กัน

เราจะเรียกมันว่า promisify(f) มันจะรับ to-promisify function f และ return fucntion ที่ถูก wrap ออกมา

function promisify(f) {
return function (...args) { // return a wrapper-function (*)
return new Promise((resolve, reject) => {
function callback(err, result) { // our custom callback for f (**)
if (err) {
reject(err);
} else {
resolve(result);
}
}

args.push(callback); // append our custom callback to the end of f arguments

f.call(this, ...args); // call the original function
});
};
}

// usage:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);

โค้ดอาจมีความซับซ้อน แต่เอาจริง ๆ มันเหมือนกับโค้ดที่เราเขียนไปก่อนหน้าเลย

การเรียกใช้  promisify(f) จะ return wrapper รอบ ๆ  f (*).  โดย wrapper นั้น return promise และ เรียกใช้ function นั้น และติดตามผลลัพธ์ใน callback ที่เราสร้างขึ้น callback

สมมติว่า promisify สามารถใส่ได้แค่ 2 argument (err, result). ดังนั้น callBack ของเรา อยู่ใน format ที่ถูกต้องและ promisify สามารถทำงานได้อย่างปกติ ในกรณีนี้

แต่ถ้าเราต้องการมากกว่านั้นล่ะ callback(err, res1, res2, ...)? เรามาทำ promisify ที่เหนือขั้นไปมากกว่านี้กัน

  • promisify(f) ควรจะเป็นเหมือนโค้ดที่เราเขียนไปด้านบน
  • เมื่อเราใช้  promisify(f, true) มันควรจะ return promise resolves ที่ผลลัพธ์เป็น Array ของ callback ดังนั้นโค้ดเราจะสามารถรับ argument หลายตัวได้
// promisify(f, true) to get array of results
function promisify(f, manyArgs = false) {
return function (...args) {
return new Promise((resolve, reject) => {
function callback(err, ...results) { // our custom callback for f
if (err) {
reject(err);
} else {
// resolve with all callback results if manyArgs is specified
resolve(manyArgs ? results : results[0]);
}
}

args.push(callback);

f.call(this, ...args);
});
};
}

// usage:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...);

อย่าที่คุณเห็น โค้ดที่เราปรับปรุง เหมือนกับโค้ดเวอร์ชั่นเก่า แต่ resolve return ค่าขึ้นอยู่กับ manyArgs

JavaScript ยังมี modules ที่สามารถช่วยในเรื่องนี้ได้ ซึ่งเป็น buitl-in function ที่อยู่ใน Node.js util.promisify 


Microtasks

Promise handlers .then/.catch/.finally จะทำงานแบบ asynchronous เสมอ แม้ว่า promise จะได้ resolve โดยทันที แต่โค้ดที่อยู่ด้านล่าง .then, .catch, และ .finally จะทำงานก่อน handler เหล่านั้น


let promise = Promise.resolve();

promise.then(() => alert("promise done!"));

alert("code finished"); // this alert shows first

ถ้าคุณลองรันมันดู จะสังเกตได้ว่า  code finished จะขึ้นมาก่อน promise done!

นั่นดูแปลกมาก เพราะว่า promise เหมือนจะทำงานเสร็จตั้งนานแล้ว แต่ทำไม .then ถึงพึ่งมาทำงานล่ะ

Microtasks queue

งาน Asynchronous ต้องการการจัดการอย่างเหมาะสม ดังนั้นมาตรฐาน ECMA จึงกำหนด internal queue ที่เรียกว่า PromiseJobs หรือมักจะเรียกว่า "microtask queue"

ตามที่ระบุใน specification:

  • คิวเป็นแบบ first-in-first-out (FIFO): งานที่เข้าคิวก่อนจะถูกดำเนินการก่อน
  • การดำเนินงานจะเริ่มต้นเฉพาะเมื่อไม่มีโค้ดอื่นกำลังทำงานอยู่

หรือกล่าวง่าย ๆ เมื่อ Promise พร้อม, handler .then, .catch, หรือ .finally จะถูกใส่เข้าไปในคิว และยังไม่ถูก executed ทันที, และเมื่อ JavaScript ทำงานเสร็จจากโค้ดปัจจุบัน จะดึงงานจากคิวและดำเนินการ

นี่คือสาเหตุที่ทำให้ "code finished" ในตัวอย่างข้างต้นแสดงก่อน

image.png

ถ้ามี .then/catch/finally หลาย ๆ ตัว ดังนั้นทุกตัวจะ executed แบบ asynchronously มันจะไปเข้าคิวก่อน ดังนั้น มันจะ executed โค้ดปัจจุบันก่อนและหลังจากนั้น handlers ที่อยู่ในคิว ค่อยถูก executed

ถ้าเราต้องการทำให้  ****code finished โชว์หลัง promise done ****เราจะทำได้อย่างไร

ง่ายมาก เราแค่จับมันใส่ .then

Promise.resolve()
.then(() => alert("promise done!"))
.then(() => alert("code finished"));

Unhandled rejection

ยังจำ unhandledrejection event จาก Error handling with promises ได้หรือไม่

ตอนนี้เราจะมาดูว่า JavaScript รู้ได้อย่างไรว่ามี unhandled rejection

การเกิด "unhandled rejection" เกิดขึ้นเมื่อมีข้อผิดพลาดใน promise ที่ไม่ได้รับการจัดการเมื่อจบการประมวลผลของ microtask queue

ตามปกติ หากเราคาดว่าจะมี Error เกิดขึ้น เราจะเพิ่ม .catch ลงใน chain promise เพื่อจัดการกับ Error :

let promise = Promise.reject(new Error("Promise Failed!"));
promise.catch(err => alert('caught'));

// doesn't run: error handled
window.addEventListener('unhandledrejection', event => alert(event.reason));

แต่ถ้าเราลืมใส่ .catch ดังนั้นหลังจาก microtask queue ว่างเปล่าแล้ว JavvaScript engine จะ triggger event

let promise = Promise.reject(new Error("Promise Failed!"));

// Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));

จะเกิดอะไรขึ้นเมื่อ Error เกิดช้าล่ะ

let promise = Promise.reject(new Error("Promise Failed!"));
setTimeout(() => promise.catch(err => alert('caught')), 1000);

// Error: Promise Failed!
window.addEventListener('unhandledrejection', event => alert(event.reason));

เมื่อรันโค้ดนี้ จะเห็นข้อความ "Promise Failed!" แสดงขึ้นก่อน และตามด้วย "caught"

ถ้าเราไม่รู้เรื่องคิวไมโครทาสก์ เราอาจสงสัยว่า: “ทำไม unhandledrejection จึงทำงาน ทั้งๆ ที่เรา catch Error แล้ว?”

แต่ตอนนี้เราทราบแล้วว่า unhandledrejection จะถูกเรียกเมื่อ microtask queue ทำงานเสร็จสมบูรณ์: Engine จะตรวจสอบ promise และถ้าหากมี promise ใดอยู่ในสถานะ "rejected" event นี้จะถูกเรียก

ในตัวอย่างด้านบน .catch ที่ถูกเพิ่มด้วย setTimeout ก็ถูกเรียกเช่นกัน แต่มันทำงานภายหลังจากที่ unhandledrejection ถูกเรียกแล้ว ดังนั้นมันจึงไม่เปลี่ยนแปลงผลลัพธ์อะไร


Summary

promise handling จะทำงานแบบ asynchronous เสมอ เนื่องจากการทำงานทั้งหมดของ promise จะผ่านคิวภายในที่เรียกว่า “promise jobs” หรือ “microtask queue”

ดังนั้น .then, .catch, และ .finally จะถูกเรียกหลังจากโค้ดปัจจุบันทำงานเสร็จ

หากเราต้องการให้มีโค้ดบางส่วนทำงานหลังจาก .then, .catch, หรือ .finally เราสามารถเพิ่มโค้ดนั้นเข้าใน .then ที่ต่อกันเป็น chain

ใน JavaScript Engine ส่วนใหญ่ ทั้งใน Browser และ Node.js แนวคิดเรื่อง microtasks จะเกี่ยวข้องโดยตรงกับ event loop และ macrotasks (งานขนาดใหญ่) ซึ่งหัวข้อนี้จะไม่ได้เกี่ยวข้องโดยตรงกับ Promise ดังนั้นจะถูกอธิบายในบทความเกี่ยวกับ Event loop: microtasks and macrotasks.


Async/await

มี syntax พิเศษ ที่ใช้ทำงานร่วมกับ promise ในทางที่สะดวกมากกว่า เรียกว่า “async/await”. มันเป็นเรื่อง่ายมากในการเข้าใจและใช้งาน

Async functions

เรามาเริ่มกับ async ซึ่งมันจะวางไว้ที่ก่อนหน้า function แบบนี้:

async function f() {
return 1;
}

async ก่อน function หมายความว่า function นั้นจะ return เป็น promise ออกมาเสมอ และค่าอื่น ๆ จะถูก wrap อยู่ใน resolved promise

ตัวอย่างเช่น function นี้ return resolved promise กับค่า 1 ออกมา

async function f() {
return 1;
}

f().then(alert); // 1

ซึ่งจะเหมือนกับ

async function f() {
return Promise.resolve(1);
}

f().then(alert); // 1

ดังนั้น async จะรับประกันว่า มันจะ return promise ออกมา แต่ไม่ใช่แค่นั้น เรายังมีอีก keyword คือ await นั่นสามารถทำงานได้ แค่ตอนที่อยู่ใน async

Await

syntax:

// works only inside async functions
let value = await promise;

await จะรอจนกว่า promise จะทำงานเสร็จและ return ค่าออกมา

นี่คือตัวอย่างของ promise ที่จะ resolve ภายใน 1 วินาที

async function f() {

let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});

let result = await promise; // wait until the promise resolves (*)

alert(result); // "done!"
}

f();

function execution “หยุด” ที่บรรทัด (*) และทำงานต่อ หลังจากมันทำงานเสร็จสิ้นแล้ว และ result จะมาเป็น ผลลัพธ์ ดังนั้น โค้ดด้านบนจะโชว์ done! ภายใน 1 วินาที

ขอเน้นย้ำ: await หยุดการทำงานของ function จนกว่า promise จะทำงานเสร็จสิ้น และ ทำงานต่อเมื่อ promise ทำงานเสร็จแล้ว นั่นไม่ได้ทำให้ CPU ทำงานหนักขึ้น เพราะว่า JavaScript ยังสามารถทำงานอื่นต่อได้ในเวลานั้น

เราลองมาเขียน showAvatar()  ใหม่กัน

  1. เราต้องแทนที่ .then เป็น await แทน
  2. แล้วเราก็ควรทำให้ function async ทำงานได้กับมันด้วย
async function showAvatar() {

// read our JSON
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();

// read github user
let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);
let githubUser = await githubResponse.json();

// show the avatar
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);

// wait 3 seconds
await new Promise((resolve, reject) => setTimeout(resolve, 3000));

img.remove();

return githubUser;
}

showAvatar();

มันดูง่ายมากเลยใช่มั้ย ดีกว่าแต่ก่อนมาก

Error handling

ถ้า promise resolve ปกติ ดังนั้น  await promise  ก็ทำงานตามปกติ แต่ถ้าในกรณีที่เกิด rejection ล่ะ

มันจะ throw Error แค่มันมี throw ในบบรทัด

โค้ดนี้

async function f() {
await Promise.reject(new Error("Whoops!"));
}

เหมือนกับ

async function f() {
throw new Error("Whoops!");
}

ในสถานการณ์จริง promise อาจใช้เวลาก่อนจะ rejects ในกรณีนั้น await จะ delay ก่อนที่จะ throw Error

เราสามารถจับ Error ได้โดยใช้ try..catch

async function f() {

try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch
}
}

f();

ในกรณีของ Error นั้น มันจะกระโดดไปที่ catch  โดยเราอาจมี Error ได้ในหลายจุด

async function f() {

try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// catches errors both in fetch and response.json
alert(err);
}
}

f();

ถ้าเราไม่มี try..catch เราก็สามารถใช้ .catch สำหรับรับมือกับ Error ได้

async function f() {
let response = await fetch('http://no-such-url');
}

// f() becomes a rejected promise
f().catch(alert); // TypeError: failed to fetch // (*)

ถ้าเราลืมใส่  .catch เราสามารถใช้ event unhandledrejection ในการรับมือกับ Error ได้

Summary

การใช้ async วางไปที่หน้าของ function จะทำให้ function เกิด effects สองอย่าง

  1. จะทำให้ function return promise ออกมาตลอด
  2. อนุญาตให้ใช้ await ได้

การใช้ await ก่อน promise ทำให้ JavaScript รอจนกว่า promise จะทำงานเสร็จ หลังจากนั้น

  1. ถ้ามี Error, exception จะถูกสร้างขึ้น เหมือนกับการใช้  throw error 
  2. ถ้าไม่มี Error มันจะสร้างผลลัพธ์ให้เรา

ทั้งหมดนี้ทำให้เกิด Framework ดี ๆ ขึ้น ที่สามารถอ่านได้ง่ายและเขียนได้ง่าย

 async/await เราอาจได้ใช้  promise.then/catch น้อยลง แต่เราก็ไม่ควรลืมพื้นฐานของ promise เพราะบางครั้งเราก็ต้องใช้ method ของมันเช่น Promise.all เมื่อเราต้องการจะรอหลาย ๆ การทำงาน


END OF PROMISE