Promises โดยสังเขป

อ้างอิงจาก: https://davidwalsh.name/promises 

ก่อนจะเข้าเรื่องของ Asynchronous ใน ES6 นั้น ผมอยากจะให้ผู้อ่านได้ทำความเข้าใจในเรื่องของ Promises กันก่อน

ตัว Promises นั้น เป็น API ของ javascript ที่ทำหน้าที่สำหรับการทำงานแบบ asynchonous หรือการสั่งงาน code ในแบบที่ไม่ต้องรอลำดับ 1 2 3 แต่สามารถข้ามไปทำงานอื่นก่อนได้ แล้วพองานที่เราสั่งไว้ทำเสร็จแล้ว ระบบก็จะกลับมาทำงานต่อจากงานชิ้นนั้นต่อ

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

Promises คือ API ที่มีความสำคัญมากตัวหนึ่งในโลกของ JavaScript ไม่ต้องพูดพร่ำทำเพลง เรามาดูกันเลยดีกว่าว่ามันทำงานอย่างไร

การใช้งาน Promise ขั้นพื้นฐาน

เวลาจะใช้งาน Promise จะเริ่มจากการใช้คำสั่ง new Promise() โดยภายใน promise นั้นจะต้องมี callback function ที่รับ argument สองตัวคือ resolve และ reject ดังตัวอย่างต่อไปนี้

ซึ่งตัว resolve จะใช้สำหรับกรณีที่การ request ทำได้สำเร็จ หลังจากนั้นจะไปสั่ง callback function ภายใน .then ให้ทำงานต่อ และหากเกิดการ reject เมื่อใด มันก็จะไปรัน callback function ภายใน .catch ให้ทำงาน

ต่อไปนี้คือตัวอย่างการใช้งานจริงเป็นการ convert XMLHttpRequest ให้ทำงานแบบ Promises ดังนี้

ฟังก์ชั่น get() จะทำหน้าที่ในการ return promises ออกมา โดยภายใน promises ดังกล่าวนั้น จะทำการส่ง GET method request หาก server return 200 status กลับมา ก็ให้สั่ง resolve หากเป็นสถานะอื่นให้ reject โดยแจ้งให้เป็น error

then

promises ทุกตัวจะมี then พ่วงติดมาด้วย มีหน้าที่ในการตอบสนองต่อ promises โดย then ตัวแรกที่ต่อจาก promises โดยตรง (then ต่อพ่วงได้หลายชั้น) จะรับค่าที่ถูกส่งมาจาก resolve() มาเป็น argument ดังตัวอย่างต่อไปนี้

callback function ภายใน then นั้นจะถูกเรียกใช้เมื่อ promises ได้รับการ resolve ที่สำคัญคือ คุณสามารถนำ then มาต่อพ่วงหลายๆ ชั้นได้ ดังตัวอย่างต่อไปนี้

โดย then แต่ละตัวจะรับค่าจาก then ก่อนหน้ามาเป็น argument ให้กับ callback ของตนเอง

catch

catch จะถูกเรียกใช้เมื่อ promises ถูก reject ดังตัวอย่างต่อไปนี้

จากตัวอย่างข้างต้น เราทำการสั่งให้เกิดการ reject แล้วส่ง string เข้าไปเป็น argument ให้กับ callback ของ catch ดังนั้น console จึงพิมพ์ค่าออกมาเช่นตัวอย่าง

ส่วนจะส่งค่าอะไรให้กับ reject นั้นก็แล้วแต่สไตล์ของแต่ละคน แต่แพทเทิร์นโดยส่วนใหญ่แล้วมักจะใช้ Error ดังนี้

Promise.all

บางครั้งเราอาจจะต้องมีการเรียกใช้งานแบบ asynchronous หลายๆ ตัว พร้อมๆ กัน และเราต้องการที่จะเริ่มทำงานบางอย่างเมื่อ request ทั้งหมดนั้น resolve แล้ว สำหรับกรณีนี้ เราจะใช้ Promise.all เพื่อรับค่า array ของ promises แล้วจากนั้นก็จะรอจนกว่า promise ทั้งหมดภายใน array นั้นจะ resolve ทุกตัว จากนั้นจึงค่อยเรียกใช้ callback function ภายใน then

