Promise的簡單介紹

豆豆大魔王發表於2017-12-14

首先,閱讀一下這兩篇文章。

  1. 非同步-阮一峰
  2. promise MDN

通過上面兩篇文章簡單瞭解了Promise後,再來思考一下

why promise?

假設我們有一個需求,執行一個ajax請求,當請求成功後,再次執行一個ajax請求,程式碼實現可以這樣寫(以jquery為例)

$.get(url1,function(data){
    console.log('第一個ajax執行成功')
    $.get(url2,function(data){
        console.log('第二個ajax執行成功')
    })
})
複製程式碼

當我們需要在回撥裡執行第二次非同步操作時,可以這樣寫,那如果我們的需求是需要連續十次執行,每次非同步操作的執行都是在上一個非同步操作的回撥中執行,難道還要層層巢狀的寫嗎?當然不是,這樣寫的話會讓程式碼非常不優雅,並且耦合度很大。 到這裡,就輪到今天的主角-Promise登場了! 參考阮一峰大神的文章,可以看到Promise的大概實現如下

fn().then(fn1).then(fn2) //fn執行完後執行fn1,fn1執行完後執行fn2
複製程式碼

把上面提到的需求帶進來,那就可以使用鏈式操作來完成我們的需求,可以看出,promise能夠將非同步的程式碼用同步的方式表示出來

Promise的簡單示例我們可以參考MDN的簡單示例

var myFirstPromise = new Promise(function(resolve, reject){
    //當非同步程式碼執行成功時,我們才會呼叫resolve(...), 當非同步程式碼失敗時就會呼叫reject(...)
    //在本例中,我們使用setTimeout(...)來模擬非同步程式碼,實際編碼時可能是XHR請求或是HTML5的一些API方法.
    setTimeout(function(){
        resolve("成功!"); //程式碼正常執行!
    }, 250);
});

myFirstPromise.then(function(successMessage){
    //successMessage的值是上面呼叫resolve(...)方法傳入的值.
    //successMessage引數不一定非要是字串型別,這裡只是舉個例子
    console.log("Yay! " + successMessage);
});
複製程式碼

接下來,我們自己動手寫一個簡單的Promise來對其原理有一個初步的認識。

何從下手?從使用開始

編寫程式碼之前,先寫一個簡單的promise的執行語句

fn().then(fn1).then(fn2)
複製程式碼

參考官方文件,我們知道,then是promise物件的方法,這句程式碼能夠鏈式呼叫,就說明fn最後返回的是一個promise物件,有頭緒了!繼續完善我們的程式碼

var p = new Promise() //建立一個promise物件,作為fn的返回

function fn() {
    console.log(1)
    setTimeout(function(){
        p.resolve("成功!")   //程式碼正常執行!這裡執行非同步回撥
    },1000)
    return p
}

fn().then(fn1).then(fn2)
複製程式碼

then方法包含兩個引數:onfulfilled 和 onrejected,它們都是 Function 型別。當Promise狀態為fulfilled時,呼叫 then 的 onfulfilled 方法,當Promise狀態為rejected時,呼叫 then 的 onrejected 方法,這裡我們假設每次非同步都是執行成功的,暫時只傳fulfilled一個函式作為引數,接下來,完善Promise物件,這裡我們把Promise物件的onfulfilled 方法的屬性名設為resolve,onrejected 方法的屬性名設為reject,完善Promise

function Promise(){

}

Promise.prototype = {
    then: function(onfulfilled, onrejected){
        //then方法包含的兩個方法引數
        this.resolve = onfulfilled
        this.reject = onrejected
        return this  //then也可以鏈式呼叫,所以執行完成後返回promise例項物件
    },
    resolve: function(result){
        // 回撥成功執行的方法
    },
    reject: function(result){
        // 回撥失敗執行的方法
    }
}

var p = new Promise() //建立一個promise物件,作為fn的返回

function fn() {
    console.log(1)
    setTimeout(function(){
        p.resolve("成功!")   //程式碼正常執行!這裡執行非同步回撥
    },1000)
    return p
}

fn().then(fn1).then(fn2)
複製程式碼

等等,貌似哪裡不對?當我們語句中包含了兩個then語句後,那就需要兩個function放到任務佇列中等待執行啊,如果這樣寫,後面的then的function豈不是會覆蓋前面一個then的function嗎?思考一下,我們可以把待執行的函式放在陣列裡順序執行呀!繼續修改程式碼

function Promise(){
    this.callbacks = []
}

Promise.prototype = {
    then: function(onfulfilled, onrejected){
        this.callbacks.push({
            resolve: onfulfilled,
            reject: onrejected
        })
        return this  //then也可以鏈式呼叫,所以執行完成後返回promise例項物件
    },
    resolve: function(result){
        // 回撥成功執行的方法
        var callbackObj = this.callbacks.shift()
        callbackObj['resolve'](result)
    },
    reject: function(result){
        // 回撥失敗執行的方法
        var callbackObj = this.callbacks.shift()
        callbackObj['reject'](result)
    }
}
var p = new Promise()

function fn() {
    console.log(1)
    setTimeout(function(){
        p.resolve("成功!")   //程式碼正常執行!這裡執行非同步回撥
    },1000)
    return p
}

fn().then(fn1).then(fn2)
複製程式碼

修改完成,看看還有哪裡可以優化的地方?咦?resolve和reject的函式執行長得好像啊,是不是可以提取出公共部分來執行呢?繼續修改,順便完善一下fn1和fn2函式

function Promise(){
    this.callbacks = []
}

