Prommise
簡介
回想一下我們使用傳統方式(回調函數)是如何來解決異步編程依賴問題的
login(param, function() {
consloe.log('登錄成功,開始獲取用戶信息')
getUser(param, function() {
console.log('已經獲取到用戶信息,現在來獲取菜單')
getMenu(param, function() {
console.log('獲取到菜單了')
})
})
})
恐怖嗎?這還只是三層依賴,如果有更深的依賴關係,那真的會陷入 ajax 的回調地獄。爲了解決這個問題, 晴空一聲霹靂響,天上掉下個 Promise
Promise 是異步編程的一種解決方案,ES6 將其寫入了語言標準。譯爲 “承諾”,你將一件事情交給他管理,等到這個事情有了結果,或者成功或者失敗,它會承諾給你一個結果。
Promise 可以理解爲一個容器,它肚子裏裝着一個函數,通常在這個函數裏進行一些異步請求,會在將來某個時候拿到結果。Promsie 對象具有以下兩個特點:
- 三個狀態:pending(進行中)、fullfiled(已成功)、rejected(已失敗),狀態只能在異步函數中改變,外界無法改變其狀態
- 狀態不可逆:狀態只能從 pending -> fullfiled 或 pending -> rejected
基本用法
先介紹下 Promise 的基本用法,ES6 規定,Promise 是一個構造函數,使用生成 Promise 實例。
var login = function (param) {
return new Promise(function (resolve, reject) {
ajax('http://127.0.0.1/login', function(rest) {
if (rest.success) {
resolve(rest)
} else {
reject(new Error('登錄失敗'))
}
})
})
}
構造函數 Promise 接收一個函數作爲入參,該函數接收 resolve、reject 兩個參數,這兩個參數會由 js 引擎提供,無需自己傳入
- 當異步函數判斷結果爲成功時調用 resolve,這時 Promise 的狀態就會從 pending 變成 fullfiled
- 當異步函數判斷結果爲失敗時調用reject,這時 Promise 的狀態就會從 pending 變成 rejected
現在我們用 Promise 把 login 做了改造,return 了一個 Promise 實例。那麼這個 login 該怎麼用呢?
login(param).then(function (rest) {
// 當執行 resolve 時就會進入這裏,表示成功,rest就是後端返回的登陸信息
}, function (e) {
// 當執行 reject 時就會進入這裏,表示失敗
})
login 函數執行會返回一個 Promise 對象,Promise 對象上有個 then 方法,它接受兩個函數
- 前者爲異步成功時的回調,暫時可以理解爲註冊了 resolve 函數
- 後者爲異步失敗時的回調,暫時可以理解爲註冊了 reject 函數
reject 函數除了可以註冊在 then 函數的第二個參數上以外,還可以註冊在 Promise.prototype.catch 函數中
login(param).then(function (rest) {
// 當執行 resolve 時就會進入這裏,表示成功,rest就是後端返回的登陸信息
}).catch(function (e) {
// 當執行 reject 時就會進入這裏,表示失敗
})
不管成功或者失敗,我都想執行一步操作怎麼辦呢?ES9 引入了 Promise.prototype.finally 函數
login(param).then(function (rest) {
// 當執行 resolve 時就會進入這裏,表示成功,rest就是後端返回的登陸信息
}).catch(function (e) {
// 當執行 reject 時就會進入這裏,表示失敗
}).finally(function () {
// 不管成功或者失敗,都會走到這裏
})
鏈式調用
咋一看,用了 Promise 之後代碼量還增加了一點對吧!那它帶來的好處是什麼呢?別急,接着往下看
當我們把另外兩個接口 getUser、getMenu 兩個接口都用 Promise 改造之後......
login(param)
.then(function(loginData) {
return getUser(loginData)
}).then(funtion(userData) {
return getMenu(userData)
}).then(function(menuData) {
// 可以一直鏈下去
})
我們跳出了嵌套地獄,函數不用再寫在回調裏了,只需要使用 Promise 提供的 then 函數把所以的依賴按順序給鏈接起來
- 前一個異步執行結束之後纔會執行下一個異步
- 前一個 then 裏面的異步執行結果會傳入到下一個 then 註冊的函數裏,如上述代碼中 getUser 的結果會傳給 getMenu 這一行
Promise.all
上面是鏈式串行調用的方式,各個函數之間是有依賴關係的。但是現在我有這個需求
- 異步函數之間沒有依賴關係
- 我想要它們並行的去執行,這樣會比串行快很多
- 等到所有的函數都執行完了把結果一起返回給我
這個時候 Promise.all 就排上用場了,現在假設上文中的 login、getUser、getMenu 三個函數之後沒有依賴關係
Promise.all([login(), getUser(), getMenu()]).then(function(rest) {
// 三個異步函數都執行完了纔會走到這裏
console.log(rest); // [loginData, userData, menuData]
}).catch(function(e) {
// 只要有一個函數失敗,執行了 reject,就會走到這裏,promise 結束
})
Promise.all 函數的入參是一個數組,數組中的每個元素都是一個 promise 實例,經過 Promise.all 封裝成了一個新的 promise 實例。等到數組中所有 promise 實例的狀態都變成 resolved 的時候就會,這個新的 promise 實例的狀態頁面變成 resolved。如果數組中有一個 promise 的狀態變成了 rejected,那麼這個新的 promise 的狀態也就變成了 rejected。
Promise.race
現在我的需求又變了,這三個異步函數誰先執行完我就先處理誰,其他兩個我就不管了。這個時候就得用 Promise.race 了,我們稱之爲“競賽模式”。
Promise.race([login(), getUser(), getMenu()]).then(function(rest) {
// 只要有一個先執行結束,它的結果就會返回到 rest 上
console.log(rest); // loginData 或 userData 或 menuData
}).catch(function(e) {
// 只要有一個函數失敗,執行了 reject,就會走到這裏,promise 結束
})
自己擼一個
現在都講究造輪子,既然 Promise 的用法我們都已經知道了,那就自己來擼一個吧!
從用法上我們可以看出一下幾點
- Promise 是一個構造函數
- 有一個狀態
- 有兩個數組,一個用來保存成功時的回調函數,一個用來保存失敗時的回調函數
- 原型上有 then、catch、finally 這麼幾個方法
- 有 resolve、reject、all、reace 這麼幾個靜態方法
構造函數
function Promise (executor) {
var self = this;
this.status = 'pending'; // 狀態
this.data = undefined;
this.onResolvedCallback = []; // 通過 then 註冊的成功回調
this.onRejectedCallback = []; // 通過 then 或者 catch 註冊的失敗回調
function resolve (value) {
if (this.status === 'pending') {
self.status = 'resolved'; // 成功時將狀態改爲 resolved
self.data = value;
for (var i=0; i<this.onResolvedCallback.length; i++) {
self.onResolvedCallback[i](value) // 執行註冊的成功函數
}
}
}
function reject (reason) {
if (this.status === 'pending') {
self.status = 'rejected'; // 失敗時將狀態改爲 rejected
for (var i=0; i<this.onRejectedCallback.length; i++) {
self.onRejectedCallback[i](reason) // 執行註冊的失敗函數
}
}
}
try {
executor(resolve, reject); // new Promise 的時候立即執行 executor
} catch (e) {
reject(e)
}
}
then
then 函數是用來註冊成功或者失敗時的回調函數的。需要注意的是,當調用 then 函數來註冊回調時,promise 實例的狀態有可能是 pending、resolved、rejected。
Promise.prototype.then = function (onResolved, onRejected) {
// 爲了可以使用鏈式寫法,then 方法返回的是一個 promise 對象
if (self.status === 'pending') {
// 通常情況下,如果 promise 包裹的是一個異步操作,那麼走到 then 是,promise 應該還是 pending狀態
return promise1 = new Promise(function (resolve, reject) {
// 註冊成功回調
// self.onResolveCallback.push(onResolved)
// 本來我們直接把 onResolved push 到 onResolveCallback 這個數組裏就可以了,異步執行完成時會調用到這個 onResolved,並把結果傳進去
// 但是 onResolved 裏面可能還有一個異步操作(假設p),我們得等到這個異步p的狀態改變了之後,才能繼續下一個 then,這樣才能完成正確的依賴關係
// 所以我們還得判斷 onResolved 函數的返回結果
self.onResolveCallback.push(function (value) {
var x = onResolved(value);
if (x instanceof Promise) {
// 如果 onResolved 返回的仍是一個 promise,那就等等到這個 promise 的狀態改變了之後,才能改變 promise1 的狀態,然後繼續下一個 then
x.then(resolve, reject)
} else {
// 否則直接改變 promise1 的狀態,把 promise1 的執行結果 x 傳入到下一個 then 中的回調
resolve(x);
}
})
// 註冊失敗回調
self.onRejectCallback.push(function(reason) {
var x = onRejected(reason);
if (x instanceof Promise) {
x.then(resolve, reject)
} else {
reject(reason)
}
})
})
}
if (self.status === 'resolved') {
// 如果 new Promise(executor) 的時候,傳入的 executor 內並沒有執行異步操作,而是直接調用了 resolve,那麼走到 then 的時候,Promise 已經是 resolved 狀態了
// 爲了能夠鏈式調用我們還是要返回一個 promise
return new Promise(function(resolve, reject) {
// 由於狀態已經是 resolved 了,就不需要把 onResolved、onRejected push 到隊列中了
// 直接把 executor 的執行直接塞進 onResolved 就行了
var x = onResolved(self.value)
if (x instanceof Promise) {
x.then(resolve, reject)
} else {
resolve(x)
}
})
}
if (self.status === 'rejected') {
// 邏輯和上面差不多,只是最後一步應該執行 reject(x)
}
}
all
做一個計數,等所有 promise 都完成了,執行 resolve 改變狀態就行了
Promise.all = function(arr) {
return new Promise(function (resolve, reject) {
var count = 0; // 記錄已完成了 promise
var list = []
// 數組 arr 中每一項都是一個 promis 對象
arr.forEach((p,index) => {
p.then(res => {
count ++;
list[index] = res;
if (count === arr.length) {
// 所有的 promise 都成功了
resolve(list)
}
}).catch(e => {
reject(e)
})
})
})
}
race
做一個標識,第一個完成的 promise 執行 resolve 改變下狀態,其他的就不管了
Promise.race = function(arr) {
return new Promise(function (resolve, reject) {
// 做一個標識,如果有一個完成了執行 resolve,等其他的 promise 完成了就不管了
var done = false;
// 數組 arr 中每一項都是一個 promis 對象
arr.forEach((p,index) => {
p.then(res => {
if (!done) {
resolve(res)
}
}).catch(e => {
if (!done) {
reject(e)
}
})
})
})
}
重要的就這麼幾個函數,好了,Promise 的使用和實現到此結束。
然而技術總是在不斷的更新,程序員在追求代碼優雅的路上也在不斷的探索。
Async/Await
簡介
async 函數在 ES2017 納入了標準,它是 Generator 函數的語法糖,使得異步操作變得更加簡單。
基本用法
async 函數的返回值是一個 promise,可以使用 then 註冊回調函數。它和 await 搭配使用,當 async 函數執行到 await 的時候,就會等待 await 後的函數執行完之後再執行後面的內容。比如文章開頭的三個函數就可以寫成這樣
async function start () {
var loginData = await login().catch(...);
var userData = await getUser();
var menuData = await getMenu();
return menuData;
}
start.then(...)
看起來是不是很簡單、很優雅,我們可以像寫同步代碼一樣去操作異步函數。而且比起 Promise 我們可以更加容易的去中斷鏈路,在任意一行 return 就行了。
這裏需要注意幾點
- async 必須緊跟着 function,像這種寫法是錯誤的 var async start = function () {}
- await 必須在 async 申明的函數內部使用,不然會報錯
- await 後面需要是一個 promise 對象,才能達到“等待”的效果。其他對象是達不到這個效果的,如 await setTimeout()