衆所周知,JavaScript的一大特色就是單線程,可是咱們有沒有思考過它爲何不能是多線程的?編程
咱們假定JavaScript有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?因此爲了不這種複雜性,從一誕生,JavaScript就是單線程。promise
儘管HTML5提出Web Worker,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,並無改變JavaScript單線程的本質。瀏覽器
定時器主要是setTimeout()和setInterval()這兩個函數,這也是平時編程時候用到最多的。多線程
console.log(1); setTimeout(function() { console.log(2); },5000); console.log(3);
上面代碼的執行結果是1,3,2。但若是將setTimeout()的第二個參數設爲0,就表示當前代碼執行完之後,當即執行(0毫秒延遲)指定的回調函數。setTimeout(fn,0)的含義是,它在任務隊列的尾部添加一個事件,在主線程最先獲得空閒時去執行,也就是說,儘量早得執行。併發
須要注意的是,定時器只是將事件插入了任務隊列,必須等到當前代碼(執行棧)執行完,主線程纔會去執行它指定的回調函數。若是當前代碼耗時很長,有可能要等好久,因此並無辦法保證,回調函數必定會在setTimeout()指定的時間執行。這也引伸出JavaScript的併發模型。異步
咱們先看一下理論上的併發模型:函數
棧(stack):函數調用會造成了一個堆棧幀
堆(heap):對象被分配在一個堆中,一個用以表示一個內存中大的未被組織的區域
隊列(queue):運行時包含的一個待處理的消息隊列。當棧爲空時,則從隊列中取出一個消息進行處理。這個處理過程包含了調用與這個消息相關聯的函數(以及於是建立了一個初始堆棧幀)oop
針對上面的併發模型和JavaScript的同步異步運行機制,咱們能夠看到整個流程大體是這樣的:spa
1.全部同步任務都在主線程上執行,造成一個執行棧(併發模型的stack)。
2.主線程以外,還存在一個任務隊列(併發模型的queue)。只要異步任務有了運行結果,就在任務隊列中放置一個事件。
3.一旦執行棧中的全部同步任務執行完畢,系統就會讀取任務隊列,看看裏面有哪些事件和那些對應的異步任務,因而等待結束狀態,進入執行棧,開始執行。
4.主線程不斷重複上面的第三步。線程
這個過程是循環不斷的,因此這種運行機制又稱爲Event Loop(事件循環)。放一張大神演講時的圖片來更好地理解Event Loop:
咱們能夠看到,主線程運行的時候,產生堆(heap)和棧(stack),棧中的代碼調用各類外部API,它們在任務隊列中加入各類事件(click,load,done)。只要棧中的代碼執行完畢,主線程就會去讀取任務隊列,依次執行那些事件所對應的回調函數。
這是一個比較冷門的知識,在併發模型中隊列又能夠分爲Macrotask 和 Microtask,它們都屬於異步任務。先來看一個例子:
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); setTimeout(function() { console.log('setTimeout in microtask'); }, 0); }).then(function() { console.log('promise2'); }); console.log('script end');
輸出:
script start
script end
promise1
promise2
setTimeout
setTimeout in microtask
Macrotask 和 Microtask有什麼區別呢?
它們的執行過程以下:
JavaScript引擎首先從macrotask queue中取出第一個任務執行完畢後,將microtask queue中的全部任務取出,按順序所有執行而後再從macrotask queue中取下一個執行完畢後,再次將microtask queue中的所有取出循環往復,直到兩個queue中的任務都取完