偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

深入淺出JavaScript異步編程

開發(fā) 前端
單一的 Promise 鏈并不能凸顯 async/await 的優(yōu)勢。但是,如果處理流程比較復(fù)雜,那么整段代碼將充斥著 then,語義化不明顯,代碼不能很好地表示執(zhí)行流程,這時async/await的優(yōu)勢就能體現(xiàn)出來了。

瀏覽器中的 JavaScript 是典型的事件驅(qū)動型程序,即它們會等待用戶觸發(fā)后才真正的執(zhí)行,而基于的JavaScript的服務(wù)器通常要等待客戶端通過網(wǎng)絡(luò)發(fā)送請求,然后才能執(zhí)行。這種異步編程在JavaScript是很常見的,下面就來介紹幾個異步編程的重要特性,它們可以使編寫異步代碼更容易。

本文將按照異步編程方式的出現(xiàn)時間來歸納整理:

圖片

一、什么是異步

下面先來看看同步和異步的概念:

  • 同步: 在執(zhí)行某段代碼時,在沒有得到返回結(jié)果之前,其他代碼暫時是無法執(zhí)行的,但是一旦執(zhí)行完成拿到返回值,即可執(zhí)行其他代碼。也就是說,在此段代碼執(zhí)行完未返回結(jié)果之前,會阻塞之后的代碼執(zhí)行,這樣的情況稱為同步。
  • 異步: 當(dāng)某一代碼執(zhí)行異步過程調(diào)用發(fā)出后,這段代碼不會立刻得到返回結(jié)果。而是在異步調(diào)用發(fā)出之后,一般通過回調(diào)函數(shù)處理這個調(diào)用之后拿到結(jié)果。異步調(diào)用發(fā)出后,不會影響阻塞后面的代碼執(zhí)行,這樣的情況稱為異步。

下面來看一個例子:

// 同步
function syncAdd(a, b) {
  return a + b;
}

syncAdd(1, 2) // 立即得到結(jié)果:3

// 異步
function asyncAdd(a, b) {
  setTimeout(function() {
    console.log(a + b);
  }, 1000)
}

asyncAdd(1, 2) // 1s后打印結(jié)果:3

這里定義了同步函數(shù) syncAdd 和異步函數(shù) asyncAdd,調(diào)用 syncAdd(1, 2) 函數(shù)時會等待得到結(jié)果之后再執(zhí)行后面的代碼。而調(diào)用 asyncAdd(1, 2) 時則會在得到結(jié)果之前繼續(xù)執(zhí)行,直到 1 秒后得到結(jié)果并打印。

我們知道,JavaScript 是單線程的,如果代碼同步執(zhí)行,就可能會造成阻塞;而如果使用異步則不會阻塞,不需要等待異步代碼執(zhí)行的返回結(jié)果,可以繼續(xù)執(zhí)行該異步任務(wù)之后的代碼邏輯。因此,在 JavaScript 編程中,會大量使用異步。

那為什么單線程的JavaScript還能實現(xiàn)異步呢,其實也沒有什么魔法,只是把一些操作交給了其他線程處理,然后采用了事件循環(huán)的機制來處理返回結(jié)果。

二、回調(diào)函數(shù)

在最基本的層面上,JavaScript的異步編程式通過回調(diào)實現(xiàn)的?;卣{(diào)的是函數(shù),可以傳給其他函數(shù),而其他函數(shù)會在滿足某個條件時調(diào)用這個函數(shù)。下面就來看看常見的不同形式的基于回調(diào)的異步編程。

1. 定時器

一種最簡單的異步操作就是在一定時間之后運行某些代碼。如下面代碼:

setTimeout(asyncAdd(1, 2), 8000)

setTimeout()方法的第一個參數(shù)是一個函數(shù),第二個參數(shù)是以毫秒為單位的時間間隔。asyncAdd()方法可能是一個回調(diào)函數(shù),而setTimeout()方法就是注冊回調(diào)函數(shù)的函數(shù)。它還代指在什么異步條件下調(diào)用回調(diào)函數(shù)。setTimeout()方法只會調(diào)用一次回調(diào)函數(shù)。

2. 事件監(jiān)聽

給目標(biāo) DOM 綁定一個監(jiān)聽函數(shù),用的最多的是 addEventListener:

document.getElementById('#myDiv').addEventListener('click', (e) => {
  console.log('我被點擊了')
}, false);

通過給 id 為 myDiv 的一個元素綁定了點擊事件的監(jiān)聽函數(shù),把任務(wù)的執(zhí)行時機推遲到了點擊這個動作發(fā)生時。此時,任務(wù)的執(zhí)行順序與代碼的編寫順序無關(guān),只與點擊事件有沒有被觸發(fā)有關(guān)。

這里使用addEventListener注冊了回調(diào)函數(shù),這個方法的第一個參數(shù)是一個字符串,指定要注冊的事件類型,如果用戶點擊了指定的元素,瀏覽器就會調(diào)用回調(diào)函數(shù),并給他傳入一個對象,其中包含著事件的詳細(xì)信息。

3. 網(wǎng)絡(luò)請求

JavaScript中另外一種常見的異步操作就是網(wǎng)絡(luò)請求:

const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 創(chuàng)建 Http 請求
xhr.open("GET", SERVER_URL, true);
// 設(shè)置狀態(tài)監(jiān)聽函數(shù)
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 當(dāng)請求成功時
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 設(shè)置請求失敗時的監(jiān)聽函數(shù)
xhr.onerror = function() {
  console.error(this.statusText);
};
// 發(fā)送 Http 請求
xhr.send(null);

這里使用XMLHttpRequest類及回調(diào)函數(shù)來發(fā)送HTTP請求并異步處理服務(wù)器返回的響應(yīng)。

4. Node中的回調(diào)與事件

Node.js服務(wù)端JavaScript環(huán)境底層就是異步的,定義了很多使用回調(diào)和事件的API。例如讀取文件默認(rèn)的API就是異步的,它會在讀取文件內(nèi)容之后調(diào)用一個回調(diào)函數(shù):

const fs = require('fs');
let options = {}

//  讀取配置文件,調(diào)用回調(diào)函數(shù)
fs.readFile('config.json', 'utf8', (err, data) => {
    if(err) {
      throw err;
    }else{
     Object.assign(options, JSON.parse(data))
    }
  startProgram(options)
});

fs.readFile()方法以接收兩個參數(shù)的回調(diào)作為最后一個參數(shù)。它會異步讀取指定文件,如果讀取成功就會將第二個參數(shù)傳遞給回調(diào)的第二個參數(shù),如果發(fā)生錯誤,就會將錯誤傳遞給回調(diào)的第一個參數(shù)。

三、Promise

1. Promise的概念

Promise是一種為簡化異步編程而設(shè)計的核心語言特性,它是一個對象,表示異步操作的結(jié)果。在最簡單的情況下,Promise就是一種處理回調(diào)的不同方式。不過,使用Promise也有實際的用處,基于回調(diào)的異步編程會有一個很現(xiàn)實的問題,那就是經(jīng)常出現(xiàn)回調(diào)多層嵌套的情況,會造成代碼難以理解。Promise可以讓這種嵌套回調(diào)以一種更線性的鏈?zhǔn)叫问奖磉_(dá)出來,因此更容易閱讀和理解。

