亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

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

一、單線程

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í)行。

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

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

宏任務隊列的執(zhí)行方式仍不變,還是一次拿一個宏任務來執(zhí)行。但是在執(zhí)行完一個宏任務后,就變了,不檢查宏任務隊列是否為空,而是檢查微任務隊列是否為空! 如果微任務隊列不為空,就執(zhí)行一個微任務,當前微任務執(zhí)行完成后,繼續(xù)檢查微任務隊列是否為空,如果微任務隊列不為空,就再執(zhí)行一個微任務,直到微任務隊列為空。當微任務隊列為空后,就渲染瀏覽器,回到宏任務隊列執(zhí)行,如此循環(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í)行了一個非常耗時的宏任務,導致阻塞了頁面的渲染。用下面一張圖說明。

上面這張圖代表著本次事件循環(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í)行。

如上圖所示,接連不斷的事件循環(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í)行,圖中,上方代表時間軸,下方代表宏任務隊列。

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

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

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

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

如上圖所示,在 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í)行。

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

假設這次事件循環(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ù)。

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

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

到了 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ā)生的事。

六、思考:劣質(zhì)的優(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)的文章就介紹到這了,更多相關JS單線程 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • Bootstrap學習筆記之css組件(3)

    Bootstrap學習筆記之css組件(3)

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

    JS返回頁面時自動回滾到歷史瀏覽位置

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

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

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

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

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

    javascript獲取當前鼠標坐標的方法

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

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

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

    Bootstrap3多級下拉菜單

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

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

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

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

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

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

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

最新評論