javascript - Skillnaden mellan microtask och macrotask inom en händelseloop-kontekst
Jag har precis läst klart Promises/A+-specifikationen och stött på termerna mikrouppgift och makrouppgift: se http://promisesaplus.com/#notes
Jag har aldrig hört talas om dessa termer tidigare, och nu är jag nyfiken på vad skillnaden kan vara?
Jag har redan försökt att hitta information på webben, men allt jag har hittat är det här inlägget från w3.org-arkiven (vilket inte förklarar skillnaden för mig): http://lists.w3.org/Archives/Public/public-nextweb/2013Jul/0018.html
Dessutom har jag hittat ett npm-modul som kallas "macrotask": https://www.npmjs.org/package/macrotask Återigen är det inte förtydligat vad skillnaden exakt är.
Allt jag vet är att det har något att göra med händelseloopen, som beskrivs i https://html.spec.whatwg.org/multipage/webappapis.html#task-queue och https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
Jag vet att jag teoretiskt sett borde kunna ta reda på skillnaderna själv, med denna WHATWG-specifikation. Men jag är säker på att andra också skulle kunna dra nytta av en kort förklaring från en expert.
Svar #1
En genomgång av händelseslingan kommer att ha exakt en uppgift som bearbetas från macrotask-kön (denna kö kallas helt enkelt task queue i WHATWG-specifikationen). Efter att denna macrotask har slutförts kommer alla tillgängliga microtasks att bearbetas, nämligen inom samma genomgångscykel. Medan dessa microtasks bearbetas kan de köa ännu fler microtasks, som alla kommer att köras en efter en, tills microtask-kön är slut.
Vilka är de praktiska konsekvenserna av detta?
Om en mikrouppgift rekursivt lägger till andra mikrouppgifter i kön, kan det ta lång tid innan nästa makrouppgift bearbetas. Detta betyder att du kan hamna med ett blockerat användargränssnitt, eller att någon avslutad I/O står och väntar i din applikation.
Men åtminstone när det gäller Node.js process.nextTick-funktion (som köar mikrouppgifter), finns det ett inbyggt skydd mot sådan blockering genom process.maxTickDepth. Detta värde är satt till ett standardvärde på 1000, vilket minskar vidare bearbetning av mikrouppgifter efter att denna gräns uppnåtts, vilket gör att nästa makrouppgift kan bearbetas)
Så när ska man använda vad?
I princip, använd microtasks när du behöver göra saker asynkront på ett synkront sätt (dvs. när du skulle säga utför denna (mikro)uppgift i allra närmaste framtid). Annars, håll dig till macrotasks.
Exempel
makrouppgifter: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI-rendering
mikrouppgifter: process.nextTick, Promises, queueMicrotask, MutationObserver
Svar #2
Grundläggande begrepp i specifikationen:
- Ett händelseloop har en eller flera uppgiftsköer. (uppgiftskö är makrouppgiftskö)
- Varje händelselopp har en mikrouppgiftskö.
- uppgiftskö=makrouppgiftskö != mikrouppgiftskö
- en uppgift kan skjutas in i makrouppgiftskö, eller mikrouppgiftskö
- när en uppgift trycks in i en kö (mikro/makro) menar vi att förberedande arbete är klart, så uppgiften kan utföras nu.
Och händelseloopens processmodell är som följer:
när anropsstacken är tom, gör stegen-
- välj den äldsta uppgiften (uppgift A) i uppgiftsköerna
- om uppgift A är null (betyder att uppgiftsköerna är tomma), hoppa till steg 6
- sätt "för närvarande körande uppgift" till "uppgift A"
- köra "uppgift A" (betyder köra återanropsfunktionen)
- sätt "för närvarande körande uppgift" till null, ta bort "uppgift A"
utför mikrouppgiftskö
- (a). välj den äldsta uppgiften (uppgift x) i mikrouppgiftskön
- (b).om uppgift x är null (betyder att mikrouppgiftsköerna är tomma), hoppa till steg (g)
- (c).sätt "aktuellt körande uppgift" till "uppgift x"
- (d).run "task x"
- (e).sätt "pågående uppgift" till null, ta bort "uppgift x"
- (f).välj nästa äldsta uppgift i mikrouppgiftskön, hoppa till steg (b)
- (g).slutför mikrouppgiftskö;
- hoppa till steg 1.
en förenklad processmodell är som följer:
- kör den äldsta uppgiften i makrouppgiftskön, ta sedan bort den.
- kör alla tillgängliga uppgifter i mikrouppgiftskön, och ta sedan bort dem.
- nästa omgång: kör nästa uppgift i makrouppgiftskön (hoppa steg 2)
något att komma ihåg:
när en uppgift (i makrouppgiftskön) körs, kan nya händelser registreras. Så nya uppgifter kan skapas. Nedan är två nyss skapade uppgifter:
promiseA.then()'s återuppringning är en uppgift
- promiseA är löst/avvisat: uppgiften kommer att skjutas in i mikrouppgiftskön i den aktuella omgången av händelseloopen.
- callbacken för promiseA.then() är en uppgift
- promiseA är löst/avvisat: uppgiften kommer att läggas i mikrouppgiftskön i den aktuella rundan av händelseloppet.
- promiseA är väntande: uppgiften kommer att skjutas in i mikrouppgiftskön i nästa runda av händelseloopen (kan vara nästa runda)
- setTimeout(callback, n)'s callback är en uppgift, och kommer att skickas till macrotask-kön, även om n är 0;
- Uppgift i mikrouppgiftskön kommer att köras i den aktuella rundan, medan uppgift i makrouppgiftskön måste vänta till nästa runda av händelseloopen.
- Vi vet alla att callback för "click", "scroll", "ajax", "setTimeout"... är uppgifter, men vi bör också komma ihåg att JS-kod som helhet i ett script-tag också är en uppgift (en makrouppgift).
Svar #3
Jag tror att vi inte kan diskutera eventloopen separat från stacken, så:
JS har tre "stackar":
- standardstack för alla synkrona anrop (en funktion anropar en annan, etc)
- mikrouppgiftskö (eller jobbkö eller mikrouppgiftsstack) för alla asynkrona operationer med högre prioritet (process.nextTick, Promises, Object.observe, MutationObserver)
- makrouppgiftskö (eller händelsekö, uppgiftskö, makrouppgiftskö) för alla asynkrona operationer med lägre prioritet (setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI-rendering)
|=======|
| macro |
| [...] |
| |
|=======|
| micro |
| [...] |
| |
|=======|
| stack |
| [...] |
| |
|=======|
Och händelseloopen fungerar på detta sätt:
- utför allt från botten till toppen av stacken, och BARA när stacken är tom, kontrollera vad som händer i köerna ovanför
- kontrollera mikrostacken och utför allt där (om det behövs) med hjälp av stacken, en mikrouppgift efter en annan tills mikrouppgiftskön är tom eller inte kräver någon körning och först då kontrollera makrostacken
- kontrollera makrostacken och kör allt där (om det behövs) med hjälp av stacken
Mikrostacken kommer inte att beröras om stacken inte är tom. Makrostacken kommer inte att beröras om mikrostacken inte är tom ELLER inte kräver någon exekvering.
Sammanfattningsvis: mikrouppgiftskön är nästan samma som makrouppgiftskön men dessa uppgifter (process.nextTick, Promises, Object.observe, MutationObserver) har högre prioritet än makrouppgifter.
Micro är som macro men med högre prioritet.
Här har du den "ultimata" koden för att förstå allt.
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]");
Svar #4
Makrouppgifter inkluderar tangentbords- och mus-händelser, timerhändelser (setTimeout), nätverkshändelser, HTML-parsing, ändring av URL med mera. En makrouppgift representerar något diskret och oberoende arbete. Mikrouppgiftskön har högre prioritet, så makrouppgifter kommer att vänta tills alla mikrouppgifter har körts först.
Mikrouppgifter är mindre uppgifter som uppdaterar applikationens tillstånd och bör utföras innan webbläsaren fortsätter med andra uppgifter såsom att åter-rendera användargränssnittet. Mikrouppgifter inkluderar callbacks för löften och förändringar av DOM-mutationer. Mikrouppgifter gör det möjligt för oss att utföra vissa åtgärder innan användargränssnittet åter-renderas, vilket därmed undviker onödig rendering av användargränssnittet som kan visa ett inkonsekvent applikationstillstånd.
Separation av makro- och mikrouppgifter gör det möjligt för händelseloopen att prioritera typer av uppgifter; till exempel genom att ge prioritet till prestandakänsliga uppgifter.
I en enda loopiteration behandlas högst en makrouppgift (andra lämnas väntande i kön), medan alla mikrouppgifter behandlas.
Båda uppgiftsköerna placeras utanför händelseloopen för att indikera att handlingen att lägga till uppgifter i deras matchande köer sker utanför händelseloopen. Annars skulle alla händelser som inträffar medan JavaScript-kod körs förbises. Handlingarna att upptäcka och lägga till uppgifter görs separat från händelseloopen.
Båda typerna av uppgifter utförs en i taget. När en uppgift börjar utföras, utförs den till sitt slut. Endast webbläsaren kan stoppa utförandet av en uppgift; till exempel om uppgiften tar för mycket tid eller minne.
Alla mikrouppgifter bör utföras innan nästa rendering eftersom deras mål är att uppdatera applikationens tillstånd innan rendering sker.
Webbläsaren försöker vanligtvis rendera sidan 60 gånger per sekund. Det är accepterat att 60 bilder per sekund är den hastighet vid vilken animationer kommer att se jämna ut. Om vi vill uppnå jämnt flytande applikationer bör en enskild uppgift och alla mikrouppgifter som genereras av den uppgiften helst slutföras inom 16 ms. Om en uppgift körs i mer än ett par sekunder visar webbläsaren ett meddelande om "Svarar inte-skript".
referens John Resig-hemligheterna med JS Ninja
