JS中的Event Loop(事件循环)机制及Promise、async、await执行顺序

JS是一门单线程非阻塞的脚本语言,也就是说,所有的任务都是串行执行,同一时间只能做一件事。但当我们做一些比如:网络请求、定时器、事件监听等一些比较耗时的任务时,如果也让JS的单线程来做,那不仅仅执行效率低,页面也会出现卡死现象,用户体验会非常差。那如何解决呢,用事件队列。这也是为什么说,它是非阻塞的

虽然JS是单线程的,但JS的宿主环境(浏览器)是多线程的,浏览器会为这些耗时的任务开辟另外的线程,主要包括:Http请求线程、浏览器定时触发器、浏览器事件触发线程,来异步执行

说明:

  • Ajax,由浏览器内核的Network模块处理,网络请求返回后,回调函数添加到任务队列中
  • setTimeout等,由浏览器内核的Timer模块处理,时间到达时,回调函数添加到任务队列中
  • 类似onclick等,由浏览器内核的DOM binding模块处理,事件触发时,回调函数添加到任务队列中

所有的同步任务都会在JS线程上执行,函数的执行是通过进栈和出栈实现的,当我们调用 一个函数时,JS会生成一个与这个函数对应的执行环境(context),又叫执行上下文,这个执行 环境中存在着这个方法的私有作用域、上层作用域的指向、方法的参数、这个作用域中定义的变量和this对象,当调用的函数内又调用了其他函数时,因为JS是单线程的,同一时间只能执行一个函数,所以这些函数被排队在一个单独的地方,这个地方就是执行栈

当一个脚本第一次执行的时候,JS引擎会将整段JS代码加入到执行栈,然后从头开始执行,当执行到一个函数,就会向执行栈中添加这个函数的执行环境,然后进入这个执行环境执行其中的代码,执行完毕后,JS会退出这个执行环境并把这个执行环境销毁,回到上一个执行环境,当遇到异步事件时,JS引擎会将这个事件挂起,执行栈继续执行,当这个异步事件返回结果后,会将这个事件加入到与当前执行栈不同的一个队列,我们称之为事件队列,被放入的事件不会立即执行其回调,而是要等到当前执行栈中所有的任务都执行完毕,JS线程处于闲置状态时,主线程才会去查找事件队列是否有任务,如果有,那么JS线程就会从中取出排在第一位的事件,并把这个事件对应的回调放入到执行栈中,然后执行其中的同步代码,如此反复,这样就形成了一个无限的循环,我们称之为事件循环

JS中的Event Loop(事件循环)机制及Promise、async、await执行顺序_第1张图片

任务队列

实际上异步任务之间并不相同,因此他们之间也有优先级之分,所以任务队列被分成两种类型:
microtask queue 微任务队列,macrotask queue 宏任务队列

属于宏任务:

  • setTimeout
  • setInterval
  • Ajax
  • DOM事件
  • I/O
  • script标签中的整体代码

属于微任务:

  • new Promise()
  • new MutaionObserver()

一次事件循环流程,JS首先从宏任务队列中取出第一个宏任务加入到执行栈开始执行,如果在执行过程中又产生了宏任务,那么这个任务将在下次事件循环中才能执行,如果在执行过程中产生了微任务,那么当执行栈为空时,就会取出微任务队列中的全部任务,放入执行栈执行,如果在执行微任务时又产生了宏任务,则也要在下下次事件循环中才能执行,如果又产生了微任务,那么这个微任务将会在这次事件循环中执行,如此反复,就形成了事件循环

可以简单的理解一次事件循环流程:取出一个宏任务开始->执行栈执行->执行栈为空->微任务队列执行->微任务队列为空->完成

示例说明(测试浏览器为Chrome 版本75)

    console.log("start");

    setTimeout(function () {
      console.log("setTimeout1");
      new Promise(function (resolve) {
        console.log("setTimeout1-Promise-start");
        resolve("setTimeout1-Promise");
      }).then(value => {
        console.log(value);
        console.log("Promise1-then1");
        return value;
      }).then(value => {
        console.log(value);
      })
    }, 0);

    new Promise(function (resolve) {

      console.log("Promise1-start");
      resolve("Promise1-resolve");
      console.log("Promise1-end");

    }).then(value => {
      console.log(value);
      console.log("Promise1-then1");
      return value;
    }).then(value => {
      console.log(value);
      console.log("Promise1-then2");

      Promise.resolve("Promise2")
        .then(value1 => {
          console.log(value1);
          console.log("Promise2-then1");
          return value1;
        }).then(value1 => {
        console.log(value1);
        console.log("Promise2-then2");
      });
    });

    setTimeout(function () {
      console.log("setTimeout2")
      new Promise(function (resolve) {
        console.log("setTimeout2-Promise-start");
        resolve("setTimeout2-Promise");
      }).then(value => {
        console.log(value);
      }).then(value => {
        console.log(value);
      })
    }, 0);
    console.log("end");

打印输出为:

	start
	Promise1-start
	Promise1-end
	end
	
	Promise1-resolve
	Promise1-then1
	Promise1-resolve
	Promise1-then2
	Promise2
	Promise2-then1
	Promise2
	Promise2-then2
	
	setTimeout1
	setTimeout1-Promise-start
	setTimeout1-Promise
	Promise1-then1
	setTimeout1-Promise
	
	setTimeout2
	setTimeout2-Promise-start
	setTimeout2-Promise
	undefined

注意:

  • Promise对象当创建完之后会立即执行
  • Promise的 then 方法会返回一个Promise对象,因此可以继续链式调用then方法,再次调用then方法中传递过来的参数是上个then方法return的内容,如果上个then没有return则参数为undefined

执行流程:

第一轮事件循环:
1:JS线程读取整段JS代码作为一个macrotask宏任务先被执行,形成相应的堆和执行栈
2:遇到console.log(“start”),输出start
3:遇到第一个setTimeout,交给浏览器异步模块处理,时间到达时,将回调函数作为macrotask宏任务,添加到macrotask queue宏任务队列
4:遇到Promise对象,立即执行,遇到内部的resolve,将回调函数作为microtask微任务,添加到microtask queue微任务队列
5:遇到第二个setTimeout,交给浏览器异步模块处理,时间到达时,将回调函数作为macrotask宏任务,添加到macrotask queue宏任务队列
6:遇到console.log(“end”),输出end
7:从macrotask queue宏任务队列中删除执行过的macrotask宏任务
8:执行栈为空,取出microtask queue微任务队列中的全部任务,放入执行栈执行
9:执行微任务队列中Promise的回调函数,在回调函数中又有新的微任务产生了,同时也一并执行
10:执行栈为空,microtask queue微任务队列为空,开始下一轮事件循环

第一轮console打印内容:

	start
	Promise1-start
	Promise1-end
	end
	
	Promise1-resolve
	Promise1-then1
	Promise1-resolve
	Promise1-then2
	Promise2
	Promise2-then1
	Promise2
	Promise2-then2

第二轮事件循环:
1:从宏任务队列中取出排在队头的任务,也就是第一轮中第三步的回调函数,将任务放到执行栈执行,输出setTimeout1,
2:在第一步的回调函数中遇到Promise,立即执行,遇到内部的resolve,将回调函数作为microtask微任务,添加到microtask queue微任务队列
3:第一步的宏任务执行完毕,此时执行栈为空,
4:从macrotask queue宏任务队列中删除执行过的macrotask宏任务
5:取出microtask queue微任务队列中的全部任务,放入执行栈执行
6:执行微任务队列中Promise的回调函数,在回调函数中又有新的微任务产生了,同时也一并执行
7:执行栈为空,microtask queue微任务队列为空,开始下一轮事件循环

第二轮console打印内容:

setTimeout1
setTimeout1-Promise-start
setTimeout1-Promise
Promise1-then1
setTimeout1-Promise

