JS異步處理的進化史

原文地址:banggan.github.io/2019/08/26/…javascript

前言

javascript是一門單線程的語言,也就是說一次只能完成一件任務,若是有多個任務,就須要排隊進行處理。若是一個任務耗時很長,後面的任務也必須排隊等待,這樣大大的影響了整個程序的執行。爲了解決這個問題,javascript語言將任務分爲兩種模式:java

  • 同步:當咱們打開網站,網頁的頁面骨架渲染和頁面元素渲染,就是一大推同步任務。
  • 異步:咱們在瀏覽新聞時,加載圖片或音樂之類佔用資源大且耗時久的任務就是異步任務。

本文主要針對近兩年javascript的發展,主要介紹異步處理的進化史。目前,在javascript異步處理中,有如下幾種方式:jquery

異步處理.png

callback

回調函數是最先解決異步編程的方法。不管是常見的setTimeout仍是ajax請求,都是採用回調的形式把事情在某一固定的時刻進行執行。git

 //常見的:setTimeout
 setTimeout(function callback(){
   console.log('aa');
  }, 1000);
  //ajax請求
  ajax(url,function callback(){
      console.log("ajax success",res);
  })
複製代碼

回調函數的處理通常將函數callback做爲參數傳進函數,在合適的時候被調用執行。回調函數的優勢就是簡單、容易理解和實現,但有個致命的缺點,容易出現回調地獄(Callback hell),即多個回調函數嵌套使用。形成代碼可讀性差、可維護性差且只能在回調中處理異常。github

ajax(url, () => {
	//todo
	ajax(url1, () => {
		//todo
		ajax(url2, () => {
			//todo
		})
	})
})
複製代碼

事件監聽

事件監聽採用的是事件驅動的模式。事件的執行不取決於代碼的順序,而是某個事件的發生。ajax

假設有兩個函數,爲f1綁定一個事件(jQuery的寫法),當f1函數發生success事件時,執行函數f2:編程

f1.on('success',f2);
複製代碼

對f1進行改寫:數組

function f1(){
	ajax(url,() => {
		//todo
		f1.trigger('success');//觸發success事件,從而執行f2函數
	})
}
複製代碼

事件監聽的方式較容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數,並且能夠"去耦合",有利於實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。閱讀代碼的時候,很難看出主流程。promise

發佈訂閱

咱們假定,存在一個"信號中心",某個任務執行完成,就向信號中心"發佈"(publish)一個信號,其餘任務能夠向信號中心"訂閱"(subscribe)這個信號,從而知道何時本身能夠開始執行。這就叫作 發佈/訂閱模式(publish-subscribe pattern),又稱**觀察者模式"(observer pattern) **。併發

//利用jquery的插件實現
//首先,f2向消息中心訂閱success事件
jQuery.subscribe('success',f2);
//對f1進行改寫:
function f1(){
	ajax(url,() => {
		//todo
		jQuery.publish('success');//當f1執行完畢後,向消息中心jQuery發佈success事件,從而執行f2函數
	})
}
//f2執行完畢後,能夠取消訂閱
jQuery.unsubscribe('success',f2)
複製代碼

該方法和事件監聽的性質相似,但咱們能夠經過消息中心來查閱一共有多少個信號,每一個信號有多少個訂閱者。

Promise

**Promise**是CommonJS工做組提出的一種規範,能夠獲取異步操做的消息,也是異步處理中經常使用的一種解決方案。Promise的出現主要是用來解決回調地獄、支持多個併發的請求,獲取併發請求的數據而且解決異步的問題。

let p = new Promise((resolve, reject) => {
    //作一些異步操做
   setTimeout(()=>{
      let num = parseInt(Math.random()*100);
      	if(num > 50){
          resolve("num > 50"); // 若是數字大於50就調用成功的函數,而且將狀態變成Resolved
       	}else{
         	reject("num <50");// 不然就調用失敗的函數,將狀態變成Rejected
       }
	},10000)
});
p.then((res) => {
	console.log(res);
}).catch((err) =>{
  console.log(err);
})
複製代碼

