從手寫一個符合Promise/A+規範Promise來深入學習Promise

春天的風劍客發表於2018-08-30

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/A+規範Promise來深入學習Promise
    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)
})
複製程式碼

哈哈,那麼問題來啦?上面會輸出什麼呢?

從手寫一個符合Promise/A+規範Promise來深入學習Promise
哈哈,是不是很困惑catch後怎麼又進入then()了呢? 實際上前面我們已經說過,promise有三種狀態pending,fulfilled,rejected,一旦從pending態到fulFilled,或者從pending態到rejected,其狀態都是不可逆的。因此,.then()中每次return出去的都是一個新的promise,而不是this. 有圖有真相(雖然圖是我盜的,說明問題就好):
從手寫一個符合Promise/A+規範Promise來深入學習Promise

那麼對於異常情況下promise的流程是這個樣子的:

從手寫一個符合Promise/A+規範Promise來深入學習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 就完成啦

相關文章