您好,登錄后才能下訂單哦!
本篇內容介紹了“Javascript單線程和事件循環實例分析”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
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(); } }
一個線程將執行 changeValue()
函數,如果元素存在就修改元素的值;一個線程將執行 deleteElement()
函數,如果元素存在就刪除元素。此時在多線程的條件下,兩個函數同時執行,線程 1 執行,判斷元素存在,準備執行修改值的代碼 e.value = "VALUE";
,此時線程 2 搶占了 CPU,執行了 deleteElement()
函數,完整的執行結束,成功刪除了元素,CPU 的控制權回到了線程 1,線程 1 繼續執行剩下的代碼,也就是將要執行的 e.value = "VALUE";
,然而因為這個元素被線程 2 刪除了,獲取不到元素,修改元素的值失敗!
能夠發現,瀏覽器環境下,不管有幾個線程,都是共享同一個文檔(Document),對 DOM 的頻繁操作,多線程將帶來極大的不穩定性。如果是單線程,則能夠保證對 DOM 的操作是極其穩定和可預見的。你永遠不用擔心有別的線程搶占了資源,做了什么操作而影響到原來的線程。
由于單線程,JS 一次只能處理一個任務,在該任務處理完成之前,其他任務必須等待。這一點非常重要,在理解下面的事件循環前,首先得明確這個概念。
如你所見,因為瀏覽器執行 Javascript
是單線程,所以一次只能夠執行一個任務。那么當出現多個要執行的任務,其他尚未執行的任務在什么地方等待呢?
為了能夠讓任務有個可以等待執行的地方,瀏覽器就建立了一個隊列,所有的任務都在隊列里等待,當要執行任務的時候,就從隊列的隊頭里拿一個任務來執行,執行過程中,其他任務繼續等待。當任務執行完之后,再從隊列里拿下一個任務來執行。
可是,除了開發者編寫的 Javascript
代碼之外,還有很多事件發生,比如瀏覽器的點擊事件,鼠標移動事件,鍵盤事件,網絡請求等。這些事件也需要執行,而且為了客戶體驗的流暢,需要盡快執行,以更新頁面。我們的隊列可能有很多任務正在等待執行,如果把瀏覽器發生的事件排入隊列的隊尾,那么在前面的任務執行完成之前,瀏覽器的頁面將一直堵塞住,在用戶看在,將是非常卡頓的。
為了應對這種問題,瀏覽器就多加了一個隊列,這個隊列中的任務,將被盡快執行。為了和前一個隊列做區分,前面一個隊列就叫宏任務隊列吧,這個新加的隊列就叫微任務隊列吧。宏任務隊列的任務叫宏任務,微任務隊列里的任務叫微任務。
宏任務隊列的執行方式仍不變,還是一次拿一個宏任務來執行。但是在執行完一個宏任務后,就變了,不檢查宏任務隊列是否為空,而是檢查微任務隊列是否為空! 如果微任務隊列不為空,就執行一個微任務,當前微任務執行完成后,繼續檢查微任務隊列是否為空,如果微任務隊列不為空,就再執行一個微任務,直到微任務隊列為空。當微任務隊列為空后,就渲染瀏覽器,回到宏任務隊列執行,如此循環往復。
通過這種模型,瀏覽器將需要快速響應的 DOM 事件放入微任務隊列,以達到快速執行的目的。當微任務隊列執行完成后,便按需要重新渲染瀏覽器,用戶就會感覺自己的操作被迅速地響應了。
這種事件執行方式,稱為事件循環。瀏覽器中的事件和代碼,就在事件循環模型下執行。
通過上圖的事件循環模型,我們得知瀏覽器渲染的順序,是在執行了一個宏任務和剩下的所有微任務之后,那么為了保證瀏覽器的渲染順暢,我們不宜讓每一個宏任務的執行事件太長,也不能讓清空微任務隊列太耗時。一次事件循環中,只執行一個宏任務,那么,對耗時的宏任務需要分解成盡可能小的宏任務,微任務卻不同。由于微任務是清空整個微任務隊列,所以,在微任務里不要生成新的微任務。畢竟微任務隊列的使命就是為了盡可能先處理微任務,然后重新渲染瀏覽器。
宏任務隊列和微任務隊列這兩者,都是獨立于事件循環的,也就是說,在執行 Javascript
代碼時,任務隊列的添加行為也在發生,即使現在正在清空微任務隊列。這是為了避免在執行代碼時,發生的事件被忽略。如此可知,即使我們分解一個耗時任務,也不能因為微任務會被優先執行就選擇將它分解成多個微任務,這將阻塞瀏覽器重新渲染。更好的做法是分解成多個宏任務,這樣執行一個分解后的宏任務不會太耗時,可以盡快達到讓瀏覽器渲染。
在瀏覽器的渲染之前,會清空微任務隊列,所以,對瀏覽器 DOM 的修改更新,就適合放到微任務里去執行。
瀏覽器渲染的次數大概是每秒 60 次,約等于 16ms 一次。在瀏覽器渲染頁面的時候,任何任務都無法再對頁面進行修改,這意味著,為了頁面的平滑順暢,我們的代碼,單個宏任務和當前微任務隊列里所有微任務,都應該在 16ms 內執行完成。否則就會造成頁面卡頓。
我會用一些簡單卻有效的代碼來說明事件循環如何影響頁面效果,以下的代碼很少,建議你一起編寫,體驗一下。
先看下面的代碼,我定義了一個 foo()
函數,它將一次性往元素中添加 5 萬個子元素,我將在頁面加載完成后立即執行它。
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); } }
可見這是一個耗時的操作,如果你電腦很好,體驗不到卡頓的話,可以換成循環 50 萬次。
在一陣時間的卡頓后,頁面一次性出現了大量子元素。雖說添加元素的目的達到了,但是元素出現之前的卡頓卻不能忍受。根據事件循環,我們能夠知道,是因為執行了一個非常耗時的宏任務,導致阻塞了頁面的渲染。用下面一張圖說明。
上面這張圖代表著本次事件循環的執行,一開始,瀏覽器就將 foo()
放進宏任務隊列。從 0ms 開始,宏任務隊列里有任務,事件循環取出一個宏任務,該宏任務為 foo()
,執行,添加 5 萬個子元素,執行非常耗時,需要 2000ms(假設的時間),foo()
執行完后,執行微任務,假設我們的清空微任務隊列需要執行 5ms,清空后,時間來到了 2005ms,這個時候才能開始重新渲染瀏覽器。經過了這一次事件循環,竟然耗時了 2015ms!
那么,我們要改善體驗,期望是一個平滑的渲染效果。因為瀏覽器頁面的變化,只有在事件循環中重新渲染瀏覽器這一步才會發生變化,所以我們要做的就是,盡可能快地到事件循環中的渲染瀏覽器這一步。所以,我們要將這個 foo()
分解成多個宏任務。
為什么不能分解成微任務?因為微任務會在宏任務完成后全部執行。假設我們將添加 5 萬 個元素分解成宏任務添加 1000 個,微任務添加 49000 個,那么事件循環還是必須執行完添加 1000 個元素的宏任務后,執行添加 49000 個元素的微任務,才能渲染頁面。所以我們要分解成宏任務。
假設我們分解成了 200 個宏任務,每個宏任務都添加 250 個元素,那么,在事件循環執行的時候,任務隊列里有 200 個宏任務,取出一個執行,這個宏任務只添加 250 個元素,耗時 10ms。當前宏任務完成后,便清空微任務,耗時 5ms,時間來到了 15ms,就可以渲染瀏覽器了。這一次事件循環,在渲染瀏覽器前只耗時 15ms!
接著,渲染瀏覽器后,頁面上出現了 250 個元素,又開始事件循環,從宏任務隊列里拿出一個宏任務執行。
如上圖所示,接連不斷的事件循環使瀏覽器渲染看起來平滑順暢。
接下來我們便改造我們的代碼,讓它分解成多個宏任務。
setTimeout()
函數,用于將一個函數延遲執行,是我們的重點方法。
你應該很熟悉這個函數的用法了,setTimeout()
接收兩個參數,第一個是一個回調函數,第二個是數字,用于指示延遲多少時間,以毫秒為單位(ms)。
這里主要介紹的是第二個參數,很多人以為第二個參數是指延遲多少毫秒后執行傳進來的函數,但其實,它的真正含義是:延遲多少毫秒后進入宏任務隊列!
假設如下代碼:
setTimeout(() => { console.log("execute setTimeout()"); }, 10);
下面我用一張圖說明這段代碼的執行,圖中,上方代表時間軸,下方代表宏任務隊列。
在 0ms 時,注冊 setTimeout
函數,第一個參數里的方法將在 10ms 后加入宏任務隊列,此時,宏任務時沒有我們代碼里的任務的。
其他我們不知道的 JS 代碼執行了 10 ms。
到了 10ms 后,setTimeout
到期,第一個參數里的方法加入宏任務隊列。
上圖中,10ms 到了,加入了宏任務隊列。但是要注意,事件循環此時可能正在執行一個宏任務,或者正在清空微任務隊列,或者正在渲染瀏覽器,所以不會馬上執行新增加的宏任務,只有又一次循環到了執行宏任務的時候,才會從宏任務隊列中獲取宏任務執行(JS 是單線程的)。假設這段時間耗時了 5ms,那么如下圖。
如上圖所示,在 15ms 的時候,我們才從宏任務隊列里取出在 10ms 時放入宏任務隊列的宏任務,并執行。和我們的代碼對比,盡管 setTimeout
的第二個參數是 10ms,卻在 15ms 才執行。
當理解了 setTimeout
的原理之后,便可以使用 setTimeout
將一個耗時的任務分解成多個宏任務,以充分給予瀏覽器渲染。
我修改了 foo
函數,如下所示:
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
是用于標記執行到了第幾個宏任務了。
接下來就是重點了,注冊了 setTimeout
,在 0ms 后將傳入的 render
函數放進宏任務隊列里。然后這個 foo
函數就執行結束了,事件循環繼續往下執行,清空微任務隊列,渲染瀏覽器。等到下一個事件循環的時候,才會從宏任務隊列里拿出由 setTimeout
放入的 render
函數(如果是第一個的話)并執行。
如上圖所示,當前的事件循環正在執行 foo()
函數,此時 render()
在宏任務隊列中等待。
假設這次事件循環需要的時間是 10ms,那么到了 10ms 后,事件循環開始了新的一輪,從宏任務隊列里獲取一個新的宏任務,獲取到了 render()
任務并執行。來看 render()
函數里的代碼:
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); } }
代碼執行了 for 循環,添加 size
次數的子元素,在示例中 size
定義為了 250,添加 250 個子元素,數量不多,添加過程會非常快。在執行完 for 循環后,將外部的 i
變量加 1,我們將使用 i
判斷所有的子元素是否添加完畢,如果是則結束函數,如果不是,則再次通過 setTimeout
注冊一個 render()
函數,然后結束當前函數。
如上圖,在 15ms 的時候,render()
函數添加了 250 個子元素,然后使用 setTimeout
注冊了一個新的宏任務,在 0ms 后進入宏任務隊列。注意此時,盡管 render()
函數添加了 250 個子元素,但是事件循環還沒有到渲染瀏覽器這一步,所以頁面沒有出現 250 個新元素。
事件循環繼續執行:
到了 15ms,執行微任務隊列,假設需要執行 5ms。到了 20 ms,清空了微任務隊列,開始渲染瀏覽器,假設渲染需要 5ms,界面上出現了 250 個新元素。這次,只花費了 15ms,就讓頁面上渲染出了元素,而不是一開始那樣卡頓了 2000ms 后才頁面才渲染!
接下來的事件循環就是一直重復 10ms 開始到 25ms 的動作了,直到所有子元素都渲染完畢。
通過改造后的 foo()
函數,我們將卡頓的頁面優化成了觀感良好順暢的頁面。從新舊 foo()
函數的代碼量來看,代碼數量的多少跟頁面順暢與否沒有太大關系。重點是理解事件循環中發生的事。
如果我將 foo()
函數改寫成如下的形式,會怎么樣,親自試一試,思考執行的事件循環和宏任務隊列中發生了什么。
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單線程和事件循環實例分析”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。