JavaScript的事件循环机制

单线程

JavaScript 是一种单线程的编程语言,也就是说 JavaScript 只有一个调用栈,任何时候只能有一个函数被调用,其他函数必须等待当前执行函数执行完毕之后才能得到执行。这意味着 JavaScript 不支持并发执行,只能按顺序来执行同步代码。为了支持并发和异步操作,JavaScript 引入了事件循环机制和任务队列。那么读到此处,我们就有中疑问————为什么 JavaScript 不能有多个线程呢?
答:JavaScript 作为浏览器脚本语言,其主要用途是与用户互动,以及操作 DOM 。这就决定了它只能是单线程,否则会带来很复杂的同步问题,比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准。而为了避免这种复杂性,JavaScript 只能是单线程。

事件循环机制(Event Loop)

目前JavaScript的主要运行环境有两个,即 浏览器Node.js 。那事件循环也就有两种,即 浏览器事件循环Node.js事件循环JavaScript 是一门单线程语言,即主线程只有一个。事件循环(Event Loop) 就是JS引擎管理事件执行的一个流程,具体由运行环境决定。

事件循环机制告诉我们JS代码的执行顺序,是指 浏览器Node 的一种解决JS单线程运行时不会阻塞的一种机制。

浏览器的事件循环中将任务分为 同步任务异步任务

1. 同步任务和异步任务

同步任务

  • 指在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务。
  • 所有同步任务都在主线程上执行,形成一个函数调用栈(执行栈)
  • 例如:事件处理程序中的同步任务代码。
  • (同步代码:逐行执行,原地等待结果后,才继续向下执行)

异步任务

  • 异步任务不进入主线程,它会进入 “任务队列”(task queue) 里等待,当 调用栈(执行栈) 为空时,再将 “任务队列”(task queue) 里的 回调函数 依次压入调用栈中来执行。
  • 异步任务不会阻塞主线程,可以与其它任务并发执行。
  • 例如:setTimeout、HTTP请求等操作完成后会进入“任务队列”等待。
  • (异步代码:调用后耗时,不阻塞代码执行,将来完成后触发回调函数)。
  • 异步任务在 “任务队列”(task queue) 里又分为 宏任务(macro-task)微任务(micro-task)

宏任务

  • 浏览器执行的异步代码
  • 宏任务包括:script(整体代码)、setTimeout、setInterval、AJAX请求完成事件、用户交互事件等

微任务

  • JS引擎执行的异步代码
  • 微任务包括:new Promise().then( )、MutationObserve( )、process.nextTick( )、Object.observe( )

PS:若同时存在new Promise().then()和process.nextTick(),先执行process.nextTick()

console.log('start');

Promise.resolve().then(() => {
  console.log('promise');
});

process.nextTick(() => {
  console.log('nextTick');
});

console.log('end');

// 执行结果为:
start
end
nextTick
promise

2. 执行过程

  1. 所有同步任务都在主线程上执行,形成一个执行栈(调用栈)
  2. 主线程之外,还存在一个“任务队列”(task queue),浏览器中的各种 Web API(如DOM事件、AJAX等) 为JavaScript提供了一个可执行异步代码的单独运行空间,当异步代码运行完毕后,会将代码中的回调加入到“任务队列”(task queue)中
  3. 一旦主线程的栈中的所有同步任务执行完毕后,调用栈为空时系统就会将“任务队列”(task queue)中的回调函数依次压入调用栈中执行,当调用栈为空时,仍然会不断循环检测任务队列中是否有代码需要执行

3. 执行顺序

  • 先执行同步代码
  • 遇到异步宏任务则将异步宏任务放入宏任务队列中
  • 遇到异步微任务则将异步微任务放入微任务队列中
  • 当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行
  • 微任务执行完毕后再将异步宏任务从队列中调入主线程执行
  • 一直循环直至所有任务执行完毕

PS:当宏任务和微任务都处于“任务队列”(task queue)中时,微任务的优先级大于宏任务,即先将微任务执行完,再执行宏任务

4. 举例

示例1

