透徹掌握promise

透徹掌握Promise的使用阵难,讀這篇就夠了

Promise的重要性我認(rèn)為我沒(méi)有必要多講饶套,概括起來(lái)說(shuō)就是必須得掌握漩蟆,而且還要掌握透徹。這篇文章的開(kāi)頭妓蛮,主要跟大家分析一下怠李,為什么會(huì)有Promise出現(xiàn)。

地獄回調(diào)

在實(shí)際的使用當(dāng)中,有非常多的應(yīng)用場(chǎng)景我們不能立即知道應(yīng)該如何繼續(xù)往下執(zhí)行捺癞。最重要也是最主要的一個(gè)場(chǎng)景就是ajax請(qǐng)求夷蚊。通俗來(lái)說(shuō),由于網(wǎng)速的不同,可能你得到返回值的時(shí)間也是不同的,這個(gè)時(shí)候我們就需要等待简烤,結(jié)果出來(lái)了之后才知道怎么樣繼續(xù)下去。

// 簡(jiǎn)單的實(shí)現(xiàn)ajax
var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
var result;

var XHR = new XMLHttpRequest();
XHR.open('get',url, true);
XHR.send();

XHR.onreadystatechange = function(){
    if(XHR.readyState == 4 && XHR.status == 200){
        result = XHR.response;
        console.log(result)
    }
}

在ajax的原生實(shí)現(xiàn)中呜笑,利用了onreadystatechange事件,當(dāng)該事件觸發(fā)并且符合一定條件時(shí)彻犁,才能拿到我們想要的數(shù)據(jù)叫胁,之后我們才能開(kāi)始處理數(shù)據(jù)。

這樣做看上去并沒(méi)有什么麻煩汞幢,但是如果這個(gè)時(shí)候驼鹅,我們還需要做另外一個(gè)ajax請(qǐng)求,這個(gè)新的ajax請(qǐng)求的其中一個(gè)參數(shù)森篷,得從上一個(gè)ajax請(qǐng)求中獲取输钩,這個(gè)時(shí)候我們就不得不如下這樣做:

var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
var result;

var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();

XHR.onreadystatechange = function() {
    if (XHR.readyState == 4 && XHR.status == 200) {
        result = XHR.response;
        console.log(result);

        // 偽代碼
        var url2 = 'http:xxx.yyy.com/zzz?ddd=' + result.someParams;
        var XHR2 = new XMLHttpRequest();
        XHR2.open('GET', url, true);
        XHR2.send();
        XHR2.onreadystatechange = function() {
            ...
        }
    }
}

當(dāng)出現(xiàn)第三個(gè)ajax(甚至更多)仍然依賴上一個(gè)請(qǐng)求的時(shí)候,我們的代碼就變成了一場(chǎng)災(zāi)難仲智。這場(chǎng)災(zāi)難买乃,往往也被稱為回調(diào)地獄

因此我們需要一個(gè)叫做Promise的東西钓辆,來(lái)解決這個(gè)問(wèn)題剪验。

當(dāng)然,除了回調(diào)地獄之外前联,還有一個(gè)非常重要的需求:為了我們的代碼更加具有可讀性和可維護(hù)性功戚,我們需要將數(shù)據(jù)請(qǐng)求與數(shù)據(jù)處理明確的區(qū)分開(kāi)來(lái)。上面的寫(xiě)法似嗤,是完全沒(méi)有區(qū)分開(kāi)啸臀,當(dāng)數(shù)據(jù)變得復(fù)雜時(shí),也許我們自己都無(wú)法輕松維護(hù)自己的代碼了烁落。這也是模塊化過(guò)程中壳咕,必須要掌握的一個(gè)重要技能,請(qǐng)一定重視顽馋。

關(guān)于promise

從前面幾篇文中的知識(shí)我們可以知道,當(dāng)我們想要確保某代碼在誰(shuí)誰(shuí)之后執(zhí)行時(shí)幌羞,我們可以利用函數(shù)調(diào)用棧寸谜,將我們想要執(zhí)行的代碼放入回調(diào)函數(shù)中。

// 一個(gè)簡(jiǎn)單的封裝
function want(){
    console.log('這是你想要執(zhí)行的代碼');
}
function fn(want){
    console.log('已經(jīng)要執(zhí)行的一段代碼') //這里比如是ajax請(qǐng)求參數(shù)
    
    want&&want();  // ajax處理參數(shù)
}

fn(want);
console.log('執(zhí)行完畢');

/*
控制臺(tái)打印為:

已經(jīng)要執(zhí)行的一段代碼
這是你想要執(zhí)行的代碼
執(zhí)行完畢

*/

利用回調(diào)函數(shù)封裝属桦,是我們?cè)诔鯇W(xué)JavaScript時(shí)常常會(huì)使用的技能熊痴。

確保我們想要的代碼壓后執(zhí)行他爸,除了利用函數(shù)調(diào)用棧的執(zhí)行順序之外,我們還可以利用上一篇文章所述的隊(duì)列機(jī)制果善。

function want(){
    console.log('這是你想要執(zhí)行的代碼');
}
function fn(want){
    console.log('已經(jīng)要執(zhí)行的一段代碼') //這里比如是ajax請(qǐng)求參數(shù)
    
    want&&setTimeout(want,0);  // ajax處理參數(shù)
}

fn(want);
console.log('執(zhí)行完畢');

/*
控制臺(tái)打印為:

已經(jīng)要執(zhí)行的一段代碼
執(zhí)行完畢
這是你想要執(zhí)行的代碼

*/

如果瀏覽器已經(jīng)支持了原生的Promise對(duì)象诊笤,那么我們就知道,瀏覽器的js引擎里已經(jīng)有了Promise隊(duì)列巾陕,這樣就可以利用Promise將任務(wù)放在它的隊(duì)列中去讨跟。

function want(){
    console.log('這是你想要執(zhí)行的代碼');
}

function fn(want){
    console.log('已經(jīng)要執(zhí)行的一段代碼') //這里比如是ajax請(qǐng)求參數(shù)
    return new Promise(function(resolve,reject){
        if(want instanceof Function){
            resolve(want)
        } else {
            reject('TypeError: '+ want +'不是一個(gè)函數(shù)')
        }
    })
}

fn(want).then(function(want,a){
    want();
})

fn('123').catch(function(err){
    console.log(err)
})
/*
控制臺(tái)打印為:

已經(jīng)要執(zhí)行的一段代碼
已經(jīng)要執(zhí)行的一段代碼
這是你想要執(zhí)行的代碼
123不是一個(gè)函數(shù)
*/

看上去變得更加復(fù)雜了”擅海可是代碼變得更加健壯晾匠,處理了錯(cuò)誤輸入的情況。

為了更好的往下擴(kuò)展Promise的應(yīng)用梯刚,這里需要先跟大家介紹一下Promsie的基礎(chǔ)知識(shí)凉馆。

一、 Promise對(duì)象三種狀態(tài)E

  • pending: 等待中亡资,或者進(jìn)行中澜共,表示還沒(méi)有得到結(jié)果

  • resolved(Fulfilled): 已經(jīng)完成,表示得到了我們想要的結(jié)果锥腻,可以繼續(xù)往下執(zhí)行

  • rejected: 也表示得到結(jié)果嗦董,但是由于結(jié)果并非我們所愿,因此拒絕執(zhí)行

這三種狀態(tài)不受外界影響旷太,而且狀態(tài)只能從pending改變?yōu)閞esolved或者rejected展懈,并且不可逆。在Promise對(duì)象的構(gòu)造函數(shù)中供璧,將一個(gè)函數(shù)作為第一個(gè)參數(shù)存崖。而這個(gè)函數(shù),就是用來(lái)處理Promise的狀態(tài)變化睡毒。

new Promise(function(resolve, reject) {
    if(true) { resolve() };
    if(false) { reject() };
})

上面的resolve和reject都為一個(gè)函數(shù)来惧,他們的作用分別是將狀態(tài)修改為resolved和rejected。

二演顾、 Promise對(duì)象中的then方法

可以接收構(gòu)造函數(shù)中處理的狀態(tài)變化供搀,并分別對(duì)應(yīng)執(zhí)行。then方法有2個(gè)參數(shù)钠至,第一個(gè)函數(shù)接收resolved狀態(tài)的執(zhí)行葛虐,第二個(gè)參數(shù)接收reject狀態(tài)的執(zhí)行。

function fn(num) {
    return new Promise(function(resolve, reject) {
        if (typeof num == 'number') {
            resolve();
        } else {
            reject();
        }
    }).then(function() {
        console.log('參數(shù)是一個(gè)number值');
    }, function() {
        console.log('參數(shù)不是一個(gè)number值');
    })
}

fn('hahha');
fn(1234);

then方法的執(zhí)行結(jié)果也會(huì)返回一個(gè)Promise對(duì)象棉钧。因此我們可以進(jìn)行then的鏈?zhǔn)綀?zhí)行屿脐,這也是解決回調(diào)地獄的主要方式。

function fn(num) {
    return new Promise(function(resolve, reject) {
        if (typeof num == 'number') {
            resolve();
        } else {
            reject();
        }
    })
    .then(function() {
        console.log('參數(shù)是一個(gè)number值');
    })
    .then(null, function() {
        console.log('參數(shù)不是一個(gè)number值');
    })
}

fn('hahha');
fn(1234);

then(null, function() {}) 就等同于catch(function() {})

三、Promise中的數(shù)據(jù)傳遞

大家自行從下面的例子中領(lǐng)悟吧的诵。

function fn(num) {
    return new Promise(function(resolve, reject) {
        if (typeof num == 'number') {
            resolve(num);
        } else {
            reject('typeError');
        }
    })
}

fn(2).then(function(num){
    console.log('first: ' + num);
    return num + 1;
}).then(function(num){
    console.log('second: ' + num);
    return num + 1;
}).then(function(num) {
    console.log('third: ' + num);
    return num + 1;
});

/*
控制臺(tái)打印為:

first: 2
second: 3
third: 4
*/

OK万栅,了解了這些基礎(chǔ)知識(shí)之后,我們?cè)倩剡^(guò)頭西疤,利用Promise的知識(shí)烦粒,對(duì)最開(kāi)始的ajax的例子進(jìn)行一個(gè)簡(jiǎn)單的封裝〈蓿看看會(huì)是什么樣子扰她。

var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';

// 封裝一個(gè)get請(qǐng)求的方法
function getJSON(url) {
    return new Promise(function(resolve, reject) {
        var XHR = new XMLHttpRequest();
        XHR.open('GET', url, true);
        XHR.send();

        XHR.onreadystatechange = function() {
            if (XHR.readyState == 4) {
                if (XHR.status == 200) {
                    try {
                        var response = JSON.parse(XHR.responseText);
                        resolve(response);
                    } catch (e) {
                        reject(e);
                    }
                } else {
                    reject(new Error(XHR.statusText));
                }
            }
        }
    })
}

getJSON(url).then(resp => console.log(resp));

為了健壯性,處理了很多可能出現(xiàn)的異常管跺,總之义黎,就是正確的返回結(jié)果,就resolve一下豁跑,錯(cuò)誤的返回結(jié)果廉涕,就reject一下。并且利用上面的參數(shù)傳遞的方式艇拍,將正確結(jié)果或者錯(cuò)誤信息通過(guò)他們的參數(shù)傳遞出來(lái)狐蜕。

現(xiàn)在所有的庫(kù)幾乎都將ajax請(qǐng)求利用Promise進(jìn)行了封裝,因此我們?cè)谑褂胘Query等庫(kù)中的ajax請(qǐng)求時(shí)卸夕,都可以利用Promise來(lái)讓我們的代碼更加優(yōu)雅和簡(jiǎn)單层释。這也是Promise最常用的一個(gè)場(chǎng)景,因此我們一定要非常非常熟悉它快集,這樣才能在應(yīng)用的時(shí)候更加靈活贡羔。

四、Promise.all

當(dāng)有一個(gè)ajax請(qǐng)求个初,它的參數(shù)需要另外2個(gè)甚至更多請(qǐng)求都有返回結(jié)果之后才能確定乖寒,那么這個(gè)時(shí)候,就需要用到Promise.all來(lái)幫助我們應(yīng)對(duì)這個(gè)場(chǎng)景院溺。

Promise.all接收一個(gè)Promise對(duì)象組成的數(shù)組作為參數(shù)楣嘁,當(dāng)這個(gè)數(shù)組所有的Promise對(duì)象狀態(tài)都變成resolved或者rejected的時(shí)候,它才會(huì)去調(diào)用then方法珍逸。

var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
var url1 = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-03-26/2017-06-10';

function renderAll() {
    return Promise.all([getJSON(url), getJSON(url1)]);
}

renderAll().then(function(value) {
    // 建議大家在瀏覽器中看看這里的value值
    console.log(value);
})

五逐虚、 Promise.race

與Promise.all相似的是,Promise.race都是以一個(gè)Promise對(duì)象組成的數(shù)組作為參數(shù)谆膳,不同的是叭爱,只要當(dāng)數(shù)組中的其中一個(gè)Promsie狀態(tài)變成resolved或者rejected時(shí),就可以調(diào)用.then方法了漱病。而傳遞給then方法的值也會(huì)有所不同涤伐,大家可以再瀏覽器中運(yùn)行下面的例子與上面的例子進(jìn)行對(duì)比馒胆。

function renderRace() {
    return Promise.race([getJSON(url), getJSON(url1)]);
}

renderRace().then(function(value) {
    console.log(value);
})

最后總結(jié)一下,這篇文章凝果,涉及到的東西,有點(diǎn)多睦尽。大概包括Promise基礎(chǔ)知識(shí)器净,ajax基礎(chǔ)知識(shí),如何利用Promise封裝ajax当凡。

延伸閱讀1:Promise簡(jiǎn)單實(shí)現(xiàn)(正常思路版)

Promise/A+規(guī)范:

首先重新閱讀了下A+的規(guī)范:

  • promise代表了一個(gè)異步操作的最終結(jié)果山害,主要是通過(guò)then方法來(lái)注冊(cè)成功以及失敗的情況,
  • Promise/A+歷史上說(shuō)是實(shí)現(xiàn)了Promise/A的行為并且考慮了一些不足之處沿量,他并不關(guān)心如何創(chuàng)建浪慌,完成,拒絕Promise朴则,只考慮提供一個(gè)可協(xié)作的then方法权纤。

術(shù)語(yǔ):

  • promise是一個(gè)擁有符合上面的特征的then方法的對(duì)象或者方法。
  • thenable是定義了then方法的對(duì)象或者方法
  • value是任何合法的js的值(包括undefined乌妒,thenable或者promise)
  • exception是一個(gè)被throw申明拋出的值
  • reason是一個(gè)指明了為什么promise被拒絕

2.1 狀態(tài)要求:

  • promise必須是在pending汹想,fulfilled或者rejected之間的一種狀態(tài)。
  • promise一旦從pending變成了fulfilled或則rejected撤蚊,就不能再改變了古掏。
  • promise變成fulfilled之后,必須有一個(gè)value侦啸,并且不能被改變
  • promise變成rejected之后槽唾,必須有一個(gè)reason,并且不能被改變

