Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES6之async函数的学习及宏任务,微任务的一点分析 #19

Open
mhfe123 opened this issue Jan 7, 2019 · 0 comments
Open

ES6之async函数的学习及宏任务,微任务的一点分析 #19

mhfe123 opened this issue Jan 7, 2019 · 0 comments

Comments

@mhfe123
Copy link
Contributor

mhfe123 commented Jan 7, 2019

Async函数是Generator函数的语法糖

async函数本质上是对generator函数的封装,使其更加有语义化,更加简洁,调用更加方便,其主要优化是以下几点:

  • 内置执行器。即async函数不需要像generator函数一样每次都需要调用.next()方法去执行,它封装了内部的执行器可以帮我们自动的执行.next()

  • 更好的语义化。async...await...语法使代码阅读更加有语义化。

  • 更广的适用性。co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的

    await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,

    但这时等同于同步操作)。

  • 返回值是 Promise。async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象

    方便多了。你可以用then方法指定下一步的操作。进一步说,async函数完全可以看

    作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的

    语法糖。

// 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等等,感兴趣可以再详细研究一下。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant