setTimeout異步


同步任務和異步任務

同步和異步操作的區別就是是否阻礙后續代碼的執行。

同步任務是那些沒有被引擎掛起、在主線程上排隊執行的任務。只有前一個任務執行完畢,才能執行后一個任務。

異步任務是那些被引擎放在一邊,不進入主線程、而進入任務隊列的任務。只有引擎認為某個異步任務可以執行了(比如 Ajax 操作從服務器得到了結果)【發布訂閱】,該任務(采用回調函數的形式)才會進入主線程執行。排在異步任務后面的代碼,不用等待異步任務結束會馬上運行,也就是說,異步任務不具有“堵塞”效應。

在setTimeout的執行形式上來看,setTimeout是不會阻礙其后續代碼的執行的。所以可以理解為setTimeout是異步操作。

單線程模式

JS是單線程的,但JS運行環境(Chrome瀏覽器)是多線程的。

GUI線程

GUI線程就是渲染頁面的,他解析HTML和CSS,然后將他們構建成DOM樹和渲染樹就是這個線程負責的。

JS引擎線程

這個線程就是負責執行JS的主線程,前面說的"JS是單線程的"就是指的這個線程。大名鼎鼎的Chrome V8引擎就是在這個線程運行的。需要注意的是,這個線程跟GUI線程是互斥的。互斥的原因是JS也可以操作DOM,如果JS線程和GUI線程同時操作DOM,結果就混亂了,不知道到底渲染哪個結果。這帶來的后果就是如果JS長時間運行,GUI線程就不能執行,整個頁面就感覺卡死了。所以我們最開始例子的while(true)這樣長時間的同步代碼在真正開發時是絕對不允許的

定時器線程

前面異步例子的setTimeout其實就運行在這里,他跟JS主線程根本不在同一個地方,所以“單線程的JS”能夠實現異步。JS的定時器方法還有setInterval,也是在這個線程。

事件觸發線程

定時器線程其實只是一個計時的作用,他並不會真正執行時間到了的回調,真正執行這個回調的還是JS主線程。所以當時間到了定時器線程會將這個回調事件給到事件觸發線程,然后事件觸發線程將它加到任務隊列里面去。最終JS主線程從任務隊列取出這個回調執行。事件觸發線程不僅會將定時器事件放入任務隊列,其他滿足條件的事件也是由他負責放進任務隊列。

異步HTTP請求線程

這個線程負責處理異步的ajax請求,當請求完成后,他也會通知事件觸發線程,然后事件觸發線程將這個事件放入任務隊列給主線程執行。

所以JS異步的實現靠的就是瀏覽器的多線程,當他遇到異步API時,就將這個任務交給對應的線程,當這個異步API滿足回調條件時,對應的線程又通過事件觸發線程將這個事件放入任務隊列,然后主線程從任務隊列取出事件繼續執行。這個流程我們多次提到了任務隊列,這其實就是Event Loop,下面我們詳細來講解下。

 

 

任務隊列(Event Loop)

JavaScript 運行時,除了一個正在運行的主線程,引擎還提供多個任務隊列(根據任務的類型,所以有多個)。

首先,主線程會去執行所有的同步任務。等到同步任務全部執行完,就會去看任務隊列里面有沒有事件回調。如果有,則取出就重新進入主線程執行,這時它就變成同步任務了。等到執行完,下一個異步任務再進入主線程開始執行。一旦任務隊列清空,程序就結束執行。

異步任務的寫法通常是回調函數。一旦異步任務重新進入主線程,就會執行對應的回調函數。如果一個異步任務沒有回調函數,就不會進入任務隊列,也就是說,不會重新進入主線程,因為沒有用回調函數指定下一步的操作。

只要同步任務執行完了,引擎就會一遍又一遍地去檢查那些掛起來的異步任務,是不是可以進入主線程了。這種循環檢查的機制,就叫做事件循環(Event Loop)

 

目前JS的主要運行環境有兩個——瀏覽器和Node.js。相比於V8引擎,node 中增加了兩種異步方式: process.nextTick()  和 setImmediate()。

Node 規定,process.nextTick()Promise的回調函數,追加在本輪循環,即同步任務一旦執行完成,就開始執行它們。

setTimeoutsetIntervalsetImmediate的回調函數,追加在次輪循環

process.nextTick()

追加到本輪循環執行,而且是所有異步任務里面最快執行的,nextTickQueue。

Promise異步

function search(term) {
  var url = 'http://example.com/search?q=' + term; var xhr = new XMLHttpRequest(); var result; var p = new Promise(function (resolve, reject) { xhr.open('GET', url, true); xhr.onload = function (e) { if (this.status === 200) { result = JSON.parse(this.responseText); resolve(result); } }; xhr.onerror = function (e) { reject(e); }; xhr.send(); }); return p; } search('Hello World').then(console.log, console.error);

Promise的回調函數不是正常的異步任務,而是微任務(microtask)。它們的區別在於,正常任務追加到下一輪事件循環,微任務隊列追加在process.nextTick隊列的后面,也屬於本輪循環。

process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 1
// 3
// 2
// 4

