JS非同步程式設計——深入理解async/await

saku發表於2018-05-05

在做專案的時候,經常會碰到關於非同步的問題,遇到多個非同步請求,又要控制其順序,該怎麼辦?涉及多個回撥形成回撥地獄又該如何處理?ES2017 標準引入了 async 函式,使得非同步操作變得更加方便。本文主要從async/await的基本用法、平行任務、注意事項幾個方面來介紹。

0.傳統js的非同步程式設計方法

ES6 誕生以前,非同步程式設計的方法,大概有下面四種。

  • 回撥函式
  • 事件監聽
  • 釋出/訂閱
  • promise物件

1.基本用法

首先我們通過一個例子,分別使用async/await與傳統非同步方法進行編寫,進而對async/await寫法有個初步的瞭解。

先定義一個 Fetch 方法用於獲取資訊:

function fetchUser(){
    return new Promise((resolve, reject) => {
        fetch('https://whuzxq.com/userList/4356')
        .then((data) => {
            resolve(data.json());
        }, (error) => {
            reject(error);
        })
    });
}
複製程式碼

Promise方式

function getuserByPromise(){
    fetchUser()
        .then((data) => {
            console.log(data);
        }, (error) => {
            console.log(error);
        })

}
複製程式碼

Promise的方式雖然解決了回撥地獄,但是整段程式碼充滿then,語義化不明顯,程式碼流程不能很好的表示執行流程。

async 方式

/**
 * async 方式
 */
 async function getUserByAsync(){
     let user = await fetchUser();
     return user;
 }
getUserByAsync()
.then(v => console.log(v));
複製程式碼

async 函式完美的解決了上面兩種方式的問題。同時 async 函式自帶執行器,執行的時候無需手動載入。

通過以上兩個例子,應該對async的使用有了一個初步的認識,下文將詳細列出async/await相關的重要知識點。

async

1.async函式返回一個promise物件。

2.async函式內部return語句返回的值,會成為then方法回撥函式的引數。

async function f() {
  return 'hello world';
}

f().then(v => console.log(v))
複製程式碼

3.async函式返回的 Promise 物件,必須等到內部所有await命令後面的 Promise 物件執行完,才會發生狀態改變,除非遇到return語句或者丟擲錯誤。也就是說,只有async函式內部的非同步操作執行完,才會執行then方法指定的回撥函式。

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
複製程式碼

上面程式碼中,函式getTitle內部有三個操作:抓取網頁、取出文字、匹配頁面標題。只有這三個操作全部完成,才會執行then方法裡面的console.log。

await命令

需要理解以下要點: 1.正常情況下,await命令後面是一個 Promise 物件。如果不是,會被轉成一個立即resolve的 Promise 物件。

async function f() {
  return await 123;
}

f().then(v => console.log(v))
// 123
複製程式碼

2.只要一個await語句後面的 Promise 變為reject,那麼整個async函式都會中斷執行。

async function f() {
  await Promise.reject('出錯了');
  await Promise.resolve('hello world'); // 不會執行
}
複製程式碼

有時,我們希望即使前一個非同步操作失敗,也不要中斷後面的非同步操作。這時可以將第一個await放在try...catch結構裡面,這樣不管這個非同步操作是否成功,第二個await都會執行。

async function f() {
  try {
    await Promise.reject('出錯了');
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// hello world
複製程式碼

另一種方法是await後面的 Promise 物件再跟一個catch方法,處理前面可能出現的錯誤。

async function f() {
  await Promise.reject('出錯了')
    .catch(e => console.log(e));
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// 出錯了
// hello world
複製程式碼

注意事項

1.前面已經說過,await命令後面的Promise物件,執行結果可能是rejected,所以最好把await命令放在try...catch程式碼塊中。

2.await命令只能用在async函式之中,如果用在普通函式,就會報錯。

2.等待平行任務

進行 JavaScript 非同步程式設計時,大家經常需要逐一編寫多個複雜語句的程式碼,並都在呼叫語句前標註了 await。由於大多數情況下,一個語句並不依賴於前一個語句,但是你仍不得不等前一個語句完成,這會導致效能問題。因此,多個await命令後面的非同步操作,如果不存在繼發關係,最好讓它們同時觸發。

let foo = await getFoo();
let bar = await getBar();
複製程式碼

上面程式碼中,getFoo和getBar是兩個獨立的非同步操作(即互不依賴),被寫成繼發關係。這樣比較耗時,因為只有getFoo完成以後,才會執行getBar,完全可以讓它們同時觸發。

// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

複製程式碼

3.例項

例項一:假定某個 DOM 元素上面,部署了一系列的動畫,前一個動畫結束,才能開始後一個。如果當中有一個動畫出錯,就不再往下執行,返回上一個成功執行的動畫的返回值。

async function chainAnimationsAsync(elem, animations) {
  let ret = null;//變數ret用來儲存上一個動畫的返回值
  try {
    for(let anim of animations) {
      ret = await anim(elem);
    }
  } catch(e) {
    /* 忽略錯誤,繼續執行 */
  }
  return ret;
}
複製程式碼

例項二:實際開發中,經常遇到一組非同步操作,需要按照順序完成。比如,依次遠端讀取一組 URL,然後按照讀取的順序輸出結果。

async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
複製程式碼

上面程式碼確實大大簡化,問題是所有遠端操作都是繼發。只有前一個 URL 返回結果,才會去讀取下一個 URL,這樣做效率很差,非常浪費時間。我們需要的是併發發出遠端請求。

async function logInOrder(urls) {
  // 併發讀取遠端URL
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序輸出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
複製程式碼

上面程式碼中,雖然map方法的引數是async函式,但它是併發執行的,因為只有async函式內部是繼發執行,外部不受影響。後面的for..of迴圈內部使用了await,因此實現了按順序輸出。

4.參考網址

http://es6.ruanyifeng.com/#docs/async https://github.com/xitu/gold-miner/pull/3738/files https://juejin.im/post/596e142d5188254b532ce2da https://juejin.im/post/5ade84c951882567113ad246

原文可訪問我的部落格

相關文章