φυβλαςのβλογ
phyblas的博客



javascript เบื้องต้น บทที่ ๔๓: คำมั่นสัญญา
เขียนเมื่อ 2020/05/05 07:07
แก้ไขล่าสุด 2021/09/28 16:42


ต่อจากบทที่แล้วซึ่งได้เขียนถึงโปรแกรมที่ทำงานแบบไม่ประสานเวลา (asynchronous) ไป

สำหรับในบทนี้จะมาพูด ออบเจ็กต์ Promise ซึ่งใช้จัดการกับโปรแกรมที่ทำงานแบบไม่ประสานเวลา




resolve และ then

Promise มีชื่อเล่นว่า ☆ゲッダン☆ เป็นออบเจ็กต์ชนิดใหม่ที่ถูกเพิ่มเข้ามาใน ES6 เพื่อใช้จัดการควบคุมลำดับการทำงานของพวกฟังก์ชันที่ทำงานแบบไม่ประสานเวลา



Promise เป็นคำภาษาอังกฤษมีความหมายว่า "คำมั่นสัญญา" ♩ ♬ ถึงม้วยดินสิ้นฟ้ามหาสมุทร ไม่สิ้นสุดความรักสมัครสมาน ♪ ♫

การใช้งาน Promise นั้นโดยพื้นฐานแล้วเริ่มจากการสร้างอินสแตนซ์ขึ้นโดยใช้ new เหมือนกับออบเจ็กต์ทั่วไป อาร์กิวเมนต์ที่ใส่คือฟังก์ชันที่เป็นตัวควบคุม
let p = new Promise(ฟังก์ชัน);

ฟังก์ชันที่ใส่ใน Promise นั้นเป็นฟังก์ชันที่มีพารามิเตอร์ ๒ ตัว โดยทั่วไปมักถูกตั้งชื่อว่า resolve กับ reject ตามลำดับ

(เพียงแต่ว่า ที่จริงแล้วจะใช้ชื่ออะไรก็ได้ ไม่จำเป็นต้องตามนี้ เพราะชื่อพารามิเตอร์กำหนดเองได้ แต่โดยธรรมเนียมปฏิบัติมักจะตั้งชื่อแบบนี้ ในที่นี้จึงใช้ตามนั้นด้วย)

ทั้ง ๒ ตัวนี้ต่างก็เป็นฟังก์ชัน ซึ่งจะถูกเรียกใช้ภายในฟังก์ชันนั้นอีกที ลักษณะการเขียนมักจะเป็นดังนี้
let p = new Promise((resolve, reject) => {
  // คำสั่งบางอย่างที่มีการเรียกใช้ฟังก์ชัน resolve();
  // คำสั่งบางอย่างที่มีการเรียกใช้ฟังก์ชัน reject();
});

พารามิเตอร์ตัวแรก resolve นั้นเป็นฟังก์ชันที่เมื่อเรียกใช้จะเป็นการประกาศว่าฟังก์ชันใน Promise นั้นทำจนเสร็จเรียบร้อยแล้ว ให้เริ่มดำเนินการขั้นต่อไปได้

ส่วน reject นั้นที่จริงแล้วก็คล้ายกัน คือเป็นฟังก์ชันที่เรียกใช้เพื่อแสดงการสิ้นสุดการทำงานใน Promise แต่ต่างกันตรงที่ว่า reject จะใช้เรียกใช้ในกรณีที่ล้มเหลว

หากไม่จำเป็นต้องคำนึงถึงกรณีที่ล้มเหลว อาจใส่แค่ resolve ตัวเดียวก็ได้
let p = new Promise(resolve => {
  // คำสั่งบางอย่างที่มีการเรียกใช้ฟังก์ชัน resolve();
});

เพื่อให้เข้าใจไปทีละขั้น ตัวอย่างในเบื้องต้นนี้จะละส่วน reject แล้วอธิบายแค่ resolve ก่อน จะไปกล่าวถึง reject อีกทีในตอนที่อธิบายการทำงานในกรณีที่ต้องมีการจัดการกับกรณีที่มีข้อผิดพลาด

ออบเจ็กต์ Promise นั้นมีเมธอดที่สำคัญ คือ .then ซึ่งใช้เรียกให้เริ่มทำงานฟังก์ชันที่จะให้ทำงานเมื่อมีการเรียกใช้ resolve

พูดอีกแบบก็คือ การเรียกใช้ฟังก์ชัน resolve จะเป็นการส่งสัญญาณบอกให้เริ่มทำฟังก์ชันใน .then ได้

ตัวอย่างการใช่้
let s = ""
let p = new Promise(resolve => {
  s += "มาทำสัญญากับผม\n";
  resolve();
});

p.then(() => {
  s += "แล้วเป็นสาวน้อยเวทมนตร์";
  alert(s);
});

ได้กรอบข้อความขึ้นมาว่า
มาทำสัญญากับผม
แล้วเป็นสาวน้อยเวทมนตร์
/人◕ ‿‿ ◕人\
ในที่นี้ออบเจ็กต์ Promise ถูกเก็บอยู่ในตัวแปร p จึงใช้เมธอด .then() ที่ตัวนี้เพื่อจะเรียกทำฟังก์ชันที่จะให้ทำหลังจากทำสิ่งที่อยู่ใน Promise เสร็จจนปิดท้ายด้วย resolve() ดังนั้นจึงเกิดการทำสิ่งที่อยู่ใน Promise เสร็จแล้วตามด้วยฟังก์ชันใน .then()

ปกติแล้วคำสั่งที่อยู่ภายในฟังก์ชันใน Promise นั้นหากไม่ใช่คำสั่งที่ตั้งให้ทำงานแบบประสานเวลาก็จะทำงานทันที ถูกเรียกใช้ในเธรดหลักเหมือนกับฟังก์ชันทั่วไป

ดังนั้นโดยทั่วไปเพื่อที่จะได้ใช้ประโยชน์จากการทำเป็น Promise จึงมักใช้กับฟังก์ชันที่ทำงานแบบไม่ประสานเวลา เช่นการโหลดข้อมูลทางอินเทอร์เน็ต แต่ในที่นี้จะยกตัวอย่างโดยใช้ setTimeout เป็นตัวแทน

ตัวอย่างการใช้
let s = "";
let p = new Promise(resolve => {
  setTimeout(() => {
    s += "จะรีบไปไหน ";
    resolve();
  }, 1000);
});

p.then(() => {
  s += "พักเดี๋ยวนึงสิครับ";
  alert(s);
});

หลังจากผ่านไป ๑ วินาทีก็จะได้กรอบข้อความขึ้นมาว่า
จะรีบไปไหน พักเดี๋ยวนึงสิครับ

จะเห็นว่าคำสั่งใน Promise เจอ setTimeout ทำให้ต้องรอ ๑ วินาทีจึงจะทำ และคำสั่งใน .then() ก็จะรอคำสั่งใน Promise ให้ทำเสร็จจนเรียกใช้ resolve() ก่อนจึงจะเริ่มทำ ไม่เช่นนั้นจะไม่มีการทำอะไร

ฟังก์ชัน resolve นั้นสามารถใส่อาร์กิวเมนต์ลงไปเพื่อที่จะส่งไปใช้ใน .then() ได้ เช่นในโปรแกรมที่โหลดข้อมูล เรามักจะต้องเอาข้อมูลที่ได้จากใน Promise ส่งไปใช้ใน .then()

ตัวอย่างเช่น
let p = new Promise((resolve) => {
  setTimeout(() => {
    let khomun = "++ข้อมูล++"; // สมมุติว่านี่คือการโหลดข้อมูลอะไรบางอย่าง
    resolve(khomun);
  }, 1000);
});

p.then(x => {
  alert(x); // แสดงค่าที่ถูกส่งมา
});

แบบนี้พารามิเตอร์ x ใน .then() ก็จะแทนข้อมูลที่ส่งมาจากใน Promise

ปกติแล้ว resolve มักจะถูกใช้เป็นตัวสิ้นสุดการทำงานภายใน Promise จึงมักจะใส่คำสั่งอะไรตามหลัง resolve อีก แต่ถ้าหากใส่ก็จะมีการทำงานทันที แถมทำทันทีโดยไม่รอฟังก์ชันใน .then() ด้วย

ตัวอย่างเช่น
let p = new Promise((resolve) => {
  setTimeout(() => {
    alert("ก. ปรากฏก่อน");
    resolve();
    alert("ข. ปรากฏทันทีถัดจาก ก");
  }, 1000);
});

p.then(() => {
  alert("ค. ปรากฏตอนท้ายสุด");
});

ผลก็ออกมาเป็น ก. ข. ค. ตามลำดับที่เห็นนี้ แสดงว่าต่อให้มีอะไรอยู่ถัดจาก resolve ก็ไม่ได้ต่างจากการวางไว้ก่อน เพื่อให้เข้าใจได้ไม่สับสนลำดับ ปกติจึงไม่วางคำสั่งอะไรต่อจาก resolve

ต่อไปขอยกตัวอย่างเพิ่มเติมเพื่อให้เข้าใจเรื่องจังหวะในการทำงานของโค้ดในส่วนต่างๆ
let t0 = new Date;
let tt = [];
// สร้าง Promise
let sanya = new Promise(resolve => {
  // ส่วนที่อยู่ใน setTimeout
  setTimeout(()=>{
    tt.push("ก. " + (new Date - t0));
    resolve();
  },500);
  // ส่วนที่อยู่ใน Promise แต่นอก setTimeout
  tt.push("ข. " + (new Date - t0));
});

tt.push("ค. " + (new Date - t0));

// วนถ่วงเวลาให้ผ่านไป ๑ วินาที
while (new Date - t0 < 1000) { }

// ส่วนใน then ตรงนี้ถูกทำตอนท้ายสุดเสมอ
sanya.then(() => {
  tt.push("ง. " + (new Date - t0));
  alert(tt.join("\n"));
});

tt.push("จ. " + (new Date - t0));

ผลที่ได้คือ
ข. 0
ค. 0
จ. 1000
ก. 1008
ง. 1008

ข. กับ ค. อยู่นอก Promise และนอก then จึงอยู่ในเธรดหลัก และทำงานก่อนตั้งแต่แรกอยู่แล้ว ตามลำดับ ส่วน จ. ก็เช่นกัน แต่อยู่หลังส่วนที่วนซ้ำใน while ไป ๑ วินาที จึงโผล่มา ๑ วินาทีหลังจากนั้น

เสร็จจากนั้นแล้วจึงไปเริ่มทำส่วนที่อยู่ใน setTimeout แม้ว่าในที่นี้จะตั้งเวลาให้รอแค่ 500 (ครึ่งวินาที) แต่เพราะไม่ได้อยู่ในเธรดหลักจึงต้องรอให้คำสั่งในเธรดหลักเสร็จก่อน แม้จะต้องรอนานกว่าเวลาที่กำหนดไว้

สุดท้ายจึงเรียกฟังก์ชัน resolve และเริ่มทำสิ่งที่อยู่ใน .then()

