JS同步执行、异步执行、及同步中的异步执行(promise和then)

时间:2022-12-21 00:54:16


首先明确两点:

1.JS 执行机制是单线程。

2.JS的Event loop是JS的执行机制,深入了解Event loop,就等于深入了解JS引擎的执行。

单线程执行带来什么问题?

        在JS执行中都是单线程执行,所以代码的执行可以说是自上而下,如果前一段的代码出现问题,就会导致下一段代码无法执行,对于用户而言就是卡死现象,所以在JS执行机制引出了异步执行操作。

那异步能解决什么呢问题,又会带来什么问题?

        异步操作能够很好的解决上面单线程执行出现的卡死现象,但是也会产生问题,比如同时对一件事情操作,不知道应该先执行那件事。

那么同步中使用异步如何实现呢?

        是通过的事件循环(Event loop)那先了解下Event loop吧。

先看一下,以下代码(1)

注意观察执行顺序

console.log("1");

setTimeout(()=>{

console.log("2");

},0);

console.log("3");

//有点JS 基础的同学会容易就知道了运行结果。

运行结果是:1、3、2

大家都知道setTimeout里的函数并没有立即执行,而是延迟一段时间,符合特定的条件才开始执行,这就是异步执行操作。

由此执行我们先了解到JS任务的执行分类为:同步任务和异步任务。

按照这种的分类方式JS的执行机制是:

首先,判断JS是同步还是异步,同步进入主线程,异步进入Event table

其次,异步任务在Event table中注册函数,当满足特定的条件,被推入Event queue

最后,同步任务进入主线程后一直执行,直到主线程空闲后,才会去Event queue中查看是否有可执行的异步任务,如果有就推入主线程中执行。

循环以上三步执行,这就是Event loop。

所以上面的例子的执行顺序是

console.log("1")是同步任务,放入主线程,

setTimeout()是异步任务,被放入Event table,0秒后被推入Event queue里,

console.log("3")是同步任务,放入主线程


当1、3任务先执行完后,主线程去Event queue(事件队列)里查看是否有可执行的函数,执行setTimeout里的函数。

​所以,上面关于Event loop就是我对JS执行机制的理解,直到遇到了下面这段代码(2)。​​​​​​​

setTimeout(()=>{

console.log("定时器开始执行");

})


new Promise(function(resolve){

console.log("准备执行for循环了");

for(var i=0;i<100;i++){

i==22&&resolve();

}

}).then(()=>console.log("执行then函数"));


console.log("代码执行完毕");

 那我们也按着上面总结的JS执行分类分析:

setTimeout 是异步任务,被放到event table

new Promise 是同步任务,被放到主进程里,直接执行打印 console.log('马上执行for循环啦')

.then里的函数是 异步任务,被放到event table

console.log('代码执行结束')是同步代码,被放到主进程里,直接执行

所以最后的执行结果是:【准备执行for循环-->代码执行完毕-->定时器开始执行-->执行then函数 】,对吗?

最后执行后发现结果并非是你想的这样,而是【准备执行for循环-->代码执行完毕-->执行then函数-->定时器开始执行】,为什么会是这样呢?难道是异步任务执行的顺序不是前后顺序,而是另有规定?事实上,按照异步同步的划分并不准确。

而准确的方式应该是:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTic

JS同步执行、异步执行、及同步中的异步执行(promise和then)

 按照上图这种分类方式:JS 的执行机制是:

  • 执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里
  • 当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完

重复以上2步骤,结合event loop(1) event loop(2) ,就是更为准确的JS执行机制了。

按照这种执行机制,去分析第二段代码

首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里

遇到 new Promise直接执行,打印"准备执行for循环"

遇到then方法,是微任务,将其放到微任务的【队列里】

打印 "代码执行完毕"

本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印"执行then函数"

到此,本轮的event loop 全部完成。

下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印"定时器开始执行"



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

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

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

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

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

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

所以最后的执行顺序就是:【准备执行for循环-->代码执行完毕-->执行then函数-->定时器开始执行】

最后再说一下setTimeout

先看一段代码

我们一般说这段代码,在3秒后执行setTimeout内的函数。

setTimeout(function(){

console.log('执行了')

},3000)

但这种说法往往是不够严谨的,准确的解释是:3秒后,setTimeout函数会被推入Event queue(事件队列),而Event queue(事件队列)内的任务,只有在主线程空闲时才会执行。

所以,只有同时满足以下条件setTimeout内的函数才能被执行

1.3秒后

2.主线程空闲时

若主线程的执行任务很多,执行时间超过3秒,比如说5秒,那么setTimeout内的函数只能在5秒后执行了 

如何区分宏任务和微任务呢?划分的标准是什么

宏任务本质:参与了事件循环的任务。

回到 Chromium 中,需要处理的消息主要分成了三类:

  • Chromium 自定义消息
  • Socket 或者文件等 IO 消息
  • UI 相关的消息

1. 与平台无关的消息,例如 setTimeout 的定时器就是属于这个

2.Chromium 的 IO 操作是基于 libevent 实现,它本身也是一个事件驱动的库

3.UI 相关的其实属于 blink 渲染引擎过来的消息,例如各种 DOM 的事件

其实与 JavaScript 的引擎无关,都是在 Chromium 实现的。

微任务本质:直接在 Javascript 引擎中的执行的,没有参与事件循环的任务。

  1. 是个内存回收的清理任务,使用过 Java 的童鞋应该都很熟悉,只是在 JavaScript 这是V8内部调用的
  2. 就是普通的回调,MutationObserver 也是这一类
  3. Callable
  4. 包括 Fullfiled 和 Rejected 也就是 Promise 的完成和失败
  5. Thenable 对象的处理任务

宏任务,微任务的优先级

promise 是在当前脚本代码执行完后,立刻执行的,它并没有参与事件循环,所以它的优先级是高于 setTimeout。

宏任务和微任务的总结:

  • 宏任务 Macrotasks 就是参与了事件循环的异步任务。
  • 微任务 Microtasks 就是没有参与事件循环的“异步”任务。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

​console.log("开始执行1")​

​console.log(Object.keys({a: 1}));​

​setTimeout(() => {​

​console.log(Object.keys({b: 2}));​

​var promise = new Promise((resolve, reject) => {​

​resolve(1);​

​});​

​promise.then(res => {​

​//后续增加测试​

​setTimeout(() => {​

​console.log(Object.keys({e:1}))​

​}, 0);​

​console.log(Object.keys({c: 1}));​

​});​

​}, 2000);​

​console.log("结束执行2")​

  

JS同步执行、异步执行、及同步中的异步执行(promise和then)

 微观任务是在当前JS调用执行完了之后立刻执行的,是同步的,在同一个调用栈里,没有多线程异步,如这里包括promise.then在内的setTimeout回调里的代码都是在DOMTimer.Fired执行的,只是说then被放到了当前要执行的整一个异步回调函数的最后面执行。所以setTimeout 0是给主线程的消息循环任务队列添加了一个新的task(回调),而promise.then是在当前task的V8里的microtask插入了一个任务。那么肯定是当前正在执行的task执行完了才执行下一个task.vue的nextTick 也是一个微观任务

除此之外的onload事件

let img = new Image();​​
​​img.src = 'image01.png?_=' + Date.now();​​
​​img.onload = function () { console.log('img ready'); } console.log(Object.keys({e: 1}));​​

微观任务是不属于事件循环的,它是V8的一个实现,用来实现Promise的then/reject,以及其它一些需要同步延后的callback,本质上它和当前的V8调用栈是同步执行的,只是放到了最后面。除了Promise/MutationObserver,在JS里面发起的请求也会创建一个微观任务延后执行。


相关文章