(A teljes hatás érdekében olvassa el husky hangon, miközben füstfelhő veszi körül)

Minden egy őszi őszi napon kezdődött. Felhős volt az ég, fújt a szél, és valaki azt mondta nekem, hogy ez setTimeout(0)
átlagosan 4 ms késést eredményez. Állításuk szerint ennyi időbe telik a visszahívás a veremből, a visszahívási sorba és ismét a verembe. Azt hittem, halásztól hangzik (ez az a darab, amit fekete-fehérben képzelsz el, szivarral a számban). Tekintettel arra, hogy a renderelési folyamatnak 16 ms-onként kell futnia a sima animációk lehetővé tétele érdekében, a 4 ms hosszúnak tűnt számomra. Nagyon sokáig.
Néhány naiv teszt a devtoolokban console.time()
megerősítette. Az átlagos késés 20 futtatás során körülbelül 1,5 ms volt. Természetesen a 20 futás nem elégséges mintaméretet jelent, de most volt egy pontom, amit bizonyítanom kellett. Nagyobb léptékű teszteket akartam lefuttatni, amelyekkel pontosabb választ kaphattam. Akkor természetesen elmehetnék, és kollégám arcába integethetném, hogy bebizonyítsam, tévedtek.
Mi másért tesszük, amit csinálunk?

A hagyományos módszer
Rögtön forró vízben találtam magam. Annak méréséhez, hogy mennyi ideig tartott setTimeout(0)
a futtatása, szükségem volt egy funkcióra, amely:
- pillanatfelvételt készített az aktuális időről
- végrehajtott
setTimeout
- majd azonnal kilépett, hogy a verem tiszta legyen, és az ütemezett visszahívás lefusson, és kiszámolhassa az időeltolódást
- és szükségem volt arra, hogy a függvény elég sokszor fusson, így a számítások statisztikailag értelmesek voltak
De az ehhez szükséges konstrukció - a for-loop - nem működne. Mivel a for-ciklus addig nem törli a verem, amíg nem hajt végre minden ciklust, a visszahívás nem fog azonnal futni. Vagy kódba foglalva ezt kapjuk:

A probléma itt benne rejlett - ha setTimeout
többször szeretnék automatikusan futtatni , akkor azt egy másik kontextusból kell megtenni. De amíg más kontextusból futottam, mindig további késleltetés következik be a teszt elindításától a visszahívás végrehajtásáig.
Természetesen meg tudnám nyomorítani, mint néhány ilyen, a semmire sem jó nyomozó, írhatok egy olyan funkciót, amely megteszi, amire szükségem van, majd 10 000-szer másolhatom és illeszthetem be. Megtanulnám, amit tudni akarok, de a kivitelezés korántsem lenne kecses. Ha ezt valaki más arcába dörzsölném, akkor inkább másképp csinálnám.
Aztán nekem jött.
A forradalmi módszer
Használhatnék egy webmunkást.
A webmunkások más szálon futnak. Tehát, ha a setTimeout
logikát egy webmunkásba helyezem, akkor ezt többször is felhívhatom. Minden hívás létrehozná saját végrehajtási kontextusát, meghívná setTimeout
és azonnal kilépne a funkcióból, így a visszahívás végrehajtható. Alig vártam, hogy dolgozzak a webmunkásokkal.
Itt volt az ideje, hogy áttérjek a megbízható Sublime Text-re.
Csak a vizeket teszteltem. Ezzel a kóddal main.js
:

Néhány vízvezeték-szerelés, hogy felkészüljek a tényleges tesztre, de kezdetben csak azt akartam biztosítani, hogy megfelelően tudjak kommunikálni a webmunkással. Tehát ez volt a kezdőbetű worker.js
:

És bár varázslatként működött - olyan eredményeket hozott, amelyekre számítottam volna, de nem:

Annyira megszokva a JS szinkronitását, nem tudtam meglepődni ezen. Az első pillanatban, amikor megláttam, az agyam hibát regisztrált. De mivel minden hurok új webmunkást állít fel, és aszinkron módon futnak, van értelme, hogy a számokat nem nyomtatjuk ki sorrendben.
Lehet, hogy meglepett, de a várakozásoknak megfelelően működött. Folytathatnám a tesztet.
Azt akartam, hogy a webmunkás onmessage
funkciója regisztráljon t0
, felhívjon setTimeout
, majd azonnal kilépjen, hogy ne blokkolja a verem. A visszahívásba azonban további funkciókat is tehetnék, miután beállítottam a t1
. Hozzáadtam postMessage
a visszahíváshoz, így nem blokkolja a veremet:

