setTimeout&Promise&Async之間的愛恨情仇

setTimeout

1、setTimeout 初現

定義:setTimeout() 方法用於在指定的毫秒數後調用函數或計算表達式。
複製代碼

語法:javascript

setTimeout(code, milliseconds, param1, param2, ...)
    setTimeout(function, milliseconds, param1, param2, ...) 複製代碼
參數 描述
code/function 必需。要調用一個代碼串,也能夠是一個函數
milliseconds 可選。執行或調用 code/function 須要等待的時間,以毫秒計。默認爲 0。
param1, param2, ... 可選。 傳給執行函數的其餘參數(IE9 及其更早版本不支持該參數)。

2、setTimeout 初識

第一種html

setTimeout(fn1, 1000);
setTimeout(fn2, 2000);
setTimeout(function fn3(){console.log(3);}, 3000);
setTimeout(function (){console.log(4);}, 4000);
function fn1(){
    console.log(1);
}

var fn2 = function(){
    console.log(2);
}
//輸出結果以下:
// 分別延遲1,2,3,4秒以後輸出 1 2 3 4
複製代碼

第二種html5

setTimeout(fn1(), 1000);
setTimeout(fn2(), 2000);
setTimeout(function fn3(){console.log(3);}(), 3000);
setTimeout(function (){console.log(4);}(), 4000);
function fn1(){
    console.log(1);
}

var fn2 = function(){
    console.log(2);
}

//輸出結果以下:
//直接輸出 1 2 3 4 ,沒有延遲
複製代碼

按照定義:setTimeout() 方法用於在指定的毫秒數後調用函數或計算表達式。第一種方法在指定毫秒數以後執行,第二種方法沒有在指定毫秒數後執行,而是馬上執行。因此我我的將其分紅正規軍setTimeout和雜牌軍setTimeout,方便後面記憶。java

正規軍咱們在後面詳細講,如今先了解下雜牌軍: 因爲setTimeout()的第一個參數是直接可執行的代碼,因此它沒有任何延遲效果,以下:node

setTimeout(console.log(1), 1000);

//輸出結果以下:
//直接輸出 1 ,沒有延遲
複製代碼

3、setTimeout 再遇

setTimeout(function(a,b){
    console.log(a+b);
},1000,4,5);

//輸出結果以下:
//9
複製代碼

從第三個參數開始,依次用來表示第一個參數(回調函數)傳入的參數 一些古老的瀏覽器是不支持,能夠用bind或apply方法來解決,以下:es6

setTimeout(function(a,b){
    console.log(a+b);
}.bind(this,4,5),1000);

//輸出結果以下:
//9
複製代碼

第一個參數表示將原函數的this綁定全局做用域,第二個參數開始是要傳入原函數的參數 當調用綁定函數時,綁定函數會以建立它時傳入bind()方法的第一個參數做爲 this面試

4、setTimeout 相知

對於setTimeout()的this問題,網上有不少的文章,我就不班門弄斧了,後面若總結的夠到位了就寫一篇文章介紹下。express

console.log(1);
setTimeout(function (){
    console.log(2);
},3000);
console.log(3);
//輸出結果以下:
//1 3 2
複製代碼
console.log(1);
setTimeout(function (){
    console.log(2);
},0);
console.log(3);

//輸出結果以下:
//1 3 2
複製代碼

這裏有些同窗可能會疑惑,第一段代碼延遲三秒以後執行輸出1,3,2能夠理解,可是第二段代碼延遲0秒執行爲何也是會輸出1,3,2呢?promise

這裏就須要提到「任務隊列」這個概念了,因爲JavaScript是一種單線程的語言,也就是說同一時間只能作一件事情。可是HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制。 單線程意味着,全部的任務須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就不得不一直等待。瀏覽器

因此設計者將任務分紅兩種,一種 同步任務 ,另外一種是 異步任務 。 同步任務是,在主線程上排隊執行的任務,只有前一個執行完,才能執行後一個; 異步任務是,不進入主線程,而是進入「任務隊列」的任務,只有「任務隊列」通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。

「任務隊列」除了放置任務事件,還能夠放置定時事件。即指定某些代碼在多少時間以後執行。知道了這些咱們基本上就能夠解釋上面兩段代碼爲何會輸出這樣的結果了。

第一段代碼,由於setTimeout()將回調函數推遲了3000毫秒以後執行。若是將setTimeout()第二個參數設置爲0,就表示當前代碼執行完之後,馬上執行(0毫秒間隔)指定的回調函數。因此只有在打印出1和3以後,系統纔會執行「任務隊列」中的回調函數。

總之,setTimeout(fn,0)的含義是,指定某個任務在主線程最先可得的空閒時間執行,也就是說,儘量早得執行。它在"任務隊列"的尾部添加一個事件,所以要等到同步任務和"任務隊列"現有的事件都處理完,纔會獲得執行。強調一遍:它在"任務隊列"的尾部添加一個事件,記住是尾部,添加到"任務隊列"尾部,因此後最後執行。

HTML5標準規定了setTimeout()的第二個參數的最小值(最短間隔),不得低於4毫秒,若是低於這個值,就會自動增長。在此以前,老版本的瀏覽器都將最短間隔設爲10毫秒。

setTimeout()只是將事件插入了"任務隊列",必須等到當前代碼(執行棧)執行完,主線程纔會去執行它指定的回調函數。要是當前代碼耗時很長,有可能要等好久,因此並無辦法保證,回調函數必定會在setTimeout()指定的時間執行。因此他們有時候不必定會守時的。守時的都是好孩子!

阮一峯老師對任務隊列有詳細的介紹,詳情戳這裏

5、setTimeout 相熟

瞭解了上面的內容,咱們得拉出來溜溜了,直接上測試題:

console.log(1);
    setTimeout(fn1, 1000);
    setTimeout(function(){
        console.log(2);
    },0);
    setTimeout(console.log(3),2000);
    function fn1(){
        console.log(4);
    }
    console.log(5);
    //輸出結果:
    //1 3 5 2 4(4會延遲一秒)
複製代碼

1.先執行主線程,打印出1;

2.遇到第一個setTimeout,1秒後執行回調函數,因此添加到任務隊列;

3.遇到第二個setTimeout,0秒後執行回調函數,再次添加到任務隊列;

4.遇到第三個setTimeout,這個第一個參數不是回調函數,而是一個直接可執行的語句,記得我前面講過的這個是個雜牌軍,它不會添加到任務隊列也不會延遲執行而是直接執行,因此打印出3;

5.繼續執行打印出5;

6.第二個setTimeout,因爲是0秒延遲因此主線程任務結束馬上執行,因此打印出2;

7.最後執行第一個setTimeout,一秒後打印出4.

上面的試題明白以後咱們就能夠明白下面的代碼了:

var timeoutId = setTimeout(function (){
        console("hello World");
    },1000);
   
    clearTimeout(timeoutId);
    //輸出結果:
    //不會打印出hello World
複製代碼

1先執行主線程,遇到setTimeout而且第一個參數是回調函數,添加到任務隊列,1秒後執行;

2.執行clearTimeout,則還未等到代碼執行就 取消了定時器,因此不會打印出任何內容。

下面咱們學習下promise

promise

1、promise 初現

ES6 將promise寫進了語言標準,統一了用法,原生提供了Promise對象。 詳細介紹戳這裏阮一峯老師進行了詳細的說明;

這裏我簡單的說下,我後面會使用到的內容: Promise 新建後就會當即執行,而後,then方法接受兩個回調函數做爲參數,將在當前腳本全部同步任務執行完纔會執行。記住這裏then以後的回調函數才異步執行的,因此會添加到任務隊列中。 第一個回調函數是Promise對象的狀態變爲resolved時調用,第二個回調函數是Promise對象的狀態變爲rejected時調用。其中,第二個函數是可選的,不必定要提供。

2、promise 初識

下面我將以代碼片斷的方式,逐漸看出現的各類面試題,加深你們的理解

console.log(1);
    new Promise((resolve,reject)=>{
        console.log(2);
        resolve()
    }).then( ()=>{
        console.log(3)
    },()=>{
        console.log(4);
    });
    console.log(5);

    //輸出結果:
    //1 2 5 3

複製代碼

1.先執行主線程,打印出1;

  1. Promise 新建後就會當即執行,因此打印出2,執行resolve代表執行成功回調;
  1. then的成功執行的是回調函數,因此是異步執行,添加到任務隊列之中,暫不執行;
  1. 繼續執行主線程,打印出5;
  1. 主線程結束以後執行任務隊列中的回調函數打印出3
console.log(1);
    new Promise((resolve,reject)=>{
        console.log(2);
        reject()
    }).then( ()=>{
        console.log(3)
    },()=>{
        console.log(4);
    });
    console.log(5);

    //輸出結果:
    //1 2 5 4

複製代碼

這個例子同上,只是執行的是異步的失敗的回調函數,因此最後一個打印出的是4

console.log(1);
    new Promise((resolve,reject)=>{
        console.log(2);
    }).then( ()=>{
        console.log(3)
    });
    console.log(4);

    //輸出結果:
    //1 2 4

複製代碼

這個例子中打印出4以後沒有打印3,是由於promise中沒有指定是執行成功回調仍是失敗的回調因此不會執行then的回調函數

console.log(1);
    new Promise((resolve,reject)=>{
        console.log(2);
    }).then(console.log(3));
    console.log(4);

    //輸出結果:
    //1 2 3 4

複製代碼

看到這個有同窗可能就懵了,怎麼回事怎麼是1234而不是1243呢,這須要考察同窗們是否細心呢,看這裏then中的直接是可執行的語句而不是回調函數,因此會出現這種狀況,異步任務必須是回調函數 若是不是回調函數就是同步的了

1.先執行主線程,打印出1;

  1. Promise 新建後就會當即執行,因此打印出2;
  1. then中不是回調函數而是直接可執行的語句,因此直接執行打印出3;
  1. 繼續執行主線程,打印出4;

嘻嘻,看了上面的這些例子相信你們已經對promise理解了很多,因此咱們繼續深刻看看下面這個例子,輸出的結果是什麼呢?

console.log(1);
    new Promise((resolve,reject)=>{
        console.log(2);
        resolve();
        console.log(3);
    }).then( ()=>{
        console.log(4)
    });
    console.log(5);

    //輸出結果:
    //1 2 3 5 4

複製代碼

你們有沒有寫對呢? 這裏你們的疑問估計就是resolve()以後的console.log(3);這個地方咯 這是由於上面代碼中,調用resolve()之後,後面的console.log(3)仍是會執行,而且會首先打印出來。由於當即 resolved 的 Promise 是在本輪事件循環的末尾執行,老是晚於本輪循環的同步任務。

因此若是想讓,調用resolve或reject之後,Promise 的使命完成,後繼操做應該放到then方法裏面,而不該該直接寫在resolve或reject的後面。因此,最好在它們前面加上return語句,這樣就不會有意外。以下:

console.log(1);
    new Promise((resolve,reject)=>{
        console.log(2);
        return resolve();
        console.log(3);
    }).then( ()=>{
        console.log(4)
    });
    console.log(5);
    //輸出結果:
    //1 2 5 4
複製代碼

這樣console.log(3);是不會執行的。

3、promise&setTimeout

下面咱們在來看若是promise&setTimeout同時出現會發生什麼樣的狀況呢?以下:

console.log('a');
    setTimeout(function() {console.log('b')}, 0);
    new Promise((resolve, reject) => {
        for(let i=0; i<10000000; i++) {
            if(i==999999) {
                console.log('c');
                resolve();
            }
        }
        console.log('d');
    }).then(() => {
        console.log('e');
    });
    console.log('f');
    //輸出結果:
    // a c d f e b
複製代碼

你們是否是有些暈,哈哈哈,彆着急這裏咱們得在拓展一點新概念,方便咱們理解:事件循環、宏任務和微任務

JavaScript的一大特色就是單線程,而這個線程中擁有惟一的一個事件循環。

一個線程中,事件循環是惟一的,可是任務隊列能夠擁有多個。

任務隊列又分爲macro-task(宏任務)與micro-task(微任務),它們又被稱爲task與jobs。

宏任務(macro-task)大概包括:script(總體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。

微任務(micro-task)大概包括: process.nextTick, Promise, MutationObserver(html5新特性)

事件循環的順序,決定了JavaScript代碼的執行順序。

它從script(總體代碼)開始第一次循環。以後全局上下文進入函數調用棧。

直到調用棧清空(只剩全局),而後執行全部的微任務(micro-task)。

當全部可執行的微任務(micro-task)執行完畢以後。

循環再次從宏任務(macro-task)開始,找到其中一個任務隊列執行完畢,而後再執行全部的微任務(micro-task),這樣一直循環下去。

注:本篇使用的宏任務(macro-task):script(總體代碼), setTimeout, setInterval;微任務(micro-task): Promise。至於其餘的瀏覽器沒有,引用了node.js的API,如: setImmediate、 process.nextTick等,至於他們的執行順序可參考這篇文章

好比上述例子,不一樣類型的任務會分別進入到他們所屬類型的任務隊列,好比全部setTimeout()的回調都會進入到setTimeout任務隊列,既宏任務(macro-task);全部then()回調都會進入到then隊列,既微任務(micro-task)。

當前的總體代碼咱們能夠認爲是宏任務。事件循環從當前總體代碼開始第一次事件循環,而後再執行隊列中全部的微任務,當微任務執行完畢以後,事件循環再找到其中一個宏任務隊列並執行其中的全部任務,而後再找到一個微任務隊列並執行裏面的全部任務,就這樣一直循環下去。這就是我所理解的事件循環。

分析上面例子:

1.首先執行總體代碼,第一個打印出來a

2.執行到第一個setTimeout時,發現它是宏任務,此時會新建一個setTimeout類型的宏任務隊列並派發當前這個setTimeout的回調函數到剛建好的這個宏任務隊列中去

3.再執行到new Promise,Promise構造函數中的第一個參數在new的時候會直接執行,所以不會進入任何隊列,因此第三個輸出是c

4.執行完resolve()以後,繼續向後執行,打印出d

5.上面有說到Promise.then是微任務,那麼這裏會生成一個Promise.then類型的微任務隊列,這裏的then回調會被push進這個隊列中

6.再向後走打印出f

7.第一輪事件循環的宏任務執行完成(總體代碼看作宏任務)。此時微任務隊列中只有一個Promise.then類型微任務隊列。宏任務隊列中也只有一個setTimeout類型的宏任務隊列。

8.下面執行第一輪事件循環的微任務,很明顯,會打印出e,至此第一輪事件循環完成

9.開始第二輪事件循環:執行setTimeout類型隊列(宏任務隊列)中的全部任務,只有一個任務,因此打印出b

10.第二輪事件的宏任務結束,這個事件循環結束。

再來一個你中有我我中有你的超級例子,體驗下處處是坑的試題,嘿嘿;-)

console.log('a');

    setTimeout(function () {
        console.log('b')
        new Promise(resolve=> {
            console.log('c')
            resolve()
        }).then(()=> {
            console.log('d')
        })
    },2000);


    new Promise((resolve,reject)=>{
        console.log('e');
        resolve();
        console.log('f');
    }).then(()=>{
        console.log('g')
    });

    console.log('h');

    new Promise((resolve,reject)=>{
        setTimeout(function () {
            console.log('i');
        },0);
    }).then(console.log('j'));

    setTimeout(function () {
        console.log('k')
        new Promise(resolve=>{
            console.log('l')
            return resolve()
            console.log('m')
        }).then(()=>{
            console.log('n')
        })
    },1000);

    console.log('p');

    //輸出結果:
    //a e f h j p g i
    //延遲1s 輸出:k l n 
    //再延遲1s 輸出:b c d
複製代碼

1.首先執行總體代碼,第一個打印出來"a";

2.執行到第一個setTimeout時,發現它是宏任務,此時會新建一個setTimeout類型的宏任務隊列並派發當前這個setTimeout的回調函數到剛建好的這個宏任務隊列中去,而且輪到它執行時要延遲2秒後再執行;

3.執行到第一個new Promise,Promise構造函數中的第一個參數在new的時候會直接執行,所以不會進入任何隊列,因此第二個輸出是"e",resolve()以後的語句會繼續執行,因此第三個輸出的是"f",Promise.then是微任務,那麼這裏會生成一個Promise.then類型的微任務隊列,這裏的then回調會被push進這個隊列中;

4.再執行總體代碼,第四個打印出來"h";

5.執行到第一個new Promise,Promise構造函數中的第一個參數在new的時候會直接執行,可是這個是一個setTimeout,發現它是宏任務,派發它的回調到上面setTimeout類型的宏任務隊列中去。後面Promise.then中是一個可執行的代碼,並非回調函數,因此會直接的執行,並不會添加到微任務中去,因此第五個輸出的是:"j";

6.執行到第二個setTimeout時,發現它是宏任務,派發它的回調到上面setTimeout類型的宏任務隊列中去,可是會延遲1s執行;

7.執行總體代碼,第六個輸出的是"p";

8.第一輪事件循環的宏任務執行完成(總體代碼看作宏任務)。此時微任務隊列中只有一個Promise.then類型微任務隊列,它裏面有一個任務;宏任務隊列中也只有一個setTimeout類型的宏任務隊列;

9.下面執行第一輪事件循環的微任務,很明顯,第七個輸出的是:"g"。此時第一輪事件循環完成;

10.開始第二輪事件循環:執行setTimeout類型隊列(宏任務隊列)中的全部任務。發現有的有延時有的沒有延時,因此先執行延時最短的宏任務;

11.執行setTimeout,第八個輸出的是"i";

12.緊接着執行延遲1s的setTimeout,因此延遲一秒以後第九個輸出的是:"k";

13.以後遇到new Promise,Promise構造函數中的第一個參數在new的時候會直接執行,所以不會進入任何隊列,因此第十個輸出是"l",以後是一個return語句,因此後面的代碼不會執行,"m"不會被輸出出來;

14.但這裏發現了then,又把它push到上面已經被執行完的then隊列中去,這裏要注意,由於出現了微任務then隊列,因此這裏會執行該隊列中的全部任務(此時只有一個任務),因此第十一個輸出的是"n";

15.再延遲1s執行setTimeout,因此延遲二秒以後第十二個輸出的是:"b";

16.以後遇到new Promise,Promise構造函數中的第一個參數在new的時候會直接執行,所以不會進入任何隊列,因此第十三個輸出是"c";

17.但這裏又發現了then,又把它push到上面已經被執行完的then隊列中去,這裏要注意,由於出現了微任務then隊列,因此這裏會執行該隊列中的全部任務(此時只有一個任務),因此第十四個輸出的是"d";

噗,終於完了,不知道你們有沒有理解呢? 生活就是這樣,你覺得度過了一個難關前面就是陽光大道,但現實就是這樣,他會給你再來一個難題,接着看下面的代碼,嘿嘿嘿~~~

async function async1() {
        console.log("a");
        await  async2();
        console.log("b");
    }

    async  function async2() {
        console.log( 'c');
    }

    console.log("d");

    setTimeout(function () {
        console.log("e");
    },0);

    async1();

    new Promise(function (resolve) {
        console.log("x");
        resolve();
    }).then(function () {
        console.log("y");
    });

    console.log('z');

    //輸出結果:
    // d a c x z y b e
複製代碼

是否是有點傻了,怎麼又出現了async了,別慌別慌且聽我慢慢道來,在說以前還得你們瞭解async,阮一峯老師對此有詳細的介紹,詳情戳這裏

Async

1、async

async的用法,它做爲一個關鍵字放到函數前面,用於表示函數是一個異步函數,由於async就是異步的意思, 異步函數也就意味着該函數的執行不會阻塞後面代碼的執行。

咱們先來觀察下async的返回值,請看下面的代碼:

async function testAsync() {
        return "hello async";
    }

    const result = testAsync();
    console.log(result);

    //輸出結果:
    // Promise { 'hello async' }
複製代碼

看到這裏咱們知道了,saync輸出的是一個promise對象

async 函數(包含函數語句、函數表達式)會返回一個 Promise 對象,若是在函數中 return 一個直接量,async 會把這個直接量經過 Promise.resolve() 封裝成 Promise 對象。

那咱們試下沒有返回值會是怎麼樣呢?

async function testAsync() {
        console.log("hello async");
    }

    const result = testAsync();
    console.log(result);

    //輸出結果:
    // hello async
    // Promise { undefined }
複製代碼

會返回一個爲空的promis對象

2、await

從字面意思上看await就是等待,await 等待的是一個表達式,這個表達式的返回值能夠是一個promise對象也能夠是其餘值。

注意到 await 不只僅用於等 Promise 對象,它能夠等任意表達式的結果,因此,await 後面實際是能夠接普通函數調用或者直接量的。

function getSomething() {
        return "something";
    }

    async function testAsync() {
        return Promise.resolve("hello async");
    }

    async function test() {
        const v1 = await getSomething();
        const v2 = await testAsync();
        console.log(v1, v2);
    }

    test();

    //輸出結果:
    // something hello async
複製代碼

await 是個運算符,用於組成表達式,await 表達式的運算結果取決於它等的東西,若是它等到的不是一個 Promise 對象,那 await 表達式的運算結果就是它等到的東西。

內容 描述
語法 [return_value] = await expression;
表達式(expression) 一個 Promise 對象或者任何要等待的值。
返回值(return_value) 返回 Promise 對象的處理結果。若是等待的不是 Promise 對象,則返回該值自己

可是當遇到await會怎麼執行呢?

async函數徹底能夠看做多個異步操做,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。 當函數執行的時候,一旦遇到await就會先返回,等到異步操做完成,再接着執行函數體內後面的語句.

即,

當遇到async函數體內的 await test();時候,執行test(),而後獲得返回值value(能夠是promise也能夠是其餘值),組成await value;,若 value是promise對象時候,此時返回的Promise會被放入到任務隊列中等待,await會讓出線程,跳出 async函數,繼續執行後續代碼;若 value是其餘值,只是不會被添加到任務隊列而已,await也會讓出線程,跳出 async函數,繼續執行後續代碼。

明白了這些,咱們分析上面最難的那部分代碼:

1.首先執行總體代碼,遇到兩個saync函數,沒有調用因此繼續向下執行,因此第一個輸出的是:"d";

2.執行到第一個setTimeout時,發現它是宏任務,此時會新建一個setTimeout類型的宏任務隊列並派發當前這個setTimeout的回調函數到剛建好的這個宏任務隊列中去,而且輪到它執行時要馬上執行;

3.遇到async1(), async1函數調用,執行async1函數,第二個輸出的是:"a";

4.而後執行到 await async2(),發現 async2 也是個 async 定義的函數,因此直接執行了「console.log('c')」。因此第三個輸出的是:"c";

5.同時async2返回了一個Promise,請注意:此時返回的Promise會被放入到任務隊列中等待,await會讓出線程,接下來就會跳出 async1函數,繼續往下執行!!!

6.執行到 new Promise,前面說過了promise是當即執行的,因此第四個輸出的是:"x";

7.而後執行到 resolve 的時候,resolve這個任務就被放到任務隊列中等待,而後跳出Promise繼續往下執行,因此第五個輸出的是:"z";

8.如今調用棧空出來了,事件循環就會去任務隊列裏面取任務繼續放到調用棧裏面;

9.取到的第一個任務,就是前面 async1 放進去的Promise,執行Promise時候,遇到resolve或者reject函數,此次會又被放到任務隊列中等待,而後再次跳出 async1函數 繼續下一個任務!!!

10.接下來取到的下一個任務,就是前面 new Promise 放進去的 resolve回調,執行then,因此第六個輸出的是:"y";

11.調用棧再次空出來了,事件循環就取到了下一個任務,async1 函數中的 async2返回的promise對象的resolve或者reject函數執行,由於 async2 並無return任何東西,因此這個resolve的參數是undefined;

12.此時 await 定義的這個 Promise 已經執行完而且返回告終果,因此能夠繼續往下執行 async1函數 後面的任務了,那就是「console.log('b')」,因此第七個輸出的是:"b";

13.調用棧再次的空了出來終於執行setTimeout的宏任務,因此第八個輸出的是:"e"

哇(@ο@) 哇~,解決了小夥伴們明白沒有,但願你們瞭解了就不再怕面試這種題目啦! 本想着簡單的寫下面試題的解決步驟沒想到一會兒寫了這麼多,耐心讀到這裏的小夥伴都是很是棒的,願你在技術的路上越走越遠!

相關文章
相關標籤/搜索