ES6經常使用但被忽略的方法(第七彈async)

寫在開頭

  • ES6經常使用但被忽略的方法 系列文章,整理做者認爲一些平常開發可能會用到的一些方法、使用技巧和一些應用場景,細節深刻請查看相關內容鏈接,歡迎補充交流。

相關文章

async 函數

介紹

  • async 函數是 Generator 函數的語法糖。
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);
    });
  });
};
// Generator函數寫法
const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
// async 函數寫法
const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};
複製代碼
  • async函數對 Generator 函數的改進:
    1. Generator 函數的執行必須靠執行器,因此纔有了co模塊(基於 ES6Generatoryield ,讓咱們能用同步的形式編寫異步代碼),而async函數自帶執行器。async函數的執行,與普通函數如出一轍,只要一行。)
    asyncReadFile();
    複製代碼
    • Generator 函數,須要調用next方法,或者用co模塊,才能真正執行,獲得最後結果。
    const gg = gen();
    gg.next() 
    複製代碼
    1. 更好的語義。asyncawait,比起*yield,語義更清楚。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。
    2. 更廣的適用性。co模塊約定,yield命令後面只能是 Thunk 函數Promise 對象,而async函數的await命令後面,能夠是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時會自動轉成當即 resolvedPromise 對象)。
    3. 返回值是 Promiseasync函數的返回值是 Promise 對象,這比 Generator 函數的返回值是 Iterator 對象方便不少。

使用

  1. 基操
    • async函數返回一個 Promise對象,可使用then方法添加回調函數。async函數內部return語句返回的值,會成爲then方法回調函數的參數。 async函數內部拋出錯誤,會致使返回的 Promise 對象變爲reject狀態。拋出的錯誤對象會被catch方法回調函數接收到。
    async function getName() {
      return 'detanx';
    }
    getName().then(val => console.log(val))
    // "detanx"
    
    // 拋出錯誤
    async function f() {
      throw new Error('出錯了');
    }
    
    f().then(
      v => console.log(v),
      e => console.log(e)
    )
    // Error: 出錯了
    複製代碼
  2. 使用方式
    // 函數聲明
    async function foo() {}
    
    // 函數表達式
    const foo = async function () {};
    
    // 對象的方法
    let obj = { async foo() {} };
    obj.foo().then(...)
    
    // 箭頭函數
    const foo = async () => {};
    
    // 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(…);
    複製代碼
  3. 狀態變化
    • async函數返回的 Promise 對象,必須等到內部全部await命令後面的 Promise 對象執行完,纔會發生狀態改變,除非遇到return語句或者拋出錯誤。
  4. await 命令
    • 正常狀況下,await命令後面是一個 Promise 對象,返回該對象的結果。若是不是 Promise 對象,就直接返回對應的值。
    async function getName() {
      return 'detanx';
    }
    getName().then(val => console.log(val))
    // "detanx"
    複製代碼
    • 另外一種狀況是,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語句後面的 Promise 對象變爲reject狀態,那麼整個async函數都會中斷執行。
      async function f() {
        await Promise.reject('出錯了');
        await Promise.resolve('hello world'); // 不會執行
      }
      複製代碼
      • 前一個異步操做失敗,也不要中斷後面的異步操做。
      1. await放在try...catch裏面
      async function f() {
        try {
          await Promise.reject('出錯了');
        } catch(e) {
        }
        return await Promise.resolve('detanx');
      }
      
      f()
      .then(v => console.log(v))
      // detanx
      複製代碼
      1. await後面的 Promise 對象再跟一個catch方法,處理前面可能出現的錯誤。
      async function f() {
        await Promise.reject('出錯了')
          .catch(e => console.log(e));
        return await Promise.resolve('detanx');
      }
      
      f()
      .then(v => console.log(v))
      // 出錯了
      // detanx
      複製代碼
  5. 使用注意點
    1. await命令後面的Promise對象,運行結果多是rejected,因此最好把await命令放在try...catch代碼塊中。
    2. 多個await命令後面的異步操做,若是不存在繼發關係,最好讓它們同時觸發。
    // 寫法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);
    
    // 寫法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;
    let bar = await barPromise;
    複製代碼
    1. await命令只能用在async函數之中,若是用在普通函數,就會報錯。正確的寫法是採用for循環或者使用數組的reduce方法。但願多個請求併發執行,可使用Promise.all或者Promise.allSettled方法。
    async function dbFuc(db) {
      let docs = [{}, {}, {}];
    
      // 報錯,await的上一級函數不是async函數
      docs.forEach(function (doc) {
        await db.post(doc);
      });
      => for循環
      for (let doc of docs) {
        await db.post(doc);
      }
      => 數組的reduce方法
      await docs.reduce(async (_, doc) => {
        await _;
        await db.post(doc);
      }, undefined);
    }
    複製代碼
    1. async 函數能夠保留運行堆棧。
    const a = () => {
      b().then(() => c());
    };
    複製代碼
    • 上面代碼中,函數a內部運行了一個異步任務b()。當b()運行的時候,函數a()不會中斷,而是繼續執行。等到b()運行結束,可能a()早就運行結束,b()所在的上下文環境已經消失。若是b()c()報錯,錯誤堆棧將不包括a()git

    • 如今將這個例子改爲async函數。es6

    const a = async () => {
      await b();
      c();
    };
    複製代碼
    • 上面代碼中,b()運行的時候,a()是暫停執行,上下文環境都保存着。一旦b()c()報錯,錯誤堆棧將包括a()

實現

  • async 函數的實現原理,就是將 Generator 函數和自動執行器,包裝在一個函數裏。
async function fn(args) {
  // ...
}
// 等同於
function fn(args) {
  return spawn(function* () {
    // ...
  });
}
複製代碼
  • 全部的async函數均可以寫成上面的第二種形式,其中的spawn函數就是自動執行器。下面spawn函數的實現,基本就是前文自動執行器的翻版。
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); });
  });
}
複製代碼

異步處理方法的比較

  • 傳統方法,ES6 誕生之前,異步編程的方法,大概有下面四種。github

    1. 回調函數
    2. 事件監聽
    3. 發佈/訂閱
    4. Promise 對象
  • 經過一個例子,主要來看 PromiseGenerator 函數與async 函數的比較,其餘的不太熟悉的能夠本身去查相關的資料。shell

  • 假定某個 DOM 元素上面,部署了一系列的動畫,前一個動畫結束,才能開始後一個。若是當中有一個動畫出錯,就再也不往下執行,返回上一個成功執行的動畫的返回值。數據庫

    1. 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) {
        /* 忽略錯誤,繼續執行 */
      }).then(function() {
        return ret;
      });
    }
    複製代碼
    • 一眼看上去,代碼徹底都是 PromiseAPIthencatch等等),操做自己的語義反而不容易看出來。
    1. Generator 函數的寫法。
    function chainAnimationsGenerator(elem, animations) {
    
      return spawn(function*() {
        let ret = null;
        try {
          for(let anim of animations) {
            ret = yield anim(elem);
          }
        } catch(e) {
          /* 忽略錯誤,繼續執行 */
        }
        return ret;
      });
    }
    複製代碼
    • 語義比 Promise 寫法更清晰,用戶定義的操做所有都出如今spawn函數的內部。問題在於,必須有一個任務運行器,自動執行 Generator 函數,上面代碼的spawn函數就是自動執行器,它返回一個 Promise 對象,並且必須保證yield語句後面的表達式,必須返回一個 Promise
    1. async 函數的寫法。
    async function chainAnimationsAsync(elem, animations) {
      let ret = null;
      try {
        for(let anim of animations) {
          ret = await anim(elem);
        }
      } catch(e) {
        /* 忽略錯誤,繼續執行 */
      }
      return ret;
    }
    複製代碼
    • Async 函數的實現最簡潔,最符合語義,幾乎沒有語義不相關的代碼。
  • 全部的異步處理方法存在即合理,沒有那個最好,只有最合適,在處理不一樣的實際狀況時,咱們選擇最適合的處理方法便可。編程

實例

  • 實際開發中,常常遇到一組異步操做,須要按照順序完成。好比,依次遠程讀取一組 URL,而後按照讀取的順序輸出結果。
    1. 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數組。而後,reduce方法依次處理每一個 Promise 對象,而後使用then,將全部 Promise 對象連起來,所以就能夠依次輸出結果。
    1. async 函數實現。
    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,所以實現了按順序輸出。

頂級 await

  • 如今 await 命令只能出如今 async 函數內部,不然都會報錯。有一個語法提案(目前提案處於Status: Stage 3),容許在模塊的頂層獨立使用 await 命令,使得上面那行代碼不會報錯了。這個提案的目的,是借用 await 解決模塊異步加載的問題。
    1. 模塊awaiting.js的輸出值output,取決於異步操做。
    // awaiting.js
    let output;
    (async function1 main() {
      const dynamic = await import(someMission);
      const data = await fetch(url);
      output = someProcess(dynamic.default, data);
    })();
    export { output };
    複製代碼
    1. 加載awaiting.js模塊
    // usage.js
    import { output } from "./awaiting.js";
    
    function outputPlusValue(value) { return output + value }
    console.log(outputPlusValue(100));
    setTimeout(() => console.log(outputPlusValue(100), 1000);
    複製代碼
    • outputPlusValue()的執行結果,徹底取決於執行的時間。若是awaiting.js裏面的異步操做沒執行完,加載進來的output的值就是undefined
  • 目前的解決方法,就是讓原始模塊輸出一個 Promise 對象,從這個 Promise 對象判斷異步操做有沒有結束。
    // 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
    • 這種寫法比較麻煩,等於要求模塊的使用者遵照一個額外的使用協議,按照特殊的方法使用這個模塊。一旦你忘了要用 Promise 加載,只使用正常的加載方法,依賴這個模塊的代碼就可能出錯。並且,若是上面的usage.js又有對外的輸出,等於這個依賴鏈的全部模塊都要使用 Promise 加載。
  • 頂層的await命令,它保證只有異步操做完成,模塊纔會輸出值。
    // awaiting.js
    const dynamic = import(someMission);
    const data = fetch(url);
    export const output = someProcess((await dynamic).default, await data);
    
    // usage.js
    import { output } from "./awaiting.js";
    function outputPlusValue(value) { return output + value }
    
    console.log(outputPlusValue(100));
    setTimeout(() => console.log(outputPlusValue(100), 1000);
    複製代碼
    • 上面代碼中,兩個異步操做在輸出的時候,都加上了await命令。只有等到異步操做完成,這個模塊纔會輸出值。
  • 頂層await的一些使用場景。
    // import() 方法加載
    const strings = await import(`/i18n/${navigator.language}`);
    
    // 數據庫操做
    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命令的模塊,加載命令是同步執行的。
    // 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.jsy.js,打印結果是X1YX2Z。這說明,z.js並無等待x.js加載完成,再去加載y.js
    • 頂層的await命令有點像,交出代碼的執行權給其餘的模塊加載,等異步操做完成後,再拿回執行權,繼續向下執行。
相關文章
相關標籤/搜索