摘要:在處理異步回調函數的情況有著越來越值得推崇的方法及類庫,下面會依次介紹處理異步函數的發展史,及源碼解讀。而對象的狀態,是由第一個的參數成功回調函數或失敗回調函數的返回值決定的。
函數的執行分為同步和異步兩種。
同步即為 同步連續執行,通俗點講就是做完一件事,再去做另一件事。
異步即為 先做一件事,中間可以去做其他事情,稍后再回來做第一件事情。
同時還要記住兩個特性:1.異步函數是沒有返回值的,return不管用哦 2.try{}catch(e){}不能捕獲異步函數中的異常。
js在處理異步回調函數的情況有著越來越值得推崇的方法及類庫,下面會依次介紹js處理異步函數的發展史,及源碼解讀。
(本文代碼是運行在node環境中)
let fs = require("fs"); fs.readFile("./1.txt","utf8",function(err,data){ console.log(data); })
如果只有一個異步請求,那用callback還好,但是相信大多數前端開發者都遇到過這兩種情況:
a.一個異步請求獲取到的結果是下一個異步請求的參數。(一直嵌套callback,代碼不好管理會形成回調地獄);
let fs = require("fs"); fs.readFile("./1.txt","utf8",(err,data)=>{ fs.readFile(data,"utf8",(err,data)=>{ console.log(data); }) })
b.發出兩個請求,只有當兩個請求都成功獲取到數據,在執行下一步操作。
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的文件的時候,我們同時獲取到兩個異步請求的結果。我們可以寫一個計數器的函數,統一處理回調;
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)});
二、我們簡易版的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;
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93554.html
摘要:接下來我們看下三類異步編程的實現。事件監聽事件發布訂閱事件監聽是一種非常常見的異步編程模式,它是一種典型的邏輯分離方式,對代碼解耦很有用處。 一、 一道面試題 前段時間面試,考察比較多的是js異步編程方面的相關知識點,如今,正好輪到自己分享技術,所以想把js異步編程學習下,做個總結。下面這個demo 概括了大多數面試過程中遇到的問題: for(var i = 0; i < 3; i++...
摘要:最受歡迎的引擎是,由和使用,用于,以及使用的。引擎它們是如何工作的全局執行上下文和調用堆棧剛剛了解了引擎如何讀取變量和函數聲明,它們最終被放入了全局內存堆中。事件循環只有一個任務它檢查調用堆棧是否為空。 為了保證可讀性,本文采用意譯而非直譯。 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! 有沒有想過瀏覽器如何讀取和運行JS代碼? 這看起來很神奇,我們可以通過瀏覽...
摘要:事件循環從回調隊列中獲取并將其推送到調用堆棧。如何工作請注意,不會自動將您的回調函數放到事件循環隊列中。它設置了一個計時器,當計時器到期時,環境將您的回調函數放入事件循環中,以便將來的某個事件會將其選中并執行它。 我們將通過回顧第一篇文章中單線程編程的缺點,然后在討論如何克服它們來構建令人驚嘆的JavaScript UI。在文章結尾處,我們將分享5個關于如何使用async / awai...
摘要:異步編程三座大山原型原型鏈作用域閉包同步異步。異步操作執行完畢后,再執行該回調函數,確保回調在異步操作之后執行。回調函數本身是我們約定俗成的一種叫法,我們定義它,但是并不會自己去執行它,它最終被其他人執行了。 JS異步編程 JS三座大山:原型原型鏈、作用域閉包、同步異步。之前有寫過自己對閉包的理解,今天來總結一下JS中的異步。 思考(案例來自stackoverflow): functi...
摘要:以下展示它是如何工作的函數使用構造函數創建一個新的對象,并立即將其返回給調用者。在傳遞給構造函數的函數中,我們確保傳遞給,這是一個特殊的回調函數。 本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。 歡迎關注我的專欄,之后的博文將在專欄同步: Encounter的掘金專欄 知乎專欄...
閱讀 1410·2021-11-17 09:33
閱讀 3018·2021-10-13 09:39
閱讀 2686·2021-10-09 10:01
閱讀 2447·2021-09-29 09:35
閱讀 3891·2021-09-26 10:01
閱讀 3518·2019-08-26 18:37
閱讀 3149·2019-08-26 13:46
閱讀 1910·2019-08-26 13:39