日韩成人免费在线_国产成人一二_精品国产免费人成电影在线观..._日本一区二区三区久久久久久久久不

當前位置:首頁 > 科技  > 軟件

深入淺出JavaScript異步編程

來源: 責編: 時間:2023-12-04 17:25:48 242觀看
導(dǎo)讀瀏覽器中的 JavaScript 是典型的事件驅(qū)動型程序,即它們會等待用戶觸發(fā)后才真正的執(zhí)行,而基于的JavaScript的服務(wù)器通常要等待客戶端通過網(wǎng)絡(luò)發(fā)送請求,然后才能執(zhí)行。這種異步編程在JavaScript是很常見的,下面就來介紹幾個

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

本文將按照異步編程方式的出現(xiàn)時間來歸納整理:T3W28資訊網(wǎng)——每日最新資訊28at.com

圖片T3W28資訊網(wǎng)——每日最新資訊28at.com

一、什么是異步

下面先來看看同步和異步的概念:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

下面來看一個例子:T3W28資訊網(wǎng)——每日最新資訊28at.com

// 同步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é)果并打印。T3W28資訊網(wǎng)——每日最新資訊28at.com

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

那為什么單線程的JavaScript還能實現(xiàn)異步呢,其實也沒有什么魔法,只是把一些操作交給了其他線程處理,然后采用了事件循環(huán)的機制來處理返回結(jié)果。T3W28資訊網(wǎng)——每日最新資訊28at.com

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

在最基本的層面上,JavaScript的異步編程式通過回調(diào)實現(xiàn)的。回調(diào)的是函數(shù),可以傳給其他函數(shù),而其他函數(shù)會在滿足某個條件時調(diào)用這個函數(shù)。下面就來看看常見的不同形式的基于回調(diào)的異步編程。T3W28資訊網(wǎng)——每日最新資訊28at.com

1. 定時器

一種最簡單的異步操作就是在一定時間之后運行某些代碼。如下面代碼:T3W28資訊網(wǎng)——每日最新資訊28at.com

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ù)。T3W28資訊網(wǎng)——每日最新資訊28at.com

2. 事件監(jiān)聽

給目標 DOM 綁定一個監(jiān)聽函數(shù),用的最多的是 addEventListener:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

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

這里使用addEventListener注冊了回調(diào)函數(shù),這個方法的第一個參數(shù)是一個字符串,指定要注冊的事件類型,如果用戶點擊了指定的元素,瀏覽器就會調(diào)用回調(diào)函數(shù),并給他傳入一個對象,其中包含著事件的詳細信息。T3W28資訊網(wǎng)——每日最新資訊28at.com

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

JavaScript中另外一種常見的異步操作就是網(wǎng)絡(luò)請求:T3W28資訊網(wǎng)——每日最新資訊28at.com

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;  // 當請求成功時  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)。T3W28資訊網(wǎng)——每日最新資訊28at.com

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

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

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ù)。T3W28資訊網(wǎng)——每日最新資訊28at.com

三、Promise

1. Promise的概念

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

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

實際上,Promise就是一個容器,里面保存著某個未來才會結(jié)束的事件(通常是異步操作)的結(jié)果。從語法上說,Promise 是一個對象,它可以獲取異步操作的消息。Promise 提供了統(tǒng)一的 API,各種異步操作都可以用同樣的方法進行處理。T3W28資訊網(wǎng)——每日最新資訊28at.com

(1)Promise實例有三個狀態(tài):T3W28資訊網(wǎng)——每日最新資訊28at.com

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

(2)Promise實例有兩個過程:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

Promise的特點:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

下面來看一個例子:T3W28資訊網(wǎng)——每日最新資訊28at.com

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í)行器里,需要把目標異步任務(wù)給放進去。在 Promise 實例創(chuàng)建后,執(zhí)行器里的邏輯會立刻執(zhí)行,在執(zhí)行的過程中,根據(jù)異步返回的結(jié)果,決定如何使用 resolve 或 reject 來改變 Promise實例的狀態(tài)。T3W28資訊網(wǎng)——每日最新資訊28at.com

在這個例子里,當用 resolve 切換到了成功態(tài)后,Promise 的邏輯就會走到 then 中傳入的方法里去;用 reject 切換到失敗態(tài)后,Promise 的邏輯就會走到 catch 傳入的方法中。T3W28資訊網(wǎng)——每日最新資訊28at.com

這樣的邏輯,本質(zhì)上與回調(diào)函數(shù)中的成功回調(diào)和失敗回調(diào)沒有差異。但這種寫法大大地提高了代碼的質(zhì)量。當我們進行大量的異步鏈式調(diào)用時,回調(diào)地獄不復(fù)存在了。取而代之的是層級簡單、賞心悅目的 Promise 調(diào)用鏈:T3W28資訊網(wǎng)——每日最新資訊28at.com

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(進行中)、fulfilled(已成功)和rejected(已失敗)。T3W28資訊網(wǎng)——每日最新資訊28at.com

Promise構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù)的兩個參數(shù)分別是resolve和reject。T3W28資訊網(wǎng)——每日最新資訊28at.com

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

一般情況下,我們會用new Promise()來創(chuàng)建Promise對象。除此之外,還也可以使用promise.resolve和 promise.reject這兩個方法來創(chuàng)建:T3W28資訊網(wǎng)——每日最新資訊28at.com

(1)Promise.resolveT3W28資訊網(wǎng)——每日最新資訊28at.com

Promise.resolve(value)的返回值是一個promise對象,我們可以對返回值進行.then調(diào)用,如下代碼:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

resolve(11)會讓promise對象進入確定(resolve狀態(tài)),并將參數(shù)11傳遞給后面then中指定的onFulfilled 函數(shù);T3W28資訊網(wǎng)——每日最新資訊28at.com

(2)Promise.rejectT3W28資訊網(wǎng)——每日最新資訊28at.com

Promise.reject 的返回值也是一個promise對象,如下代碼:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

上面是以下代碼的簡單形式:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

下面來綜合看看resolve方法和reject方法:T3W28資訊網(wǎng)——每日最新資訊28at.com

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()方法,則會進入then的第二個函數(shù)內(nèi),會打印No thanks。T3W28資訊網(wǎng)——每日最新資訊28at.com

3. Promise的作用

在開發(fā)中可能會碰到這樣的需求:使用ajax發(fā)送A請求,成功后拿到數(shù)據(jù),需要把數(shù)據(jù)傳給B請求,那么需要這樣編寫代碼:T3W28資訊網(wǎng)——每日最新資訊28at.com

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é)其原因有兩點:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

既然原因分析出來了,那么問題的解決思路就很清晰了:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

這么說可能有點抽象,不過 Promise 解決了這兩個問題。接下來就看看 Promise 是怎么消滅嵌套調(diào)用和合并多個任務(wù)的錯誤處理的。T3W28資訊網(wǎng)——每日最新資訊28at.com

Promise出現(xiàn)之后,代碼可以這樣寫:T3W28資訊網(wǎng)——每日最新資訊28at.com

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ù)延遲綁定、返回值穿透、錯誤冒泡。T3W28資訊網(wǎng)——每日最新資訊28at.com

下面來看一段代碼:T3W28資訊網(wǎng)——每日最新資訊28at.com

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)整,如下:T3W28資訊網(wǎng)——每日最新資訊28at.com

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 后面可以依次完成鏈式調(diào)用。這便是返回值穿透的效果,這兩種技術(shù)一起作用便可以將深層的嵌套回調(diào)寫成下面的形式。T3W28資訊網(wǎng)——每日最新資訊28at.com

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)生了鏈式調(diào)用的效果。T3W28資訊網(wǎng)——每日最新資訊28at.com

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

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)鏈式調(diào)用,解決多層嵌套問題;實現(xiàn)錯誤冒泡后一站式處理,解決每次任務(wù)中判斷錯誤、增加代碼混亂度的問題。T3W28資訊網(wǎng)——每日最新資訊28at.com

4. Promise的方法

Promise常用的方法:then()、catch()、all()、race()、finally()、allSettled()、any()。T3W28資訊網(wǎng)——每日最新資訊28at.com

(1)then()

