引入
事件循环
是JavaScript执行上下文中的一种机制,用于处理异步操作
。它的核心思想是将所有的异步任务放入一个队列中,然后按照队列中的顺序依次执行,直到队列为空为止。主线程从任务队列
中读取事件,这个过程是循环不断
的,所以整个的这种运行机制称为事件循环(Event Loop) 。下文将一步步引导,让您明白这JS事件循环机制!
进程和线程
进程
:进程是一个内存中正在运行的应用程序,是cpu资源分配的最小单位。每个进程都有自己独立的内存空间和执行环境,可以同时运行多个进程,例如:电脑中打开的任务管理器里面可以看到一个个进程,如下图:
线程
:线程是cpu调度的最小单位,它是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程。线程共享进程的内存空间和资源,但是每个线程有自己的栈空间和程序计数器。
以音乐播放软件为例,一个进程可能负责整个软件的运行,而其中的线程则分别负责播放音乐、下载音乐、处理用户界面等不同的任务。这些线程在进程的管理下,协同工作,共同完成音乐播放软件的功能。
单线程
:同一个时间只能运行一个任务,就意味着所有的任务都需要排队,前一个任务结束,才会执行后一个任务
栈
:是一种后进先出
的数据结构,数据元素在插入(即进栈)和删除(即出栈)时均从栈顶
进行操作。
队列
:是一种先进先出
的数据结构,数据元素在队尾插入,从队首删除的。
任务队列
:是一个事件的队列,只要指定过回调函数,这些事件发生时就会进入任务队列,等待主线程读取。主线程读取任务队列,就是读取里面有哪些事件。
回调函数(callback)
:就是那些会被主线程挂起来的代码,异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
JS是单线程语言
原因
JS作为浏览器脚本语言,主要是用于与用户互动
以及操作DOM
。这就决定了JS只能是单线程的,否则会带来很多复杂的同步问题。比如JS有两个线程,一个线程在DOM节点上添加内容,一个线程在DOM节点上删除内容,那浏览器应该执行哪一个线程的任务呢?
问题
单线程只能一个个排着队执行,当执行以下代码会导致阻塞:while死循环,不会弹出hello
while(1){} alert('hello');
解决
上述出现的问题,可知:如果前一个任务耗时很长,后一个任务就得一直等着,这样就会造成线程堵塞。
这时javascript开发人员意识到,为了不影响主线程正常运行,就把那些耗时的任务挂起来,依次放进一个任务队列,等主线程的任务执行完毕后,再回来去继续执行队列中的任务。
同步任务和异步任务
按照以上的方法,可以将执行任务可以分为两种:
同步任务
:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。异步任务
:不进入主线程,进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。宏任务
:需要相对较长时间才能完成的任务,如:setTimeout回调函数
、setInterval回调函数
、I/O
、AJAX
、DOM事件
微任务
:较短时间内完成的任务,如:Promise.then(回调)
、async/await
、nextTick
- 注:在JavaScript中,当创建一个新的
Promise
时,传递一个函数(称为executor函数
)给Promise构造函数。这个executor函数会立即同步执行,并且在这个函数内部执行的任何代码都是同步
的。
补充:
- 当宏任务和微任务都处于任务队列中时,
微任务的优先级大于宏任务
,即先将微任务执行完,再执行宏任务;- 当执行一个宏任务时,如果宏任务中产生了新的微任务,
这些微任务不会立刻执行
,而是会被放入到当前微任务队列中
,在当前宏任务执行完毕后被依次执行。- 执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是
在当前的循环中继续执行
。
JS事件循环机制
事件循环的工作原理
步骤大致如下:
1.先执行同步代码
,所有同步代码都在主线程上执行,形成一个执行栈。
2.当遇到异步任务时,会将其挂起并添加到任务队列中,宏任务放入宏任务队列,微任务放进微任务队列
,只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
3.一旦执行栈中的所有同步任务执行完毕
(执行栈为空时),系统就会读取任务队列
,看看里面有哪些事件。那些对应的异步任务就会结束等待状态,进入执行栈,开始执行
。
4.重复上述步骤,直到任务队列为空
。
注:主线程的读取过程基本上是自动的,只要执行栈一清空,”任务队列”上第一位的事件就自动进入主线程。但是,由于存在后文提到的”定时器”功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
练习
输出1
console.log('1')
setTimeout(function (){
console.log('2')
}, 1000)
console.log('3')
输出结果:1 3 2
setTimeout回调进入异步任务中的宏任务,从上到下先执行同步任务,再执行宏任务。
输出2
console.log('1')
setTimeout(function callback(){
console.log('2')
}, 1000)
new Promise((resolve, reject) => {
console.log('3')
resolve()
})
.then(res => {
console.log('4');
})
console.log('5')
输出结果:1 3 5 4 2
创建一个新的Promise时,传递的
executor函数
给Promise
构造函数。这个executor函数会立即同步执行,并且在这个函数内部执行的任何代码(如console.log('3')
)都是同步的。
输出3
setTimeout( () => {
console.log('1')
Promise.resolve().then( () => {
console.log('2')
})
},0)
new Promise((resolve, reject) => {
console.log('3')
resolve()
})
.then(res => {
console.log('4');
})
console.log('5')
输出结果:3 5 4 1 2
解析:
(1)首先遇到定时器,将定时器的回调函数放入任务队列中的宏任务,其中宏任务中包括同步任务console.log('1')
和微任务then回调函数
(2)创建一个新的Promise,传递的executor函数给Promise构造函数。executor函数立即同步执行,即
console.log('3')
是同步的,即先输出3,再将then回调函数放入微任务队列(3)执行同步代码,输出5
(4)主线程执行栈为空,微任务列和宏任务列非空,执行微任务列,输出4
(5)微任务为空,执行宏任务列,先输出同步任务1,再输出微任务2
输出4
setTimeout(function(){
console.log('定时器setTimeout')
},0);
new Promise(function(resolve){
console.log('同步任务1');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('Promise.then')
});
this.$nextTick(() => {
console.log('nextTick')
})
console.log('同步任务2');
输出结果
: 同步任务1 => 同步任务2 => nextTick => Promise.then => 定时器setTimeout
提示及注意
:(1)遇到
nextTick
,把它放到微任务队列
中。在同步代码执行完成后,不管其他异步任务,优先执行 nextTick。因为nextTick函数是 Node.js 环境提供的一个特殊函数, nextTick 函数中的回调函数将在当前事件循环中的其他微任务之前执行。所以
由于触发时机不同
,nextTick函数中的微任务会优先其它微任务执行(2)在浏览器中 setTimeout 的延时设置为 0 的话,会默认为 4ms,NodeJS 为 1ms;具体值可能不固定,但不是为 0。
输出5
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log('异步任务', i);
}, 1000);
}
console.log('同步任务', i);
输出结果
:第一个5直接输出,1秒后,输出5 5 5 5 5
解析
:(1)当 i = 0,1,2,3,4都满足条件,进入循环,遇到setTimeout是异步操作,依次将定时器的回调函数放入任务队列的宏任务;
(2)当 i = 5 时,不满足条件,for循环结束。 console.log(‘同步任务’, i)入栈,此时i值被累加到了5。输出5;
(3)1s过去后,主线程去执行任务队列中的函数,5个function依次入栈执行之后再出栈,此时的i已经变成了5。因此几乎同时输出5个5;
因此输出结果就是一个5直接输出,1s之后,输出5个5。
如果想要的结果是 5 0 1 2 3 4,需要再改下代码:上面的代码使用var定义变量,当定时器触发时,for循环已经完成,变量i的值已经变成了5,为了解决这个问题,可以使用块级作用域
来捕获每次循环的值。
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log('异步任务', i);
}, 1000);
}
console.log('同步任务', i);
输出结果:第一个5直接输出,1秒后,输出0 1 2 3 4
将 var 改为 let 后,我们就可以得到期望的输出结果 0、1、2、3、4。
这是因为 let 声明的变量具有块级作用域,每次迭代都会创建一个新的 i 变量,并且在每个 setTimeout 回调函数内部,都能访问到对应的 i 变量。因此,改用 let 关键字声明 i,可以有效避免由
闭包
引起的变量共享
问题。
输出6
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2 start");
return new Promise((resolve, reject) => {
resolve();
console.log("async promise");
});
}
console.log("script start");
async1();
setTimeout(() => {
console.log("setTimeout");
}, 0);
new Promise((resolve, reject) => {
console.log("promise1");
resolve();
})
.then(() => {
console.log("promise2");
})
.then(() => {
console.log("promise3");
});
console.log("script end");
输出结果
:script start
=>async1 start
=>async2 start
=>async promise
=>promise1
=>script end
=>promise2
=>promise3
=>async1 end
=>setTimeout
注:
async/await函数
是为了简化多个then链情况而出现的async关键字用于声明一个函数是异步的,
搭配await实现函数的异步执行
,使用async/await时要等await后面的返回值之后才能继续往下执行在async函数中,如果没有await关键字
,则函数与普通函数没有什么区别,函数代码将会同步执行
。在
含有await的async
中,同一代码块中await后的所有代码将被放置返回的promise的then方法中执行,即await后的代码被加载进微任务队列
当函数内部
执行到一个await语句
的时候,如果语句返回一个 promise 对象
,那么函数将会等待promise 对象的状态变为 resolve 后再继续向下执行
。
解析
:(1)
async
关键字声明两个异步函数async1和async2
(2)执行
console.log("script start");
,输出 “script start”(3)调用
async1()
,开始执行异步函数async1
。console.log("async1 start");
,输出 “async1 start”(4)调用
await async2()
,async1
暂停执行,等待async2
的Promise
解决(5)
async2
被调用,输出” async2 start “,async2
创建一个新的Promise
并立即执行其 executor 函数,在Promise
的 executor 函数中,resolve()
被调用,同时,console.log("async promise");
同步执行,输出 “async promise”。(6)
new Promise((resolve, reject) => { ... })
创建了一个新的Promise
,其 executor 函数立即执行,输出 “promise1″。(7)
console.log("script end");
同步执行,输出 “script end”。执行栈为空,处理微任务队列:
(8)
.then(() => { console.log("promise2"); })
被执行,输出 “promise2″。(9)
.then(() => { console.log("promise3"); })
被执行,输出 “promise3″。微任务队列为空,处理宏任务队列:
(10)
setTimeout
的回调函数执行,输出 “setTimeout”。(11)回到
async2
,因为resolve()
已经被调用,async1
会继续执行。(12)
console.log("async1 end");
在async1
中同步执行,输出 “async1 end”。
最后
JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。
拓展:
浏览器是多进程的
浏览器是多进程的,也是多线程的。假如假如,浏览器是单进程的,那么某个Tab页崩了或者某个插件崩了,都会影响整个浏览器,这样用户的体验感会完全差到爆炸滴!
浏览器每一个tab页都代表一个独立的浏览器进程,浏览器可能有自己的优化机制,有时候打开多个tab页后,可以在任务管理器中看到,有些进程被合并了,如下图的子帧,所以每一个Tab标签对应一个进程并不一定是绝对的。
—>浏览器设置—>更多工具—>任务管理器—>查看浏览器具体的进程内容
浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
其中JS引擎线程就是执行javascript脚本程序的地方,JS引擎是浏览器的重要组成部分,主要是用于读取并执行JS。
JS引擎是多线程的,JS引擎解析但不仅限于javascript,单线程是指JS引擎执行JS时只分了一个线程给它执行,意思是JS引擎分配了一个线程给JavaScript执行,也就是我们所说的单线程。
各大浏览器的JS引擎
浏览器 | Js引擎 |
---|---|
Chrome | V8 |
Firefox | SpiderMonkey |
IE | Chakra |
Safari | Nitro/JavaScript Core |
Opera | Carakan |
虽然各大浏览器的JS引擎都不同,但其执行机制大致相同。
原文链接:https://juejin.cn/post/7345356037404459034 作者:不咸的鱼