เราอาจตัดสินใจที่จะรันฟังก์ชันไม่ใช่ตอนนี้ แต่ในเวลาต่อมา นั่นเรียกว่า "การกำหนดเวลาการโทร"
มีสองวิธีสำหรับมัน:
setTimeout
ช่วยให้เราสามารถเรียกใช้ฟังก์ชันได้หนึ่งครั้งหลังจากช่วงเวลาหนึ่ง
setInterval
ช่วยให้เราสามารถเรียกใช้ฟังก์ชันซ้ำๆ โดยเริ่มต้นหลังจากช่วงเวลาหนึ่ง จากนั้นจึงทำซ้ำอย่างต่อเนื่องในช่วงเวลานั้น
วิธีการเหล่านี้ไม่ได้เป็นส่วนหนึ่งของข้อกำหนด JavaScript แต่สภาพแวดล้อมส่วนใหญ่มีตัวกำหนดเวลาภายในและจัดเตรียมวิธีการเหล่านี้ โดยเฉพาะอย่างยิ่ง รองรับในทุกเบราว์เซอร์และ Node.js
ไวยากรณ์:
ให้ timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
พารามิเตอร์:
func|code
ฟังก์ชันหรือสตริงของโค้ดที่จะดำเนินการ โดยปกติแล้ว นั่นคือฟังก์ชัน ด้วยเหตุผลทางประวัติศาสตร์ คุณสามารถส่งสตริงโค้ดได้ แต่ไม่แนะนำ
delay
ความล่าช้าก่อนรัน มีหน่วยเป็นมิลลิวินาที (1,000 ms = 1 วินาที) โดยค่าเริ่มต้นคือ 0
arg1
, arg2
…
อาร์กิวเมนต์สำหรับฟังก์ชัน
ตัวอย่างเช่น โค้ดนี้จะเรียก sayHi()
หลังจากผ่านไปหนึ่งวินาที:
ฟังก์ชั่น sayHi() { alert('สวัสดี'); - setTimeout (สวัสดี 1,000);
พร้อมข้อโต้แย้ง:
ฟังก์ชั่น sayHi (วลีใคร) { การแจ้งเตือน ( วลี + ', ' + ใคร ); - setTimeout(sayHi, 1,000, "สวัสดี", "จอห์น"); //สวัสดีจอห์น
หากอาร์กิวเมนต์แรกเป็นสตริง JavaScript จะสร้างฟังก์ชันจากอาร์กิวเมนต์นั้น
ดังนั้นสิ่งนี้ก็จะได้ผลเช่นกัน:
setTimeout("alert('สวัสดี')", 1000);
แต่ไม่แนะนำให้ใช้สตริง ให้ใช้ฟังก์ชันลูกศรแทน เช่นนี้
setTimeout(() => การแจ้งเตือน ('สวัสดี'), 1,000);
ส่งผ่านฟังก์ชัน แต่อย่าเรียกใช้
นักพัฒนามือใหม่บางครั้งทำผิดพลาดโดยการเพิ่มวงเล็บ ()
หลังฟังก์ชัน:
// ผิด! setTimeout(sayHi(), 1,000);
ไม่ได้ผล เนื่องจาก setTimeout
คาดว่าจะมีการอ้างอิงถึงฟังก์ชัน และที่นี่ sayHi()
รันฟังก์ชัน และ ผลลัพธ์ของการดำเนินการ จะถูกส่งไปที่ setTimeout
ในกรณีของเรา ผลลัพธ์ของ sayHi()
undefined
(ฟังก์ชันไม่ส่งคืนสิ่งใด) ดังนั้นจึงไม่มีการกำหนดเวลาใดๆ
การเรียก setTimeout
ส่งคืน "ตัวระบุตัวจับเวลา" timerId
ที่เราสามารถใช้เพื่อยกเลิกการดำเนินการ
ไวยากรณ์ที่จะยกเลิก:
ให้ timerId = setTimeout(...); clearTimeout(รหัสตัวจับเวลา);
ในโค้ดด้านล่าง เรากำหนดเวลาฟังก์ชันแล้วยกเลิก (เปลี่ยนใจ) เป็นผลให้ไม่มีอะไรเกิดขึ้น:
ให้ timerId = setTimeout(() => alert("ไม่เคยเกิดขึ้น"), 1,000); การแจ้งเตือน (รหัสตัวจับเวลา); // ตัวระบุตัวจับเวลา clearTimeout(รหัสตัวจับเวลา); การแจ้งเตือน (รหัสตัวจับเวลา); // ตัวระบุเดียวกัน (ไม่เป็นโมฆะหลังจากการยกเลิก)
ดังที่เราเห็นจากเอาต์พุต alert
ในเบราว์เซอร์ ตัวระบุตัวจับเวลาจะเป็นตัวเลข ในสภาพแวดล้อมอื่น นี่อาจเป็นอย่างอื่นก็ได้ ตัวอย่างเช่น Node.js ส่งคืนวัตถุตัวจับเวลาพร้อมวิธีการเพิ่มเติม
ขอย้ำอีกครั้งว่าไม่มีข้อกำหนดสากลสำหรับวิธีการเหล่านี้ ดังนั้นจึงเป็นเรื่องปกติ
สำหรับเบราว์เซอร์ ตัวจับเวลาจะอธิบายไว้ในส่วนตัวจับเวลาของ HTML Living Standard
เมธอด setInterval
มีไวยากรณ์เหมือนกับ setTimeout
:
ให้ timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
ข้อโต้แย้งทั้งหมดมีความหมายเหมือนกัน แต่ต่างจาก setTimeout
ที่รันฟังก์ชันนี้ไม่ใช่แค่ครั้งเดียว แต่เป็นประจำหลังจากช่วงเวลาที่กำหนด
หากต้องการหยุดการโทรเพิ่มเติม เราควรเรียก clearInterval(timerId)
ตัวอย่างต่อไปนี้จะแสดงข้อความทุกๆ 2 วินาที หลังจากผ่านไป 5 วินาที เอาต์พุตจะหยุดทำงาน:
// ทำซ้ำด้วยช่วงเวลา 2 วินาที ให้ timerId = setInterval(() => alert('tick'), 2000); // หลังจากผ่านไป 5 วินาทีให้หยุด setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);
เวลาดำเนินต่อไปในขณะที่แสดง alert
ในเบราว์เซอร์ส่วนใหญ่ รวมถึง Chrome และ Firefox ตัวจับเวลาภายในจะยังคง "ติ๊ก" ต่อไปในขณะที่แสดง alert/confirm/prompt
ดังนั้น หากคุณเรียกใช้โค้ดด้านบนและไม่ปิดหน้าต่าง alert
เป็นระยะเวลาหนึ่ง alert
ครั้งถัดไปจะแสดงทันทีที่คุณดำเนินการ ช่วงเวลาจริงระหว่างการแจ้งเตือนจะสั้นกว่า 2 วินาที
มีสองวิธีในการทำงานบางอย่างเป็นประจำ
หนึ่งคือ setInterval
อีกอันหนึ่งคือ setTimeout
ที่ซ้อนกันเช่นนี้
/** แทน: ให้ timerId = setInterval(() => alert('tick'), 2000); - ให้ timerId = setTimeout(ติ๊กฟังก์ชัน() { alert('ติ๊ก'); timerId = setTimeout (ติ๊ก, 2000); - }, 2000);
setTimeout
ด้านบนจะกำหนดเวลาการโทรครั้งต่อไปเมื่อสิ้นสุดการโทรปัจจุบัน (*)
setTimeout
ที่ซ้อนกันเป็นวิธีการที่ยืดหยุ่นมากกว่า setInterval
วิธีนี้อาจทำให้กำหนดเวลาการโทรครั้งถัดไปแตกต่างออกไป ขึ้นอยู่กับผลลัพธ์ของการโทรครั้งปัจจุบัน
เช่น เราต้องเขียน Service ที่ส่ง Request ไปยัง Server ทุกๆ 5 วินาที เพื่อขอข้อมูล แต่ในกรณีที่ Server Overload ควรเพิ่ม Interval เป็น 10, 20, 40 วินาที…
นี่คือรหัสเทียม:
ให้ล่าช้า = 5,000; ให้ timerId = setTimeout (คำขอฟังก์ชัน () { ...ส่งคำขอ... ถ้า (คำขอล้มเหลวเนื่องจากเซิร์ฟเวอร์โอเวอร์โหลด) { // เพิ่มช่วงเวลาในการรันครั้งถัดไป ล่าช้า *= 2; - timerId = setTimeout (คำขอ, ล่าช้า); }, ล่าช้า);
และหากฟังก์ชันที่เรากำลังจัดกำหนดการต้องใช้ CPU มาก เราก็สามารถวัดเวลาที่ใช้ในการดำเนินการและวางแผนการโทรครั้งต่อไปไม่ช้าก็เร็ว
setTimeout
ที่ซ้อนกันช่วยให้ตั้งค่าการหน่วงเวลาระหว่างการประมวลผลได้แม่นยำกว่า setInterval
ลองเปรียบเทียบสองส่วนของโค้ด อันแรกใช้ setInterval
:
ให้ฉัน = 1; setInterval (ฟังก์ชัน () { ฟังก์ชั่น(i++); }, 100);
อันที่สองใช้ setTimeout
ที่ซ้อนกัน :
ให้ฉัน = 1; setTimeout (ฟังก์ชันรัน () { ฟังก์ชั่น(i++); setTimeout (รัน, 100); }, 100);
สำหรับ setInterval
ตัวกำหนดเวลาภายในจะทำงาน func(i++)
ทุก ๆ 100ms:
คุณสังเกตเห็นไหม?
ความล่าช้าที่แท้จริงระหว่างการเรียก func
สำหรับ setInterval
น้อยกว่าในโค้ด!
นั่นเป็นเรื่องปกติ เพราะเวลาที่ดำเนินการของ func
จะ "สิ้นเปลือง" ส่วนหนึ่งของช่วงเวลา
เป็นไปได้ว่าการดำเนินการของ func
นานกว่าที่เราคาดไว้และใช้เวลานานกว่า 100 มิลลิวินาที
ในกรณีนี้ เครื่องยนต์รอให้ func
เสร็จสิ้น จากนั้นตรวจสอบตัวกำหนดตารางเวลา และหากหมดเวลาให้รันอีกครั้ง ทันที
ในกรณี Edge หากฟังก์ชันดำเนินการนานกว่า delay
ms เสมอ การโทรจะเกิดขึ้นโดยไม่หยุดเลย
และนี่คือรูปภาพของ setTimeout
ที่ซ้อนกัน :
setTimeout
ที่ซ้อนกันรับประกันความล่าช้าคงที่ (ที่นี่ 100ms)
นั่นเป็นเพราะมีการวางแผนการโทรใหม่เมื่อสิ้นสุดการโทรครั้งก่อน
การรวบรวมขยะและการโทรกลับ setInterval/setTimeout
เมื่อฟังก์ชันถูกส่งผ่านใน setInterval/setTimeout
การอ้างอิงภายในจะถูกสร้างขึ้นและบันทึกไว้ในตัวกำหนดตารางเวลา ป้องกันไม่ให้ฟังก์ชันถูกรวบรวมขยะ แม้ว่าจะไม่มีการอ้างอิงอื่นใดก็ตาม
// ฟังก์ชั่นจะยังคงอยู่ในหน่วยความจำจนกว่าตัวกำหนดตารางเวลาจะเรียกใช้ setTimeout(ฟังก์ชั่น() {...}, 100);
สำหรับ setInterval
ฟังก์ชันจะยังคงอยู่ในหน่วยความจำจนกว่าจะมีการเรียก clearInterval
มีผลข้างเคียง. ฟังก์ชันอ้างอิงสภาพแวดล้อมคำศัพท์ภายนอก ดังนั้นในขณะที่มันมีชีวิตอยู่ ตัวแปรภายนอกก็มีชีวิตอยู่เช่นกัน อาจใช้หน่วยความจำมากกว่าตัวฟังก์ชันมาก ดังนั้นเมื่อเราไม่ต้องการฟังก์ชันที่กำหนดเวลาไว้อีกต่อไป ก็ควรยกเลิกดีกว่า แม้ว่าจะมีเพียงเล็กน้อยก็ตาม
มีกรณีการใช้งานพิเศษ: setTimeout(func, 0)
หรือเพียงแค่ setTimeout(func)
นี่เป็นการจัดกำหนดการการดำเนินการ func
โดยเร็วที่สุด แต่ตัวกำหนดเวลาจะเรียกใช้หลังจากสคริปต์ที่ดำเนินการอยู่ในปัจจุบันเสร็จสมบูรณ์เท่านั้น
ดังนั้นฟังก์ชันจึงถูกกำหนดให้รัน "ทันทีหลังจาก" สคริปต์ปัจจุบัน
ตัวอย่างเช่น ผลลัพธ์นี้จะออกมาเป็น "Hello" จากนั้นจะเป็น "World" ทันที:
setTimeout(() => การแจ้งเตือน ("โลก")); alert("สวัสดี");
บรรทัดแรก “วางการโทรลงในปฏิทินหลังจาก 0ms” แต่ตัวกำหนดเวลาจะ "ตรวจสอบปฏิทิน" หลังจากที่สคริปต์ปัจจุบันเสร็จสมบูรณ์แล้วเท่านั้น ดังนั้น "Hello"
จึงเป็นอันดับแรก และ "World"
– หลังจากนั้น
นอกจากนี้ยังมีกรณีการใช้งานที่เกี่ยวข้องกับเบราว์เซอร์ขั้นสูงของการหมดเวลาหน่วงเป็นศูนย์ ซึ่งเราจะกล่าวถึงในบทที่วนรอบเหตุการณ์: ไมโครทาสก์และแมโครทาสก์
ความล่าช้าเป็นศูนย์ในความเป็นจริงไม่ใช่ศูนย์ (ในเบราว์เซอร์)
ในเบราว์เซอร์ มีข้อจำกัดว่าตัวจับเวลาที่ซ้อนกันสามารถทำงานได้บ่อยแค่ไหน HTML Living Standard กล่าวว่า: “หลังจากห้าตัวจับเวลาที่ซ้อนกัน ช่วงเวลาจะถูกบังคับให้ต้องมีอย่างน้อย 4 มิลลิวินาที”
มาสาธิตความหมายด้วยตัวอย่างด้านล่าง การเรียก setTimeout
จะกำหนดเวลาใหม่อีกครั้งโดยมีความล่าช้าเป็นศูนย์ การโทรแต่ละครั้งจะจดจำเวลาจริงจากการโทรครั้งก่อนในอาร์เรย์ times
ความล่าช้าที่แท้จริงมีลักษณะอย่างไร? มาดูกัน:
เริ่มกันเลย = Date.now(); ให้ครั้ง = []; setTimeout (ฟังก์ชันรัน () { times.push(Date.now() - เริ่มต้น); // จำความล่าช้าจากการโทรครั้งก่อน ถ้า (เริ่ม + 100 < Date.now ()) การแจ้งเตือน (ครั้ง); // แสดงความล่าช้าหลังจาก 100ms มิฉะนั้น setTimeout (รัน); // อย่างอื่นกำหนดเวลาใหม่ - // ตัวอย่างของผลลัพธ์: // 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
ตัวจับเวลาแรกจะทำงานทันที (ตามที่เขียนไว้ในข้อมูลจำเพาะ) จากนั้นเราจะเห็น 9, 15, 20, 24...
ความล่าช้าบังคับระหว่างการร้องขอ 4+ ms มีผล
สิ่งที่คล้ายกันจะเกิดขึ้นหากเราใช้ setInterval
แทน setTimeout
: setInterval(f)
รัน f
สองสามครั้งโดยมีความล่าช้าเป็นศูนย์ และหลังจากนั้นมีความล่าช้า 4+ ms
ข้อจำกัดดังกล่าวมีมาตั้งแต่สมัยโบราณและมีสคริปต์จำนวนมากต้องอาศัยข้อจำกัดดังกล่าว ดังนั้นจึงมีอยู่ด้วยเหตุผลทางประวัติศาสตร์
สำหรับ JavaScript ฝั่งเซิร์ฟเวอร์ ไม่มีข้อจำกัดดังกล่าว และมีวิธีอื่นในการกำหนดเวลางานอะซิงโครนัสทันที เช่น setImmediate สำหรับ Node.js ดังนั้นบันทึกนี้จึงเป็นเฉพาะเบราว์เซอร์
เมธอด setTimeout(func, delay, ...args)
และ setInterval(func, delay, ...args)
ช่วยให้เราสามารถรัน func
หนึ่งครั้ง/สม่ำเสมอหลังจาก delay
มิลลิวินาที
หากต้องการยกเลิกการดำเนินการ เราควรเรียก clearTimeout/clearInterval
ด้วยค่าที่ส่งคืนโดย setTimeout/setInterval
การเรียก setTimeout
ที่ซ้อนกันเป็นทางเลือกที่ยืดหยุ่นมากกว่าสำหรับ setInterval
ช่วยให้เราสามารถตั้งเวลา ระหว่าง การดำเนินการได้แม่นยำยิ่งขึ้น
การตั้งเวลาหน่วงเป็นศูนย์ด้วย setTimeout(func, 0)
(เช่นเดียวกับ setTimeout(func)
) ใช้เพื่อกำหนดเวลาการโทร "โดยเร็วที่สุด แต่หลังจากสคริปต์ปัจจุบันเสร็จสมบูรณ์"
เบราว์เซอร์จำกัดความล่าช้าขั้นต่ำสำหรับการเรียก setTimeout
ที่ซ้อนกันห้าครั้งขึ้นไปหรือสำหรับ setInterval
(หลังการโทรครั้งที่ 5) ไว้ที่ 4ms นั่นเป็นเหตุผลทางประวัติศาสตร์
โปรดทราบว่าวิธีการจัดตารางเวลาทั้งหมดไม่ รับประกัน ความล่าช้าที่แน่นอน
ตัวอย่างเช่น ตัวจับเวลาในเบราว์เซอร์อาจช้าลงด้วยเหตุผลหลายประการ:
CPU มีโอเวอร์โหลด
แท็บเบราว์เซอร์อยู่ในโหมดพื้นหลัง
แล็ปท็อปอยู่ในโหมดประหยัดแบตเตอรี่
ทั้งหมดที่อาจเพิ่มความละเอียดตัวจับเวลาขั้นต่ำ (ความล่าช้าขั้นต่ำ) เป็น 300ms หรือ 1000ms ขึ้นอยู่กับเบราว์เซอร์และการตั้งค่าประสิทธิภาพระดับระบบปฏิบัติการ
ความสำคัญ: 5
เขียนฟังก์ชัน printNumbers(from, to)
ที่จะส่งออกตัวเลขทุกๆ วินาที โดยเริ่ม from
และลงท้ายด้วย to
สร้างโซลูชันสองแบบ
การใช้ setInterval
การใช้ setTimeout
ที่ซ้อนกัน
ใช้ setInterval
:
ฟังก์ชั่น printNumbers (จาก, ถึง) { ให้ปัจจุบัน = จาก; ให้ timerId = setInterval(function() { การแจ้งเตือน (ปัจจุบัน); ถ้า (ปัจจุบัน == ถึง) { clearInterval(รหัสตัวจับเวลา); - ปัจจุบัน++; }, 1,000); - // การใช้งาน: พิมพ์ตัวเลข (5, 10);
การใช้ setTimeout
ที่ซ้อนกัน :
ฟังก์ชั่น printNumbers (จาก, ถึง) { ให้ปัจจุบัน = จาก; setTimeout (ฟังก์ชั่นไป () { การแจ้งเตือน (ปัจจุบัน); ถ้า (ปัจจุบัน <ถึง) { setTimeout (ไป 1,000); - ปัจจุบัน++; }, 1,000); - // การใช้งาน: พิมพ์ตัวเลข (5, 10);
โปรดทราบว่าในโซลูชันทั้งสอง มีการหน่วงเวลาเริ่มต้นก่อนเอาต์พุตแรก ฟังก์ชันนี้ถูกเรียกหลังจาก 1000ms
ในครั้งแรก
หากเราต้องการให้ฟังก์ชันทำงานทันที เราก็สามารถเพิ่มการโทรเพิ่มเติมในสายแยกได้ เช่นนี้
ฟังก์ชั่น printNumbers (จาก, ถึง) { ให้ปัจจุบัน = จาก; ฟังก์ชั่นไป () { การแจ้งเตือน (ปัจจุบัน); ถ้า (ปัจจุบัน == ถึง) { clearInterval(รหัสตัวจับเวลา); - ปัจจุบัน++; - ไป(); ให้ timerId = setInterval (go, 1,000); - พิมพ์ตัวเลข (5, 10);
ความสำคัญ: 5
ในโค้ดด้านล่างนี้ มีการกำหนดเวลาการโทร setTimeout
จากนั้นจึงทำการคำนวณจำนวนมาก ซึ่งใช้เวลามากกว่า 100 มิลลิวินาทีจึงจะเสร็จสิ้น
ฟังก์ชันตามกำหนดเวลาจะทำงานเมื่อใด
หลังจากการวนซ้ำ
ก่อนวนซ้ำ
ในตอนต้นของวง
alert
จะแสดงอะไร?
ให้ฉัน = 0; setTimeout(() => การแจ้งเตือน (i), 100); - // สมมติว่าเวลาในการรันฟังก์ชันนี้คือ >100ms สำหรับ (ให้ j = 0; j < 100000000; j++) { ฉัน++; -
setTimeout
ใด ๆ จะทำงานหลังจากโค้ดปัจจุบันเสร็จสิ้นแล้วเท่านั้น
i
จะเป็นคนสุดท้าย: 100000000
ให้ฉัน = 0; setTimeout(() => การแจ้งเตือน (i), 100); // 100000000 // สมมติว่าเวลาในการรันฟังก์ชันนี้คือ >100ms สำหรับ (ให้ j = 0; j < 100000000; j++) { ฉัน++; -