通俗地来说, JavaScript 事件机制描述的是事件在 DOM 里面的传递顺序,以及我们可以对这些事件做出如何的响应。
javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript
是单线程这一核心仍未改变。所以一切javascript
版的"多线程"都是用单线程模拟出来的。
既然js是单线程,那就像只有一个窗口的银行,客户需要排队一个一个办理业务,同理js任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此聪明的程序员将任务分为两类:
a:同步任务: 指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
b:异步任务: 指的是不进入主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
依据微软的MSDN上的解说:
(1) 同步函数:当一个函数是同步执行时,那么当该函数被调用时不会立即返回,直到该函数所要做的事情全都做完了才返回。
(2) 异步函数:如果一个异步函数被调用时,该函数会立即返回尽管该函数规定的操作任务还没有完成。
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。关于这部分有严格的文字定义,但本文的目的是用最小的学习成本彻底弄懂执行机制,所以我们用导图来说明:
导图要表达的内容用文字来表述的话:
- 同步和异步任务在不同的执行"场所",同步的进入主线程,异步的进入Event Table执行并注册函数。
- 当指定的异步事情完成时,
Event Table
会将这个函数移入Event Queue
。- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,推入主线程执行。
- js引擎的monitoring process进程会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。上述过程会不断重复,也就是常说的Event Loop(事件循环也可以叫事件轮询)。
我们不禁要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process
进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue
那里检查是否有等待被调用的函数。
我们进入正题,除了广义的同步任务和异步任务,我们对任务有更精细的定义:
宏任务(macrotask)
进入任务栈等待主线程执行的主代码块,包括从异步队列里加入到栈的,如setTimeout()
、setInterval()
的回调,其中不含异步队列中的微任务如Promise.then
回调。
此时注意事件循环中(event loop)从异步队列加入到栈的宏任务是作为下一个事件来执行的,由于GUI渲染线程机制,每次事件循环后都会进行页面渲染,如下图:
第一次宏任务完成 → 页面渲染 → 第一次宏任务完成(包含上一次宏任务事件时,异步队列中加入的宏任务) → 页面渲染…
微任务(microtask)
是异步队列中,在当前这一次宏任务执行完后,页面渲染之前要执行的任务。
此时注意,即使当前微任务执行过程中,产生了新的微任务,也会在下一个宏任务开始执行之前且当前事件循环结束之前执行完所有的微任务。
第一次宏任务 → 第一次所有微任务 → 页面渲染 → 第二次宏任务(包含上一次宏任务事件时,异步队列中加入的宏任务)→ 第二次所有微任务 →页面渲染…
macro-task(宏任务):包括整体代码script
,setTimeout
,setInterval
micro-task(微任务):Promise
,process.nextTick
事件循环:JS 会创建一个类似于
while (true)
的循环,每执行一次循环体的过程称之为Tick。每次Tick的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中,也就是每次Tick会查看任务队列中是否有需要执行的任务。
主线程:JS只有一个线程,称之为主线程。而事件循环是主线程中执行栈里的代码执行完毕之后,才开始执行的。所以,主线程中要执行的代码时间过长,会阻塞事件循环的执行,也就会阻塞异步操作的执行。只有当主线程中执行栈为空的时候(即同步代码执行完后),才会进行事件循环来观察要执行的事件回调,当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行。
不同类型的任务会进入对应的Event Queue
,比如setTimeout
和setInterval
会进入相同的Event Queue
。
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,我们用文章最开始的一段代码说明:
setTimeout(function() { console.log('setTimeout'); }) new Promise(function(resolve) { console.log('promise'); }).then(function() { console.log('then'); }) console.log('console');
setTimeout
,那么将其回调函数注册后分发到宏任务Event Queue
。(注册过程与上同,下文不再描述)Promise
,new Promise
立即执行,then
函数分发到微任务Event Queue
。console.log()
,立即执行。script
作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue
里面,执行。Event Queue
开始。我们发现了宏任务Event Queue
中setTimeout
对应的回调函数,立即执行。 结束。1.主体代码(第一次事件循环开始,所有的script代码)作为宏任务进入任务执行栈,但在主线程执行之前要做一系列操作判断。
2.判断当前任务是同步还是异步,同步的由主线程在任务栈中按先进后出顺序(先局部上下文,再全局上下文)执行,异步判断是宏任务还是微任务。
3.异步中的宏任务放入异步的宏任务event Table(异步队列分两种,宏任务队列和微任务队列,event Table也一样),微任务进入微任务event Table,在回调函数注册之后,再次进入它们对应的队列。
4.当主线程的任务执行完后,会检查微任务队列是否有任务,如果有就执行,如此循环,知道微任务队列没有任务。
5.当前事件的微任务执行完后,开始执行下一次事件,即会执行宏任务队列中的宏任务,如此循环下去,直到没有任务。
我们来分析一段较复杂的代码,看看你是否真的掌握了js的执行机制:
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) })
JavaScript 执行机制:
主线程任务——>微任务——>宏任务 如果宏任务里还有微任就继续执行宏任务里的微任务,如果宏任务中的微任务中还有宏任务就在依次进行
主线程任务——>微任务——>宏任务——>宏任务里的微任务——>宏任务里的微任务中的宏任务——>知道任务全部完成
我的理解是在同级下,微任务要优先于宏任务执行
什么是进程
我们都知道,CPU
是计算机的核心,承担所有的计算任务
官网说法,进程
是CPU
资源分配的最小单位
字面意思就是进行中的程序,我将它理解为一个可以独立运行且拥有自己的资源空间的任务程序
进程
包括运行中的程序和程序所使用到的内存和系统资源
CPU
可以有很多进程,我们的电脑每打开一个软件就会产生一个或多个进程
,为什么电脑运行的软件多就会卡,是因为CPU
给每个进程
分配资源空间,但是一个CPU
一共就那么多资源,分出去越多,越卡,每个进程
之间是相互独立的,CPU
在运行一个进程
时,其他的进程处于非运行状态,CPU
使用 时间片轮转调度算法 来实现同时运行多个进程
什么是线程
线程
是CPU
调度的最小单位
线程
是建立在进程
的基础上的一次程序运行单位,通俗点解释线程
就是程序中的一个执行流,一个进程
可以有多个线程
一个进程
中只有一个执行流称作单线程
,即程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行
一个进程
中有多个执行流称作多线程
,即在一个程序中可以同时运行多个不同的线程
来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程
来完成各自的任务
进程和线程的区别
进程是操作系统分配资源的最小单位,线程是程序执行的最小单位
一个进程由一个或多个线程组成,线程可以理解为是一个进程中代码的不同执行路线
进程之间相互独立,但同一进程下的各个线程间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)
调度和切换:线程上下文切换比进程上下文切换要快得多
多进程和多线程
多进程:多进程指的是在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的,比如大家可以在网易云听歌的同时打开编辑器敲代码,编辑器和网易云的进程之间不会相互干扰
多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务
本文借鉴了多位博主的总结出来的笔记,以下是他们的文章地址:
矢臻镶博客链接:https://blog.csdn.net/weixin_45888701/article/details/116781078
雨生百谷,方为谷雨博客链接:https://blog.csdn.net/weixin_44058725/article/details/107272969