内容简介:转自IMWeb社区,作者:sugerpocket,原文链接众所周知,javascript 是单线程的,其通过使用异步而不阻塞主进程执行。那么,他是如何实现的呢?本文就浏览器与nodejs环境下异步实现与event loop进行相关解释。浏览器环境下,会维护一个任务队列,当异步任务到达的时候加入队列,等待事件循环到合适的时机执行。
转自IMWeb社区,作者:sugerpocket,原文链接
众所周知,javascript 是单线程的,其通过使用异步而不阻塞主进程执行。那么,他是如何实现的呢?本文就浏览器与nodejs环境下异步实现与event loop进行相关解释。
浏览器环境
浏览器环境下,会维护一个任务队列,当异步任务到达的时候加入队列,等待事件循环到合适的时机执行。
实际上,js 引擎并不只维护一个任务队列,总共有两种任务
- Task(macroTask):
setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
- microTask:
Promise
,process.nextTick
,Object.observe
,MutationObserver
,MutaionObserver
那么两种任务的行为有何不同呢?
实验一下,请看下段代码
setTimeout(function() { console.log(4); }, 0); var promise = new Promise(function executor(resolve) { console.log(1); for (var i = 0; i < 10000; i++) { i == 9999 && resolve(); } console.log(2); }).then(function() { console.log(5); }); console.log(3); 复制代码
输出:
1 2 3 5 4 复制代码
这说明 Promise.then
注册的任务先执行了。
我们再来看一下之前说的 Promise
注册的任务属于 microTask
, setTimeout
属于 Task,两者有何差别?
实际上, microTasks
和 Tasks
并不在同一个队列里面,他们的调度机制也不相同。比较具体的是这样:
- event-loop start
- microTasks 队列开始清空(执行)
- 检查 Tasks 是否清空,有则跳到 4,无则跳到 6
- 从 Tasks 队列抽取一个任务,执行
- 检查 microTasks 是否清空,若有则跳到 2,无则跳到 3
- 结束 event-loop
也就是说,microTasks 队列在一次事件循环里面不止检查一次,我们做个实验
// 添加三个 Task // Task 1 setTimeout(function() { console.log(4); }, 0); // Task 2 setTimeout(function() { console.log(6); // 添加 microTask promise.then(function() { console.log(8); }); }, 0); // Task 3 setTimeout(function() { console.log(7); }, 0); var promise = new Promise(function executor(resolve) { console.log(1); for (var i = 0; i < 10000; i++) { i == 9999 && resolve(); } console.log(2); }).then(function() { console.log(5); }); console.log(3); 复制代码
输出为
1 2 3 5 4 6 8 7 复制代码
microTasks
会在每个 Task
执行完毕之后检查清空,而这次 event-loop
的新 task
会在下次 event-loop
检测。
Node 环境
实际上,node.js环境下,异步的实现根据操作系统的不同而有所差异。而不同的异步方式处理肯定也是不相同的,其并没有严格按照js单线程的原则,运行环境有可能会通过其他线程完成异步,当然,js引擎还是单线程的。
node.js使用了Google的V8解析引擎和Marc Lehmann的libev。Node.js将事件驱动的I/O模型与适合该模型的编程语言(Javascript)融合在了一起。随着node.js的日益流行,node.js需要同时支持windows, 但是libev只能在Unix环境下运行。Windows 平台上与kqueue(FreeBSD)或者(e)poll(Linux)等内核事件通知相应的机制是IOCP。libuv提供了一个跨平台的抽象,由平台决定使用libev或IOCP。
关于event loop,node.js 环境下与浏览器环境有着巨大差异。
先来一张图
先解释一下各个阶段
- timers: 这个阶段执行setTimeout()和setInterval()设定的回调。
- I/O callbacks: 执行几乎所有的回调,除了close回调,timer的回调,和setImmediate()的回调。
- idle, prepare: 仅内部使用。
- poll: 获取新的I/O事件;node会在适当条件下阻塞在这里。
- check: 执行setImmediate()设定的回调。
- close callbacks: 执行比如socket.on('close', ...)的回调。
每个阶段的详情
timer
一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。
注意:技术上来说,poll 阶段控制 timers 什么时候执行。
I/O callbacks 这个阶段执行一些系统操作的回调。比如TCP错误,如一个TCP socket在想要连接时收到ECONNREFUSED, 类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行。
poll
poll 阶段的功能有两个
- 执行 timer 阶段到达时间上限的任务。
- 执行 poll 阶段的任务队列。
如果进入 poll 阶段,并且没有 timer 阶段加入的任务,将会发生以下情况
- 如果 poll 队列不为空的话,会执行 poll 队列直到清空或者系统回调数达到上限
- 如果 poll 队列为空
如果设定了 setImmediate 回调,会直接跳到 check 阶段。 如果没有设定 setImmediate 回调,会阻塞住进程,并等待新的 poll 任务加入并立即执行。
check
这个阶段在 poll 结束后立即执行,setImmediate 的回调会在这里执行。
一般来说,event loop 肯定会进入 poll 阶段,当没有 poll 任务时,会等待新的任务出现,但如果设定了 setImmediate,会直接执行进入下个阶段而不是继续等。
close
close 事件在这里触发,否则将通过 process.nextTick 触发。
一个例子
var fs = require('fs'); function someAsyncOperation (callback) { // 假设这个任务要消耗 95ms fs.readFile('/path/to/file', callback); } var timeoutScheduled = Date.now(); setTimeout(function () { var delay = Date.now() - timeoutScheduled; console.log(delay + "ms have passed since I was scheduled"); }, 100); // someAsyncOperation要消耗 95 ms 才能完成 someAsyncOperation(function () { var startCallback = Date.now(); // 消耗 10ms... while (Date.now() - startCallback < 10) { ; // do nothing } }); 复制代码
当event loop进入 poll 阶段,它有个空队列(fs.readFile()尚未结束)。所以它会等待剩下的毫秒, 直到最近的timer的下限时间到了。当它等了95ms,fs.readFile()首先结束了,然后它的回调被加到 poll 的队列并执行——这个回调耗时10ms。之后由于没有其它回调在队列里,所以event loop会查看最近达到的timer的 下限时间,然后回到 timers 阶段,执行timer的回调。
所以在示例里,回调被设定 和 回调执行间的间隔是105ms。
setImmediate() vs setTimeout()
现在我们应该知道两者的不同,他们的执行阶段不同,setImmediate() 在 check 阶段,而settimeout 在 poll 阶段执行。但,还不够。来看一下例子。
// timeout_vs_immediate.js setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); }); 复制代码
$ node timeout_vs_immediate.js timeout immediate $ node timeout_vs_immediate.js immediate timeout 复制代码
结果居然是不确定的,why?
还是直接给出解释吧。
- 首先进入timer阶段,如果我们的机器性能一般,那么进入timer阶段时,1毫秒可能已经过去了(setTimeout(fn, 0) 等价于setTimeout(fn, 1)),那么setTimeout的回调会首先执行。
- 如果没到一毫秒,那么我们可以知道,在check阶段,setImmediate的回调会先执行。
那我们再来一个
// timeout_vs_immediate.js var fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) }) 复制代码
输出始终为
$ node timeout_vs_immediate.js immediate timeout 复制代码
这个就很好解释了吧。 fs.readFile 的回调执行是在 poll 阶段。当 fs.readFile 回调执行完毕之后,会直接到 check 阶段,先执行 setImmediate 的回调。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 用于业务的精炼js工具函数(浏览器环境)
- 从浏览器环境到JavaScript执行流程的一次简单梳理
- jest-electron:在 electron 浏览器环境运行 jest 单测
- 挖洞经验 | Outlook.com邮箱环境在iOS浏览器下的Stored XSS漏洞
- GitHub推出云端IDE,几秒完成开发环境配置,今后可以在浏览器里使用VS Code了
- 恶意网站可利用浏览器扩展 API,窃取浏览器数据
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Python编程初学者指南
[美]Michael Dawson / 王金兰 / 人民邮电出版社 / 2014-10-1
Python是一种解释型、面向对象、动态数据类型的高级程序设计语言。Python可以用于很多的领域,从科学计算到游戏开发。 《Python编程初学者指南》尝试以轻松有趣的方式来帮助初学者掌握Python语言和编程技能。《Python编程初学者指南》共12章,每一章都会用一个完整的游戏来演示其中的关键知识点,并通过编写好玩的小软件这种方式来学习编程,引发读者的兴趣,降低学习的难度。每章最后都会......一起来看看 《Python编程初学者指南》 这本书的介绍吧!