理解JavaScript之Event Loop(JavaScript和node环境下区别)

javascript是一门单线程语言,按照语句出现的顺序执行的。

执行和运行

JavaScript 的执行和运行是两个不同概念的,执行,一般依赖于环境,比如 node、浏览器、Ringo 等, JavaScript 在不同环境下的执行机制可能并不相同。而Event Loop 就是 JavaScript 的一种执行方式。而运行,是指JavaScript 的解析引擎。这是统一的。

事件循环(Event Loop)

JavaScript 有一个主线程 main thread,和调用栈 call-stack 也称之为执行栈。所有的任务都会放到调用栈中等待主线程来执行。

JavaScript 的 Event Loop 是伴随着整个源码文件生命周期的,只要当前 JavaScript 在运行中,内部的这个循环就会不断地循环下去,去寻找 queue 里面能执行的 task

任务队列(task queue)

task,就是任务的意思,可以理解为每一个语句就是一个任务。

queue ,就是FIFO的队列。

 Task Queue 就是承载任务的队列。而 JavaScript 的 Event Loop 就是会不断地查找这个 queue,看有没有 task 可以运行。

同步任务(SyncTask)、异步任务(AsyncTask)

同步任务:就是主线程来执行的时候立即就能执行的代码

异步任务:就是先去执行别的 task,等所有task执行完之后再往 Task Queue 里面塞一个 task 的同步任务来等待被执行

setTimeout(() => {
    task()
},3000)

sleep(100000)

执行上面的代码,执行task()的时间远远大于3秒。

看看上述代码的执行

  • task()进入Event Table并注册,计时开始。
  • 执行sleep函数,继续计时。
  • 3秒到了,计时事件timeout完成,task()进入Event Queue,等待sleep执行完成。
  • sleep执行完毕,task()从Event Queue进入主线程执行。此时计时时间将大于3秒

setInterval()的执行效果同理。

再看一个ajax请求

ajax({
    url:www.xxx.com,
    data:prams,
    success:() => {
        console.log('请求成功!');
    },
    error:()=>{
        console.log('请求失败~');
    }
})
console.log('这是一个同步任务');
  • ajax 请求首先进入到 Event Table ,分别注册了onErroronSuccess回调函数。
  • 主线程执行同步任务:console.log('这是一个同步任务');
  • 主线程任务执行完毕,看Event Queue是否有待执行的 task,这里是不断地检查,只要主线程的task queue没有任务执行了,主线程就一直在这等着
  • ajax 执行完毕,将回调函数pushEvent Queue。(步骤 3、4 没有先后顺序而言)
  • 主线程“终于”等到了Event Queue里有 task可以执行了,执行对应的回调任务。
  • 如此往复。

宏任务(MacroTask)、微任务(MicroTask)

除了广义的同步任务和异步任务,我们对任务有更精细的定义:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick

不同类型的任务会进入对应的Event Queue,比如setTimeoutsetInterval会进入相同的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。
  • 接下来遇到了Promisenew Promise立即执行,then函数分发到微任务Event Queue。
  • 遇到console.log(),立即执行。
  • 整体代码script作为第一个宏任务执行结束,查询微任务,执行在微任务Event Queue中的then
  • 第一轮事件循环结束,开始第二轮循环,从宏任务Event Queue开始。遇到宏任务Event Queue中setTimeout对应的回调函数,立即执行。
  • 结束。

promise、process.nextTick与async/await

Promise中的异步体现在thencatch中,所以写在Promise中的代码是被当做同步任务立即执行的。

await实际上是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码;

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')
    })
})

第一轮事件循环流程:

  • 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
  • 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
  • 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1
  • 遇到Promisenew Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1
  • 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2
宏任务Event Queue微任务Event Queue
setTimeout1process1
setTimeout2then1
  • 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。

  • 我们发现了process1then1两个微任务。

  • 执行process1,输出6。
  • 执行then1,输出8。

第一轮时间循环结束,输出1,7,6,8。

第二轮时间循环从setTimeout1宏任务开始:

首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2

宏任务Event Queue微任务Event Queue
setTimeout2process2
 then2
  • 第二轮事件循环宏任务结束,我们发现有process2then2两个微任务可以执行。
  • 输出3。
  • 输出5。

第二轮事件循环结束,第二轮输出2,4,3,5。

第三轮事件循环开始,此时只剩setTimeout2了,执行。

  • 直接输出9。
  • process.nextTick()分发到微任务Event Queue中。记为process3
  • 直接执行new Promise,输出11。
  • then分发到微任务Event Queue中,记为then3
