JS異步編程異步編程的方法和原理,進程和線程的區別。JS是單線程的,為何能讓ajax異步

Javascript語言的執行環境是"單線程"(single thread)

所謂"單線程",就是指一次只能完成一件任務。如果有多個任務,就必須排隊,前面一個任務完成,再執行后面一個任務,以此類推。

這種模式的好處是實現起來比較簡單,執行環境相對單純;壞處是只要有一個任務耗時很長,后面的任務都必須排隊等著,會拖延整個程序的執行。常見的瀏覽器無響應(假死),往往就是因為某一段Javascript代碼長時間運行(比如死循環),導致整個頁面卡在這個地方,其他任務無法執行。

為了解決這個問題,Javascript語言將任務的執行模式分成兩種:同步(Synchronous)和異步(Asynchronous)

"同步模式"就是上一段的模式,后一個任務等待前一個任務結束,然后再執行,程序的執行順序與任務的排列順序是一致的、同步的;

"異步模式"則完全不同,每一個任務有一個或多個回調函數(callback),前一個任務結束后,不是執行后一個任務,而是執行回調函數,后一個任務則是不等前一個任務結束就執行,所以程序的執行順序與任務的排列順序是不一致的、異步的。

"異步模式"非常重要。在瀏覽器端,耗時很長的操作都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操作。在服務器端,"異步模式"甚至是唯一的模式,因為執行環境是單線程的,如果允許同步執行所有http請求,服務器性能會急劇下降,很快就會失去響應。

本文總結了"異步模式"編程的4種方法,理解它們可以讓你寫出結構更合理、性能更出色、維護更方便的Javascript程序。

一、回調函數

這是異步編程最基本的方法。

假定有兩個函數f1和f2,后者等待前者的執行結果。

  f1();

  f2();

如果f1是一個很耗時的任務,可以考慮改寫f1,把f2寫成f1的回調函數。

  function f1(callback){

    setTimeout(function () {

      // f1的任務代碼

      callback();

    }, 1000);

  }

執行代碼就變成下面這樣:

  f1(f2);

采用這種方式,我們把同步操作變成了異步操作,f1不會堵塞程序運行,相當于先執行程序的主要邏輯,將耗時的操作推遲執行。

回調函數的優點是簡單、容易理解和部署,缺點是不利于代碼的閱讀和維護,各個部分之間高度耦合(Coupling),流程會很混亂,而且每個任務只能指定一個回調函數。

二、事件監聽

另一種思路是采用事件驅動模式。任務的執行不取決于代碼的順序,而取決于某個事件是否發生。

還是以f1和f2為例。首先,為f1綁定一個事件(這里采用的jQuery的寫法)。

  f1.on('done', f2);

上面這行代碼的意思是,當f1發生done事件,就執行f2。然后,對f1進行改寫:

  function f1(){

    setTimeout(function () {

      // f1的任務代碼

f1.trigger('done');

    }, 1000);

  }

f1.trigger('done')表示,執行完成后,立即觸發done事件,從而開始執行f2。

這種方法的優點是比較容易理解,可以綁定多個事件,每個事件可以指定多個回調函數,而且可以"去耦合"(Decoupling),有利于實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。

三、發布/訂閱

上一節的"事件",完全可以理解成"信號"。

我們假定,存在一個"信號中心",某個任務執行完成,就向信號中心"發布"(publish)一個信號,其他任務可以向信號中心"訂閱"(subscribe)這個信號,從而知道什么時候自己可以開始執行。這就叫做"發布/訂閱模式"(publish-subscribe pattern),又稱"觀察者模式"(observer pattern)。

這個模式有多種實現,下面采用的是Ben Alman的Tiny Pub/Sub,這是jQuery的一個插件。

首先,f2向"信號中心"jQuery訂閱"done"信號。

  jQuery.subscribe("done", f2);

然后,f1進行如下改寫:

  function f1(){

    setTimeout(function () {

      // f1的任務代碼

jQuery.publish("done");

    }, 1000);

  }

jQuery.publish("done")的意思是,f1執行完成后,向"信號中心"jQuery發布"done"信號,從而引發f2的執行。

此外,f2完成執行后,也可以取消訂閱(unsubscribe)。

  jQuery.unsubscribe("done", f2);

這種方法的性質與"事件監聽"類似,但是明顯優于后者。因為我們可以通過查看"消息中心",了解存在多少信號、每個信號有多少訂閱者,從而監控程序的運行。

