ควบคุมการทำงานใน [Node.js ,Js] ให้อยู่หมัด ด้วย [callback , promise , async await]
เมื่อผมเริ่มเขียน javascript , Node.js ครั้งแรก มักเจอปัญหาใหญ่ๆด้วยความที่ไม่รู้…เห้ยทำไมค่าจาก code ที่เขียน บรรทัดนี้ทำไมมันทำก่อน ทั้งๆที่บรรทัดก่อนหน้านี้มันยังทำงานไม่เสร็จเลย ตัวอย่างเช่น
/* run onnode.js version 7.10 */const axios = require('axios');/*use package "npm i axios -S"*/var instance = axios.create({baseURL: 'https://unsplash.it/'});function getData() { /*get data from api https://unsplash.it*/ instance.get('/list').then(function (response) { console.log('main todo'); console.log("response total %s <",response.data.length); }).catch(function (error) { console.log("if error"); });}function main() { console.log('main work'); getData(); console.log("main end");}main();
ผลลัพธ์ที่ได้
main work <-ทำงานอันแรกmain end <-ข้ามมาอันสุดท้ายmain todo <- ทำงานพึ่งเสร็จresponse total 1013 < <- ทำงานพึ่งเสร็จ
ซึ่งจริงๆแล้วที่เราอยากได้ คือให้ main todo ทำงานเสร็จก่อน แถมต้องการข้อมูลที่ได้จาก function getData(); เพื่อนำไปใช้อีกใน function อื่นๆ อีก
ส่งที่ต้องเตรียม
- Node.js 7.10 เพราะ async/await support version นี้ขึ้นไป เช็คได้จาก node.green สำหรับ version ที่น้อยกว่านั้น หรือ front-end(javascript) สามารถใช้ได้เช่นกัน โดยติดตั้ง Transpiler babel stage-0 เพื่อแปลง code ที่เราเขียนให้เป็น es5
- *ในแต่ละตัวอย่างผมใช้ package ‘axios’ เพื่อดึง api จากเว็บ https://unsplash.it/
- เตรียมใจให้พร้อมเราทำได้…. 😄
ปัญหาเหล้านี้จะหมดไป…ผมขอเสนอ 3 วิธี
Hello Callback (Hell) ไม่แนะนำนาจาแต่รู้ไว้ก็ไม่เสียหาย
/* run onnode.js version 7.10 */const axios = require('axios');/*use package "npm i axios -S"*/var instance = axios.create({baseURL: 'https://unsplash.it/'});function getData(cb) { /*step2 รับ function เข้ามาผ่าน parameter ด้วยตัวแปร cb*/ instance.get('/list').then(function (response) { /*step3 โยนค่ากับไป โดยเรากำหนด param ตัวแรก เป็นที่สำหรับกรณี error ตัวที่สอง สำหรับส่ง ข้อมูลกลับ*/ cb(null,response.data); }).catch(function (error) { cb("if error");/*step3 กรณี error*/ });}function main() { console.log('main work'); /*step1 เราโยน function ลงไปเป็น parameter*/ getData(function(error,result){ /*step4 รอรับค่าจากที่ step3 ส่งกลับมา โดยเราอาจดัก error เเบบนี้ if(error){ ทำบางอย่างเมื่อ error }*/ console.log('main todo'); console.log("response total %s <",result.length); console.log("main end") });}main();
function getData(); เรารับ parameter cb ซึ่งสิ่งที่เรารับมานั้นคือ function
function getData(cb) { instance.get('/list').then(function (response) { cb(null,response.data); }).catch(function (error) { cb("if error");/*step3 กรณี error*/ });}
เมื่อเราต้องการโยนค่ากลับไปยัง getData(); ที่ถูกเรียกใช้ เราก็จะใช้ cb โดยสั่งให้ cb ทำงาน โดย cb() คือเรียกใช้ function นั่นเอง โดยผมได้แนบ parameter กลับไปด้วย กำหนดให้ parameter
- ตัวแรกนั้นเป็น กรณี่ที่ error เท่านั้น และ parameter
- ตัวที่สองเป็น กรณี ต้องการส่งข้อมูลกลับไป
cb("case error","send result");
ตอนเราเรียกใช้ function getData(); เราโยน function ลงไปเป็น parameter ของ function getData() เพื่อจะรอรับ callback function ที่เราเรียกผ่าน cb() ตามตัวอย่างนี้
getData(function(error,result){ //รอรับค่าที่กลับมาจาก callback})
หลายคนอาจมองว่าแล้วมันยังไงหละ ก็เเค่ส่งค่ากลับมานิ
คำตอบ : ใช่ครับ
แต่มันมีเห็นผลอยู่ว่าทำไมถึงมี คำว่า HELL ต่อท้าย
ตัวอย่างเมื่อ เราต้องทำงานกับ function getData(); หรือที่คล้ายๆกรณีนี้ ใน code ใน function main() ก็จะเปลี่ยนไปเป็นอย่างนี้
function main() { getData(function (error, result) { getData(function (error, result) { getData(function (error, result) { getData(function (error, result) { // HELL หรือยังครับ เเต่ใครว่ายัง หรือชอบ ก็ตามสบายครับ ไม่ขัดศรัทธา }); }); }); });}
HELL หรือยังครับ เเต่ใครว่ายัง หรือชอบ ก็ตามสบายครับ ไม่ขัดศรัทธา
Hello Promise(เราจะทำตามคำสัญญาขอเวลาอีกไม่นานน…)
/* run onnode.js version 7.10 */const axios = require('axios'); //use package "npm i axios -S"var instance = axios.create({baseURL: 'https://unsplash.it/'});function getData() { /*step2 ใช้คำสั่ง new Promise , return new Promise เพื่อต้องการส่ง promise กลับ*/ return new Promise(function (resolve, reject) { instance.get('/list').then(function (response) { /*step3 โยนค่ากับไป ผ่าน reslove */ resolve(response.data); }).catch(function (error) { /*step3 กรณี error โยนค่ากับไป reject*/ reject("if error"); }); })}function main() { console.log('main work'); /*step1 เราโยน function ลงไปเป็น parametor*/ getData().then(function(result){ console.log('main todo'); console.log("response total %s <", result.length); console.log("main end") }).catch(function(error){ console.log("case error"); })}main();
hell Promise โดยใช้คำสั่ง new Promise
new Promise(function (resolve, reject) { resolve("result"); reject("if error");});
โดยเมื่อ code ทำงานมาถึง resolve หรือ reject จะทำการส่งค่ากลับทันที (เอาจริงมันก็ไม่ได้ ดีดตัวออกเหมือน break; หรือ return ผมชอบเขียนแบบนี้เพื่อให้มันหยุดเลย)
new Promise(function (resolve, reject) { return resolve("result"); return reject("if error");});
ใส่ return ไปเลย ฮาๆ
เมื่อนำมาใช้งานกับ function getData(); ก็จะเป็นแบบนี้
function getData() { /*step2 ใช้คำสั่ง new Promise , return new Promise เพื่อต้องการส่ง promise กลับ*/ return new Promise(function (resolve, reject) { instance.get('/list').then(function (response) { /*step3 โยนค่ากับไป ผ่าน reslove */ resolve(response.data); }).catch(function (error) { /*step3 กรณี error โยนค่ากับไป reject*/ reject("if error"); }); })}
และตอนเรียกใช้ function getData(); โดยใช้
- nameFunction().then ต่อท้าย function ที่ถูกเรียกใช้งาน และภายใน .then จะมี function .then(function(…param ที่ส่งกลับผ่าน resolve
- .then(…).catch สำหรับเอาไว้ดักจับ error ภายใน Promise หรือ กรณี reject ตัวอย่างก็ตามนี้
getData().then(function(result){ console.log('main todo'); console.log("response total %s <", result.length); console.log("main end")}).catch(function(error){ console.log("case error");})
ตัวอย่างเมื่อ เราต้องทำงานกับ function getData(); หรือที่คล้ายๆกรณีนี้ ใน code ใน function main() ก็จะเปลี่ยนไปเป็นอย่างนี้
getData().then(function (result) { getData().then(function (result) { getData().then(function (result) { getData().then(function (result) { console.log('result',result.length); // ผมว่าก็ HELL ดีนะครับ สวยดี }) }) })})
แต่เดี๋ยวก่อนเราสามารถเขียนแบบนี้เพื่อให้ code สวยขึ้นครับ
getData().then(function (result) { return getData();}).then(function(result){ return getData();}).then(function(result){ return getData();}).then(function(result){ return getData();}).then(function(result){ console.log('BAKA result',result.length);})
I love async / await (^ ^)
/* run onnode.js version 7.10 */const axios = require('axios'); //use package "npm i axios -S"var instance = axios.create({baseURL: 'https://unsplash.it/'});/*เมื่อต้องการใช้ await ประกาศ async หน้า function เสมอ */async function getData() { /*await หน้า function promise ที่ต้องการให้รอ*/ return await instance.get('/list');}/*เมื่อต้องการใช้ await ประกาศ async หน้า function เสมอ */async function main() { try { /*await หน้า function promise ที่ต้องการให้รอ*/ let result = await getData(); console.log('BAKA result',result.data.length); } catch (error) { /*ดักจับ error */ console.log('case error',error.code); }}main();
บรรทัด code ลดไปเยอะเลยครับ โดยส่วนตัวผมชอบมาก code ดูอ่านง่ายขึ้นมาดูวิธีใช้กัน
การทำงานของ async await ไม่ใช่สิ่งแปลกใหม่เลย เรามาดูกัน เริ่มจากใส่ async นำหน้า function ที่เราต้องการจะใช้ await โดย await อยู่ภายใต้ function ใหนให้ async นำหน้า function นั้นๆ
async function getData() { //await something}/*or*/function baka(async function(){ //await something})
await นำหน้า function ที่เป็น promise
async function getData() { return await instance.get('/list');}
เอ๊ะ แล้วมันไม่ใช่สิ่งแปลกใหม่ยังไง ??
- จะใช้ await ได้ต่อเมื่อมี function แล้วถ้า เรียกใช้ function getData() นอก function ละทำไง
getData().then(function(result){ //receive result})
เอ๊ะ คุ้นๆๆใหม Promise นั่นเองครับบบบ
ตอนเรียกใช้ใน function main(); ตามตัวอย่าง
async function main() { try { let result = await getData(); console.log('BAKA result',result.data.length); } catch (error) { console.log('case error',error.code); }}
โดย async / await สามารถทำงานร่วมกับ try / catch ได้ เพื่อดักจับ error ครับเป็นไงครับง่ายใช่ใหมครับ แต่อีกข้อหนึ่งที่ควรรู้ไว้ async / await ไม่สามารถทำงาน กับ callback function ได้ ตัวอย่างเช่น
function getData(cb){ cb("my name is callback hell")}async function main() { try { let result = await getData(); console.log('BAKA result',result.data.length); } catch (error) { console.log('case error',error.code); }}
main();
กรณีนี้เข้า catch เเน่นอน แต่เราก็มีวิธีแก้ โดยเรานำ Promise มาช่วยนั่นเอง
function getDataWithCallback(cb){ cb("my name is callback hell")}function getDataWithPromise() { return new Promise(function (resolve, reject) { getDataWithCallback(function(error,result){ if(error) return reject(error); return resolve(result); }) });}async function main() { try { let result = await getDataWithPromise(); console.log('BAKA result',result.data.length); } catch (error) { console.log('case error',error.code); }}
main();
เอ๊ะแล้วงี้ มันก่อคล้ายๆ promise นิมันต่างกันไงอะ ????
ตัวอย่างเมื่อ เราต้องทำงานกับ function getData(); หรือที่คล้ายๆกรณีนี้ ใน code ใน function main() ก็จะเปลี่ยนไปเป็นอย่างนี้
async function main() { try { let result1 = await getData();
let result2 = await getData();
let result3 = await getData();
let result4 = await getData();
let result5 = await getData();
let result6 = await getData(); console.log('BAKA result',result6.data.length); } catch (error) { console.log('case error',error.code); }}
code จะดูสวยขึ้นมาทันทีเลย โดยส่วนตัวผมชอบเเบบนี้มาก รักเลย
ใครจะใช้วิธีใหนก็แล้วแต่ ทุกวิธีที่เขียนมา สุดท้าย ก็ได้ผลลัพธ์ของโปรแกรมเหมือนกันหมด
code ตัวอย่าง https://github.com/bakatest-me/nodejs-callback-promise-async-await
แปะ อ้างอิง https://www.twilio.com/blog/2015/10/asyncawait-the-hero-javascript-deserved.html