自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

深入淺出JavaScript異步編程

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

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

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

圖片

一、什么是異步

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

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

下面來看一個(gè)例子:

// 同步
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ù)時(shí)會(huì)等待得到結(jié)果之后再執(zhí)行后面的代碼。而調(diào)用 asyncAdd(1, 2) 時(shí)則會(huì)在得到結(jié)果之前繼續(xù)執(zhí)行,直到 1 秒后得到結(jié)果并打印。

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

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

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

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

1. 定時(shí)器

一種最簡(jiǎn)單的異步操作就是在一定時(shí)間之后運(yùn)行某些代碼。如下面代碼:

setTimeout(asyncAdd(1, 2), 8000)

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

2. 事件監(jiān)聽

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

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

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

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

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

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

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

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

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

Node.js服務(wù)端JavaScript環(huán)境底層就是異步的,定義了很多使用回調(diào)和事件的API。例如讀取文件默認(rèn)的API就是異步的,它會(huì)在讀取文件內(nèi)容之后調(diào)用一個(gè)回調(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()方法以接收兩個(gè)參數(shù)的回調(diào)作為最后一個(gè)參數(shù)。它會(huì)異步讀取指定文件,如果讀取成功就會(huì)將第二個(gè)參數(shù)傳遞給回調(diào)的第二個(gè)參數(shù),如果發(fā)生錯(cuò)誤,就會(huì)將錯(cuò)誤傳遞給回調(diào)的第一個(gè)參數(shù)。

三、Promise

1. Promise的概念

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

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

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

(1)Promise實(shí)例有三個(gè)狀態(tài):

  • pending 狀態(tài):表示進(jìn)行中。Promise 實(shí)例創(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實(shí)例有兩個(gè)過程:

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

Promise的特點(diǎn):

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

下面來看一個(gè)例子:

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

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

這樣的邏輯,本質(zhì)上與回調(diào)函數(shù)中的成功回調(diào)和失敗回調(diào)沒有差異。但這種寫法大大地提高了代碼的質(zhì)量。當(dāng)我們進(jìn)行大量的異步鏈?zhǔn)秸{(diào)用時(shí),回調(diào)地獄不復(fù)存在了。取而代之的是層級(jí)簡(jiǎn)單、賞心悅目的 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對(duì)象代表一個(gè)異步操作,有三種狀態(tài):pending(進(jìn)行中)、fulfilled(已成功)和rejected(已失?。?。

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

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

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

(1)Promise.resolve

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

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

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

(2)Promise.reject

Promise.reject 的返回值也是一個(gè)promise對(duì)象,如下代碼:

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

上面是以下代碼的簡(jiǎn)單形式:

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

下面來綜合看看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方法傳遞一個(gè)參數(shù),返回一個(gè)promise對(duì)象,如果為true,那么調(diào)用Promise對(duì)象中的resolve()方法,并且把其中的參數(shù)傳遞給后面的then第一個(gè)函數(shù)內(nèi),因此打印出 “hello world”, 如果為false,會(huì)調(diào)用promise對(duì)象中的reject()方法,則會(huì)進(jìn)入then的第二個(gè)函數(shù)內(nèi),會(huì)打印No thanks。

3. Promise的作用

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

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ǎn):

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

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

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

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

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ù)延遲綁定、返回值穿透、錯(cuò)誤冒泡。

下面來看一段代碼:

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ù)延遲綁定。接下來針對(duì)上面的代碼做一下調(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ā)體驗(yàn)更好,兩種技術(shù)結(jié)合產(chǎn)生了鏈?zhǔn)秸{(diào)用的效果。

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

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

4. Promise的方法

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

(1)then()

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

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

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

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

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對(duì)象的catch方法相當(dāng)于then方法的第二個(gè)參數(shù),指向reject的回調(diào)函數(shù)。

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

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

(3)all()

all方法可以完成并行任務(wù), 它接收一個(gè)數(shù)組,數(shù)組的每一項(xiàng)都是一個(gè)promise對(duì)象。當(dāng)數(shù)組中所有的promise的狀態(tài)都達(dá)到resolved時(shí),all方法的狀態(tài)就會(huì)變成resolved,如果有一個(gè)狀態(tài)變成了rejected,那么all方法的狀態(tài)就會(huì)變成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方法時(shí)的結(jié)果成功的時(shí)候是回調(diào)函數(shù)的參數(shù)也是一個(gè)數(shù)組,這個(gè)數(shù)組按順序保存著每一個(gè)promise對(duì)象resolve執(zhí)行時(shí)的值。

(4)race()

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

如果第一個(gè)promise對(duì)象狀態(tài)變成resolved,那自身的狀態(tài)變成了resolved;反之,第一個(gè)promise變成rejected,那自身狀態(tài)就會(huì)變成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方法有什么實(shí)際作用呢?當(dāng)需要執(zhí)行一個(gè)任務(wù),超過多長(zhǎng)時(shí)間就不做了,就可以用這個(gè)方法來解決:

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

(5)finally()

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

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

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

下面來看例子,服務(wù)器使用 Promise 處理請(qǐng)求,然后使用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ù)接受一個(gè) Promise 的數(shù)組,返回一個(gè)新的 Promise。唯一的不同在于,執(zhí)行完之后不會(huì)失敗,也就是說當(dāng) Promise.allSettled 全部處理完成后,我們可以拿到每個(gè) Promise 的狀態(tài),而不管其是否處理成功。

