φυβλαςのβλογ
บล็อกของ 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)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ

บทความแต่ละเดือน

2024年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2023年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2022年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2021年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2020年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

ค้นบทความเก่ากว่านั้น

ไทย

日本語

中文