函數的執行分爲同步和異步兩種。
同步即爲 同步連續執行,通俗點講就是作完一件事,再去作另外一件事。
異步即爲 先作一件事,中間能夠去作其餘事情,稍後再回來作第一件事情。
同時還要記住兩個特性:1.異步函數是沒有返回值的,return無論用哦 2.try{}catch(e){}不能捕獲異步函數中的異常。前端
js在處理異步回調函數的狀況有着愈來愈值得推崇的方法及類庫,下面會依次介紹js處理異步函數的發展史,及源碼解讀。
(本文代碼是運行在node環境中)node
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();
高階函數實現了將計時任務與業務邏輯拆分,高階函數的實現主要得益於做用域的查找。
在看完了上面的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;