下面使用 allSettled 實(shí)現(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 最后返回的是一個(gè)數(shù)組,記錄傳進(jìn)來的參數(shù)中每個(gè) Promise 的返回值,這就是和 all 方法不太一樣的地方。你也可以根據(jù) all 方法提供的業(yè)務(wù)場(chǎng)景的代碼進(jìn)行改造,其實(shí)也能知道多個(gè)請(qǐng)求發(fā)出去之后,Promise 最后返回的是每個(gè)參數(shù)的最終狀態(tài)。

(7)any()

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

下面對(duì)上面 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

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

5. Promise的異常處理

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

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 秒后,獲得一個(gè)“Uncaught Error”的錯(cuò)誤打印,注意觀察這個(gè)錯(cuò)誤的堆棧:

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

可以看到,其中的 setTimeout (async) 這樣的字樣,表示著這是一個(gè)異步調(diào)用拋出的堆棧。但是,captured”這樣的字樣也并未打印,因?yàn)槟阜椒?fail() 本身的原始順序執(zhí)行并沒有失敗,這個(gè)異常的拋出是在回調(diào)行為里發(fā)生的。 從上面的例子可以看出,對(duì)于異步編程來說,我們需要使用一種更好的機(jī)制來捕獲并處理可能發(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ā)生錯(cuò)誤。在錯(cuò)誤發(fā)生的時(shí)候,no 字符串會(huì)被傳遞給 reject 函數(shù),進(jìn)一步傳遞給調(diào)用鏈:

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

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

false
Uncaught (in promise) no