2.2 then方法的要求:

  • promise必須有個(gè)then方法來(lái)接觸當(dāng)前的或者最后的value或者reason
  • then方法接受兩個(gè)參數(shù)光涂,onFulfilled和onRejected庞萍,這兩個(gè)都是可選的,如果傳入的不是function的話顶捷,就會(huì)被忽略
  • 如果onFulfilled是一個(gè)函數(shù)挂绰,他必須在promise完成后被執(zhí)行(不能提前),并且value是第一個(gè)參數(shù)服赎,并且不能被執(zhí)行超過(guò)一次
  • 如果onRejected是一個(gè)函數(shù)葵蒂,他必須在promise拒絕后被執(zhí)行(不能提前),并且reason是第一個(gè)參數(shù)重虑,并且不能被執(zhí)行超過(guò)一次
  • onFulfilled或者onRejected只能在執(zhí)行上下文堆只包含了平臺(tái)代碼的時(shí)候執(zhí)行(就是要求onfulfilled和onrejected必須異步執(zhí)行践付,必須在then方法被調(diào)用的那一輪事件循環(huán)之后的新執(zhí)行棧執(zhí)行,這里可以使用macro-task或者micro-task缺厉,這兩個(gè)的區(qū)別參見(jiàn)文章)
  • onFulfilled或者onRejected必須作為function被執(zhí)行(就是說(shuō)沒(méi)有一個(gè)特殊的this永高,在嚴(yán)格模式中隧土,this就是undefined,在粗糙的模式命爬,就是global)
  • then方法可能在同一個(gè)promise被調(diào)用多次曹傀,當(dāng)promise被完成,所有的onFulfilled必須被順序執(zhí)行饲宛,onRejected也一樣
  • then方法必須也返回一個(gè)promise(這個(gè)promise可以是原來(lái)的promise皆愉,實(shí)現(xiàn)必須申明什么情況下兩者可以相等)promise2 = promise1.then(onFulfilled, onRejected);
  • 如果onFulfilledonRejected都返回一個(gè)value x,執(zhí)行2.3Promise的解決步驟[[Resolve]](promise2, x)
  • 如果onFulfilledonRejected都拋出exception e艇抠,promise2必須被rejected同樣的e
  • 如果onFulfilled不是個(gè)function幕庐,且promise1 is fulfilled,promise2也會(huì)fulfilled家淤,和promise1的值一樣
  • 如果onRejected不是個(gè)function异剥,且promise1 is rejected,promise2也會(huì)rejected絮重,理由和promise1一樣

2.3 Promise的解決步驟==[[Resolve]](promise2, x)

  • 這個(gè)是將promise和一個(gè)值x作為輸入的一個(gè)抽象操作冤寿。如果這個(gè)x是支持then的,他會(huì)嘗試讓promise接受x的狀態(tài)绿鸣;否則疚沐,他會(huì)用x的值來(lái)fullfill這個(gè)promise。運(yùn)行這樣一個(gè)東西潮模,遵循以下的步驟
  • 如果promise和x指向同一個(gè)對(duì)象亮蛔,則reject這個(gè)promise使用TypeError。
  • 如果x是一個(gè)promise擎厢,接受他的狀態(tài)
  • 如果x在pending究流,promise必須等待x的狀態(tài)改變
  • 如果x被fullfill,那么fullfill這個(gè)promise使用同一個(gè)value
  • 如果x被reject动遭,那么reject這個(gè)promise使用同一個(gè)理由
  • 如果x是一個(gè)對(duì)象或者是個(gè)方法
  • 如果x.then返回了錯(cuò)誤芬探,則reject這個(gè)promise使用錯(cuò)誤。
  • 如果then是一個(gè)方法厘惦,使用x為this偷仿,resolvePromise為一參,rejectPromise為二參宵蕉,
  • 如果resolvePromise被一個(gè)值y調(diào)用酝静,那么運(yùn)行[[Resolve]](promise, y)
  • 如果rejectPromise被reason r,使用r來(lái)reject這個(gè)promise
  • 如果resolvePromise和rejectPromise都被調(diào)用了羡玛,那么第一個(gè)被調(diào)用的有優(yōu)先權(quán)别智,其他的beihulue
  • 如果調(diào)用then方法得到了exception,如果上面的方法被調(diào)用了稼稿,則忽略薄榛,否則reject這個(gè)promise
  • 如果then方法不是function讳窟,那么fullfill這個(gè)promise使用x
  • 如果x不是一個(gè)對(duì)象或者方法,那么fullfill這個(gè)promise使用x

如果promise產(chǎn)生了環(huán)形的嵌套敞恋,比如[[Resolve]](promise, thenable)最終喚起了[[Resolve]](promise, thenable)丽啡,那么實(shí)現(xiàn)建議且并不強(qiáng)求來(lái)發(fā)現(xiàn)這種循環(huán),并且reject這個(gè)promise使用一個(gè)TypeError耳舅。

接下來(lái)正式寫(xiě)一個(gè)promise

思路都是最正常的思路碌上,想要寫(xiě)一個(gè)Promise,肯定得使用一個(gè)異步的函數(shù)浦徊,就拿setTimeout來(lái)做。

 var p = new Promise(function(resove){
            setTimeout(resove, 100);
        })

 p.then(function(){console.log('success')},function(){console.log('fail')});

初步構(gòu)建

上面是個(gè)最簡(jiǎn)單的使用場(chǎng)景我們需要慢慢來(lái)構(gòu)建

function Promise(fn){
  //需要一個(gè)成功時(shí)的回調(diào)
  var doneCallback;
  //一個(gè)實(shí)例的方法天梧,用來(lái)注冊(cè)異步事件
  this.then = function(done){
    doneCallback = done;
  }
  function resolve(){
    doneCallback();
  }
  fn(resolve);
}

加入鏈?zhǔn)街С?/h4>

下面加入鏈?zhǔn)娇裕晒卣{(diào)的方法就得變成數(shù)組才能存儲(chǔ)

function Promise(fn){
  //需要成功以及成功時(shí)的回調(diào)
  var doneList = [];
  //一個(gè)實(shí)例的方法,用來(lái)注冊(cè)異步事件
  this.then = function(done ,fail){
    doneList.push(done);
    return this;
  }
  function resolve(){
    doneList.forEach(function(fulfill){
      fulfill();
    });
  }
  fn(resolve);
}

這里promise里面如果是同步的函數(shù)的話呢岗,doneList里面還是空的冕香,所以可以加個(gè)setTimeout來(lái)將這個(gè)放到j(luò)s的最后執(zhí)行。這里主要是參照了promiseA+的規(guī)范后豫,就像這樣

function resolve(){
  setTimeout(function(){
    doneList.forEach(function(fulfill){
      fulfill();
    });
  },0);
}

加入狀態(tài)機(jī)制

這時(shí)如果promise已經(jīng)執(zhí)行完了悉尾,我們?cè)俳opromise注冊(cè)then方法就怎么都不會(huì)執(zhí)行了,這個(gè)不符合預(yù)期挫酿,所以才會(huì)加入狀態(tài)這種東西构眯。更新過(guò)的代碼如下

function Promise(fn){
  //需要成功以及成功時(shí)的回調(diào)
  var state = 'pending';
  var doneList = [];
  //一個(gè)實(shí)例的方法,用來(lái)注冊(cè)異步事件
  this.then = function(done){
    switch(state){
      case "pending":
        doneList.push(done);
        return this;
        break;
      case 'fulfilled':
        done();
        return this;
        break;
    }
  }
  function resolve(){
    state = "fulfilled";
    setTimeout(function(){
      doneList.forEach(function(fulfill){
        fulfill();
      });
    },0);
  }
  fn(resolve);
}

支持串行

這樣子我們就可以將then每次的結(jié)果交給后面的then了早龟。但是我們的promise現(xiàn)在還不支持promise的串行寫(xiě)法惫霸。比如我們想要

var p = new Promise(function(resolve){
    setTimeout(function(){
      resolve(12);
    }, 100);
});
var p2 = new Promise(function(resolve){
    setTimeout(function(){
      resolve(42);
    }, 100);
});
p.then(
      function(name){
        console.log(name);return 33;
      }
  )
  .then(function(id){console.log(id)})
  .then(p2)
  .then(function(home){console.log(home)});

所以我們必須改下then方法。

當(dāng)then方法傳入一般的函數(shù)的時(shí)候葱弟,我們目前的做法是將它推進(jìn)了一個(gè)數(shù)組壹店,然后return this來(lái)進(jìn)行鏈?zhǔn)降恼{(diào)用,并且期望在resolve方法調(diào)用時(shí)執(zhí)行這個(gè)數(shù)組芝加。

最開(kāi)始我是研究的美團(tuán)工程師的一篇博客,到這里的時(shí)候發(fā)現(xiàn)他的解決方案比較跳躍硅卢,于是我就按照普通的正常思路先嘗試了下:

如果傳入一個(gè)promise的話,我們先嘗試?yán)^續(xù)推入數(shù)組中藏杖,在resolve的地方進(jìn)行區(qū)分将塑,發(fā)現(xiàn)是可行的,我先貼下示例代碼制市,然后會(huì)有詳細(xì)的注釋抬旺。

function Promise(fn){
  //需要成功以及成功時(shí)的回調(diào)
  var state = 'pending';
  var doneList = [];
  this.then = function(done){
    switch(state){
      case "pending":
        doneList.push(done);
        return this;
        break;
      case 'fulfilled':
        done();
        return this;
        break;
    }
  }
  function resolve(newValue){
    state = "fulfilled";
    setTimeout(function(){
      var value = newValue;
      //執(zhí)行resolve時(shí),我們會(huì)嘗試將doneList數(shù)組中的值都執(zhí)行一遍
      //當(dāng)遇到正常的回調(diào)函數(shù)的時(shí)候祥楣,就執(zhí)行回調(diào)函數(shù)
      //當(dāng)遇到一個(gè)新的promise的時(shí)候开财,就將原doneList數(shù)組里的回調(diào)函數(shù)推入新的promise的doneList汉柒,以達(dá)到循環(huán)的目的
      for (var i = 0;i<doneList.length;i++){
        var temp = doneList[i](value)
        if(temp instanceof Promise){
            var newP =  temp;
            for(i++;i<doneList.length;i++){
                newP.then(doneList[i]);
            }
        }else{
            value = temp;
        }
      }
    },0);
  }
  fn(resolve);
}
var p = function (){
    return new Promise(function(resolve){
        setTimeout(function(){
          resolve('p 的結(jié)果');
        }, 100);
    });
}
var p2 = function (input){
    return new Promise(function(resolve){
        setTimeout(function(){
            console.log('p2拿到前面?zhèn)魅氲闹担? + input)
            resolve('p2的結(jié)果');
        }, 100);
    });
}
p()
.then(function(res){console.log('p的結(jié)果:' + res); return 'p then方法第一次返回'})
.then(function(res){console.log('p第一次then方法的返回:'+res); return 'p then方法第二次返回'})
.then(p2)
.then(function(res){console.log('p2的結(jié)果:' + res)});

加入reject

我按照正常思路這么寫(xiě)的時(shí)候發(fā)現(xiàn)出了點(diǎn)問(wèn)題,因?yàn)榘凑兆钌厦娴囊?guī)范责鳍。即使一個(gè)promise被rejected碾褂,他注冊(cè)的then方法之后再注冊(cè)的then方法會(huì)可能繼續(xù)執(zhí)行resolve的。即我們?cè)趖hen方法中為了鏈?zhǔn)椒祷氐膖his的status是可能會(huì)被改變的历葛,假設(shè)我們?cè)趯?shí)現(xiàn)中來(lái)改變狀態(tài)而不暴露出來(lái)(這其實(shí)一點(diǎn)也不推薦)正塌。

我直接貼實(shí)現(xiàn)的代碼,還有注釋作為講解

function Promise(fn){
  var state = 'pending';
  var doneList = [];
  var failList= [];
  this.then = function(done ,fail){
    switch(state){
      case "pending":
        doneList.push(done);
        //每次如果沒(méi)有推入fail方法恤溶,我也會(huì)推入一個(gè)null來(lái)占位
        failList.push(fail || null);
        return this;
        break;
      case 'fulfilled':
        done();
        return this;
        break;
      case 'rejected':
        fail();
        return this;
        break;
    }
  }
  function resolve(newValue){
    state = "fulfilled";
    setTimeout(function(){
      var value = newValue;
      for (var i = 0;i<doneList.length;i++){
        var temp = doneList[i](value);
        if(temp instanceof Promise){
            var newP =  temp;
            for(i++;i<doneList.length;i++){
                newP.then(doneList[i],failList[i]);
            }
        }else{
            value = temp;
        }
      }
    },0);
  }
  function reject(newValue){
    state = "rejected";
    setTimeout(function(){
      var value = newValue;
      var tempRe = failList[0](value);
      //如果reject里面?zhèn)魅肓艘粋€(gè)promise乓诽,那么執(zhí)行完此次的fail之后,將剩余的done和fail傳入新的promise中
      if(tempRe instanceof Promise){
        var newP = tempRe;
        for(i=1;i<doneList.length;i++){
            newP.then(doneList[i],failList[i]);
        }
      }else{
        //如果不是promise咒程,執(zhí)行完當(dāng)前的fail之后鸠天,繼續(xù)執(zhí)行doneList
        value =  tempRe;
        doneList.shift();
        failList.shift();
        resolve(value);
      }
    },0);
  }
  fn(resolve,reject);
}
var p = function (){
    return new Promise(function(resolve,reject){
        setTimeout(function(){
          reject('p 的結(jié)果');
        }, 100);
    });
}
var p2 = function (input){
    return new Promise(function(resolve){
        setTimeout(function(){
            console.log('p2拿到前面?zhèn)魅氲闹担? + input)
            resolve('p2的結(jié)果');
        }, 100);
    });
}
p()
.then(function(res){console.log('p的結(jié)果:' + res); return 'p then方法第一次返回'},function(value){console.log(value);return 'p then方法第一次錯(cuò)誤的返回'})
.then(function(res){console.log('p第一次then方法的返回:'+res); return 'p then方法第二次返回'})
.then(p2)
.then(function(res){console.log('p2的結(jié)果:' + res)});

延伸閱讀2:理解 JavaScript 的 async/await

async 和 await 在干什么

任意一個(gè)名稱都是有意義的,先從字面意思來(lái)理解帐姻。async 是“異步”的簡(jiǎn)寫(xiě)稠集,而 await 可以認(rèn)為是 async wait 的簡(jiǎn)寫(xiě)。所以應(yīng)該很好理解 async 用于申明一個(gè) function 是異步的饥瓷,而 await 用于等待一個(gè)異步方法執(zhí)行完成剥纷。

async 起什么作用

這個(gè)問(wèn)題的關(guān)鍵在于,async 函數(shù)是怎么處理它的返回值的呢铆!

我們當(dāng)然希望它能直接通過(guò) return 語(yǔ)句返回我們想要的值晦鞋,但是如果真是這樣,似乎就沒(méi) await 什么事了刺洒。所以鳖宾,寫(xiě)段代碼來(lái)試試,看它到底會(huì)返回什么:

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

const result = testAsync();
console.log(result);  // Promise {<resolved>: "hello"}

看到輸出就恍然大悟了——輸出的是一個(gè) Promise 對(duì)象逆航。

所以鼎文,async 函數(shù)返回的是一個(gè) Promise 對(duì)象。從文檔中也可以得到這個(gè)信息因俐。async 函數(shù)(包含函數(shù)語(yǔ)句拇惋、函數(shù)表達(dá)式、Lambda表達(dá)式)會(huì)返回一個(gè) Promise 對(duì)象抹剩,如果在函數(shù)中 return 一個(gè)直接量撑帖,async 會(huì)把這個(gè)直接量通過(guò) Promise.resolve() 封裝成 Promise 對(duì)象。

async 函數(shù)返回的是一個(gè) Promise 對(duì)象澳眷,所以在最外層不能用 await 獲取其返回值的情況下胡嘿,我們當(dāng)然應(yīng)該用原來(lái)的方式:then() 鏈來(lái)處理這個(gè) Promise 對(duì)象,就像這樣

testAsync().then(v => {
    console.log(v);    // 輸出 hello async
});

現(xiàn)在回過(guò)頭來(lái)想下钳踊,如果 async 函數(shù)沒(méi)有返回值衷敌,又該如何勿侯?很容易想到,它會(huì)返回 Promise.resolve(undefined)缴罗。

聯(lián)想一下 Promise 的特點(diǎn)——無(wú)等待助琐,所以在沒(méi)有 await 的情況下執(zhí)行 async 函數(shù),它會(huì)立即執(zhí)行面氓,返回一個(gè) Promise 對(duì)象兵钮,并且,絕不會(huì)阻塞后面的語(yǔ)句舌界。這和普通返回 Promise 對(duì)象的函數(shù)并無(wú)二致掘譬。

那么下一個(gè)關(guān)鍵點(diǎn)就在于 await 關(guān)鍵字了。

await 到底在等啥

一般來(lái)說(shuō)呻拌,都認(rèn)為 await 是在等待一個(gè) async 函數(shù)完成屁药。不過(guò)按語(yǔ)法說(shuō)明,await 等待的是一個(gè)表達(dá)式柏锄,這個(gè)表達(dá)式的計(jì)算結(jié)果是 Promise 對(duì)象或者其它值(換句話說(shuō),就是沒(méi)有特殊限定)复亏。

因?yàn)?async 函數(shù)返回一個(gè) Promise 對(duì)象趾娃,所以 await 可以用于等待一個(gè) async 函數(shù)的返回值——這也可以說(shuō)是 await 在等 async 函數(shù),但要清楚缔御,它等的實(shí)際是一個(gè)返回值抬闷。注意到 await 不僅僅用于等 Promise 對(duì)象,它可以等任意表達(dá)式的結(jié)果耕突,所以笤成,await 后面實(shí)際是可以接普通函數(shù)調(diào)用或者直接量的。所以下面這個(gè)示例完全可以正確運(yùn)行

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();