宏任务Event Queue微任务Event Queue
 process3
 then3
  • 第三轮事件循环宏任务执行结束,执行两个微任务process3then3
  • 输出10。
  • 输出12。
  • 第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。

PS:此处是JavaScript执行环境下的执行过程

 

Node 环境下的 Event Loop

Node.js

Node.js的运行机制

  • V8引擎解析JavaScript脚本。
  • 解析后的代码,调用Node API。
  • libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
  • V8引擎再将结果返回给用户。

Node中的Event Loop是基于libuv实现的,而libuv是 Node 的新跨平台抽象层,libuv使用异步,事件驱动的编程方式,核心是提供i/o的事件循环和异步回调。libuvAPI包含有时间,非阻塞的网络,异步文件操作,子进程等等。

区分:nodejs的event是基于libuv,而浏览器的event loop则在html5的规范中明确定义。

Event Loop运作机制

上述的五个阶段都是按照先进先出的规则执行回调函数。按顺序执行每个阶段的回调函数队列,直至队列为空或是该阶段执行的回调函数达到该阶段所允许一次执行回调函数的最大限制后,才会将操作权移交给下一阶段。

  • timers:执行setTimeout()setInterval()中到期的callback。
  • I/O callback:执行除了close事件的callbacks、被timers设定的callbacks、setImmediate()设定的callbacks这些之外的callbacks
  • idle, prepare:仅node内部使用
  • poll: 最为重要的阶段,获取新的I/O事件,在适当的条件会阻塞在这个阶段
  • check: 执行setImmediate的callback
  • close callbacks: 执行close事件的callback,例如socket.on('close'[,fn])http.server.on('close, fn)

在每次运行事件循环之间,node.j检查是否有正在等待的异步i/o调用、timers等。如果没有,就清除并结束(退出程序),例如:执行一个程序,仅有一句话(var a= ‘hello’;),处理完目标代码后,不会进入evetloop,而是直接结束程序。

PS:上面六个阶段都不包括 process.nextTick()

重要的三阶段:

timers,定时器阶段:
执行定时任务(setTimeOut(), setInterval())

poll 轮询阶段:

处理到期的定时器任务,然后(因为最开始阶段队列为空,一旦队列为空,就会检查是否有到期的定时器任务)
处理队列任务,直到队列空,或达到上限
如果队列为空:如果setImmediate,终止轮询阶段,进入检查阶段执行。如果没setImmediate,查看有没有定时器任务到期,有的话就到timers阶段,执行回调函数.
check 检查阶段

轮询阶段空闲,且有setImmediate的时候,进入检查阶段

不重要二阶段

  • I/O callbacks阶段:处理I/O异常错误
  • close callbacks阶段: 处理各种close事件回调

从event loop机制的角度上区分process.nextTick()与setImmediate()

setImmediate() 和 setTimeout()非常的相似,区别取决于谁调用了它。

  • setImmediate在 poll 阶段后执行,即check 阶段
  • setTimeout 在 poll 空闲时且设定时间到达的时候执行,在 timer 阶段

计时器的执行顺序将根据调用它们的上下文而有所不同。 如果两者都是从主模块中调用的,则时序将受到进程性能的限制。

例如,如果我们运行以下不在I / O周期(即主模块)内的脚本,则两个计时器的执行顺序是不确定的,因为它受进程性能的约束:

// timeout_vs_immediate.js
setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});
$ node timeout_vs_immediate.js 
timeout
immediate 
$ node timeout_vs_immediate.js
immediate
timeout

如果在一个I/O 周期内移动这两个调用,则始终首先执行立即回调:

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
$ node timeout_vs_immediate.js
immediate
timeout
$ node timeout_vs_immediate.js
immediate
timeout

所以与setTimeout()相比,使用setImmediate()的主要优点是,如果在I / O周期内安排了任何计时器,则setImmediate()将始终在任何计时器之前执行,而与存在多少计时器无关。

nextTick queue

process.nextTick()从技术上讲不是Event Loop的一部分。 相反,无论当前事件循环的当前阶段如何,都将在当前操作完成之后处理nextTickQueue。

如果存在 nextTickQueue,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

setTimeout(() => {
 console.log('timer1')
 Promise.resolve().then(function() {
   console.log('promise1')
 })
}, 0)
process.nextTick(() => {
 console.log('nextTick')
 process.nextTick(() => {
   console.log('nextTick')
   process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
     })
   })
 })
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

Node与浏览器的 Event Loop 差异

浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。

在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值