ในที่นี้วาง alert ไว้ใน .then() เพราะรู้ว่าส่วนนี้จะต้องมาท้ายสุด แต่ถ้าหากเผลอไปวางไว้หลังส่วน จ. แบบนี้ส่วน ก. กับ ง. ก็คงไม่ปรากฏให้เห็นเพราะเพิ่มเข้ามาหลัง alert




then ต่อจาก then

เมธอด .then() หลังจากที่ถูกใช้ไปเสร็จจะคืนออบเจ็กต์ Promise ตัวใหม่มาอีกที และเมื่อเป็น Promise นั่นหมายความว่ามันสามาถเรียกใช้เมธอด .then() ได้อีกเช่นกัน

ตัวอย่างเช่น
let p1 = new Promise(resolve => {
  setTimeout(()=>{
    alert("ก."); // ทำหลังผ่านไป ๑ วินาที
    resolve();
  },1000);
});

let p2 = p1.then(() => {
  alert("ข."); // ทำหลังเสร็จ ก.
})
alert(p2); // ทำงานทันที ได้ [object Promise]

p2.then(() => {
  alert("ค."); // ทำหลังเสร็จ ค.
});

ในที่นี้ p1 คือ Promise ที่ถูกสร้างขึ้นมาโดยตรงตอนแรก พอใช้ .then() เสร็จก็จะให้ Promise ตัวใหม่มา เก็บไว้ใน p2 จากนั้นก็เอามาเรียกใช้ .then() ได้อีก คำสั่งของ .then() ใน p2 จะทำต่อจาก .then() ใน p1

แต่หากใน .then() แรกมีการใช้คำสั่งที่ทำงานแบบไม่ประสานเวลาก็จะถูกสั่งไปทำในอีกเธรดต่อและจะเริ่มทำช้ากว่า
let p1 = new Promise(resolve => {
  setTimeout(()=>{
    alert("ก."); // ทำหลังผ่านไป ๑ วินาที
    resolve();
  },1000);
});

let p2 = p1.then(() => {
  setTimeout(()=>{
    alert("ข."); // ทำหลังเสร็จ ค.
  },0);  
})

p2.then(() => {
  alert("ค."); // ทำหลังเสร็จ ก.
});

หากต้องการส่งค่าที่ได้จาก .then() แรกเข้า .then() หลังก็สามารถทำได้

โดยเช่นเดียวกับที่ .then() ตัวแรกจะรับอาร์กิวเมนต์จาก resolve() มาเป็นพารามิเตอร์ .then() ตัวหลังนั้นจะรับค่าที่ใส่ใน return ใน .then() ตัวก่อนมาเป็นพารามิเตอร์ ดังนั้นจึงสามารถส่งต่อข้อมูลมาได้
let p1 = new Promise(resolve => {
  setTimeout(() => {
    s = "ก. ";
    alert(s); // ได้ ก.
    resolve(s);
  }, 1000);
});

let p2 = p1.then((x) => {
  x += "ข. ";
  alert(x); // ได้ ก. ข.
  return x;
})

p2.then((y) => {
  y += "ค.";
  alert(y); // ได้ ก. ข. ค.
});

เพียงแต่ว่าถ้า return ใน .then() ไปอยู่ในส่วนที่ทำงานแบบไม่ประสานเวลาด้วย ค่านั้นจะไม่อาจถูกส่งไปยัง .then() ถัดไป
let p1 = new Promise(resolve => {
  setTimeout(() => {
    resolve();
  }, 1000);
});

let p2 = p1.then(() => {
  setTimeout(() => {
    return "ฉันอยู่นี่แต่ไม่มีใครสนใจ";
  }, 0);
})

p2.then((x) => {
  setTimeout(() => {
    alert(x); // ได้ undefined
  }, 1000);
});

อนึ่ง ในที่นี้เขียนแยกเพื่อให้ดูง่ายๆ แต่ในความเป็นจริงคนมักจะเขียนต่อๆกันไปในลักษณะแบบนี้เลย
p.then(() => {
  // 555
}).then(() => {
  // xxx
}).then(() => {
  // yyy
}).then(() => {
  // zzz
});

เมธอดเขียนต่อๆกันไปแบบนี้ก็หมายความว่าเมื่อทำตัวนึงเสร็จก็จะคืนออบเจ็กต์ Promise ตัวใหม่มาแล้วตัวใหม่นั้นก็เรียกใช้เมธอด .then() ต่อไปในทันที แล้วเมธอด .then() ก็คืนออบเจ็กต์ Promise ก็จะเป็นอย่างนี้ต่อไปเรื่อยๆกี่รอบก็ได้




Promise.all

Promise นั้นนอกจากเป็นคลาสเอาไว้สร้างอินสแตนซ์แล้ว ก็ยังมีฟังก์ชันในตัว เช่น .all() .race() .allSettled() ซึ่งมีไว้ใช้ในการทำคำสั่งหลายๆตัวแบบคู่ขนานกันไปแบบไม่ประสานเวลา

Promise.all() ใช้รวม Promise หลายๆอันที่ต้องการให้ทำคู่ขนานกันไปเข้าเป็น Promise อันเดียวแล้วสุดท้ายใช้ .then() เรียกออกมาพร้อมกัน

โดยให้รวม Promise ทั้งหมดที่ต้องการทำใส่เอาไว้ในแถวลำดับใส่เข้าไปใน Promise.all() และเมื่อใช้ .then() พารามิเตอร์ในนั้นจะได้เป็นแถวลำดับของค่าอาร์กิวเมนต์ที่ใส่ใน .resolve() โดยจะออกมาตามลำดับ

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

ตัวอย่างเช่น สร้าง Promise ๓ อันให้ทำคำสั่งตามที่กำหนดไว้ในเวลาต่างกันตามที่กำหนดแบบนี้
let t0 = new Date;
let ttt = [];

