欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Javascript單線程和事件循環(huán)

 更新時(shí)間:2022年06月07日 15:00:38   作者:DvorakChen  
這篇文章主要介紹了Javascript單線程和事件循環(huán),文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下

一、單線程

Javascript 是單線程的,意味著不會(huì)有其他線程來競(jìng)爭(zhēng)。為什么是單線程呢?

假設(shè) Javascript 是多線程的,有兩個(gè)線程,分別對(duì)同一個(gè)元素進(jìn)行操作:

function changeValue() {
  const e = document.getElementById("ele1");
  if (e) {
    e.value = "VALUE";
  }
}

function deleteElement() {
  const e = document.getElementById("ele1");
  if (e) {
    e.remove();
  }
}

一個(gè)線程將執(zhí)行changeValue()函數(shù),如果元素存在就修改元素的值;一個(gè)線程將執(zhí)行deleteElement()函數(shù),如果元素存在就刪除元素。此時(shí)在多線程的條件下,兩個(gè)函數(shù)同時(shí)執(zhí)行,線程 1 執(zhí)行,判斷元素存在,準(zhǔn)備執(zhí)行修改值的代碼e.value = "VALUE";,此時(shí)線程 2 搶占了 CPU,執(zhí)行了deleteElement()函數(shù),完整的執(zhí)行結(jié)束,成功刪除了元素,CPU 的控制權(quán)回到了線程 1,線程 1 繼續(xù)執(zhí)行剩下的代碼,也就是將要執(zhí)行的e.value = "VALUE";,然而因?yàn)檫@個(gè)元素被線程 2 刪除了,獲取不到元素,修改元素的值失??!

能夠發(fā)現(xiàn),瀏覽器環(huán)境下,不管有幾個(gè)線程,都是共享同一個(gè)文檔(Document),對(duì) DOM 的頻繁操作,多線程將帶來極大的不穩(wěn)定性。如果是單線程,則能夠保證對(duì) DOM 的操作是極其穩(wěn)定和可預(yù)見的。你永遠(yuǎn)不用擔(dān)心有別的線程搶占了資源,做了什么操作而影響到原來的線程。

由于單線程,JS 一次只能處理一個(gè)任務(wù),在該任務(wù)處理完成之前,其他任務(wù)必須等待。這一點(diǎn)非常重要,在理解下面的事件循環(huán)前,首先得明確這個(gè)概念。

二、事件循環(huán)

如你所見,因?yàn)闉g覽器執(zhí)行Javascript是單線程,所以一次只能夠執(zhí)行一個(gè)任務(wù)。那么當(dāng)出現(xiàn)多個(gè)要執(zhí)行的任務(wù),其他尚未執(zhí)行的任務(wù)在什么地方等待呢?

為了能夠讓任務(wù)有個(gè)可以等待執(zhí)行的地方,瀏覽器就建立了一個(gè)隊(duì)列,所有的任務(wù)都在隊(duì)列里等待,當(dāng)要執(zhí)行任務(wù)的時(shí)候,就從隊(duì)列的隊(duì)頭里拿一個(gè)任務(wù)來執(zhí)行,執(zhí)行過程中,其他任務(wù)繼續(xù)等待。當(dāng)任務(wù)執(zhí)行完之后,再從隊(duì)列里拿下一個(gè)任務(wù)來執(zhí)行。

可是,除了開發(fā)者編寫的Javascript代碼之外,還有很多事件發(fā)生,比如瀏覽器的點(diǎn)擊事件,鼠標(biāo)移動(dòng)事件,鍵盤事件,網(wǎng)絡(luò)請(qǐng)求等。這些事件也需要執(zhí)行,而且為了客戶體驗(yàn)的流暢,需要盡快執(zhí)行,以更新頁面。我們的隊(duì)列可能有很多任務(wù)正在等待執(zhí)行,如果把瀏覽器發(fā)生的事件排入隊(duì)列的隊(duì)尾,那么在前面的任務(wù)執(zhí)行完成之前,瀏覽器的頁面將一直堵塞住,在用戶看在,將是非??D的。

