async 函數與 Generator 與co模塊

含義 

ES2017 標準引入了 async 函數,使得異步操做變得更加方便javascript

async 函數是什麼?一句話,它就是 Generator 函數的語法糖html

前文有一個 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);
    });
  });
};

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

上面代碼的函數gen能夠寫成async函數,就是下面這樣。java

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 函數的星號(*)替換成async,將yield替換成await,僅此而已。node

async函數對 Generator 函數的改進,體如今如下四點。webpack


(1)內置執行器。git

Generator 函數的執行必須靠執行器,因此纔有了co模塊,而async函數自帶執行器。也就是說,async函數的執行,與普通函數如出一轍,只要一行。github

asyncReadFile();
複製代碼

上面的代碼調用了asyncReadFile函數,而後它就會自動執行,輸出最後結果。這徹底不像 Generator 函數,須要調用next方法,或者用co模塊,才能真正執行,獲得最後結果。web

(2)更好的語義。ajax

asyncawait,比起星號和yield,語義更清楚了。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。

(3)更廣的適用性。

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

(4)返回值是 Promise。

async函數的返回值是 Promise 對象,這比 Generator 函數的返回值是 Iterator 對象方便多了。你能夠用then方法指定下一步的操做。

進一步說,async函數徹底能夠看做多個異步操做,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。

基本用法

async函數返回一個 Promise 對象,可使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到異步操做完成,再接着執行函數體內後面的語句。

下面是一個例子。

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

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});
複製代碼

上面代碼是一個獲取股票報價的函數,函數前面的async關鍵字,代表該函數內部有異步操做。調用該函數時,會當即返回一個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函數返回的是 Promise 對象,能夠做爲await命令的參數。因此,上面的例子也能夠寫成下面的形式。

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 函數有多種使用形式。

// 函數聲明
async function foo() { return 1}

// 函數表達式
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(…);

// 箭頭函數
const foo = async () => {};
複製代碼

語法

async函數的語法規則整體上比較簡單,難點是錯誤處理機制。


返回 Promise 對象 

async函數返回一個 Promise 對象。

async函數內部return語句返回的值,會成爲then方法回調函數的參數。

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

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

上面代碼中,函數f內部return命令返回的值,會被then方法回調函數接收到。


@
async function f() {
  await 'hello world';
}

f().then(v => console.log(v))
// undefined
// Promise {<resolved>: undefined}
// 上面的例子 沒有return 因此是undefined

@
async function f() {
  a=await 'hello world';
  return a
}

f().then(v => console.log(v))
// hello world
//Promise {<resolved>: undefined}

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

f().then(v => console.log(v))
// hello world
// Promise {<resolved>: undefined}

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

f().then(v => console.log(v))
// hello world
// Promise {<resolved>: undefined}@
async function f() {
  return setTimeout(()=>{console.log('hello world')});
}

f().then(v => console.log(v))
// 102
// Promise {<resolved>: undefined}
// hello world複製代碼



async函數內部拋出錯誤,會致使返回的 Promise 對象變爲reject狀態。拋出的錯誤對象會被catch方法回調函數接收到。

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

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

Promise 對象的狀態變化

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 命令

正常狀況下,await命令後面是一個 Promise 對象,返回該對象的結果。若是不是 Promise 對象,就直接返回對應的值。

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

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

上面代碼中,await命令的參數是數值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處理。

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

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 對象若是變爲reject狀態,則reject的參數會被catch方法的回調函數接收到。

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

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

注意,上面代碼中,await語句前面沒有return,可是reject方法的參數依然傳入了catch方法的回調函數。這裏若是在await前面加上return,效果是同樣的。

任何一個await語句後面的 Promise 對象變爲reject狀態,那麼整個async函數都會中斷執行。

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

上面代碼中,第二個await語句是不會執行的,由於第一個await語句狀態變成了reject

有時,咱們但願即便前一個異步操做失敗,也不要中斷後面的異步操做。這時能夠將第一個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
複製代碼

錯誤處理

若是await後面的異步操做出錯,那麼等同於async函數返回的 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函數f執行後,await後面的 Promise 對象會拋出一個錯誤對象,致使catch方法的回調函數被調用,它的參數就是拋出的錯誤對象。具體的執行機制,能夠參考後文的「async 函數的實現原理」。

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

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

若是有多個await命令,能夠統一放在try...catch結構中。

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結構,實現屢次重複嘗試。

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語句退出循環;若是失敗,會被catch語句捕捉,而後進入下一輪循環。

使用注意點

第一點,前面已經說過,await命令後面的Promise對象,運行結果多是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);
  });
}
複製代碼

@錯誤處理的更優雅的方式:


你是否會爲了系統健壯性,亦或者是爲了捕獲異步的錯誤,而頻繁的在 async 函數中寫 try/catch 的邏輯?

async function func() {
    try {
        let res = await asyncFunc()
    } catch (e) {
      //......
    }
}複製代碼


這樣咱們就可使用一個輔助函數包裹這個 async 函數實現錯誤捕獲

async function func() {
    let [err, res] = await errorCaptured(asyncFunc)
    if (err) {
        //... 錯誤捕獲
    }
    //...
}
複製代碼

可是這麼作有一個缺陷就是每次使用的時候,都要引入 errorCaptured 這個輔助函數,有沒有「懶」的方法呢?

答案確定是有的,我在那篇博客後提出了一個新的思路,能夠經過一個 webpack loader 來自動注入 try/catch 代碼,最後的結果但願是這樣的

// development
async function func() {
   let res = await asyncFunc()
    //...其餘邏輯
}

// release
async function func() {
    try {
        let res = await asyncFunc()
    } catch (e) {
      //......
    }
    //...其餘邏輯
}
複製代碼

是否是很棒?在開發環境中不須要任何多餘的代碼,讓 webpack 自動給生產環境的代碼注入錯誤捕獲的邏輯,此處是經過ast來處理,上方是處理以前和以後的代碼。

@此處優雅的錯誤處理方式來自於此連接
https://juejin.im/post/5d25b39bf265da1bb67a4176


第二點,多個await命令後面的異步操做,若是不存在繼發關係,最好讓它們同時觸發。

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

上面代碼中,getFoogetBar是兩個獨立的異步操做(即互不依賴),被寫成繼發關係。這樣比較耗時,由於只有getFoo完成之後,纔會執行getBar,徹底可讓它們同時觸發。

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

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

上面兩種寫法,getFoogetBar都是同時觸發,這樣就會縮短程序的執行時間。

第三點,await命令只能用在async函數之中,若是用在普通函數,就會報錯。

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

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

上面代碼會報錯,由於await用在普通函數之中了。可是,若是將forEach方法的參數改爲async函數,也有問題。

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

  // 可能獲得錯誤結果
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
}
複製代碼

上面代碼可能不會正常工做,緣由是這時三個db.post操做將是併發執行,也就是同時執行,而不是繼發執行。正確的寫法是採用for循環。

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

  for (let doc of docs) {
    await db.post(doc);
  }
}
複製代碼

若是確實但願多個請求併發執行,可使用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 函數能夠保留運行堆棧。

const a = () => {
  b().then(() => c());
};
複製代碼

上面代碼中,函數a內部運行了一個異步任務b()。當b()運行的時候,函數a()不會中斷,而是繼續執行。等到b()運行結束,可能a()早就運行結束了,b()所在的上下文環境已經消失了。若是b()c()報錯,錯誤堆棧將不包括a()

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

const a = async () => {
  await b();
  c();
};
複製代碼

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

async 函數的實現原理

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); });
  });
}
複製代碼

與其餘異步處理方法的比較

咱們經過一個例子,來看 async 函數與 Promise、Generator 函數的比較。

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

首先是 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;
  });

}
複製代碼

雖然 Promise 的寫法比回調函數的寫法大大改進,可是一眼看上去,代碼徹底都是 Promise 的 API(thencatch等等),操做自己的語義反而不容易看出來。

接着是 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;
  });

}
複製代碼

上面代碼使用 Generator 函數遍歷了每一個動畫,語義比 Promise 寫法更清晰,用戶定義的操做所有都出如今spawn函數的內部。這個寫法的問題在於,必須有一個任務運行器,自動執行 Generator 函數,上面代碼的spawn函數就是自動執行器,它返回一個 Promise 對象,並且必須保證yield語句後面的表達式,必須返回一個 Promise。

最後是 async 函數的寫法。

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

能夠看到 Async 函數的實現最簡潔,最符合語義,幾乎沒有語義不相關的代碼。它將 Generator 寫法中的自動執行器,改在語言層面提供,不暴露給用戶,所以代碼量最少。若是使用 Generator 寫法,自動執行器須要用戶本身提供。

