Promise/Generator/Async/await

·  阅读 329
Promise/Generator/Async/await

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

概念:

Async/await是通过等待执行,而不是通过阻塞,身为异步的他,和同步的区别便在于阻塞和非阻塞

思考:

为什么要引入async 众所周知,JavaScript语言的执行环境是“单线程”的,那么异步编程对JavaScript语言来说就显得尤为重要。

以前我们大多数的做法是使用回调函数来实现JavaScript语言的异步编程。

回调函数本身没有问题,但如果出现多个回调函数嵌套

例如:进入某个页面,需要先登录,拿到用户信息之后,调取用户商品信息,代码如下:

this.$http.jsonp('/login'(res) => {
  this.$http.jsonp('/getInfo'(info) => {
    // do something  })
})
复制代码

假如上面还有更多的请求操作,就会出现多重嵌套。代码很快就会乱成一团,这种情况就被称为“回调函数地狱”(callback hell)。

Promise

于是,我们提出了Promise,它将回调函数的嵌套,改成了链式调用。写法如下:

var promise = new Promise((resolve, reject) => {
  this.login(resolve)
})
.then(() => this.getInfo())
.catch(() => { console.log("Error") })
复制代码

从上面可以看出,Promise的写法只是回调函数的改进,使用then方法,只是让异步任务的两段执行更清楚而已。

Generator

Promise的最大问题是代码冗余,请求任务多时,一堆的then,也使得原来的语义变得很不清楚。此时我们引入了另外一种异步编程的机制:Generator。

Generator 函数是一个普通函数,但是有两个特征。

  1. function关键字与函数名之间有一个星号
  2. 函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)

一个简单的例子用来说明它的用法:

functionhelloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();
复制代码

上面代码定义了一个 Generator 函数helloWorldGenerator

它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)

Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,必须调用遍历器对象的next方法,使得指针移向下一个状态。

也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。

换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

上述代码分步执行如下:

hw.next()// { value: 'hello'donefalse }
hw.next()// { value: 'world'donefalse }
hw.next()// { value: 'ending'donetrue }
hw.next()// { value: undefined, donetrue }
复制代码

Generator函数的机制更符合我们理解的异步编程思想。

用户登录的例子,我们用Generator来写,如下:

var gen = function* () {
  const f1 = yield this.login()
  const f2 = yield this.getInfo()
};
复制代码

虽然Generator将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。此时,我们便希望能出现一种能自动执行Generator函数的方法。

async/await

我们的主角来了:async/await。

ES8引入了async函数,使得异步操作变得更加方便。简单说来,它就是Generator函数的语法糖。

async function asyncFunc(params) {
  const result1 = await this.login()
  const result2 = await this.getInfo()
}
复制代码

是不是更加简洁易懂呢?

异步函数存在以下四种使用形式:

  1. 函数声明: async function foo() {}
  2. 函数表达式: const foo = async function() {}
  3. 对象的方式: let obj = { async foo() {} }
  4. 箭头函数: const foo = async () => {}

常见用法汇总

处理单个异步结果

async function asyncFunc() {
  const result = await otherAsyncFunc();
  console.log(result);
}
复制代码

顺序处理多个异步结果:

async function asyncFunc() {
  const result1 = await otherAsyncFunc1();
  console.log(result1);
  const result2 = await otherAsyncFunc2();
  console.log(result2);
}
复制代码

并行处理多个异步结果:

async function asyncFunc() {
  const [result1, result2] = await Promise.all([
    otherAsyncFunc1(),
    otherAsyncFunc2()
  ]);
  console.log(result1, result2);
}
 
处理错误:
async function asyncFunc() {
  try {
    await otherAsyncFunc();
  } catch (err) {
    console.error(err);
  }
}
复制代码

若想进一步了解async的具体实践,可参见阮老师的博客文章,链接奉上:ECMAScript 6 入门

除了异步async,还有顺序执行settimeout

setTimeout(function(){
  console.log(1)
},0);
new Promise(function(resolve){
    console.log(2)
    forvar i=0 ; i<10000 ; i++ ){
        i==9999 && resolve()
    }
    console.log(3)
}).then(function(){
    console.log(4)
});
console.log(5);
复制代码

这的问题是,为什么答案是 2 3 5 4 1

而不是 2 3 5 1 4

就是因为所有的代码都写在script标签中,所以读取所有代码是第一个宏任务,我们开始执行第一个宏任务。

我们首先遇到setTimeout,他是第二个宏任务,将它扔进宏任务事件队列里先排队。

下来我们遇到promise,promise执行器里的代码会被同步调用,所以我们依次打印出2和3。

下来遇到promise的回调,他是一个微任务,将它扔进微任务事件对列中。

下来我们接着打印出5,然后执行微任务并且打印出4.

我们第一个宏任务执行完毕,执行下一个宏任务,打印出1,到此,所有任务都执行完毕。

所以我们最后的结果为2 3 5 4 1。

关于这方面的概念: Javascript是单线程的,所有的同步任务都会在主线程中执行。

当主线程中的任务,都执行完之后,系统会 “依次” 读取任务队列里的事件。与之相对应的异步任务进入主线程,开始执行。

异步任务之间,会存在差异,所以它们执行的优先级也会有区别。大致分为 微任务(micro task,如:Promise、MutaionObserver等)和宏任务(macro task,如:setTimeout、setInterval、I/O等)。

Promise 执行器中的代码会被同步调用,但是回调是基于微任务的。

宏任务的优先级高于微任务

每一个宏任务执行完毕都必须将当前的微任务队列清空

第一个 script 标签的代码是第一个宏任务

主线程会不断重复上面的步骤,直到执行完所有任务。

关于宏任务和微任务我在这篇文章有讲解[vue2]熬夜编写为了让你们通俗易懂的去深入理解nextTick原理

那么要把settimeout提前应该怎么做呢?

console.log('test');

setTimeout(test,2000);

function test(){
    console.log(2秒后执行我’);
}
复制代码

这种写法,可以防止settimeout最后进行

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改