淺談promise和js執行機制(一)

chaos_G發表於2019-05-22

作為一個入門級前端,今天是一個非常值得紀念的日子,因為這是我第一次在論壇上發表帖子,作為起步。雖然我覺得自己水平還是十分的有限,對一些細節的理解還不是很透徹,但是還是要邁出這一步,不管是給別的新手作為學習參考,還是自己以後回顧,總覺得需要把自己的成長記錄下來,希望自己以後還是要多堅持,如果有不對的地方還是希望大家及時提出來,共同進步

今天有時間翻到了es6的promise,可能大家都對此熟悉不過,我之前一直覺得promise也很簡單,但是今天確實讓我對promise有了一個新的瞭解,以前的理解可能是錯誤的。。。先來看看官方的promise的定義是:

所謂Promise,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise 是一個物件,從它可以獲取非同步操作的訊息。Promise 提供統一的 API,各種非同步操作都可以用同樣的方法進行處理。

特點:

(1)物件的狀態不受外界影響。Promise物件代表一個非同步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法改變

(2)一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise物件的狀態改變,只有兩種可能:從pending變為fulfilled和從pending變為rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型)。如果改變已經發生了,你再對Promise物件新增回撥函式,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。

初讀上面這段話我讀了不下5次,但是我還是沒能真正瞭解其真正表達的意思,於是我查閱了部分資料,終於找到了一個比較容易理解的說法,這個估計小白應該是可以看得懂得。

就拿做飯吃飯洗碗來舉例子吧,這是三個步驟,第一步做飯,再第二步吃飯的時候我們需要拿到第一步做的飯,在第三步洗碗的時候我們需要拿到第二步的碗筷,而且這三個步驟必須是按照順序執行,有嚴格的先後順序。

//做飯
function cook(){
    console.log('開始做飯。');
    var p = new Promise(function(resolve, reject){        //做一些非同步操作
        setTimeout(function(){
            console.log('做飯完畢!');
            resolve('雞蛋炒飯');
        }, 1000);
    });
    return p;
}
 
//吃飯
function eat(data){
    console.log('開始吃飯:' + data);
    var p = new Promise(function(resolve, reject){        //做一些非同步操作
        setTimeout(function(){
            console.log('吃飯完畢!');
            resolve('一個碗和一雙筷子');
        }, 2000);
    });
    return p;
}
 //洗碗
function wash(data){
    console.log('開始洗碗:' + data);
    var p = new Promise(function(resolve, reject){        //做一些非同步操作
        setTimeout(function(){
            console.log('洗碗完畢!');
            resolve('乾淨的碗筷');
        }, 2000);
    });
    return p;
}

//函式呼叫
cook().then(res1 => {
	console.log(res1,'這是第一步傳遞給第二步的引數')
	return eat(res1)
}).then(res2 => {
	console.log(res2,'這是第二步傳給第三步的引數')
	return wash(res2)
}).then(res3 => {
	console.log(res3,'飯吃完了還你乾淨的碗筷')
})

複製程式碼

結果如下:

淺談promise和js執行機制(一)

看完上面的程式碼大家可能會有好多疑問,我在這裡把我當時初學promise的疑問和大家分享一下:

(1)為什麼我要在promise物件外面需要用一個函式來包裹起來呢?

答:Promise也有一些缺點。首先,無法取消Promise,一旦新建它就會立即執行,無法中途取消。其次,如果不設定回撥函式,Promise內部丟擲的錯誤,不會反應到外部。第三,當處於pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。

這是promise的一些缺點,一旦新建它就會立即執行 所以為了控制這個promise物件什麼時候執行,在開發過程中我們需要在外面包裹一個函式,通過呼叫函式的形式來控制promise的執行。大家可以在自己的編輯器中試試。

new Promise(function(resolve, reject) {
	setTimeout(()=> {
		console.log('開車!!!')
	},2000)
});
複製程式碼

淺談promise和js執行機制(一)

(2)在每一個函式中我為什麼要將我的promise物件用一個變數接收然後return出去呢?

答:因為有了Promise物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式。此外,Promise物件提供統一的介面,使得控制非同步操作更加容易。

//函式呼叫
cook().then(res1 => {
	console.log(res1,'這是第一步傳遞給第二步的引數')
	return eat(res1)
}).then(res2 => {
	console.log(res2,'這是第二步傳給第三步的引數')
	return wash(res2)
}).then(res3 => {
	console.log(res3,'飯吃完了還你乾淨的碗筷')
})
複製程式碼

大家看我的這段程式碼 在執行了cook()函式的時候cook函式return出來一個promise物件,promise物件上有.then()或.catch()方法,那麼直接cook().then就可以在.then()方法中我們可以拿到promise物件中向外傳遞的引數,這個引數我們將傳遞給下一個eat()函式,作為eat()函式的引數繼續執行。 而eat()函式也return了一個promise物件。那麼我們將我們的這段程式碼拆解一下:

第一步: cook()    //拿到的是cook return出來的promise物件

第二步: cook().then(res => {
 console.log(res)    //拿到內部向外部傳遞的引數 
})