// สร้าง Promise ทั้งหมด ๓ อัน ใส่ในแถวลำดับ
let ppp = [2, 0.5, 1].map((n) => {
  return new Promise(resolve => {
    setTimeout(() => {
      ttt.push(new Date - t0); // บันทึกเวลาหลังผ่านไปตามเวลาที่กำหนด
      resolve("รอ " + n + " วิ ");
    }, n * 1000);
  });
});

// เอา Promise ในแถวลำดับนั้นมารวมกันเป็น Promise เดียวด้วย Promise.all()
let pAll = Promise.all(ppp);
ttt.push(new Date - t0); // บันทึกเวลาหลังทันที

// ใช้ .then() กับ Promise ตัวที่สร้างขึ้นมาจาก Promise.all()
pAll.then((xxx) => {
  alert(xxx); // ได้ รอ 2 วิ ,รอ 0.5 วิ ,รอ 1 วิ 
  alert(ttt); // ได้ 0,501,1004,2000
})

ในที่นี้พารามิเตอร์ใน .then() คือ xxx จะเป็นแถวลำดับของค่าที่ใส่ใน .resolve() ใน Promise แต่ละอัน

ค่าที่จะได้มาเป็นพารามิเตอร์ใน .then() นั้นเป็นแถวลำดับของค่า ใน .resolve() แต่ละตัว โดยเรียงตามลำดับของ Promise ในแถวลำดับที่ใส่ไว้ใน Promise.all() ตอนแรก ไม่ได้ขึ้นกับลำดับเวลาว่าอันไหนเสร็จก่อนหลัง

คุณสมบัติตรงนี้มีความสำคัญ เพราะช่วยรับประกันลำดับของข้อมูลที่จะนำมาใช้ใน .then() ได้ ว่าจะต้องเรียงตามลำดับที่ต้องการโดยไม่สนว่าอันไหนเสร็จก่อน

ในขณะที่ ttt คือแถวลำดับที่เก็บเวลาในแต่ละขั้น จึงเรียงตามลำดับว่าอันไหนเสร็จก่อนหลัง จะเห็นว่าตัวเลขน้อยกว่าขึ้นก่อนเพราะใช้เวลาน้อยกว่า

ตัวอย่างที่อาจเห็นเช่นฟังก์ชันสำหรับโหลดข้อมูลบางอย่างอาจให้ promise มา ซึ่งเราสามารถให้โหลดไปพร้อมๆกันโดยรวมไว้ด้วย Promise.all() แล้วสุดท้ายเอาข้อมูลทั้งหมดมาใช้ใน .then() ทีเดียวได้

ตัวอย่างเช่น
function loadkhomun(khong, wela) {
  // khong คือของที่จะโหลด
  // wela คือเวลาที่ต้องใช้
  return new Promise(resolve => {
    setTimeout(() => {
      // บันทึกเวลาที่โหลดเสร็จ
      log.push("โหลด" + khong + "ใช้เวลา " + (new Date - t0)/1000 + " วินาที");
      // สมมุติว่าได้โหลดข้อมูลแล้วใส่ใน resolve เพื่อส่งไปใช้ใน then
      resolve(khong);
    }, wela * 1000);
  });
}

// ข้อมูลที่ต้องการโหลด และเวลาที่ต้องใช้
let ppp = [];
[
  ["หนัง", 3],
  ["เพลง", 1],
  ["เกม", 2.5],
  ["อนิเมะ", 1.5],
].forEach(([khong, wela]) => {
  ppp.push(loadkhomun(khong, wela));
});

// ดูข้อมูลที่ได้มา
Promise.all(ppp).then((khomun) => {
  alert("ข้อมูลที่ได้: **" + khomun + "**\n\n" + log.join("\n"));
});

let log = []; // แถวลำดับที่บันทึกเวลา
let t0 = new Date; // เริ่มนับเวลา

ได้
ข้อมูลที่ได้: **หนัง,เพลง,เกม,อนิเมะ**

โหลดเพลงใช้เวลา 1.003 วินาที
โหลดอนิเมะใช้เวลา 1.504 วินาที
โหลดเกมใช้เวลา 2.503 วินาที
โหลดหนังใช้เวลา 3 วินาที

ในที่นี้ loadkhomun() เป็นฟังก์ชันโหลดข้อมูลที่สมมุติขึ้นมา โดยเนื้อในเป็น setTimeout แต่สมมุติว่าเป็นการโหลดข้อมูล

โดยที่ฟังก์ชันนี้จะคืน Promise มา และเราเอา Promise ทั้งหมดมารวมกันใน Promise.all() ก็จะได้ข้อมูลที่โหลดมาได้มาใช้แสดงใน .then() ได้ตามลำดับ




Promise.race

Promise.race() จะคล้ายกับ Promise.all() คือใช้กับแถวลำดับของ Promise หลายตัวที่ต้องการให้ทำไปพร้อมๆกัน

แต่ข้อแตกต่างก็คือ Promise.race() นั้นคำสั่งใน .then() จะทำเมื่อมีตัวใดตัวหนึ่งเสร็จ ส่วนพารามิเตอร์ใน .then() จะได้เป็นค่าใน .resolve() จากตัวที่เสร็จก่อน

ตัวอย่างการใช้
// ฟังก์ชันโหลดข้อมูล
function loadkhomun(s) {
  return new Promise(resolve => {
    // ให้ใช้เวลาตามความยาวของสายอักขระ ดังนั้นยิ่งสั้นยิ่งเร็ว
    setTimeout(() => {
      resolve("~" + s + "~");
    }, s.length * 500);
  });
}

// สร้างแถวลำดับของ Promise
let arp = [
  loadkhomun("facebook"),
  loadkhomun("line"),
  loadkhomun("twitter")
]

// ใช้ Promise.race() และแสดงผลตัวที่เสร็จก่อน
Promise.race(arp).then((khomun) => {
  alert(khomun); // ได้ ~line~
});

ในที่นี้ line สั้นสุดเลยเสร็จก่อน จึงแสดงผลออกมาใน .then() ส่วนตัวที่เหลือจะไม่ได้มายุ่งเกี่ยวอะไรในนี้

อย่างไรก็ตาม แม้ว่า .then() จะถูกทำทันทีหลังจากที่คำสั่งใน Promise ตัวแรกถูกทำจนเสร็จ แต่ก็ไม่ได้เป็นการหยุดการทำงานของ Promise ตัวอื่นที่เสร็จหลังจากนั้น มันจะยังคงทำงานต่อไปและเสร็จตามเวลา แต่จะไม่ส่งผลอะไรต่อใน .then()




reject และ catch

ดังที่ได้กล่าวถึงในตอนต้น พารามิเตอร์เวลาสร้าง Promise มีอยู่ ๒ ตัว คือ resolve และ reject ที่ผ่านมาใช้แค่ตัวแรกคือ resolve คือพิจารณาแต่กรณีที่รันสำเร็จ

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

reject เป็นฟังก์ชันที่เรียกใช้ในกรณีที่เกิดเหตุการณ์ไม่พึงประสงค์ขึ้นระหว่างทำคำสั่งใน Promise

การใช้จะคล้ายกับ resolve แต่ต่างกันตรงที่ว่าฟังก์ชันที่ทำงานหลังเรียก resolve จะใส่ในเมธอด .then() ส่วนฟังก์ช้นที่ทำงานหลังเรียก reject จะเรียกใช้ในเมธอด .catch()

ตัวอย่างการใช้
let p = new Promise((resolve,reject) => {
  setTimeout(() => {
    reject("มีข้อผิดพลาดเกิดขึ้น");
  }, 500);
})

p.catch((e) => {
  alert(e); // ได้ มีข้อผิดพลาดเกิดขึ้น
});

โดยทั่วไปมักจะเขียน .catch() ควบคู่ไปกับ .then() เพื่อรองรับสำหรับกรณีที่สำเร็จหรือผิดพลาดไปพร้อมๆกัน โดยเขียน .catch() ต่อจาก .then() ได้เลย เช่น
let p = new Promise((resolve,reject) => {
  setTimeout(() => {
    resolve("สำเร็จแล้ว");
  }, 500);
  setTimeout(() => {
    reject("มีข้อผิดพลาดเกิดขึ้น");
  }, 1000);
})

p.then((r) => {
  alert(r);
}).catch((e) => {
  alert(e);
});

ในกรณีนี้มีการเรียกใช้ resolve ขึ้นก่อน เพราะเวลาใน setTimeout สั้นกว่า จึงทำฟังก์ชันใน .then() แต่ไม่ทำใน .catch() จึงขึ้นว่า สำเร็จแล้ว

ในทางกลับกัน ถ้าเกิดเรียก reject ก่อนก็จะไม่มีการทำใน .then() แต่ข้ามไปทำใน .catch() เช่น
new Promise((resolve,reject) => {
  setTimeout(() => {
    resolve("สำเร็จแล้ว");
  }, 1200);
  setTimeout(() => {
    reject("มีข้อผิดพลาดเกิดขึ้น");
  }, 600);
}).then((r) => {
  alert(r);
}).catch((e) => {
  alert(e);
});

คราวนี้เวลาใน setTimeout ที่ทำ reject ขึ้นก่อน จึงเกิดการเรียก reject แล้วทำให้ฟังก์ชันใน .catch() ทำงาน แบบนี้จะขึ้นว่า มีข้อผิดพลาดเกิดขึ้น

นอกจากนี้ยังมีวิธีการเขียนอีกแบบ คือใส่ทั้งกรณี resolve และ reject ไว้ใน .then() ทั้งคู่ โดยใส่ต่อกันไปเลย เช่น
p.then((r) => {
  alert(r);
}, (e) => {
  alert(e);
});

แบบนี้ไม่ต่างจากเขียน .then() กับ .catch() แต่ว่าทำให้ดูแล้วเข้าใจยากกว่า โดยทั่วไปจึงไม่นิยม แม้จะเขียนแบบนี้ได้ก็ตาม

ตัวอย่างที่จะกล่าวถึงต่อจากนี้ไปก็จะเขียนแยกเป็น .then() .catch() ตลอด ไม่ใช้วิธีการเขียนแบบนี้

และกรณีที่มี .then() หลายตัวก็อาจเขียนต่อๆกันไปแล้วปิดท้ายด้วย .catch()
p.then(() => {
  // aaa
}).then(() => {
  // bbb
}).then(() => {
  // ccc
}).catch(() => {
  // 555
});




reject ใช้ร่วมกับ try catch ในการรับมือข้อผิดพลาดทั่วไป

เมธอด .catch() ของ Promise นั้นปกติจะทำงานเมื่อมีการเรียกใช้ฟังก์ชัน reject ใน Promise เพื่อเป็นการบอกว่ามีข้อผิดพลาดเท่านั้น

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

เช่น
let p = new Promise((resolve,reject) => {
  setTimeout(() => {
    a += 1; // ขึ้นข้อผิดพลาด ReferenceError: a is not defined
    resolve("สำเร็จ");
  }, 1000);
})

p.then((r) => { // ส่วนนี้ไม่ทำงานเพราะ error ก่อน resolve
  alert(r);
}).catch((e) => {
  alert(e);
});

