ES6 async 函數(shù)

2020-06-11 14:35 更新

1. 含義

ES2017 標準引入了async 函數(shù),使得異步操作變得更加方便。

async 函數(shù)是什么?一句話,它就是 Generator 函數(shù)的語法糖。

前文有一個 Generator 函數(shù),依次讀取兩個文件。

const fs = require('fs');


const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};


const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

上面代碼的函數(shù) gen 可以寫成 async 函數(shù),就是下面這樣。

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

一比較就會發(fā)現(xiàn), async 函數(shù)就是將 Generator 函數(shù)的星號( * )替換成 async ,將 yield 替換成 await ,僅此而已。

async 函數(shù)對 Generator 函數(shù)的改進,體現(xiàn)在以下四點。

(1)內(nèi)置執(zhí)行器。

Generator 函數(shù)的執(zhí)行必須靠執(zhí)行器,所以才有了 co 模塊,而 async 函數(shù)自帶執(zhí)行器。也就是說, async 函數(shù)的執(zhí)行,與普通函數(shù)一模一樣,只要一行。

asyncReadFile();

上面的代碼調(diào)用了 asyncReadFile 函數(shù),然后它就會自動執(zhí)行,輸出最后結(jié)果。這完全不像 Generator 函數(shù),需要調(diào)用 next 方法,或者用 co 模塊,才能真正執(zhí)行,得到最后結(jié)果。

(2)更好的語義。

async 和 await ,比起星號和 yield ,語義更清楚了。 async 表示函數(shù)里有異步操作, await 表示緊跟在后面的表達式需要等待結(jié)果。

(3)更廣的適用性。

co 模塊約定, yield 命令后面只能是 Thunk 函數(shù)或 Promise 對象,而 async 函數(shù)的 await 命令后面,可以是 Promise 對象和原始類型的值(數(shù)值、字符串和布爾值,但這時會自動轉(zhuǎn)成立即 resolved 的 Promise 對象)。

(4)返回值是 Promise。

async 函數(shù)的返回值是 Promise 對象,這比 Generator 函數(shù)的返回值是 Iterator 對象方便多了。你可以用 then 方法指定下一步的操作。

進一步說, async 函數(shù)完全可以看作多個異步操作,包裝成的一個 Promise 對象,而 await 命令就是內(nèi)部 then 命令的語法糖。

2. 基本用法

async 函數(shù)返回一個 Promise對象,可以使用 then方法添加回調(diào)函數(shù)。當函數(shù)執(zhí)行的時候,一旦遇到 await 就會先返回,等到異步操作完成,再接著執(zhí)行函數(shù)體內(nèi)后面的語句。

下面是一個例子。

async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}


getStockPriceByName('goog').then(function (result) {
  console.log(result);
});

上面代碼是一個獲取股票報價的函數(shù),函數(shù)前面的 async 關(guān)鍵字,表明該函數(shù)內(nèi)部有異步操作。調(diào)用該函數(shù)時,會立即返回一個 Promise 對象。

下面是另一個例子,指定多少毫秒后輸出一個值。

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}


async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}


asyncPrint('hello world', 50);

上面代碼指定 50 毫秒以后,輸出 hello world 。

由于 async 函數(shù)返回的是 Promise 對象,可以作為 await 命令的參數(shù)。所以,上面的例子也可以寫成下面的形式。

async function timeout(ms) {
  await new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}


async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}


asyncPrint('hello world', 50);

async 函數(shù)有多種使用形式。

// 函數(shù)聲明
async function foo() {}


// 函數(shù)表達式
const foo = async function () {};


// 對象的方法
let obj = { async foo() {} };
obj.foo().then(...)


// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }


  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}


const storage = new Storage();
storage.getAvatar('jake').then(…);


// 箭頭函數(shù)
const foo = async () => {};

3. 語法

async 函數(shù)的語法規(guī)則總體上比較簡單,難點是錯誤處理機制。

返回 Promise 對象

async 函數(shù)返回一個Promise對象。

async 函數(shù)內(nèi)部return 語句返回的值,會成為then 方法回調(diào)函數(shù)的參數(shù)。

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


f().then(v => console.log(v))
// "hello world"

上面代碼中,函數(shù) f 內(nèi)部 return 命令返回的值,會被 then 方法回調(diào)函數(shù)接收到。

async 函數(shù)內(nèi)部拋出錯誤,會導致返回的 Promise 對象變?yōu)?code>reject狀態(tài)。拋出的錯誤對象會被 catch方法回調(diào)函數(shù)接收到。

async function f() {
  throw new Error('出錯了');
}


f().then(
  v => console.log(v),
  e => console.log(e)
)
// Error: 出錯了

Promise 對象的狀態(tài)變化

async函數(shù)返回的 Promise 對象,必須等到內(nèi)部所有 await 命令后面的 Promise 對象執(zhí)行完,才會發(fā)生狀態(tài)改變,除非遇到 return 語句或者拋出錯誤。也就是說,只有 async 函數(shù)內(nèi)部的異步操作執(zhí)行完,才會執(zhí)行 then 方法指定的回調(diào)函數(shù)。

下面是一個例子。

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"

上面代碼中,函數(shù) getTitle 內(nèi)部有三個操作:抓取網(wǎng)頁取出文本、匹配頁面標題。只有這三個操作全部完成,才會執(zhí)行 then 方法里面的 console.log 。

await 命令

正常情況下, await 命令后面是一個Promise對象,返回該對象的結(jié)果。如果不是 Promise 對象,就直接返回對應(yīng)的值。

async function f() {
  // 等同于
  // return 123;
  return await 123;
}


f().then(v => console.log(v))
// 123

上面代碼中, await 命令的參數(shù)是數(shù)值 123 ,這時等同于 return 123 。

另一種情況是, await 命令后面是一個 thenable 對象(即定義了 then 方法的對象),那么 await 會將其等同于 Promise 對象。

class Sleep {
  constructor(timeout) {
    this.timeout = timeout;
  }
  then(resolve, reject) {
    const startTime = Date.now();
    setTimeout(
      () => resolve(Date.now() - startTime),
      this.timeout
    );
  }
}


(async () => {
  const sleepTime = await new Sleep(1000);
  console.log(sleepTime);
})();
// 1000

上面代碼中, await 命令后面是一個 Sleep 對象的實例。這個實例不是 Promise 對象,但是因為定義了 then 方法, await 會將其視為 Promise 處理。

這個例子還演示了如何實現(xiàn)休眠效果。JavaScript 一直沒有休眠的語法,但是借助 await 命令就可以讓程序停頓指定的時間。下面給出了一個簡化的 sleep 實現(xiàn)。

function sleep(interval) {
  return new Promise(resolve => {
    setTimeout(resolve, interval);
  })
}


// 用法
async function one2FiveInAsync() {
  for(let i = 1; i <= 5; i++) {
    console.log(i);
    await sleep(1000);
  }
}


one2FiveInAsync();

await 命令后面的 Promise 對象如果變?yōu)? reject 狀態(tài),則 reject 的參數(shù)會被 catch 方法的回調(diào)函數(shù)接收到。

async function f() {
  await Promise.reject('出錯了');
}


f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出錯了

注意,上面代碼中, await 語句前面沒有 return ,但是 reject 方法的參數(shù)依然傳入了 catch 方法的回調(diào)函數(shù)。這里如果在 await 前面加上 return ,效果是一樣的。

任何一個 await 語句后面的 Promise 對象變?yōu)? reject 狀態(tài),那么整個 async 函數(shù)都會中斷執(zhí)行。

async function f() {
  await Promise.reject('出錯了');
  await Promise.resolve('hello world'); // 不會執(zhí)行
}

上面代碼中,第二個 await 語句是不會執(zhí)行的,因為第一個 await 語句狀態(tài)變成了 reject 。

有時,我們希望即使前一個異步操作失敗,也不要中斷后面的異步操作。這時可以將第一個 await 放在 try...catch 結(jié)構(gòu)里面,這樣不管這個異步操作是否成功,第二個 await 都會執(zhí)行。

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 方法,處理前面可能出現(xiàn)的錯誤。

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

錯誤處理

如果 await 后面的異步操作出錯,那么等同于 async 函數(shù)返回的 Promise 對象被 reject 。

async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error('出錯了');
  });
}


f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出錯了

上面代碼中, async 函數(shù) f 執(zhí)行后, await 后面的 Promise 對象會拋出一個錯誤對象,導致 catch 方法的回調(diào)函數(shù)被調(diào)用,它的參數(shù)就是拋出的錯誤對象。具體的執(zhí)行機制,可以參考后文的“async 函數(shù)的實現(xiàn)原理”。

防止出錯的方法,也是將其放在 try...catch 代碼塊之中。

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

如果有多個 await 命令,可以統(tǒng)一放在 try...catch 結(jié)構(gòu)中。

async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);


    console.log('Final: ', val3);
  }
  catch (err) {
    console.error(err);
  }
}

下面的例子使用 try...catch 結(jié)構(gòu),實現(xiàn)多次重復嘗試。

const superagent = require('superagent');
const NUM_RETRIES = 3;