四、Promises對象

Promises對象是CommonJS工作組提出的一種規范,目的是為異步編程提供統一接口

簡單說,它的思想是,每一個異步任務返回一個Promise對象,該對象有一個then方法,允許指定回調函數。比如,f1的回調函數f2,可以寫成:

  f1().then(f2);

f1要進行如下改寫(這里使用的是jQuery的實現):

  function f1(){

    var dfd = $.Deferred();

    setTimeout(function () {

      // f1的任務代碼

      dfd.resolve();

    }, 500);

return dfd.promise;

  }

這樣寫的優點在于,回調函數變成了鏈式寫法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以實現許多強大的功能。

比如,指定多個回調函數:

  f1().then(f2).then(f3);

再比如,指定發生錯誤時的回調函數:

  f1().then(f2).fail(f3);

而且,它還有一個前面三種方法都沒有的好處:如果一個任務已經完成,再添加回調函數,該回調函數會立即執行。所以,你不用擔心是否錯過了某個事件或信號。這種方法的缺點就是編寫和理解,都相對比較難。

進程和線程

線程和進程區分不清,是很多新手都會犯的錯誤,沒有關系。這很正常。先看看下面這個形象的比喻:

- 進程是一個工廠,工廠有它的獨立資源

- 工廠之間相互獨立

- 線程是工廠中的工人,多個工人協作完成任務

- 工廠內有一個或多個工人

- 工人之間共享空間

再完善完善概念:

- 工廠的資源 -> 系統分配的內存(獨立的一塊內存)

- 工廠之間的相互獨立 -> 進程之間相互獨立

- 多個工人協作完成任務 -> 多個線程在進程中協作完成任務

- 工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成

- 工人之間共享空間 -> 同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)

進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)

線程是cpu調度的最小單位(線程是建立在進程的基礎上的一次程序運行單位,一個進程中可以有多個線程)

對瀏覽器進行一定程度上的認識:(先看下簡化理解)

瀏覽器是多進程的

瀏覽器之所以能夠運行,是因為系統給它的進程分配了資源(cpu、內存)

簡單點理解,每打開一個Tab頁,就相當于創建了一個獨立的瀏覽器進程。

瀏覽器多進程的優勢

相比于單進程瀏覽器,多進程有如下優點:

避免單個page crash影響整個瀏覽器

避免第三方插件crash影響整個瀏覽器

多進程充分利用多核優勢

方便使用沙盒模型隔離插件等進程,提高瀏覽器穩定性

簡單點理解:如果瀏覽器是單進程,那么某個Tab頁崩潰了,就影響了整個瀏覽器,體驗有多差;同理如果是單進程,插件崩潰了也會影響整個瀏覽器;而且多進程還有其它的諸多優勢。。。

當然,內存等資源消耗也會更大,有點空間換時間的意思。

重點是瀏覽器內核(渲染進程)

重點來了,我們可以看到,上面提到了這么多的進程,那么,對于普通的前端操作來說,最終要的是什么呢?答案是渲染進程

可以這樣理解,頁面的渲染,JS的執行,事件的循環,都在這個進程內進行。接下來重點分析這個進程

