在写js代码的时候我们往往都希望js代码的执行顺序是按照自己所想那样执行,但结果总事与愿违。所以今天就想花时间搞懂js的事件执行机制到底是什么样的。
EventLoop事件循环机制
js中事件分为宏任务和微任务两类,任务又分为同步任务和异步任务。他们之间的执行顺序将依次进行说明
js中事件的同步异步的执行顺序
js中事件分为两类
- 同步事件
- 异步事件
- js执行顺序:先执行同步再执行异步
- js在执行时如果遇到同步任务就直接放入主线程执行
- 如果遇到异步任务就将其放入event table 中并注册函数
- 当异步事件完成后会将他的放入Event Queue中等待执行(这里特别说明宏任务和微任务各自有一个Event Queue)
那么等到什么时候才能执行呢?就是等到主线程的任务执行完毕为空的时候。那我们怎么知道什么时候主线程为空呢?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。 用流程图来概括上述步骤:
- 代码示例
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
- 从上往下,先遇到$.ajax异步请求,所以ajax进入event table中注册回调函数success
- 然后执行同步任务console.log("代码执行结束")
- ajax请求完成,回调函数success进入Event Queue
- 主线程任务执行完毕,从Event Queue中读取回调函数success并执行
js中事件的宏任务与微任务的执行顺序
js中任务分为两类
- 宏任务:包括整体代码script,setTimeout,setInterval
- 微任务:Promise.then(非new Promise),process.nextTick(node中)
- js执行顺序:先执行宏任务再执行微任务
- js执行事件时先执行线程或Event Queue的宏任务
- 当前事件的宏任务执行完毕查看此线程或Event Queue是否有微任务,有的话执行微任务,没有的话执行新的宏任务
用流程图表示上述流程:
- 代码示例
setTimeout(function() {
console.log('setTimeout');
},1000)
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
- 首先遇到了setTimeout函数,他是异步任务,所以将它放入event table中注册回调函数,又因为setTimeout是宏任务,所以1秒后将setTimeout放入宏任务的Event Queue中等待执行。
- 遇到同步代码new promise放入主线程直接开始执行
- 执行new promise的console.log('promise')然后看到.then是微任务因此将其放入微任务的Event Queue中
- 接下来执行同步代码console.log('console')
- 主线程的宏任务,已经执行完毕,接下来要执行微任务,因此会执行Event Queue中的Promise.then,到此,第一轮事件循环执行完毕
- 第二轮事件循环开始,先执行宏任务,即setTimeout的回调函数,然后查找是否有微任务,没有,时间循环结束
现在看一个比较复杂的例子
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')
})
})
输出:
1
2
4
3
5
- 同步代码console.log('1')直接放入主线程执行
- setTimeout异步代码放到event table中注册回调函数,setTimeout事件完成将其回调函数放入宏任务的Event Queue中等待执行
- 然后主线程为空,先在宏任务的Event Queue中读取回调函数运行,直接执行console.log('2');
- 然后遇到process.nextTick放入微任务的Event Queue中
- 遇到new Promise执行console.log('4')然后将.then放入微任务的Event Queue中
- 宏任务执行完毕,查看微任务的Event Queue中是否有等待执行的微任务
- 执行process.nextTick的console.log('3')
- 执行.then的console.log('5')
js中的同步和异步
大家都知道js是是一门单线程语言,所以他的语句肯定是一句一句执行的。但是js中又存在同步操作和异步操作,那么就会有疑问,js是如何通过单线程的方式来实现异步操作的呢?
什么是同步操作
当函数执行的时候,按照函数内部的顺序依次执行,比如:如果此调用的函数是很耗时的,但它依然会等待调用函数的返回值,直到拿到预期的结果(即拿到了预期的返回值或者看到了预期的效果)为止才会执行后面的操作,那么这个函数就是同步的。
//在函数返回时,获得了预期值,即2的平方根
Math.sqrt(2);
//在函数返回时,获得了预期的效果,即在控制台上打印了'hello'
console.log('hello');
什么是异步操作
如果函数是异步的,发出调用之后,马上返回,但是不会马上返回预期结果。调用者不必主动等待,当被调用者得到结果之后会通过回调函数主动通知调用者。
//读取文件
fs.readFile('hello.txt', 'utf8', function(err, data) {
console.log(data);
});
//网络请求
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx; // 添加回调函数
xhr.open('GET', url);
xhr.send(); // 发起函数
上述示例中读取文件函数 readFile和网络请求的发起函数 send都将执行耗时操作,虽然函数会立即返回,但是不能立刻获取预期的结果,因为耗时操作交给其他线程执行,暂时获取不到预期结果(后面介绍)。而在JavaScript中通过回调函数 function(err, data) { console.log(data); }和 onreadystatechange ,在耗时操作执行完成后把相应的结果信息传递给回调函数,通知执行JavaScript代码的线程执行回调。
浏览器
前面说到js是一门单线程语言,他是怎么实现异步操作的呢。JS的运行通常是在浏览器中进行的,具体由JS引擎去解析和运行。下面我们来具体了解一下浏览器。 浏览器的内核是多线程的。 一个浏览器通常由以下几个常驻的线程:
- 渲染引擎线程:顾名思义,该线程负责页面的渲染
- JS引擎线程:负责JS的解析和执行
- 定时触发器线程:处理定时事件,比如setTimeout, setInterval
- 事件触发线程:处理DOM事件
- 异步http请求线程:处理http请求
需要注意的是,渲染线程和JS引擎线程是不能同时进行的。 渲染线程在执行任务的时候,JS引擎线程会被挂起。因为JS可以操作DOM,若在渲染中JS处理了DOM,浏览器可能就不知所措了。
JS引擎可以说是JS虚拟机,负责JS代码的解析和执行。之所以说JavaScript是单线程,就是因为浏览器在运行时只开启了一个JS引擎线程来解析和执行JS。那为什么只有一个引擎呢?如果同时有两个线程去操作DOM,浏览器是不是又要不知所措了。 所以,虽然JavaScript是单线程的,可是浏览器内部不是单线程的。一些I/O操作、定时器的计时和事件监听(click, keydown...)等都是由浏览器提供的其他线程来完成的。也就是之前说的Event Queue。
参考文章
JavaScript异步机制详解
这一次,彻底弄懂 JavaScript 执行机制
简单总结下JS中EventLoop事件循环机制