回調(diào)的另一個問題就是難以處理錯誤, 如果一個異步函數(shù)拋出異常,則該異常沒有辦法傳播到異步操作的發(fā)起者。異步編程的一個基本事實就是它破壞了異常處理。而Promise則標(biāo)準(zhǔn)化了異步錯誤處理,通過Promise鏈提供一種讓錯誤正確傳播的途經(jīng)。

實際上,Promise就是一個容器,里面保存著某個未來才會結(jié)束的事件(通常是異步操作)的結(jié)果。從語法上說,Promise 是一個對象,它可以獲取異步操作的消息。Promise 提供了統(tǒng)一的 API,各種異步操作都可以用同樣的方法進(jìn)行處理。

(1)Promise實例有三個狀態(tài):

  • pending 狀態(tài):表示進(jìn)行中。Promise 實例創(chuàng)建后的初始態(tài);
  • fulfilled 狀態(tài):表示成功完成。在執(zhí)行器中調(diào)用 resolve 后達(dá)成的狀態(tài);
  • rejected 狀態(tài):表示操作失敗。在執(zhí)行器中調(diào)用 reject 后達(dá)成的狀態(tài)。

(2)Promise實例有兩個過程:

  • pending -> fulfilled : Resolved(已完成);
  • pending -> rejected:Rejected(已拒絕)。

Promise的特點:

  • 一旦狀態(tài)改變就不會再變,promise對象的狀態(tài)改變,只有兩種可能:從pending變?yōu)閒ulfilled,從pending變?yōu)閞ejected。當(dāng) Promise 實例被創(chuàng)建時,內(nèi)部的代碼就會立即被執(zhí)行,而且無法從外部停止。比如無法取消超時或消耗性能的異步調(diào)用,容易導(dǎo)致資源的浪費;
  • 如果不設(shè)置回調(diào)函數(shù),Promise內(nèi)部拋出的錯誤,不會反映到外部;
  • Promise 處理的問題都是“一次性”的,因為一個 Promise 實例只能 resolve 或 reject 一次,所以面對某些需要持續(xù)響應(yīng)的場景時就會變得力不從心。比如上傳文件獲取進(jìn)度時,默認(rèn)采用的就是事件監(jiān)聽的方式來實現(xiàn)。

下面來看一個例子:

const https = require('https');

function httpPromise(url){
  return new Promise((resolve,reject) => {
    https.get(url, (res) => {
      resolve(data);
    }).on("error", (err) => {
      reject(error);
    });
  })
}

httpPromise().then((data) => {
  console.log(data)
}).catch((error) => {
  console.log(error)
})

可以看到,Promise 會接收一個執(zhí)行器,在這個執(zhí)行器里,需要把目標(biāo)異步任務(wù)給放進(jìn)去。在 Promise 實例創(chuàng)建后,執(zhí)行器里的邏輯會立刻執(zhí)行,在執(zhí)行的過程中,根據(jù)異步返回的結(jié)果,決定如何使用 resolve 或 reject 來改變 Promise實例的狀態(tài)。

在這個例子里,當(dāng)用 resolve 切換到了成功態(tài)后,Promise 的邏輯就會走到 then 中傳入的方法里去;用 reject 切換到失敗態(tài)后,Promise 的邏輯就會走到 catch 傳入的方法中。

