關於js中異步問題的解決方案


在js中有一個始終無法繞過的問題,如何優雅地解決異步問題。實際上,js在執行過程中,每遇到一個異步函數,都會將這個異步函數放入一個異步隊列中,只有當同步線程執行結束之后,才會開始執行異步隊列中的函數,這個是討論解決異步方案的前提。

解決問題的方法

主流的解決方法主要有以下幾種:

  1. 回調函數
  2. 事件觸發
  3. 發布/訂閱者模式
  4. promise
  5. generate

方法介紹

回調函數

回調函數應該屬於最簡單粗暴的一種方式,主要表現為在異步函數中將一個函數進行參數傳入,當異步執行完成之后執行該函數

 
回調函數

這種寫法最大的問題是:如果存在這樣的一個業務場景,有三個異步函數A,B,C,其中B的執行需要在A執行結束之后,C的執行需要在B之后,這樣的場景模擬成代碼就是(jquery中ajax方法為例)
 
回調函數地獄

試想,如果再多幾個異步函數,代碼整體的維護性,可讀性都變的極差,如果出了bug,修復的排查過程也變的極為困難,這個便是所謂的 回調函數地獄

 

事件監聽

事件監聽最常用的常見在於DOM元素事件綁定觸發,如果我們想在DOM元素與用戶進行鼠標或其他交互之后執行某些邏輯,就可以使用事件監聽了


 
事件監聽

引申開來,我們可以創建一個Event類,定義on和emit方法來綁定和觸發自定義事件


 
Event類

同樣回到在回調函數上遇到的難題,遇到多層嵌套的異步問題,使用事件觸發的方法如何進行解決?
 
事件監聽

嗯,從寫法上來看,相比於回調函數,整潔了一些。

發布/訂閱者模式

訂閱發布模式定義了一種一對多的依賴關系,讓多個訂閱者對象同時監聽某發布者對象。這個發布者對象在自身狀態變化時,會通知所有訂閱者對象,使它們能夠自動更新自己的狀態。vue就是基於發布/訂閱者模式。
如何通過代碼實現一個發布/訂閱者模式?
引用了《Pro JavaScript Design Patterns》第15章的例子

//創建一個主題發布類 var Publisher=function(){ this.subscribers=[] } Publisher.prototype.publish=function(data){ this.subscribers.forEach(function(fn){ fn(data) }) } /* 在Function上掛載這個些方法,所有的函數都可以調用這些方法 表示所有函數都可以訂閱/取消訂閱相關的主題發布 */ //訂閱 Function.prototype.subscribe=function(publisher){ var that=this; var isExist=publisher.subscribers.some(function(el){ if(el===that){ return true } }) if(!isExist){ publisher.subscribers.push(that) } //return this是為了支持鏈式調用 return this } //取消訂閱 Function.prototype.unsubscribe=function(publisher){ var that=this; //就是將函數從發布者的訂閱者列表中進行刪除 publisher.subscribers=publisher.subscribers.filter(function(el){ if(el!==that){ return true } }) return this } var publisher=new Publisher(); var subscriberObj=function(data){ console.log(data) } subscriberObj.subscribe(publisher) 

這樣就實現了一個簡單的發布訂閱者模式,每次發布者發布新內容時,就會調用publish方法,然后將內容作為參數,依次調用訂閱者函數(subscribers)。
其實,發布/訂閱模式與事件監聽很類似,

  • 事件監聽是將一個回調函數事件綁定在一起,觸發了相應事件,就會執行相應的回調函數
  • 發布/訂閱模式是將訂閱函數放入了發布者的訂閱者列表中,更新時,遍歷訂閱者列表,執行所有的訂閱者函數

promise

promise由社區最早提出和實現,ES6 將其寫進了語言標准,統一了用法,原生提供了Promise對象。


 
阮大神對promise的描述

簡單來講,就還是promise中有着三種狀態pending,fulfilled,rejected。在代碼中我們可以控制狀態的變更

 
Promise