Promise有三種狀態:等待pending、成功fulfied、失敗rejected;狀態一旦改變,就不會再變化,在Promise對象建立後,會立刻執行。等待狀態能夠變爲fulfied狀態並傳遞一個值給相應的狀態處理方法,也可能變爲失敗狀態rejected並傳遞失敗信息。任一一種狀況出現時,Promise對象的 then 方法就會被調用(then方法包含兩個參數:onfulfilled 和 onrejected,均爲 Function。當Promise狀態爲fulfilled時,調用 then 的 onfulfilled 方法,當Promise狀態爲rejected時,調用 then 的 onrejected 方法)。

須要注意的是: Promise.prototype.thenPromise.prototype.catch 方法返回promise 對象, 因此能夠被鏈式調用,以下圖:

promise.png

Promise的方法:

  • Promise.all(iterable):誰執行得慢,以誰爲準執行回調。返回一個promise對象,只有當iterable裏面的全部promise對象成功後纔會執行。一旦iterable裏面有promise對象執行失敗就觸發該對象的失敗。對象在觸發成功後,會把一個包iterable裏全部promise返回值的數組做爲成功回調的返回值,順序跟iterable的順序保持一致;若是這個新的promise對象觸發了失敗狀態,它會把iterable裏第一個觸發失敗的promise對象的錯誤信息做爲它的失敗錯誤信息。Promise.all方法常被用於處理多個promise對象的狀態集合。
  • Promise.race(iterable): 誰執行得快,以誰爲準執行回調。iterable參數裏的任意一個子promise被成功或失敗後,父promise立刻也會用子promise的成功返回值或失敗詳情做爲參數調用父promise綁定的相應句柄,並返回該promise對象。
  • Promise.reject(err)Promise.resolve(res)

Generators/yield

Generators是ES6提供的異步解決方案,其最大的特色就是能夠控制函數的執行。能夠理解成一個內部封裝了不少狀態的狀態機,也是一個遍歷器對象生成函數。Generator 函數的特徵:

  • function關鍵字與函數名之間有一個星號;
  • 函數體內部使用yield表達式,定義不一樣的內部狀態;
    • 經過yield暫停函數,next啓動函數,每次返回的是yield表達式結果。next能夠接受參數,從而實如今函數運行的不一樣階段,能夠從外部向內部注入不一樣的值。next返回一個包含valuedone的對象,其中value表示迭代的值,後者表示迭代是否完成。

舉個例子:

function* createIterator(x) {
  let y = yield (x+1)
  let z = 2*(yield(y/3))
  return (x+y+z)
}
// generators能夠像正常函數同樣被調用,不一樣的是會返回一個 iterator
let iterator = createIterator(4);
console.log(iterator.next()); // {value:5,done:false}
console.log(iterator.next()); // {value:NaN,done:false}
console.log(iterator.next()); // {value:NaN,done:true}
let iterator1 = createIterator(4);//返回一個iterator
//next傳參數
console.log(iterator1.next());   // {value:5,done:false}
console.log(iterator1.next(12)); // {value:4,done:false}
console.log(iterator1.next(15)); // {value:46,done:true}
複製代碼

代碼分析:

  • 當不參數時,next的value返回NaN;

  • 當傳參數時,做爲上一個yeild的值,在第一次使用next時,傳參數無效,只有第二次開始,纔有效。

  • 第一次執行next時,函數會被暫停在yeild(x+1),因此返回的是4+1=5;

  • 第二次執行next時,傳入的12爲上一次yeild表達式的值,因此y=12,返回的是12/3=4;

  • 第三次執行next時,傳入的15爲上一次yeild表達式的值,因此z=30,y=12;x=4,返回30+12+4=46

async/await

初入async/await

async/await在ES7提出,是目前在javascript異步處理的終極解決方案。

