問題
假設我們有一個需求:1. 獲取使用者所在的城市;2. 根據城市獲取天氣;3. 根據天氣獲取出行建議。那我們的程式碼應該是這樣的
getCity(url1, function(){
getWeather(url2, function(weather){
getSuggestion(url3, function(suggestion){
console.log(suggestion)
})
})
})
複製程式碼
這就是典型的非同步 callback 『回撥地獄』,程式碼層層巢狀可讀性很差。關於非同步的解決方式可參考這篇文章 Node.js非同步漫談
使用 Promise 是解決上述問題的一種方式,這裡我們不去講如何去使用內建的 Promise,而是帶大家手把手寫一個 Promise。
思路
我們希望有一個工具,能讓我們使用下面的的寫法來實現上述功能
promise.then(getCity)
.then(getWeather)
.then(getSuggestion)
複製程式碼
整理下思路:
- Tool 是一個物件
- Tool 有 then 這個方法
- 執行 then 方法返回的應該還是 Tool 物件
function Promise(){}
Promise.prototype.then = function(fn){
//todo...
return this
}
var promise = new Promise()
複製程式碼
那如何實現非同步操作序列執行呢?關鍵思路如下:
在 promise 物件內容維護一個陣列,當執行 promise.then(getCity) .then(getWeather) .then(getSuggestion) 時把這幾個函式依次放入陣列中。注意此時這些函式並沒有執行。
執行promise.resolve()時,會從陣列中拿出一個函式去執行。函式執行的過程中在非同步操作的結果到來後會再次自動呼叫 promise.resolve(),觸發下一個函式的取出並執行,下一個函式結果到來後再次自動呼叫promise.resolve() ......,這樣就實現了非同步鏈式執行。和原子彈爆炸原理類似。
所以需要對原來的非同步函式做一點小小的改動,在資料到來的地方,加一個promise.resolve,用於啟動後續函式的執行
function getCity(){
var xhr = new XMLHttpRequest()
xhr.open(url, 'get', true)
xhr.onload = function(){
if (this.status == 200) {
promise.resolve(xhr.responseText) //注意這裡的promise.resolve
}
}
xhr.send()
}
複製程式碼
現在我們就能實現一個簡易的 Promise 了,這裡我們先暫不考慮特殊情況:
function Promise(){
this.callbacks = []
}
Promise.prototype.then = function(fn){
this.callbacks.push(fn) //呼叫 then 時把函式放入陣列
return this //返回當前物件供鏈式呼叫
}
Promise.prototype.resolve = function(data){
var fn = this.callbacks.shift() //當呼叫resolve時拿出一個函式
fn&&fn(data) //執行這個函式,並且把resolve的引數做引數
}
var promise = new Promise()
promise.then(getCity)
.then(getWeather)
.then(getSuggestion)
promise.resolve() //啟動
function getCity(){
setTimeout(function(){
promise.resolve('杭州')
}, 1000)
}
function getWeather(city){
setTimeout(function(){
promise.resolve(city + ' 晴天')
}, 1000)
}
function getSuggestion(weather){
setTimeout(function(){
console.log(weather + ' 天氣不錯,可攜女友與狗出行')
}, 1000)
}
複製程式碼
當然,如果覺得promise.resolve 單獨啟動一次看起來不舒服,也可以這樣執行
getCity()
.then(getWeather)
.then(getSuggestion)
function getCity(){
setTimeout(function(){
promise.resolve('杭州')
}, 1000)
return promise //注意這裡
}
複製程式碼
實現
到此為止我們已經寫了一個簡單的 Promise,甚至能滿足很大一部分使用需求。但有個問題,每次非同步操作可能存在失敗的情況,而上面的程式碼並沒有非同步函式的失敗處理。下面考慮非同步的失敗處理,原理和上面類似,可以閱讀程式碼動手做個測試
class Promise {
constructor (){
this.callbacks = []
this.oncatch = null
}
reject(result){
this.complete('reject', result)
}
resolve(result){
this.complete('resolve', result)
}
complete(type, result){
if(type==='reject' && this.oncatch){
this.callbacks = []
this.oncatch(result)
}else if(this.callbacks[0]) {
var handlerObj = this.callbacks.shift()
if(handlerObj[type]){
handlerObj[type](result)
}
}
}
then(onsuccess, onfail){
this.callbacks.push({
resolve: onsuccess,
reject: onfail
})
return this
}
catch(onfail){
this.oncatch = onfail
return this
}
}
var promise = new Promise()
fn1().then(fn2, onfn1error)
.then(fn3, onfn2error)
.catch(onerror)
function fn1(){
setTimeout(function(){
if(Math.random()>0.5){
promise.resolve('杭州')
}else{
promise.reject('fn1 error')
}
})
return promise
}
複製程式碼
總結
現在我們已經手寫了一個 Promise, 當然和瀏覽器內建物件Promise原理有些差異, 但至少『達到』類似的目的了
加微訊號: astak10或者長按識別下方二維碼進入前端技術交流群 ,暗號:寫程式碼啦
每日一題,每週資源推薦,精彩部落格推薦,工作、筆試、面試經驗交流解答,免費直播課,群友輕分享... ,數不盡的福利免費送