4 วิธีเล่นกับ Asynchronous Function ใน JavaScript
TL;DR
- Callback
- Promise
- async/await
- Rx (Observable)
ในทางคณิตศาสตร์ ฟังก์ชัน (function) ถูกนิยามไว้ว่าคือความสัมพันธ์จากเซตหนึ่งที่เรียกว่าโดเมน (เซตของสิ่งนำเข้า) ไปยังอีกเซตหนึ่งที่เรียกว่าโคโดเมน (เซตของผลลัพธ์ที่เป็นไปได้) โดยที่สมาชิกตัวหน้าไม่ซ้ำกัน (แปลว่าไรแวะ !!)
In mathematics, a function is a relation between sets that associates to every element of a first set exactly one element of the second set. Typical examples are functions from integers to integers or from the real numbers to real numbers.
แปลเป็นภาษาบ้านๆ ตามความเข้าใจส่วนตัว มันคือความสัมพันธ์ระหว่าง input กับ output เช่น f(x) = x² หมายถึงฟังก์ชันที่ให้ output เป็นค่ายกกำลังสองของ input
ส่วนฟังก์ชันในทางโปรแกรมมิ่งหมายถึงชุดของโค้ดที่ทำงานอย่างใดอย่างหนึ่งโดยเฉพาะ มีการรับอินพุทที่เรียกว่า “พารามีเตอร์ (parameter)” (ส่วน “อาร์กิวเมนท์ (argument)” ใช้สำหรับเรียกอินพุทฝั่งขาส่ง) โปรแกรมที่ดีจะต้องแตกโค้ดออกมาเป็นฟังก์ชันย่อยๆ ซึ่งมีข้อดีมากมาย เช่น readability, maintainability, unit testability, reusability
function add(x: number, y: number): number {
return x + y;
}add(1, 2);
ในโลกของ JavaScript ฟังก์ชันถือเป็นพลเมืองชั้นหนึ่ง (first-class citizen) เทียบเท่ากับ primitive type ทั้งหลาย เช่น int, float, string, boolean นั่นคือเราสามารถ
- ส่งฟังก์ชันเข้าไปเป็นอาร์กิวเมนท์ของฟังก์ชันอื่นได้ (higher-order function)
- รีเทิร์นค่าออกมาเป็นฟังก์ชันได้
- กำหนดค่าให้ตัวแปรเป็นฟังก์ชันได้ หรือจับยัดใส่ data structure ใดๆ ก็ได้
- รองรับการเขียน anonymous function ได้
หากภาษาใดมีคุณสมบัติทั้ง 4 ข้อดังกล่าว ภาษานั้นจัดว่าเป็น “first-class function”
JavaScript เป็นภาษาที่ไม่มี official runtime เป็นเพียงสเปค ที่ออกโดยองค์กร ECMA International ซึ่งเดิมทีมีการใช้งาน JavaScript เฉพาะบนเว็บไซต์เพียงอย่างเดียว บริษัทที่ทำเว็บเบราเซอร์ต่างก็ทำ JavaScript engine ของตัวเองออกมา เช่น SpiderMonkey ของ Mozilla ที่ใช้ใน Firefox หรือ V8 ของ Google ที่ใช้ใน Chrome
JavaScript (ECMAScript, ES) ถูกออกแบบมาให้ทำงานเป็น single thread นั่นหมายความว่าการรัน synchronous function จะต้องมีการหยุดรอผลลัพธ์เสมอ (blocking function) งานที่ทำเสร็จเร็ว ก็จะรอแค่ช่วงสั้นๆ ส่วนงานที่เสร็จช้า จำพวก I/O ต่างๆ เช่นการเรียก HTTP, การอ่านไฟล์ ก็ต้องรอจนกว่ามันจะเสร็จ งานอื่นๆ ที่เหลือก็จะถูกบล็อคไว้ อาการแบบโปรแกรมค้าง
ส่วน asynchronous function นั้น เรียกอีกอย่างว่า non-blocking function หลังเรียกใช้แล้วก็แค่ฝากเบอร์โทรกลับ จากนั้นจะสามารถไปทำงานอื่นที่ไม่เกี่ยวข้องกับ output ของ function นี้ต่อได้เลยทันที ซึ่ง “เบอร์โทรกลับ” ที่ว่านี้แบ่งออกได้ 4 วิธี คือ
1. Callback
เป็นบรรพบุรุษของวิธีการจัดการกับ asynchronous function ทั้งปวง callback คือฟังก์ชันที่ถูกส่งเข้าไปเป็นอาร์กิวเมนท์ของฟังก์ชันอื่น (higher-order function) การใช้งาน callback ที่เห็นกันแพร่หลายใน JavaScript มักจะหน้าตาแบบนี้
document.getElementById('x').addEventListener('click', function() {
console.log('DOM #x has been clicked.')
})หรือในรูปแบบกระทัดรัดอ่านง่ายของ jQuery
$('#x').click(function() {
console.log('DOM #x has been clicked.')
})เมื่อโค้ดเรามีขนาดใหญ่ขึ้น มีการทำงานที่ซับซ้อนขึ้น เราจะเจอการเรียกใช้ callback ซ้อน callback ซ้อน callback ซึ่งมีชื่อเรียกกันในวงการว่า “Callback Hell” วิธีการแก้ไขเบื้องต้นคือยกฟังก์ชันออกมาเขียนแยกต่างหาก ไม่เขียนแบบ anonymous function ซึ่งในลิงก์ได้อธิบายวิธีแก้ปัญหาไว้แล้ว
2. Promise
วิวัฒนาการต่อจาก Callback แรกเริ่มถูกพัฒนาเป็น library ก่อน ต่อมาถูกกำหนดไว้ใน ECMAScript 2015 (ES6) หน้าตาเป็นประมาณนี้
function isValid(param) {
return new Promise((resolve, reject) => {
try {
// do some work
if (valid) resolve(true)
else resolve(false)
} catch (err) {
reject(err)
}
})
}resolve ใช้สำหรับส่งกลับ (yield) ค่าปกติที่เกิดจากการทำงานของฟังก์ชัน
reject ใช้สำหรับส่งกลับค่าที่ผิดปกติ เช่น error, exception
หน้าตาฝั่งขาเรียกใช้งาน
isValid(arg).then(param => {
if (valid) {
} else {
}
}).catch(e => {
// handle the error
}).then() จะรับค่าที่ถูกส่งมาจากฟังก์ชัน resolve()
.catch() จะรับค่าจากฟังก์ชัน reject()
3. async/await
ต่อมาใน ECMAScript 2016 (หรือ ES2016, หรือ ES7) ได้มีการเพิ่มสเปค async/await เข้ามา มันคือการพัฒนาต่อยอดจาก Promise ที่ช่วยแปลงโค้ดฝั่งผู้เรียกซึ่งมีหน้าตาซับซ้อนแบบ asynchronous ให้อ่านง่ายดูสบายตาแบบ synchronous (ยกเลิกการใช้ .then) แบบนี้
const validateSomething = async input => {
if (await isSomething(input)) {
return output
}
}const output = await validateSomething(arg)
จำง่ายๆ ว่า ใช้ await แทน .then และฟังก์ชันไหนที่มีการเรียกใช้ await ภายใน จะต้องแปะคำว่า async ที่หัวฟังก์ชันด้วย
4. Reactive Programming (Observable)