這樣的邏輯,本質(zhì)上與回調(diào)函數(shù)中的成功回調(diào)和失敗回調(diào)沒有差異。但這種寫法大大地提高了代碼的質(zhì)量。當(dāng)我們進(jìn)行大量的異步鏈?zhǔn)秸{(diào)用時,回調(diào)地獄不復(fù)存在了。取而代之的是層級簡單、賞心悅目的 Promise 調(diào)用鏈:

httpPromise(url1)
    .then(res => {
        console.log(res);
        return httpPromise(url2);
    })
    .then(res => {
        console.log(res);
        return httpPromise(url3);
    })
    .then(res => {
      console.log(res);
      return httpPromise(url4);
    })
    .then(res => console.log(res));

2. Promise的創(chuàng)建

Promise對象代表一個異步操作,有三種狀態(tài):pending(進(jìn)行中)、fulfilled(已成功)和rejected(已失?。?。

Promise構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù)的兩個參數(shù)分別是resolve和reject。

const promise = new Promise((resolve, reject) => {
  if (/* 異步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

一般情況下,我們會用new Promise()來創(chuàng)建Promise對象。除此之外,還也可以使用promise.resolve和 promise.reject這兩個方法來創(chuàng)建:

(1)Promise.resolve

Promise.resolve(value)的返回值是一個promise對象,我們可以對返回值進(jìn)行.then調(diào)用,如下代碼:

Promise.resolve(11).then(function(value){
  console.log(value); // 打印出11
});

resolve(11)會讓promise對象進(jìn)入確定(resolve狀態(tài)),并將參數(shù)11傳遞給后面then中指定的onFulfilled 函數(shù);

(2)Promise.reject

Promise.reject 的返回值也是一個promise對象,如下代碼:

Promise.reject(new Error("我錯了!"));

上面是以下代碼的簡單形式:

new Promise((resolve, reject) => {
   reject(new Error("我錯了!"));
});

下面來綜合看看resolve方法和reject方法:

function testPromise(ready) {
  return new Promise(resolve,reject) => {
    if(ready) {
      resolve("hello world");
    }else {
      reject("No thanks");
    }
  });
};

testPromise(true).then((msg) => {
  console.log(msg);
},(error) => {
  console.log(error);
});

上面的代碼給testPromise方法傳遞一個參數(shù),返回一個promise對象,如果為true,那么調(diào)用Promise對象中的resolve()方法,并且把其中的參數(shù)傳遞給后面的then第一個函數(shù)內(nèi),因此打印出 “hello world”, 如果為false,會調(diào)用promise對象中的reject()方法,則會進(jìn)入then的第二個函數(shù)內(nèi),會打印No thanks。

3. Promise的作用

在開發(fā)中可能會碰到這樣的需求:使用ajax發(fā)送A請求,成功后拿到數(shù)據(jù),需要把數(shù)據(jù)傳給B請求,那么需要這樣編寫代碼:

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

這段代碼之所以看上去很亂,歸結(jié)其原因有兩點:

  • 第一是嵌套調(diào)用,下面的任務(wù)依賴上個任務(wù)的請求結(jié)果,并在上個任務(wù)的回調(diào)函數(shù)內(nèi)部執(zhí)行新的業(yè)務(wù)邏輯,這樣當(dāng)嵌套層次多了之后,代碼的可讀性就變得非常差了。
  • 第二是任務(wù)的不確定性,執(zhí)行每個任務(wù)都有兩種可能的結(jié)果(成功或者失?。泽w現(xiàn)在代碼中就需要對每個任務(wù)的執(zhí)行結(jié)果做兩次判斷,這種對每個任務(wù)都要進(jìn)行一次額外的錯誤處理的方式,明顯增加了代碼的混亂程度。

既然原因分析出來了,那么問題的解決思路就很清晰了:

  • 消滅嵌套調(diào)用;
  • 合并多個任務(wù)的錯誤處理。

這么說可能有點抽象,不過 Promise 解決了這兩個問題。接下來就看看 Promise 是怎么消滅嵌套調(diào)用和合并多個任務(wù)的錯誤處理的。

Promise出現(xiàn)之后,代碼可以這樣寫:

let fs = require('fs')
function read(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,'utf8',function(error,data){
      error && reject(error)
      resolve(data)
    })
  })
}
read('./a.txt').then(data=>{
  return read(data) 
}).then(data=>{
  return read(data)  
}).then(data=>{
  console.log(data)
})

通過引入 Promise,上面這段代碼看起來就非常線性了,也非常符合人的直覺。Promise 利用了三大技術(shù)手段來解決回調(diào)地獄:回調(diào)函數(shù)延遲綁定、返回值穿透、錯誤冒泡。

下面來看一段代碼:

let readFilePromise = (filename) => {
  fs.readFile(filename, (err, data) => {
    if(err) {
      reject(err);
    }else {
      resolve(data);
    }
  })
}
readFilePromise('1.json').then(data => {
  return readFilePromise('2.json')
});

可以看到,回調(diào)函數(shù)不是直接聲明的,而是通過后面的 then 方法傳入的,即延遲傳入,這就是回調(diào)函數(shù)延遲綁定。接下來針對上面的代碼做一下調(diào)整,如下:

let x = readFilePromise('1.json').then(data => {
  return readFilePromise('2.json')  //這是返回的Promise
});
x.then()

根據(jù) then 中回調(diào)函數(shù)的傳入值創(chuàng)建不同類型的 Promise,然后把返回的 Promise 穿透到外層,以供后續(xù)的調(diào)用。這里的 x 指的就是內(nèi)部返回的 Promise,然后在 x 后面可以依次完成鏈?zhǔn)秸{(diào)用。這便是返回值穿透的效果,這兩種技術(shù)一起作用便可以將深層的嵌套回調(diào)寫成下面的形式。

readFilePromise('1.json').then(data => {
    return readFilePromise('2.json');
}).then(data => {
    return readFilePromise('3.json');
}).then(data => {
    return readFilePromise('4.json');
});

這樣就顯得清爽許多,更重要的是,它更符合人的線性思維模式,開發(fā)體驗更好,兩種技術(shù)結(jié)合產(chǎn)生了鏈?zhǔn)秸{(diào)用的效果。

這樣解決了多層嵌套的問題,那另外一個問題,即每次任務(wù)執(zhí)行結(jié)束后分別處理成功和失敗的情況怎么解決的呢?Promise 采用了錯誤冒泡的方式。下面來看效果:

readFilePromise('1.json').then(data => {
    return readFilePromise('2.json');
}).then(data => {
    return readFilePromise('3.json');
}).then(data => {
    return readFilePromise('4.json');
}).catch(err => {
  // xxx
})

這樣前面產(chǎn)生的錯誤會一直向后傳遞,被 catch 接收到,就不用頻繁地檢查錯誤了。從上面的這些代碼中可以看到,Promise 解決效果也比較明顯:實現(xiàn)鏈?zhǔn)秸{(diào)用,解決多層嵌套問題;實現(xiàn)錯誤冒泡后一站式處理,解決每次任務(wù)中判斷錯誤、增加代碼混亂度的問題。

4. Promise的方法

Promise常用的方法:then()、catch()、all()、race()、finally()、allSettled()、any()。

(1)then()

當(dāng)Promise執(zhí)行的內(nèi)容符合成功條件時,調(diào)用resolve函數(shù),失敗就調(diào)用reject函數(shù)。那Promise創(chuàng)建完了,該如何調(diào)用呢?這時就該then出場了:

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法接受兩個回調(diào)函數(shù)作為參數(shù)。第一個回調(diào)函數(shù)是Promise對象的狀態(tài)變?yōu)閞esolved時調(diào)用,第二個回調(diào)函數(shù)是Promise對象的狀態(tài)變?yōu)閞ejected時調(diào)用。其中第二個參數(shù)可以省略。

then方法返回的是一個新的Promise實例。因此可以采用鏈?zhǔn)綄懛?,即then方法后面再調(diào)用另一個then方法。當(dāng)寫有順序的異步事件時,需要串行時,可以這樣寫:

let promise = new Promise((resolve,reject)=>{
    ajax('first').success(function(res){
        resolve(res);
    })
})
promise.then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    
})

(2)catch()

Promise對象的catch方法相當(dāng)于then方法的第二個參數(shù),指向reject的回調(diào)函數(shù)。

不過catch方法還有一個作用,就是在執(zhí)行resolve回調(diào)函數(shù)時,如果出現(xiàn)錯誤,拋出異常,不會停止運行,而是進(jìn)入catch方法中:

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

(3)all()

all方法可以完成并行任務(wù), 它接收一個數(shù)組,數(shù)組的每一項都是一個promise對象。當(dāng)數(shù)組中所有的promise的狀態(tài)都達(dá)到resolved時,all方法的狀態(tài)就會變成resolved,如果有一個狀態(tài)變成了rejected,那么all方法的狀態(tài)就會變成rejected:

let promise1 = new Promise((resolve,reject)=>{
 setTimeout(()=>{
       resolve(1);
 },2000)
});
let promise2 = new Promise((resolve,reject)=>{
 setTimeout(()=>{
       resolve(2);
 },1000)
});
let promise3 = new Promise((resolve,reject)=>{
 setTimeout(()=>{
       resolve(3);
 },3000)
});

Promise.all([promise1,promise2,promise3]).then(res=>{
    console.log(res);  //結(jié)果為:[1,2,3] 
})

調(diào)用all方法時的結(jié)果成功的時候是回調(diào)函數(shù)的參數(shù)也是一個數(shù)組,這個數(shù)組按順序保存著每一個promise對象resolve執(zhí)行時的值。

(4)race()

race方法和all一樣,接受的參數(shù)是一個每項都是promise的數(shù)組,但與all不同的是,當(dāng)最先執(zhí)行完的事件執(zhí)行完之后,就直接返回該promise對象的值。

如果第一個promise對象狀態(tài)變成resolved,那自身的狀態(tài)變成了resolved;反之,第一個promise變成rejected,那自身狀態(tài)就會變成rejected。

let promise1 = new Promise((resolve,reject) => {
 setTimeout(() =>  {
       reject(1);
 },2000)
});
let promise2 = new Promise((resolve,reject) => {
 setTimeout(() => {
       resolve(2);
 },1000)
});
let promise3 = new Promise((resolve,reject) => {
 setTimeout(() => {
       resolve(3);
 },3000)
});
Promise.race([promise1,promise2,promise3]).then(res => {
 console.log(res); //結(jié)果:2
},rej => {
    console.log(rej)};
)

那么race方法有什么實際作用呢?當(dāng)需要執(zhí)行一個任務(wù),超過多長時間就不做了,就可以用這個方法來解決:

Promise.race([promise1, timeOutPromise(5000)]).then(res => console.log(res))

(5)finally()

finally方法用于指定不管 Promise 對象最后狀態(tài)如何,都會執(zhí)行的操作。該方法是 ES2018 引入標(biāo)準(zhǔn)的。

promise.then(result => {···})
    .catch(error => {···})
       .finally(() => {···});

上面代碼中,不管promise最后的狀態(tài)如何,在執(zhí)行完then或catch指定的回調(diào)函數(shù)以后,都會執(zhí)行finally方法指定的回調(diào)函數(shù)。

下面來看例子,服務(wù)器使用 Promise 處理請求,然后使用finally方法關(guān)掉服務(wù)器。

server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);

finally方法的回調(diào)函數(shù)不接受任何參數(shù),這意味著沒有辦法知道,前面的 Promise 狀態(tài)到底是fulfilled還是rejected。這表明,finally方法里面的操作,應(yīng)該是與狀態(tài)無關(guān)的,不依賴于 Promise 的執(zhí)行結(jié)果。

finally本質(zhì)上是then方法的特例:

promise
.finally(() => {
  // 語句
});

// 等同于
promise
.then(
  result => {
    // 語句
    return result;
  },
  error => {
    // 語句
    throw error;
  }
);

上面代碼中,如果不使用finally方法,同樣的語句需要為成功和失敗兩種情況各寫一次。有了finally方法,則只需要寫一次。

(6)allSettled()

Promise.allSettled 的語法及參數(shù)跟 Promise.all 類似,其參數(shù)接受一個 Promise 的數(shù)組,返回一個新的 Promise。唯一的不同在于,執(zhí)行完之后不會失敗,也就是說當(dāng) Promise.allSettled 全部處理完成后,我們可以拿到每個 Promise 的狀態(tài),而不管其是否處理成功。

下面使用 allSettled 實現(xiàn)的一段代碼:

const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
  console.log(results);
});
// 返回結(jié)果:
// [
//    { status: 'fulfilled', value: 2 },
//    { status: 'rejected', reason: -1 }
// ]

可以看到,Promise.allSettled 最后返回的是一個數(shù)組,記錄傳進(jìn)來的參數(shù)中每個 Promise 的返回值,這就是和 all 方法不太一樣的地方。你也可以根據(jù) all 方法提供的業(yè)務(wù)場景的代碼進(jìn)行改造,其實也能知道多個請求發(fā)出去之后,Promise 最后返回的是每個參數(shù)的最終狀態(tài)。

(7)any()

any 方法返回一個 Promise,只要參數(shù) Promise 實例有一個變成 fullfilled 狀態(tài),最后 any 返回的實例就會變成 fullfilled 狀態(tài);如果所有參數(shù) Promise 實例都變成 rejected 狀態(tài),包裝實例就會變成 rejected 狀態(tài)。

下面對上面 allSettled 這段代碼進(jìn)行改造,來看下改造完的代碼和執(zhí)行結(jié)果:

const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.any([resolved, rejected]);
allSettledPromise.then(function (results) {
  console.log(results);
});
// 返回結(jié)果:2

可以看出,只要其中一個 Promise 變成 fullfilled 狀態(tài),那么 any 最后就返回這個 Promise。由于上面 resolved 這個 Promise 已經(jīng)是 resolve 的了,故最后返回結(jié)果為 2。

5. Promise的異常處理

錯誤處理是所有編程范型都必須要考慮的問題,在使用 JavaScript 進(jìn)行異步編程時,也不例外。如果我們不做特殊處理,會怎樣呢?來看下面的代碼,先定義一個必定會失敗的方法

let fail = () => {
    setTimeout(() => {
 throw new Error("fail");
    }, 1000);
};

調(diào)用:

console.log(1);
try {
    fail();
} catch (e) {
    console.log("captured");
}
console.log(2);

可以看到打印出了 1 和 2,并在 1 秒后,獲得一個“Uncaught Error”的錯誤打印,注意觀察這個錯誤的堆棧:

Uncaught Error: fail
    at <anonymous>:3:9

可以看到,其中的 setTimeout (async) 這樣的字樣,表示著這是一個異步調(diào)用拋出的堆棧。但是,captured”這樣的字樣也并未打印,因為母方法 fail() 本身的原始順序執(zhí)行并沒有失敗,這個異常的拋出是在回調(diào)行為里發(fā)生的。 從上面的例子可以看出,對于異步編程來說,我們需要使用一種更好的機制來捕獲并處理可能發(fā)生的異常。

Promise 除了支持 resolve 回調(diào)以外,還支持 reject 回調(diào),前者用于表示異步調(diào)用順利結(jié)束,而后者則表示有異常發(fā)生,中斷調(diào)用鏈并將異常拋出:

const exe = (flag) => () => new Promise((resolve, reject) => {
    console.log(flag);
    setTimeout(() => {
        flag ? resolve("yes") : reject("no");
    }, 1000);
});

上面的代碼中,flag 參數(shù)用來控制流程是順利執(zhí)行還是發(fā)生錯誤。在錯誤發(fā)生的時候,no 字符串會被傳遞給 reject 函數(shù),進(jìn)一步傳遞給調(diào)用鏈:

Promise.resolve()
       .then(exe(false))
       .then(exe(true));

上面的調(diào)用鏈,在執(zhí)行的時候,第二行就傳入了參數(shù) false,它就已經(jīng)失敗了,異常拋出了,因此第三行的 exe 實際沒有得到執(zhí)行,執(zhí)行結(jié)果如下:

false
Uncaught (in promise) no

這就說明,通過這種方式,調(diào)用鏈被中斷了,下一個正常邏輯 exe(true) 沒有被執(zhí)行。 但是,有時候需要捕獲錯誤,而繼續(xù)執(zhí)行后面的邏輯,該怎樣做?這種情況下就要在調(diào)用鏈中使用 catch 了:

Promise.resolve()
       .then(exe(false))
       .catch((info) => { console.log(info); })
       .then(exe(true));

這種方式下,異常信息被捕獲并打印,而調(diào)用鏈的下一步,也就是第四行的 exe(true) 可以繼續(xù)被執(zhí)行。將看到這樣的輸出:

false
no
true

6. Promise的實現(xiàn)

這一部分就來簡單實現(xiàn)一下Promise及其常用的方法。

(1)Promise

const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";

function MyPromise(fn) {
  // 保存初始化狀態(tài)
  var self = this;

  // 初始化狀態(tài)
  this.state = PENDING;

  // 用于保存 resolve 或者 rejected 傳入的值
  this.value = null;

  // 用于保存 resolve 的回調(diào)函數(shù)
  this.resolvedCallbacks = [];

  // 用于保存 reject 的回調(diào)函數(shù)
  this.rejectedCallbacks = [];

  // 狀態(tài)轉(zhuǎn)變?yōu)?resolved 方法
  function resolve(value) {
    // 判斷傳入元素是否為 Promise 值,如果是,則狀態(tài)改變必須等待前一個狀態(tài)改變后再進(jìn)行改變
    if (value instanceof MyPromise) {
      return value.then(resolve, reject);
    }

    // 保證代碼的執(zhí)行順序為本輪事件循環(huán)的末尾
    setTimeout(() => {
      // 只有狀態(tài)為 pending 時才能轉(zhuǎn)變,
      if (self.state === PENDING) {
        // 修改狀態(tài)
        self.state = RESOLVED;

        // 設(shè)置傳入的值
        self.value = value;

        // 執(zhí)行回調(diào)函數(shù)
        self.resolvedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

  // 狀態(tài)轉(zhuǎn)變?yōu)?rejected 方法
  function reject(value) {
    // 保證代碼的執(zhí)行順序為本輪事件循環(huán)的末尾
    setTimeout(() => {
      // 只有狀態(tài)為 pending 時才能轉(zhuǎn)變
      if (self.state === PENDING) {
        // 修改狀態(tài)
        self.state = REJECTED;

        // 設(shè)置傳入的值
        self.value = value;

        // 執(zhí)行回調(diào)函數(shù)
        self.rejectedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

  // 將兩個方法傳入函數(shù)執(zhí)行
  try {
    fn(resolve, reject);
  } catch (e) {
    // 遇到錯誤時,捕獲錯誤,執(zhí)行 reject 函數(shù)
    reject(e);
  }
}

MyPromise.prototype.then = function(onResolved, onRejected) {
  // 首先判斷兩個參數(shù)是否為函數(shù)類型,因為這兩個參數(shù)是可選參數(shù)
  onResolved =
    typeof onResolved === "function"
      ? onResolved
      : function(value) {
          return value;
        };

  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : function(error) {
          throw error;
        };

  // 如果是等待狀態(tài),則將函數(shù)加入對應(yīng)列表中
  if (this.state === PENDING) {
    this.resolvedCallbacks.push(onResolved);
    this.rejectedCallbacks.push(onRejected);
  }

  // 如果狀態(tài)已經(jīng)凝固,則直接執(zhí)行對應(yīng)狀態(tài)的函數(shù)

  if (this.state === RESOLVED) {
    onResolved(this.value);
  }

  if (this.state === REJECTED) {
    onRejected(this.value);
  }
};

(2)Promise.then

then 方法返回一個新的 promise 實例,為了在 promise 狀態(tài)發(fā)生變化時(resolve / reject 被調(diào)用時)再執(zhí)行 then 里的函數(shù),我們使用一個 callbacks 數(shù)組先把傳給then的函數(shù)暫存起來,等狀態(tài)改變時再調(diào)用。

那么,怎么保證后一個 then 里的方法在前一個 then(可能是異步)結(jié)束之后再執(zhí)行呢?

可以將傳給 then 的函數(shù)和新 promise 的 resolve 一起 push 到前一個 promise 的 callbacks 數(shù)組中,達(dá)到承前啟后的效果:

  • 承前:當(dāng)前一個 promise 完成后,調(diào)用其 resolve 變更狀態(tài),在這個 resolve 里會依次調(diào)用 callbacks 里的回調(diào),這樣就執(zhí)行了 then 里的方法了
  • 啟后:上一步中,當(dāng) then 里的方法執(zhí)行完成后,返回一個結(jié)果,如果這個結(jié)果是個簡單的值,就直接調(diào)用新 promise 的 resolve,讓其狀態(tài)變更,這又會依次調(diào)用新 promise 的 callbacks 數(shù)組里的方法,循環(huán)往復(fù)。。如果返回的結(jié)果是個 promise,則需要等它完成之后再觸發(fā)新 promise 的 resolve,所以可以在其結(jié)果的 then 里調(diào)用新 promise 的 resolve
then(onFulfilled, onReject){
    // 保存前一個promise的this
    const self = this; 
    return new MyPromise((resolve, reject) => {
      // 封裝前一個promise成功時執(zhí)行的函數(shù)
      let fulfilled = () => {
        try{
          const result = onFulfilled(self.value); // 承前
          return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); //啟后
        }catch(err){
          reject(err)
        }
      }
      // 封裝前一個promise失敗時執(zhí)行的函數(shù)
      let rejected = () => {
        try{
          const result = onReject(self.reason);
          return result instanceof MyPromise? result.then(resolve, reject) : reject(result);
        }catch(err){
          reject(err)
        }
      }
      switch(self.status){
        case PENDING: 
          self.onFulfilledCallbacks.push(fulfilled);
          self.onRejectedCallbacks.push(rejected);
          break;
        case FULFILLED:
          fulfilled();
          break;
        case REJECT:
          rejected();
          break;
      }
    })
   }

注意:

  • 連續(xù)多個 then 里的回調(diào)方法是同步注冊的,但注冊到了不同的 callbacks 數(shù)組中,因為每次 then 都返回新的 promise 實例(參考上面的例子和圖)
  • 注冊完成后開始執(zhí)行構(gòu)造函數(shù)中的異步事件,異步完成之后依次調(diào)用 callbacks 數(shù)組中提前注冊的回調(diào)

(3)Promise.all

該方法的參數(shù)是 Promise 的實例數(shù)組, 然后注冊一個 then 方法。 待數(shù)組中的 Promise 實例的狀態(tài)都轉(zhuǎn)為 fulfilled 之后則執(zhí)行 then 方法.,這里主要就是一個計數(shù)邏輯, 每當(dāng)一個 Promise 的狀態(tài)變?yōu)?fulfilled 之后就保存該實例返回的數(shù)據(jù), 然后將計數(shù)減一, 當(dāng)計數(shù)器變?yōu)?nbsp;0 時, 代表數(shù)組中所有 Promise 實例都執(zhí)行完畢.

Promise.all = function (arr) {
  let args = Array.prototype.slice.call(arr)
  return new Promise(function (resolve, reject) {
    if (args.length === 0) return resolve([])
    let remaining = args.length
    function res(i, val) {
      try {
        if (val && (typeof val === 'object' || typeof val === 'function')) {
          let then = val.then
          if (typeof then === 'function') {
            then.call(val, function (val) { // 這里如果傳入?yún)?shù)是 promise的話需要將結(jié)果傳入 args, 而不是 promise實例
              res(i, val) 
            }, reject)
            return
          }
        }
        args[i] = val
        if (--remaining === 0) {
          resolve(args)
        }
      } catch (ex) {
        reject(ex)
      }
    }
    for (let i = 0; i < args.length; i++) {
      res(i, args[i])
    }
  })
}

(4)Promise.race

該方法的參數(shù)是 Promise 實例數(shù)組, 然后其 then 注冊的回調(diào)方法是數(shù)組中的某一個 Promise 的狀態(tài)變?yōu)?fulfilled 的時候就執(zhí)行. 因為 Promise 的狀態(tài)只能改變一次, 那么我們只需要把 Promise.race 中產(chǎn)生的 Promise 對象的 resolve 方法, 注入到數(shù)組中的每一個 Promise 實例中的回調(diào)函數(shù)中即可:

oPromise.race = function (args) {
  return new oPromise((resolve, reject) => {
    for (let i = 0, len = args.length; i < len; i++) {
      args[i].then(resolve, reject)
    }
  })
}

四、Generator

1. Generator 概述

(1)Generator

Generator(生成器)是 ES6 中的關(guān)鍵詞,通俗來講 Generator 是一個帶星號的函數(shù)(它并不是真正的函數(shù)),可以配合 yield 關(guān)鍵字來暫?;蛘邎?zhí)行函數(shù)。先來看一個例子:

function* gen() {
  console.log("enter");
  let a = yield 1;
  let b = yield (function () {return 2})();
  return 3;
}
var g = gen()           // 阻塞,不會執(zhí)行任何語句
console.log(typeof g)   // 返回 object 這里不是 "function"
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())

輸出結(jié)果如下:

object
enter
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: true }
{ value: undefined, done: true }

Generator 中配合使用 yield 關(guān)鍵詞可以控制函數(shù)執(zhí)行的順序,每當(dāng)執(zhí)行一次 next 方法,Generator 函數(shù)會執(zhí)行到下一個存在 yield 關(guān)鍵詞的位置。

總結(jié),Generator 的執(zhí)行的關(guān)鍵點如下:

  • 調(diào)用 gen() 后,程序會阻塞,不會執(zhí)行任何語句;
  • 調(diào)用 g.next() 后,程序繼續(xù)執(zhí)行,直到遇到 yield 關(guān)鍵詞時執(zhí)行暫停;
  • 一直執(zhí)行 next 方法,最后返回一個對象,其存在兩個屬性:value 和 done。

(2)yield

yield 同樣也是 ES6 的關(guān)鍵詞,配合 Generator 執(zhí)行以及暫停。yield 關(guān)鍵詞最后返回一個迭代器對象,該對象有 value 和 done 兩個屬性,其中 done 屬性代表返回值以及是否完成。yield 配合著 Generator,再同時使用 next 方法,可以主動控制 Generator 執(zhí)行進(jìn)度。

下面來看看多個 Generator 配合 yield 使用的情況:

function* gen1() {
    yield 1;
    yield* gen2();
    yield 4;
}
function* gen2() {
    yield 2;
    yield 3;
}
var g = gen1();
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())

執(zhí)行結(jié)果如下:

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: 4, done: false }
{value: undefined, done: true}

可以看到,使用 yield 關(guān)鍵詞的話還可以配合著 Generator 函數(shù)嵌套使用,從而控制函數(shù)執(zhí)行進(jìn)度。這樣對于 Generator 的使用,以及最終函數(shù)的執(zhí)行進(jìn)度都可以很好地控制,從而形成符合你設(shè)想的執(zhí)行順序。即便 Generator 函數(shù)相互嵌套,也能通過調(diào)用 next 方法來按照進(jìn)度一步步執(zhí)行。

(3)生成器原理

其實,在生成器內(nèi)部,如果遇到 yield 關(guān)鍵字,那么 V8 引擎將返回關(guān)鍵字后面的內(nèi)容給外部,并暫停該生成器函數(shù)的執(zhí)行。生成器暫停執(zhí)行后,外部的代碼便開始執(zhí)行,外部代碼如果想要恢復(fù)生成器的執(zhí)行,可以使用 result.next 方法。

那 V8 是怎么實現(xiàn)生成器函數(shù)的暫停執(zhí)行和恢復(fù)執(zhí)行的呢?

它用到的就是協(xié)程,協(xié)程是—種比線程更加輕量級的存在。我們可以把協(xié)程看成是跑在線程上的任務(wù),一個線程上可以存在多個協(xié)程,但是在線程上同時只能執(zhí)行一個協(xié)程。比如,當(dāng)前執(zhí)行的是 A 協(xié)程,要啟動 B 協(xié)程,那么 A 協(xié)程就需要將主線程的控制權(quán)交給 B 協(xié)程,這就體現(xiàn)在 A 協(xié)程暫停執(zhí)行,B 協(xié)程恢復(fù)執(zhí)行; 同樣,也可以從 B 協(xié)程中啟動 A 協(xié)程。通常,如果從 A 協(xié)程啟動 B 協(xié)程,我們就把 A 協(xié)程稱為 B 協(xié)程的父協(xié)程。

正如一個進(jìn)程可以擁有多個線程一樣,一個線程也可以擁有多個協(xié)程。每一時刻,該線程只能執(zhí)行其中某一個協(xié)程。最重要的是,協(xié)程不是被操作系統(tǒng)內(nèi)核所管理,而完全是由程序所控制(也就是在用戶態(tài)執(zhí)行)。這樣帶來的好處就是性能得到了很大的提升,不會像線程切換那樣消耗資源。

2. Generator 和 thunk 結(jié)合

下面先來了解一下什么是 thunk 函數(shù),以判斷數(shù)據(jù)類型為例:

let isString = (obj) => {
  return Object.prototype.toString.call(obj) === '[object String]';
};
let isFunction = (obj) => {
  return Object.prototype.toString.call(obj) === '[object Function]';
};
let isArray = (obj) => {
  return Object.prototype.toString.call(obj) === '[object Array]';
};
....

可以看到,這里出現(xiàn)了很多重復(fù)的判斷邏輯,平常在開發(fā)中類似的重復(fù)邏輯的場景也同樣會有很多。下面來進(jìn)行封裝:

let isType = (type) => {
  return (obj) => {
    return Object.prototype.toString.call(obj) === `[object ${type}]`;
  }
}

封裝之后就可以這樣使用,從而來減少重復(fù)的邏輯代碼:

let isString = isType('String');
let isArray = isType('Array');
isString("123");    // true
isArray([1,2,3]);   // true

相應(yīng)的 isString 和 isArray 是由 isType 方法生產(chǎn)出來的函數(shù),通過上面的方式來改造代碼,明顯簡潔了不少。像 isType 這樣的函數(shù)稱為 thunk 函數(shù),它的基本思路都是接收一定的參數(shù),會生產(chǎn)出定制化的函數(shù),最后使用定制化的函數(shù)去完成想要實現(xiàn)的功能。

這樣的函數(shù)在 JS 的編程過程中會遇到很多,抽象度比較高的 JS 代碼往往都會采用這樣的方式。那 Generator 和 thunk 函數(shù)的結(jié)合是否能帶來一定的便捷性呢?

下面以文件操作的代碼為例,看一下 Generator 和 thunk 的結(jié)合能夠?qū)Ξ惒讲僮鳟a(chǎn)生的效果:

const readFileThunk = (filename) => {
  return (callback) => {
    fs.readFile(filename, callback);
  }
}
const gen = function* () {
  const data1 = yield readFileThunk('1.txt')
  console.log(data1.toString())
  const data2 = yield readFileThunk('2.txt')
  console.log(data2.toString)
}
let g = gen();
g.next().value((err, data1) => {
  g.next(data1).value((err, data2) => {
    g.next(data2);
  })
})

readFileThunk 就是一個 thunk 函數(shù),上面的這種編程方式就讓 Generator 和異步操作關(guān)聯(lián)起來了。上面第三段代碼執(zhí)行起來嵌套的情況還算簡單,如果任務(wù)多起來,就會產(chǎn)生很多層的嵌套,可讀性不強,因此有必要把執(zhí)行的代碼進(jìn)行封裝優(yōu)化:

function run(gen){
  const next = (err, data) => {
    let res = gen.next(data);
    if(res.done) return;
    res.value(next);
  }
  next();
}
run(g);

可以看到, run 函數(shù)和上面的執(zhí)行效果其實是一樣的。代碼雖然只有幾行,但其包含了遞歸的過程,解決了多層嵌套的問題,并且完成了異步操作的一次性的執(zhí)行效果。這就是通過 thunk 函數(shù)完成異步操作的情況。

3. Generator 和 Promise 結(jié)合

其實 Promise 也可以和 Generator 配合來實現(xiàn)上面的效果。還是利用上面的輸出文件的例子,對代碼進(jìn)行改造,如下所示:

const readFilePromise = (filename) => {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, (err, data) => {
      if(err) {
        reject(err);
      }else {
        resolve(data);
      }
    })
  }).then(res => res);
}
// 這塊和上面 thunk 的方式一樣
const gen = function* () {
  const data1 = yield readFilePromise('1.txt')
  console.log(data1.toString())
  const data2 = yield readFilePromise('2.txt')
  console.log(data2.toString)
}
// 這里和上面 thunk 的方式一樣
function run(gen){
  const next = (err, data) => {
    let res = gen.next(data);
    if(res.done) return;
    res.value(next);
  }
  next();
}
run(g);

可以看到,thunk 函數(shù)的方式和通過 Promise 方式執(zhí)行效果本質(zhì)上是一樣的,只不過通過 Promise 的方式也可以配合 Generator 函數(shù)實現(xiàn)同樣的異步操作。

4. co 函數(shù)庫

co 函數(shù)庫用于處理 Generator 函數(shù)的自動執(zhí)行。核心原理其實就是通過和 thunk 函數(shù)以及 Promise 對象進(jìn)行配合,包裝成一個庫。它使用起來非常簡單,比如還是用上面那段代碼,第三段代碼就可以省略了,直接引用 co 函數(shù),包裝起來就可以使用了,代碼如下:

const co = require('co');
let g = gen();
co(g).then(res =>{
  console.log(res);
})

這段代碼比較簡單,幾行就完成了之前寫的遞歸的那些操作。那么為什么 co 函數(shù)庫可以自動執(zhí)行 Generator 函數(shù),它的處理原理如下:

  1. 因為 Generator 函數(shù)就是一個異步操作的容器,它需要一種自動執(zhí)行機制,co 函數(shù)接受 Generator 函數(shù)作為參數(shù),并最后返回一個 Promise 對象。
  2. 在返回的 Promise 對象里面,co 先檢查參數(shù) gen 是否為 Generator 函數(shù)。如果是,就執(zhí)行該函數(shù);如果不是就返回,并將 Promise 對象的狀態(tài)改為 resolved。
  3. co 將 Generator 函數(shù)的內(nèi)部指針對象的 next 方法,包裝成 onFulfilled 函數(shù)。這主要是為了能夠捕捉拋出的錯誤。
  4. 關(guān)鍵的是 next 函數(shù),它會反復(fù)調(diào)用自身。

五、Async/Await

1. async/await 的概念

ES7 新增了兩個關(guān)鍵字: async和await,代表異步JavaScript編程范式的遷移。它改進(jìn)了生成器的缺點,提供了在不阻塞主線程的情況下使用同步代碼實現(xiàn)異步訪問資源的能力。其實 async/await 是 Generator 的語法糖,它能實現(xiàn)的效果都能用then鏈來實現(xiàn),它是為優(yōu)化then鏈而開發(fā)出來的。

從字面上來看,async是“異步”的簡寫,await則為等待,所以 async 用來聲明異步函數(shù),這個關(guān)鍵字可以用在函數(shù)聲明、函數(shù)表達(dá)式、箭頭函數(shù)和方法上。因為異步函數(shù)主要針對不會馬上完成的任務(wù),所以自然需要一種暫停和恢復(fù)執(zhí)行的能力,使用await關(guān)鍵字可以暫停異步代碼的執(zhí)行,等待Promise解決。async 關(guān)鍵字可以讓函數(shù)具有異步特征,但總體上代碼仍然是同步求值的。

它們的用法很簡單,首先用 async 關(guān)鍵字聲明一個異步函數(shù):

async function httpRequest() {
}

然后就可以在這個函數(shù)內(nèi)部使用 await 關(guān)鍵字了:

async function httpRequest() {
  let res1 = await httpPromise(url1)
  console.log(res1)
}

這里,await關(guān)鍵字會接收一個期約并將其轉(zhuǎn)化為一個返回值或一個拋出的異常。通過情況下,我們不會使用await來接收一個保存期約的變量,更多的是把他放在一個會返回期約的函數(shù)調(diào)用面前,比如上述例子。這里的關(guān)鍵就是,await關(guān)鍵字并不會導(dǎo)致程序阻塞,代碼仍然是異步的,而await只是掩蓋了這個事實,這就意味著任何使用await的代碼本身都是異步的。

下面來看看async函數(shù)返回了什么:

async function testAsy(){
   return 'hello world';
}
let result = testAsy(); 
console.log(result)

圖片

可以看到,async 函數(shù)返回的是 Promise 對象。如果異步函數(shù)使用return關(guān)鍵字返回了值(如果沒有return則會返回undefined),這個值則會被 Promise.resolve() 包裝成 Promise 對象。異步函數(shù)始終返回Promise對象。

2. await 到底在等啥?

那await到底在等待什么呢?

一般我們認(rèn)為 await 是在等待一個 async 函數(shù)完成。不過按語法說明,await 等待的是一個表達(dá)式,這個表達(dá)式的結(jié)果是 Promise 對象或其它值。

因為 async 函數(shù)返回一個 Promise 對象,所以 await 可以用于等待一個 async 函數(shù)的返回值——這也可以說是 await 在等 async 函數(shù)。但要清楚,它等的實際是一個返回值。注意,await 不僅用于等 Promise 對象,它可以等任意表達(dá)式的結(jié)果。所以,await 后面實際是可以接普通函數(shù)調(diào)用或者直接量的。所以下面這個示例完全可以正確運行:

function getSomething() {
    return "something";
}
async function testAsync() {
    return Promise.resolve("hello async");
}
async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}
test(); // something hello async

await 表達(dá)式的運算結(jié)果取決于它等的是什么:

  • 如果它等到的不是一個 Promise 對象,那 await 表達(dá)式的運算結(jié)果就是它等到的內(nèi)容;
  • 如果它等到的是一個 Promise 對象,await 就就會阻塞后面的代碼,等著 Promise 對象 resolve,然后將得到的值作為 await 表達(dá)式的運算結(jié)果。

下面來看一個例子:

function testAsy(x){
   return new Promise(resolve=>{setTimeout(() => {
       resolve(x);
     }, 3000)
    }
   )
}
async function testAwt(){    
  let result =  await testAsy('hello world');
  console.log(result);    // 3秒鐘之后出現(xiàn)hello world
  console.log('cuger')   // 3秒鐘之后出現(xiàn)cug
}
testAwt();
console.log('cug')  //立即輸出cug

這就是 await 必須用在 async 函數(shù)中的原因。async 函數(shù)調(diào)用不會造成阻塞,它內(nèi)部所有的阻塞都被封裝在一個 Promise 對象中異步執(zhí)行。await暫停當(dāng)前async的執(zhí)行,所以'cug''最先輸出,hello world'和 cuger 是3秒鐘后同時出現(xiàn)的。

3. async/await的優(yōu)勢

單一的 Promise 鏈并不能凸顯 async/await 的優(yōu)勢。但是,如果處理流程比較復(fù)雜,那么整段代碼將充斥著 then,語義化不明顯,代碼不能很好地表示執(zhí)行流程,這時async/await的優(yōu)勢就能體現(xiàn)出來了。

假設(shè)一個業(yè)務(wù),分多個步驟完成,每個步驟都是異步的,而且依賴于上一個步驟的結(jié)果。首先用 setTimeout 來模擬異步操作:

/**
 * 傳入?yún)?shù) n,表示這個函數(shù)執(zhí)行的時間(毫秒)
 * 執(zhí)行的結(jié)果是 n + 200,這個值將用于下一步驟
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}
function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}
function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}
function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

現(xiàn)在用 Promise 方式來實現(xiàn)這三個步驟的處理:

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}
doIt();
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

輸出結(jié)果 result 是 step3() 的參數(shù) 700 + 200 = 900。doIt() 順序執(zhí)行了三個步驟,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 計算的結(jié)果一致。

如果用 async/await 來實現(xiàn)呢,會是這樣:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}
doIt();

結(jié)果和之前的 Promise 實現(xiàn)是一樣的,但是這個代碼看起來會清晰得多,幾乎和同步代碼一樣。

async/await對比Promise的優(yōu)勢就顯而易見了:

  • 代碼讀起來更加同步,Promise雖然擺脫了回調(diào)地獄,但是then的鏈?zhǔn)秸{(diào)?也會帶來額外的理解負(fù)擔(dān);
  • Promise傳遞中間值很麻煩,?async/await?乎是同步的寫法,?常優(yōu)雅;
  • 錯誤處理友好,async/await可以?成熟的try/catch,Promise的錯誤捕獲比較冗余;
  • 調(diào)試友好,Promise的調(diào)試很差,由于沒有代碼塊,不能在?個返回表達(dá)式的箭頭函數(shù)中設(shè)置斷點,如果在?個.then代碼塊中使?調(diào)試器的步進(jìn)(step-over)功能,調(diào)試器并不會進(jìn)?后續(xù)的.then代碼塊,因為調(diào)試器只能跟蹤同步代碼的每?步。

4. async/await 的異常處理

利用 async/await 的語法糖,可以像處理同步代碼的異常一樣,來處理異步代碼,這里還用上面的示例:

const exe = (flag) => () => new Promise((resolve, reject) => {
    console.log(flag);
    setTimeout(() => {
        flag ? resolve("yes") : reject("no");
    }, 1000);
});
const run = async () => {
 try {
  await exe(false)();
  await exe(true)();
 } catch (e) {
  console.log(e);
 }
}
run();

這里定義一個異步方法 run,由于 await 后面需要直接跟 Promise 對象,因此通過額外的一個方法調(diào)用符號 () 把原有的 exe 方法內(nèi)部的 Thunk 包裝拆掉,即執(zhí)行 exe(false)() 或 exe(true)() 返回的就是 Promise 對象。在 try 塊之后,使用 catch 來捕捉。運行代碼會得到這樣的輸出:

false
no

這個 false 就是 exe 方法對入?yún)⒌妮敵?,而這個 no 就是 setTimeout 方法 reject 的回調(diào)返回,它通過異常捕獲并最終在 catch 塊中輸出。就像我們所認(rèn)識的同步代碼一樣,第四行的 exe(true) 并未得到執(zhí)行。

責(zé)任編輯:武曉燕 來源: 前端充電寶
相關(guān)推薦

2022-10-31 09:00:24

Promise數(shù)組參數(shù)

2022-09-26 09:01:15

語言數(shù)據(jù)JavaScript

2012-02-21 13:55:45

JavaScript

2010-07-16 09:11:40

JavaScript內(nèi)存泄漏

2011-05-30 14:41:09

Javascript閉

2021-03-16 08:54:35

AQSAbstractQueJava

2011-07-04 10:39:57

Web

2009-06-22 15:34:00

Javascript

2009-06-18 10:23:03

Javascript 基本框架

2022-05-26 09:20:01

JavaScript原型原型鏈

2019-01-07 15:29:07

HadoopYarn架構(gòu)調(diào)度器

2021-07-20 15:20:02

FlatBuffers阿里云Java

2012-05-21 10:06:26

FrameworkCocoa

2017-07-02 18:04:53

塊加密算法AES算法

2017-10-10 14:36:07

前端Javascriptapply、call、

2016-12-27 09:10:29

JavaScript原型鏈繼承

2025-02-06 09:47:33

2022-09-29 09:19:04

線程池并發(fā)線程

2018-11-09 16:24:25

物聯(lián)網(wǎng)云計算云系統(tǒng)

2022-11-09 08:06:15

GreatSQLMGR模式
點贊
收藏

51CTO技術(shù)棧公眾號