這就說明,通過這種方式,調(diào)用鏈被中斷了,下一個(gè)正常邏輯 exe(true) 沒有被執(zhí)行。 但是,有時(shí)候需要捕獲錯(cuò)誤,而繼續(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的實(shí)現(xiàn)

這一部分就來簡(jiǎn)單實(shí)現(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)改變必須等待前一個(gè)狀態(tài)改變后再進(jìn)行改變
    if (value instanceof MyPromise) {
      return value.then(resolve, reject);
    }

    // 保證代碼的執(zhí)行順序?yàn)楸据喪录h(huán)的末尾
    setTimeout(() => {
      // 只有狀態(tài)為 pending 時(shí)才能轉(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í)行順序?yàn)楸据喪录h(huán)的末尾
    setTimeout(() => {
      // 只有狀態(tài)為 pending 時(shí)才能轉(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);
  }

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

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

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

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

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

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

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

(2)Promise.then

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

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

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

  • 承前:當(dāng)前一個(gè) promise 完成后,調(diào)用其 resolve 變更狀態(tài),在這個(gè) resolve 里會(huì)依次調(diào)用 callbacks 里的回調(diào),這樣就執(zhí)行了 then 里的方法了
  • 啟后:上一步中,當(dāng) then 里的方法執(zhí)行完成后,返回一個(gè)結(jié)果,如果這個(gè)結(jié)果是個(gè)簡(jiǎn)單的值,就直接調(diào)用新 promise 的 resolve,讓其狀態(tài)變更,這又會(huì)依次調(diào)用新 promise 的 callbacks 數(shù)組里的方法,循環(huán)往復(fù)。。如果返回的結(jié)果是個(gè) promise,則需要等它完成之后再觸發(fā)新 promise 的 resolve,所以可以在其結(jié)果的 then 里調(diào)用新 promise 的 resolve
then(onFulfilled, onReject){
    // 保存前一個(gè)promise的this
    const self = this; 
    return new MyPromise((resolve, reject) => {
      // 封裝前一個(gè)promise成功時(shí)執(zhí)行的函數(shù)
      let fulfilled = () => {
        try{
          const result = onFulfilled(self.value); // 承前
          return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); //啟后
        }catch(err){
          reject(err)
        }
      }
      // 封裝前一個(gè)promise失敗時(shí)執(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ù)多個(gè) then 里的回調(diào)方法是同步注冊(cè)的,但注冊(cè)到了不同的 callbacks 數(shù)組中,因?yàn)槊看?nbsp;then 都返回新的 promise 實(shí)例(參考上面的例子和圖)
  • 注冊(cè)完成后開始執(zhí)行構(gòu)造函數(shù)中的異步事件,異步完成之后依次調(diào)用 callbacks 數(shù)組中提前注冊(cè)的回調(diào)

(3)Promise.all

該方法的參數(shù)是 Promise 的實(shí)例數(shù)組, 然后注冊(cè)一個(gè) then 方法。 待數(shù)組中的 Promise 實(shí)例的狀態(tài)都轉(zhuǎn)為 fulfilled 之后則執(zhí)行 then 方法.,這里主要就是一個(gè)計(jì)數(shù)邏輯, 每當(dāng)一個(gè) Promise 的狀態(tài)變?yōu)?fulfilled 之后就保存該實(shí)例返回的數(shù)據(jù), 然后將計(jì)數(shù)減一, 當(dāng)計(jì)數(shù)器變?yōu)?nbsp;0 時(shí), 代表數(shù)組中所有 Promise 實(shí)例都執(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實(shí)例
              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í)例數(shù)組, 然后其 then 注冊(cè)的回調(diào)方法是數(shù)組中的某一個(gè) Promise 的狀態(tài)變?yōu)?fulfilled 的時(shí)候就執(zhí)行. 因?yàn)?Promise 的狀態(tài)只能改變一次, 那么我們只需要把 Promise.race 中產(chǎn)生的 Promise 對(duì)象的 resolve 方法, 注入到數(shù)組中的每一個(gè) Promise 實(shí)例中的回調(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 是一個(gè)帶星號(hào)的函數(shù)(它并不是真正的函數(shù)),可以配合 yield 關(guān)鍵字來暫?;蛘邎?zhí)行函數(shù)。先來看一個(gè)例子:

function* gen() {
  console.log("enter");
  let a = yield 1;
  let b = yield (function () {return 2})();
  return 3;
}
var g = gen()           // 阻塞,不會(huì)執(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ù)會(huì)執(zhí)行到下一個(gè)存在 yield 關(guān)鍵詞的位置。

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

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

(2)yield

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

下面來看看多個(gè) 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)度。這樣對(duì)于 Generator 的使用,以及最終函數(shù)的執(zhí)行進(jìn)度都可以很好地控制,從而形成符合你設(shè)想的執(zhí)行順序。即便 Generator 函數(shù)相互嵌套,也能通過調(diào)用 next 方法來按照進(jìn)度一步步執(zhí)行。

(3)生成器原理

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

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

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

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

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ù)邏輯的場(chǎng)景也同樣會(huì)有很多。下面來進(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ù),通過上面的方式來改造代碼,明顯簡(jiǎn)潔了不少。像 isType 這樣的函數(shù)稱為 thunk 函數(shù),它的基本思路都是接收一定的參數(shù),會(huì)生產(chǎn)出定制化的函數(shù),最后使用定制化的函數(shù)去完成想要實(shí)現(xiàn)的功能。