ตัวอย่างจริงที่เรามักจะทำกันก็คือ การยิง AJAX request หลายๆ ตัวพร้อมๆ กัน ดังนี้

สำหรับ Promise.all นั้น หากมี promise ตัวใดตัวหนึ่ง reject มันจะยิงไปที่ catch ทันที ดังตัวอย่างต่อไปนี้

Promise.race

Promise.race จะต่างจาก Promise.all ตรงที่ มันไม่รอให้ promises ภายใน array ทั้งหมด resolve เพราะขอเพียงแค่มี promise ตัวใดตัวหนึ่ง resolve มันจะเรียก callback ภายใน then ทันที ดังตัวอย่างต่อไปนี้

จาก code ข้างต้น req2 จะรอแค่ 3 วินาที ส่วน req1 ต้องรอ 8 วินาทีจึงจะ resolve ดังนั้น ผลลัพธ์ของมันจึงเป็นการพิมพ์คำว่า “Then: Seocnd!” ออกมาทาง console โดยไม่รอให้ req1 resolve

Async/Await

ใน ES6 นั้นได้เพิ่มฟีเจอร์สำหรับการเขียน code แบบ asynchronous เอาไว้ โดยมันได้รับการออกแบบมาให้เขียนได้เหมือนกับ code แบบ synchronous เลย ช่วยให้อ่าน และทำความเข้าใจง่ายกว่าการเขียนแบบเดิม แต่ก็ไม่ได้แตกต่างกันมากจนถึงกับเชื่อมกันไม่ติด

ให้เราท่องประโยคดังต่อไปนี้ให้ขึ้นใจ

async function ทุกตัวจะ return promises และ await จะใช้กับ promises เท่านั้น 

ให้คุณกลับมาอ่านประโยคข้างต้นซ้ำแล้วซ้ำอีกจนกว่าจะขึ้นใจ async function ทุกตัวจะ return promises และ await จะใช้กับ promises เท่านั้น

โอเค เรามาดูตัวอย่างจริงกันเลยดีกว่า

ฟังก์ชั่น getUser จะ return promise ออกมา โดยในที่นี้ต้องรอ 2 วิกว่าที่มันจะ resolve โดยยิง object ที่มี property ที่ชื่อว่า name อยู่ภายในออกมา

จากนั้นก็ทำการ handle promise จาก getUser โดยเขียนดังนี้

สรุป โปรแกรมนี้จะรอ 2 วินาที แล้วจึงค่อยพิมพ์คำว่า Object {name: “Tyler”} ออกมาทาง console

screen-shot-2559-11-12-at-11-39-25-am

คราวนี้หากอยากจะ handle ในกรณีเกิด reject ด้วยก็ให้ใส่ catch เข้าไป ดังนี้

จบ เพียงแค่นี้การจัดการกับ user ก็สมบูรณ์

คราวนี้สมมติว่า เราอยากจะเขียนให้ฟังก์ชั่นมันดูเป็นฟังก์ชั่นปรกติ ไม่ดูแปลกแบบ promise ที่ต้องพ่วง .then และ .catch ต่อท้ายเสมอ โดยเราอยากจะเขียนให้ฟังก์ชั่น handleGetUser มีหน้าตาประมาณนี้

โชคร้ายที่ code ข้างต้นไม่สามารถใช้งานได้ เพราะ promise (ที่ return มาจาก getUser()) มันไม่สามารถทำงานแบบนี้ได้ (คือกรูต้องแปลกกว่าชาวบ้าน) งานของเราคือ ต้องบอกกับ compiler (คือจริงๆ ก็ browser นั่นแหละ) ว่า ไอตัว getUser() เนี่ยมันคือ asynchronous function ซึ่งระบบจะต้องไม่รัน console.log(user) จนกว่า getUser() จะทำงานเสร็จ ซึ่งสามารถทำได้โดยใช้คำว่า await ไว้ข้างหน้า getUser() ดังนี้

ง่ายปะ คราวนี้ขอย้อนประโยคที่ผมให้คุณผู้อ่านท่องจำไปในตอนแรกที่ว่า

AWAIT จะใช้กับ PROMISES เท่านั้น 

นั่นหมายความว่า เมื่อใดก็ตามที่เราเติมคำว่า await ไว้หน้าฟังก์ชั่นใด ก็เท่ากับบอก compiler ว่า ฟังก์ชั่นดังกล่าวจะ return promise เสมอ และระบบจะทำการรอจนกว่า promise ดังกล่าวจะมีการ resolve จากนั้นจึงค่อยดำเนินการต่อไป ซึ่งในที่นี้ก็คือรับคำสั่ง console.log(user)

ปัญหาของ code ตัวอย่างข้างต้นก็คือ await ไม่สามารถวางไว้เดี่ยวๆ แบบนี้ได้ เพราะมันต้องใช้คู่กับ async (นี่คือสาเหตุที่ฟีเจอร์นี้ของ ES6 มีชื่อว่า async/await) ซึ่งกฎของการใช้งานฟีเจอร์นี้ก็คือ “await ต้องถูกใช้ภายใน async function เท่านั้น” ดังตัวอย่างต่อไปนี้

เพียงเท่านี้ คุณก็สามารถกำจัด .then อันแสนอัปลักษณ์ออกไปจาก code ของเราได้แล้ว (อันนี้ก็ขึ้นอยู่กับมุมมองของ dev. อีกนั่นแหละว่า ชอบ .then หรือชอบ async/await มากกว่ากัน เป็นเรื่องของรสนิยมล้วนๆ ใช้อะไรก็ได้ เพราะมันให้ผลเหมือนกัน)

ว่าแต่ catch หายไปไหนอ่ะ?

อันนี้แหละคือส่วนที่งดงามที่สุดของ async/await เพราะมันทำให้เราเขียน code แบบ try/catch ใน Javascript ได้เสียที ดังนั้น เราสามารถแทรก catch handler เข้าไปใน code ข้างต้น ดังนี้

สวยงาม! คราวนี้ การเขียน code สำหรับ asynchronous ก็ดูดีมีสกุลรุนชาติ ไม่เหมือนเด็กไร้หัวนอนปลายเท้า (แบบ javascript รุ่นพ่อมัน)

Project refactoring

ได้เวลานำความรู้เรื่องนี้มา refactor โปรเจ็กท์ของเราแล้ว

Install new feature of ECMAScript spec.

เนื่องจาก Javascript ในเวอร์ชั่นใหม่ๆ นั้น ได้รับการเสนอฟีเจอร์ใหม่ๆ เข้ามาตลอดเวลา ดังนั้น ทางฝ่ายจัด specification ของ ECMAScript (ชื่อทางการของ Javascript) จึงได้แบ่ง feature ออกเป็นสาม stage ดังนี้

  • Stage 0: เป็นฟีเจอร์ที่กำลังอยู่ในช่วงทดลอง
  • Stage 1-3: กำลังอยู่ระหว่างการดำเนินงานเพื่อนำเข้าเป็นฟีเจอร์ทางการของ ECMAScript
  • Stage 4: คือฟีเจอร์ที่ได้เสร็จแล้ว และพร้อมปล่อยออกสู่ตลาดใน ECMAScript เวอร์ชั่นถัดไป

ในส่วนของ Async/Await นั้น เป็น feature ที่อยู่ใน stage 3 ดังนั้น เราจึงจำเป็นต้องทำการ install ตัว stage 3 เข้าไปในระบบของเราด้วย ไม่อย่างนั้น babel ก็จะไม่รู้จักกับฟีเจอร์นี้

ให้เปิด terminal แล้วเข้าไปที่ root project จากนั้นพิมพ์คำสั่งต่อไปนี้

ในที่นี้เรา install สอง module คือ babel-preset-stage-3 สำหรับแทรกฟีเจอร์ของ stage-3 เข้าไปใหักับโปรเจ็กท์ของเรา ส่วน babel-polyfill นั้นเป็นตัว runtime ที่ช่วยให้ Async/Await ทำงานได้เป็นปรกติ

Configuration

เปิดไฟล์ .babelrc ขึ้นมาแล้วแทรก “babel-preset-stage-3” เข้าไปใน presets ดังนี้ (อย่าลืมใส่ ‘,’ หลัง “babel-preset-es2015” ด้วยนะครับ)

จากนั้นให้เปิดไฟล์ webpack.config.js ที่อยู่ใน root path แล้วทำการแก้ config ดังนี้

จากนั้นให้ทำการ start server โดยพิมพ์คำสั่งต่อไปนี้ใน terminal

ConfirmBattleContainer Refactoring

ได้เวลา refactor code ของจริงกันแล้ว เริ่มจาก app/containers/ConfirmBattleContainter.js กันก่อนเลย โดยเราจะใส่ Async/Await เข้าไปดังนี้

code ข้างต้น ผมได้ทำการแทรก async เข้าไปที่ componentDidMount แล้วจากนั้น ก็ใส่ await ให้กับฟังก์ชั่น getPlayersInfo (ผมได้เปลี่ยนวิธีการ import getPlayersInfo โดยใช้วิธี Destructuring สำหรับ githubHelpers อีกด้วย ทำให้ไม่ต้องเขียน code ยาว) เมื่อได้ผลลัพธ์มา ก็ assign ให้กับ players แล้วจากนั้นก็นำไปใช้สำหรับการ setState ต่อไป โดยจำเป็นต้องมี then อีกต่อไป

คราวนี้ผมอยากจะเติม catch handler ให้กับฟังก์ชั่นตัวนี้ด้วย จึงขอ refactor code ตัวนี้อีกซักรอบ ดังนี้

ResultsContainer Refactoring

จากนั้นให้เราเปิดไฟล์ app/containers/ResultsContainer.js ขึ้นมา แล้วทำการ refactor ดังนี้

หลักการในการ refactor ของ ResultsContainer นั้นไม่ได้ต่างจาก ConfirmBattleContainer เลย ดังนั้น ผมขอไม่อธิบายเพิ่มเติมนะครับ

githubHelpers Refactoring

ให้เปิดไฟล์ app/utils/githubHelper.js ขึ้นมาแล้วทำการแก้ code ดังต่อไปนี้

จากการ refactor ข้างต้น มีจุดที่ผมอยากเน้นดังนี้คือ

หากจะใช้ Async/Await กับ promise ที่มี then ต่อกันหลายตัว ให้ใช้ await ต่อกันหลายชั้น โดยผมเปลี่ยน code จาก

มาเป็น

ผมได้ตัดการครอบ function หลักสองตัวออกจาก const helpers แล้ว เพื่อทำการ export ออกไปโดยตรง โดยผมได้แก้ code จาก

มาเป็น

จากนั้นให้ลองเปิด browser เพื่อทดสอบระบบว่า ยังทำงานได้เหมือนเดิมอยู่หรือไม่

สำหรับผู้ที่ต้องการตรวจสอบกับ code ต้นแบบ สามารถเข้าไปดูได้ที่

https://github.com/himaeng/react-fun-course/tree/master/ES05-async-await

สรุป

Async/Await เป็นฟีเจอร์ใหม่ของ ES6 ที่ช่วยให้เราทำงานร่วมกัน promise ได้อย่างสวยงามมากขึ้น ส่วนมันจะงดงามหรืออัปลักษณ์เพียงใด ก็ขึ้นอยู่กับมุมมองของผู้พัฒนาแล้วล่ะครับ เพราะบางครั้ง เราก็ไม่ควรให้ใคร (หรือกลุ่มใด) มากำหนดรสนิยมในการเขียน code ของเราได้ (แต่ควรจะเคารพระเบียบการเขียน code ขององค์กรที่ตนทำงานอยู่ เพื่อประโยชน์ของส่วนรวม และทีมงาน)