ความแตกต่างระหว่าง microtask และ macrotask ภายในบริบทของเหตุการณ์


147

ฉันเพิ่งอ่านข้อกำหนด Promises / A + เสร็จแล้วและสะดุดกับคำว่า microtask และ macrotask: ดูที่http://promisesaplus.com/#notes

ฉันไม่เคยได้ยินคำศัพท์เหล่านี้มาก่อนและตอนนี้ฉันสงสัยว่าความแตกต่างคืออะไร?

ฉันพยายามค้นหาข้อมูลบางอย่างบนเว็บแล้ว แต่ทั้งหมดที่ฉันพบคือโพสต์นี้จากคลังเก็บ w3.org (ซึ่งไม่ได้อธิบายความแตกต่างให้ฉัน): http://lists.w3.org/Archives /Public/public-nextweb/2013Jul/0018.html

นอกจากนี้ฉันพบโมดูล npm ที่เรียกว่า "macrotask": https://www.npmjs.org/package/macrotask อีกครั้งยังไม่มีการชี้แจงว่าความแตกต่างคืออะไรกันแน่

สิ่งที่ฉันรู้ก็คือมันมีบางอย่างเกี่ยวข้องกับการวนซ้ำของเหตุการณ์ดังที่อธิบายไว้ในhttps://html.spec.whatwg.org/multipage/webappapis.html#task-queue และhttps: //html.spec.whatwg .org / multipage / webappapis.html # perform-a-microtask-จุดตรวจ

ฉันรู้ว่าในทางทฤษฎีฉันควรจะสามารถดึงความแตกต่างออกมาได้ด้วยตัวเองตามข้อกำหนด WHATWG นี้ แต่ฉันแน่ใจว่าคนอื่น ๆ ก็อาจได้รับประโยชน์เช่นกันจากคำอธิบายสั้น ๆ ที่ผู้เชี่ยวชาญให้มา


กล่าวโดยย่อ: คิวเหตุการณ์ที่ซ้อนกันหลายคิว คุณสามารถใช้งานได้ด้วยตัวเอง:while (task = todo.shift()) task();
Bergi

1
สำหรับคนที่ต้องการรายละเอียดเพิ่มเติม: Secrets of the JavaScript Ninja, 2nd Edition, CHAPTER 13 Surviving events
Ethan

คำตอบ:


232

การวนรอบเหตุการณ์หนึ่งรอบจะมีงานเดียวที่ถูกประมวลผลจากคิว macrotask (คิวนี้เรียกง่ายๆว่าคิวงานในข้อกำหนด WHATWG ) หลังจาก macrotask นี้ได้ดำเนินการเสร็จสิ้นที่มีอยู่ทั้งหมดmicrotasksจะดำเนินการคือภายในวงจรไปรอบเดียวกัน ในขณะที่มีการประมวลผล microtasks พวกเขาสามารถจัดคิว microtasks ได้มากขึ้นซึ่งทั้งหมดจะถูกเรียกใช้ทีละรายการจนกว่าคิว microtask จะหมด

ผลที่ตามมาในทางปฏิบัติคืออะไร?

หากmicrotaskจัดคิวไมโครเทสก์อื่นซ้ำ ๆ อาจใช้เวลานานกว่าจะประมวลผล macrotask ถัดไป ซึ่งหมายความว่าคุณอาจจบลงด้วย UI ที่ถูกบล็อกหรือ I / O บางตัวที่ไม่ทำงานในแอปพลิเคชันของคุณ

อย่างไรก็ตามอย่างน้อยเกี่ยวกับฟังก์ชัน process.nextTick ของ Node.js (ซึ่งจัดคิวmicrotasks ) มีการป้องกันแบบ inbuilt จากการบล็อกดังกล่าวโดยใช้ process.maxTickDepth ค่านี้ถูกตั้งค่าเป็นค่าเริ่มต้นที่ 1,000 ซึ่งจะลดการประมวลผลไมโครเทสก์เพิ่มเติมหลังจากถึงขีด จำกัด นี้ซึ่งจะทำให้สามารถประมวลผลmacrotaskถัดไปได้)

