Node.jsâde olduÄu gibi tarayıcı JavaScript yürütme akıÅı da bir olay döngüsüne dayanır.
Olay döngüsünün nasıl çalıÅtıÄını anlamak, optimizasyonlar ve bazen de doÄru mimari için önemlidir.
Bu bölümde önce iÅlerin nasıl yürüdüÄüyle ilgili teorik ayrıntıları ele alacaÄız ve ardından bu bilginin pratik uygulamalarını göreceÄiz.
Olay Döngüsü
Olay döngüsü kavramı çok basittir. JavaScript motorunun görevleri beklediÄi, yürüttüÄü ve daha sonra uyuyarak daha fazla görev beklediÄi sonsuz bir döngü vardır.
Motorun genel algoritması:
- Görevler varken:
- en eski görevden baÅlayarak bunları yürütün.
- Bir görev görünene kadar uyuyun, ardından 1âe gidin.
Bu, bir sayfaya göz atarken gördüÄümüz Åeyin biçimselleÅtirilmesidir. JavaScript motoru çoÄu zaman hiçbir Åey yapmaz, yalnızca bir script/iÅleyici/olay etkinleÅtirildiÄinde çalıÅır.
Görev örnekleri:
- Harici bir script
<script src="...">yüklendiÄinde, görev onu yürütmektir. - Bir kullanıcı faresini hareket ettirdiÄinde, görev
mousemoveolayını göndermek ve iÅleyicileri yürütmektir. - ZamanlanmıŠbir
setTimeoutiçin zaman geldiÄinde, görev callbackâi çalıÅtırmaktır. - â¦ve benzeri.
Görevler belirlenir â motor bunları iÅler â sonra daha fazla görev bekler (uyurken ve sıfıra yakın CPU tüketirken).
Motor meÅgulken bir görev gelebilir, sonra sıraya girebilir.
Görevler, âmacrotask sırasıâ (v8 terimi) olarak adlandırılan bir sıra oluÅturur:
ÃrneÄin, motor bir scriptâi yürütmekle meÅgulken, bir kullanıcı faresini hareket ettirerek mousemoveâa neden olabilir ve setTimeout zamanı gelmiÅ olabilir ve benzeri, yukarıdaki resimde gösterildiÄi gibi bu görevler bir kuyruk oluÅturur.
Kuyruktaki görevler âilk gelene ilk hizmetâ esasına göre iÅlenir. Tarayıcı motoru script ile iÅi bittiÄinde, mousemove olayını, ardından setTimeout iÅleyicisini vb. iÅler.
Buraya kadar oldukça basit, deÄil mi?
İki ayrıntı daha:
- Motor bir görevi yürütürken oluÅturma(Render) asla gerçekleÅmez. Görevin uzun sürmesi önemli deÄil. DOMâdaki deÄiÅiklikler yalnızca görev tamamlandıktan sonra boyanır.
- Bir görev çok uzun sürerse tarayıcı, kullanıcı olaylarını iÅleme gibi diÄer görevleri yapamaz. Bu yüzden bir süre sonra âSayfa Yanıt Vermiyorâ gibi bir uyarı vererek görevi tüm sayfayla sonlandırmayı önerir. Bu, çok sayıda karmaÅık hesaplama olduÄunda veya sonsuz bir döngüye yol açan bir programlama hatası olduÄunda olur.
Teori buydu. Åimdi bu bilgiyi nasıl uygulayabileceÄimizi görelim.
Kullanım Senaryosu 1: CPUâya aç görevleri bölme
Diyelim ki CPUâya aç bir görevimiz var.
ÃrneÄin, sözdizimi vurgulama(syntax-highlighting) (bu sayfadaki kod örneklerini renklendirmek için kullanılır) oldukça CPU aÄırlıklıdır. Kodu vurgulamak için, analizi gerçekleÅtirir, birçok renkli öÄe oluÅturur, bunları belgeye ekler â çok fazla zaman alan büyük miktarda metin için.
Motor sözdizimi vurgulama ile meÅgulken, DOM ile ilgili diÄer iÅlemleri yapamaz, kullanıcı olaylarını iÅleyemez vb. Hatta tarayıcının bir süre âhıçkırmasınaâ ve hatta âtakılmasınaâ neden olabilir ki bu kabul edilemez bir durumdur.
Büyük görevi parçalara bölerek sorunlardan kaçınabiliriz. İlk 100 satırı vurgulayın, ardından sonraki 100 satır için âsetTimeoutâ (sıfır gecikmeli) zamanlayın, vb.
Bu yaklaÅımı göstermek için, basitlik adına, metin vurgulama yerine 1 ile 1000000000 arasında sayan bir fonksiyon alalım.
AÅaÄıdaki kodu çalıÅtırırsanız, motor bir süre âaskıda kalırâ. Açıkça fark edilen sunucu tarafı JS için ve tarayıcıda çalıÅtırıyorsanız, sayfadaki diÄer düÄmeleri tıklamayı deneyin â sayım bitene kadar baÅka hiçbir olayın iÅlenmediÄini göreceksiniz.
let i = 0;
let start = Date.now();
function count() {
// aÄır bir iÅ yap
for (let j = 0; j < 1e9; j++) {
i++;
}
alert("Done in " + (Date.now() - start) + 'ms');
}
count();
Tarayıcı, âscript çok uzun sürüyorâ uyarısı bile gösterebilir.
İÅi iç içe setTimeout çaÄrılarını kullanarak bölelim:
let i = 0;
let start = Date.now();
function count() {
// aÄır iÅin bir parçasını yap (*)
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
} else {
setTimeout(count); // yeni caÄrıyı planla (**)
}
}
count();
Artık tarayıcı arayüzü âsaymaâ iÅlemi sırasında tamamen iÅlevseldir.
A single run of count does a part of the job (*), and then re-schedules itself (**) if needed:
Tek bir count çalıÅtırması (*) iÅinin bir bölümünü yapar ve ardından gerekirse kendisini (**) olarak yeniden zamanlar:
- İlk çalıÅtırma sayar:
i=1...1000000. - İkinci çalıÅtırma sayar:
i=1000001..2000000. - â¦ve benzeri.
Åimdi, motor bölüm 1âi yürütmekle meÅgulken yeni bir yan görev (örneÄin onclick olayı) ortaya çıkarsa, sıraya alınır ve sonraki bölümden önce bölüm 1 bittiÄinde yürütülür. count yürütmeleri arasındaki olay döngüsüne periyodik geri dönüÅler, JavaScript motorunun baÅka bir Åey yapması, diÄer kullanıcı eylemlerine tepki vermesi için yeterli âhavaâ saÄlar.
Dikkate deÄer olan Åey, her iki varyantın da â iÅi setTimeout ile bölerek ve bölmeden â hız açısından karÅılaÅtırılabilir olmasıdır. Toplam sayım süresinde pek bir fark yok.
Onları daha da yakınlaÅtırmak için bir iyileÅtirme yapalım.
Zamanlamayı count()un baÅına taÅıyacaÄız:
let i = 0;
let start = Date.now();
function count() {
// zamanlamayı en baÅa taÅı
if (i < 1e9 - 1e6) {
setTimeout(count); // yeni caÄrıyı planla
}
do {
i++;
} while (i % 1e6 != 0);
if (i == 1e9) {
alert("Done in " + (Date.now() - start) + 'ms');
}
}
count();
Åimdi count() yapmaya baÅladıÄımızda ve daha fazla count() yapmamız gerektiÄini gördüÄümüzde, iÅi yapmadan önce bunu hemen zamanlıyoruz.
ÃalıÅtırırsanız, önemli ölçüde daha az zaman aldıÄını fark etmek kolaydır.
Neden?
Ãok basit: HatırladıÄınız gibi, iç içe geçmiÅ birçok setTimeout çaÄrısı için tarayıcıda minimum 4 ms gecikme vardır. 0 ayarlasak bile, 4ms (veya biraz daha fazla). Yani ne kadar erken zamanlarsak o kadar hızlı çalıÅır.
Son olarak, CPUâya aç bir görevi parçalara ayırdık â artık kullanıcı arayüzünü engellemiyor. Ve genel yürütme süresi çok daha uzun deÄil.
Kullanım Senaryosu 2: ilerleme göstergesi
Tarayıcı komut dosyaları için aÄır görevleri bölmenin bir baÅka yararı da ilerleme göstergesi gösterebilmemizdir.
Daha önce belirtildiÄi gibi, DOMâdaki deÄiÅiklikler, ne kadar sürdüÄüne bakılmaksızın, yalnızca Åu anda çalıÅan görev tamamlandıktan sonra boyanır.
Bir yandan, bu harika, çünkü fonksiyonumuz birçok öÄe oluÅturabilir, bunları tek tek belgeye ekleyebilir ve stillerini deÄiÅtirebilir â ziyaretçi herhangi bir âaraâ, tamamlanmamıŠdurum görmez. Ãnemli bir Åey, deÄil mi?
İÅte demo, iâdeki deÄiÅiklikler fonksiyon bitene kadar görünmeyecek, bu yüzden yalnızca son deÄeri göreceÄiz:
<div id="progress"></div>
<script>
function count() {
for (let i = 0; i < 1e6; i++) {
i++;
progress.innerHTML = i;
}
}
count();
</script>
â¦Ancak görev sırasında da bir Åey göstermek isteyebiliriz, örneÄin bir ilerleme çubuÄu.
EÄer aÄır görevi setTimeout kullanarak parçalara ayırırsak, o zaman deÄiÅiklikler aralarında boyanır.
Bu daha güzel görünüyor:
<div id="progress"></div>
<script>
let i = 0;
function count() {
// aÄır iÅin bir parçasını yap (*)
do {
i++;
progress.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e7) {
setTimeout(count);
}
}
count();
</script>
Åimdi <div>, bir tür ilerleme çubuÄu olan iânin artan deÄerlerini gösteriyor.
Kullanım Senaryosu 3: olaydan sonra bir Åeyler yapmak
Bir olay iÅleyicide, bazı eylemleri olay kabarıp tüm seviyelerde iÅlenene kadar ertelemeye karar verebiliriz. Bunu, kodu sıfır gecikmeli setTimeout içine sararak yapabiliriz.
<bilgi:olayları gönderme(dispatch-events)> bölümünde bir örnek gördük: menu-open özel olayı(custom event) setTimeout içinde gönderilir, böylece âclickâ olayı tamamen iÅlendikten sonra gerçekleÅir.
menu.onclick = function() {
// ...
// tıklanan menü öÄesi verileriyle özel bir olay oluÅturun
let customEvent = new CustomEvent("menu-open", {
bubbles: true
});
// özel olayı eÅzamansız(asynchronously) olarak gönder
setTimeout(() => menu.dispatchEvent(customEvent));
};
Macrotasks ve Microtasks
Bu bölümde açıklanan macrotaskâler ile birlikte, bilgi:microtasks-sırası bölümünde bahsedilen microtaskâler vardır.
Microtaskâler yalnızca kodumuzdan gelir. Genellikle promiseâlarla oluÅturulurlar: .then/catch/finally iÅleyicisinin yürütülmesi bir microtask haline gelir. Microtaskâler, bir baÅka promise iÅleme biçimi olduÄu için, waitâin âörtüsü altındaâ da kullanılır.
Ayrıca, microtask kuyruÄunda yürütülmek üzere funcâu sıraya sokan özel bir queueMicrotask(func) fonksiyonu da vardır.
Her macrotaskâdan hemen sonra, motor, diÄer macrotaskâları çalıÅtırmadan veya oluÅturmadan veya baÅka herhangi bir Åeyden önce tüm görevleri microtask kuyruÄundan yürütür.
ÃrneÄin, bir göz atın:
setTimeout(() => alert("timeout"));
Promise.resolve()
.then(() => alert("promise"));
alert("code");
Buradaki sıra ne olacak?
- Sıradan bir eÅzamanlı(synchronous) çaÄrı olduÄu için önce
kodgösterilir. promiseikinci sıradadır, çünkü.thenmicrotask kuyruÄundan geçer ve geçerli koddan sonra çalıÅır.timeoutâu en son gösterir, çünkü bu bir macrotaskâdir.
Daha zengin olay döngüsü resmi Åöyle görünür (sıra yukarıdan aÅaÄıya doÄrudur, yani: önce script, ardından microtaskâler, oluÅturma(rendering) vb.):
Tüm microtaskâler, baÅka herhangi bir olay iÅleme(handling) veya oluÅturma(rendering) veya baÅka herhangi bir macrotask gerçekleÅmeden önce tamamlanır.
Uygulama ortamının microtaskâler arasında temelde aynı olmasını (fare koordinat deÄiÅikliÄi yok, yeni aÄ verisi yok, vb.) garanti ettiÄi için bu önemlidir.
Bir fonksiyonu eÅzamansız(asynchronously) olarak (geçerli koddan sonra) yürütmek istiyorsak, ancak deÄiÅiklikler oluÅturulmadan(rendered) veya yeni olaylar iÅlenmeden(handled) önce, bunu queueMicrotask ile zamanlayabiliriz.
Hereâs an example with âcounting progress barâ, similar to the one shown previously, but queueMicrotask is used instead of setTimeout. You can see that it renders at the very end. Just like the synchronous code:
Burada, daha önce gösterilene benzer bir âsayan ilerleme çubuÄuâ örneÄi verilmiÅtir, ancak setTimeout yerine queueMicrotask kullanılmıÅtır. En sonunda oluÅtuÄunu(render) görebilirsiniz. Tıpkı senkron kod gibi:
<div id="progress"></div>
<script>
let i = 0;
function count() {
// aÄır iÅin bir parçasını yap (*)
do {
i++;
progress.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e6) {
queueMicrotask(count);
}
}
count();
</script>
Ãzet
Daha ayrıntılı bir olay döngüsü algoritması (yine de spesifikasyona kıyasla basitleÅtirilmiÅ olsa da):
- En eski görevi macrotask kuyruÄundan ayırın ve çalıÅtırın (ör. âscriptâ).
- Tüm microtaskâleri yürütün:
- Microtask kuyruÄu boÅ deÄilken:
- En eski microtaskâi sıraya alın ve çalıÅtırın.
- Microtask kuyruÄu boÅ deÄilken:
- Varsa oluÅturme(render) deÄiÅiklikleri.
- Macrotask kuyruÄu boÅsa, bir macrotask görünene kadar bekleyin.
- 1.Adıma gidin.
Yeni bir macrotask zamanlamak için:
- Sıfır gecikmeli
setTimeout(f)kullanın.
Bu, tarayıcının kullanıcı olaylarına tepki verebilmesi ve aralarındaki ilerlemeyi gösterebilmesi için büyük bir hesaplama aÄırlıklı görevi parçalara ayırmak için kullanılabilir.
Ayrıca, olay tamamen iÅlendikten (köpürme iÅlemi) sonra bir eylem zamanlamak için olay iÅleyicilerinde kullanılır.
Yeni bir microtask planlamak için
queueMicrotask(f)kullanın.- Ayrıca promise iÅleyicileri microtask kuyruÄundan geçer.
Microtaskâler arasında UI veya aÄ olayı iÅleme yoktur: Bunlar birbiri ardına hemen çalıÅır.
Bu nedenle, bir fonksiyonu eÅzamansız(asynchronously) olarak ancak ortam durumu içinde yürütmek için queueMicrotask isteyebilirsiniz.
Olay döngüsünü engellememesi gereken uzun aÄır hesaplamalar için Web Workersâı kullanabiliriz.
Bu, baÅka bir paralel iÅ parçacıÄında(thread) kod çalıÅtırmanın bir yoludur.
Web Workers ana süreçle mesaj alıÅveriÅinde bulunabilirler, ancak kendi deÄiÅkenleri ve kendi olay döngüleri vardır.
Web Workerâlarının DOMâa eriÅimi yoktur, bu nedenle, esas olarak hesaplamalar için, aynı anda birden fazla CPU çekirdeÄi kullanmak için yararlıdırlar.
Yorumlar
<code>kullanınız, birkaç satır eklemek için ise<pre>kullanın. EÄer 10 satırdan fazla kod ekleyecekseniz plnkr kullanabilirsiniz)