ในตัวอย่างนี้ a += 1 เกิดข้อผิดพลาดขึ้นเพราะไม่ได้ประกาศตัวแปรมาก่อน โปรแกรมจึงหยุดทำงานก่อนที่จะเรียกใช้ resolve จึงไม่มีการทำงานใน .then() ส่วน .catch() เองก็จะทำงานเมื่อเรียก reject เท่านั้น ในกรณีนี้ซึ่งเกิดข้อผิดพลาดทั่วไปจึงไม่ได้ทำงานไปด้วย

เพื่อให้ความผิดพลาดทั่วไปที่เกิดในโปรแกรมมาทำงานใน .catch() อาจใช้คู่กับโครงสร้าง try catch ซึ่งใช้รับมือกับข้อผิดพลาดทั่วไป (ดังที่เขียนถึงในบทที่ ๑๘)

โดยที่ใส่ resolve ลงในส่วนของ catch ของ try catch เช่นแบบนี้
let p = new Promise((resolve,reject) => {
  setTimeout(() => {
    try {
      a += 1;
      resolve("สำเร็จ");
    }
    catch (e) {
      reject(e);
    }
  }, 1000);
})

p.then((r) => {
  alert(r);
}).catch((e) => {
  alert(e); // ขึ้นกรอบข้อความ ReferenceError: a is not defined
});

อนึ่ง catch ในโครงสร้าง try catch นั้นก็ใช้คำว่า catch เหมือนกัน แต่ไม่ได้เกี่ยวข้องกับเมธอด .catch() ของ​ Promise แม้จะมีความคล้ายกันตรงที่ใช้รับมือข้อผิดพลาดเหมือนกันก็ตาม แต่เป็นคนละตัวกัน ดังนั้นระวังสับสน

catch ใน try catch เป็นรูปแบบไวยากรณ์การเขียน ในขณะที่ .catch() ใน Promise นั้นเป็นเมธอด

ข้อควรระวังอีกอย่างคือ ปกติ try catch จะจัดการกับข้อผิดพลาดที่เกิดในเธรดนั้นเท่านั้น ดังนั้นกรณีที่ใช้ setTimeout แบบนี้ต้องใส่ try catch ข้างในโดยตรง ถ้าใส่ข้างนอกแบบนี้ try catch จะไม่ทำงาน
let p = new Promise((resolve, reject) => {
  try {
    setTimeout(() => {
      a += 1;
      resolve("สำเร็จ");
    }, 1000);
  }
  catch (e) {
    reject(e);
  }
});




finally [ES2018]

ฟังก์ชันใน .then() จะทำงานเมื่อมีการเรียก resolve ส่วน .catch() จะทำงานเมื่อเรียก reject แต่ถ้าหากมีฟังก์ชันบางอย่างที่ต้องการให้ถูกเรียกตอนท้ายสุดเสมอ ไม่ว่าจะกรณีไหน แบบนี้อาจใช้ .finally()

เมธอด .finally() ถูกเพิ่มเข้ามาใน ES2018 วิธีการใช้ให้เขียนต่อจาก .then() และ .catch()

ตัวอย่างเช่น กรณีที่เรียก resolve จะมีการทำงานใน .then() แล้วตามด้วย .finally()
new Promise((resolve, reject) => {
  resolve();
}).then(() => { // ทำงาน
  alert("ถูกต้องนะคร้าบ");
}).catch(() => { // ไม่ทำงาน
  alert("ผิดนะครับ ไม่ใช่ครับ");
}).finally(() => { // ทำงาน
  alert("สุดท้ายนี้ขอลาไปก่อน");
});

ในทางกลับกัน ถ้าเรียก reject จะทำใน .catch() แล้วทำใน .finally()
new Promise((resolve, reject) => {
  reject();
}).then(() => { // ไม่ทำงาน
  alert("ถูกต้องนะคร้าบ");
}).catch(() => { // ทำงาน
  alert("ผิดนะครับ ไม่ใช่ครับ");
}).finally(() => { // ทำงาน
  alert("สุดท้ายนี้ขอลาไปก่อน");
});

หากไม่ได้ใช้ finally คำสั่งส่วนนี้อาจจำเป็นต้องเขียนไว้ทั้งใน .then() และ .catch() ซ้ำกัน เพื่อที่จะให้ทำหมดทั้งใน ๒ กรณี ดังนั้นในสถานการณ์แบบนี้ใช้ finally จึงทำให้เขียนสะดวกขึ้น




reject ใน Promise.all

กรณีที่ใช้ Promise.all() เพื่อมัดรวม Promise หลายตัวเข้าด้วยกัน หากมีตัวใดตัวหนึ่งเรียก reject ขึ้นมาก็จะทำให้ .catch() ถูกเรียกทันที และจะไม่มีการเรียกใช้ .then() เพียงแต่ว่าคำสั่งในแต่ละ Promise ตัวที่เหลือถ้ายังทำไม่เสร็จก็จะยังคงทำต่อไปอยู่ แค่จะไม่มีผลอะไรกับใน .then() หรือ .catch()

ตัวอย่าง
function loadkhomun(s) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (s != "หนัง") {
        log.push("โหลด" + s + "เสร็จ"); // แสดงสถานะบอกว่าโหลดเสร็จ
        resolve("$" + s + "$");
      }
      else {
        reject("ห้ามโหลดหนัง");
      }
    }, s.length * 500);
  });
}

let arp = [
  loadkhomun("เกม"),
  loadkhomun("อนิเมะ"),
  loadkhomun("หนัง"),
  loadkhomun("งาน")
];