แล้วจะใช้อะไรเมื่อไหร่?

โดยพื้นฐานแล้วให้ใช้ไมโครเทสก์เมื่อคุณต้องการทำสิ่งต่างๆแบบอะซิงโครนัสในลักษณะซิงโครนัส (เช่นเมื่อคุณพูดว่าทำงานนี้ (ไมโคร) ในอนาคตอันใกล้ที่สุด ) มิฉะนั้นติดmacrotasks

ตัวอย่าง

macrotasks: setTimeout , setInterval , setImmediate , requestAnimationFrame , I / O , การแสดงผล UI
microtasks: process.nextTick , Promises , queMicrotask , MutationObserver


4
แม้ว่าจะมีจุดตรวจ microtask ในลูปเหตุการณ์ แต่นี่ไม่ใช่ที่ที่นักพัฒนาส่วนใหญ่จะพบกับ microtasks Microtasks ถูกประมวลผลเมื่อสแต็ก JS ว่างเปล่า สิ่งนี้สามารถเกิดขึ้นได้หลายครั้งภายในงานหรือแม้แต่ในขั้นตอนการแสดงผลของลูปเหตุการณ์
JaffaTheCake

3
process.maxTickDepthถูกลบเมื่อนานมาแล้ว: github.com/nodejs/node/blob/…
RidgeA

2
นอกจากนี้คุณยังสามารถใช้เมธอด queMicrotask ()เพื่อเพิ่ม microtask ใหม่
ZoomAll

ขอบคุณ @ZoomAll ไม่ทราบว่า queMicrotask () จนถึงตอนนี้ ฉันได้เพิ่มคำตอบพร้อมลิงก์ไปยังทุกสิ่ง ...
NicBright

1
requestAnimationFrame (rAF) ไม่เพียง แต่สร้าง microtasks โดยทั่วไปการเรียก RAF จะสร้างคิวแยกต่างหาก
ZoomAll

74

แนวคิดพื้นฐานในข้อมูลจำเพาะ :

  • ลูปเหตุการณ์มีคิวงานตั้งแต่หนึ่งคิวขึ้นไป (คิวงานคือคิว macrotask)
  • แต่ละลูปของเหตุการณ์มีคิวไมโครทาสก์
  • คิวงาน = คิว macrotask! = คิว microtask
  • งานอาจถูกผลักเข้าไปในคิว macrotask หรือคิว microtask
  • เมื่องานถูกผลักเข้าไปในคิว (ไมโคร / มาโคร) เราหมายถึงการเตรียมงานเสร็จสิ้นดังนั้นจึงสามารถดำเนินการได้ทันที

และแบบจำลองกระบวนการวนซ้ำเหตุการณ์มีดังนี้:

เมื่อcall stackว่างเปล่าให้ทำตามขั้นตอน -

  1. เลือกงานที่เก่าที่สุด (งาน A) ในคิวงาน
  2. ถ้างาน A เป็นโมฆะ (หมายถึงคิวงานว่าง) ให้ข้ามไปที่ขั้นตอนที่ 6
  3. ตั้งค่า "งานที่กำลังทำงานอยู่" เป็น "งาน A"
  4. เรียกใช้ "งาน A" (หมายถึงเรียกใช้ฟังก์ชันเรียกกลับ)
  5. ตั้งค่า "งานที่กำลังทำงานอยู่" เป็นค่าว่างลบ "งาน A"
  6. ดำเนินการคิว microtask
    • (a). เลือกงานที่เก่าที่สุด (งาน x) ในคิว microtask
    • (b) ถ้างาน x เป็นโมฆะ (หมายถึงคิว microtask ว่างเปล่า) ให้ข้ามไปที่ขั้นตอน (g)
    • (c). ตั้งค่า "งานที่กำลังทำงานอยู่" เป็น "งาน x"
    • (d). run "งาน x"
    • (e). ตั้งค่า "งานที่กำลังทำงานอยู่" เป็นค่าว่างลบ "งาน x"
    • (f) เลือกงานที่เก่าที่สุดถัดไปในคิว microtask ข้ามไปที่ขั้นตอน (b)
    • (g) เสร็จสิ้นคิว microtask;
  7. ข้ามไปที่ขั้นตอนที่ 1

รูปแบบกระบวนการที่เรียบง่ายมีดังนี้:

  1. รันงานที่เก่าที่สุดในคิว macrotask แล้วลบออก
  2. รันงานที่มีทั้งหมดในคิว microtask จากนั้นลบออก
  3. รอบต่อไป: รันงานต่อไปในคิว macrotask (ข้ามขั้นตอนที่ 2)

สิ่งที่ต้องจำ:

  1. เมื่องาน (ในคิว macrotask) กำลังทำงานอาจมีการลงทะเบียนเหตุการณ์ใหม่ดังนั้นอาจมีการสร้างงานใหม่ด้านล่างนี้เป็นงานที่สร้างขึ้นใหม่สองงาน:
    • การโทรกลับของ PromiseA.then () คืองาน
      • PromiseA ได้รับการแก้ไข / ปฏิเสธ: งานจะถูกผลักเข้าไปในคิว microtask ในรอบเหตุการณ์ปัจจุบัน
      • PromiseA กำลังรอดำเนินการ: งานจะถูกผลักเข้าไปในคิว microtask ในรอบเหตุการณ์ในอนาคต (อาจเป็นรอบถัดไป)
    • การเรียกกลับของ setTimeout (การเรียกกลับ, n) เป็นงานและจะถูกผลักเข้าไปในคิว macrotask แม้ n คือ 0;
  2. งานในคิว microtask จะถูกรันในรอบปัจจุบันในขณะที่งานในคิว macrotask ต้องรอรอบถัดไปของเหตุการณ์วนซ้ำ
  3. เราทุกคนรู้จักการเรียกกลับของ "click", "scroll", "ajax", "setTimeout" ... เป็นงานอย่างไรก็ตามเราควรจำรหัส js โดยรวมในแท็กสคริปต์ว่าเป็นงาน (macrotask) ด้วยเช่นกัน

2
นี่คือคำอธิบายที่ยอดเยี่ยม! ขอบคุณสำหรับการแบ่งปัน!. อีกสิ่งหนึ่งที่จะกล่าวถึงอยู่ในNodeJs , setImmediate()เป็นงาน / แมโครและprocess.nextTick()เป็นไมโคร / งาน
LeOn - Han Li

6
แล้วpaintงานเบราว์เซอร์ล่ะ เข้ากับหมวดหมู่ใด
ตำนาน

ฉันคิดว่าพวกเขาน่าจะเหมาะกับงานเล็ก ๆ (เช่นrequestAnimationFrame)
Divyanshu Maithani

นี่คือลำดับที่เหตุการณ์ v8 ทำงานวน -> Call Stack || งานไมโคร || คิวงาน || rAF || ทำให้ต้นไม้ || เค้าโครง || สี || <การเรียกใช้ Native OS เพื่อวาดพิกเซลบนหน้าจอ> <----- 1) DOM (การเปลี่ยนแปลงใหม่), CSSOM (การเปลี่ยนแปลงใหม่), การแสดงผลทรี, เค้าโครงและสีจะเกิดขึ้นหลังจากการเรียกกลับ requestAnimationFrame ตามตัวจับเวลาของเหตุการณ์ นี่คือเหตุผลว่าทำไมการดำเนินการ DOM ของคุณให้เสร็จสิ้นก่อน RAF ให้มากที่สุดเท่าที่จะทำได้การที่เหลือสามารถทำได้ใน RAF PS: การเรียก rAF จะทริกเกอร์การเรียกใช้งานมาโคร
Anvesh Checka

ฉันไม่รู้ว่าฉันเข้าใจผิด แต่ฉันไม่เห็นด้วยกับคำตอบนี้ microtasks ทำงานก่อน macrotask codepen.io/walox/pen/yLYjNRq ?
walox

17

ฉันคิดว่าเราไม่สามารถพูดถึงการวนซ้ำของเหตุการณ์ในการแยกออกจากสแตกได้ดังนั้น:

JS มี "สแต็ก" สามรายการ:

  • สแต็กมาตรฐานสำหรับการโทรแบบซิงโครนัสทั้งหมด (ฟังก์ชันหนึ่งเรียกอีกฟังก์ชันหนึ่ง ฯลฯ )
  • microtask que (หรือคิวงานหรือ microtask stack) สำหรับการดำเนินการ async ทั้งหมดที่มีลำดับความสำคัญสูงกว่า(process.nextTick, Promises, Object.observe, MutationObserver)
  • คิว macrotask (หรือคิวเหตุการณ์คิวงานคิว macrotask) สำหรับการดำเนินการ async ทั้งหมดที่มีลำดับความสำคัญต่ำกว่า(setTimeout, setInterval, setImmediate, requestAnimationFrame, I / O, การแสดงผล UI)
|=======|
| macro |
| [...] |
|       |
|=======|
| micro |
| [...] |
|       |
|=======|
| stack |
| [...] |
|       |
|=======|

และลูปเหตุการณ์ทำงานในลักษณะนี้:

  • ดำเนินการทุกอย่างจากล่างขึ้นบนจากสแต็กและเฉพาะเมื่อสแต็กว่างเปล่าให้ตรวจสอบสิ่งที่เกิดขึ้นในคิวด้านบน
  • ตรวจสอบไมโครสแต็กและดำเนินการทุกอย่างที่นั่น (ถ้าจำเป็น) ด้วยความช่วยเหลือของสแต็กงานไมโครหนึ่งงานต่อเนื่องกันจนกว่าคิวไมโครทาสก์จะว่างเปล่าหรือไม่ต้องดำเนินการใด ๆ จากนั้นตรวจสอบมาโครสแต็ก
  • ตรวจสอบมาโครสแต็กและดำเนินการทุกอย่างที่นั่น (ถ้าจำเป็น) ด้วยความช่วยเหลือของสแต็ก

Mico stack จะไม่ถูกสัมผัสหากสแต็กไม่ว่างเปล่า Macro stack จะไม่ถูกสัมผัสหากไมโครสแต็กไม่ว่างเปล่าหรือไม่ต้องการการดำเนินการใด ๆ

สรุป: คิว microtask เกือบจะเหมือนกับคิว macrotask แต่งานเหล่านั้น(process.nextTick, Promises, Object.observe, MutationObserver)มีลำดับความสำคัญสูงกว่า macrotasks

ไมโครเป็นเหมือนมาโคร แต่มีลำดับความสำคัญสูงกว่า

ที่นี่คุณมีรหัส "ขั้นสูงสุด" สำหรับการทำความเข้าใจทุกอย่าง

console.log('stack [1]');
setTimeout(() => console.log("macro [2]"), 0);
setTimeout(() => console.log("macro [3]"), 1);

const p = Promise.resolve();
for(let i = 0; i < 3; i++) p.then(() => {
    setTimeout(() => {
        console.log('stack [4]')
        setTimeout(() => console.log("macro [5]"), 0);
        p.then(() => console.log('micro [6]'));
    }, 0);
    console.log("stack [7]");
});

console.log("macro [8]");

/* Result:
stack [1]
macro [8]

stack [7], stack [7], stack [7]

macro [2]
macro [3]

stack [4]
micro [6]
stack [4]
micro [6]
stack [4]
micro [6]

macro [5], macro [5], macro [5]
--------------------
but in node in versions < 11 (older versions) you will get something different


stack [1]
macro [8]

stack [7], stack [7], stack [7]

macro [2]
macro [3]

stack [4], stack [4], stack [4]
micro [6], micro [6], micro [6]

macro [5], macro [5], macro [5]

more info: https://blog.insiderattack.net/new-changes-to-timers-and-microtasks-from-node-v11-0-0-and-above-68d112743eb3
*/

7
การเรียกคิวกองซ้อนทำให้สับสนโดยสิ้นเชิง
Bergi

ขอบคุณสำหรับข้อคิดเห็นเกี่ยวกับ Node.js
L_K
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.