async / await อนาคตใหม่แห่ง JavaScript

Pallop Chaoputhipuchong
hamcompe
Published in
2 min readJan 16, 2017

--

ว่าด้วยประเด็นที่ว่า JavaScript นั้นเป็นภาษาโปรแกรมประเภท Non Blocking I/O หากใครเขียนผ่านการเขียนภาษาประเภทนี้มาก่อน ก็คงเข้าใจความน่าปวดหัวของมัน โดยเราต้องเขียนโปรแกรมออกมาเป็นรูปแบบ Asynchronous เสมอ

Asynchronous จะมีรูปแบบคือตัวโปรแกรมจะทำคำสั่ง 1, 2, 3 โดยไม่สนว่าคำสั่งก่อนหน้านั้นเสร็จหรือยัง ดังนั้นการทำงานแต่ละคำสั่ง ผลลัพธ์ที่คำนวณได้จึงไม่จำเป็นต้องออกมาตามลำดับที่ถูกเรียกใช้

ความไม่เป็นลำดับของ JavaScript !?

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

ตัวอย่างผลลัพธ์ที่ไม่ทำงานเป็นลำดับ

function doA() {
setTimeout(function() {
console.log('I have been executed first')
}, 1000)
}
function doB() {
console.log('I have been executed second')
}
doA()
doB()
/**
OUTPUT:
I have been executed second
I have been executed first

หากสังเกตจากโค้ดข้างต้นจะเห็นว่าคำสั่ง doA() ของเรานั้นถูกรันก่อน doB() แต่ผลลัพธ์ที่ได้จาก doB() กลับถูกแสดงผลก่อน doA()

นั้นเป็นเพราะ JavaScript ไม่สนใจว่าคำสั่ง doA() ที่ถูกรันไปก่อนหน้านั้นเสร็จหรือยัง หลังจากมันทำการสั่งให้ doA() เริ่มทำงาน มันก็ทำการสั่งให้ doB() เริ่มทำงานต่อทันที

งั้นหากเราต้องการให้คำสั่ง doA() ทำงานเสร็จก่อน ค่อยรัน doB() เป็นลำดับต่อละ?

ดังนั้นเอง callback จึงถือกำเนิดขึ้นมาในโลกของ JavaScript

Callback

callback คือการใช้ความสามารถของ JavaScript ที่ตัวแปรในฟังก์ชัน สามารถรับฟังก์ชันเข้ามารันได้

ตัวอย่าง

function doA(callback) {
setTimeout(function() {
console.log('I have been executed first')
callback()
}, 1000)
}
function doB() {
console.log('I have been executed second')
}
doA(function() {
doB()
})
/**
OUTPUT:
I have been executed first
I have been executed second

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

จากโค้ดข้างต้น Callback จึงเป็นรูปแบบที่นำมาใช้ในการแก้ปัญหาให้โค้ดของเรานั้นถูกรันเป็นลำดับได้….

แต่แล้วไม่นานในเวลาต่อมา ปัญหาก็ได้เกิดขึ้นซึ่งสิ่งนั้นเองเรียกว่า Callback Hell…

ปัญหาของ Callback (Callback Hell)

ตามธรรมเนียมปฏิบัติแล้ว การเขียน callback จริงๆ ตัวแปรตัวแรกจะทำการเก็บ Error object และตัวแปรถัดมาทำการเก็บข้อมูลที่จะใช้

หากตัวแปรตัวแรกไม่มีค่านั้น ก็หมายความว่าไม่มี Error เกิดขึ้น

โดยเราสามารถพบเห็นโค้ด JavaScript ทั่วไปที่ทำงานต่อๆ กันได้แบบนี้

function todo() {
getA(function(err, a) {
if(err) throw new Error('getA error.')
getB(a, function(err, b) {
if(err) throw new Error('getB error.')
getC(b, function(err, c) {
if(err) throw new Error('getC error.')
// Do something next
})
})
})
}

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

รูปแบบ callback ที่เกิดขึ้นซ้อนๆ กันนี้ถูกเรียกว่า Callback Hell นั่นเอง

จากประสบการณ์เลวร้ายที่เกิดขึ้น ในเวลาต่อมาจึงได้เกิดรูปแบบการเขียนใหม่ ที่เรียกว่า Promise

Promise

ตามชื่อมันเลย Promise เหมือนเป็นคำมั่นสัญญาบอกว่า “ฉันจะทำมันให้เสร็จนะ” ซึ่งถูกนำมาใช้ในการแก้ไขปัญหาของ Callback Hell นั่นเอง

โดย Promise มีรูปแบบการเขียนดังต่อไปนี้

function todo() {
return getA()
.then(function(a) {
return getB(a)
})
.then(function(b) {
return getC(b)
})
.then(function(c) {
return getD(c)
})
.catch(function(err) {
// Handle error
})
}

โอ้โห! สวยงามสะอาดตา แถมอ่านง่ายเป็นภาษาคนสุดๆ แบบนี้ดีงามๆ

ปัญหาของ Promise

หากคำสั่งลำดับล่างกลับต้องการค่าตัวแปรจากด้านบนล่ะ เช่น

getC() ต้องการค่าตัวแปร a, b ส่วน getD() ต้องการค่าตัวแปร b, c

โค้ดในรูปแบบ promise ก็จะเริ่มไม่สวยงามอีกต่อไป จะกลายเป็น

function todo() {
return getA()
.then(function(a) {
return getB(a)
.then(function(b) {
return getC(a, b)
.then(function(c) {
return getD(b, c)
})
})
})
}

สุดท้ายแล้วมันก็เริ่มกลับมาอีหรอบเดิมกันกับตอน Callback นี่หว่า

งั้นทายสิ เจ้าปัญหาที่เกิดขึ้นนี้เรียกว่าอะไร?…. 1.. 2.. 3…

Promise Hell นั่นเองจ้า!

async / await

Syntax ใหม่ที่จะมาเปลี่ยนโลกของการเขียน JavaScript ช่วยให้การเขียน code JavaScript ของคุณสบายขึ้นสุดๆ สิ่งนั้นก็คือ async/await นั่นเอง!

เริ่มเขียน async / await

การเขียน async/await นั้นง่ายมาก ให้คิดภาพเหมือนเราเขียนโค้ดในภาษาอื่นที่ไม่ใช่ Non Blocking I/O

โดยจะมีรูปแบบตัวอย่างดังต่อไปนี้

async function todo() {
const a = await getA()
const b = await getB(a)
const c = await getC(b)
return getD(c)
}

โอ้วพระเจ้าจอร์จ! มันเยี่ยมมาก

แล้วปัญหาที่ว่า คำสั่งลำดับหลัง ต้องการค่าตัวแปรจากคำสั่งก่อนหน้า อย่างที่เคยเกิดขึ้นใน Promise ล่ะ?

เมื่อมาเขียนในรูปแบบ async/await ก็จะได้ดังนี้

async function todo() {
const a = await getA()
const b = await getB(a)
const c = await getC(a, b)
return getD(b, c)
}

นอกจากนั้นความดีงามของ async/await คือเราสามารถใช้งานกับ module ที่ถูกเขียนขึ้นด้วยรูปแบบ Promise อยู่แล้วได้เลย

หรือในกรณีที่เราต้องการให้ฟังก์ชัน 2 ตัวถูกรันพร้อมกัน เราก็สามารถทำได้ด้วยวิธีการดั้งเดิมโดยใช้ Promise.all เช่น

async function todo() {
const [a, b] = await Promise.all([getA(), getB()])
}

บทสรุป

ตอนนี้ feature การเขียนในรูปแบบ async/await นี้ยังไม่ถูกนำมาใส่ใน Node.js เวอร์ชั่นปัจจุบัน แต่คาดว่าคงได้มาให้เห็นกันในเร็วๆนี้ไม่นานนัก

หากเพื่อนๆ คนไหนสนใจก็สามารถไปลองทดสอบเขียนได้ด้วยการ compile ผ่าน babel เอานะครับ (ซึ่งส่วนตัวกำลังใช้อยู่ ชอบมาก)

สำหรับบทความนี้ ก็ขอจบแต่เพียงเท่านี้ หวังว่าบทความนี้จะมีประโยชน์ต่อผู้เริ่มต้นเขียน JavaScript ทุกคนนะครับ ขอบคุณครับ

--

--