與正常任務比較:

setTimeout(function() {
  console.log(1); }, 0); new Promise(function (resolve, reject) {
console.log(4); resolve(2); }).then(console.log); console.log(3);

// 4 // 3 // 2 // 1
// 注意: Promise構造函數中的代碼為同步執行。

setTimeout與setImmediate

setTimeout和setInterval的運行機制是,將指定的代碼移出本次執行,等到下一輪Event Loop時,再檢查是否到了指定時間。如果到了,就執行對應的代碼;如果不到,就等到再下一輪Event Loop時重新判斷。這意味着,setTimeout指定的代碼,必須等到本次事件循環執行的所有代碼都執行完,才會執行。

setTimeout的作用是將代碼推遲到指定時間執行,如果指定時間為0,即setTimeout(f,0),也不會立刻執行(放到任務隊列后,還要等待主線程空閑且任務隊列中在它之前沒有未執行的回調)。

setTimeout(f,0)將第二個參數設為0,作用是讓 f 在現有的任務(腳本的同步任務和“任務隊列”中已有的事件)一結束就立刻執行。也就是說,setTimeout(f,0)的作用是,盡可能早地執行指定的任務。

var a = 1;
setTimeout(function(){
    a = 2;    
    console.log(a);
}, 0);
var a ;
console.log(a);
a = 3;
console.log(a);//輸出//1//3//2
//因為先執行同步代碼
var flag = true;
setTimeout(function(){
    flag = false;
},0)
while(flag){

}
console.log(flag);
//什么都不會輸出,而且瀏覽器會出現卡死狀態
//因為先執行同步代碼,所以相當於一直在做while(true){}的無限循環。因此不會輸出console.log(flag)也不會執行到異步

setTimeout在 timers 階段執行,而setImmediate在 check 階段執行(將下方,Nodejs事件循環的六個階段)。但是,setTimeout不一定總是早於setImmediate完成。

console.log('outer');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

 原因是,在nodejs中 setTimeout(fn, 0) 會被強制 setTimeout(fn, 1),詳情見官方文檔

1 同步代碼執行完畢,進入Event Loop
2 先進入times階段,檢查當前時間過去了1毫秒沒有。如果過了1毫秒,滿足setTimeout條件,執行回調;如果沒過1毫秒,跳過
3 跳過空的階段,進入check階段,執行setImmediate回調

 但如果 setTimeout 和 setImmediate 在同一個 setTimeout 中,因為已經在timers階段,所以里面的setTimeout只能等下個循環了,所以setImmediate肯定先執行。同理的還有其他poll階段的API也是這樣的。

var fs = require('fs')

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('setTimeout');
    }, 0);
    setImmediate(() => {
        console.log('setImmediate');
    });
});

這里setTimeoutsetImmediatereadFile的回調里面,由於readFile回調是I/O操作,屬於poll階段,所以里面的定時器只能進入下個timers階段,而setImmediate卻可以在接下來的check階段運行。所以setImmediate先運行,運行完后,再去檢查timers,才會運行setTimeout

Nodejs事件循環的六個階段

首先,事件循環是在主線程上完成的。其次,腳本開始執行時,事件循環只進行了初始化,並沒有開始。只有當下面事件執行完后,事件循環才會開始

  • 同步任務
  • 發出異步請求
  • 規划定時器生效的時間
  • 執行 process.nextTick()等等

事件循環會無限次地執行,一輪又一輪。只有異步任務的回調函數隊列清空了,才會停止執行。

每一輪的事件循環,分成六個階段。

  1. timers:定時器階段,處理setTimeout()setInterval()的回調函數。進入這個階段后,主線程會檢查一下當前時間,是否滿足定時器的條件。如果滿足就執行回調函數,否則就離開這個階段。
  2. I/O callbacks:除了以下操作的回調函數,其他的回調函數都在這個階段執行。【setTimeout()setInterval()的回調函數;setImmediate()的回調函數;用於關閉請求的回調函數,比如socket.on('close', ...)】
  3. idle, prepare:這個階段是輪詢時間,用於等待還未返回的 I/O 事件,比如服務器的回應、用戶移動鼠標等等。

    這個階段的時間會比較長。如果沒有其他異步任務要處理(比如到期的定時器),會一直停留在這個階段,等待 I/O 請求返回結果。

  4. poll:這個階段是輪詢時間,用於等待還未返回的 I/O 事件,比如服務器的回應、用戶移動鼠標等等。
  5. check:該階段執行setImmediate()的回調函數。
  6. close callbacks:該階段執行關閉請求的回調函數,比如socket.on('close', ...)

每個階段都有一個先進先出的回調函數隊列。只有一個階段的回調函數隊列清空了,該執行的回調函數都執行了,事件循環才會進入下一個階段。

轉載自:https://www.imooc.com/article/71081

http://javascript.ruanyifeng.com/advanced/promise.html
單線程模型

作者:holdtom
鏈接:https://www.imooc.com/article/71081
來源:慕課網
單線程模型

作者:holdtom
鏈接:https://www.imooc.com/article/71081
來源:慕課網


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM