原來你是這樣的Promise

windlany發表於2018-04-15

1. Promise簡介

promise是非同步程式設計的一種解決方案,它出現的初衷是為了解決回撥地獄的問題。

打個比方,我需要:--(延遲1s)--> 輸出1 --(延遲2s)--> 輸出2 --(延遲3s)--> 輸出3,通常寫法:

setTimeout(()=> {
    console.log('1');
    setTimeout(()=> {
        console.log('2');
        setTimeout(()=> {
            console.log('3'); 
        }, 3000)
    }, 2000)
}, 1000)
複製程式碼

這樣的多重的巢狀的回撥被稱為回撥地獄,這樣的程式碼可讀性很差,不利於理解。

如果用promise的話畫風一轉

function delay(time, num) {
    return new Promise((res, rej)=> {
        setTimeout(()=> {
            console.log(num);
            res();
        }, time*1000)
    });
}
delay(1, 1).then(()=> {
    return delay(2, 2);
}).then(()=> {
    delay(3, 3);
})
複製程式碼

使用了promise的鏈式呼叫,程式碼結構更清晰。

是不是很棒?那還不趕快get起來~

2. Promise的使用

呼叫方式如下:

new Promise((resolve, reject)=> {
    if('some option') {
        resolve('some value');
    } else {
        reject('some error');
    }
}).then(
    val=> {
        // ...
    },
    error=> {
        // ...
    }
)
複製程式碼

Promise建構函式接收一個函式型引數fn,fn有兩個引數,分別是:resolve、reject,Promise還有一個Promise.prototype.then方法,該方法接收兩個引數,分別是成功的回撥函式succ和失敗的回撥函式error。

在fn中呼叫resolve會觸發then中的succ回撥,呼叫reject會觸發error回撥。

2.1 引數傳遞

  • 在fn內部呼叫resolve/reject傳入的引數會作為相應引數傳入相應的回撥函式
new Promise((res, rej)=> {
    res('happy')
}).then(val=> {
    console.log(val);  // happy
});

new Promise((res, rej)=> {
    rej('error!');
}).then(val=> {}, err=> {
    console.log(err);  // error!
});
複製程式碼
  • 鏈式呼叫時若上一級沒有傳遞值則預設為undefined
new Promise((res, rej)=> {
    res('a');    
}).then(val=> {
    return 'b'
}).then(val=> {
    console.log(val);  // 'b'
}).then((val)=> {
    console.log(val);  // 'undefined'
});
複製程式碼
  • 若上一級的then中傳遞的並非函式,則忽略該級
new Promise((res, rej)=> {
    res('a');    
}).then(val=> {
    return 'b';
}).then(val=> {
    console.log(val);  // 'b'
	return 'c';
}).then({  // 並非函式
    name: 'lan'
}).then((val)=> {
    console.log(val);   // 'c'
});
複製程式碼

2.2 引數傳遞例題

let doSomething = function() {
    return new Promise((resolve, reject) => {
        resolve('返回值');
    });
};

let doSomethingElse = function() {
    return '新的值';
}

doSomething().then(function () {
    return doSomethingElse();
}).then(resp => {
    console.warn(resp);
    console.warn('1 =========<');
});

doSomething().then(function () {
    doSomethingElse();
}).then(resp => {
    console.warn(resp);
    console.warn('2 =========<');
});

doSomething().then(doSomethingElse()).then(resp => {
    console.warn(resp);
    console.warn('3 =========<');
});

doSomething().then(doSomethingElse).then(resp => {
    console.warn(resp);
    console.warn('4 =========<');
});
複製程式碼

結合上面的講解想一想會輸出什麼?(答案及解析

3. Promise.prototype.then

當Promise中的狀態(pending ---> resolved or rejected)發生變化時才會執行then方法。

  • 呼叫then返回的依舊是一個Promise例項 ( 所以才可以鏈式呼叫... )
new Promise((res, rej)=> {
    res('a');
}).then(val=> {
    return 'b';
});

// 等同於
new Promise((res, rej)=> {
    res('a');
}).then(val=> {
    return new Promise((res, rej)=> {
        res('b');
    });
});
複製程式碼
  • then中的回撥總會非同步執行
new Promise((res, rej)=> {
    console.log('a');
    res('');
}).then(()=> {
    console.log('b');
});
console.log('c');
// a c b
複製程式碼
  • 如果你不在Promise的引數函式中呼叫resolve或者reject那麼then方法永遠不會被觸發
new Promise((res, rej)=> {
    console.log('a'); 
}).then(()=> {
    console.log('b');
});
console.log('c'); 
// a c
複製程式碼

4. Promise的靜態方法

Promise還有四個靜態方法,分別是resolverejectallrace,下面我們一一介紹一下。

4.1 Promise.resolve()

除了通過new Promise()的方式,我們還有兩種建立Promise物件的方法,Promise.resolve()相當於建立了一個立即resolve的物件。如下兩段程式碼作用相同:

Promise.resolve('a');

new Promise((res, rej)=> {
    res('a');
});
複製程式碼

當然根據傳入的引數不同,Promise.resolve()也會做出不同的操作。

  • 引數是一個 Promise 例項

如果引數是 Promise 例項,那麼Promise.resolve將不做任何修改、原封不動地返回這個例項。

  • 引數是一個thenable物件

thenable物件指的是具有then方法的物件,比如下面這個物件。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};
複製程式碼

Promise.resolve方法會將這個物件轉為 Promise物件,然後就立即執行thenable物件的then方法。

  • 引數不是具有then方法的物件,或根本就不是物件

如果引數是一個原始值,或者是一個不具有then方法的物件,則Promise.resolve方法返回一個新的 Promise 物件,狀態為resolved。

  • 不帶有任何引數