為了應(yīng)對(duì)這種問題,瀏覽器就多加了一個(gè)隊(duì)列,這個(gè)隊(duì)列中的任務(wù),將被盡快執(zhí)行。為了和前一個(gè)隊(duì)列做區(qū)分,前面一個(gè)隊(duì)列就叫宏任務(wù)隊(duì)列吧,這個(gè)新加的隊(duì)列就叫微任務(wù)隊(duì)列吧。宏任務(wù)隊(duì)列的任務(wù)叫宏任務(wù),微任務(wù)隊(duì)列里的任務(wù)叫微任務(wù)。

宏任務(wù)隊(duì)列的執(zhí)行方式仍不變,還是一次拿一個(gè)宏任務(wù)來執(zhí)行。但是在執(zhí)行完一個(gè)宏任務(wù)后,就變了,不檢查宏任務(wù)隊(duì)列是否為空,而是檢查微任務(wù)隊(duì)列是否為空! 如果微任務(wù)隊(duì)列不為空,就執(zhí)行一個(gè)微任務(wù),當(dāng)前微任務(wù)執(zhí)行完成后,繼續(xù)檢查微任務(wù)隊(duì)列是否為空,如果微任務(wù)隊(duì)列不為空,就再執(zhí)行一個(gè)微任務(wù),直到微任務(wù)隊(duì)列為空。當(dāng)微任務(wù)隊(duì)列為空后,就渲染瀏覽器,回到宏任務(wù)隊(duì)列執(zhí)行,如此循環(huán)往復(fù)。

通過這種模型,瀏覽器將需要快速響應(yīng)的 DOM 事件放入微任務(wù)隊(duì)列,以達(dá)到快速執(zhí)行的目的。當(dāng)微任務(wù)隊(duì)列執(zhí)行完成后,便按需要重新渲染瀏覽器,用戶就會(huì)感覺自己的操作被迅速地響應(yīng)了。

這種事件執(zhí)行方式,稱為事件循環(huán)。瀏覽器中的事件和代碼,就在事件循環(huán)模型下執(zhí)行。

三、事件循環(huán)的應(yīng)用

通過上圖的事件循環(huán)模型,我們得知瀏覽器渲染的順序,是在執(zhí)行了一個(gè)宏任務(wù)和剩下的所有微任務(wù)之后,那么為了保證瀏覽器的渲染順暢,我們不宜讓每一個(gè)宏任務(wù)的執(zhí)行事件太長,也不能讓清空微任務(wù)隊(duì)列太耗時(shí)。一次事件循環(huán)中,只執(zhí)行一個(gè)宏任務(wù),那么,對(duì)耗時(shí)的宏任務(wù)需要分解成盡可能小的宏任務(wù),微任務(wù)卻不同。由于微任務(wù)是清空整個(gè)微任務(wù)隊(duì)列,所以,在微任務(wù)里不要生成新的微任務(wù)。畢竟微任務(wù)隊(duì)列的使命就是為了盡可能先處理微任務(wù),然后重新渲染瀏覽器。

宏任務(wù)隊(duì)列和微任務(wù)隊(duì)列這兩者,都是獨(dú)立于事件循環(huán)的,也就是說,在執(zhí)行Javascript代碼時(shí),任務(wù)隊(duì)列的添加行為也在發(fā)生,即使現(xiàn)在正在清空微任務(wù)隊(duì)列。這是為了避免在執(zhí)行代碼時(shí),發(fā)生的事件被忽略。如此可知,即使我們分解一個(gè)耗時(shí)任務(wù),也不能因?yàn)槲⑷蝿?wù)會(huì)被優(yōu)先執(zhí)行就選擇將它分解成多個(gè)微任務(wù),這將阻塞瀏覽器重新渲染。更好的做法是分解成多個(gè)宏任務(wù),這樣執(zhí)行一個(gè)分解后的宏任務(wù)不會(huì)太耗時(shí),可以盡快達(dá)到讓瀏覽器渲染。

在瀏覽器的渲染之前,會(huì)清空微任務(wù)隊(duì)列,所以,對(duì)瀏覽器 DOM 的修改更新,就適合放到微任務(wù)里去執(zhí)行。