第三轮事件循环:
1:从宏任务队列中取出排在队头的任务,也就是第一轮中第五步的回调函数,将任务放到执行栈执行,输出setTimeout2,
2:在第一步的回调函数中遇到Promise,立即执行,遇到内部的resolve,将回调函数作为microtask微任务,添加到microtask queue微任务队列
3:第一步的宏任务执行完毕,此时执行栈为空,
4:从macrotask queue宏任务队列中删除执行过的macrotask宏任务
5:取出microtask queue微任务队列中的全部任务,放入执行栈执行
6:执行微任务队列中Promise的回调函数,在回调函数中又有新的微任务产生了,同时也一并执行
7:执行栈为空,microtask queue微任务队列为空,开始下一轮事件循环

第三轮console打印内容:

setTimeout2
setTimeout2-Promise-start
setTimeout2-Promise
undefined

总结:

1:JS事件循环总是从一个宏任务开始执行
2:一个事件循环过程中,只执行一个宏任务,但可执行多个微任务
3:执行栈中的任务产生的微任务会在当前事件循环内执行
4:执行栈中的任务产生的宏任务要在下一次事件循环才会执行

Promise说明

Promise出现主要是解决回调地狱的问题,让代码看起来更优雅,更易维护,但Promise一旦状态改变,就不会再变,状态会被凝固

Promise使用

	new Promise(resolve => {
	   console.log("promise-start");
	    resolve("resolve");
	    console.log("promise-end");
	  }).then(value => {
	    console.log(value);
	    return "then";
	  }).then(value => {
	    console.log(value);
	    throw new Error("error");
	  }).catch(result => {
	    console.log(result);
	  })

打印输出:

	promise-start
	promise-end
	resolve
	then
	Error: error

Promise.resolve说明

  • 如果传入的是普通对象:将普通对象转化为Promise对象,并调用Promise的resolve方法
  • 如果传入的是Promise对象:则直接返回当前对象,并不会调用resolve方法

Promise.resolve使用

传入普通对象

	let p = Promise.resolve("promise");
	console.log(p);
	p.then(value => {
	    console.log(value)
	  }
	);

打印输出:

	Promise {: "promise"}
	promise

传入Promise对象

	let promise = new Promise(function (resolve) {
	  console.log("promise");
	});
	
	let p = Promise.resolve(promise);
	console.log(p);
	console.log(promise===p);
	p.then(value => {
	  //没有调用resolve方法所以不会打印东西
	  console.log(value);
	})

打印输出:

	promise
	Promise {}
	true

async await说明

async:申明一个函数是异步的,返回是一个promise对象,但必须要等到内部所有await后面的表达式都执行完,状态才会发生改变
await:等待的意思,必须出现在async修饰的函数中,等待的可以是promise对象也可以是其他值,但如果是其他值,则会被转成一个立即resolve的Promise对象,await实际上是一个让出线程的标志,首先await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面JS执行栈的代码,等本轮事件循环 执行完以后再跳回到async函数中等待await。

个人理解

  • 如果await返回值为promise则会等到本轮事件循环后,再把promise回调函数放入到微任务队列中并在本轮事件循环中执行
  • 如果await返回非promise对象,则会立即转成Promise对象并调用resolve方法,并把回调函数放入到微任务队列中并在本轮事件循环中执行

返回promise对象和非promise对象的区别是:
返回promise对象会把回调函数放到微任务队列的末尾,返回非promise对象,会马上把回调函数放到微任务队列中去。

注意:
await后面可以跟着promise对象,但不必写then方法,可直接拿到返回值

async使用

1:如果在async函数中return一个直接量,async会把这个直接量通过Promise.resolve封装成Promise对象,可通过then方法获取到这个值

	async function testAsync() {
	  return "hello async";
	}
	
	let result = testAsync();
	
	console.log(result);
	
	result.then(value => {
	  console.log(value);
	})