當Promise執(zhí)行的內(nèi)容符合成功條件時,調(diào)用resolve函數(shù),失敗就調(diào)用reject函數(shù)。那Promise創(chuàng)建完了,該如何調(diào)用呢?這時就該then出場了:T3W28資訊網(wǎng)——每日最新資訊28at.com

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ù)可以省略。T3W28資訊網(wǎng)——每日最新資訊28at.com

then方法返回的是一個新的Promise實例。因此可以采用鏈式寫法,即then方法后面再調(diào)用另一個then方法。當寫有順序的異步事件時,需要串行時,可以這樣寫:T3W28資訊網(wǎng)——每日最新資訊28at.com

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方法相當于then方法的第二個參數(shù),指向reject的回調(diào)函數(shù)。T3W28資訊網(wǎng)——每日最新資訊28at.com

不過catch方法還有一個作用,就是在執(zhí)行resolve回調(diào)函數(shù)時,如果出現(xiàn)錯誤,拋出異常,不會停止運行,而是進入catch方法中:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

(3)all()

all方法可以完成并行任務(wù), 它接收一個數(shù)組,數(shù)組的每一項都是一個promise對象。當數(shù)組中所有的promise的狀態(tài)都達到resolved時,all方法的狀態(tài)就會變成resolved,如果有一個狀態(tài)變成了rejected,那么all方法的狀態(tài)就會變成rejected:T3W28資訊網(wǎng)——每日最新資訊28at.com

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í)行時的值。T3W28資訊網(wǎng)——每日最新資訊28at.com

(4)race()

race方法和all一樣,接受的參數(shù)是一個每項都是promise的數(shù)組,但與all不同的是,當最先執(zhí)行完的事件執(zhí)行完之后,就直接返回該promise對象的值。T3W28資訊網(wǎng)——每日最新資訊28at.com

如果第一個promise對象狀態(tài)變成resolved,那自身的狀態(tài)變成了resolved;反之,第一個promise變成rejected,那自身狀態(tài)就會變成rejected。T3W28資訊網(wǎng)——每日最新資訊28at.com

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方法有什么實際作用呢?當需要執(zhí)行一個任務(wù),超過多長時間就不做了,就可以用這個方法來解決:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

(5)finally()

finally方法用于指定不管 Promise 對象最后狀態(tài)如何,都會執(zhí)行的操作。該方法是 ES2018 引入標準的。T3W28資訊網(wǎng)——每日最新資訊28at.com

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

上面代碼中,不管promise最后的狀態(tài)如何,在執(zhí)行完then或catch指定的回調(diào)函數(shù)以后,都會執(zhí)行finally方法指定的回調(diào)函數(shù)。T3W28資訊網(wǎng)——每日最新資訊28at.com

下面來看例子,服務(wù)器使用 Promise 處理請求,然后使用finally方法關(guān)掉服務(wù)器。T3W28資訊網(wǎng)——每日最新資訊28at.com

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é)果。T3W28資訊網(wǎng)——每日最新資訊28at.com

finally本質(zhì)上是then方法的特例:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

上面代碼中,如果不使用finally方法,同樣的語句需要為成功和失敗兩種情況各寫一次。有了finally方法,則只需要寫一次。T3W28資訊網(wǎng)——每日最新資訊28at.com

(6)allSettled()

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

下面使用 allSettled 實現(xiàn)的一段代碼:T3W28資訊網(wǎng)——每日最新資訊28at.com

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ù)組,記錄傳進來的參數(shù)中每個 Promise 的返回值,這就是和 all 方法不太一樣的地方。你也可以根據(jù) all 方法提供的業(yè)務(wù)場景的代碼進行改造,其實也能知道多個請求發(fā)出去之后,Promise 最后返回的是每個參數(shù)的最終狀態(tài)。T3W28資訊網(wǎng)——每日最新資訊28at.com

(7)any()

any 方法返回一個 Promise,只要參數(shù) Promise 實例有一個變成 fullfilled 狀態(tài),最后 any 返回的實例就會變成 fullfilled 狀態(tài);如果所有參數(shù) Promise 實例都變成 rejected 狀態(tài),包裝實例就會變成 rejected 狀態(tài)。T3W28資訊網(wǎng)——每日最新資訊28at.com

