溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Javascript單線程和事件循環(huán)實例分析

發(fā)布時間:2022-06-08 09:23:42 來源:億速云 閱讀:146 作者:zzz 欄目:開發(fā)技術

本篇內(nèi)容介紹了“Javascript單線程和事件循環(huán)實例分析”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

一、單線程

Javascript 是單線程的,意味著不會有其他線程來競爭。為什么是單線程呢?

假設 Javascript 是多線程的,有兩個線程,分別對同一個元素進行操作:

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

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

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

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

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

二、事件循環(huán)

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

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

Javascript單線程和事件循環(huán)實例分析

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

Javascript單線程和事件循環(huán)實例分析

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

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

Javascript單線程和事件循環(huán)實例分析

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

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

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

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

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

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

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

四、使用代碼來說明

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

先看下面的代碼,我定義了一個 foo() 函數(shù),它將一次性往元素中添加 5 萬個子元素,我將在頁面加載完成后立即執(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);
  }
}

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

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

Javascript單線程和事件循環(huán)實例分析

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

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

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

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

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

Javascript單線程和事件循環(huán)實例分析

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

接下來我們便改造我們的代碼,讓它分解成多個宏任務。

五、setTimeout()

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

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

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

假設如下代碼:

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

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

Javascript單線程和事件循環(huán)實例分析

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

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

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

Javascript單線程和事件循環(huán)實例分析

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

Javascript單線程和事件循環(huán)實例分析

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

當理解了 setTimeout 的原理之后,便可以使用 setTimeout 將一個耗時的任務分解成多個宏任務,以充分給予瀏覽器渲染。

我修改了 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 表示一共有幾個元素要添加,因為我電腦性能差,所以是 5 萬,你可以修改成你喜歡的值;size 是指我們分解后每個宏任務要添加幾次元素;chunk 是指分解后,一共有幾個宏任務,通過簡單的計算得到;i 是用于標記執(zhí)行到了第幾個宏任務了。

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

Javascript單線程和事件循環(huán)實例分析

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

Javascript單線程和事件循環(huán)實例分析

假設這次事件循環(huán)需要的時間是 10ms,那么到了 10ms 后,事件循環(huán)開始了新的一輪,從宏任務隊列里獲取一個新的宏任務,獲取到了 render() 任務并執(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 個子元素,數(shù)量不多,添加過程會非???。在執(zhí)行完 for 循環(huán)后,將外部的 i 變量加 1,我們將使用 i 判斷所有的子元素是否添加完畢,如果是則結束函數(shù),如果不是,則再次通過 setTimeout 注冊一個 render() 函數(shù),然后結束當前函數(shù)。

Javascript單線程和事件循環(huán)實例分析

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

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

Javascript單線程和事件循環(huán)實例分析

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

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

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

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

如果我將 foo() 函數(shù)改寫成如下的形式,會怎么樣,親自試一試,思考執(zhí)行的事件循環(huán)和宏任務隊列中發(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);
  }
}

“Javascript單線程和事件循環(huán)實例分析”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關的知識可以關注億速云網(wǎng)站,小編將為大家輸出更多高質量的實用文章!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI