ในบทนี้จะพูดถึงเรื่องของการทำงานของโปรแกรมแบบที่เรียกว่า
"ไม่ประสานเวลา" (asynchronous)
นี่เป็นเรื่องที่เข้าใจยากมากเรื่องหนึ่งแต่ก็มีความสำคัญมากในจาวาสคริปต์ โค้ดมักจะดูซับซ้อน
พฤติกรรมของฟังก์ชันหรือคำสั่งต่างๆที่เกี่ยวข้องกับเรื่องนี้มักเข้าใจยากและต่างจากทั่วไป
งานที่ต้องใช้การทำงานแบบไม่ประสานเวลาก็คืองานที่เกี่ยวกับเว็บไซต์และอินเทอร์เน็ต
ในบทนี้จะอธิบายเรื่องโปรแกรมที่ทำงานแบบไม่ประสานเวลา โดยจะเริ่มจากแนะนำ setTimeout
เพื่อเป็นตัวอย่างที่ให้เห็นภาพของโปรแกรมที่ทำงานแบบไม่ประสานเวลา
และจะนำไปสู่เรื่องของการใช้ออบเจ็กต์ Promise และคำสั่ง async และ await ในบทถัดไป
แนวคิดเรื่องการทำงานของโปรแกรมที่ทำงานไม่ประสานเวลา
โดยทั่วไปแล้วโค้ดโปรแกรมส่วนใหญ่จะทำงานตามลำดับจากบนลงล่าง มีลำดับชัดเจนแน่นอน เช่น
alert("ก. ");
alert("ข. ");
alert("ค. ");
แบบนี้โค้ดก็ควรจะทำงานไปตามลำดับ ก. ข. ค. เห็นการทำงานเป็นลำดับที่แน่นอน
แบบนี้เราอ่านโค้ดแล้วก็สามารถรู้ได้ชัดเจนว่าอะไรจะเกิดก่อนหลัง
การทำงานแบบนี้เรียกว่า
"ซิงโครนัส" (synchronous) ถ้าแปลเป็นไทยก็คือ
"ประสานเวลา"
แต่ในการใช้งานบางอย่างนั้นเราอาจไม่จำเป็นต้องรอให้เสร็จอย่างหนึ่งแล้วค่อยทำอีกอย่าง
แต่ว่าอาจทำอะไรหลายๆอย่างไปพร้อมๆกัน บางอย่างเสร็จก่อนหรือหลัง แล้วก็จะมีผลให้โปรแกรมต่อจากนั้นทำงานต่างกันออกไป
งานประเภทนี้มักเกี่ยวกับการรับส่งข้อมูลผ่านทางเว็บไซต์ เพราะมีจังหวะที่ต้องรอโหลดข้อมูล
ระหว่างรอนั้นเราอาจสามารถเอาเวลาไปทำอะไรๆอย่างอื่นได้
ถ้ามัวแต่รอให้โหลดอย่างหนึ่งเสร็จแล้วค่อยมาทำอะไรอย่างอื่นต่อจะเสียเวลา จึงมักจะพยายามทำหลายๆอย่างไปพร้อมๆกัน
การทำอะไรหลายๆอย่างไปพร้อมๆกันในลักษณะแบบนี้เรียกว่า
"อะซิงโครนัส" (asynchronous) หรือแปลเป็นไทยว่า
"ไม่ประสานเวลา" ชื่อเรียกในภาษาอังกฤษแค่เติม a เข้ามาข้างหน้าเพื่อแสดงความหมายในเชิงปฏิเสธ
การทำงานแบบไม่ประสานเวลาทำให้ทำอะไรหลายๆอย่างไปพร้อมๆกันได้ ช่วยประหยัดเวลา ใช้เวลาให้เป็นประโยชน์
แต่ก็ทำให้โปรแกรมมีการเขียนที่ซับซ้อนขึ้น ลำดับการทำงานยากขึ้น ต้องอาศัยความรัดกุมมากขึ้น
จาวาสคริปต์เป็นภาษาที่ใช้งานกับเว็บไซต์เป็นหลัก จึงต้องยุ่งเกี่ยวกับเรื่องการส่งข้อมูลทางอินเทอร์เน็ตมากเป็นธรรมดา
การทำงานแบบไม่ประสานเวลาจึงเป็นเรื่องที่เหมาะจะใช้ และก็มักถูกใช้
เดิมทีในตัวจาวาสคริปต์เองไม่ได้มีฟังก์ชันที่ยุ่งเกี่ยวกับการทำงานแบบไม่ประสานเวลาแบบนี้โดยตรง
แต่ต่อมาได้มีการพัฒนาเทคนิค
AJAX (Asynchronous JavaScript and XML)
ขึ้นมาเพื่อช่วยจัดการเรื่องการรับส่งข้อมูลในหน้าเว็บไซต์ได้โดยไม่ต้องโหลดหน้าใหม่ตลอด
(เกี่ยวกับเรื่องนี้รายละเอียดเพิ่มเติมอาจดูใน
วิกิพีเดียได้)
AJAX นั้นถือว่าเป็นไลบรารีเสริมเพิ่มเติม
ไม่ได้เป็นฟังก์ชันหรือไวยากรณ์ที่ถูกกำหนดเป็นมาตรฐานไว้ในจาวาสคริปต์โดยตรงจริงๆ
แต่ว่าตั้งแต่ใน ES6 ได้มีการเพิ่มคำสั่งสำหรับช่วยในการทำงานแบบไม่ประสานเวลาเข้ามาภายในจาวาสคริปต์โดยตรงโดยใน ES2015
ได้เพิ่มออบเจ็กต์ Promise และต่อมาใน ES2017 ก็ได้เพิ่มไวยากรณ์การใช้คำสั่ง async และ await
ฟังก์ชันที่เกี่ยวข้องกับการทำงานแบบไม่ประสานเวลา เช่น ฟังก์ชัน setTimeout ซึ่งเอาไว้ใช้หน่วงเวลา
หรืออย่างเช่นฟังก์ชันใน AJAX ที่ใช้รับส่งข้อมูลทางอินเทอร์เน็ตอย่าง XMLHttpRequest หรือ fetch
ฟังก์ชันที่เกี่ยวกับการรับส่งข้อมูลทางอินเทอร์เน็ตนั้นจะยังไม่ได้กล่าวถึงในที่นี้
เพราะมีรายละเอียดเพิ่มเติมอีกมาก ซึ่งจะเกินขอบเขตเนื้อหาเบื้องต้นตรงนี้
ในที่นี้เพื่อเป็นตัวอย่างจะเริ่มจากยกฟังก์ชัน setTimeout ซึ่งมักถูกใช้บ่อยในตัวอย่างที่อธิบายการใช้ Promise async
await
การหน่วงเวลาด้วย setTimeout
setTimeout ไม่ใช่ฟังก์ชันที่เป็นมาตรฐานที่อยู่ในจาวาสคริปต์โดยตรง
แต่เป็นฟังก์ชันที่ใช้ได้ทั่วไปในเบราว์เซอร์ และยังใช้ได้ใน node.js ด้วย จึงถือได้ว่าใช้ได้โดยทั่วไปอยู่
ที่ต้องระวังอย่างหนึ่งคือเนื่องจาก setTimeout ไม่ใช่ฟังก์ชันมาตรฐานในจาวาสคริปต์ การทำงานของ setTimeout
ในที่ต่างๆอาจมีความต่างออกไป ไม่ได้เหมือนกันทีเดียว เช่นในเบราว์เซอร์กับใน node.js จะมีความต่างกันอยู่
ในที่นี้จะพูดถึงการทำงานของ setTimeout ภายในเบราว์เซอร์เป็นหลัก หากใครไปใช้ที่อื่นเช่นใน node.js
ก็อาจพบว่ามีการใช้งานที่ต่างกันไปเล็กน้อย
setTimeout เป็นฟังก์ชันที่มีไว้สำหรับสั่งให้โค้ดบางอย่างทำงานหลังจากที่ผ่านเวลาไปช่วงหนึ่งตามที่กำหนด
ชื่อฟังก์ชัน setTimeout นี้ถ้าดูจากชื่อแล้วอาจชวนให้เข้าใจความหมายผิดได้เหมือนกัน
อาจชวนให้คิดว่าใช้กำหนดขอบเขตเวลาจำกัดไม่ให้เกินที่กำหนดหรืออะไรแบบนั้น
แต่จริงๆแล้ว setTimeout เป็นฟังก์ชันสำหรับถ่วงเวลาให้ฟังก์ชันอะไรบางอย่างต้องทำหลังจากผ่านเวลาที่กำหนดไว้
วิธีการใช้คือเขียนในลักษณะแบบนี้
setTimeout(คำสั่งที่ต้องการให้ทำในเวลาที่กำหนด, เวลาเป็นหน่วยมิลลิวินาที)
ฟังก์ชันที่ว่านี้อาจเขียนใส่เป็นสายอักขระหรือเป็นฟังก์ชันก็ได้
ตัวอย่างการใส่คำสั่งเป็นสายอักขระ เช่น สมมุติว่าต้องการให้มีข้อความทัดขึ้นมาใน ๑ วินาทีข้างหน้า (ก็คือ ๑๐๐๐
ไมโครวินาที) อาจเขียนแบบนี้
setTimeout("alert('สวัสดี')",1000)
จากนั้นหลังจากเปิดเบราว์เซอร์ ข้อความจะยังไม่ขึ้นทันที แต่ผ่านไป ๑ วินาทีจึงจะเด้งขึ้นมา
อย่างไรก็ตาม การเขียนแบบนี้คล้ายๆกับการใช้ eval (ซึ่งเขียนถึงใน
บทที่ ๑๗) ซึ่งก่อให้เกิดปัญหาได้ง่าย เป็นโค้ดที่ไม่ปลอดภัย ดังนั้นโดยทั่วไปจึงไม่แนะนำให้ใช้
แม้ว่าจะเขียนแบบนี้ได้ก็ตาม
ดังนั้นโดยทั่วไปจะไม่เขียนแบบนั้น แม้ว่าจะสามารถทำได้ก็ตาม แต่จะใช้อีกวิธี คือใส่เป็นฟังก์ชัน แบบนี้
f = () => {
alert("สวัสดี");
};
setTimeout(f, 1000);
แบบนี้ฟังก์ชัน f จะถูกเรียกใช้หลังผ่านไป ๑ วินาที ขึ้นกรอบข้อความ "สวัสดี"
การเขียนแบบนี้ดูแล้วยังพอเข้าใจได้ไม่ยาก อย่างไรก็ตาม โดยทั่วไปแล้วบ่อยครั้งที่ผู้คนมักจะใส่กรอบสร้างฟังก์ชันลงไปใน
setTimeout โดยตรงเลย
นั่นคือเขียนแบบนี้
setTimeout(() => {
alert("สวัสดี");
}, 1000);
ถ้าหากมองออกก็คงจะเข้าใจขึ้นมาได้ว่าการเขียนแบบนี้ให้ผลเหมือนกับเขียนแบบในตัวอย่างที่แล้ว โค้ดอาจดูเข้าใจยากสักหน่อย
แต่คนที่ใช้จาวาสริปต์เป็นประจำนิยมเขียนแบบนี้กันจริงๆจึงต้องคุ้นเคยไว้
คำสั่งที่ถูกสั่งใน setTimeout จะไม่ถูกทำทันทีแต่จะทำหลังจากผ่านไปตามเวลาที่กำหนด
นั่นหมายความว่าหากเราเขียนแบบนี้
setTimeout(() => {
alert("ก.");
}, 4000);
setTimeout(() => {
alert("ข.");
}, 2000);
alert("ค.");
แบบนี้ลำดับการปรากฏกรอบข้อความก็จะเป็น ค. ข. ก. เพราะว่า ค. อยู่นอก setTimeout จึงทำงานทันที ส่วน ข. ต้องรอ ๒ วินาที
และ ก. รอ ๔ วินาที
เพื่อให้เห็นเวลาชัดเจน อาจใช้ออบเจ็กต์ Date เข้าช่วยบอกระยะเวลา (รายละเอียดเขียนถึงใน
บทที่ ๑๕) ดังนี้
let t0 = new Date;
setTimeout(() => {
alert(new Date - t0); // ได้ 4003
}, 4000);
setTimeout(() => {
alert(new Date - t0); // ได้ 2003
}, 2000);
alert(new Date - t0); // ได้ 1
new Date
จะให้ค่าเวลาในขณะนั้นๆ หากบันทึกเวลาเริ่มต้นไว้แล้วเอามาลบก็จะได้เวลาที่ผ่านไปตั้งแต่เริ่มต้น
หน่วยเป็นมิลลิวินาที
ในตัวอย่างนี้จะเห็นว่าได้ 1, 2003 และ 4003 ตามลำดับ ตัวเลขอาจจะคลาดเคลื่อนไปจากนี้ขึ้นอยู่กับจังหวะที่รัน อาจมีช้าลงไป
แต่จะอยู่ที่ประมาณใกล้เคียงกับ 0, 2000 และ 4000
อีกตัวอย่างหนึ่งเพื่อเปรียบเทียบคือกรณีที่ใช้ setTimeout ซ้อนกัน
let t0 = new Date;
setTimeout(() => {
setTimeout(() => {
let t = "ผ่านไป " + (new Date - t0) + " ms";
alert(t); // ได้ ผ่านไป 5003 ms
}, 3000);
let t = "ผ่านไป " + (new Date - t0) + " ms";
alert(t); // ได้ ผ่านไป 2003 ms
}, 2000);
let t = "ผ่านไป " + (new Date - t0) + " ms";
alert(t); // ได้ ผ่านไป 0 ms
หากเริ่มเข้าใจหลักการทำงานของ setTimeout แล้วคงจะพอเดาผลที่ออกมาได้
ตัวเลขเวลาที่ได้จะออกมาเป็น 0, 2003, 5003 (หรือตัวเลขใกล้เคียงประมาณนี้) ตามลำดับ
setTimeout เป็นแค่ตัวอย่างหนึ่งของฟังก์ชันที่ทำให้เกิดการทำงานแบบไม่ประสานเวลา
แต่สิ่งที่มันทำก็คือแค่การรอเวลาให้ถึงเวลาตามที่กำหนดเฉยๆ ไม่ได้ทำอะไรเป็นพิเศษ
แต่ปกติแล้วการทำงานแบบไม่ประสานเวลานั้นถูกใช้ในงานที่เกี่ยวกับการขนส่งข้อมูลทางอินเทอร์เน็ตเป็นหลัก
เพราะการรับส่งข้อมูลเป็นกระบวนการที่ต้องใช้เวลา แต่ไม่ได้กินทรัพยากร CPU ดังนั้นจึงสามารถทำไปพร้อมๆกันหลายงานได้
และควรจะทำเพื่อประหยัดเวลา
เพราะถ้ารอให้โหลดข้อมูลเสร็จไปทีละขั้นแล้วค่อยให้ CPU ทำงานตาม แบบนั้นจะมีช่วงที่ CPU หยุดทำงาน
ทั้งที่จริงๆมันควรไปใช้ทำอย่างอื่นได้
setTimeout เป็นแค่การถ่วงเวลาเฉยๆ แต่ฟังก์ชันใน AJAX ที่ใช้ดึงข้อมูลจากเว็บ เช่น XMLHttpRequest หรือ fetch นั้น
พวกนี้จะใช้เวลาเพื่อทำการขนส่งข้อมูล
ในที่นี้ใช้ setTimeout เพื่อเป็นตัวแทนของงานที่ต้องใช้เวลาในการรอ
ให้สมมุติว่าเวลาที่รอนี้เป็นเวลาที่ใช้ในการโหลดข้อมูลหรือทำงานอะไรบางอย่างที่ไม่ได้กิน CPU
ว่าด้วยเรื่องของเธรด
การทำงานของฟังก์ชัน setTimeout นี้ทำให้เกิดการทำงานแบบไม่ประสานเวลา คือโค้ดถูกทำงานแยกกัน ไม่ได้เกี่ยวข้องกัน
ที่จริงพอแปล "อะซิงโครนัส" ว่า "ไม่ประสานเวลา" แบบนี้แล้วอาจทำให้เข้าใจยากหรือชวนให้เข้าใจผิด
แต่ความหมายที่ชื่อนี้ต้องการจะสื่อก็คือ มันทำงานแบบแยกเป็นอิสระจากกัน ไม่ได้ยุ่งเกี่ยวกัน
เพื่อให้เข้าใจว่าการแยกการทำงานกันตรงนี้เกิดขึ้นได้อย่างไร
ก่อนอื่นอาจต้องทำความเข้าใจเกี่ยวกับเรื่องของสิ่งที่เรียกว่า
"เธรด" (thread)
หากเปรียบ CPU คอมพิวเตอร์เป็นเหมือนโรงงานโรงหนึ่ง เธรดก็เปรียบเสมือนคนงานคนหนึ่งที่คอยรับงานไปทำ
(รายละเอียดเกี่ยวกับเรื่องเธรดอาจอ่านเพิ่มเติมได้เช่นใน
https://sites.google.com/site/rabbpdibatikarkhxmphiwtexr/-5--thread หรือวิกิพีเดีย
https://th.wikipedia.org/wiki/เทร็ด
โปรแกรมหนึ่งอาจมีหลายเธรดช่วยกันทำงาน
แต่สำหรับภาษาจาวาสคริปต์ซึ่งเป็นภาษาที่ออกแบบมาเพื่อใช้งานในเบราว์เซอร์ตั้งแต่แรกนั้น
ปกติจะใช้เธรดเดียวในการรันตลอดโปรแกรม เนื่องจากเหตุผลด้านความปลอดภัย
ดังนั้นปกติเวลาจาวาสคริปต์ทำงานแบบประสานเวลาจะใช้เธรดเดียวในการรันโค้ดทั้งหมด
เหมือนการสั่งให้คนงานคนหนึ่งเข้าไปในห้องทำงานแล้วทำงานที่สั่งเป็นขั้นเป็นตอน ไล่ตามลำดับต่อเนื่องไปเรื่อยๆ
แต่ว่าในงานบางอย่างการใช้เธรดเดียวตลอดนั้นจะไม่มีประสิทธิภาพ
จึงต้องอาศัยการทำงานแบบไม่ประสานเวลาเพื่อกระจายงานไปยังเธรดอื่น
เมื่อมีการใช้ฟังก์ชันที่ทำให้เกิดการทำงานแบบไม่ประสานเวลาขึ้น จะเกิดการแยกงานไปทำในอีกเธรด
เปรียบเสมือนคนงานคนนึงทำงานหลักอยู่ พอถึงจุดหนึ่งก็สั่งงานให้คนงานคนอื่นแยกไปทำงานอีกอย่างที่ไม่ได้เกี่ยวข้องกัน
ส่วนตัวเองก็ทำงานหลักของตัวเองต่อไป
สมมุติเหตุการณ์ว่ากำลังทำอาหารอยู่ หากเขียนเป็นจาวาสคริปต์เปรียบเทียบก็อาจได้ประมาณนี้
หั่น(กะหล่ำปลี);
setTimeout(() => {
ไปหยิบ(ไข่ไก่);
}, 10000);
สับ(เนื้อหมู);
ในนี้คือคนงานหลักหั่นกะหล่ำปลีเสร็จก็เริ่มสั่งให้คนงานอีกคนไปหยิบไข่ไก่มาใส่
ระหว่างนั้นตัวเองก็กลับมาสับเนื้อหมูต่อไปด้วย เสร็จแล้วผ่านไป ๑๐ วินาที คนที่ไปหยิบไข่ก็กลับมาเอาไข่มาใส่ให้
ในที่นี้งานหยิบไข่แทนงานที่แค่ต้องใช้เวลารอ แต่ไม่ได้กินแรง ไม่ต้องใช้อุปกรณ์หรือทรัพยากรอะไรในโรงงาน
เปรียบเสมือนกับงานเช่นการส่งถ่ายข้อมูลทางอินเทอร์เน็ต ซึ่งไม่กินทรัพยากร CPU
และสามารถทำพร้อมๆกันไปขณะที่ทำงานอื่นได้
ในความเป็นจริงแล้วกรณีนี้เปรียบเสมือนกับว่าแม้เราจะมีคนงานหลายคนที่พร้อมจะทำงาน แต่ว่าห้องทำงานก็มีพื้นที่จำกัด
อยู่ได้แค่คนเดียว ขณะที่คนหนึ่งกำลังหั่นผักอยู่ ถึงจะมีคนอื่นที่ว่างอยู่ก็ไม่สามารถมาใช้ห้องเพื่อทำอะไรได้
แต่สิ่งที่จะทำได้ก็คือเช่นเดินไปหยิบของมาส่งเพิ่ม เป็นต้น
ปัญหาเรื่องลำดับการทำงานของโปรแกรมที่มีการวิ่งในแบบไม่ประสานเวลา
งานที่ถูกสั่งให้ทำงานแบบไม่ประสานเวลาจะไปถูกทำในอีกเธรดหนึ่ง ซึ่งแยกจากเธรดที่กำลังทำงานรันโปรแกรมหลักอยู่
ถ้าหากงานนั้นเป็นอะไรที่ไม่เกี่ยวข้องกับโปรแกรมหลักเลย เช่นถ้าโหลดข้อมูลเสร็จก็ไปเซฟเก็บไว้
ไม่ได้เอาข้อมูลนั้นมาใช้ในโปรแกรม แบบนั้นก็ยังจะไม่มีปัญหาอะไร
แต่ถ้าเป็นกรณีที่โหลดข้อมูลมาแล้วจะใช้ทำอะไรต่อในโปรแกรม แบบนั้นจะยุ่งยากขึ้นมา
เพราะข้อมูลต้องมาก่อนจึงจะทำงานต่อได้
แต่ปกติแล้วงานที่ทำงานอยู่ในเธรดหลักจะถูกทำต่อทันทีโดยไม่รอให้งานที่ถูกสั่งให้อีกเธรดไปทำ
ยกตัวอย่าง เช่น
let krachao = "กระเช้าใบหนึ่ง";
setTimeout(() => {
krachao += "ใส่ผลไม้";
}, 3000);
krachao += "วางบนโต๊ะ";
alert(krachao); // ได้ กระเช้าใบหนึ่งวางบนโต๊ะ
ผลที่ได้ก็คือกรอบข้อความขึ้นมาว่า
"กระเช้าใบหนึ่งวางบนโต๊ะ"
เพราะส่วน
krachao += "ใส่ผลไม้"
ซึ่งอยู่ใน setTimeout นั้นต้องรอ ๓ นาที ในขณะที่ส่วน
krachao += "วางบนโต๊ะ";
เป็นต้นไปนั้นทำทันที
ดังนั้นตอนที่แสดงผลจึงยังไม่ได้ถูกบวกเข้าไป
พอเป็นแบบนี้งั้นลองคิดต่อว่าแล้วถ้ากำหนดเวลารอใน setTimeout เป็น 0 จะเป็นอย่างไร จะเกิดการทันทีหรือเปล่า เช่น
let krachao = "กระเช้าใบหนึ่ง";
setTimeout(() => {
krachao += "ใส่ผลไม้";
}, 0);
krachao += "วางบนโต๊ะ";
alert(krachao); // ได้ กระเช้าใบหนึ่งวางบนโต๊ะ
คำตอบคือ... ยังไงคำสั่งใน setTimeout ก็ถูกทำทีหลังอยู่ดี กระเช้าจึงยังคงไม่มีผลไม้
ที่เป็นแบบนี้ก็เพราะว่าคำสั่งที่สั่งให้ทำแบบไม่ประสานเวลาจะถูกสั่งให้แยกไปทำในอีกเธรด (ในที่นี้เรียกว่าเธรดลูก)
ในขณะที่คำสั่งอื่นจะยังทำงานอยู่ในเธรดเดิม (ในที่นี้เรียกว่าเธรดหลัก)
ปกติคำสั่งในเธรดหลักจะถูกให้ความสำคัญมากกว่า ยังไงโปรแกรมก็จะทำคำสั่งในเธรดหลักให้เสร็จก่อน
เลขเวลาที่กำหนดให้รอใน setTimeout เป็นแค่ตัวกำหนดว่าจะให้ทำคำสั่งในนี้หลังจากรอไปนานแค่ไหนเท่านั้น
ไม่ได้หมายความว่าพอถึงเวลาแล้วคำสั่งนั้นจะทำทันที
เพราะการรอเวลานั้นไม่ใช้ CPU ก็จริง แต่คำสั่งที่ให้ทำหลังจากรอ ในที่นี้คือ
krachao += "ใส่ผลไม้";
ต้องใช้
CPU ดังนั้นถ้าตอนนั้นเธรดหลักยังมีงานต้องทำอยู่ CPU ก็ยังไม่ว่างที่จะมาทำ จึงต้องรอทำทีหลังอยู่ดี
ต่อให้มีอะไรบางอย่างที่ถ่วงเวลาการทำงานในเธรดหลักไว้ เช่นใช้วังวน while
ให้วนซ้ำไปเปล่าๆรอจนกว่าจะถึงเวลาที่กำหนด
let t0 = new Date
let krachao = "กระเช้าใบหนึ่ง";
setTimeout(() => {
krachao += "ใส่ผลไม้";
}, 1000);
while (new Date - t0 < 2000) { }
krachao += "วางบนโต๊ะ";
alert(krachao); // ได้ กระเช้าใบหนึ่งวางบนโต๊ะ
จะเห็นว่าผลลัพธ์ก็ไม่ต่างไปจากเดิม ในที่นี้
krachao += "วางบนโต๊ะ";
ทำงานหลังจากรอวน while ไป ๒ วินาที
ในขณะที่ตรง setTimeout กำหนดให้รอแค่ ๑ วินาที
แต่ว่าแม้จะรอคนถึงเวลาแล้วมันก็ยังต้องรอให้คำสั่งในเธรดหลักเสร็จก่อนอยู่ดี
เพื่อให้เห็นภาพชัดขึ้น คราวนี้ลองให้ alert ภายใน setTimeout และใส่เวลาบอกไปด้วย แบบนี้
let t0 = new Date
let krachao = "กระเช้าใบหนึ่ง";
setTimeout(() => {
krachao += "ใส่ผลไม้";
alert("เวลาผ่านไป " + (new Date - t0)/1000 + " วินาที ~ " + krachao);
}, 1000);
while (new Date - t0 < 2000) { }
krachao += "วางบนโต๊ะ";
ผลที่ได้ก็คือ
เวลาผ่านไป 2.008 วินาที ~ กระเช้าใบหนึ่งวางบนโต๊ะใส่ผลไม้
นั่นหมายความว่าคำสั่งใน
setTimeout ยังไงก็ทำงานหลังจากที่ผ่านไป ๒ วินาที คือรอให้วน while แล้วตามด้วยทำ
krachao += "วางบนโต๊ะ";
ไปจนเสร็จก่อน
แต่ก็ไม่ได้หมายความว่ามันต้องรอให้ผ่านไป ๒ วินาทีก่อนจึงเริ่มนับเริ่มต้นเวลารอ ๑ วินาที แบบนั้นจะรวมกันเป็น ๓ วินาที
แต่การรอเริ่มต้นตั้งแต่เริ่มสั่ง setTimeout แล้ว
เพียงแต่ต้องรอต่อไปอีกจนเธรดหลักทำงานเสร็จจึงจะได้ทำคำสั่งในนั้นเท่านั้น
ไม่ใช่แค่ setTimeout แต่คำสั่งโหลดข้อมูลต่างๆซึ่งทำงานแบบไม่ประสานเวลาก็จะเป็นแบบนี้เหมือนกัน
คือเมื่อสั่งให้อีกเธรดไปรอโหลดเอาข้อมูลมา ก็ไม่สามารถเอาข้อมูลมาใช้ในโค้ดส่วนที่รันในเธรดหลักอยู่ดี
ในจาวาสคริปต์มีเธรดหลักอยู่ตัวเดียว
งานในเธรดหลักจะถูกรันต่อเนื่องจากต้นจนจบโดยระหว่างนั้นจะไม่มีการไปทำงานในเธรดลูก
ดังนั้นการที่จะให้คำสั่งในเธรดหลักรอคำสั่งในเธรดลูกแล้วทำงานให้ถูกตามลำดับขั้นตอนที่ควรเป็นนั้น
โดยปกติจึงเป็นเรื่องที่ทำไม่ได้
แต่งานที่ถูกสั่งในเธรดลูกสามารถเอามาทำต่อในเธรดลูกอีกอันหนึ่งได้ เช่น
let takra = "ตะกร้าใบหนึ่ง";
setTimeout(() => {
takra += "ใส่ผัก";
}, 1000);
setTimeout(() => {
takra += "วางบนตู้";
alert(takra); // ได้ ตะกร้าใบหนึ่งใส่ผักวางบนตู้
}, 2000);
แบบนี้คำสั่งใน setTimeout ตัวหลังจะทำต่อจากตัวแรกเนื่องจากตั้งเวลาไว้ช้ากว่า และจะได้กรอบข้อความขึ้นว่า
ตะกร้าใบหนึ่งใส่ผักวางบนตู้
แต่การเขียนแบบนี้มีปัญหาตรงที่ว่าเธรดลูก ๒ ตัวทำงานแยกกันโดยสมบูรณ์
จึงไม่มีอะไรรับประกันว่ามันจะทำงานไปตามลำดับที่วางไว้ อย่างในตัวอย่างนี้ ถ้าสลับลำดับกันผลที่ได้ก็จะต่างกันออกไป
ในตัวอย่างนี้ใช้ setTimeout จึงกำหนดเวลาค่อนข้างตายตัว
แต่ในการใช้งานจริงเช่นเมื่อใช้กับงานส่งข้อมูลทางอินเทอร์เน็ตนั้นเวลาจะไม่แน่นอน
ใน ES6 การใช้ Promise กับเมธอด .then .catch หรือ async await จะช่วยควบคุมจัดลำดับการทำงานของโปรแกรมได้
ในบทต่อไปจะเขียนถึงเรื่องของ Promise ส่วนเนื้อหาข้างล่างตั้งแต่ต่อจากตรงนี้ไปเป็นส่วนเสริมเกี่ยวกับฟังก์ชัน setTimeout ซึ่งอาจไม่ใช่ส่วนสำคัญที่จะได้ใช้ต่อในบทต่อไปที่อธิบายเรื่อง Promise จึงอาจข้ามไปได้
การใส่อาร์กิวเมนต์ให้ setTimeout
จากตัวอย่างที่ผ่านมาฟังก์ชันที่กำหนดให้ทำใน setTimeout เป็นฟังก์ชันที่ไม่ต้องรับอาร์กิวเมนต์เลยสักตัวเดียว
แต่ถ้าหากต้องการใช้ฟังก์ชันที่มีอาร์กิวเมนต์ ก็สามารถใส่อาร์กิวเมนต์ให้ได้ โดยใส่ในฟังก์ชัน setTimeout เป็นตัวที่ ๓
ถัดจากระยะเวลารอ
setTimeout(ฟังก์ชัน, ระยะเวลารอ, อาร์กิวเมนต์ตัวที่ ๑, อาร์กิวเมนต์ตัวที่ ๒, ...)
ตัวอย่าง
f = (a,b)=>{
alert(a + " + " +b+ " = "+(a+b));
}
setTimeout(f,1000,2.5,3.5);
// ๑ วินาทีผ่านไปขึ้นหน้าต่างว่า 2.5 + 3.5 = 6
มีข้อควรระวังคือ บางคนอาจเผลอเขียนผิดเป็นแบบนี้
setTimeout(f(2.5,3.5),1000);
ถ้าทำแบบนี้ผลที่ได้ก็คือฟังก์ชันนี้จะทำงานทันที และจะไม่มีค่าใดๆถูกส่งให้ setTimeout
ดังนั้นต้องระวังว่าสิ่งที่ส่งให้ setTimeout คือตัวฟังก์ชัน ไม่ใช่ผลที่ได้จากฟังก์ชัน
การเรียกใช้ฟังก์ชันจะได้ผลคืนกลับจากฟังก์ชันนั้นมาแทน ไม่ใช่ตัวฟังก์ชันเอง
การยกเลิก setTimeout
คำสั่งที่สั่งด้วย setTimeout ไปแล้ว ถ้ายังไม่ถูกทำก็สามารถยกเลิกก่อนได้ด้วยฟังก์ชัน clearTimeout
เวลาที่ใช้ฟังก์ชัน setTimeout จะมีการคืนหมายเลขของคำสั่งมาให้ เพื่อนำมาใช้อ้างอิงได้ตอนหลัง
การใช้ clearTimeout นั้นจะต้องระบุหมายเลขของคำสั่งที่ต้องการยกเลิก ดังนั้นตอนสั่ง setTimeout
ต้องเก็บค่าตัวเลขนั้นไว้ด้วย
ตัวอย่างการใช้
let setao = setTimeout(()=>{
alert("คำสั่งนี้ถูกยกเลิกก่อนได้ทำงาน")
},1000);
clearTimeout(setao);
ในที่นี้ setTimeout ถูกยกเลิกก่อนได้ใช้งานจริงๆจึงไม่เกิดอะไรขึ้น
หมายเหตุ: กรณีที่ใช้ node.js นั้น setTimeout จะคืนเป็นตัวออบเจ็กต์ Timeout แต่ก็นำมาใช้ใส่ใน clearTimeout
ได้ในลักษณะเดียวกัน
การใช้ setInterval
มีอีกฟังก์ชันที่คล้ายๆกับ setTimeout คือ setInterval
setInterval เป็นฟังก์ชันสำหรับสั่งให้เกิดการทำคำสั่งต่อเนื่องไปเรื่อยๆโดยเว้นช่วงตามช่วงเวลาที่กำหนด ต่างจาก
setTimeout ที่จะแค่ทำครั้งเดียวหลังเวลาผ่านไปตามที่กำหนด
ถ้าไม่สั่งให้หยุดมันก็จะทำต่อไปเรื่อยๆ การจะหยุดได้นั้นอาจใช้ฟังก์ชัน clearInterval
ตัวอย่างเช่น สร้างแถวลำดับขึ้นมาอันหนึ่ง แล้วให้มีการใส่ค่าลงไปทุกๆครึ่งวินาที แต่ถ้าถึง ๕
วินาทีแล้วก็ให้แสดงค่านั้นออกมาแล้วก็เลิกทำ
let arr = [];
let t0 = new Date
let sitv = setInterval(()=>{
let t = new Date - t0;
if(t<5000) arr.push(t);
else {
alert(arr);
clearInterval(sitv);
}
},500)
ผลที่ได้คือจะมีกรอบข้อความขึ้นมาเป็นแถวลำดับ
503,1005,1509,2013,2517,3022,3524,4025,4527