瀏覽器渲染的次數(shù)大概是每秒 60 次,約等于 16ms 一次。在瀏覽器渲染頁面的時(shí)候,任何任務(wù)都無法再對(duì)頁面進(jìn)行修改,這意味著,為了頁面的平滑順暢,我們的代碼,單個(gè)宏任務(wù)和當(dāng)前微任務(wù)隊(duì)列里所有微任務(wù),都應(yīng)該在 16ms 內(nèi)執(zhí)行完成。否則就會(huì)造成頁面卡頓。

四、使用代碼來說明

我會(huì)用一些簡(jiǎn)單卻有效的代碼來說明事件循環(huán)如何影響頁面效果,以下的代碼很少,建議你一起編寫,體驗(yàn)一下。

先看下面的代碼,我定義了一個(gè)foo()函數(shù),它將一次性往元素中添加 5 萬個(gè)子元素,我將在頁面加載完成后立即執(zhí)行它。

function foo() {
  const d = document.getElementById("container");
  for (let index = 0; index < 50000; index++) {
    const e = document.createElement("div");
    e.textContent = "NEW";
    d.appendChild(e);
  }
}

可見這是一個(gè)耗時(shí)的操作,如果你電腦很好,體驗(yàn)不到卡頓的話,可以換成循環(huán) 50 萬次。

在一陣時(shí)間的卡頓后,頁面一次性出現(xiàn)了大量子元素。雖說添加元素的目的達(dá)到了,但是元素出現(xiàn)之前的卡頓卻不能忍受。根據(jù)事件循環(huán),我們能夠知道,是因?yàn)閳?zhí)行了一個(gè)非常耗時(shí)的宏任務(wù),導(dǎo)致阻塞了頁面的渲染。用下面一張圖說明。

上面這張圖代表著本次事件循環(huán)的執(zhí)行,一開始,瀏覽器就將foo()放進(jìn)宏任務(wù)隊(duì)列。從 0ms 開始,宏任務(wù)隊(duì)列里有任務(wù),事件循環(huán)取出一個(gè)宏任務(wù),該宏任務(wù)為foo(),執(zhí)行,添加 5 萬個(gè)子元素,執(zhí)行非常耗時(shí),需要 2000ms(假設(shè)的時(shí)間),foo()執(zhí)行完后,執(zhí)行微任務(wù),假設(shè)我們的清空微任務(wù)隊(duì)列需要執(zhí)行 5ms,清空后,時(shí)間來到了 2005ms,這個(gè)時(shí)候才能開始重新渲染瀏覽器。經(jīng)過了這一次事件循環(huán),竟然耗時(shí)了 2015ms!

那么,我們要改善體驗(yàn),期望是一個(gè)平滑的渲染效果。因?yàn)闉g覽器頁面的變化,只有在事件循環(huán)中重新渲染瀏覽器這一步才會(huì)發(fā)生變化,所以我們要做的就是,盡可能快地到事件循環(huán)中的渲染瀏覽器這一步。所以,我們要將這個(gè)foo()分解成多個(gè)宏任務(wù)。

為什么不能分解成微任務(wù)?因?yàn)槲⑷蝿?wù)會(huì)在宏任務(wù)完成后全部執(zhí)行。假設(shè)我們將添加 5 萬 個(gè)元素分解成宏任務(wù)添加 1000 個(gè),微任務(wù)添加 49000 個(gè),那么事件循環(huán)還是必須執(zhí)行完添加 1000 個(gè)元素的宏任務(wù)后,執(zhí)行添加 49000 個(gè)元素的微任務(wù),才能渲染頁面。所以我們要分解成宏任務(wù)。

假設(shè)我們分解成了 200 個(gè)宏任務(wù),每個(gè)宏任務(wù)都添加 250 個(gè)元素,那么,在事件循環(huán)執(zhí)行的時(shí)候,任務(wù)隊(duì)列里有 200 個(gè)宏任務(wù),取出一個(gè)執(zhí)行,這個(gè)宏任務(wù)只添加 250 個(gè)元素,耗時(shí) 10ms。當(dāng)前宏任務(wù)完成后,便清空微任務(wù),耗時(shí) 5ms,時(shí)間來到了 15ms,就可以渲染瀏覽器了。這一次事件循環(huán),在渲染瀏覽器前只耗時(shí) 15ms!

接著,渲染瀏覽器后,頁面上出現(xiàn)了 250 個(gè)元素,又開始事件循環(huán),從宏任務(wù)隊(duì)列里拿出一個(gè)宏任務(wù)執(zhí)行。

如上圖所示,接連不斷的事件循環(huán)使瀏覽器渲染看起來平滑順暢。

接下來我們便改造我們的代碼,讓它分解成多個(gè)宏任務(wù)。

五、setTimeout()

setTimeout()函數(shù),用于將一個(gè)函數(shù)延遲執(zhí)行,是我們的重點(diǎn)方法。

你應(yīng)該很熟悉這個(gè)函數(shù)的用法了,setTimeout()接收兩個(gè)參數(shù),第一個(gè)是一個(gè)回調(diào)函數(shù),第二個(gè)是數(shù)字,用于指示延遲多少時(shí)間,以毫秒為單位(ms)。

這里主要介紹的是第二個(gè)參數(shù),很多人以為第二個(gè)參數(shù)是指延遲多少毫秒后執(zhí)行傳進(jìn)來的函數(shù),但其實(shí),它的真正含義是:延遲多少毫秒后進(jìn)入宏任務(wù)隊(duì)列!

假設(shè)如下代碼:

setTimeout(() => {
  console.log("execute setTimeout()");
}, 10);

下面我用一張圖說明這段代碼的執(zhí)行,圖中,上方代表時(shí)間軸,下方代表宏任務(wù)隊(duì)列。

在 0ms 時(shí),注冊(cè)setTimeout函數(shù),第一個(gè)參數(shù)里的方法將在 10ms 后加入宏任務(wù)隊(duì)列,此時(shí),宏任務(wù)時(shí)沒有我們代碼里的任務(wù)的。

其他我們不知道的 JS 代碼執(zhí)行了 10 ms。

到了 10ms 后,setTimeout到期,第一個(gè)參數(shù)里的方法加入宏任務(wù)隊(duì)列。

上圖中,10ms 到了,加入了宏任務(wù)隊(duì)列。但是要注意,事件循環(huán)此時(shí)可能正在執(zhí)行一個(gè)宏任務(wù),或者正在清空微任務(wù)隊(duì)列,或者正在渲染瀏覽器,所以不會(huì)馬上執(zhí)行新增加的宏任務(wù),只有又一次循環(huán)到了執(zhí)行宏任務(wù)的時(shí)候,才會(huì)從宏任務(wù)隊(duì)列中獲取宏任務(wù)執(zhí)行(JS 是單線程的)。假設(shè)這段時(shí)間耗時(shí)了 5ms,那么如下圖。

如上圖所示,在 15ms 的時(shí)候,我們才從宏任務(wù)隊(duì)列里取出在 10ms 時(shí)放入宏任務(wù)隊(duì)列的宏任務(wù),并執(zhí)行。和我們的代碼對(duì)比,盡管setTimeout的第二個(gè)參數(shù)是 10ms,卻在 15ms 才執(zhí)行。

當(dāng)理解了setTimeout的原理之后,便可以使用setTimeout將一個(gè)耗時(shí)的任務(wù)分解成多個(gè)宏任務(wù),以充分給予瀏覽器渲染。

我修改了foo函數(shù),如下所示:

function foo() {
  const d = document.getElementById("container");
  const total = 50000;
  const size = 250;
  const chunk = total / size;
  let i = 0;
  setTimeout(function render() {
    for (let index = 0; index < size; index++) {
      const e = document.createElement("div");
      e.textContent = "NEW";
      d.appendChild(e);
    }
    i++;
    if (i < chunk) {
      setTimeout(render, 0);
    }
  }, 0);
}

foo方法中,首先獲取了要添加子元素的元素,和定義了各種變量。total表示一共有幾個(gè)元素要添加,因?yàn)槲译娔X性能差,所以是 5 萬,你可以修改成你喜歡的值;size是指我們分解后每個(gè)宏任務(wù)要添加幾次元素;chunk是指分解后,一共有幾個(gè)宏任務(wù),通過簡(jiǎn)單的計(jì)算得到;i是用于標(biāo)記執(zhí)行到了第幾個(gè)宏任務(wù)了。

接下來就是重點(diǎn)了,注冊(cè)了setTimeout,在 0ms 后將傳入的render函數(shù)放進(jìn)宏任務(wù)隊(duì)列里。然后這個(gè)foo函數(shù)就執(zhí)行結(jié)束了,事件循環(huán)繼續(xù)往下執(zhí)行,清空微任務(wù)隊(duì)列,渲染瀏覽器。等到下一個(gè)事件循環(huán)的時(shí)候,才會(huì)從宏任務(wù)隊(duì)列里拿出由setTimeout放入的render函數(shù)(如果是第一個(gè)的話)并執(zhí)行。

如上圖所示,當(dāng)前的事件循環(huán)正在執(zhí)行foo()函數(shù),此時(shí)render()在宏任務(wù)隊(duì)列中等待。

假設(shè)這次事件循環(huán)需要的時(shí)間是 10ms,那么到了 10ms 后,事件循環(huán)開始了新的一輪,從宏任務(wù)隊(duì)列里獲取一個(gè)新的宏任務(wù),獲取到了render()任務(wù)并執(zhí)行。來看render()函數(shù)里的代碼:

function render() {
  for (let index = 0; index < size; index++) {
    const e = document.createElement("div");
    e.textContent = "NEW";
    d.appendChild(e);
  }
  i++;
  if (i < chunk) {
    setTimeout(render, 0);
  }
}

代碼執(zhí)行了 for 循環(huán),添加size次數(shù)的子元素,在示例中size定義為了 250,添加 250 個(gè)子元素,數(shù)量不多,添加過程會(huì)非常快。在執(zhí)行完 for 循環(huán)后,將外部的i變量加 1,我們將使用i判斷所有的子元素是否添加完畢,如果是則結(jié)束函數(shù),如果不是,則再次通過setTimeout注冊(cè)一個(gè)render()函數(shù),然后結(jié)束當(dāng)前函數(shù)。

如上圖,在 15ms 的時(shí)候,render()函數(shù)添加了 250 個(gè)子元素,然后使用setTimeout注冊(cè)了一個(gè)新的宏任務(wù),在 0ms 后進(jìn)入宏任務(wù)隊(duì)列。注意此時(shí),盡管render()函數(shù)添加了 250 個(gè)子元素,但是事件循環(huán)還沒有到渲染瀏覽器這一步,所以頁面沒有出現(xiàn) 250 個(gè)新元素。

事件循環(huán)繼續(xù)執(zhí)行:

到了 15ms,執(zhí)行微任務(wù)隊(duì)列,假設(shè)需要執(zhí)行 5ms。到了 20 ms,清空了微任務(wù)隊(duì)列,開始渲染瀏覽器,假設(shè)渲染需要 5ms,界面上出現(xiàn)了 250 個(gè)新元素。這次,只花費(fèi)了 15ms,就讓頁面上渲染出了元素,而不是一開始那樣卡頓了 2000ms 后才頁面才渲染!

接下來的事件循環(huán)就是一直重復(fù) 10ms 開始到 25ms 的動(dòng)作了,直到所有子元素都渲染完畢。

通過改造后的foo()函數(shù),我們將卡頓的頁面優(yōu)化成了觀感良好順暢的頁面。從新舊foo()函數(shù)的代碼量來看,代碼數(shù)量的多少跟頁面順暢與否沒有太大關(guān)系。重點(diǎn)是理解事件循環(huán)中發(fā)生的事。

六、思考:劣質(zhì)的優(yōu)化

如果我將foo()函數(shù)改寫成如下的形式,會(huì)怎么樣,親自試一試,思考執(zhí)行的事件循環(huán)和宏任務(wù)隊(duì)列中發(fā)生了什么。

