- Published on
JavaScript - Promise
- Authors
- Name
- Somprasong Damyos
- @somprasongd
Promise
Promise เป็นฟีเจอร์ใหม่ใน ES6 ช่วยให้การเขียนโปรแกรมแบบอะซิงโครนัส มีประสิทธิภาพมากขึ้น
Asyncohronous คือ การทำงานของโปรแกรมที่ไม่ต้องรอคอยให้ประโยคคำสั่งบรรทัดปัจจุบันทำงานให้เสร็จก่อน และค่อยไปทำงานประโยคคำสั่งบรรทัดถัดไป เช่น
- setTimeout() ที่ใช้ดีเลย์การทำงานของโปรแกรม
- setInterval() ที่ใช้วนซ้ำการทำงาน ตามระยะเวลาที่ระบุ
- การเขียนโปรแกรมแบบ AJAX เช่น การใช้ XMLHttpRequest
- การประมวลผลจาวาสคริปต์ใน Node.js
ตัวอย่าง
setTimeout(() => console.log('Timeout'), 10000)
console.log('Last Statement')
/* Result
"Last Statement"
"Timeout" */
// จะเห็นว่าบรรทัดแรก ต้องรอ 10 วินาที มันจึงข้ามมาทำงานที่บรรทัดสุดท้ายก่อน ไม่ต้องหยุดคอย
ปัญหา เนื่องจากการเขียนโปรแกรมแบบอะซิงโครนัส เราจะไม่ทราบลำดับว่าประโยคคำสั่งไหนจะทำงานเสร็จก่อนเสร็จทีหลัง จึงมักจะพบปัญหาเรื่องของการจัดการซอร์สโค้ด เราจึงใช้ Promise มาช่วยจัดการกับปัญหานี้
Promise จะแบ่งเป็น 3 ส่วนหลักๆ
1. การสร้าง Promise
สร้าง Promise จากคอนสตรัคเตอร์
let promise = new Promise(executor_function)
2. Executor Function
ซึ่งก็คือ Callback Function ธรรมดาตัวหนึ่ง ที่จะต้องมีพารามิเตอร์ 2 ตัว คือ resolve
กับ reject
(จะตั้งชื่ออื่นก็ได้) ซึ่งจะเกี่ยวข้องกับสถานะของ Promise เมื่อทำงานเสร็จแล้ว
สถานะของ Promise จะมี 3 สถานะ
- สถานะ Pending คือ promise ยังทำงานไม่เสร็จ
- เมือ Promise ทำงานเสร็จแล้วจะเข้าสู่สถานะตัดสินใจ (Settled) ตัวแรกคือ สถานะ Fulfilled คือ ทำงานสำเร็จ จะเรียกไปที่
resolve()
- สถานะ Rejected คือ ทำงานไม่สำเร็จ หรือเกิด error ขึ้นระหว่างการทำงาน จะเรีกยไปที่
reject()
รูปแบบการใช้งาน
function executorFunction(resolve, reject) {
// Asyncohronous Code
if (success) {
resolve(value)
}
if (failed) {
reject(error)
}
}
3. การมอนิเตอร์การทำงานของ Promise ว่าทำงานสำเร็จ หรือไม่สำเร็จ
เมื่อทำงานสำเร็จ
promise.then()
จะรับอาร์กิวเมนต์ 2 ตัว ที่เป็น callback function ทั้งคู่ โดยตัวแรกจะดักกับสถานะ fulfilled ตัวที่สองจะดักจับสถานะ rejected
let asynFunc = (resolve, reject) => {
console.log('Start a job')
// Asyncohronous Code ...
setTimeout(() => {
let min = 1,
max = 10
// Getting a random number between two values
let result = Math.floor(Math.random() * (max - min)) + min
if (result > 5) {
resolve('success')
} else {
reject(new Error('failed'))
// หรือ
// throw new Error('failed');
}
}, 5000)
}
let promise = new Promise(asynFunc)
// monitor
promise.then(
(value) => console.log(`Promise: ${value}`), // เมื่อทำงานสำเร็จ
(error) => console.log(`Promise: ${error.message}`) // เมื่อทำงานไม่สำเร็จ
)
console.log('Last Statement')
/*Result
"Start a job"
"Last Statement"
"Promise: success" หรือ "Promis: failed"
*/
[ Note 1 ] ถ้ากรณีที่ต้องการดักจับเฉพาะกรณีที่สำเร็จเพียงอย่างเดียว ก็ไม่ต้องใส่อาร์กิวเมนต์ที่สองลงไปก็ได้ promise.then((value) => console.log('Promise: ', value));
[ Note 2 ] ถ้ากรณีที่ต้องการดักจับเฉพาะกรณีที่ไม่สำเร็จเพียงอย่างเดียว จะต้องใส่อาร์กิวเมนต์ที่แรกเป็น null
ดังนี้ promise.then(null, (error) => console.log('Promise: ', error.message));
เมื่อไม่สำเร็จ
promise.catch()
จาก [Note 2] สามารถเขียนให้สั้นกระชับลงได้โดยการใช้ promise.catch((error) => {}))
แทน ตัวอย่าง
let promise = new Promise((resolve, reject) => {
console.log('Start a job')
setTimeout(() => reject('failed'), 5000)
})
promise.catch((reason) => console.log(`Promise: ${reason}`))
console.log('Last Statement')
/*Result
"Start a job"
"Last Statement"
"Promis: failed"
*/
การดักจับผลการทำงานแบบต่อเนื่อง
เนื่องจากเมื่อเรียกใช้งานทั้ง then() และ catch() มันจะถูกรีเทิร์นออกมาเป็น Promise ที่มีสถานะ fulfilled ให้อัตโนมัติโดยไม่ต้องเขียน return อะไรเลย ยกเว้นว่าจะเกิด error มันจะรีเทิร์น Promise ที่มีสถานะ rejected ออกมาแทน ทำให้สามารถเรียกใช้งาน .then() หรือ .catch() ต่อกันไปได้เรื่อยๆ
// then().then()
let p1 = new Promise((resolve, reject) => resolve('success'))
let p2 = p1.then((value) => console.log('Promise: ', value))
p2.then(() => console.log('Finish'))
/* Result
"Promise: success"
"Finish" */
// หรือจะเขียนเป็นลูกโซ่ต่อเนื่องไปเรื่อยๆ ก็ได้
let promise = new Promise((resolve, reject) => {
resolve('success')
})
promise
.then((value) => {
console.log('Promise: ', value)
})
.then(() => {
// callbackFunc ไม่มีพารามิเตอร์ ถ้ามีจะเป็น undefined
console.log('then1: finish')
})
.then(() => {
// callbackFunc ไม่มีพารามิเตอร์ ถ้ามีจะเป็น undefined
console.log('then2: finish')
})
/* Result
"Promise: success"
"then1: finish"
"then2: finish" */
// catch().catch()
let promise = new Promise((resolve, reject) => {
throw new Error('error1')
})
promise
.catch((error) => {
console.log('catch1: ', error.message)
throw new Error('error2')
})
.catch((error) => {
console.log('catch2: ', error.message)
throw new Error('error3')
})
.catch((error) => {
console.log('catch3: ', error.message) // ไม่มีการ throw แสดงว่าไม่เกิด error จะไม่เข้า .catch() ตัวถัดไป
})
.catch((error) => {
console.log('catch4: ', error.message)
})
/*Result
"catch1: error1"
"catch2: error2"
"catch3: error3"
*/
// การใช้ return ส่งค่าออกไปให้ .then() ตัวถัดไป
let promise = new Promise((resolve, reject) => {
resolve('success')
})
promise
.then((value) => {
console.log('then1: ', value)
return 2 // ส่ง 2 ไปให้ then() ตัวถัดไป
})
.then((value) => {
// callbackFunc มีพารามิเตอร์
console.log('then2: ', value)
return 3 // ส่ง 3 ไปให้ then() ตัวถัดไป
})
.then((value) => {
// callbackFunc มีพารามิเตอร์
console.log('then3: ', value)
})
/* Result
"then1: success"
"then2: 2"
"then3: 3" */
รีเทิร์น promise
return promise;
จากด้านบน then() กับ catch() จะรีเทิร์น promise ออกมาให้อัตโนมัติ แต่เราสามารถเขียนให้รีเทิร์น promise ตัว ที่เราต้องการออกมาเองได้
let p1 = new Promise((resolve, reject) => resolve('promise1'))
let p2 = new Promise((resolve, reject) => resolve('promise2'))
let p3 = p1.then((value) => {
console.log('First then:', value)
return p2
})
p3.then((value) => {
console.log('Second then:', value)
})
/*Result
"First then: promise1"
"Second then: promise2"*/
// หรือเขียนแบบนี้
p1.then((value) => {
console.log('First then:', value)
return p2
}).then((value) => {
console.log('Second then:', value)
})
- การใช้
then()
ร่วมกับcatch()
ถ้า Promise ที่ทำงานเสร็จแล้วมีสถานะเป็น rejected มันจะไม่เรียกใช้งาน promise.then() แต่ promise.then() จะรีเทิร์นพรอมิสตัวที่ทำงานไม่เสร็จตัวเดิมออกมา จึงทำให้ promise.catch() ตัวถัดมาทำงานได้ เสมือนว่ามันกระโดข้ามจาก then() ตัวแรกไปยัง catch() ตัวสุดท้าย
let promise = new Promise((resolve, reject) => resolve(1))
promise
.then((value) => {
console.log('then:', value)
return 2
})
.then((value) => {
console.log('then:', value)
throw new Error(3)
})
.then((value) => {
// ไม่ถูกเรียกให้ทำงาน
console.log('then:', value)
})
.catch((error) => {
console.log('catch:', error.message)
})
/*Result
"then: 1"
"then: 2"
"catch: 3"*/
การสร้างพรอมิสที่มีสถานะ settled ตั้งแต่เริ่มต้น
การสร้างพรอมิสที่มีสถานะ settled ตั้งแต่เริ่มต้นสร้างมันขึ้นมา ทำได้โดย
Promise.resolve();
สร้างพรอมิสที่มีสถานะเป็น fulfilled ตั้งแต่แรกPromise.reject();
สร้างพรอมิสที่มีสถานะเป็น rejected ตั้งแต่แรก
// fulfilled
let promise = Promise.resolve('Promise is fulfilled')
/* เหมือนกับ
let promise = new Promise((resolve, reject) => resolve('Promise is fulfilled'));
*/
promise.then((value) => console.log(value))
// rejected
let promise2 = Promise.reject('Promise is rejected')
/* เหมือนกับ
let promise = new Promise((resolve, reject) => reject('Promise is rejected'));
*/
promise2.catch((value) => console.log(value))
อื่นๆ
Promise.all()
จะใช้มอนิเตอร์พรอมิสหลายๆ ตัวพร้อมกัน (then()
หรือcatch()
จะมอนิเตอร์ได้ทีละตัว) โดยมันจะดักจับเหตุการณ์ที่พรอมิสทุกตัวต้องอยู่ในสถานะ fulfilled เท่านั้น แต่ถ้ามีเกิดมีพรอมิสที่มีสถานะเป็น rejected ขึ้นมา มันจะเลือกมอนิเตอร์แค่เฉพาะตัวแรกที่เป็น rejected เท่านั้นPromise.all()
จะรับค่าอาร์กิวเมนต์เป็นอาร์เรย์Promise.all([promise1, promise2, promise3])
Promise.all()
จะรีเทิร์นออกมาเป็นพรอมิสตัวใหม่Promise.all().then()
ถ้าเป็น fulfilled ค่าที่ส่งมาจะเป็นอาร์เรย์เรียงผลลัพธ์ของแต่ละพรอมิส ถ้าเป็น rejected จะได้ออกมาตัวเดียวคือของพรอมิสที่ rejected ตัวแรก
// fulfilled
let p1 = Promise.resolve('Promise1')
let p2 = Promise.resolve('Promise2')
let p3 = Promise.resolve('Promise3')
let p4 = Promise.all([p1, p2, p3])
p4.then((value) => console.log(value))
/*Result
["Promise1", "Promise2", "Promise3"]*/
// rejected
let p1 = Promise.resolve('Promise1')
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('Promise2')
}, 2000)
})
let p3 = Promise.reject('Promise3') // ตัวนี้จะเสร็จก่อน p2 ที่ดีเลย์ไว้ 2 วินาที
let p4 = Promise.all([p1, p2, p3])
p4.then(
(value) => console.log(value, 'success'),
(value) => console.log(value, 'failed')
)
/* Result
"Promise3 failed" */
Promise.race()
จะคล้ายๆ กับPromise.all()
ต่างกันที่Promise.race()
จะมอนิเตอร์พรอมิสแค่ตัวเดียว คือตัวที่ทำงานเสร็จก่อน (มีสถานะ settled ก่อน) อาจจะเป็น fulfilled หรือ rejected ก็ได้
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Promise21')
}, 5000) // delay 5 sec
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('Promise2')
}, 2000) // delay 5 sec
})
let p3 = Promise.reject('Promise3') // ตัวนี้จะเสร็จก่อน
let p4 = Promise.race([p1, p2, p3])
p4.then(
(value) => console.log(value, 'success'),
(value) => console.log(value, 'failed')
)
/* Result
"Promise3 failed" */
ตัวอย่างการใช้ Promise ใน Node.js
เขียนโปรแกรมอ่านไฟล์ json.txt
const fs = require('fs') // ใช้อ่านไฟล์
function readJSONFile(fileName) {
return new Promise((resolve, reject) => {
fs.readFile(fileName, (err, data) => {
if (err) {
reject(err)
} else {
let text = JSON.parse(data)
let json = JSON.stringify(text, null, 2)
resolve(json)
}
})
})
}
let readerPromise = readJSONFile('json.txt')
console.log('Read a file')
readerPromise
.then((value) => {
console.log(value)
})
.catch((err) => {
console.log('Error:', err.message)
})
/*Result กรณีไม่มีไฟล์ json.txt
"Read a file"
Error: ENOENT: no such file or directory, open 'C:\Users\sompr\Desktop\json.txt'
*/
/*Result อ่านสำเร็จ
"Read a file"
{
"name": "Somprasong Damyos",
"gender": "male",
"emails": [
{
"email": "a@mail.com"
},
{
"email": "b@mail.com"
},
{
"email": "c@mail.com"
}
]
}
*/
Fetch API
การใช้งาน มันจะทำงานแบบ Promise โดยมี default เป็น GET
fetch('http://localhost:8080/resources')
.then((res) => res.json())
.then((json) => console.log(json))
การเรียกใช้งาน HTTP Method อื่นๆ
fetch('http://localhost:8080/authentication', {
method: 'POST',
headers: {
'Accept': 'application/json', // รับข้อมูลกลับมาเป็น json
'Content-Type': 'application/json', // ส่งข้อมูลไปเป็น json
},
body: JSON.stringify({
username: 'ball',
password: '1234'
})
})
.then(res => res.json())
.then({user} => console.log(user));