คำจำกัดความของ Reactive Programming หรือ Rx หรือ Observable คือ
Observable is just a function that takes an observer and returns a function
มีบล็อกมากมาย (หาได้จาก Google เช่น นี่ , นี่ , นี่ , นี่ , นี่) ที่พยายามอธิบายว่า Observable คืออะไร สำหรับผม (ซึ่งเคยงงกับมันมาหลายวัน) สุดท้ายเข้าใจว่ามันก็คือ Promise ที่สามารถยิงค่าออกมาได้หลายๆ ค่า โดยมี Operator ซึ่งก็คือฟังก์ชันทั้งหลายที่สามารถใช้ได้กับ Enumerable Object (Array, Hash) เช่น map, reduce, filter ซึ่งสามารถ chain ต่อๆ กันได้ โดย output ของฟังก์ชันก่อนหน้าจะเป็น input ของอีกฟังก์ชันตัวถัดไป สำหรับ Observable เราสามารถใช้กับ single source เช่น Array หรือจะใช้กับ multiple sources เช่น Stream (ยกตัวอย่าง event การเคลื่อนที่ของ mouse) ก็ได้

Ben Lesh ซึ่งเป็นหัวหน้าทีม RxJS 5+ ได้เขียนคำอธิบายที่เข้าใจได้ง่ายๆ ไว้ดังนี้
Promises are always multicast. Promise resolution and rejection is always async. Observables are sometimes multicast. Observables are usually async.
Observable ฝั่งขาส่งจะมี 3 ฟังก์ชัน คือ next(), complete(), และ error() หน้าตาประมาณนี้
const hello = new Observable(observer => {
try {
// do some work
observer.next('Hello')
observer.complete()
} catch (e) {
observer.error(e)
}
})ส่วนฝั่งขารับจะใช้ subscribe() ร่วมกับ operator ต่างๆ เช่น map(), filter()
hello.map(val => `${val}, World`)
.subscribe(val => console.log(val), err => console.error(err))// Success output: Hello, World
มีคนสงสัยว่าเมื่อไหร่ควรจะใช้ Promise เมื่อไหร่ควรจะใช้ Observable บางคำตอบบอกว่าสำหรับ single source / single value เช่น Array ก็ให้ใช้ Promise ไป ส่วน multiple sources / multiple values เช่น Stream ต่างๆ ก็ให้ Observable แต่บางคำตอบก็บอกว่าควรใช้ Observable สำหรับทุกๆ อย่างเลย เพราะว่า…