async 其本質是 Generator 函數的語法糖。相較於Generator放入改進以下:

  • 內置執行器:Generator 函數的執行必須靠執行器,而async函數自帶執行器。其調用方式與普通函數如出一轍,不須要調next方法;
  • 更好的語義:async表示定義異步函數,而await表示後面的表達式須要等待,相較於*和yeild更語義化;
  • 更廣的適用性:co模塊約定,yield命令後面只能是Thunk函數或 Promise對象。而 async 函數的await命令後面則能夠是Promise 或者 原始類型的值;
  • 返回Promise:async 函數返回值是Promise對象,比 Generator函數返回的 Iterator對象方便,能夠直接使用 then() 方法進行鏈式調用;
語法分析
  • async語法

    • 用來定義異步函數,自動將函數轉換爲promise對象,可使用then來添加回調,其內部return的值做爲then回調的參數。

      async function f(){
      	return "hello async";
      }
      f().then((res) => {   //經過then來添加回調且內部返回的res做爲回調的參數
      	console.log(res);   // hello async
      })
      複製代碼
    • 在異步函數的內部可使用await,其返回的promise對象必須等到內部因此await命令後的promise對象執行完,纔會發生狀態變化即執行then回調。

      const delay = function(timeout){ 
      	return new Promise(function(resolve){
      		return setTimeout(resolve, timeout);
        });
      }
      async function f(){
          await delay(1000);
          await delay(2000);
          return '完成';
      }
      f().then(res => console.log(res));//須要等待3秒以後纔會打印:完成
      
      複製代碼
  • await即表示異步等待,用來暫停異步函數的執行,只能在異步函數和promise使用,且當使用在promise前面,表示等待promise完成並返回結果。

    async function f() {
        return await 1   //await後面不是Promise的話,也會被轉換爲一個當即爲resolve的promise
    };
    f().then( res => console.log("處理成功",res))//打印出:處理成功 1
    	 .catch(err => console.log("處理是被",err))////打印出:Promise{<resolved>:undefined}
    
    複製代碼
錯誤處理

若是await後面的異步出現錯誤,等同於async返回的promise對象爲reject,其錯誤會被catch的回調函數接收到。須要注意的是,當 async 函數中只要一個 await 出現 reject 狀態,則後面的 await 都不會被執行。

let a;
async function f(){
    await Promise.reject("error")
    a = await 1       //該await並無執行 
}
err().then(res => console.log(a))

複製代碼

怎麼處理呢,能夠把第一個await放在try/catch,遇到函數的時候,能夠將錯誤拋出並往下執行。

async function f() {  
    try{  
       await Promise.reject('error');    
    }catch(error){
        console.log(error);
    }
    return await 1
}
f().then(res => console.log('成功', res))//成功打印出1
複製代碼

若是有多個await處理,能夠統一放在try/catch模塊中,並且async可使得try/catch同時處理同步和異步錯誤。

總結

經過以上六種javascript異步處理的經常使用方法,能夠看出async/await能夠說是異步終極解決方案了,最後看一下async/await用得最多的場景:

若是一個業務須要不少個異步操做組成,而且每一個步驟都依賴於上一步的執行結果,這裏採用不一樣的延時來體現:

//首先定義一個延時函數
function delay(time) {
    return new Promise(resolve => {
        setTimeout(() => resolve(time), time);
    });
}
//採用promise鏈式調用實現
delay(500).then(result => {
    return delay(result + 1000)
}).then(result => {
    return delay(result + 2000)
}).then(result => {
    console.log(result)   //3500ms後打印出3500
}).catch(error => {
    console.log(error)
}) 
//採用async實現
async function f(){
  const r1 = await delay(500)
  const r2 = await delay(r1+1000)
  const r3 = await delay(r2+2000)
  return r3
}
f().then(res =>{
  console.log(res)
}).catch(err=>{
  console.log(err)
})
複製代碼

能夠看出,採用promise實現採用了不少then進行不停的鏈式調用,使得代碼變得冗長和複雜且沒有語義化。而 async/await首先使用同步的方法來寫異步,代碼很是清晰直觀,並且使代碼語義化,一眼就能看出代碼執行的順序,最後 async 函數自帶執行器,執行的時候無需手動加載。

相關文章
相關標籤/搜索