下面對上面 allSettled 這段代碼進行改造,來看下改造完的代碼和執(zhí)行結(jié)果:T3W28資訊網(wǎng)——每日最新資訊28at.com

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。T3W28資訊網(wǎng)——每日最新資訊28at.com

5. Promise的異常處理

錯誤處理是所有編程范型都必須要考慮的問題,在使用 JavaScript 進行異步編程時,也不例外。如果我們不做特殊處理,會怎樣呢?來看下面的代碼,先定義一個必定會失敗的方法T3W28資訊網(wǎng)——每日最新資訊28at.com

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

調(diào)用:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

可以看到打印出了 1 和 2,并在 1 秒后,獲得一個“Uncaught Error”的錯誤打印,注意觀察這個錯誤的堆棧:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

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

Promise 除了支持 resolve 回調(diào)以外,還支持 reject 回調(diào),前者用于表示異步調(diào)用順利結(jié)束,而后者則表示有異常發(fā)生,中斷調(diào)用鏈并將異常拋出:T3W28資訊網(wǎng)——每日最新資訊28at.com

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ù),進一步傳遞給調(diào)用鏈:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

上面的調(diào)用鏈,在執(zhí)行的時候,第二行就傳入了參數(shù) false,它就已經(jīng)失敗了,異常拋出了,因此第三行的 exe 實際沒有得到執(zhí)行,執(zhí)行結(jié)果如下:T3W28資訊網(wǎng)——每日最新資訊28at.com

falseUncaught (in promise) no

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

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

這種方式下,異常信息被捕獲并打印,而調(diào)用鏈的下一步,也就是第四行的 exe(true) 可以繼續(xù)被執(zhí)行。將看到這樣的輸出:T3W28資訊網(wǎng)——每日最新資訊28at.com

falsenotrue

6. Promise的實現(xiàn)

這一部分就來簡單實現(xiàn)一下Promise及其常用的方法。T3W28資訊網(wǎng)——每日最新資訊28at.com

(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)改變后再進行改變    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)用。T3W28資訊網(wǎng)——每日最新資訊28at.com

那么,怎么保證后一個 then 里的方法在前一個 then(可能是異步)結(jié)束之后再執(zhí)行呢?T3W28資訊網(wǎng)——每日最新資訊28at.com

可以將傳給 then 的函數(shù)和新 promise 的 resolve 一起 push 到前一個 promise 的 callbacks 數(shù)組中,達到承前啟后的效果:T3W28資訊網(wǎng)——每日最新資訊28at.com

  • 承前:當前一個 promise 完成后,調(diào)用其 resolve 變更狀態(tài),在這個 resolve 里會依次調(diào)用 callbacks 里的回調(diào),這樣就執(zhí)行了 then 里的方法了
  • 啟后:上一步中,當 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;      }    })   }

注意:T3W28資訊網(wǎng)——每日最新資訊28at.com

  • 連續(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ù)邏輯, 每當一個 Promise 的狀態(tài)變?yōu)?fulfilled 之后就保存該實例返回的數(shù)據(jù), 然后將計數(shù)減一, 當計數(shù)器變?yōu)?nbsp;0 時, 代表數(shù)組中所有 Promise 實例都執(zhí)行完畢.T3W28資訊網(wǎng)——每日最新資訊28at.com

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ù)中即可:T3W28資訊網(wǎng)——每日最新資訊28at.com

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ù)。先來看一個例子:T3W28資訊網(wǎng)——每日最新資訊28at.com

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é)果如下:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

Generator 中配合使用 yield 關(guān)鍵詞可以控制函數(shù)執(zhí)行的順序,每當執(zhí)行一次 next 方法,Generator 函數(shù)會執(zhí)行到下一個存在 yield 關(guān)鍵詞的位置。T3W28資訊網(wǎng)——每日最新資訊28at.com

總結(jié),Generator 的執(zhí)行的關(guān)鍵點如下:T3W28資訊網(wǎng)——每日最新資訊28at.com

  • 調(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í)行進度。T3W28資訊網(wǎng)——每日最新資訊28at.com

下面來看看多個 Generator 配合 yield 使用的情況:T3W28資訊網(wǎng)——每日最新資訊28at.com

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é)果如下:T3W28資訊網(wǎng)——每日最新資訊28at.com

{ 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í)行進度。這樣對于 Generator 的使用,以及最終函數(shù)的執(zhí)行進度都可以很好地控制,從而形成符合你設(shè)想的執(zhí)行順序。即便 Generator 函數(shù)相互嵌套,也能通過調(diào)用 next 方法來按照進度一步步執(zhí)行。T3W28資訊網(wǎng)——每日最新資訊28at.com

(3)生成器原理

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

那 V8 是怎么實現(xiàn)生成器函數(shù)的暫停執(zhí)行和恢復(fù)執(zhí)行的呢?T3W28資訊網(wǎng)——每日最新資訊28at.com

它用到的就是協(xié)程,協(xié)程是—種比線程更加輕量級的存在。我們可以把協(xié)程看成是跑在線程上的任務(wù),一個線程上可以存在多個協(xié)程,但是在線程上同時只能執(zhí)行一個協(xié)程。比如,當前執(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é)程。T3W28資訊網(wǎng)——每日最新資訊28at.com

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

2. Generator 和 thunk 結(jié)合

下面先來了解一下什么是 thunk 函數(shù),以判斷數(shù)據(jù)類型為例:T3W28資訊網(wǎng)——每日最新資訊28at.com

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ù)邏輯的場景也同樣會有很多。下面來進行封裝:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

封裝之后就可以這樣使用,從而來減少重復(fù)的邏輯代碼:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

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

這樣的函數(shù)在 JS 的編程過程中會遇到很多,抽象度比較高的 JS 代碼往往都會采用這樣的方式。那 Generator 和 thunk 函數(shù)的結(jié)合是否能帶來一定的便捷性呢?T3W28資訊網(wǎng)——每日最新資訊28at.com

下面以文件操作的代碼為例,看一下 Generator 和 thunk 的結(jié)合能夠?qū)Ξ惒讲僮鳟a(chǎn)生的效果:T3W28資訊網(wǎng)——每日最新資訊28at.com

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í)行的代碼進行封裝優(yōu)化:T3W28資訊網(wǎng)——每日最新資訊28at.com

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ù)完成異步操作的情況。T3W28資訊網(wǎng)——每日最新資訊28at.com

3. Generator 和 Promise 結(jié)合

其實 Promise 也可以和 Generator 配合來實現(xiàn)上面的效果。還是利用上面的輸出文件的例子,對代碼進行改造,如下所示:T3W28資訊網(wǎng)——每日最新資訊28at.com

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)同樣的異步操作。T3W28資訊網(wǎng)——每日最新資訊28at.com

4. co 函數(shù)庫

co 函數(shù)庫用于處理 Generator 函數(shù)的自動執(zhí)行。核心原理其實就是通過和 thunk 函數(shù)以及 Promise 對象進行配合,包裝成一個庫。它使用起來非常簡單,比如還是用上面那段代碼,第三段代碼就可以省略了,直接引用 co 函數(shù),包裝起來就可以使用了,代碼如下:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

這段代碼比較簡單,幾行就完成了之前寫的遞歸的那些操作。那么為什么 co 函數(shù)庫可以自動執(zhí)行 Generator 函數(shù),它的處理原理如下:T3W28資訊網(wǎng)——每日最新資訊28at.com

  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編程范式的遷移。它改進了生成器的缺點,提供了在不阻塞主線程的情況下使用同步代碼實現(xiàn)異步訪問資源的能力。其實 async/await 是 Generator 的語法糖,它能實現(xiàn)的效果都能用then鏈來實現(xiàn),它是為優(yōu)化then鏈而開發(fā)出來的。T3W28資訊網(wǎng)——每日最新資訊28at.com

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

它們的用法很簡單,首先用 async 關(guān)鍵字聲明一個異步函數(shù):T3W28資訊網(wǎng)——每日最新資訊28at.com

async function httpRequest() {}

然后就可以在這個函數(shù)內(nèi)部使用 await 關(guān)鍵字了:T3W28資訊網(wǎng)——每日最新資訊28at.com

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的代碼本身都是異步的。T3W28資訊網(wǎng)——每日最新資訊28at.com

下面來看看async函數(shù)返回了什么:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

圖片T3W28資訊網(wǎng)——每日最新資訊28at.com

可以看到,async 函數(shù)返回的是 Promise 對象。如果異步函數(shù)使用return關(guān)鍵字返回了值(如果沒有return則會返回undefined),這個值則會被 Promise.resolve() 包裝成 Promise 對象。異步函數(shù)始終返回Promise對象。T3W28資訊網(wǎng)——每日最新資訊28at.com

2. await 到底在等啥?

那await到底在等待什么呢?T3W28資訊網(wǎng)——每日最新資訊28at.com

一般我們認為 await 是在等待一個 async 函數(shù)完成。不過按語法說明,await 等待的是一個表達式,這個表達式的結(jié)果是 Promise 對象或其它值。T3W28資訊網(wǎng)——每日最新資訊28at.com

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

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 表達式的運算結(jié)果取決于它等的是什么:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

下面來看一個例子:T3W28資訊網(wǎng)——每日最新資訊28at.com

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暫停當前async的執(zhí)行,所以'cug''最先輸出,hello world'和 cuger 是3秒鐘后同時出現(xiàn)的。T3W28資訊網(wǎng)——每日最新資訊28at.com

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

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

假設(shè)一個業(yè)務(wù),分多個步驟完成,每個步驟都是異步的,而且依賴于上一個步驟的結(jié)果。首先用 setTimeout 來模擬異步操作:T3W28資訊網(wǎng)——每日最新資訊28at.com

/** * 傳入?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)這三個步驟的處理:T3W28資訊網(wǎng)——每日最新資訊28at.com

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é)果一致。T3W28資訊網(wǎng)——每日最新資訊28at.com

如果用 async/await 來實現(xiàn)呢,會是這樣:T3W28資訊網(wǎng)——每日最新資訊28at.com

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)是一樣的,但是這個代碼看起來會清晰得多,幾乎和同步代碼一樣。T3W28資訊網(wǎng)——每日最新資訊28at.com

async/await對比Promise的優(yōu)勢就顯而易見了:T3W28資訊網(wǎng)——每日最新資訊28at.com

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

4. async/await 的異常處理

利用 async/await 的語法糖,可以像處理同步代碼的異常一樣,來處理異步代碼,這里還用上面的示例:T3W28資訊網(wǎng)——每日最新資訊28at.com

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 來捕捉。運行代碼會得到這樣的輸出:T3W28資訊網(wǎng)——每日最新資訊28at.com

falseno

這個 false 就是 exe 方法對入?yún)⒌妮敵觯@個 no 就是 setTimeout 方法 reject 的回調(diào)返回,它通過異常捕獲并最終在 catch 塊中輸出。就像我們所認識的同步代碼一樣,第四行的 exe(true) 并未得到執(zhí)行。T3W28資訊網(wǎng)——每日最新資訊28at.com

本文鏈接:http://www.www897cc.com/showinfo-26-37662-0.html深入淺出JavaScript異步編程

聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com

上一篇: 解鎖 C++ 并發(fā)編程的鑰匙:探索 Atomic 變量

下一篇: 四個解決特定的任務(wù)的Pandas高效代碼

標簽:
  • 熱門焦點
Top 主站蜘蛛池模板: 四会市| 清水河县| 二手房| 郧西县| 内乡县| 抚远县| 七台河市| 盐亭县| 丹巴县| 蓬莱市| 义马市| 邵武市| 沧源| 白朗县| 新源县| 乐业县| 乌拉特后旗| 洛阳市| 陕西省| 平陆县| 茌平县| 通江县| 琼中| 周宁县| 天长市| 桂阳县| 萨嘎县| 柳江县| 湟中县| 漾濞| 宝兴县| 溧阳市| 武宁县| 斗六市| 且末县| 淮阳县| 留坝县| 潮安县| 逊克县| 乐陵市| 海城市|