1.什麼是Promise
一項技術不會憑空產生,都是為了解決某些實際的問題而出現。瞭解技術產生的背景,可以讓我們更好的知道他擅長解決什麼問題,哪些場景我們可以利用他來解決。那麼就讓我們一步一步來揭開promise神祕的面紗。
1.1.什麼是promise
首先我們來了解一下promise。promise英語的意思是:諾言。Promise是抽象非同步處理物件及其對其進行各種操作的元件。用大白話說,promise就是用來解決非同步回撥問題的。
1.2.promise解決了什麼問題
在沒有promise 之前,前端處理ajax請求通常是這樣的:
function fetchData (callback) {
$.ajax({
type: 'GET',
url:'xxx',
data: data,
success: function (res) {
callback(res) // 回撥函式
}
})
}
複製程式碼
這只是處理一個ajax請求,可很多時候,我們回面臨第一個請求回來的結果,是第二次請求的引數,或者我們想一個極端的例子,前一次請求的結果是第二次請求的引數,那麼我們的程式碼可能是這樣的:
function fetchData (callback) {
$.ajax({
type: 'GET',
url: 'xxx',
data:data,
success: function (res) {
if (res.data) {
const params1 = res.data;
$.ajax({
type:'GET',
url: 'xxx',
data: params1,
success: function (res) {
if (res.data) {
const params2 = res.data
$.ajax({
type:'GET',
url: 'xxx',
data: paramsw,
success: function (res) {
if (res.data) {
const params3 = res.data;
......
}
}
})
}
}
})
}
}
})
}
複製程式碼
上面這個就是回撥地獄。程式碼的可讀性,可維護性大打折扣。promise就是為他而生的,通過promise我們可以像寫同步那樣寫非同步方法。同樣是上面的場景我們完全可以用另一種方法:
function fetchData (url, data) {
return new Promise((resolve, reject) => {
$.ajax({
type: 'GET',
url:url,
data: data,
success: function (res) {
resolve(res)
},
fail: function (err) {
reject(res)
}
})
})
}
fetchData(url,data).then(res => {
const url1 = 'yyyy';
const data1 = res.data;
return fetchData(url1,data1)
}).then(res => {
const url2 = 'zzz';
const data2 = res.data
return fetchData(url2, data2)
}).then(res => {
const url3 = 'wwww';
const data3 = res.data;
render()
}).catch(err => {
console.log(err)
})
複製程式碼
這樣似乎美觀了那麼一丟丟,但是不要著急,我們一步一步來。
2.promise的實際應用
上一章我們主要探討了promise的背景知識,簡單的知道了promise使用的場景,這章我們就來跟進一步的學習promise
2.1 promised的狀態
promise有三種狀態:
- pending
- fulfilled
- rejected 如下圖: promise物件的狀態,從Pending轉換為Fulfilled或Rejected之後, 這個promise物件的狀態就不會再發生任何變化。
2.2 promise是非同步的麼?
如下程式碼:你覺得輸出的結果是什麼呢?
var promise = new Promise(function (resolve){
console.log("1"); //1
resolve(2);
});
promise.then(function(value){
console.log(value); // 2
});
console.log("3"); // 3
複製程式碼
實際上執行上面的程式碼會輸出下面的內容
1
3
2
複製程式碼
你會很好奇為什麼會是這樣?
由於JavaScript程式碼會按照檔案的從上到下的順序執行,所以最開始 <1> 會執行,然後是 resolve(2); 被執行。這時候 promise 物件的已經變為確定狀態,FulFilled被設定為了 2 。
由於 promise.then 執行的時候promise物件已經是確定狀態,從程式上說對回撥函式進行同步呼叫也是行得通的。
但是即使在呼叫 promise.then 註冊回撥函式的時候promise物件已經是確定的狀態,Promise也會以非同步的方式呼叫該回撥函式,這是在Promise設計上的規定方針。
因此 <3> 會最先被呼叫,最後才會呼叫回撥函式 <2> 。
2.2 promise的鏈式呼叫
在前面的<1.2>中我們簡單介紹了下promise的鏈式呼叫,在這裡我們再次深入理解一下 看下面的程式碼
function task () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('task')
},1000)
})
}
task().then((res) => {
console.log(res)
return 'taskB'
}).then(res => {
console.log(res)
return 'taskC'
}).then(res => {
console.log(res)
throw new Error()
}).catch(e => {
console.log(e)
return 'taskD'
}).then(res => {
console.log(res)
})
複製程式碼
哈哈,那麼問題來啦?上面會輸出什麼呢?
哈哈,是不是很困惑catch後怎麼又進入then()了呢? 實際上前面我們已經說過,promise有三種狀態pending,fulfilled,rejected,一旦從pending態到fulFilled,或者從pending態到rejected,其狀態都是不可逆的。因此,.then()中每次return出去的都是一個新的promise,而不是this. 有圖有真相(雖然圖是我盜的,說明問題就好):那麼對於異常情況下promise的流程是這個樣子的:
3.嘗試寫出符合Promse/A+規範的promise
前兩小節介紹了promise 的背景,及promise的更深入的學習,這節我們通過自己動手實現一個符合promise/A+規範的promise來徹底弄懂promise
3.1 promise 建構函式
通過2.2的探究我們知道當你建立一個new Promise()的時候,實際上在new Promise()內部是同步執行的,也就是說
const promise = new Promise((resolve,reject) => {
console.log(1)
})
複製程式碼
當程式碼執行到console.log(1) 的時候,1會被立馬列印出來。也就是說,promise的建構函式中有一個executor的函式,他會立馬執行。因此:
function Promise (executor) {
executor()
}
複製程式碼
我們有向executor中傳入了兩個函式,resolve和reject,因此我們來進一步完善我們的建構函式:
function Promise(executro) {
function resolve () {}
function reject () {}
executor(resolve, reject)
}
複製程式碼
我們知道promise中有三種狀態,那麼我們需要一個屬性來控制這個狀態,我們新增一個status的屬性吧:
function Promise(executor) {
let self = this;
self.status = 'pending' // 初始狀態為pending
function resolve() {}
function reject() {}
}
複製程式碼
當執行resolve函式的時候,status的狀態應該從penging態轉為resolved,同時儲存resolve函式的值,同樣的當執行reject函式的時候,status的狀態應該從pengding態轉為rejected,同時我們儲存reject的值,程式碼入下:
function Promise (executor) {
let self = this;
self.status = 'pending'
self.value = undefined
self.reason = undefined
self.onResolved = [] // 3.2小節新增
self.onRejected = [] // 3.2小姐新增
function resolve (value) {
if (self.status === 'pending') {
self.status = 'resolved'
self.value = value
self.onResolved.forEach(fn => fn())
}
}
function reject (reason) {
if (self.status === 'pending') {
self.status = 'rejected'
self.reason = reason
self.onRejected.forEach(fn => fn())
}
}
// 處理下異常
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
複製程式碼
nice ^_^ 現在我們的Promise的建構函式就基本能滿足我們的需求啦。
3.2 promise then方法
Promise/A+規範中,每個promise都有一個then方法,該方法返回的還是一個promise.這個then方法實在例項上呼叫,因此該then方法應該在promise的原型上,同樣的我們需要根據promsie的狀態來進行相應的處理,不同的是,當status是pending態的時候我們要收集resolve和reject,同時在建構函式中增加相應的處理,程式碼如下:
Promise.prototype.then = function (onfulfilled, onrejected) {
// 這裡坐下簡單的處理
onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : val => val
onrejected = typeof onrejected === 'function' ? onrejected : err => {throw err}
let promise2 = new Promise((resolve, reject) => {
if (self.status === 'resolved') {
// onfulfilled(self.value) 3.2
let x = onfulfilled(self.value) // 3.3
resolvePromise(promise2, x, resolve, reject) // 3.3
}
if (self.status === 'rejected') {
// onrejected(self.reason) 3.2
let x = onrejected(self.reason) // 3.3
resolvePromise(promise2, x, resolve, reject) // 3.3
}
if (self.status === 'pending') {
self.onResolved.push(function () {
setTimeout(() => {
// onfulfilled(self.value) 3.2
let x = onfulfilled(self.value) // 3.3
resolvePromise(promise2, x, resolve, reject) // 3.3
},0)
})
sef.onRejected.push(function () {
setTimeout(() => {
// onrejected(self.reason) 3.2
let x = onrejected(self.reason) // 3.3
resolvePromise(promise2, x, resolve, reject) // 3.3
}, 0)
})
}
})
return promise2
}
複製程式碼
我們的then 函式就實現啦,最重要的就是對pending態的處理
3.3 onfullfiled和onrejected返回結果的處理
我們在3.2小節留了一個坑,你會發現,新的Promise建構函式中傳入的resolve,reject好像沒有用到啊。那麼這小節就是要填這個坑的。那我們會發現then方法中的onResolved和onRejected實際上分別是在Promise建構函式中的resolve 和reject中處理的。實際上我們會面臨以下集中情況:
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
},0)
})
promise.then((res) => {
return res
},(err) => {
// 直接 reject
reject(err)
}).then(res => {
// 1. 直接返回一個變數
return res
// 2. 返回一個物件或者函式
return obj
// 3. 迴圈引用
})
複製程式碼
因此我們對3.2的程式碼改造,我直接寫在3.2的程式碼中加上3.3的標記,我們還要給出對不同返回值的解析處理
function resolvePromise(promise2, x, resolve, reject) {
// 判斷是否迴圈引用
if (x === promise2) {
return reject(new TypeError('迴圈引用'))
}
// 判斷是否為物件或者函式
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then
// 判斷是否為promise
if (typeof then === 'function') {
then.call(x, (y) => {
resolvePromise(promise2, y, resolve, reject)
}, (e) => {
reject(e)
})
} else {
resolve(x)
}
} catch (e) {
reject(e)
}
} else {
resolve(x)
}
}
複製程式碼
這下我們的promise 就完成啦