Promise.prototype = {
    then: function(onfulfilled, onrejected){
        this.callbacks.push({
            resolve: onfulfilled,
            reject: onrejected
        })
        return this  //then也可以鏈式呼叫,所以執行完成後返回promise例項物件
    },
    resolve: function(result){
        // 回撥成功執行的方法
        this.complete('resolve', result)
    },
    reject: function(result){
        // 回撥失敗執行的方法
        this.complete('reject', result)
    },
    complete: function(type, result){
        var callbackObj = this.callbacks.shift()
        callbackObj[type](result)
    }
}
var p = new Promise()

function fn() {
    console.log('我是立即執行語句~')
    setTimeout(function(){
        p.resolve("成功!")   //程式碼正常執行!這裡執行非同步回撥
    },1000)
    return p
}

function fn1(result) {
    console.log('fn1', result)
    setTimeout(function() {
        p.resolve('data2')
    }, 2000)
}

function fn2(result) {
    console.log('fn2', result)
}

fn().then(fn1).then(fn2)
複製程式碼

初步完成,把程式碼放到html中執行,效果如下

gif.gif

是不是還差了點兒什麼?

再回頭看看MDN文件,我們發現Promise的原型上有兩個方法,then和catch,then我們已經實現了,接下來再實現一個簡單的catch。回到我們的程式碼,當我們執行fn失敗時,可以通過fn().then(successFunction,errorFunction)的形式來執行錯誤處理函式errorFunction,但是到這一步已經出錯了,正常的邏輯是應該直接結束後續的回撥,跳出這個流程,而我們目前完成的程式碼貌似還是會繼續往後執行。。。再改改!

function Promise(){
    this.callbacks = []
    this.oncatch = null  //定義一個oncatch屬性獲取錯誤處理方法
}

Promise.prototype = {
    then: function(onfulfilled, onrejected){
        this.callbacks.push({
            resolve: onfulfilled,
            reject: onrejected
        })
        return this  //then也可以鏈式呼叫,所以執行完成後返回promise例項物件
    },
    resolve: function(result){
        // 回撥成功執行的方法
        this.complete('resolve', result)
    },
    reject: function(result){
        // 回撥失敗執行的方法
        this.complete('reject', result)
    },
    complete: function(type, result){
        // 此處增加一個錯誤處理判斷
        if (type === 'reject' && this.oncatch) {
            this.callbacks = []
            this.oncatch(result)
        } else if (this.callbacks[0]) {
            var callbackObj = this.callbacks.shift()
            if(callbackObj[type]) callbackObj[type](result)
        }
    },
    catch: function(onfail){
        this.oncatch = onfail
        return this
    }
}
var p = new Promise()

function fn() {
    console.log('我是立即執行語句~')
    setTimeout(function(){
        p.reject("失敗!")   //程式碼正常執行!這裡執行非同步回撥
    },1000)
    return p
}

function fn1(result) {
    console.log('fn1', result)
    setTimeout(function() {
        p.resolve('data2')
    }, 2000)
}

function fn2(result) {
    console.log('fn2', result)
}

function errCatch(result) {  //定義錯誤處理函式
    console.log('err', result)
}

fn().then(fn1).then(fn2).catch(errCatch)
複製程式碼

至此,我們完成了一個簡單的promise實現,來看看錯誤獲取的效果

gif.gif

可以看到,fn1和fn2並沒有執行!大功告成!

簡單的Promise物件已經完成了,來實戰操(zhuang)作(b)一波! 1.將Promise作為一個common.js規範模組來引用 Promise.js

function Promise() {
    this.callbacks = []
    this.oncatch = null
}

Promise.prototype = {
    then: function(onfulfilled, onrejected) {
        this.callbacks.push({
            resolve: onfulfilled,
            reject: onrejected
        })
        return this //then也可以鏈式呼叫,所以執行完成後返回promise例項物件
    },
    resolve: function(result) {
        // 回撥成功執行的方法
        this.complete('resolve', result)
    },
    reject: function(result) {
        // 回撥失敗執行的方法
        this.complete('reject', result)
    },
    complete: function(type, result) {
        if (type === 'reject' && this.oncatch) {
            this.callbacks = []
            this.oncatch(result)
        } else if (this.callbacks[0]) {
            var callbackObj = this.callbacks.shift()
            if (callbackObj[type]) callbackObj[type](result)
        }
    },
    catch: function(onfail) {
        this.oncatch = onfail
        return this
    }
}

module.exports = Promise
複製程式碼

2.新建3個txt檔案,內容隨意,再新建一個test.js檔案

var Promise = require('./Promise')
var fs = require('fs')

var p = new Promise

var str = ''

function readFile(path){
    fs.readFile(path, 'utf-8', function(err, str){
        if(err){
            p.reject(path)
        }else{
            p.resolve(str)
        }
    })
    return p
}

readFile('a.txt').then(function(line){
    str += line
    console.log('讀取a...')
    readFile('b.txt')
}).then(function(line){
    str += line
    console.log('讀取b...')
    readFile('c.txt')
}).then(function(line){
    str += line
    console.log('讀取c...')
    p.resolve(str)
}).then(function(result){
    console.log(result)
}).catch(function(){
    console.log('err')
})
複製程式碼

目錄結構如下

Paste_Image.png

在node環境下執行test.js,效果如下

gif.gif

再把某個讀取步驟的路徑改為一個不存在的檔案試試

gif.gif

至此,一個簡單的promise實現完成,如有錯誤,歡迎指正!拜拜