És itt van a main.js
kód:

Ennek a verziónak problémája van.
Természetesen - mivel új vagyok a webmunkások előtt, eleinte nem gondoltam rá. De amikor a funkció többszöri futtatása folytatódott 0
, gondoltam, hogy valami nem stimmel.
Amikor belülről kinyomtattam az összegeket, onmessage
megkaptam a választ. A fő funkció szinkron módon haladt tovább, és nem várta a munkavállaló üzenetének visszatérését, ezért kiszámította az átlagot, mielőtt a webmunkás végzett.
Gyors és piszkos megoldás az, hogy hozzáad egy számlálót, és csak akkor végezze el a számítást, ha a számláló eléri a maximális értéket. Tehát itt van az újmain.js:

És itt vannak az eredmények:
main(10)
: 0.1
main(100)
: 1.41
main(1000)
: 13.082
Oh. Az én. Nos, ez nem nagyszerű, igaz? Mi folyik itt?

Feláldoztam a teljesítményteszteket, hogy betekinthessek a belsőbe. Most naplózom, t0
és t1
amikor létrejönnek, csak azért, hogy lássam, mi folyik ott.
És az eredmények:

Kiderült, hogy az a várakozásom, t1
hogy azonnal kiszámolják, t0
szintén téves volt. Alapvetően az a tény, hogy a webmunkásokról semmi sincs szinkron, azt jelenti, hogy a legalapvetőbb feltételezéseim a kódom viselkedéséről már nem felelnek meg. Nehéz vakfolt látni.
Nem csak erre, de még azokra az eredményekre sem , amelyekre számíthattam, main(10)
és main(100)
amelyek eredetileg nagyon boldoggá és önelégültté tettek, nem tudtam támaszkodni.
A webmunkások aszinkronitása szintén megbízhatatlan proxyként teszi őket a szokásos veremben való viselkedésükhöz. Tehát bár egy webmunkás teljesítményének mérése setTimeout
érdekes eredményeket ad, ezek nem olyan eredmények, amelyek megválaszolják a kérdésünket.
A tankönyv módszer
Csalódott voltam ... tényleg nem találnék egy vanília JS megoldást, amely egyszerre lenne elegáns és bizonyítaná kollégám tévedését?
És akkor rájöttem - volt valami, amit tehettem, de nem tetszett.
Nevezhetném setTimeout
rekurzív.

Most, amikor felhívom, felhívja, main
hogy testRunner
melyik intézkedés t0
, majd ütemezi a visszahívást. Ezután a visszahívás azonnal fut, kiszámítja t1
, majd testRunner
újra hív , amíg el nem éri a kívánt hívásszámot.
A kód eredménye különösen meglepő volt. Íme néhány kinyomtatott main(10)
és main(1000)
:

Az eredmények jelentősen különböznek a függvény 1000-szeres meghívásánál, szemben a tízszeres hívásával. Ezt többször is kipróbáltam, és nagyjából ugyanazokat az eredményeket kaptam main(10)
, 3–4 ms-os main(1000)
beérkezéssel és 5 ms-os feltöltéssel.
Hogy őszinte legyek, nem tudom, mi történik itt. Kerestem a választ, de nem találtam ésszerű magyarázatot. Ha ezt olvassa, és képzett tippje van arról, hogy mi folyik itt - szívesen hallanék rólad a megjegyzésekben.
A bevált módszer
Valahol a fejemben mindig tudtam, hogy ez így fog történni ... A mutatós dolgok kedvesek azok számára, akik megszerezhetik, de a bevált és igazak mindig ott lesznek a végén. Annak ellenére, hogy megpróbáltam elkerülni, mindig is tudtam, hogy ez egy lehetőség. setInterval
.

Ez a kód kissé durva erővel végzi a trükköt. setInterval
A függvény ismételt futtatása, az egyes futtatások között 50 ms-ot várva, hogy a verem tiszta legyen. Ez nem teljes, de pontosan azt teszteli, amire szükségem volt.
És az eredmények is ígéretesek voltak. Úgy tűnik, hogy az idők megfelelnek eredeti várakozásomnak - 1,5 ms alatt.


Végül betehetném ezt az esetet. Volt néhány hullámvölgyem, és részem a váratlan eredményekből, de végül csak egy dolog számított - egy másik fejlesztőt tévedtem be! Ez elég jó volt nekem.
Szeretne játszani ezzel a kóddal? nézze meg itt: //github.com/NettaB/setTimeout-test