文章资料来自
Node.js 事件循环机制
JS灵魂之问(下)
EventLoop的中国名字叫事件循环,这个玩意真的是高深莫测,一般开发都用不到,代码只管写就行,虽然不用懂,但是面试就是要问,这对我这种小菜鸡真是满满的恶意
先说说异步IO
这个在Linux笔记里有,但是异步IO只有 Linux 下存在,在其他系统中没有异步 IO 支持,那window的异步IO是怎么实现的,==利用多线程==,我们可以让一个进程进行计算操作,另外一些进行 IO 调用,IO 完成后把信号传给计算的线程,进而执行回调,这不就好了吗?没错,异步 IO 就是使用这样的==线程池==来实现的,只不过在不同的系统下面表现会有所差异,在 Linux 下可以直接使用==线程池==来完成,在Window系统下则采用 IOCP 这个系统API(其内部还是用==线程池==完成的)
上面的三个线程池都加粗了,因为他就是关键字,线程池的运行图很常见
V8、事件循环、事件队列都在单线程中运行,最右侧还有工作线程(Worker Thread)负责提供异步的I/O操作,这就是为什么说Node.js拥有非阻塞的,事件驱动的异步IO架构
不仅是异步IO运行在线程池,NodeJS的计时器,http请求,浏览器的计时器,http请求ajax,ui渲染也都是运行在线程池的,也就是说==js是单线程运行是错的==,他是==同步任务单线程运行==,在【Linux/IO】笔记里把NodeJS比作餐厅是最简单的理解,他有个问题是菜做好了通知服务生来拿,IO执行完是不会通知服务生来拿的,正在的通知是线程池里的线程做的,也就是说服务生拿了菜单到厨房后,放了一招【影分身之术】,叫了一个线程在门口等着【上图的观察者】,菜做好了影分身喊了一句菜做好了,然后自己就消失了,这是主线程服务生才知道才做好了
原理代码
/** * 定义事件队列 * 入队:push() * 出队:shift() * 空队列:length == 0 */ var globalEventQueue = [] /** * 接收用户请求 * 每一个请求都会进入到该函数 * 传递参数request和response */ function processHttpRequest(request,response){ // 定义一个事件对象 var event = createEvent({ params:request.params, // 传递请求参数 result:null, // 存放请求结果 callback:function(){} // 指定回调函数 }); // 在队列的尾部添加该事件 globalEventQueue.push(event); } /** * 事件循环主体,主线程择机执行 * 循环遍历事件队列 * 处理非IO任务 * 处理IO任务 * 执行回调,返回给上层 */ function eventLoop(){ // 如果队列不为空,就继续循环 while(this.globalEventQueue.length > 0){ // 从队列的头部拿出一个事件 var event = this.globalEventQueue.shift(); // 如果是耗时任务 if(isIOTask(event)){ // 从线程池里拿出一个线程 var thread = getThreadFromThreadPool(); // 交给线程处理 thread.handleIOTask(event) }else { // 非耗时任务处理后,直接返回结果 var result = handleEvent(event); // 最终通过回调函数返回给V8,再由V8返回给应用程序 event.callback.call(null,result); } } } /** * 处理IO任务 * 完成后将事件添加到队列尾部 * 释放线程 */ function handleIOTask(event){ //当前线程 var curThread = this; // 操作数据库 var optDatabase = function(params,callback){ var result = readDataFromDb(params); callback.call(null,result) }; // 执行IO任务 optDatabase(event.params,function(result){ // 返回结果存入事件对象中 event.result = result; // IO完成后,将不再是耗时任务 event.isIOTask = false; // 将该事件重新添加到队列的尾部 this.globalEventQueue.push(event); // 释放当前线程 releaseThread(curThread) }) }
MicroTask
这个词的中国名字叫==微任务==,这个概念是跟着Promise
一起出现的,百度Promise都会提到他解决了回调地狱
// 之前 fs.readFile('1.json', (err, data) => { fs.readFile('2.json', (err, data) => { fs.readFile('3.json', (err, data) => { fs.readFile('4.json', (err, data) => { }); }); }); }); // 现在 readFilePromise('1.json').then(data => { return readFilePromise('2.json') }).then(data => { return readFilePromise('3.json') }).then(data => { return readFilePromise('4.json') });
Promise确实是解决了回调地狱,但这只是改变了写法,在没有Promise的时代,代码也一样运行,那Promise到底带来了什么,微任务带来了什么,带来了==宏任务==,233333,上面的EventLoop就是宏任务的运行规则,在没有微任务的时候就是这么循环执行的,但是看上面的模拟运行,异步回调被放在了执行栈数组的最后面,倘若现在的任务队列非常长,那么回调迟迟得不到执行,造成应用卡顿,于是他们开辟了微任务队列,也就是第二个数组
- 一开始整段脚本作为第一个宏任务执行
- 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
- 当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空
- 执行浏览器 UI 线程的渲染工作
- 检查是否有Web worker任务,有则执行
- 执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
放到微任务队列怎么理解呢
把下面的代码运行一下,再把注释解开运行一下
正常来说第一次是 1 2 3 4 2.1,因为400ms的计数器,等他回到微任务队列,0ms的计数器都执行完了
正常来说第二次是 1 2 3 ... 2.1 4,当0ms的定时器返回,循环还在继续,循环快完的时候,400ms的定时器也返回了,这时4是在2.1之前的,但是还是2.1比4先输出,因为他插队了,在微任务队列了实现了插队
console.log(1) new Promise(function(x,y){ console.log(2) setTimeout(()=>{ console.log(2.1) },400) }).then(x=>{ console.log(x) }) console.log(3) // for(var i=3;i<10000;i++){ // console.log(i) // } setTimeout(()=>{ console.log(4) })
在浏览器是上面这么执行的,而NodeJS还在循环结束加了个nextTick函数,这是必须在微任务执行队列执行完后执行的,也就是第三个数组,Vue也有一个nextTick是在异步的更新dom后执行的,模仿nodejs的执行概念
就这个理解面试应该没问题了吧,广州有没有招人的,年后想换工作,求收留
来源:https://www.cnblogs.com/pengdt/p/12240497.html