Promise.all(arp).then((khomun) => {
  alert(khomun); // ไม่ทำงาน
}).catch((e) => {
  alert(e); // ขึ้นว่า ห้ามโหลดหนัง
}).finally(() => {
  alert(log); // ขึ้นว่า โหลดเกมเสร็จ,โหลดงานเสร็จ
});

let log = [];

หากต้องการให้สิ่งที่อยู่ใน .then() เสมอ ต่อให้จะมีบางอันเกิดข้อผิดพลาดขึ้นก็ตาม กรณีแบบนี้อาจใช้ Promise.allSettled() แทน




Promise.allSettled [ES2020]

Promise.allSettled() เป็นฟังก์ชันที่เพิ่มเข้ามาใน ES2020 มีลักษณะการทำงานในลักษณะเดียวกันกับ Promise.all() คือใช้มัดรวม Promise หลายๅตัวให้ทำงานไปพร้อมๆกัน

แต่ข้อแตกต่างก็คือ Promise.allSettled() จะรอจนทุก Promise ในนั้นทำงานเสร็จ ไม่ว่าจะเรียก resolve หรือ reject ก็ได้ แล้วก็จะเรียก .then() เสมอไม่มีการเรียกใช้ .catch()

ในขณะที่ถ้าเป็น Promise.all() จะเข้า .catch() ทันทีเมื่อตัวใดตัวหนึ่ง reject

และค่าพารามิเตอร์ที่จะได้ใน .then() ของ Promise.allSettled() นั้นจะไม่ใช่แค่ค่าที่ส่งจาก resolve หรือ reject แต่จะเป็นออบเจ็กต์ที่มีพรอเพอร์ตี ๓ อย่าง คือ
  • status: สถานะ ถ้าเรียก resolve จะได้ fulfilled ถ้าเรียก reject จะได้ rejected
  • value: ค่าที่ได้จากการเรียก resolve
  • reason: ค่าที่ได้จากการเรียก reject มักจะบอกสาเหตุว่าทำไมถึงผิดพลาด

ตัวอย่าง
function loadkhomun(s) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (s != "หนัง") {
        log.push("โหลด" + s + "เสร็จ"); // แสดงสถานะบอกว่าโหลดเสร็จ
        resolve("$" + s + "$");
      }
      else {
        reject("ห้ามโหลดหนัง");
      }
    }, s.length * 500);
  });
}

let arp = [
  loadkhomun("เกม"),
  loadkhomun("อนิเมะ"),
  loadkhomun("หนัง"),
  loadkhomun("งาน")
];

Promise.allSettled(arp).then((khomun) => { // ทำงาน
  khomun.forEach((x) => { // เอาค่าที่ได้จาก Promise แต่ละอันมาไล่เรียงว่าได้อะไรบ้าง
    log.push(
      "~ สถานะ: " + x.status + 
      ", ค่า: " + x.value + 
      ", เหตุผล: " + x.reason + " ~"
    );
  });
}).catch((e) => { // catch จะไม่ทำงานในกรณีนี้ จะไม่ใส่ก็ได้
  alert(e);
}).finally(() => { // ทำงาน
  alert(log.join("\n")); // ให้แสดงข้อความที่เก็บไว้ทั้งหมดออกมา
});

let log = [];

ผลลัพธ์จะได้กรอบข้อความขึ้นมาว่า
โหลดเกมเสร็จ
โหลดงานเสร็จ
โหลดอนิเมะเสร็จ
~ สถานะ: fulfilled, ค่า: $เกม$, เหตุผล: undefined ~
~ สถานะ: fulfilled, ค่า: $อนิเมะ$, เหตุผล: undefined ~
~ สถานะ: rejected, ค่า: undefined, เหตุผล: ห้ามโหลดหนัง ~
~ สถานะ: fulfilled, ค่า: $งาน$, เหตุผล: undefined ~

ในที่นี้ครึ่งบนเหมือนตัวอย่างที่แล้วที่ใช้ Promise.all() เลย แต่ต่างกันที่ด้านล่างใช้ Promise.allSettled() และวิธีการใช้ก็ต่างกันไปพอสมควรดังที่เขียนในตัวอย่าง

ในตัวอย่างนี้เนื่องจากตั้งไว้ให้ reject เฉพาะเมื่อโหลดหนัง สถานะของ Promise ตัวที่โหลดหนังจึงเป็น rejected ส่วนตัวอื่นเป็น fulfilled เพราะเรียก resolve

และเมื่อเป็น fulfilled ก็จะมีค่า value ซึ่งได้จาก resolve ส่วนที่เป็น rejected จะไม่มี value แต่จะมี reason ซึ่งบอกเหตุผลข้อผิดพลาดที่ได้จากการ reject




Promise.resolve และ Promise.reject

ออบเจ็กต์ Promise ที่ถูกเรียกใช้ resolve ในฟังก์ชันด้านในไปแล้วจะอยู่ในสถานะเสร็จสิ้น (fulfilled) ซึ่งพร้อมที่จะทำฟังก์ชันใน .then() ทันที

ในทางกลับกัน หากถูกเรียกใช้ reject ก็จะอยู่ในสถานะถูกตีกลับ (rejected) ทำให้ฟังก์ชันใน .catch() พร้อมทำงานขึ้นมา

หากต้องการสร้าง Promise ขึ้นพร้อมกับให้อยู่ในสถานะเสร็จสิ้นทันทีอาจใช้ฟังก์ชัน Promise.resolve()

let p = Promise.resolve(x); จะเท่ากับการเขียนว่า
let p = new Promise(resolve => {
  resolve(x);
});

Promise ที่สร้างมาในลักษณะอย่างนี้เมื่อใช้ .then() ก็จะพร้อมทำงานฟังก์ชันในนั้นได้ทันที
Promise.resolve("สวัสดี").then((x) => {
  alert(x); // ได้ สวัสดี
})