打印结果:

	Promise {: "hello async"}
	hello async

2:如果在async函数中return一个Promise.resolve,async会重新封装一个Promise对象并返回,也可通过then方法拿到Promise.resolve中的值

	let promise =  Promise.resolve("promise");
	 console.log(promise);
	 async function testAsync() {
	   return promise;
	 }
	
	 let result = testAsync();
	 console.log(result);
	 console.log(result === promise);
	 result.then(value => {
	   console.log(value);
	 })

打印结果:

	Promise {: "promise"}
	Promise {}
	false
	promise

3:如果在async函数中return一个new Promise,async也会重新封装一个Promise对象并返回,也可通过then方法拿到new Promise中resolve中的值

    let promise = new Promise(resolve => {
      resolve("promise");
    });
    console.log(promise);

    async function testAsync() {
      return promise;
    }

    let result = testAsync();
    console.log(result);
    console.log(result === promise);
    result.then(value => {
      console.log(value);
    })

打印结果:

	Promise {: "promise"}
	Promise {}
	false
	promise

4:如果在async函数中没有返回值,async也会通过Promise.resolve封装成Promise对象返回,只是通过then方法获取的值为undefined

    async function testAsync() {
      console.log("hello async");
    }

    let result = testAsync();
    console.log(result);
    result.then(value => {
      console.log(value);
    })

打印结果:

	hello async
	Promise {: undefined}
	undefined

async await示例说明

	console.log("script start");
	
	new Promise(function (resolve) {
	  console.log("promise1");
	  resolve();
	}).then(function () {
	  console.log("promise1-then");
	});
	
	setTimeout(function () {
	  console.log("settimeout1");
	}, 0);
	
	async function async1() {
	  console.log("async1 start");
	  let resulet = await async2();
	  console.log(resulet);
	  console.log("async1 end");
	}
	
	async function async2() {
	  console.log('async2');
	  return "async2 return"
	}
	
	setTimeout(function () {
	  console.log("settimeout2");
	}, 0);
	
	async1();
	
	new Promise(function (resolve) {
	  console.log("promise2");
	  resolve();
	}).then(function () {
	  console.log("promise2-then");
	});
	
	console.log('script end');

打印输出:

	script start
	promise1
	async1 start
	async2
	promise2
	script end
	
	promise1-then
	async2 return
	async1 end
	promise2-then
	
	settimeout1
	settimeout2

Promise对象和async修饰的函数都是立即执行函数,所以上述代码放到执行栈中,首先打印出的是

	script start
	promise1
	async1 start
	async2
	promise2
	script end

因为async2由async修饰,所以是一个异步函数,而内部返回了一个直接量,所以会被Promise封装,并立即调用resolve方法,所以此时也会马上放到微任务队列中,紧接着就是Promise2中的resolve也会放到微任务队列,所以打印的顺序是

	promise1-then
	async2 return
	async1 end
	promise2-then

微任务执行完后再打印宏任务中的

    settimeout1
    settimeout2

如果把async2改成通过返回new Promise对象

	async function async2() {
	   console.log('async2');
	   return new Promise(resolve => resolve("async2 return"))
	 }

则结果会是:

	promise1-then
	promise2-then
	async2 return
	async1 end

首先记住这个结论:如果在async中return了通过new的Promise对象,则这个里面的任务会排在微任务队列的末尾

我个人理解:
因为当async2函数内返回promise对象时,会先把promise对象放到微任务队列(只有对象内的resolve没有执行),等执行promise时,其实就是执行resolve,然后又放到微任务队列,所以返回promise对象的时候,它的任务会排在微任务队列末尾

参考内容-感谢感谢

https://www.cnblogs.com/cangqinglang/p/8967268.html
https://segmentfault.com/a/1190000015112913
https://www.cnblogs.com/hity-tt/p/6733062.html
https://segmentfault.com/a/1190000015057278?utm_medium=referral&utm_source=tuicool

你可能感兴趣的:(前端)