setTimeout(() => {
  console.log("4")
  setTimeout(() => { 
    console.log("8")
  }, 0)
  new Promise(r => {
    console.log("5") // 构造函数是同步的
    r()
  }).then(() => {
    console.log("7") // then()是异步的,这入队
  })
  console.log("6")
}, 0)
new Promise(r => {
  r()
  console.log("1") // 构造函数是同步的
}).then(() => {
  console.log("3") // then()是异步的,这里队
})
console.log("2")
//输出顺序:1 2 3 4 5 6 7 8 

解释:

  1. 遇到 setTimeout,为异步宏任务,放入 Web API 提供的运行空间里运行(于此同时代码往下执行),运行完后将回调放入宏任务队列中
  2. 遇到 new Promise,Promise 在实例化的过程中所执行代码都是同步代码(即同步任务),所以输出1
  3. 而** Promise().then() 中注册的回调才是异步执行**,将其放入微任务队列中
  4. 遇到同步任务console.log(“2”),输出2,此时主线程中的同步任务执行完
  5. 从微任务队列中取出任务(即 Promise().then()的回调)到主线程中,输出3,微任务队列为空
  6. 从宏任务队列中取出任务(即 setTimeout的回调)到主线程中
    1. 遇到同步任务(console.log(“4”)),输出4
    2. 遇到setTimeout,为异步宏任务,放入宏任务队列中
    3. 遇到new Promise实例化,输出5
    4. 遇到Promise().then(),放入微任务队列中
    5. 遇到console.log(“6”),输出6
    6. 从微任务队列中取出任务(即6.4的回调),输出7,微任务队列为空
    7. 从宏任务队列中取出任务(即6.2的回调),输出8,宏任务队列为空,结束~~~

示例2

setTimeout(function(){
    console.log('1')
})
new Promise(function(resolve){          
    console.log('2')
    resolve()
}).then(function(){         
    console.log('3')
});        
console.log('4');
//输出顺序:2 4 3 1

解释:

  1. 遇到setTimout,异步宏任务,放入宏任务队列中;
  2. 遇到new Promise,Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2;
  3. 而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
  4. 遇到同步任务console.log(‘4’);输出4;主线程中同步任务执行完
  5. 从微任务队列中取出任务到主线程中,输出3,微任务队列为空
  6. 从宏任务队列中取出任务到主线程中,输出1,宏任务队列为空,结束~~~

示例3

setTimeout(()=>{
  new Promise(resolve =>{
    resolve()
  }).then(()=>{
    console.log('test')
  })

  console.log(4)
})

new Promise(resolve => {
  resolve()
  console.log(1)
}).then( () => {
  console.log(3)
  Promise.resolve().then(() => {
    console.log('before timeout');
  }).then(() => {
    Promise.resolve().then(() => {
      console.log('also before timeout')
    })
  })
})
console.log(2);
//输出:1 2 3 before timeout also before timeout 4 test

解释:

  1. 遇到setTimeout,异步宏任务,将() => {console.log(4)}放入宏任务队列中;
  2. 遇到new Promise,Promise在实例化的过程中所执行的代码都是同步进行的,所以输出1;
  3. 而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
  4. 遇到同步任务console.log(2),输出2;主线程中同步任务执行完
  5. 从微任务队列中取出任务到主线程中,输出3,此微任务中又有微任务,Promise.resolve().then(微任务a).then(微任务b),将其依次放入微任务队列中;
  6. 从微任务队列中取出任务a到主线程中,输出 before timeout;
  7. 从微任务队列中取出任务b到主线程中,任务b又注册了一个微任务c,放入微任务队列中;
  8. 从微任务队列中取出任务c到主线程中,输出 also before timeout;微任务队列为空
  9. 从宏任务队列中取出任务到主线程,此任务中注册了一个微任务d,将其放入微任务队列中,接下来遇到输出4,宏任务队列为空
  10. 从微任务队列中取出任务d到主线程 ,输出test,微任务队列为空,结束~~~

总的来说:事件循环就是执行代码和收集异步任务,在调用栈空闲时,反复调用任务队列里的回调函数执行机制。

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值