大約從半年前開始,就想試着寫一個符合規範的Promise,可是一直寫不出來,期間也看了很多Promise的文章,可是一般看了一點就看不懂了。最近幾天,又仔仔細細地研究了一遍並查閱了不少文章,終於完全整明白了Promise了。之因此要寫這個小系列文章,是由於我以爲網上大部分寫Promise實現的文章都有點深,之前我看的時候就看不懂,並非說寫的很差,只是有很多還在學習中的小夥伴看不明白,因此,我決定盡我所能,努力寫一個能讓大多數前端小夥伴都能看懂的Promise實現,只須要你有Promise的使用經驗便可。javascript
這三篇文章,會和你從零起步,一點一點完成一個徹底符合Promise A+規範的Promise,而且完美經過官方提供的872個測試用例。我會把寫一個Promise所須要的所有知識和注意點掰開揉碎,所有講清楚。 接下來,咱們就開始吧!前端
關於Promise的實現,咱們先無論規範怎樣,先看一下它是怎麼用的。咱們以chrome裏原生支持的Promise爲例。vue
let promise1 = new Promise(function(resolve, reject) {
setTimeout(() => { // 模擬一個異步執行
resolve(1)
}, 1000)
})
promise1.then(function(res) {
console.log(res)
}, function(err){
console.log(err)
})
複製代碼
以上是咱們使用Promise時常常寫的代碼,從這些代碼來看,咱們能夠獲得如下信息:java
從這些條件中,咱們能夠對咱們本身的MyPromise做出如下的實現:chrome
function MyPromise(executor) {
}
MyPromise.prototype.then = function() {
}
複製代碼
接下來,咱們仔細研究一下構造Promise時傳遞的參數,也就是我上面稱之爲executor的函數和它的兩個參數設計模式
從上面使用Promise的常規代碼中,咱們能夠知道,executor是一個函數,那接下來要明確一件事:executor的具體代碼是由使用者寫的,並由Promise內部被調用。大概就是這樣:數組
function MyPromise(executor) {
executor()
}
MyPromise.prototype.then = function() {
}
// 使用MyPromise
// executor是由使用MyPromise的人來寫的
function executor(resolve, reject) {
setTimeout(() => {
resolve(1)
}, 1000)
}
let promise1 = new MyPromise(executor)
複製代碼
根據使用經驗,使用者在寫executor時候,會有兩個形參resolve和reject,同時,會在適當的時候調用resolve和reject,因此,resolve和reject都是函數,並且都是在promise內部實現。 因此,咱們要實現的MyPromise應該包含resolve和reject方法的實現,並在調用時做爲實參傳遞給executor。promise
function MyPromise(executor) {
let resolve = function() {} // resolve和reject名字能夠隨便起
let reject = function() {}
executor(resolve, reject) // 只要調用的時候傳遞
}
MyPromise.prototype.then = function() {
}
// 使用Promise
let promise1 = new MyPromise(function(resolve, reject) {
setTimeout(() => {
resolve(1) // resolve或者reject是由使用者調用
}, 1000)
})
複製代碼
其實,在MyPromise內部實現resolve和reject函數的時候不必定叫resolve或者reject,叫a、b甚至阿貓阿狗也行,只要在executor執行的時候傳遞給它就行。由於只有這樣,使用者在寫executor具體內容的時候,能夠經過executor的形參拿到它並使用。瀏覽器
因此,resolve和reject函數由咱們,也就是實現這個MyPromise的人實現,而由使用這個MyPromise的人調用的。 釐清這一點很重要。app
如今,咱們實現Promise的代碼以下:
function MyPromise(executor) {
let resolve = function() {}
let reject = function() {}
executor(resolve, reject)
}
MyPromise.prototype.then = function() {
}
複製代碼
接下來,是本節的重點,明確MyPromise裏resolve和reject函數的功能和實現
如今的MyPromise只有一個架子,到這裏必須完成resolve和reject兩個函數。那麼,resovle和reject到底是幹啥的呢?這裏,必需要提一些Promise A+規範的內容了。
根據規範,一個Promise的實例可能有三種狀態:
pending
未決fulfilled
成功狀態rejected
拒絕狀態,也能夠理解成失敗狀態之因此會有這三種狀態,是由於咱們一般用Promise來處理異步操做,而異步操做的結果根據狀況可能成功可能失敗。
一個Promise在實例化的時候默認是pending
狀態,那麼它的狀態由誰來改變?答案是由resolve或者reject這兩個函數來改變。當resolve或者reject函數調用時,resolve會把Promise實例由pending
狀態更改成fulfilled
成功狀態,reject函數會把pending
狀態更改成rejected
狀態。到這裏,resolve和reject這兩兄弟的第一個功能就清楚了。
可是,要實現這個功能,就須要在咱們的MyPromise裏先定義一個狀態,而後在resolve和reject裏更改
function MyPromise(executor) {
this.status = 'pending' // 默認是pending狀態哦
let resolve = function() {
// resolve方法會把pending狀態改成fulfilled
this.status = 'fulfilled'
}
let reject = function() {
// reject方法會把狀態改成rejected
this.status = 'rejected'
}
executor(resolve, reject)
}
MyPromise.prototype.then = function() {
}
複製代碼
可是,上面的代碼是有問題的,一個是this的指向問題,咱們在MyPromise構造函數裏聲明的resolve和reject函數,它的內部this默認都是window,而不是MyPromise實例。這個問題有不少解決,能夠將this先存一下,也能夠直接使用箭頭函數,這裏咱們就使用箭頭函數來解決。
function MyPromise(executor) {
this.status = 'pending'
let resolve = () => { // this指向和外面保持一致哦
this.status = 'fulfilled'
}
let reject = () => { // this指向和外面保持一致哦
this.status = 'rejected'
}
executor(resolve, reject)
}
MyPromise.prototype.then = function() {
}
複製代碼
上面的代碼還有一個問題,根據規範,若是一個Promise實例狀態改變,就會被固定住,之後它的狀態就不再會更改了。 也就是說,若是一個Promise實例由pending
狀態變成fulfilled
狀態,就不能再變回pending
或者rejected
了。可是咱們這個這個不行,你能夠把下面的代碼粘到瀏覽器裏運行,就會發現這個問題。
function MyPromise(executor) {
this.status = 'pending' // 默認是pending狀態哦
let resolve = () => {
this.status = 'fulfilled'
}
let reject = () => {
this.status = 'rejected'
}
executor(resolve, reject)
}
MyPromise.prototype.then = function() {
}
let promise1 = new MyPromise(function(resolve, reject) {
setTimeout(() => { // 不要忘了它哦,由於只有在異步下,才能打印promise1實例
resolve(1)
console.log(promise1) // 這裏是{status: 'fulfilled'} 成功狀態
reject(1)
console.log(promise1) // 可是到了這裏又變成失敗狀態了
}, 1000)
})
複製代碼
要解決這個問題,也很簡單,加上一個if條件判斷就能夠了,當resolve函數運行時,先看下this.status
是否是pending狀態,若是是,就更改它,若是不是就啥都不作。reject也是如此,這樣,當promise的staus狀態變化後,再調用resolve或者reject也會被忽略掉了。
function MyPromise(executor) {
this.status = 'pending'
let resolve = () => {
// 判斷是不是pending狀態,若是是就改,不是就啥都不幹,這樣起到狀態固定做用
if (this.status === 'pending') {
this.status = 'fulfilled'
}
}
let reject = () => {
// 這裏也要加if判斷
if (this.status === 'pending') {
this.status = 'rejected'
}
}
executor(resolve, reject)
}
MyPromise.prototype.then = function() {
}
let promise1 = new MyPromise(function(resolve, reject) {
setTimeout(() => { // 不要忘了它哦,由於只有在異步下,才能打印promise1的實例
resolve(1)
console.log(promise1) // 這裏是{status: 'fulfilled'} 成功狀態
reject(1) // 雖然reject了,可是被忽略掉了
console.log(promise1) // 到這裏依然是成功狀態
}, 1000)
})
複製代碼
這樣,resolve、reject這哥倆的第一個功能完成了。
接下來,咱們實現resolve和reject的第二個功能。
有Promise使用經驗的小夥伴確定早就知道:咱們在調用resolve或者reject方法時通常會給它傳值,而這個值和then方法的實現息息相關。咱們先看一下chrome使用Promise的例子:
let promise1 = new Promise(function(resolve, reject) {
setTimeout(() => { // 模擬一個異步執行
let flag = Math.random() > 0.5 ? true: false
if (flag) {
resolve('success') // 傳遞一個值
} else {
reject('fail') // 傳遞一個值
}
}, 1000)
})
promise1.then(function(res) { // 調用resolve傳過來的值會被這個函數拿到
console.log(res)
}, function(err) { // 調用reject傳過來的值會被這裏拿到
console.log(err)
})
複製代碼
從這個例子裏,咱們能夠發現,resolve和reject調用時傳遞過來的值,會被then方法執行時傳遞的兩個函數分別做爲參數拿到。 這裏咱們知道,resolve和reject執行時傳過來的值必定被存儲起來了,當then方法執行時傳遞的兩個函數在某個時機拿到了它們並執行。
因此,resolve和reject函數的第二個功能也呼之欲出:將調用時的值存儲起來,後面then方法裏傳遞的兩個函數會使用它們。
由於它們分別是成功時和失敗時調用的,因此咱們須要分開存放。爲此,MyPromise須要在構造函數里加兩個屬性,並在resolve和reject函數執行時賦值。
MyPromise寫成以下:
function MyPromise(executor) {
this.status = 'pending'
this.data = undefined // 用來存入resolve傳遞過來的值
this.reason = undefined // 用來存儲reject傳遞過來的值
// 添加參數,由於使用者調用時通常會給傳參
let resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.data = value
}
}
// 添加參數,reject表示失敗,因此寫作失敗緣由reason
let reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = reason
}
}
executor(resolve, reject)
}
MyPromise.prototype.then = function() {
}
複製代碼
其實,resolve和reject傳遞過來的值放在一個屬性裏也是能夠的,由於promise實例狀態一旦更改就不會再變了,也就是resolve和reject只可能執行其中一個,後面即便再執行也會被裏面的if條件判斷忽略掉。不過爲了好對應,咱們仍是使用兩個屬性來分別存放resolve和reject函數傳過來的值。
寫到這裏,resolve和reject分別實現了兩個功能。實際上它們兄弟倆每一個人都有三個功能,只是第三個功能和then方法密切相關,因此第三個功能須要和then一塊兒寫。不過在此以前,咱們要先聊聊promise處理異步代碼的執行順序。
咱們經過console.log()
的方式來看chrome裏原生支持的Promise處理異步代碼時的執行順序。請仔細看下面的例子:
let promise1 = new Promise(function(resolve, reject) {
console.log(1)
setTimeout(() => { // 模擬一個異步執行
let flag = Math.random() > 0.5 ? true: false
if (flag) {
resolve('success')
console.log(2) // 注意這裏和reject都是打印2
} else {
reject('fail')
console.log(2)
}
}, 1000)
})
console.log(3)
promise1.then(function(res) {
console.log(res)
}, function(err) {
console.log(err)
})
console.log(4)
複製代碼
若是你把上面的代碼貼到瀏覽器裏執行的話,你會發現打印結果是1 3 4 2 success或者fail,咱們縷一下這個順序:
new Promise
的時候,開始構造實例,傳遞給構造函數的函數執行,因此先打印出1setTimeout
了,裏面的代碼須要等到下一個執行序列,而後構造結束,構造出來的實例賦值給變量promise1
console.log(3)
執行,打印出3promise1.then()
執行,可是,then方法裏面傳遞的兩個函數都沒有執行,否則這裏就會打印出success或者fail,沒有打印說明then方法傳遞的兩個函數都沒執行console.log(4)
執行,打印出4。當前序列結束resolve
或者reject
執行,而後console.log(2)
執行,打印2then()
方法裏傳遞的兩個函數根據條件執行,拿到以前resolve或者reject傳遞並存儲的值,而且執行,打印success或者fail總結一下:當Promise用resolve和reject方法處理異步的代碼的時候,then方法先於resolve或者reject執行,可是then方法傳遞的兩個函數此時並未執行,而是等到resolve或者reject執行以後再執行。這實際上是一種設計模式:分發訂閱模式,也叫觀察者模式。
這個總結若是看不明白不要緊,由於接下來就要說它。
分發-訂閱模式,也叫觀察者模式,它在前端應用是如此的普遍,你幾乎在因此的事件機制和異步處理中均可以見到它的身影。咱們先舉個例子:
let app = document.getElementById('app')
app.addEventListener('click', function fn1() {
console.log(1)
})
app.addEventListener('click', function fn2() {
console.log(2)
})
app.addEventListener('click', function fn3() {
console.log(3)
})
複製代碼
以上代碼對於前端的同窗再日常不過,當點擊id爲app的標籤時,fn一、fn2和fn3纔會執行。而代碼執行到app.addEventListener
時,相應的函數並未執行,而是等到點擊的時候才執行。因此,你能夠猜到,fn一、fn2和fn3一開始必定會被存放在某個地方,當某種條件發生時,它們纔會被一次性執行。
若是你使用vue的話,vue裏的watch也是同樣的道理:先把某個函數或者某些函數註冊存放到一個地方,當某個狀態發生改變時,就把這些存放起來的函數一次性所有執行掉。
Promise裏的then也是這樣作的。當Promise處理異步時,then方法先執行,把做爲參數的兩個函數分別註冊存放在實例中。等到resolve或者reject函數調用的時候再把它們執行掉。
此時,then方法和resolve和reject的第三項功能也呼之欲出了。
this.data
的值傳給它們;reject也是如此function MyPromise(executor) {
this.status = 'pending'
this.data = undefined
this.reason = undefined
this.resolvedCallbacks = [] // 存儲then方法傳遞進來的第一個參數,成功的回調
this.rejectedCallbacks = [] // 存儲then方法傳遞進來的第二個參數,失敗的回調
let resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.data = value
// 將成功的回調所有執行,而且將this.data傳遞過去
this.resolvedCallbacks.forEach(fn => fn(this.data))
}
}
let reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = reason
// 將失敗的回調所有執行,而且將this.reason傳遞過去
this.rejectedCallbacks.forEach(fn => fn(this.reason))
}
}
executor(resolve, reject)
}
// then方法接收到參數,分別命名onResolved和onRejected
MyPromise.prototype.then = function(onResolved, onRejected) {
this.resolvedCallbacks.push(onResolved) // 將onResolved存起來
this.rejectedCallbacks.push(onRejected) // 將onRejected存起來
}
複製代碼
請注意,以上的代碼都是基於處理異步代碼,也就是then方法會早於resolve或者reject執行。 因此then裏還須要作一步判斷,即當前promise爲pending狀態時,再把回調push存放到相應的地方。
function MyPromise(executor) {
this.status = 'pending'
this.data = undefined
this.reason = undefined
this.resolvedCallbacks = []
this.rejectedCallbacks = []
let resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.data = value
this.resolvedCallbacks.forEach(fn => fn(this.data))
}
}
let reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = reason
this.rejectedCallbacks.forEach(fn => fn(this.reason))
}
}
executor(resolve, reject)
}
MyPromise.prototype.then = function(onResolved, onRejected) {
// 判斷狀態,只有當pending時才執行
if (this.status === 'pending') {
this.resolvedCallbacks.push(onResolved)
this.rejectedCallbacks.push(onRejected)
}
}
複製代碼
看到這一步,你可能會有點疑問,爲啥用來存放then方法傳遞的函數要用數組?由於Promise能夠像下面這樣用哦
let promise = new Promise(function(resolve, reject){
setTimeout(() => {
resolve(1)
}, 1000)
})
promise.then(function(res) {
console.log('處理res')
})
promise.then(function(res) {
console.log('再來一次')
})
複製代碼
這個例子在同一個Promise實例上then了兩次,註冊了兩次函數。當resolve執行的時候,會把then註冊的兩個函數都執行掉。
還有,你可能問,如今咱們的Promise都是處理異步的狀況,若是是同步的狀況怎麼辦呢? 嗯,這個就是接下來要說的。
咱們一般使用Promise是用來處理異步的狀況,咱們的MyPromise寫到如今也都是基於處理異步這個前提。實際上,Promise也是能夠處理同步情況的,並且很是簡單。
若是你還記得前面有關Promise執行序列講解的話,應該還記得,異步時then方法是先於resolve或者reject執行的,而同步時then方法是在resolve或者reject以後執行的。 請看下面的例子:
let promise = new Promise(function(resolve, reject){
resolve('success')
})
promise.then(function(res) {
console.log(res)
})
複製代碼
上面的例子中,在構造時resolve就已經調用,狀態就已經肯定,此時then晚執行,因此then此時只須要根據已經肯定的狀態直接調用成功或者失敗的回調就完事了,沒必要再註冊存放了。
MyPromise進行以下更改:
function MyPromise(executor) {
this.status = 'pending'
this.data = undefined
this.reason = undefined
this.resolvedCallbacks = []
this.rejectedCallbacks = []
let resolve = (value) => {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.data = value
this.resolvedCallbacks.forEach(fn => fn(this.data))
}
}
let reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = reason
this.rejectedCallbacks.forEach(fn => fn(this.reason))
}
}
executor(resolve, reject)
}
MyPromise.prototype.then = function(onResolved, onRejected) {
if (this.status === 'pending') {
this.resolvedCallbacks.push(onResolved)
this.rejectedCallbacks.push(onRejected)
}
// 若是是成功狀態直接成功的回調函數
if (this.status === 'fulfilled') {
onResolved(this.data)
}
// 若是是失敗狀態直接調失敗的回調函數
if (this.status === 'rejected') {
onRejected(this.reason)
}
}
複製代碼
咱們能夠測試一下哦~
let promise = new MyPromise(function(resolve, reject) {
setTimeout(() => {
let flag = Math.random() > 0.5 ? true : false
if (flag) {
resolve('success')
} else {
reject('fail')
}
}, 1000)
})
promise.then(res => {
console.log(res)
}, error => {
console.log(error)
})
複製代碼
到這裏,MyPromise的雛形完成了!嗯,只是一個雛形,它最核心的then方法咱們幾乎還沒怎麼實現。可是,若是你能徹底看懂這四十幾行的代碼,那表示你已經離成功不遠了!
接下來的一篇,咱們須要完成最最核心的then方法的實現了!進入下一篇包教包會,和你實現一個Promise(二)