js處理異步函數:從callback到promise

函數的執行分爲同步和異步兩種。
同步即爲 同步連續執行,通俗點講就是作完一件事,再去作另外一件事。
異步即爲 先作一件事,中間能夠去作其餘事情,稍後再回來作第一件事情。
同時還要記住兩個特性:1.異步函數是沒有返回值的,return無論用哦 2.try{}catch(e){}不能捕獲異步函數中的異常。前端

js在處理異步回調函數的狀況有着愈來愈值得推崇的方法及類庫,下面會依次介紹js處理異步函數的發展史,及源碼解讀。
(本文代碼是運行在node環境中)node

1.callback

let fs = require('fs');
fs.readFile('./1.txt','utf8',function(err,data){
    console.log(data);
})

若是隻有一個異步請求,那用callback還好,可是相信大多數前端開發者都遇到過這兩種狀況:
a.一個異步請求獲取到的結果是下一個異步請求的參數。(一直嵌套callback,代碼很差管理會造成回調地獄);jquery

let fs = require('fs');
    fs.readFile('./1.txt','utf8',(err,data)=>{
        fs.readFile(data,'utf8',(err,data)=>{
            console.log(data);
        })
    })

b.發出兩個請求,只有當兩個請求都成功獲取到數據,在執行下一步操做。es6

let fs =require('fs');
    fs.readFile('./1.txt','utf8',(err,data)=>{
        console.log(data);
    })
    fs.readFile('./2.txt','utf8',(err,data)=>{
        console.log(data);
    })

像相似這種狀況,只有當讀取到1.txt 和2.txt的文件的時候,咱們同時獲取到兩個異步請求的結果。咱們能夠寫一個計數器的函數,統一處理回調;promise

function after(time,callback){
    let arr = [];
    return function(data){
        arr.push(data)
        if(--time==0){
            callback(arr);
        }
    }
}
  //統一處理回調結果的回調傳到after函數中。
  let out = after(2,(res)=>{console.log(res)});
  let fs =require('fs');
    fs.readFile('./1.txt','utf8',(err,data)=>{
        out(data);
    })
    fs.readFile('./2.txt','utf8',(err,data)=>{
        out(data);
    })

tips:併發

方便咱們更好的瞭解計數器的實現原理,咱們須要瞭解一個概念:高階函數
高階函數:能夠把函數做爲參數 或者 return返回出一個函數。
舉個例子:異步

①.判斷一個變量是否是屬於一個類型:模塊化

function isType(type,content){
   return Object.protoType.toString.call(content) ==`[Object ${type}]`
}
let a = [1,2,3];
isType('Array', a) == true;

②.js數據類型有好多,咱們每次調用都要傳入他的類型,麻不麻煩。因此咱們寫一個方法,能夠批量生成函數。函數

function isType(type){
    return function(content){
        return Object.protoType.toString.call(content) == `[Oject ${type}]`
    }
}
let isArray = isType('Array');
let a = [1,2,3]
isArray(a);

前兩種示例講的是return返回一個函數,下面示例是一個預置函數及返回函數參數的結合示例(預置函數)。測試

③.場景加入我有一個函數,執行第三次的時候我想輸出'我很可愛';日常咱們能夠這樣去實現:

let time =0;
     function say(){
         if(++item==3){
         console.log('我很可愛')
         }
     }
     say();
     say();
     say();

高階函數實現的話:

function after(time,callback){
        return function(){
            if(--time ==0){
                   callback();
            }
        }
     }
     function say(){
         console.log('我很可愛');
     }
     let out =after(3,say)
     out();
     out();
     out();

高階函數實現了將計時任務與業務邏輯拆分,高階函數的實現主要得益於做用域的查找。

2.Promise

在看完了上面的callback講述,主要其實仍是講述了callback的弊端:
a.回調地獄(callback沒法解決)
b.併發請求,同時拿到結果(可經過計數器方式,可是太費勁,不太樂觀)
這個時候duang~duang~duang~,ES6帶着Promise來了~
Promise主要是es6提供的主要用於處理異步請求的一個對象,他可以很好的解決回調地獄以及併發請求。
在寫promise源碼以前,咱們先經過幾個調用promise的示例,瞭解一下promise的一些原理及特性,這在咱們封裝promise的時候可以起到很大的做用:
普通調用實例:

let fs = require('fs');
let p = new Promise(function(resolve,reject){
  fs.readFile('./1.txt','utf8',(err,data)=>{
      err?reject(err):resolve(data);
  })
})

p.then((data)=>{console.log(data)},(err)=>{console.log(err)});

1.promise實例能夠屢次調用then方法;

p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
  p.then((data)=>{console.log(data)},(err)=>{console.log(err)});

2.promise實例能夠支持then方法的鏈式調用,jquery實現鏈式是經過返回當前的this。可是promise不能夠經過返回this來實現。由於後續經過鏈式增長的then不是經過原始的promise對象的狀態來決定走成功仍是走失敗的。

p.then((data)=>{console.log(data)},(err)=>{console.log(err)}).then((data)=>{console.log(data)})

3.只要then方法中的成功回調和失敗回調,有返回值(包括undefiend),都會走到下個then方法中的成功回調中,而且把返回值做爲下個then成功回調的參數傳進去。

第一個then走成功:
p.then((data)=>{return undefined},(err)={console.log()}).then((data)=>{console.log(data)})
輸出:undefiend
第一個then走失敗:
  p.then((data)=>{console.log(1)},(err)={return undefined).then((data)=>{console.log(data)})
輸出:undefiend

4.只要then方法中的成功回調和失敗回調,有一個拋出異常,則都會走到下一個then中的失敗回調中;

第一個then走成功:
p.then((data)=>{throw new Err("錯誤")},(err)={console.log(1)}).then((data)=>{console.log('成功')},(err)=>{console.log(err)})
輸出:錯誤
第一個then走失敗:
  p.then((data)=>{console.log(1)},(err)={throw new Err("錯誤")).then((data)=>{console.log('成功')},(err)=>{console.log(err)})
輸出:錯誤

5.成功和失敗 只能走一個,若是成功了,就不會走失敗,若是失敗了,就不會走成功;

6.若是then方法中,返回的不是一個普通值,仍舊是一個promise對象,該如何處理?
答案:它會等待這個promise的執行結果,而且傳給下一個then方法。若是成功,就把這個promise的結果傳給下一個then的成功回調而且執行,若是失敗就把錯誤傳給下一個then的失敗回調而且執行。

7.具有catch捕獲錯誤;若是catche前面的全部then方法都沒有失敗回調,則catche會捕獲到錯誤信息執行他就是用來兜兒底用的。

p是一個失敗的回調:
p.then((data)=>{console.log('成功')}).then((data)=>{成功}).catche(e){console.log('錯誤')}

8.返回的結果和 promise是同一個,永遠不會成功和失敗

var  r  = new Promise(function(resolve,reject){
   return r;
})
r.then(function(){
    console.log(1)
},function(err){
    console.log(err)
})

以上是通過調用es6提供的promise,發現的一些特性,下面咱們會根據這些特性去封裝Promise類。

一.咱們先經過初步瞭解的promise和簡單的基本調用,簡單的實現一個promise;

1.Promise支持傳入一個參數,函數類型,這個函數每每是咱們本身發起異步請求的函數,咱們稱它爲執行器actuator,這個函數會在調用new Promise()的做用域內當即執行,而且傳入兩個函數一個resolve另外一個是reject做爲參數;

2.promise對象支持.then()的方法,then方法支持兩個參數一個爲onFulfilled成功回調另外一個爲onRejected失敗回調;onFulfilled接受參數data爲異步請求拿到的數據,onRejected接受的參數爲捕獲到的異常錯誤。

3.當異步回調成功時,執行resolve,而且把回調結果傳給resolve函數。失敗則執行reject,把異常信息傳給reject函數。(這一步每每是在actuator執行器函數中咱們本身去控制執行的)

4.一個promise對象,執行了resolve,就不會在去執行reject。執行了reject,也不會在去執行resolve;
因此promise內部中有一個相似狀態機的機制,它分爲三種狀態,建立一個promise對象,默認狀態爲'pending'狀態,當執行了resolve,則該狀態變爲'fulfilled',若果執行了reject則該狀態變爲'rejected',因此咱們在then方法中須要根據狀態做出判斷;

5.promise對象已是成功狀態或是失敗狀態時,均可以繼續經過then傳入函數,會經過當前的狀態,來決定執行成功還失敗,而且把結果或是錯誤傳給相應的函數。因此咱們須要拿到的結果和捕獲的錯誤。

function Promise(fn){
    this.status = 'pending';//狀態機
    //一個promise支持執行多個then,因此須要一個池子把他的回調函數存儲起來,統一遍歷執行;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks =[]; 
    //保存結果或者錯誤異常
    this.result = '';//當前promise回調成功獲取到的數據;
    this.reason = '';//當前promise失敗的緣由
    var self = this;
    function resolve(data){
        //執行了reject就不能執行resolve,因此必須保證是pending狀態;
        //當執行回調成功,在執行器調用resolve,咱們去遍歷成功回調的池子,依次執行;
        //保存結果,而且將當前狀態設置爲'fulfilled'
        if(self.status=='pending'){
            self.result = data;
            self.status = 'fulfilled';
            self.onFulfilledCallbacks.forEach((fn)=>{
                fn(data);
            })
        }
          
    }
    function reject(err){
        //執行了resolve就不能執行reject,因此必須保證是pending狀態;
        //當執行回調失敗,在執行器調用reject,咱們去遍歷成功回調的池子,依次執行;
        //保存錯誤緣由而且將當前狀態設置爲'rejected'
        if(self.status=='pending'){
          self.reason= err;
          self.status ='rejected';
          self.onRejectedCallbacks.forEach((fn)=>{
              fn(err);
          })
        }
    }
    fn(resolve,reject)
}
Promise.prototype.then= function(onFulfilled,onRejected){
   //若是當前promise對象成功狀態,則直接執行onFulfilled回調函數,而且把拿到的已經保存的成功數據傳進去。
   if(this.status =='fulfilled'){
       onFulfilled(this.result)    
   }
   //若是當前promise對象失敗狀態,則直接執行rejected回調函數,而且把已經保存的補貨失敗的緣由傳進去。
   if(this.status =='rejected'){
       onRejected(this.reason);
   }
   if(this.status == 'pending'){
       this.onFulfilledCallbacks.push(onFulfilled);
       this.onRejectedCallbacks.push(onRejected);
   }
}

到目前爲止咱們已經封裝了一個簡易版的promise了,咱們能夠經過一些case去測試一下,是否知足上面所描述的特性。

let fs = require('fs');
let p = new Promise((resolve,reject)=>{
   fs.readFile('./1.txt','utf8',function (err,data) {
              err ? reject(err):resolve(data);
   })
});
p.then(data=>{console.log(data)},err=>{console.log(err)}); 
p.then(data=>{console.log(data)},err=>{console.log(err)});

2、咱們簡易版的promise類,已經初步實現了一些promise的基本特性;這一節咱們咱們簡易版的promise進行改版,把promise的更復雜的功能增長進去。

1.當咱們調用promise時,傳入的執行器會馬上執行,執行器函數內部是一個同步的過程,咱們能夠用try...catch捕獲錯誤,而且應該直接調用失敗的函數。

2.promise支持鏈式寫法,then後面繼續.then ,原理並非像jquery同樣返回一個this;而是無論當前promise狀態是什麼,都返回一個新的promise對象,官方文檔命名這個新的promise對象爲promise2。
3.鏈式寫法中第二個then中的回調走成功仍是走失敗,取決於上一個then中返回的promise(就是promise2)對象的狀態。 而 promise2對象的狀態,是由第一個then的參數(成功回調函數或失敗回調函數)的返回值決定的。若是返回的是一個值(包括返回的是undefined、""),則第二個then走成功;若是返回的仍舊是一個promise對象,那麼promise2會等待返回的這個promise對象的回調結果而肯定promise2的狀態值,若是回調結果拿到的是一個值(成功),那麼promise2會將此值做爲參數傳入字節的reosolve中並執行,若是回調中拋出異常(失敗),那麼promise2會把異常傳到reject中而且執行;

function Promise(fn){
    this.status = 'pending';
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks =[]; 
    this.result = '';
    this.reason = '';
    var self = this;
    function resolve(data){
        if(self.status=='pending'){
            self.result = data;
            self.status = 'fulfilled';
            self.onFulfilledCallbacks.forEach((fn)=>{
                fn(data);
            })
        }
          
    }
    function reject(err){           
        if(self.status=='pending'){
          self.reason= err;
          self.status ='rejected';
          self.onRejectedCallbacks.forEach((fn)=>{
              fn(err);
          })
        }
    }
    try{
        fn(resolve,reject)
    }catch(e){
        reject(e)
    }
    
}
Promise.prototype.then= function(onFulfilled,onRejected){
   //then方法什麼都不傳,也能夠支持連續調用  
   onFulfilled = onFulfilled ?onFulfilled :function(data){ return data};
   onRejected =onFulfilled ? onFulfilled :function(err){throw new Error(err)}
   let self = this;
   let Promise2;//聲明primise2
   if(this.status =='fulfilled'){
       Promise2 = new Promise(function(resolve,reject){
           //promise2的狀態,決定下一個then方法中執行成功仍是失敗。
           //promise2的狀態,是由第一個then的onFulfilled的返回值決定的。
           //當咱們執行onFulfilled(咱們經過then方法傳進來的本身的函數)的時候,是同步操做,須要經過trycatch捕獲異常,若是發現異常就直接走下一個then的reject失敗回調。
           //promise官方文檔規定,每個resolve或是reject回調的執行必須保證是在異步中執行,因此咱們強制加定時器,保證onFulfilled是異步執行的。
           setTimeOut(function(){
               try{
                   let x = onFulfilled(self.result);
                   //獲取到返回值,須要去解析,從而判斷出promise2應該走失敗仍是成功。    
                   resolvePromise(Promise2,x,resolve,reject)                                              
               }catch(e){
                  //執行reject,下一個then就會走失敗
                   reject(e);
               }
           })                                                      
       })          
   }
   if(this.status =='rejected'){
     Promise2 = new Promise(function(resolve,reject){
       setTimeout(function(){
           try{
               let x = onRejected(self.reason);
               resolvePromise(Promise2,x,resolve,reject)
           }catch(e){
               reject(e)
           }
       })
     })
       
   }
   if(this.status == 'pending'){
   Promise2 = new Promise(function(resolve,reject){
        self.onFulfilledCallbacks.push(function(){
            setTimeout(function(){
                try{
                    let x =  onFulfilled(self.result);
                    resolvePromise(Promise2,x,resolve,reject);
                }catch (e){
                    reject(e)
                }
            })

        });
        self.onRejectedCallbacks.push(function(){
            setTimeout(function(){
                try {
                    let x =  onRejected(self.reason);
                    resolvePromise(Promise2,x,resolve,reject)
                }catch (e){
                    reject(e);
                }
            })

        });
    })
   }
   return Promise2;
}

function resolvePromise(promise2,x,resolve,reject){
    //此處若是相等會爆出類型錯誤;
    if(promise2 == x){
        reject(new TypeError('循環引用了'))
    }
    //若是x是對象或函數(引用類型的值),則須要進一步判斷。(這塊兒要想的多一些,由於x是開發人員寫的函數返回的,第一個then中回調返回的)
    //若果x是一個普通值,則直接執行resolve,而且傳給下個then的成功; 
    //若是返回的是一個promise對象,則promise2則會等待返回的promise對象執行完成,若是執行完成後,看這個promise走的成功仍是失敗,若是失敗則拋出異常。若是成功則將獲取的數據做爲onFulfilled返回的結果,用於判斷promise2走成功或者失敗,由於返回的結果可能仍是promise對象,因此用遞歸去執行,知道拿到數據或者異常。(遞歸)
    //判斷是否是promise對象,經過有沒有then方法
    //捕獲異常是由於判斷不嚴謹,存在then方法,可能也不是promise對象,調用它的then可能會報錯。      
    let called =false;
    if(x!==null &&(typeof x =='object'|| typeof x =='function')){        
           try{
               let then =x.then;
               if(typeof then =='function'){
                   //promise對象
                   then.call(x,function(y){
                       if(called)return;
                       called = true;
                       resolvePromise(promise2,y,resolve,reject)
                   },function(err){
                       if(called)return;
                       called = true;
                       reject(err)
                   })
               
               }else{
                   //普通對象
                   resolve(x)
               }
           }catch(e){
              if(called)return;
              called = true;
              reject(e)
           }
    }else{
        resolve(x);
    }    
}

 到此,Promise的大部分特性都已經具有了。可是Promise對象還有一些其餘的方法,可供調用,好比說catch方法,還有他的私有屬性all 、race、defferd,若是前面的Promise封裝懂了,那這些方法就so easy了,下面會根據這些方法的功能一一進行封裝,

1.all方法處理 併發請求,同時得到結果。一個失敗,則失敗,都成功,纔算成功.這個時候咱們就想到前面咱們寫的計數器的用法。

Promise.all([read('./1.txt'),read('./2.txt')]).then(res=>{console.log(res)})
 
 Promise.all = function(promiseArray){               
       return new Promise(function(resolve,reject){
           var result = [];
           var i=0;
           function processData(index,res){
               result[index] = res;
               if(++i==promiseArray.length){
                   resolve(result)
               } 
           }
           promiseArray.forEach((item,index)=>{
               item.then(res=>{processData(index,res)},reject)
           })
       })        
 };

2.race方法,Pomise.race,顧名思義「賽拍」,傳入多個異步promise,只要有一個成功,則就成功,有一個失敗則失敗,後面也可跟then方法。

Promise.race = function(promiseArray){
    return new Promise(function(resolve,reject){
        promiseArray.forEach((item,index)=>{
            item.then(resolve,reject);
        })
    })
}
Promise.race([read('./1.txt'),read('./5.txt')]).then(res=>{console.log(res)},err=>{console.log(err)})

3.生成一個成功的promise,把傳入的參數,傳入到then的成功回調中,該方法返回一個promise

Promise.resolve=function(value){
    return new Promise(function(resolve,reject){
        //promise規範 resolve和reject函數必須是在異步回調中執行
        setTimeout(function(){
            resolve(value);
        })
    })
}
Promise.resolve('123').then(res=>{console.log(res)})

4.生成一個失敗的promise,把傳入的參數,傳入到then的失敗回調中。該方法返回一個promise

Promise.reject = function(err){
    return new Promise(function(resolve,reject){
        setTimeout(function(){
            reject(err);
        })

    })
}
Promise.reject('error').then(res=>{console.log(res)},err=>{console.log(err)})

5.catch託底捕獲錯誤,這個方法是實例的共有方法,應該放到Promise的原型上,每個 promise實例均可以調用.它支持一個參數,該參數是以前全部的then中,並無失敗回調,當發 生錯誤時,最後統一在catch中進行捕獲

Promise.prototype.catch = function(calllback){
return this.then(null,callback)
}

6.不少人都用過jquery的deferrd對象,他和promise的deffer對象很相似。promise的deferred對象只是對promise進行了一次封裝

Promise.defer = Promise.deferred=function(){
    var obj = {};
    obj.promise = new Promise(function(resolve,reject){
        obj.resolve = resolve;
        obj.reject = reject;
    })
    return obj;
}
    let fs = require('fs');
 function read2 (url){
    var deferr = Promise.deferred();
    fs.readFile('./1.txt','utf8',(err,res)=>{
        err?deferr.reject(err):deferr.resolve(res);
    })
     return deferr;
}
read2('./1.txt').then(data=>{console.log(data)})

至此,一個完整的Promise.js封裝完成,固然最後是須要模塊化導出的,咱們採用CommonJS規範導出一個模塊 採用

module.exports = Promise;
相關文章
相關標籤/搜索