第三步: cook().then(res => {
    console.log(res)
    return eat(res)    //eat執行以後return的是eat的promise物件,然後再把這個物件繼續向外return
})    //那麼第三步的最終結果就是eat()的promise物件

第四步: cook().then(res => {
    console.log(res)
    return eat(res) //此時的結果是eat()的promise物件
}).then(res2 => {
    // 此時eat的promise又有.then方法 .....以此類推
})

複製程式碼

我們就這樣一步一步的完成了整個做飯、吃飯、洗碗的整個流程。 縱觀以上程式碼你會發現雖然每一個操作流程中我都是以非同步函式setTimeout來進行的,但是在呼叫過程中確是按照 做飯-吃飯-洗碗的正常流程表達的。這不是promise的有了Promise物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式

(3)如何判斷失敗的情況呢?

//做飯
function cook(){
    console.log('開始做飯。');
    var p = new Promise(function(resolve, reject){        //做一些非同步操作
        setTimeout(function(){
            console.log('飯糊了....沒法吃');
            reject('糊了的飯');
        }, 1000);
    });
    return p;
}
//吃飯
function eat(data){
    console.log('開始吃飯:' + data);
    var p = new Promise(function(resolve, reject){        //做一些非同步操作
        setTimeout(function(){
            console.log('吃飯完畢!');
            resolve('一個碗和一雙筷子');
        }, 2000);
    });
    return p;
}

cook().then(res1 => {
	console.log(res1,'這是第一步傳遞給第二步的引數')
	return eat(res1)
}).catch(err => {
	console.log(err,'返回錯誤')
})
複製程式碼

catch()方法用來指定 reject 的回撥。

(4)如何在多個非同步操作都完成後才執行回撥呢?

function tackBus1(){
    var p = new Promise(function(resolve, reject){
        //做一些非同步操作
        setTimeout(function(){
            console.log('甲正在上車');
            resolve('甲坐好了');
        }, 1000);
    });
    return p;            
}
function tackBus2(){
    var p = new Promise(function(resolve, reject){
        //做一些非同步操作
        setTimeout(function(){
            console.log('乙正在上車');
            resolve('乙坐好了');
        }, 2000);
    });
    return p;            
}
function tackBus3(){
    var p = new Promise(function(resolve, reject){
        //做一些非同步操作
        setTimeout(function(){
            console.log('丙正在上車');
            resolve('丙坐好了');
        }, 3000);
    });
    return p;            
}
Promise.all([tackBus1(),tackBus2(),tackBus3()]).then(res => {
	console.log(res)
})
複製程式碼

司機在等人上車,在乘客都坐穩了以後開車發車,這就用Promise.all來執行,all接收一個陣列引數,裡面的值最終都算返回Promise物件。這樣,三個非同步操作的並行執行的,等到它們都執行完後才會進到then裡面。那麼,三個非同步操作返回的資料哪裡去了呢?都在then裡面呢,all會把所有非同步操作的結果放進一個陣列中傳給then,就是上面的results。所以上面程式碼的輸出結果就是:

淺談promise和js執行機制(一)

這是.all的方法,promise還有一種.race的方法,它和all方法的區別就是: all方法的效果實際上是誰跑的慢,以誰為準執行回撥,那麼相對的就有另一個方法誰跑的快,以誰為準執行回撥,剛剛的執行結果我設定了他們的時間間隔分別是1s,2s,3s,在等最慢的執行完以後才執行了all這個回撥方法,現在我們們來試試promise.race方法

function tackBus1(){
    var p = new Promise(function(resolve, reject){
        //做一些非同步操作
        setTimeout(function(){
            console.log('甲正在上車');
            resolve('甲坐好了');
        }, 1000);
    });
    return p;            
}
function tackBus2(){
    var p = new Promise(function(resolve, reject){
        //做一些非同步操作
        setTimeout(function(){
            console.log('乙正在上車');
            resolve('乙坐好了');
        }, 2000);
    });
    return p;            
}
function tackBus3(){
    var p = new Promise(function(resolve, reject){
        //做一些非同步操作
        setTimeout(function(){
            console.log('丙正在上車');
            resolve('丙坐好了');
        }, 3000);
    });
    return p;            
}
Promise.race([tackBus1(),tackBus2(),tackBus3()]).then(res => {
	console.log(res)
})
複製程式碼

結果如下:

淺談promise和js執行機制(一)
可以看出來在甲執行完畢後立即就執行了.race()方法,但是不耽誤其他兩個非同步操作的進行,.race()中拿到的引數也只是當前最先執行完的非同步操作中傳遞出來的引數。

(4)由promise聯絡到的js的巨集任務和微任務

在看到promise的時候有一個地方還是令我有困惑,現在先留一個懸念,大家可以先看看下面這段程式碼,你覺得輸出結果是什麼呢? 我們下回見!

setTimeout(function(){
  console.log('1')
});
new Promise(function(resolve){
    console.log('2');
    resolve();
}).then(function(){
    console.log('3')
});
console.log('4');
複製程式碼

相關文章