前端非同步技術之Promise

蟹丸發表於2019-02-23

前言

從事前端的朋友或多或少的接觸過Promise,當程式碼中回撥函式層級過多你就會發現Promise非同步程式設計的魅力,相信此文一定能幫你排憂解惑

Promise概念

PromiseJS非同步程式設計中的重要概念,非同步抽象處理物件,是目前比較流行Javascript非同步程式設計解決方案之一 或許是筆者理解能力有限,對官方術語怎麼也感受不到親切,下面我來用通俗易懂的語言解釋下: 

Promise是一個包含三種狀態的物件(pendingfulfilledrejected),可以鏈式的處理非同步請求(then方法)並能很好地處理異常問題, 是解決回撥地獄的良好方案之一。 

 回撥函式處理多層非同步示例

$.ajax({
    url: url1,
    success: function(rsp){
        $.ajax({
           url: url2,
           success: function(rsp){
               $.ajax({
                  url: url3,
                  success: function(rsp){
                      //do sth
                  },
                  error: function(error){
                  }
              });
           },
           error: function(error){
           }
       });
    },
    error: function(error){
    }
});
複製程式碼

promise封裝在$.ajax

$.ajax = function(config){
    return new Promise(function(resolve, reject){
        //1省略...
        xmlhttp.onreadystatechange = function(){
            if(xmlhttp.status==200){
                resolve(rspData);
            }else{
                reject(xmlhttp.statusText);
            }
        };
        //2省略...
    })
}
$.ajax({url: url1}).then(function(val){
    return $.ajax({url: val.url})
}).then(function(val){
    return $.ajax({url: val.url})
}).catch(function(err){
    console.log(err);
}}
複製程式碼

封裝好的Promise處理非同步可讀性可維護性以及程式碼美觀度不言而喻

Promise API

'new' Promise

//pending狀態的promise
var promise = new Promise(function(resolve, reject){
	//do sth
})
//fulfilled狀態的promise
var promise = Promise.resolve(1)
.then(function resolve(value){console.log(value)});
// var promise = new Promise(function(resolve){resolve(1)})
//rejected狀態的promise
var promise = Promise.reject(new Error('error'))
.catch(function(error){console.error(error)});
// var promise = new Promise(function(resolve,reject){resolve(new Error('error'))})
複製程式碼

Promise.prototype.then

Promise#then
promise.then(onFulfilled, onRejected)
複製程式碼

返回一個新的promise 這裡經常會有一個疑問:為什麼不返回原來的promise,個人是這樣認為的,若返回同一個promise則狀態不一致,promise規範說明當pendingfulfilled/ rejected時狀態確定後不能再改變。

Promise.prototype.catch

Promise#catch 
promise.catch(function(error){ throw new Error(error); })
複製程式碼

注意:IE8及以下版本會出現 identifier not found 的語法錯誤,可將點標記法改為中括號標記法

promise['catch'](function(error){
    throw new Error(error);
})
複製程式碼

rejected狀態的promise丟擲異常,相當於

promise.then(undefined, onRejected)
複製程式碼

then & catch 結合示例

promise.then(function f1(value){
    //do sth 1
}).then(function f2(value){
    //do sth 2
}).then(function f3(value){
    //do sth 3
}).catch(function(error){
    console.log(error);
})
複製程式碼

Promise.prototype.finally

promise.finally(onFinally)
複製程式碼

返回一個Promise,在promise執行結束時,無論結果是fulfilled或者是rejected,在執行then()catch()後,都會執行

Promise.all

promise.all([promise1, promise2, promise3]).then(resolve);
複製程式碼

示例

// `delay`毫秒後執行resolve
function timerPromisefy(delay) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(delay);
        }, delay);
    });
}
var startDate = Date.now();
// 所有promise變為resolve後程式退出
Promise.all([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
]).then(function (values) {
    console.log(Date.now() - startDate + 'ms');
    // 約128ms
    console.log(values);    // [1,32,64,128]
});
複製程式碼

在接收到所有的物件promise都變為 FulFilled 返回一個resolve(array);或者 某一個promise物件變成Rejected 狀態返回resolve(err) 傳遞給 Promise.allpromise並不是一個個的順序執行的,而是同時開始、並行執行的

Promise.race

promise.race([promise1, promise2]).then(resolve, reject)
複製程式碼

示例

// `delay`毫秒後執行resolve
function timerPromisefy(delay) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(delay);
        }, delay);
    });
}
// 任何一個promise變為resolve或reject 的話程式就停止執行
Promise.race([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
]).then(function (value) {
    console.log(value);    // => 1
});
複製程式碼

只要有一個promise物件進入 FulFilled 或者 Rejected 狀態的話,就會繼續進行後面的處理。

Promise polyfill & Test

promise-polyfill.js

學習完Promise後,必定要重寫Promise,後續遇到瀏覽器環境不支援也可有的放矢 程式碼如下

/**
 * @author chenchangyuan
 * @date 2019-02-23
 * */