เช่นเดียวกัน หากต้องการสร้าง Promise ขึ้นพร้อมกับให้อยู่ในสถานะถูกปฏิเสธ อาจใช้ฟังก์ชัน Promise.reject()

let p = Promise.reject(e); จะเท่ากับการเขียนว่า
let p = new Promise((_, reject) => {
  reject(e);
});

Promise ที่สร้างมาแบบนี้ถ้าใช้ .catch() ก็จะทำงานได้ทันที
Promise.reject("ลาก่อน").catch((x) => {
  alert(x); // ได้ ลาก่อน
})

ลองดูตัวอย่างการใช้คู่กับ Promise.allSettled
Promise.allSettled([
  new Promise(resolve => { resolve("ทำได้") }),
  new Promise((_, reject) => { reject("ล้มเหลว") }),
  Promise.resolve("ทำได้แล้วแฮะ"),
  Promise.reject("ล้มเหลวซะแล้ว")
]).then((x) => {
  alert(x.map((y) => {
    return "# สถานะ: " + y.status + ", ค่า: " + y.value + ", เหตุผล: " + y.reason + " #";
  }).join("\n"));
});

ได้
# สถานะ: fulfilled, ค่า: ทำได้, เหตุผล: undefined #
# สถานะ: rejected, ค่า: undefined, เหตุผล: ล้มเหลว #
# สถานะ: fulfilled, ค่า: ทำได้แล้วแฮะ, เหตุผล: undefined #
# สถานะ: rejected, ค่า: undefined, เหตุผล: ล้มเหลวซะแล้ว #

Promise.resolve() และ Promise.reject() นั้นมีประโยชน์เมื่อต้องการจะได้ Promise ที่ถูก resolve หรือ reject ทันที

เช่น ปกติแล้วถ้าใน .then() มีการ return Promise ที่ reject ก็จะทำ .catch() ต่อได้ เช่น
let p = new Promise((resolve) => {
  resolve("เสร็จเลย");
});

p.then((x) => {
  alert(x); // ได้ เสร็จเลย
  return new Promise((_, reject) => { reject("จบเห่"); });
}).catch((e) => {
  alert(e); // ได้ จบแห่
});

แบบนี้หลังจากทำคำสั่งใน .then() เสร็จแล้ว เนื่องจากใน .then() มีการ return reject ออกมาจึงทำให้คำสั่งใน catch ถูกทำไปด้วย

หากใช้ Promise.reject แล้วภายในส่วน .then() ก็จะเขียนแทนได้แบบนี้ ดูสั้นลง
p.then((x) => {
  alert(x);
  return Promise.reject("จบเห่");
}).catch((e) => {
  alert(e);
});

ดังนั้น Promise.reject() จึงใช้ประโยชน์เช่นเมื่อต้องการให้มีการทำ .catch() ต่อจาก .then() ได้

และเช่นเดียวกัน Promise.resolve() ก็ใช้เพื่อให้เกิดการทำ .then() ต่อจาก .catch() ได้
new Promise((resolve, reject) => {
  reject("ยังจะต่ออีกนะ");
}).catch((e) => {
  return Promise.resolve(e)
}).then((x) => {
  alert(x); // ได้ ยังจะต่ออีกนะ
});



ทั้งหมดนี้คือวิธีการใช้ Promise และในบทต่อไปจะเขียนถึงการใช้ async await ซึ่งเป็นส่วนที่จะมาช่วยเสริมการทำงานของ Promise






-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> เขียนโปรแกรม >> javascript

ไม่อนุญาตให้นำเนื้อหาของบทความไปลงที่อื่นโดยไม่ได้ขออนุญาตโดยเด็ดขาด หากต้องการนำบางส่วนไปลงสามารถทำได้โดยต้องไม่ใช่การก๊อปแปะแต่ให้เปลี่ยนคำพูดเป็นของตัวเอง หรือไม่ก็เขียนในลักษณะการยกข้อความอ้างอิง และไม่ว่ากรณีไหนก็ตาม ต้องให้เครดิตพร้อมใส่ลิงก์ของทุกบทความที่มีการใช้เนื้อหาเสมอ

目录

从日本来的名言
模块
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
机器学习
-- 神经网络
javascript
蒙古语
语言学
maya
概率论
与日本相关的日记
与中国相关的日记
-- 与北京相关的日记
-- 与香港相关的日记
-- 与澳门相关的日记
与台湾相关的日记
与北欧相关的日记
与其他国家相关的日记
qiita
其他日志

按类别分日志



ติดตามอัปเดตของบล็อกได้ที่แฟนเพจ

  查看日志

  推荐日志

ตัวอักษรกรีกและเปรียบเทียบการใช้งานในภาษากรีกโบราณและกรีกสมัยใหม่
ที่มาของอักษรไทยและความเกี่ยวพันกับอักษรอื่นๆในตระกูลอักษรพราหมี
การสร้างแบบจำลองสามมิติเป็นไฟล์ .obj วิธีการอย่างง่ายที่ไม่ว่าใครก็ลองทำได้ทันที
รวมรายชื่อนักร้องเพลงกวางตุ้ง
ภาษาจีนแบ่งเป็นสำเนียงอะไรบ้าง มีความแตกต่างกันมากแค่ไหน
ทำความเข้าใจระบอบประชาธิปไตยจากประวัติศาสตร์ความเป็นมา
เรียนรู้วิธีการใช้ regular expression (regex)
การใช้ unix shell เบื้องต้น ใน linux และ mac
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ทำความรู้จักกับปัญญาประดิษฐ์และการเรียนรู้ของเครื่อง
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