創建一個Promise對象需要傳入一個函數,函數的參數是resolve和reject,在函數內部調用時,就分別代表狀態由pending=>fulfilled(成功),pending=>rejected(失敗)
一旦promise狀態發生變化之后,之后狀態就不會再變了。比如:調用resolve之后,狀態就變為fulfilled,之后再調用reject,狀態也不會變化

Promise對象在創建之后會立刻執行,因此一般的做法是使用一個函數進行包裝,然后return一個promise對象

function timeout(){ return new Promise(function(resolve,reject){ ...//異步操作 }) } 

在使用時可以通過promise對象的內置方法then進行調用,then有兩個函數參數,分別表示promise對象中調用resolve和reject時執行的函數

function timeout(){ return new Promise(function(resolve,reject){ setTimeout(function(){ resolve(); },1000) }) } timeout() .then(function(){ ...//對應resolve時執行的邏輯 },function(){ ...//對應reject時執行的邏輯 }) 

可以使用多個then來實現鏈式調用,then的函數參數中會默認返回promise對象

timeout()
    .then(function(){ ...//對應resolve時執行的邏輯 },function(){ ...//對應reject時執行的邏輯 }) .then(function(){ ...//上一個then返回的promise對象對應resolve狀態時執行的邏輯 },function(){ ...//上一個then返回的promise對象對應reject狀態時執行的邏輯 }) 

使用promise來解決回調地獄的做法就是使用then的鏈式調用

function fnA(){ return new Promise(resolve=>{ ...//異步操作中resolve }) } function fnB(){ return new Promise(resolve=>{ ...//異步操作中resolve }) } function fnC(){ return new Promise(resolve=>{ ...//異步操作中resolve }) } fnA() .then(()=>{ return fnB() }) .then(()=>{ return fnC() }) 

清晰直觀了許多

generate函數

創建一個generate函數很簡單

function* gen(){ yield 1 yield 2 return 3 } 

區別於普通函數的地方在於function后面的*號,以及函數內部的yield。
*號是定義方式,帶有 * 號表示是一個generate函數,yield是其內部獨特的語法。

function* gen(){ yield 1 yield 2 return 3 } let g=gen(); console.log(g.next())//{value:1,done:false} console.log(g.next())//{value:2,done:false} console.log(g.next())//{value:3,done:true} console.log(g.next())//{value:undefined,done:true} 

調用generate函數會生成一個遍歷器對象,不會立即執行,需要調用next執行,執行到帶有yield的那一步,next會返回一個對象,對象中value表示yield或return后的值,done表示函數是否已經執行結束(是否已經執行到return)。之后每次執行next都會從上一個yield開始繼續執行

function* gen(){ let res=yield 1 yield res return 3 } let g=gen(); console.log(g.next())//{value:1,done:false} console.log(g.next(333))//{value:333,done:false} 

在next中傳入參數會作為上一次yield的返回值(會忽略第一個next中傳遞的參數)

但是如何使用generate函數來進行異步編程?
這里可以使用ES2017中的async和await語法(其實屬於generate函數的語法糖)
使用async替換*號,使用await替換yield就將generate函數改造成了一個async函數

async function test(){ await 1 await asyncFn() } test() 

其中await后面可以跟promise和原始數據類型(相當與同步操作),而async返回一個promise對象(因此可以使用then進行鏈式調用),使用時直接調用就行了

我們再來看看如何解決回調地獄的問題

function fnA(){ return new Promise(resolve=>{ ...//異步操作中resolve }) } function fnB(){ return new Promise(resolve=>{ ...//異步操作中resolve }) } function fnC(){ return new Promise(resolve=>{ ...//異步操作中resolve }) } async function gen(){ let resA=await fnA() let resB=await fnB(resA) let resC=await fnC(resB) } gen() 

相比於之前的方法更簡單直觀,也更容易理解整體的代碼流程。



作者:醉里挑燈看劍_0696
鏈接:https://www.jianshu.com/p/80dcd01d415e
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM