ES6之async函數的學習及宏任務,微任務的一點分析

Async函數是Generator函數的語法糖

async函數本質上是對generator函數的封裝,使其更加有語義化,更加簡潔,調用更加方便,其主要優化是如下幾點:javascript

  • 內置執行器。即async函數不須要像generator函數同樣每次都須要調用.next()方法去執行,它封裝了內部的執行器能夠幫咱們自動的執行.next()java

  • 更好的語義化。async...await...語法使代碼閱讀更加有語義化。es6

  • 更廣的適用性。co模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,而async函數的promise

    await命令後面,能夠是 Promise 對象和原始類型的值(數值、字符串和布爾值,異步

    但這時等同於同步操做)。async

  • 返回值是 Promise。async函數的返回值是 Promise 對象,這比 Generator 函數的返回值是 Iterator 對象函數

    方便多了。你能夠用then方法指定下一步的操做。進一步說,async函數徹底能夠看優化

    做多個異步操做,包裝成的一個 Promise 對象,而await命令就是內部then命令的動畫

    語法糖。this

// async與generator的寫法對比
const hello = function (name) {
  console.log(`hello ${name}`);;
};

const say = function* () {
  yield hello('xiaoming');
  yield hello('aiMi');
};
let g = say();
g.next();
g.next();
// 踩了一個坑,generator函數必需要先調用而且賦值給一個變量,才能夠next,先調用不會執行,但會返回一個指向內部狀態的指針對象(遍歷器對象(Iterator Object))
// say().next();
const asyncSay = async function () {
  await hello('xiaoming');
  await hello('aiMi');
};
let result = asyncSay();
console.log(result); // Promise {<pending>}

Async函數基本使用

async函數支持多種聲明方式

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

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

一個簡單demo來看下async函數在宏任務與微任務之間的表現

async function asyncTest1() {
    console.log('2');
    await console.log('3');
}
async function asyncTest2() {
    await console.log('4');
    console.log('5');
}
async function asyncTest3() {
    await new Promise(function(resolve) {
               console.log('6');
               resolve();
           }).then(function() {
               console.log('7');
           });
    console.log('8');
}
console.log('1');
setTimeout(function() {
    console.log('9');
    new Promise(function(resolve) {
        console.log('10');
        resolve();
    }).then(function() {
        console.log('11');
    })
    asyncTest1();
});
asyncTest2();
asyncTest3();
new Promise(function(resolve) {
    console.log('12');
    resolve();
}).then(function() {
    console.log('13');
});
// 打印順序
1==>4==>6==>12==>5==>7==>13==>8==>9==>10==>2==>3==>11

image

借用大神的一張圖來理解一下js的執行順序,一般咱們任務分爲同步與異步,通常同步任務進入主線程,異步任務進入event table並完成指定的事情之後註冊回調函數進入event queue,當主線程的任務執行完畢之後會從event queue去取異步任務進入主線程。(ps:我的理解這裏其實會遵循js單線程的原則,執行的一直是主線程。)這時候任務又會進行細分,分爲宏任務與微任務,宏任務與微任務分別存放在不一樣的event queue中,當主線程完成之後會先取微任務中的任務,當微任務中的任務執行完之後再執行宏任務隊列中的任務,全都完成之後會繼續循環次操做(事件輪詢)。

結合上述描述分析上面代碼,由於以前不太肯定await後邊會被分配在什麼任務中,執行之後分析發現await後的內容能夠看作是一個promise對象中的resolve,因此是當即執行的,屬於主線程,await之後的會被丟進微任務中。

那麼咱們能夠肯定首先執行主線程中的任務即1,4,6,12;這時候settimeout中的內容被丟進宏任務,5,7,8,13被丟進微任務,又由於8是在await的promise對象的then以後,await的做用是其後的代碼執行完畢之後才執行下一行代碼,因此8被丟進了更後一層的微任務,因此如今會先按順序執行微任務5,7,13,而後執行完之後再執行8;當這次循環匯中的微任務執行完之後開始執行宏任務,將宏任務中的內容歸入主線程,按照以前的方式先執行主線程,遇到異步將異步丟進event table,因此按順序執行時9,10,2,3,最後剩下promise的then在微任務中,最後執行11。

Async函數使用注意

  • 若是await後面的異步操做出錯,那麼等同於async函數返回的 Promise 對象被reject。因此最好把await命令放在try...catch代碼塊中。

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

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

  • async 函數能夠保留運行堆棧。

  • 還有一點須要注意的是咱們經過上邊那段demo代碼能夠發現若是你await後邊跟的是一個沒有.then()的promise對象其實仍是至關於同步,因此要想要獲得await後的代碼全都執行完畢的結果必定要將異步代碼放到.then()中並暴露出來,這樣await後的代碼纔會在其異步全都執行完之後再執行。

Async函數的實現原理

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

// 等同於

function fn(args) {
  return spawn(function* () {
    // ...
  });
}
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); });
  });
}

與其餘異步寫法對比

// 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;
  });
}
// 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;
  });
}
// async 函數
async function chainAnimationsAsync(elem, animations) {
  let ret = null;
  try {
    for(let anim of animations) {
      ret = await anim(elem);
    }
  } catch(e) {
    /* 忽略錯誤,繼續執行 */
  }
  return ret;
}

經過對比咱們能夠發現async函數寫法簡潔語義明確,能夠極大地減小代碼量,而且讓代碼的可讀性提升。在阮大神的《ECMAScript 6 入門》中還介紹了異步遍歷器,for await...of,異步generator等等,感興趣能夠再詳細研究一下。

相關文章
相關標籤/搜索