浏览器事件环和Node事件环不得不说的故事!
进入正文之前,首先要感谢各位大佬对本人第一篇掘金文章《ES6版Promise实现,给你不一样的体验》的肯定及指正,可能写的不尽人意,但是你们的点赞会是我继续分享的动力之一,只要努力过,结果就不要太在意,因为努力之后的结果会让你满意!与诸位共勉!
好了,话不多说,接下来让我们进入今天的话题。今天我们来谈一谈事件环到底是什么?javaScript
的事件环和Node
的事件环有什么区别?有没有一种无从下手的感觉,别捉急,只要你仔细阅读本篇文章,相信能够解开心中的疑惑。
一、先了解几组常见概念
俗话说,工欲善其事必先利其器。在进入浏览器事件环和Node事件环情节之前呢,我们有必要了解以下几组常见的概念。
1、heap(堆)和 stack(栈)
堆栈是在计算机领域不可忽视的概念,如果想要详细了解,请移步《堆栈_百度百科》。在javaScript中,栈中存的是基本数据类型,会自动分配内存空间,自动释放;堆中存的是引用数据类型,是动态分配的内存,大小不定也不会自动释放。
heap
堆:也可以叫堆内存;是一种队列优先,先进先出的数据结构;stack
栈:又名'堆栈',也是一种数据结构,不过它是按照先进后出原则存储数据的。
嘻嘻?,自己花了半天时间(夸张)画得,自我感觉良好。
既然我们大致理解了堆和栈的含义,我们来看一道面试题,如何用js代码实现队列和栈的功能呢?其实很简单啦,就是数组最基本常用的增删方法。
- 实现队列的方法(先进先出)
let arr = new Array();
arr.push(1);
arr.push(2);
arr.shift();
复制代码
- 实现栈的方法(先进后出)
let arr = new Array();
arr.push(1);
arr.push(2);
arr.pop();
复制代码
2、线程和进程
首先,我们应该知道进程比线程要大。一个程序至少要有一个进程,一个进程至少要有一个线程。就拿我们经常用的浏览器为例吧,为了更直观一些,先看下这张图片:
由此可见,浏览器就是多进程的,当一个网页崩溃时不会影响其他网页的正常运行。我们主要了解下一下几个方面:- 渲染引擎:渲染引擎内部是多线程的,内部包含了两个最重要的线程ui线程和js线程。这里要特别注意ui线程和js线程是互斥的,因为JS运行结果会影响到ui线程的结果。ui更新会被保存在队列中等到js线程空闲时立即被执行。
- js单线程:
JavaScript
最大的特点就是单线程的,其实应该说其主线程是单线程的。为什么这么说呢?你想一下,如果js是多线程的,我们在页面中这个线程要删了那个元素(不顺眼),另一个线程呢我要保留那个元素(我罩着的),这样岂不是就乱套了。这也是为什么JavaScript
执行同步代码,异步代码并不会阻塞代码的运行。 - 其他线程:
- 浏览器事件触发线程(用来控制事件循环,存放
setTimeout
、浏览器事件、ajax
的回调函数) - 定时触发器线程(
setTimeout
定时器所在线程) - 异步HTTP请求线程(
ajax
请求线程)
- 浏览器事件触发线程(用来控制事件循环,存放
3、宏任务和微任务
宏任务和微任务可以说都是异步任务。如果了解vue
源码的同学,应该知道宏任务macrotask
和微任务microtask
这两个概念,他们的执行时机是不一样的。vue
的$nextTick
的源码就是通过宏任务和微任务实现的。(可以去vue的github了解一下其实现原理)。
- 常见的宏任务
macrotask
有:setTimeout
、setInterval
、setImmediate
(ie浏览器才支持,node中自己也实现了)、MessageChannel
- 常见的微任务
microtask
有:promise.then()
、process.nextTick
(node的)
二、javaScript的事件环
浏览器中,事件环的运行机制是,先会执行栈中的内容,栈中的内容执行后执行微任务,微任务清空后再执行宏任务,先取出一个宏任务,再去执行微任务,然后在取宏任务清微任务这样不停的循环,我们可以看下面这张图理解一下:
从图中可以看出,同步任务会进入执行栈,而异步任务会进入任务队列(callback queue)等待执行。一旦执行栈中的内容执行完毕,就会读取任务队列中等待的任务放入执行栈开始执行。(图中缺少微任务)
那么,我们来道面试题检验一下,当我们在浏览器中运行下面的代码,输出的结果是什么呢?
setTimeout(() => {
console.log('setTimeout1');
Promise.resolve().then(data => {
console.log('then3');
});
},1000);
Promise.resolve().then(data => {
console.log('then1');
});
Promise.resolve().then(data => {
console.log('then2');
setTimeout(() => {
console.log('setTimeout2');
},1000);
});
console.log(2);
// 输出结果:2 then1 then2 setTimeout1 then3 setTimeout2
复制代码
- 先执行栈中的内容,也就是同步代码,所以2被输出出来;
- 然后清空微任务,所以依次输出的是
then1
then2
;- 因代码是从上到下执行的,所以1s后
setTimeout1
被执行输出;- 接着再次清空微任务,
then3
被输出;- 最后执行输出
setTimeout2
三、Node的事件环
Node
是基于V8引擎的JavaScript
运行环境,在处理高并发、I/O密集(文件操作、网络操作、数据库操作等)场景有明显的优势。Node的事件环机制与浏览器的是不太一样。 在Node运行环境中:
- 我们写的js代码会交由V8引擎进行处理
- 代码中可能会调用NodeApi,node会交由
libuv
处理 libuv
通过阻塞I/O和多线程实现异步I/O- 然后通过事件驱动的方式,将结果放到事件队列中,最终交给我们的应用。
其实本质是在libuv
(一个高性能的,事件驱动的I/O库)内部有这样一个事件环机制。在Node启动时会初始化事件环,话不多说,先上图:
event loop
执行到某个阶段时会将当前阶段对应的队列依次执行。当队列执行完毕或者执行数量超过上限时,才会转入下一个阶段。node中的微任务在切换队列时执行。
timers
计时器:执行setTimeout
、setInterval
的回调函数;I/O callbacks
:执行I/O callback
被延迟到下一阶段执行;idle, prepare
:队列的移动,仅内部使用poll
轮询:检索新的I/O事件;执行I/O相关的回调check
:执行setImmediate
回调close callbacks
:执行close
事件的callback
,例如socket.on("close",func)
好了,接下来我们先来看一道简单的测试题:
setTimeout(function () {
console.log('setTimeout');
});
setImmediate(function () {
console.log('setImmediate');
});
复制代码
这道题中如果你在
node
环境中多运行几次,就会发现输出顺序是不固定的。也就是说虽然上图中timers
队列在check
队列前面,但是setTimeout
和setImmediate
没有明确的先后顺序的,这是由node
的准备时间(准备工作会浪费一定的时间)导致的。
对应上面这道练习题,我们再来看下面这道题:
let fs = require('fs');
fs.readFile('./1.txt', function () {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
});
复制代码
这道题的输出顺序是
setImmediate
然后setTimeout
,无论你运行多少次,结果顺序不会发生改变。这是因为fs
文件操作(I/O操作)属于属于poll
阶段,poll
阶段的下一阶段就是check
阶段,所以输出顺序是毋庸置疑的。
最后,让我们来再看一道面试题加深对Node事件环的理解:
setImmediate(() => {
console.log('setImmediate1');
setTimeout(() => {
console.log('setTimeout1')
}, 0);
});
Promise.resolve().then(res=>{
console.log('then');
})
setTimeout(() => {
process.nextTick(() => {
console.log('nextTick');
});
console.log('setTimeout2');
setImmediate(() => {
console.log('setImmediate2');
});
}, 0);
复制代码
这道题的输出顺序是:
then、setTimeout2、nextTick、setImmediate1、setImmediate2、setTimeout1
,为什么是这样的顺序呢?微任务nextTick
的输出是因为timers
队列切换到check
队列,setImmediate1
和setImmediate2
连续输出是因只有当前队列执行完毕后才能进去下一对列。
总结:今天的话题就先到这里了。可能本文有表述不清楚或者理解不对的地方,还请各位大佬给予指正,我会继续努力每周分享,万分感谢!如果您觉得看了这篇文章有所收获,请不要忘了动动手指点个小❤️哦!让我们一起荡起双桨,每天进步一点点,在技术的道路上越走越远!
原创不易,转载请注明出处!谢谢!