await 等到了要等的眷茁,然后呢

await 等到了它要等的東西炕泳,一個(gè) Promise 對(duì)象,或者其它值上祈,然后呢培遵?我不得不先說(shuō),await 是個(gè)運(yùn)算符登刺,用于組成表達(dá)式籽腕,await 表達(dá)式的運(yùn)算結(jié)果取決于它等的東西。

如果它等到的不是一個(gè) Promise 對(duì)象纸俭,那 await 表達(dá)式的運(yùn)算結(jié)果就是它等到的東西皇耗。

如果它等到的是一個(gè) Promise 對(duì)象,await 就忙起來(lái)了揍很,它會(huì)阻塞后面的代碼郎楼,等著 Promise 對(duì)象 resolve万伤,然后得到 resolve 的值,作為 await 表達(dá)式的運(yùn)算結(jié)果箭启。

看到上面的阻塞一詞壕翩,心慌了吧……放心,這就是 await 必須用在 async 函數(shù)中的原因傅寡。async 函數(shù)調(diào)用不會(huì)造成阻塞放妈,它內(nèi)部所有的阻塞都被封裝在一個(gè) Promise 對(duì)象中異步執(zhí)行。

async/await 幫我們干了啥

作個(gè)簡(jiǎn)單的比較

上面已經(jīng)說(shuō)明了 async 會(huì)將其后的函數(shù)(函數(shù)表達(dá)式或 Lambda)的返回值封裝成一個(gè) Promise 對(duì)象荐操,而 await 會(huì)等待這個(gè) Promise 完成芜抒,并將其 resolve 的結(jié)果返回出來(lái)。

現(xiàn)在舉例托启,用 setTimeout 模擬耗時(shí)的異步操作宅倒,先來(lái)看看不用 async/await 會(huì)怎么寫(xiě)

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

takeLongTime().then(v => {
    console.log("got", v);
});

如果改用 async/await 呢,會(huì)是這樣

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

async function test() {
    const v = await takeLongTime();
    console.log(v);
}

test();

眼尖的同學(xué)已經(jīng)發(fā)現(xiàn) takeLongTime() 沒(méi)有申明為 async屯耸。實(shí)際上拐迁,takeLongTime() 本身就是返回的 Promise 對(duì)象,加不加 async 結(jié)果都一樣疗绣,如果沒(méi)明白线召,請(qǐng)回過(guò)頭再去看看上面的“async 起什么作用”。

又一個(gè)疑問(wèn)產(chǎn)生了多矮,這兩段代碼缓淹,兩種方式對(duì)異步調(diào)用的處理(實(shí)際就是對(duì) Promise 對(duì)象的處理)差別并不明顯,甚至使用 async/await 還需要多寫(xiě)一些代碼塔逃,那它的優(yōu)勢(shì)到底在哪讯壶?

async/await 的優(yōu)勢(shì)在于處理 then 鏈

單一的 Promise 鏈并不能發(fā)現(xiàn) async/await 的優(yōu)勢(shì),但是湾盗,如果需要處理由多個(gè) Promise 組成的 then 鏈的時(shí)候伏蚊,優(yōu)勢(shì)就能體現(xiàn)出來(lái)了(很有意思,Promise 通過(guò) then 鏈來(lái)解決多層回調(diào)的問(wèn)題淹仑,現(xiàn)在又用 async/await 來(lái)進(jìn)一步優(yōu)化它)丙挽。

假設(shè)一個(gè)業(yè)務(wù),分多個(gè)步驟完成匀借,每個(gè)步驟都是異步的颜阐,而且依賴于上一個(gè)步驟的結(jié)果。我們?nèi)匀挥?setTimeout 來(lái)模擬異步操作:

/**
 * 傳入?yún)?shù) n吓肋,表示這個(gè)函數(shù)執(zhí)行的時(shí)間(毫秒)
 * 執(zhí)行的結(jié)果是 n + 200凳怨,這個(gè)值將用于下一步驟
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

現(xiàn)在用 Promise 方式來(lái)實(shí)現(xiàn)這三個(gè)步驟的處理

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

輸出結(jié)果 resultstep3() 的參數(shù) 700 + 200 = 900doIt() 順序執(zhí)行了三個(gè)步驟,一共用了 300 + 500 + 700 = 1500 毫秒肤舞,和 console.time()/console.timeEnd() 計(jì)算的結(jié)果一致紫新。

如果用 async/await 來(lái)實(shí)現(xiàn)呢,會(huì)是這樣

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

結(jié)果和之前的 Promise 實(shí)現(xiàn)是一樣的李剖,但是這個(gè)代碼看起來(lái)是不是清晰得多芒率,幾乎跟同步代碼一樣

除了覺(jué)得執(zhí)行時(shí)間變長(zhǎng)了之外,似乎和之前的示例沒(méi)啥區(qū)別案菟场偶芍!別急,認(rèn)真想想如果把它寫(xiě)成 Promise 方式實(shí)現(xiàn)會(huì)是什么樣子德玫?

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => {
            return step2(time1, time2)
                .then(time3 => [time1, time2, time3]);
        })
        .then(times => {
            const [time1, time2, time3] = times;
            return step3(time1, time2, time3);
        })
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

沒(méi)有感覺(jué)有點(diǎn)復(fù)雜的樣子匪蟀?那一堆參數(shù)處理,就是 Promise 方案的死穴—— 參數(shù)傳遞太麻煩了宰僧,看著就暈材彪!

參考:

前端基礎(chǔ)進(jìn)階(十三):透徹掌握Promise的使用,讀這篇就夠了

Promise簡(jiǎn)單實(shí)現(xiàn)(正常思路版)

剖析 Promise 之基礎(chǔ)篇

理解 JavaScript 的 async/await

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末琴儿,一起剝皮案震驚了整個(gè)濱河市段化,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌造成,老刑警劉巖穗泵,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異谜疤,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)现诀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)夷磕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人仔沿,你說(shuō)我怎么就攤上這事坐桩。” “怎么了封锉?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵绵跷,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我成福,道長(zhǎng)碾局,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任奴艾,我火速辦了婚禮净当,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己像啼,他們只是感情好俘闯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著忽冻,像睡著了一般真朗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上僧诚,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天遮婶,我揣著相機(jī)與錄音,去河邊找鬼振诬。 笑死蹭睡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的赶么。 我是一名探鬼主播肩豁,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼辫呻!你這毒婦竟也來(lái)了清钥?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤放闺,失蹤者是張志新(化名)和其女友劉穎祟昭,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體怖侦,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡篡悟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了匾寝。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片搬葬。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖艳悔,靈堂內(nèi)的尸體忽然破棺而出急凰,到底是詐尸還是另有隱情,我是刑警寧澤猜年,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布抡锈,位于F島的核電站,受9級(jí)特大地震影響乔外,放射性物質(zhì)發(fā)生泄漏床三。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一杨幼、第九天 我趴在偏房一處隱蔽的房頂上張望勿璃。 院中可真熱鬧,春花似錦、人聲如沸补疑。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)莲组。三九已至诊胞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間锹杈,已是汗流浹背撵孤。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留竭望,地道東北人邪码。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像咬清,于是被迫代替她去往敵國(guó)和親闭专。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355