当一个任务进入执行栈,先判断是同步任务还是异步任务,如果是同步任务就在执行栈的主线程中进行执行,如果是异步任务就推送到任务队列中去执行,当主线程中的任务执行完毕就去读取任务队列中已经完成的异步任务的回调并将其放入执行栈中继续执行,当这个任务执行完之后再去任务队列中去找,如此循环往复就是事件循环。
JS引擎遇到一个异步事件后会将这个事件挂起,继续执行主线程中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列——事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕,主线程处于闲置状态时,主线程回去找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入到执行栈中,然后执行其中的同步代码,如此循环往复,这样就形成了一个无线的循环。这个过程就被称为事件循环(EventLoop) 的原因。
事件循环处理宏任务和微任务的逻辑如下:
- JavaScript引擎首先从宏任务队列中取出第一个任务;
- 执行完毕后,再将微任务中的所有任务取出,按照顺序全部指执行,如果在这个过程中产生了新的微任务,也就是执行微任务过程中产生新的微任务并不会推迟到下一个循环中执行,而是在当前循环中继续执行。
- 然后再从宏任务队列中取下一个,执行完毕后,再将微任务中的任务全部取出,循环往复,直到两个队列中的任务都取完。
也就是说,一次事件循环会处理一个宏任务和所有这次循环中产生的微任务。
**所有,宏任务和微任务的本质区别如下: **
- 微任务:不需要特定的异步线程去执行,没有明确的异步任务去执行,只有回调;
- 宏任务:需要特定的异步线程去执行,有明确的的异步任务去执行,有回调。
单线程JS,多线程浏览器
JS是单线程的,为什么浏览器可以同时执行异步任务?
浏览器是多线程的,当JS需要执行异步任务时,浏览器会另外启动一个线程去执行该任务。也就是说JS是单线程指的是执行JS代码的线程只有一个,也就是浏览器提供的JS引擎线程只有一个(主线程)。除此之外,浏览器中还有定时器线程、HTTP线程,这些线程不是用来执行JS代码的。
比如:主线程需要发送数据请求,就回把这个任务交给异步HTTP请求线程去执行,等请求数据返回之后,再将callback里需要执行的JS代码交给浏览器去执行。也就是说,浏览器才是真正执行发送请求这个任务的角色,而JS只是负责最后的回调处理。所以这里的异步不是JS自身实现的,而是浏览器提供的。
可以看到,Chrome不仅拥有多个进程,还有多个线程。以渲染进程为例,就包含GUI渲染线程、JS引擎线程、事件触发线程、定时器触发线程、异步HTTP请求线程。这些线程为 JS 在浏览器中完成异步任务提供了基础。
同步任务与异步任务
- 同步任务:在主线程上排队执行的任务,只有一个任务执行完才能执行下一个任务。
- 异步任务:不进入 主线程,而是放在任务队列中,若有多个异步任务则需要在队列中排队等待,任务队列类似于缓冲区,任务下一步会被移到执行栈然后主线程执行调用栈的任务。 )
执行栈
执行栈使用的是数据结构中的栈结构,是一个存储函数调用的栈结构,遵循先进后出的原则。主要负责跟踪所有要执行的代码。 每执行完一个函数就会从栈中弹出该执行完的函数;如果有代码需要进去执行的话,就进行push操作。
JavaScript在按顺序执行栈中的方法时,每次执行一个方法,都会为它生成独有的执行环境(上下文),当这个方法执行完成后,就会销毁当前的执行环境,并从栈中弹出,然后继续执行下一个方法。
任务队列
任务队列用的是数据结构中的队列结构,它用来保存异步任务,遵循先进先出的原则。主要负责将新的任务发送到队列中进行处理。
在执行JS代码时,会将同步的代码按照顺序排在执行栈中,然后依次执行里面的函数。当遇到异步任务时,就将其放入任务队列中,等待当前所有同步代码执行完成之后,就会从异步任务队列中取出已完成的异步任务的回调并将其放入执行栈中继续执行,如此循环往复,直到执行完所有任务。
当一个任务进入执行栈,先判断是同步任务还是异步任务,如果是同步任务就在执行栈的主线程中进行执行,如果是异步任务就推送到任务队列中去执行,当主线程中的任务执行完毕就去读取任务队列中已经完成的异步任务的回调并将其放入执行栈中继续执行,当这个任务执行完之后再去任务队列中去找,如此循环往复就是事件循环。
宏任务和微任务
任务队列 可以根据种类分为微任务队列(micro task)和宏任务队列(macro task)。常见的任务如下:
- 宏任务:script( 整体代码)、setTimeout、setInterval、I/O、UI交互事件、setImmediate(Node.js 环境)
- 微任务:Promise、MutaionObserver、process.nextTick(Node.js环境)。