這樣的函數(shù)在 JS 的編程過程中會(huì)遇到很多,抽象度比較高的 JS 代碼往往都會(huì)采用這樣的方式。那 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 就是一個(gè) thunk 函數(shù),上面的這種編程方式就讓 Generator 和異步操作關(guān)聯(lián)起來了。上面第三段代碼執(zhí)行起來嵌套的情況還算簡(jiǎn)單,如果任務(wù)多起來,就會(huì)產(chǎn)生很多層的嵌套,可讀性不強(qiáng),因此有必要把執(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í)行效果其實(shí)是一樣的。代碼雖然只有幾行,但其包含了遞歸的過程,解決了多層嵌套的問題,并且完成了異步操作的一次性的執(zhí)行效果。這就是通過 thunk 函數(shù)完成異步操作的情況。

3. Generator 和 Promise 結(jié)合

其實(shí) Promise 也可以和 Generator 配合來實(shí)現(xiàn)上面的效果。還是利用上面的輸出文件的例子,對(duì)代碼進(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ù)實(shí)現(xiàn)同樣的異步操作。

4. co 函數(shù)庫

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

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

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

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

五、Async/Await

1. async/await 的概念

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

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

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

async function httpRequest() {
}

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

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

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

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

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

圖片

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

2. await 到底在等啥?

那await到底在等待什么呢?

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

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

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á)式的運(yùn)算結(jié)果取決于它等的是什么:

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

下面來看一個(gè)例子:

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)用不會(huì)造成阻塞,它內(nèi)部所有的阻塞都被封裝在一個(gè) Promise 對(duì)象中異步執(zhí)行。await暫停當(dāng)前async的執(zhí)行,所以'cug''最先輸出,hello world'和 cuger 是3秒鐘后同時(shí)出現(xiàn)的。

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

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

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

/**
 * 傳入?yún)?shù) n,表示這個(gè)函數(shù)執(zhí)行的時(shí)間(毫秒)
 * 執(zhí)行的結(jié)果是 n + 200,這個(gè)值將用于下一步驟
 */
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 方式來實(shí)現(xiàn)這三個(gè)步驟的處理:

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í)行了三個(gè)步驟,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 計(jì)算的結(jié)果一致。

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

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 實(shí)現(xiàn)是一樣的,但是這個(gè)代碼看起來會(huì)清晰得多,幾乎和同步代碼一樣。

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

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

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

false
no

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

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

2022-10-31 09:00:24

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

2022-09-26 09:01:15

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

2010-07-16 09:11:40

JavaScript內(nèi)存泄漏

2012-02-21 13:55:45

JavaScript

2011-07-04 10:39:57

Web

2011-05-30 14:41:09

Javascript閉

2021-03-16 08:54:35

AQSAbstractQueJava

2009-06-18 10:23:03

Javascript 基本框架

2022-05-26 09:20:01

JavaScript原型原型鏈

2009-06-22 15:34:00

Javascript

2025-02-06 09:47:33

2017-07-02 18:04:53

塊加密算法AES算法

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

2022-09-29 09:19:04

線程池并發(fā)線程

2016-12-27 09:10:29

JavaScript原型鏈繼承

2017-10-10 14:36:07

前端Javascriptapply、call、

2009-11-17 17:31:58

Oracle COMM

2021-07-19 11:54:15

MySQL優(yōu)先隊(duì)列
點(diǎn)贊
收藏

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