φυβλαςのβλογ
บล็อกของ phyblas



javascript เบื้องต้น บทที่ ๔๒: การทำงานแบบไม่ประสานเวลา
เขียนเมื่อ 2020/05/03 01:08
แก้ไขล่าสุด 2024/03/28 23:03


ในบทนี้จะพูดถึงเรื่องของการทำงานของโปรแกรมแบบที่เรียกว่า "ไม่ประสานเวลา" (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








-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> เขียนโปรแกรม >> javascript

ไม่อนุญาตให้นำเนื้อหาของบทความไปลงที่อื่นโดยไม่ได้ขออนุญาตโดยเด็ดขาด หากต้องการนำบางส่วนไปลงสามารถทำได้โดยต้องไม่ใช่การก๊อปแปะแต่ให้เปลี่ยนคำพูดเป็นของตัวเอง หรือไม่ก็เขียนในลักษณะการยกข้อความอ้างอิง และไม่ว่ากรณีไหนก็ตาม ต้องให้เครดิตพร้อมใส่ลิงก์ของทุกบทความที่มีการใช้เนื้อหาเสมอ

สารบัญ

รวมคำแปลวลีเด็ดจากญี่ปุ่น
มอดูลต่างๆ
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
การเรียนรู้ของเครื่อง
-- โครงข่าย
     ประสาทเทียม
ภาษา javascript
ภาษา mongol
ภาษาศาสตร์
maya
ความน่าจะเป็น
บันทึกในญี่ปุ่น
บันทึกในจีน
-- บันทึกในปักกิ่ง
-- บันทึกในฮ่องกง
-- บันทึกในมาเก๊า
บันทึกในไต้หวัน
บันทึกในยุโรปเหนือ
บันทึกในประเทศอื่นๆ
qiita
บทความอื่นๆ

บทความแบ่งตามหมวด



ติดตามอัปเดตของบล็อกได้ที่แฟนเพจ

  ค้นหาบทความ

  บทความแนะนำ

ตัวอักษรกรีกและเปรียบเทียบการใช้งานในภาษากรีกโบราณและกรีกสมัยใหม่
ที่มาของอักษรไทยและความเกี่ยวพันกับอักษรอื่นๆในตระกูลอักษรพราหมี
การสร้างแบบจำลองสามมิติเป็นไฟล์ .obj วิธีการอย่างง่ายที่ไม่ว่าใครก็ลองทำได้ทันที
รวมรายชื่อนักร้องเพลงกวางตุ้ง
ภาษาจีนแบ่งเป็นสำเนียงอะไรบ้าง มีความแตกต่างกันมากแค่ไหน
ทำความเข้าใจระบอบประชาธิปไตยจากประวัติศาสตร์ความเป็นมา
เรียนรู้วิธีการใช้ regular expression (regex)
การใช้ unix shell เบื้องต้น ใน linux และ mac
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ทำความรู้จักกับปัญญาประดิษฐ์และการเรียนรู้ของเครื่อง
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ

ไทย

日本語

中文