請牢記,瀏覽器的渲染進程是多線程的(這點如果不理解,請回頭看進程和線程的區分

終于到了線程這個概念了?,好親切。那么接下來看看它都包含了哪些線程(列舉一些主要常駐線程):

1.GUI渲染線程

負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,布局和繪制等。

當界面需要重繪(Repaint)或由于某種操作引發回流(reflow)時,該線程就會執行

注意,GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(相當于被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閑時立即被執行。

2.JS引擎線程

也稱為JS內核,負責處理Javascript腳本程序。(例如V8引擎)

JS引擎線程負責解析Javascript腳本,運行代碼。

JS引擎一直等待著任務隊列中任務的到來,然后加以處理,瀏覽器無論什么時候都只有一個JS線程在運行JS程序

同樣注意,GUI渲染線程與JS引擎線程是互斥的,所以如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞。

3.事件觸發線程

歸屬于瀏覽器而不是JS引擎,用來控制事件循環(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開線程協助)

當JS引擎執行代碼塊如setTimeOut時(也可來自瀏覽器內核的其他線程,如鼠標點擊、AJAX異步請求等),會將對應任務添加到事件線程中

當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理

注意,由于JS的單線程關系,所以這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閑時才會去執行)

4.定時觸發器線程

傳說中的setInternal與setTimeout所在線程

瀏覽器定時計數器并不是由JavaScript引擎計數的,(因為JavaScript引擎是單線程的, 如果處于阻塞線程狀態就會影響記計時的準確)

因此通過單獨線程來計時并觸發定時(計時完畢后,添加到事件隊列中,等待JS引擎空閑后執行)

注意,W3C在HTML標準中規定,規定要求setTimeout中低于4ms的時間間隔算為4ms。

5.異步http請求線程

在XMLHttpRequest在連接后是通過瀏覽器新開一個線程請求

將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件,將這個回調再放入事件隊列中。再由JavaScript引擎執行。

看到這里,如果覺得累了,可以先休息下,這些概念需要被消化,畢竟后續將提到的事件循環機制就是基于事件觸發線程的,所以如果僅僅是看某個碎片化知識, 可能會有一種似懂非懂的感覺。要完成的梳理一遍才能快速沉淀,不易遺忘。放張圖鞏固下吧:


再說一點,為什么JS引擎是單線程的?額,這個問題其實應該沒有標準答案,譬如,可能僅僅是因為由于多線程的復雜性,譬如多線程操作一般要加鎖,因此最初設計時選擇了單線程。。。

Browser進程和瀏覽器內核(Renderer進程)的通信過程

看到這里,首先,應該對瀏覽器內的進程和線程都有一定理解了,那么接下來,再談談瀏覽器的Browser進程(控制進程)是如何和內核通信的, 這點也理解后,就可以將這部分的知識串聯起來,從頭到尾有一個完整的概念。

如果自己打開任務管理器,然后打開一個瀏覽器,就可以看到:任務管理器中出現了兩個進程(一個是主控進程,一個則是打開Tab頁的渲染進程), 然后在這前提下,看下整個的過程:(簡化了很多)

Browser進程收到用戶請求,首先需要獲取頁面內容(譬如通過網絡下載資源),隨后將該任務通過RendererHost接口傳遞給Render進程

Renderer進程的Renderer接口收到消息,簡單解釋后,交給渲染線程,然后開始渲染

渲染線程接收請求,加載網頁并渲染網頁,這其中可能需要Browser進程獲取資源和需要GPU進程來幫助渲染

當然可能會有JS線程操作DOM(這樣可能會造成回流并重繪)

最后Render進程將結果傳遞給Browser進程

Browser進程接收到結果并將結果繪制出來

這里繪一張簡單的圖:(很簡化)

看完這一整套流程,應該對瀏覽器的運作有了一定理解了,這樣有了知識架構的基礎后,后續就方便往上填充內容。

這塊再往深處講的話就涉及到瀏覽器內核源碼解析了,不屬于本文范圍。

如果這一塊要深挖,建議去讀一些瀏覽器內核源碼解析文章,或者可以先看看參考下來源中的第一篇文章,寫的不錯

梳理瀏覽器內核中線程之間的關系

到了這里,已經對瀏覽器的運行有了一個整體的概念,接下來,先簡單梳理一些概念

GUI渲染線程與JS引擎線程互斥

由于JavaScript是可操縱DOM的,如果在修改這些元素屬性同時渲染界面(即JS線程和UI線程同時運行),那么渲染線程前后獲得的元素數據就可能不一致了。

因此為了防止渲染出現不可預期的結果,瀏覽器設置GUI渲染線程與JS引擎為互斥的關系,當JS引擎執行時GUI線程會被掛起, GUI更新則會被保存在一個隊列中等到JS引擎線程空閑時立即被執行。

JS阻塞頁面加載

從上述的互斥關系,可以推導出,JS如果執行時間過長就會阻塞頁面。

譬如,假設JS引擎正在進行巨量的計算,此時就算GUI有更新,也會被保存到隊列中,等待JS引擎空閑后執行。 然后,由于巨量計算,所以JS引擎很可能很久很久后才能空閑,自然會感覺到巨卡無比。

所以,要盡量避免JS執行時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞的感覺。

WebWorker,JS的多線程?

前文中有提到JS引擎是單線程的,而且JS執行時間過長會阻塞頁面,那么JS就真的對cpu密集型計算無能為力么?

所以,后來HTML5中支持了Web?Worker。

MDN的官方解釋是:

Web Worker為Web內容在后臺線程中運行腳本提供了一種簡單的方法。線程可以執行任務而不干擾用戶界面

一個worker是使用一個構造函數創建的一個對象(e.g. Worker()) 運行一個命名的JavaScript文件

這個文件包含將在工作線程中運行的代碼; workers 運行在另一個全局上下文中,不同于當前的window

因此,使用 window快捷方式獲取當前全局的范圍 (而不是self) 在一個 Worker 內將返回錯誤

這樣理解下:

創建Worker時,JS引擎向瀏覽器申請開一個子線程(子線程是瀏覽器開的,完全受主線程控制,而且不能操作DOM)

JS引擎線程與worker線程間通過特定的方式通信(postMessage API,需要通過序列化對象來與線程交互特定的數據)

所以,如果有非常耗時的工作,請單獨開一個Worker線程,這樣里面不管如何翻天覆地都不會影響JS引擎主線程, 只待計算出結果后,將結果通信給主線程即可,perfect!

而且注意下,JS引擎是單線程的,這一點的本質仍然未改變,Worker可以理解是瀏覽器給JS引擎開的外掛,專門用來解決那些大量計算問題。

其它,關于Worker的詳解就不是本文的范疇了,因此不再贅述。

WebWorker與SharedWorker

既然都到了這里,就再提一下SharedWorker(避免后續將這兩個概念搞混)

WebWorker只屬于某個頁面,不會和其他頁面的Render進程(瀏覽器內核進程)共享

所以Chrome在Render進程中(每一個Tab頁就是一個render進程)創建一個新的線程來運行Worker中的JavaScript程序。

SharedWorker是瀏覽器所有頁面共享的,不能采用與Worker同樣的方式實現,因為它不隸屬于某個Render進程,可以為多個Render進程共享使用

所以Chrome瀏覽器為SharedWorker單獨創建一個進程來運行JavaScript程序,在瀏覽器中每個相同的JavaScript只存在一個SharedWorker進程,不管它被創建多少次。

看到這里,應該就很容易明白了,本質上就是進程和線程的區別。SharedWorker由獨立的進程管理,WebWorker只是屬于render進程下的一個線程。

簡單梳理下瀏覽器渲染流程

本來是直接計劃開始談JS運行機制的,但想了想,既然上述都一直在談瀏覽器,直接跳到JS可能再突兀,因此,中間再補充下瀏覽器的渲染流程(簡單版本)

為了簡化理解,前期工作直接省略成:(要展開的或完全可以寫另一篇超長文)

- 瀏覽器輸入url,瀏覽器主進程接管,開一個下載線程,

然后進行 http請求(略去DNS查詢,IP尋址等等操作),然后等待響應,獲取內容,

隨后將內容通過RendererHost接口轉交給Renderer進程

- 瀏覽器渲染流程開始

瀏覽器器內核拿到內容后,渲染大概可以劃分成以下幾個步驟:

解析html建立dom樹

解析css構建render樹(將CSS代碼解析成樹形的數據結構,然后結合DOM合并成render樹)

布局render樹(Layout/reflow),負責各元素尺寸、位置的計算

繪制render樹(paint),繪制頁面像素信息

瀏覽器會將各層的信息發送給GPU,GPU會將各層合成(composite),顯示在屏幕上。

所有詳細步驟都已經略去,渲染完畢后就是load事件了,之后就是自己的JS邏輯處理了

既然略去了一些詳細的步驟,那么就提一些可能需要注意的細節把。

load事件與DOMContentLoaded事件的先后

上面提到,渲染完畢后會觸發load事件,那么你能分清楚load事件與DOMContentLoaded事件的先后么?

很簡單,知道它們的定義就可以了:

當 DOMContentLoaded 事件觸發時,僅當DOM加載完成,不包括樣式表,圖片。

(譬如如果有async加載的腳本就不一定完成)

當 onload 事件觸發時,頁面上所有的DOM,樣式表,腳本,圖片都已經加載完成了。

(渲染完畢了)

所以,順序是:DOMContentLoaded?->?load

css加載是否會阻塞dom樹渲染?

這里說的是頭部引入css的情況

首先,我們都知道:css是由單獨的下載線程異步下載的。

然后再說下幾個現象:

css加載不會阻塞DOM樹解析(異步加載時DOM照常構建)

但會阻塞render樹渲染(渲染時需等css加載完畢,因為render樹需要css信息)

這可能也是瀏覽器的一種優化機制。

因為你加載css的時候,可能會修改下面DOM節點的樣式,

如果css加載不阻塞render樹渲染的話,那么當css加載完之后, render樹可能又得重新重繪或者回流了,這就造成了一些沒有必要的損耗。

所以干脆就先把DOM樹的結構先解析完,把可以做的工作做完,然后等你css加載完之后,

在根據最終的樣式來渲染render樹,這種做法性能方面確實會比較好一點。

普通圖層和復合圖層

渲染步驟中就提到了composite概念。

可以簡單的這樣理解,瀏覽器渲染的圖層一般包含兩大類:普通圖層以及復合圖層

首先,普通文檔流內可以理解為一個復合圖層(這里稱為默認復合層,里面不管添加多少元素,其實都是在同一個復合圖層中)

其次,absolute布局(fixed也一樣),雖然可以脫離普通文檔流,但它仍然屬于默認復合層。

然后,可以通過硬件加速的方式,聲明一個新的復合圖層,它會單獨分配資源 (當然也會脫離普通文檔流,這樣一來,不管這個復合圖層中怎么變化,也不會影響默認復合層里的回流重繪)

可以簡單理解下:GPU中,各個復合圖層是單獨繪制的,所以互不影響,這也是為什么某些場景硬件加速效果一級棒

可以Chrome源碼調試?->?More?Tools?->?Rendering?->?Layerborders中看到,黃色的就是復合圖層信息

如下圖。可以驗證上述的說法

如何變成復合圖層(硬件加速)

將該元素變成一個復合圖層,就是傳說中的硬件加速技術

最常用的方式:translate3d、translateZ

opacity屬性/過渡動畫(需要動畫執行的過程中才會創建合成層,動畫沒有開始或結束后元素還會回到之前的狀態)

will-chang屬性(這個比較偏僻),一般配合opacity與translate使用(而且經測試,除了上述可以引發硬件加速的屬性外,其它屬性并不會變成復合層),

作用是提前告訴瀏覽器要變化,這樣瀏覽器會開始做一些優化工作(這個最好用完后就釋放)

其它,譬如以前的flash插件

absolute和硬件加速的區別

可以看到,absolute雖然可以脫離普通文檔流,但是無法脫離默認復合層。

所以,就算absolute中信息改變時不會改變普通文檔流中render樹,

但是,瀏覽器最終繪制時,是整個復合層繪制的,所以absolute中信息的改變,仍然會影響整個復合層的繪制。

(瀏覽器會重繪它,如果復合層中內容多,absolute帶來的繪制信息變化過大,資源消耗是非常嚴重的)

而硬件加速直接就是在另一個復合層了(另起爐灶),所以它的信息改變不會影響默認復合層 (當然了,內部肯定會影響屬于自己的復合層),僅僅是引發最后的合成(輸出視圖)

復合圖層的作用?

一般一個元素開啟硬件加速后會變成復合圖層,可以獨立于普通文檔流中,改動后可以避免整個頁面重繪,提升性能

但是盡量不要大量使用復合圖層,否則由于資源消耗過度,頁面反而會變的更卡

硬件加速時請使用index

使用硬件加速時,盡可能的使用index,防止瀏覽器默認給后續的元素創建復合層渲染

具體的原理時這樣的:webkit CSS3中,如果這個元素添加了硬件加速,并且index層級比較低, 那么在這個元素的后面其它元素(層級比這個元素高的,或者相同的,并且releative或absolute屬性相同的), 會默認變為復合層渲染,如果處理不當會極大的影響性能

簡單點理解,其實可以認為是一個隱式合成的概念:如果a是一個復合圖層,而且b在a上面,那么b也會被隱式轉為一個復合圖層,這點需要特別注意

另外,這個問題可以在這個地址看到重現(原作者分析的挺到位的,直接上鏈接):

http://web.jobbole.com/83575/

從Event Loop談JS的運行機制

到此時,已經是屬于瀏覽器頁面初次渲染完畢后的事情,JS引擎的一些運行機制分析。

注意,這里不談可執行上下文,VO,scop chain等概念(這些完全可以整理成另一篇文章了),這里主要是結合Event?Loop來談JS代碼是如何執行的。

讀這部分的前提是已經知道了JS引擎是單線程,而且這里會用到上文中的幾個概念:(如果不是很理解,可以回頭溫習)

然后再理解一個概念:

JS分為同步任務和異步任務

同步任務都在主線程上執行,形成一個執行棧

主線程之外,事件觸發線程管理著一個任務隊列,只要異步任務有了運行結果,就在任務隊列之中放置一個事件。

一旦執行棧中的所有同步任務執行完畢(此時JS引擎空閑),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中,開始執行。

看圖:

看到這里,應該就可以理解了:為什么有時候setTimeout推入的事件不能準時執行?因為可能在它推入到事件列表時,主線程還不空閑,正在執行其它代碼, 所以自然有誤差。

事件循環機制進一步補充

這里就直接引用一張圖片來協助理解:(參考自Philip Roberts的演講《Help, I'm stuck in an event-loop》)

上圖大致描述就是:

棧中的代碼調用某些api時,它們會在事件隊列中添加各種事件(當滿足觸發條件后,如ajax請求完畢)

而棧中的代碼執行完畢,就會讀取事件隊列中的事件,去執行那些回調

如此循環

注意,總是要等待棧中的代碼執行完畢后才會去讀取事件隊列中的事件

單獨說說定時器

上述事件循環機制的核心是:JS引擎線程和事件觸發線程

但事件上,里面還有一些隱藏細節,譬如調用setTimeout后,是如何等待特定時間后才添加到事件隊列中的?

是JS引擎檢測的么?當然不是了。它是由定時器線程控制(因為JS引擎自己都忙不過來,根本無暇分身)

為什么要單獨的定時器線程?因為JavaScript引擎是單線程的, 如果處于阻塞線程狀態就會影響記計時的準確,因此很有必要單獨開一個線程用來計時。

什么時候會用到定時器線程?當使用setTimeout或setInterval時,它需要定時器線程計時,計時完成后就會將特定的事件推入事件隊列中。

譬如:

setTimeout(function(){

? ?console.log('hello!');

}, 1000);

這段代碼的作用是當1000毫秒計時完畢后(由定時器線程計時),將回調函數推入事件隊列中,等待主線程執行

setTimeout(function(){

? ?console.log('hello!');

}, 0);

console.log('begin');

這段代碼的效果是最快的時間內將回調函數推入事件隊列中,等待主線程執行

注意:

執行結果是:先begin后hello!

雖然代碼的本意是0毫秒后就推入事件隊列,但是W3C在HTML標準中規定,規定要求setTimeout中低于4ms的時間間隔算為4ms。

(不過也有一說是不同瀏覽器有不同的最小時間設定)

就算不等待4秒,就算假設0毫秒就推入事件隊列,也會先執行begin(因為只有可執行棧內空了后才會主動讀取事件隊列)

setTimeout而不是setInterval

用setTimeout模擬定期計時和直接用setInterval是有區別的。

因為每次setTimeout計時到后就會去執行,然后執行一段時間后才會繼續setTimeout,中間就多了誤差 (誤差多少與代碼執行時間有關)

而setInterval則是每次都精確的隔一段時間推入一個事件 (但是,事件的實際執行時間不一定就準確,還有可能是這個事件還沒執行完畢,下一個事件就來了)

而且setInterval有一些比較致命的問題就是:

累計效應(上面提到的),如果setInterval代碼在(setInterval)再次添加到隊列之前還沒有完成執行,

就會導致定時器代碼連續運行好幾次,而之間沒有間隔。 就算正常間隔執行,多個setInterval的代碼執行時間可能會比預期小(因為代碼執行需要一定時間)

譬如像iOS的webview,或者Safari等瀏覽器中都有一個特點,在滾動的時候是不執行JS的

如果使用了setInterval,會發現在滾動結束后會執行多次由于滾動不執行JS積攢回調, 如果回調執行時間過長,就會非常容器造成卡頓問題和一些不可知的錯誤

而且把瀏覽器最小化顯示等操作時,setInterval并不是不執行程序,

它會把setInterval的回調函數放在隊列中,等瀏覽器窗口再次打開時,一瞬間全部執行時

所以,鑒于這么多但問題,目前一般認為的最佳方案是:用setTimeout模擬setInterval,或者特殊場合直接用requestAnimationFrame

事件循環進階:macrotask與microtask

這段參考了參考來源中的第2篇文章(英文版的),(加了下自己的理解重新描述了下), 強烈推薦有英文基礎的同學直接觀看原文,作者描述的很清晰,示例也很不錯,如下:

https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

上文中將JS事件循環機制梳理了一遍,在ES5的情況是夠用了,但是在ES6盛行的現在,仍然會遇到一些問題,譬如下面這題:

console.log('script start');

setTimeout(function() {

? ?console.log('setTimeout');

}, 0);

Promise.resolve().then(function() {

? ?console.log('promise1');

}).then(function() {

? ?console.log('promise2');

});

console.log('script end');

嗯哼,它的正確執行順序是這樣子的:

script start

script end

promise1

promise2

setTimeout

為什么呢?因為Promise里有了一個一個新的概念:microtask

或者,進一步,JS中分為兩種任務類型:macrotask和microtask,在ECMAScript中,microtask稱為jobs,macrotask可稱為task

它們的定義?區別?簡單點可以按如下理解:

macrotask(又稱之為宏任務),可以理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調并放到執行棧中執行)

每一個task會從頭到尾將這個任務執行完畢,不會執行其它

瀏覽器為了能夠使得JS內部task與DOM任務能夠有序的執行,會在一個task執行結束后,在下一個 task 執行開始前,對頁面進行重新渲染

(`task->渲染->task->...`)

microtask(又稱為微任務),可以理解是在當前 task 執行結束后立即執行的任務

也就是說,在當前task任務后,下一個task之前,在渲染之前

所以它的響應速度相比setTimeout(setTimeout是task)會更快,因為無需等渲染

也就是說,在某一個macrotask執行完后,就會將在它執行期間產生的所有microtask都執行完畢(在渲染前)

分別很么樣的場景會形成macrotask和microtask呢?

macrotask:主代碼塊,setTimeout,setInterval等(可以看到,事件隊列中的每一個事件都是一個macrotask)

microtask:Promise,process.nextTick等

再根據線程來理解下:

macrotask中的事件都是放在一個事件隊列中的,而這個隊列由事件觸發線程維護

microtask中的所有微任務都是添加到微任務隊列(Job Queues)中,等待當前macrotask執行完畢后執行,而這個隊列由JS引擎線程維護

(這點由自己理解+推測得出,因為它是在主線程下無縫執行的)

所以,總結下運行機制:

執行一個宏任務(棧中沒有就從事件隊列中獲取)

執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中

宏任務執行完畢后,立即執行當前微任務隊列中的所有微任務(依次執行)

當前宏任務執行完畢,開始檢查渲染,然后GUI線程接管渲染

渲染完畢后,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)

如圖:

另外,請注意下Promise的polyfill與官方版本的區別:

官方版本中,是標準的microtask形式

polyfill,一般都是通過setTimeout模擬的,所以是macrotask形式(目前沒見過有能直接模擬microtask,如果確實有,后續會修改這部分)

請特別注意這兩點區別

注意,有一些瀏覽器執行結果不一樣(因為它們可能把microtask當成macrotask來執行了), 但是為了簡單,這里不描述一些不標準的瀏覽器下的場景(但記住,有些瀏覽器可能并不標準)

JS是單線程的,為何能讓ajax異步

JS的單線程是指一個瀏覽器進程中只有一個JS的執行線程,同一時刻內只會有一段代碼在執行(你可以使用IE的標簽式瀏覽試試看效果,這時打開的多個頁面使用的都是同一個JS執行線程,如果其中一個頁面在執行一個運算量較大的function時,其他窗口的JS就會停止工作)。而異步機制是瀏覽器的兩個或以上常駐線程共同完成的,例如異步請求是由兩個常駐線程:JS執行線程和事件觸發線程共同完成的,JS的執行線程發起異步請求(這時瀏覽器會開一條新的HTTP請求線程來執行請求,這時JS的任務已完成,繼續執行線程隊列中剩下的其他任務),然后在未來的某一時刻事件觸發線程監視到之前的發起的HTTP請求已完成,它就會把完成事件插入到JS執行隊列的尾部等待JS處理。又例如定時觸發(settimeout和setinterval)是由瀏覽器的定時器線程執行的定時計數,然后在定時時間把定時處理函數的執行請求插入到JS執行隊列的尾端(所以用這兩個函數的時候,實際的執行時間是大于或等于指定時間的,不保證能準確定時的)。所以,所謂的JS的單線程和異步更多的應該是屬于瀏覽器的行為,他們之間沒有沖突,更不是同一種事物,沒有什么區別不區別的。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容