function foo() {
  const d = document.getElementById("container");
  const size = 1000;
  const chunk = 50000 / size;
  for (let index = 0; index < chunk; index++) {
    setTimeout(() => {
      const e = document.createElement("div");
      e.textContent = "NEW";
      d.appendChild(e);
    }, 0);
  }
}

到此這篇關(guān)于Javascript單線程和事件循環(huán)的文章就介紹到這了,更多相關(guān)JS單線程 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Bootstrap學(xué)習(xí)筆記之css組件(3)

    Bootstrap學(xué)習(xí)筆記之css組件(3)

    這篇文章主要為大家詳細(xì)介紹了bootstrap學(xué)習(xí)筆記中的css組件,感興趣的小伙伴們可以參考一下
    2016-06-06
  • JS返回頁面時(shí)自動(dòng)回滾到歷史瀏覽位置

    JS返回頁面時(shí)自動(dòng)回滾到歷史瀏覽位置

    這篇文章主要介紹了JS返回頁面時(shí)自動(dòng)回滾到歷史瀏覽位置的相關(guān)知識(shí),本文給大家使用的是SessionStorage來存儲(chǔ)頁面內(nèi)容,在返回頁面時(shí)重新加載,具體實(shí)現(xiàn)代碼大家跟隨小編一起看看吧
    2018-09-09
  • js定時(shí)器出現(xiàn)第一次延遲的原因及解決方法

    js定時(shí)器出現(xiàn)第一次延遲的原因及解決方法

    在本篇文章里小編給大家整理的是一篇關(guān)于js定時(shí)器出現(xiàn)第一次延遲的原因及解決方法,對(duì)此有需要的朋友們可以學(xué)習(xí)下。
    2021-01-01
  • IE中鼠標(biāo)經(jīng)過option觸發(fā)mouseout的解決方法

    IE中鼠標(biāo)經(jīng)過option觸發(fā)mouseout的解決方法

    這篇文章主要介紹了IE中鼠標(biāo)經(jīng)過option觸發(fā)mouseout的解決方法,分析了IE中鼠標(biāo)移到option時(shí)window.event.toElement返回值為null的原因及解決方法,需要的朋友可以參考下
    2015-01-01
  • javascript獲取當(dāng)前鼠標(biāo)坐標(biāo)的方法

    javascript獲取當(dāng)前鼠標(biāo)坐標(biāo)的方法

    這篇文章主要介紹了javascript獲取當(dāng)前鼠標(biāo)坐標(biāo)的方法,可針對(duì)不同瀏覽器獲取鼠標(biāo)的坐標(biāo)位置,是非常實(shí)用技巧,需要的朋友可以參考下
    2015-01-01
  • JavaScript中的eval()函數(shù)使用介紹

    JavaScript中的eval()函數(shù)使用介紹

    這篇文章主要介紹了JavaScript中的eval()函數(shù)使用介紹,本文講解了eval()的使用、eval()的返回值、變量環(huán)境(variable environment)等內(nèi)容,需要的朋友可以參考下
    2014-12-12
  • Bootstrap3多級(jí)下拉菜單

    Bootstrap3多級(jí)下拉菜單

    這篇文章主要為大家詳細(xì)介紹了Bootstrap3多級(jí)下拉菜單的相關(guān)資料,需引用bootstrap.min.css和bootstrap.min.css.js,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-02-02
  • 基于JavaScript Array數(shù)組方法(新手必看篇)

    基于JavaScript Array數(shù)組方法(新手必看篇)

    下面小編就為大家?guī)硪黄贘avaScript Array數(shù)組方法(新手必看篇)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2016-08-08
  • 前端性能優(yōu)化及技巧

    前端性能優(yōu)化及技巧

    這篇文章主要介紹了前端性能優(yōu)化及技巧,需要的朋友可以參考下
    2016-05-05
  • js父頁面中使用子頁面的方法

    js父頁面中使用子頁面的方法

    這篇文章主要向大家介紹了js父頁面中使用子頁面的方法,即js父頁面使用iframe中的函數(shù),感興趣的朋友可以參考一下
    2016-01-01

最新評(píng)論