Promise.resolve方法允許呼叫時不帶引數,直接返回一個resolved狀態的 Promise 物件。

值得注意的一點是該靜態方法是在本次事件輪詢結束前呼叫,而不是在下一次事件輪詢開始時呼叫。關於事件輪詢可以看這裡——>JavaScript 執行機制詳解:再談Event Loop

4.2 Promise.reject()

和Promise.resolve()類似,只不過一個是觸發成功的回撥,一個是觸發失敗的回撥

4.3 Promise.all()

Promise的all方法提供了並行執行非同步操作的能力,並且在所有非同步操作執行完後才執行回撥。

function asyncFun1() {
	return new Promise((res, rej)=> {
		setTimeout(()=> { 
			res('a');
		}, 1000);
	}); 
}
function asyncFun2() {
	return new Promise((res, rej)=> {
		setTimeout(()=> { 
			res('b');
		}, 1000);
	}); 
}
function asyncFun3() {
	return new Promise((res, rej)=> {
		setTimeout(()=> { 
			res('c');
		}, 1000);
	}); 
}
Promise.all([asyncFun1(), asyncFun2(), asyncFun3()]).then((val)=> {
    console.log(val);
});
Promise.all([asyncFun1(), asyncFun2(), asyncFun3()]).then((val)=> {
    console.log(val);  // ['a', 'b', 'c']
});
複製程式碼

用Promise.all來執行,all接收一個陣列引數,裡面的值最終都算返回Promise物件。這樣,三個非同步操作的並行執行的,等到它們都執行完後才會進到then裡面。有了all,你就可以並行執行多個非同步操作,並且在一個回撥中處理所有的返回資料。

適用場景:開啟網頁時,預先載入需要用到的各種資源如圖片、flash以及各種靜態檔案。所有的都載入完後,我們再進行頁面的初始化。

4.4 Promise.race()

race()和all相反,all()是陣列中所有Promise都執行完畢就執行then,而race()是一旦有一個Promise執行完畢就會執行then(),用上面的三個Promise返回值函式舉例

Promise.race([asyncFun1(), asyncFun2(), asyncFun3()]).then((val)=> {
    console.log(val);  // a
});
複製程式碼

5. 鏈式呼叫經典例題

看了這麼多關於Promise的知識,我們來做一道題鞏固一下。

寫一個類Man實現以下鏈式呼叫

呼叫方式:
new Man('lan').sleep(3).eat('apple').sleep(5).eat('banana');
列印:
'hello, lan' -(等待3s)--> 'lan eat apple' -(等待5s)--> 'lan eat banana'
複製程式碼

思路:

  • 在原型方法中返回this達到鏈式呼叫的目的
  • 等待3s執行的效果可以用Promise & then實現

具體實現如下:

class Man {
    constructor(name) {
        this.name = name;
        this.sayName();
        this.rope = Promise.resolve();  // 定義全域性Promise作鏈式呼叫
    }
    sayName() {
        console.log(`hello, ${this.name}`);
    }
    sleep(time) {
        this.rope = this.rope.then(()=> {
            return new Promise((res, rej)=> {
                setTimeout(()=> {
                    res();
                }, time*1000);
            });
        });
        return this;
    }
    eat(food) {
        this.rope = this.rope.then(()=> {
            console.log(`${this.name} eat ${food}`); 
        });

        return this;
    }
}

new Man('lan').sleep(3).eat('apple').sleep(5).eat('banana');
複製程式碼

ok!不知道你有沒有看懂呢?如果能完全理解程式碼那你的Promise可以通關了,順便來個小思考,下面這種寫法可以嗎?和上面相比有什麼區別?:

class Man1345 {
    constructor(name) {
        this.name = name;
        this.sayName(); 
    }
    sayName() {
        console.log(`hello, ${this.name}`);
    }
    sleep(time) { 
        this.rope = new Promise((res, rej)=> {
                setTimeout(()=> {
                    res();
                }, time*1000);
            }); 
        return this;
    }
    eat(food) {
        this.rope = this.rope.then(()=> { 
            console.log(`${this.name} eat ${food}`);  
        });

        return this;
    }
}

new Man('lan').sleep(3).eat('apple').sleep(5).eat('banana');
複製程式碼

簡單的說,第二段程式碼的執行結果是

'hello, lan' -(等待3s)--> 'lan eat apple' ---> 'lan eat banana'
複製程式碼

為什麼會出現這種差別? 因為第二段程式碼每一次呼叫sleep都會new一個新的Promise物件,呼叫了兩次sleep就new了兩個Promise物件。這兩個物件是非同步並行執行,會造成兩句eat同時顯示。

和以下情況類似

var time1 = setTimeout(()=> {
	console.log('a');
}, 1000)
var time2 = setTimeout(()=> {
	console.log('b');
}, 1000)
// 同時輸出 a b
複製程式碼

抽象一點的講解是:

// 第一段正確的程式碼的執行為
var p1 = new Promise().then('停頓3s').then('列印食物').then('停頓5s').then('列印食物');

// 第二段程式碼的執行行為,p1、p2非同步並行執行
var p1 = new Promise().then('停頓3s').then('列印食物');
var p2 = new Promise().then('停頓5s').then('列印食物');
複製程式碼

總結

Promise的經常用到的地方:

  • 擺脫回撥地獄
  • 多個非同步任務同步

Promise是我們的好幫手,不過還有另一種方法也可以做到,那就是async&await,可以多多瞭解一下。

參考資料

ECMAScript 6 入門

通俗淺顯的理解Promise中的then

大白話講解promise

相關文章