js执行机制

252 阅读6分钟

在写js代码的时候我们往往都希望js代码的执行顺序是按照自己所想那样执行,但结果总事与愿违。所以今天就想花时间搞懂js的事件执行机制到底是什么样的。

EventLoop事件循环机制

js中事件分为宏任务和微任务两类,任务又分为同步任务和异步任务。他们之间的执行顺序将依次进行说明

js中事件的同步异步的执行顺序

js中事件分为两类

  • 同步事件
  • 异步事件
  • js执行顺序:先执行同步再执行异步
  1. js在执行时如果遇到同步任务就直接放入主线程执行
  2. 如果遇到异步任务就将其放入event table 中并注册函数
  3. 当异步事件完成后会将他的放入Event Queue中等待执行(这里特别说明宏任务和微任务各自有一个Event Queue)

那么等到什么时候才能执行呢?就是等到主线程的任务执行完毕为空的时候。那我们怎么知道什么时候主线程为空呢?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。 用流程图来概括上述步骤:

  • 代码示例
let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('发送成功!');
    }
})
console.log('代码执行结束');
  1. 从上往下,先遇到$.ajax异步请求,所以ajax进入event table中注册回调函数success
  2. 然后执行同步任务console.log("代码执行结束")
  3. ajax请求完成,回调函数success进入Event Queue
  4. 主线程任务执行完毕,从Event Queue中读取回调函数success并执行

js中事件的宏任务与微任务的执行顺序

js中任务分为两类

  • 宏任务:包括整体代码script,setTimeout,setInterval
  • 微任务:Promise.then(非new Promise),process.nextTick(node中)
  • js执行顺序:先执行宏任务再执行微任务
  1. js执行事件时先执行线程或Event Queue的宏任务
  2. 当前事件的宏任务执行完毕查看此线程或Event Queue是否有微任务,有的话执行微任务,没有的话执行新的宏任务

用流程图表示上述流程:

  • 代码示例
setTimeout(function() {
    console.log('setTimeout');
},1000)

new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})

console.log('console');
  1. 首先遇到了setTimeout函数,他是异步任务,所以将它放入event table中注册回调函数,又因为setTimeout是宏任务,所以1秒后将setTimeout放入宏任务的Event Queue中等待执行。
  2. 遇到同步代码new promise放入主线程直接开始执行
  3. 执行new promise的console.log('promise')然后看到.then是微任务因此将其放入微任务的Event Queue中
  4. 接下来执行同步代码console.log('console')
  5. 主线程的宏任务,已经执行完毕,接下来要执行微任务,因此会执行Event Queue中的Promise.then,到此,第一轮事件循环执行完毕
  6. 第二轮事件循环开始,先执行宏任务,即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
  1. 同步代码console.log('1')直接放入主线程执行
  2. setTimeout异步代码放到event table中注册回调函数,setTimeout事件完成将其回调函数放入宏任务的Event Queue中等待执行
  3. 然后主线程为空,先在宏任务的Event Queue中读取回调函数运行,直接执行console.log('2');
  4. 然后遇到process.nextTick放入微任务的Event Queue中
  5. 遇到new Promise执行console.log('4')然后将.then放入微任务的Event Queue中
  6. 宏任务执行完毕,查看微任务的Event Queue中是否有等待执行的微任务
  7. 执行process.nextTick的console.log('3')
  8. 执行.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事件循环机制