异步与EventLoop

https://www.youtube.com/watch?v=8aGhZQkoFbQ&t=225s
http://nodejs.cn/learn/the-nodejs-event-loop

为什么JavaScript是一个单线程的语言

因为JavaScript有且只有一个调用栈(Call Stack),程序每次只可以运行一段代码
one thread===one call stack===one thing at a time

JS单线程的特性,与它的用途有关,作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

下面有一段代码,来告诉大家在js代码在浏览器中是如何运行的

  1. // step 4
  2. function multiply(a,b){
  3. let result = a*b;
  4. return result;
  5. }
  6. // step 3
  7. function square(n){
  8. let result = multiply(n,n);
  9. return result;
  10. }
  11. // step 2
  12. function printSquare(n){
  13. let squared = square(n);
  14. console.log(squared);
  15. }
  16. // step 1
  17. printSquare(4);

我们从上而下声明了三个方法,然后在18行进行调用了printSquare。

这时代码会从step1—> step2—> step 3—>step 4执行,按照这个顺序,所有的function的会被压入Call Stack(调用栈)中,然后会从Step4—> step3—>step2—> step1,一层一层退出来,当一个function执行完会被从Call Stack中POP出来。
这就是整个代码的执行过程,可以通过浏览器的开发者工具观察Call Stack的执行过程

什么是EventLoop


这段我们讲讲什么叫做EventLoop(事件循环),我们在实际开发中会碰到好多代码执行顺序的问题,那到底是为什么产生了这种代码的执行顺序不一致的问题,答案就是eventloop,哈哈哈哈哈哈哈。

直接上代码

  1. function loopEvent(){
  2. let a: number = 1;
  3. setTimeout(function(){
  4. console.log('a的值:',a);
  5. },5000);
  6. let b: number = 2;
  7. console.log('b的值:',b);
  8. }
  9. loopEvent();

这段代码显然输出的先后顺序是 ‘b的值: 2’ —> ‘a的值: 1’,因为它设置了5秒的延迟执行
下面我们再看一段代码

  1. function loopEvent(){
  2. let a: number = 1;
  3. setTimeout(function(){
  4. console.log('a的值:',a);
  5. },0);
  6. let b: number = 2;
  7. console.log('b的值:',b);
  8. }
  9. loopEvent();

这段代码将延迟改为0,那么它的执行顺序是什么? 最后它输出还是’b的值: 2’ —> ‘a的值: 1’,这就是要说到的eventloop.设置0秒的延迟,但是代码不会执行,而是要等到console.log(‘b的值:’,b)执行完才会执行

我们来看图
eventloop_1.png

上图有Call Stack,Web API,CallBack Queue三部分。
CallStack是代码的调用栈,Web API 是浏览器自身的API,CallBack Queue则是回调函数的队列。

从上面的代码说起,当loopEvent的方法被压入CallStack中,遇到setTimeout时,回调函数会在web API中等待0秒钟,此时它会被加到CallBack Queue的队列中,等待整个方法执行完时,CallBack Queue中队列中的第一个回调会被压入Call Stack调用栈中,进行执行。整个的过程就是这样,当在方法中碰到多个setTimeout时,会按等待的时间推到队列中,而对列会等整个方法执行完,将对列中第一个回调函数压入执行栈,依次类推,实现事件循环。

宏任务与微任务

一个任务就是指计划由标准机制来执行的任何 JavaScript,如程序的初始化、事件触发的回调等。 除了使用事件,你还可以使用 setTimeout() 或者 setInterval() 来添加任务
任务队列和微任务队列的区别很简单,但却很重要:

  • 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行.
  • 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。

ES6 规范中,microtask 称为 jobs,macrotask 称为 task

宏任务是由宿主发起的,而微任务由JavaScript自身发起。

在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。

image.png

  1. setTimeout(() => {
  2. console.log('setTimeout');
  3. },0);
  4. let p = new Promise((resolve,reject) => {
  5. console.log('promise');
  6. // setTimeout(() => resolve(), 100);
  7. resolve();
  8. });
  9. p.then(() => {
  10. console.log('resolve');
  11. });
  12. console.log('主线程');

Promise

Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。
一个 Promise 对象代表一个在这个 promise 被创建出来时不一定已知的值。它让您能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。 这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。

其实就是控制反转再反转。

一个 Promise 必然处于以下几种状态之一:

  • 待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
  • 已兑现(fulfilled): 意味着操作成功完成。
  • 已拒绝(rejected): 意味着操作失败。

  • pending 指初始等待状态,初始化 promise 时的状态

  • resolve 指已经解决,将 promise 状态设置为fulfilled
  • reject 指拒绝处理,将 promise 状态设置为rejected

待定状态的 Promise 对象要么会通过一个值被兑现(fulfilled),要么会通过一个原因(错误)被拒绝(rejected)。当这些情况之一发生时,我们用 promise 的 then 方法排列起来的相关处理程序就会被调用。如果 promise 在一个相应的处理程序被绑定时就已经被兑现或被拒绝了,那么这个处理程序就会被调用,因此在完成异步操作和绑定处理方法之间不会存在竞争状态。

因为 Promise.prototype.thenPromise.prototype.catch 方法返回的是 promise, 所以它们可以被链式调用。

image.png

构造函数

Promise()
创建一个新的 Promise 对象。该构造函数主要用于包装还没有添加 promise 支持的函数。

  1. let promise = new Promise((resolve,reject) => {
  2. console.log('这是一个promise');
  3. resolve();
  4. });

静态方法

Promise.all(iterable)
这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。这个新的promise对象在触发成功状态以后,会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致;如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常被用于处理多个promise对象的状态集合。

  1. let p1 = new Promise((resolve,reject) => {
  2. setTimeout(() => {
  3. resolve('p1请求完成');
  4. },1000);
  5. });
  6. let p2 = new Promise((resolve,reject) => {
  7. setTimeout(() => {
  8. resolve('p2请求完成');
  9. },2000);
  10. });
  11. Promise.all([p1,p2]).then((val) => {
  12. console.log(val); // ["p1请求完成", "p2请求完成"]
  13. });

Promise.allSettled(iterable)
等到所有promises都已敲定(settled)(每个promise都已兑现(fulfilled)或已拒绝(rejected))。
返回一个promise,该promise在所有promise完成后完成。并带有一个对象数组,每个对象对应每个promise的结果。

  1. let p1 = new Promise((resolve,reject) => {
  2. setTimeout(() => {
  3. resolve('p1请求完成');
  4. },1000);
  5. });
  6. let p2 = new Promise((resolve,reject) => {
  7. setTimeout(() => {
  8. reject('报错');
  9. },2000);
  10. });
  11. Promise.allSettled([p1,p2]).then((val) => {
  12. console.log(val);
  13. });
  14. [
  15. { status: 'fulfilled', value: 'p1请求完成' },
  16. { status: 'rejected', reason: '报错' }
  17. ]

Promise.any(iterable)
接收一个Promise对象的集合,当其中的一个 promise 成功,就返回那个成功的promise的值。

Promise.race(iterable)
当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。
—-可以用来做请求超时处理

  1. let p1 = new Promise((resolve,reject) => {
  2. setTimeout(() => {
  3. resolve('p1请求完成');
  4. },3000);
  5. });
  6. let p2 = new Promise((resolve,reject) => {
  7. setTimeout(() => {
  8. reject('请求超时');
  9. },1000);
  10. });
  11. Promise.race([p1,p2]).then((val) => {
  12. console.log(val);
  13. });

Promise.reject(reason)
返回一个状态为失败的Promise对象,并将给定的失败信息传递给对应的处理方法。

Promise.resolve(value)
返回一个状态由给定value决定的Promise对象。如果这个值是一个 promise ,那么将返回这个 promise ;如果该值是thenable(即,带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定;否则的话(该value为空,基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled,并且将该value传递给对应的then方法。通常而言,如果您不知道一个值是否是Promise对象,使用Promise.resolve(value) 来返回一个Promise对象,这样就能将该value以Promise对象形式使用。

  1. let thenable = {
  2. then: (resolve,reject) => {
  3. resolve('这是个thenable对象');
  4. }
  5. };
  6. Promise.resolve(thenable).then(val => console.log(val));

方法

Promise.prototype.catch(onRejected)
添加一个拒绝(rejection) 回调到当前 promise, 返回一个新的promise。当这个回调函数被调用,新 promise 将以它的返回值来resolve,否则如果当前promise 进入fulfilled状态,则以当前promise的完成结果作为新promise的完成结果.

Promise.prototype.then(onFulfilled, onRejected)
添加解决(fulfillment)和拒绝(rejection)回调到当前 promise, 返回一个新的 promise, 将以回调的返回值来resolve.

Promise.prototype.finally(onFinally)
添加一个事件处理回调于当前promise对象,并且在原promise对象解析完毕后,返回一个新的promise对象。回调会在当前promise运行完毕后被调用,无论当前promise的状态是完成(fulfilled)还是失败(rejected)

async和await语法糖

async function 用来定义一个返回AsyncFunction 对象的异步函数。异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的 [Promise](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise) 返回其结果。如果你在代码中使用了异步函数,就会发现它的语法和结构会更像是标准的同步函数。

返回的[Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise)对象会运行执行(resolve)异步函数的返回结果,或者运行拒绝(reject)——如果异步函数抛出异常的话。

异步函数可以包含[await](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/await)指令,该指令会暂停异步函数的执行,并等待Promise执行,然后继续执行异步函数,并返回结果。
记住,await 关键字只在异步函数内有效。如果你在异步函数外使用它,会抛出语法错误。

async/await的目的是简化使用多个 promise 时的同步行为,并对一组 Promises执行某些操作。
async/await 本质还是promise,只是更简洁的语法糖书写。
async/await 使用更清晰的promise来替换 promise.then/catch 的方式。

async

函数前加上async,函数将返回promise,我们就可以像使用标准Promise一样使用了。

  1. async function test(){
  2. return '这是个Promise';
  3. }
  4. let val = test();
  5. console.log(val);
  6. val.then(val => console.log('resolve' + val));

await

使用 await 关键词后会等待promise 完

  • await 后面一般是promise,如果不是直接返回
  • await 必须放在 async 定义的函数中使用
  • await 用于替代 then 使编码更优雅
  1. async function test() {
  2. const promise = new Promise((resolve, reject) => {
  3. setTimeout(() => {
  4. resolve("返回then");
  5. }, 2000);
  6. });
  7. let result = await promise;
  8. console.log(result);
  9. }
  10. test();