實例:按順序完成異步操做

實際開發中,常常遇到一組異步操做,須要按照順序完成。好比,依次遠程讀取一組 URL,而後按照讀取的順序輸出結果。

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 對象連起來,所以就能夠依次輸出結果。

這種寫法不太直觀,可讀性比較差。下面是 async 函數實現。

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,所以實現了按順序輸出。

頂層 await

根據語法規格,await命令只能出如今 async 函數內部,不然都會報錯。

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

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

目前,有一個語法提案,容許在模塊的頂層獨立使用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 函數裏面,而後調用這個函數,只有等裏面的異步操做都執行,變量output纔會有值,不然就返回undefined

上面的代碼也能夠寫成當即執行函數的形式。

// awaiting.js
let output;
(async function 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()的執行結果,徹底取決於執行的時間。若是awaiting.js裏面的異步操做沒執行完,加載進來的output的值就是undefined

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

// 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 函數當即執行後,返回一個 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);
複製代碼

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

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

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

console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);
複製代碼

上面代碼的寫法,與普通的模塊加載徹底同樣。也就是說,模塊的使用者徹底不用關心,依賴模塊的內部有沒有異步操做,正常加載便可。

這時,模塊的加載會等待依賴模塊(上例是awaiting.js)的異步操做完成,才執行後面的代碼,有點像暫停在那裏。因此,它老是會獲得正確的output,不會由於加載時機的不一樣,而獲得不同的值。

下面是頂層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命令有點像,交出代碼的執行權給其餘的模塊加載,等異步操做完成後,再拿回執行權,繼續向下執行。

Generator 

Generator 函數是一個普通函數,可是有兩個特徵。一是,function關鍵字與函數名之間有一個星號;二是,函數體內部使用yield表達式,定義不一樣的內部狀態(yield在英語裏的意思就是「產出」)。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();複製代碼

上面代碼定義了一個 Generator 函數helloWorldGenerator,它內部有兩個yield表達式(helloworld),即該函數有三個狀態:hello,world 和 return 語句(結束執行)。

而後,Generator 函數的調用方法與普通函數同樣,也是在函數名後面加上一對圓括號。不一樣的是,調用 Generator 函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象,也就是上一章介紹的遍歷器對象(Iterator Object)。

下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。也就是說,每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式(或return語句)爲止。換言之,Generator 函數是分段執行的,yield表達式是暫停執行的標記,而next方法能夠恢復執行。


上面代碼一共調用了四次next方法。

第一次調用,Generator 函數開始執行,直到遇到第一個yield表達式爲止。next方法返回一個對象,它的value屬性就是當前yield表達式的值hellodone屬性的值false,表示遍歷尚未結束。

第二次調用,Generator 函數從上次yield表達式停下的地方,一直執行到下一個yield表達式。next方法返回的對象的value屬性就是當前yield表達式的值worlddone屬性的值false,表示遍歷尚未結束。

第三次調用,Generator 函數從上次yield表達式停下的地方,一直執行到return語句(若是沒有return語句,就執行到函數結束)。next方法返回的對象的value屬性,就是緊跟在return語句後面的表達式的值(若是沒有return語句,則value屬性的值爲undefined),done屬性的值true,表示遍歷已經結束。

第四次調用,此時 Generator 函數已經運行完畢,next方法返回對象的value屬性爲undefineddone屬性爲true。之後再調用next方法,返回的都是這個值。

總結一下,調用 Generator 函數,返回一個遍歷器對象,表明 Generator 函數的內部指針。之後,每次調用遍歷器對象的next方法,就會返回一個有着valuedone兩個屬性的對象。value屬性表示當前的內部狀態的值,是yield表達式後面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束。

ES6 沒有規定,function關鍵字與函數名之間的星號,寫在哪一個位置。這致使下面的寫法都能經過。

function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
複製代碼

因爲 Generator 函數仍然是普通函數,因此通常的寫法是上面的第三種,即星號緊跟在function關鍵字後面。

yield 表達式 

因爲 Generator 函數返回的遍歷器對象,只有調用next方法纔會遍歷下一個內部狀態,因此其實提供了一種能夠暫停執行的函數。yield表達式就是暫停標誌。

遍歷器對象的next方法的運行邏輯以下。

(1)遇到yield表達式,就暫停執行後面的操做,並將緊跟在yield後面的那個表達式的值,做爲返回的對象的value屬性值。

(2)下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。

(3)若是沒有再遇到新的yield表達式,就一直運行到函數結束,直到return語句爲止,並將return語句後面的表達式的值,做爲返回的對象的value屬性值。

(4)若是該函數沒有return語句,則返回的對象的value屬性值爲undefined

須要注意的是,yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行,所以等於爲 JavaScript 提供了手動的「惰性求值」(Lazy Evaluation)的語法功能。

function* gen() {
  yield  123 + 456;
}
複製代碼

上面代碼中,yield後面的表達式123 + 456,不會當即求值,只會在next方法將指針移到這一句時,纔會求值。

用途:

ajax的異步處理

function* main() {
    var result = yield request("http://www.filltext.com?rows=10&f={firstName}");
    console.log(result);
    //do 別的ajax請求;
}複製代碼


co 模塊

一、co 模塊,它基於 ES6 的 generator 和 yield ,讓咱們能用同步的形式編寫異步代碼的nodejs模塊。

二、co 模塊是能讓咱們以同步的形式編寫異步代碼的 nodejs 模塊

三、學習網絡地址:https://segmentfault.com/a/1190000002732081

四、代碼以下:

var co = require ('co');

co(function*() {
    執行代碼。。。
});複製代碼
co 模塊能夠將異步解放成同步。co 函數接受一個 generator 函數做爲參數,在函數內部自動執行 yield 。co 函數接受一個 generator 函數,而且在 co 函數內部執行,生成一個 generator 實例。調用 generator 的 next 方法, 對生成的對象的 value 屬性值使用 toPromise 方法,生成一個 promise 實例,當這個 promise 實例的狀態變爲 resolved 時,執行 onFulfilled 方法,再次對 generator 實例執行 next 方法,而後重複整個過程。若是出現錯誤,則執行這個 promise 實例定義的 reject 函數即 onRejected 方法。

thunk函數

javascript中的thunk函數就是一個單參數函數,且該參數必須是一個callback函數,callback的簽名必須爲callback(err,args...);

所謂的thunkify就是將一個多參數函數轉化爲一個thunk函數,該多參數函數必須有一個callback做爲參數

總結:

  • Generator 函數,返回一個遍歷器對象,表明 Generator 函數的內部指針。之後,每次調用遍歷器對象的next方法,就會返回一個有着valuedone兩個屬性的對象
  • async 函數是什麼?一句話,它就是 Generator 函數的語法糖
  • async函數就是將 Generator 函數的星號(*)替換成async,將yield替換成await
  •  async 函數 比 Generator的改進:1,返回promise  2,內置執行器 三、語法更人性化
  • async中能夠沒有await,可是不能沒有renturn ,不然將什麼也取不到
  • async 必然返回promise對象,可以使用then 取到 async中真正的返回值
  • async function foo() { return 1}複製代碼

         以上async函數雖然有返回值,可是,其實真正的返回值並不是所看到的那樣,而是被                 promise 包裝過的返回值。


async function a() {
  return await 2;
}
a(); // Promise {<resolved>: 2}

a().then(function (result) {
  console.log(result);return 3
})
.then((data)=>console.log(data));
// 2
// 3
// Promise {<resolved>: undefined}複製代碼

小提示:

  • then始終返回一個promise,返回上一級then return 返回的數據 或者  若是上一級是一個promise的話,reslove()中的數據。
  • async之因此返回promise 就是爲了方便使用。
  • async的錯誤處理可經過一個包 來這樣寫 [err,data] = await wrap( getData() )
  • 異步編程變化史 回調=》promise 經過then來拿resolve的數據 =》async 同步的方式去寫異步的代碼。直接獲取數據,在下方直接操做,不須要then的方式。
  • 思考async與promise的關係。
  • 從前端請求後端的方法進化說異步: 從剛開始的的ajax的屬性success中的回調,層層嵌套 =》到 promise包裝後的 經過then來取上一個異步任務的返回數據=》在到 同步寫法的async await 等後面的promise執行完後,直接返回值就是 數據。
  • co 函數接受一個 generator 函數,讓其自自動執行


感謝阮一峯老師的付出,本文主要內容來源於阮一峯老師博客,特此說明。

相關文章
相關標籤/搜索