function Promise(executor){
    if(typeof executor !== 'function'){
        throw new Error('executor is not a function');
    }
    var self = this;
    self.state = 'pending';//pending fulfilled rejected
    self.value = null;
    self.reason = null;
    self.callbackResolveFn = [];
    self.callbackRejectFn = [];
    function resolve(value){
        if(self.state === 'pending'){
            self.state = 'fulfilled';
            self.value = value;
            self.callbackResolveFn.forEach(function(fn){
                fn();
            });
        }
    }
    function reject(reason){
        if(self.state === 'pending'){
            self.state = 'rejected';
            self.reason = reason;
            self.callbackRejectFn.forEach(function(fn){
                fn();
            });
        }
    }
    try{
        executor(resolve, reject);
    }catch(err){
        reject(err);
    }
}
//回溯函式
function resolvePromise(promise, x, resolve, reject){
    if(promise === x) return reject(new TypeError('迴圈引用'));
    var flag = false;
    if(x !== null && (typeof x === 'object' || typeof x === 'function')){
        try{
            var then = x.then;
            if(typeof then === 'function'){
                then.call(x, function(val){
                    if(flag) return;
                    flag = true;
                    resolvePromise(promise, val, resolve, reject);
                }, function(err){
                    if(flag) return;
                    flag = true;
                    reject(err);
                });
            }else{
                resolve(x);
            }
        } catch(err){
            if(flag) return;
            flag = true;
            reject(err);
        }

    }else{
        resolve(x);
    }
}
//返回一個新的promise(pending:push(fn),fulfilled:resolve(val),rejected:reject(reason))
Promise.prototype.then = function(onFulfilled, onRejected){
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(value){
        return value;
    };
    onRejected = typeof onRejected === 'function' ? onRejected : function(err){
        throw new Error(err);
    };
    var self = this,
        promise2;
    if(self.state === 'fulfilled'){
        promise2 = new Promise(function(resolve, reject){
            setTimeout(function(){
                try{
                    //將x處理成一個原始值
                    var x = onFulfilled(self.value);
                    resolvePromise(promise2, x, resolve, reject);
                } catch(e){
                    reject(e);
                }
            })
        })
    }
    if(self.state === 'rejected'){
        promise2 = new Promise(function(resolve, reject){
            setTimeout(function(){
                try{
                    //將x處理成一個原始值
                    var x = onRejected(self.reason);
                    resolvePromise(promise2, x, resolve, reject);
                } catch(e){
                    reject(e);
                }
            })
        })
    }
    if(self.state === 'pending'){
        promise2 = new Promise(function(resolve, reject){
            self.callbackResolveFn.push(function(){
                setTimeout(function(){
                    try{
                        //將x處理成一個原始值
                        var x = onFulfilled(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch(e){
                        reject(e);
                    }
                })
            });
            self.callbackRejectFn.push(function(){
                setTimeout(function(){
                    try{
                        //將x處理成一個原始值
                        var x = onRejected(self.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch(e){
                        reject(e);
                    }
                })
            });
        })
    }
    return promise2;
}
Promise.prototype['catch']= function (callback) {
    return this.then(undefined, callback)
}
Promise.all = function (promises) {
    return new Promise(function (resolve, reject) {
        let arr = [];
        let i = 0;
        function processData(index, y) {
            arr[index] = y;
            if (++i === promises.length) {
                resolve(arr);
            }
        }
        for (let i = 0; i < promises.length; i++) {
            promises[i].then(function (y) {
                processData(i, y)
            }, reject)
        }
    })
}
Promise.race = function (promises) {
    return new Promise(function (resolve, reject) {
        for (var i = 0; i < promises.length; i++) {
            promises[i].then(resolve,reject)
        }
    });
}
Promise.resolve = function(value){
    return new Promise(function(resolve,reject){
        resolve(value);
    });
}
Promise.reject = function(reason){
    return new Promise(function(resolve,reject){
        reject(reason);
    });
}
Promise.defer = Promise.deferred = function () {
    var d = {};
    d.promise = new Promise(function (resolve, reject) {
        d.resolve = resolve;
        d.reject = reject;
    });
    return d
}
module.exports = Promise;複製程式碼

promise-aplus-tests

由於是參(抄)考(襲)前輩的polyfill,自己編碼測試時出現了兩處錯誤,ES6 Promise 規範的2.3.12.3.4

2.3.1

前端非同步技術之Promise

2.3.4

前端非同步技術之Promise

經過改正測試成功

前端非同步技術之Promise

後記

你們的支援是我最大的動力,熬夜碼字不易,如果此文對你有幫助,請不吝stargithub.com/chenchangyu…  

有興趣加筆者好友的同學請掃描下方二維碼(1.本人微信,2.微信公眾號,3.技術交流微信群),願與您成為好友共同探討技術,暢聊生活!

前端非同步技術之Promise

參考資料

promisesaplus.com/

liubin.org/promises-bo…

juejin.im/post/5ab20c…


相關文章