async function test() {
  let i;
  for (i = 0; i < NUM_RETRIES; ++i) {
    try {
      await superagent.get('http://google.com/this-throws-an-error');
      break;
    } catch(err) {}
  }
  console.log(i); // 3
}


test();

上面代碼中,如果 await 操作成功,就會使用 break 語句退出循環(huán);如果失敗,會被 catch 語句捕捉,然后進入下一輪循環(huán)。

使用注意點

第一點,前面已經(jīng)說過, await 命令后面的 Promise 對象,運行結(jié)果可能是 rejected ,所以最好把 await 命令放在 try...catch 代碼塊中。

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}


// 另一種寫法


async function myFunction() {
  await somethingThatReturnsAPromise()
  .catch(function (err) {
    console.log(err);
  });
}

第二點,多個 await 命令后面的異步操作,如果不存在繼發(fā)關(guān)系,最好讓它們同時觸發(fā)。

let foo = await getFoo();
let bar = await getBar();

上面代碼中, getFoo 和 getBar 是兩個獨立的異步操作(即互不依賴),被寫成繼發(fā)關(guān)系。這樣比較耗時,因為只有 getFoo 完成以后,才會執(zhí)行 getBar ,完全可以讓它們同時觸發(fā)。

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


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

上面兩種寫法, getFoo 和 getBar 都是同時觸發(fā),這樣就會縮短程序的執(zhí)行時間。

第三點, await 命令只能用在 async 函數(shù)之中,如果用在普通函數(shù),就會報錯。

async function dbFuc(db) {
  let docs = [{}, {}, {}];


  // 報錯
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}

上面代碼會報錯,因為 await 用在普通函數(shù)之中了。但是,如果將 forEach 方法的參數(shù)改成 async 函數(shù),也有問題。

function dbFuc(db) { //這里不需要 async
  let docs = [{}, {}, {}];


  // 可能得到錯誤結(jié)果
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
}

上面代碼可能不會正常工作,原因是這時三個 db.post 操作將是并發(fā)執(zhí)行,也就是同時執(zhí)行,而不是繼發(fā)執(zhí)行。正確的寫法是采用 for 循環(huán)。

async function dbFuc(db) {
  let docs = [{}, {}, {}];


  for (let doc of docs) {
    await db.post(doc);
  }
}

另一種方法是使用數(shù)組的 reduce 方法。

async function dbFuc(db) {
  let docs = [{}, {}, {}];


  await docs.reduce(async (_, doc) => {
    await _;
    await db.post(doc);
  }, undefined);
}

上面例子中, reduce 方法的第一個參數(shù)是 async 函數(shù),導致該函數(shù)的第一個參數(shù)是前一步操作返回的 Promise 對象,所以必須使用 await 等待它操作結(jié)束。另外, reduce 方法返回的是 docs 數(shù)組最后一個成員的 async 函數(shù)的執(zhí)行結(jié)果,也是一個 Promise 對象,導致在它前面也必須加上 await 。

如果確實希望多個請求并發(fā)執(zhí)行,可以使用 Promise.all 方法。當三個請求都會 resolved 時,下面兩種寫法效果相同。

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));


  let results = await Promise.all(promises);
  console.log(results);
}


// 或者使用下面的寫法


async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));


  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}

第四點,async 函數(shù)可以保留運行堆棧。

const a = () => {
  b().then(() => c());
};

上面代碼中,函數(shù) a 內(nèi)部運行了一個異步任務(wù) b() 。當 b() 運行的時候,函數(shù) a() 不會中斷,而是繼續(xù)執(zhí)行。等到 b() 運行結(jié)束,可能 a() 早就運行結(jié)束了, b() 所在的上下文環(huán)境已經(jīng)消失了。如果 b() 或 c() 報錯,錯誤堆棧將不包括 a() 。

現(xiàn)在將這個例子改成 async 函數(shù)。

const a = async () => {
  await b();
  c();
};

上面代碼中, b() 運行的時候, a() 是暫停執(zhí)行,上下文環(huán)境都保存著。一旦 b() 或 c() 報錯,錯誤堆棧將包括 a() 。

4. async 函數(shù)的實現(xiàn)原理

async 函數(shù)的實現(xiàn)原理,就是將 Generator函數(shù)和自動執(zhí)行器,包裝在一個函數(shù)里。

async function fn(args) {
  // ...
}


// 等同于


function fn(args) {
  return spawn(function* () {
    // ...
  });
}

所有的 async 函數(shù)都可以寫成上面的第二種形式,其中的 spawn 函數(shù)就是自動執(zhí)行器。

下面給出 spawn 函數(shù)的實現(xiàn),基本就是前文自動執(zhí)行器的翻版。

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

5. 與其他異步處理方法的比較

我們通過一個例子,來看 async函數(shù)與Promise、Generator函數(shù)的比較。

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

首先是 Promise 的寫法。

function chainAnimationsPromise(elem, animations) {


  // 變量ret用來保存上一個動畫的返回值
  let ret = null;


  // 新建一個空的Promise
  let p = Promise.resolve();


  // 使用then方法,添加所有動畫
  for(let anim of animations) {
    p = p.then(function(val) {
      ret = val;
      return anim(elem);
    });
  }


  // 返回一個部署了錯誤捕捉機制的Promise
  return p.catch(function(e) {
    /* 忽略錯誤,繼續(xù)執(zhí)行 */
  }).then(function() {
    return ret;
  });


}

雖然 Promise 的寫法比回調(diào)函數(shù)的寫法大大改進,但是一眼看上去,代碼完全都是 Promise 的 API( then 、 catch 等等),操作本身的語義反而不容易看出來。

接著是 Generator 函數(shù)的寫法。

function chainAnimationsGenerator(elem, animations) {


  return spawn(function*() {
    let ret = null;
    try {
      for(let anim of animations) {
        ret = yield anim(elem);
      }
    } catch(e) {
      /* 忽略錯誤,繼續(xù)執(zhí)行 */
    }
    return ret;
  });


}

上面代碼使用 Generator 函數(shù)遍歷了每個動畫,語義比 Promise 寫法更清晰,用戶定義的操作全部都出現(xiàn)在 spawn 函數(shù)的內(nèi)部。這個寫法的問題在于,必須有一個任務(wù)運行器,自動執(zhí)行 Generator 函數(shù),上面代碼的 spawn 函數(shù)就是自動執(zhí)行器,它返回一個 Promise 對象,而且必須保證 yield 語句后面的表達式,必須返回一個 Promise。

最后是 async 函數(shù)的寫法。

async function chainAnimationsAsync(elem, animations) {
  let ret = null;
  try {
    for(let anim of animations) {
      ret = await anim(elem);
    }
  } catch(e) {
    /* 忽略錯誤,繼續(xù)執(zhí)行 */
  }
  return ret;
}

可以看到 Async 函數(shù)的實現(xiàn)最簡潔,最符合語義,幾乎沒有語義不相關(guān)的代碼。它將 Generator 寫法中的自動執(zhí)行器,改在語言層面提供,不暴露給用戶,因此代碼量最少。如果使用 Generator 寫法,自動執(zhí)行器需要用戶自己提供。

6. 實例:按順序完成異步操作

實際開發(fā)中,經(jīng)常遇到一組異步操作,需要按照順序完成。比如,依次遠程讀取一組 URL,然后按照讀取的順序輸出結(jié)果。

Promise 的寫法如下。

function logInOrder(urls) {
  // 遠程讀取所有URL
  const textPromises = urls.map(url => {
    return fetch(url).then(response => response.text());
  });


  // 按次序輸出
  textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise)
      .then(text => console.log(text));
  }, Promise.resolve());
}

上面代碼使用 fetch 方法,同時遠程讀取一組 URL。每個 fetch 操作都返回一個 Promise 對象,放入 textPromises 數(shù)組。然后, reduce 方法依次處理每個 Promise 對象,然后使用 then ,將所有 Promise 對象連起來,因此就可以依次輸出結(jié)果。

這種寫法不太直觀,可讀性比較差。下面是 async 函數(shù)實現(xiàn)。

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

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

async function logInOrder(urls) {
  // 并發(fā)讀取遠程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 方法的參數(shù)是 async 函數(shù),但它是并發(fā)執(zhí)行的,因為只有 async 函數(shù)內(nèi)部是繼發(fā)執(zhí)行,外部不受影響。后面的 for..of 循環(huán)內(nèi)部使用了 await ,因此實現(xiàn)了按順序輸出。

7. 頂層 await

根據(jù)語法規(guī)格, await 命令只能出現(xiàn)在 async 函數(shù)內(nèi)部,否則都會報錯。

// 報錯
const data = await fetch('https://api.example.com');

上面代碼中, await 命令獨立使用,沒有放在 async 函數(shù)里面,就會報錯。

目前,有一個語法提案,允許在模塊的頂層獨立使用 await 命令,使得上面那行代碼不會報錯了。這個提案的目的,是借用 await 解決模塊異步加載的問題。

// awaiting.js
let output;
async function main() {
  const dynamic = await import(someMission);
  const data = await fetch(url);
  output = someProcess(dynamic.default, data);
}
main();
export { output };

上面代碼中,模塊 awaiting.js 的輸出值 output ,取決于異步操作。我們把異步操作包裝在一個 async 函數(shù)里面,然后調(diào)用這個函數(shù),只有等里面的異步操作都執(zhí)行,變量 output 才會有值,否則就返回 undefined 。

上面的代碼也可以寫成立即執(zhí)行函數(shù)的形式。

// awaiting.js
let output;
(async function1 main() {
  const dynamic = await import(someMission);
  const data = await fetch(url);
  output = someProcess(dynamic.default, data);
})();
export { output };

下面是加載這個模塊的寫法。

// usage.js
import { output } from "./awaiting.js";


function outputPlusValue(value) { return output + value }


console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);

上面代碼中, outputPlusValue() 的執(zhí)行結(jié)果,完全取決于執(zhí)行的時間。如果 awaiting.js 里面的異步操作沒執(zhí)行完,加載進來的 output 的值就是 undefined 。

目前的解決方法,就是讓原始模塊輸出一個 Promise 對象,從這個 Promise 對象判斷異步操作有沒有結(jié)束。

// awaiting.js
let output;
export default (async function main() {
  const dynamic = await import(someMission);
  const data = await fetch(url);
  output = someProcess(dynamic.default, data);
})();
export { output };

上面代碼中, awaiting.js 除了輸出 output ,還默認輸出一個 Promise 對象(async 函數(shù)立即執(zhí)行后,返回一個 Promise 對象),從這個對象判斷異步操作是否結(jié)束。

下面是加載這個模塊的新的寫法。

// usage.js
import promise, { output } from "./awaiting.js";


function outputPlusValue(value) { return output + value }


promise.then(() => {
  console.log(outputPlusValue(100));
  setTimeout(() => console.log(outputPlusValue(100), 1000);
});

上面代碼中,將 awaiting.js 對象的輸出,放在 promise.then() 里面,這樣就能保證異步操作完成以后,才去讀取 output 。

這種寫法比較麻煩,等于要求模塊的使用者遵守一個額外的使用協(xié)議,按照特殊的方法使用這個模塊。一旦你忘了要用 Promise 加載,只使用正常的加載方法,依賴這個模塊的代碼就可能出錯。而且,如果上面的 usage.js 又有對外的輸出,等于這個依賴鏈的所有模塊都要使用 Promise 加載。

頂層的 await 命令,就是為了解決這個問題。它保證只有異步操作完成,模塊才會輸出值。

// awaiting.js
const dynamic = import(someMission);
const data = fetch(url);
export const output = someProcess((await dynamic).default, await data);

上面代碼中,兩個異步操作在輸出的時候,都加上了 await 命令。只有等到異步操作完成,這個模塊才會輸出值。

加載這個模塊的寫法如下。

// usage.js
import { output } from "./awaiting.js";
function outputPlusValue(value) { return output + value }


console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);

上面代碼的寫法,與普通的模塊加載完全一樣。也就是說,模塊的使用者完全不用關(guān)心,依賴模塊的內(nèi)部有沒有異步操作,正常加載即可。

這時,模塊的加載會等待依賴模塊(上例是 awaiting.js )的異步操作完成,才執(zhí)行后面的代碼,有點像暫停在那里。所以,它總是會得到正確的 output ,不會因為加載時機的不同,而得到不一樣的值。

下面是頂層 await 的一些使用場景。

// import() 方法加載
const strings = await import( /i18n/${navigator.language} );


// 數(shù)據(jù)庫操作
const connection = await dbConnector();


// 依賴回滾
let jQuery;
try {
  jQuery = await import('https://cdn-a.com/jQuery');
} catch {
  jQuery = await import('https://cdn-b.com/jQuery');
}

注意,如果加載多個包含頂層 await 命令的模塊,加載命令是同步執(zhí)行的。

// x.js
console.log("X1");
await new Promise(r => setTimeout(r, 1000));
console.log("X2");


// y.js
console.log("Y");


// z.js
import "./x.js";
import "./y.js";
console.log("Z");

上面代碼有三個模塊,最后的 z.js 加載 x.js 和 y.js ,打印結(jié)果是 X1 、 Y 、 X2 、 Z 。這說明, z.js 并沒有等待 x.js 加載完成,再去加載 y.js 。

頂層的 await 命令有點像,交出代碼的執(zhí)行權(quán)給其他的模塊加載,等異步操作完成后,再拿回執(zhí)行權(quán),繼續(xù)向下執(zhí)行。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號