浏览器事件环和Node事件环不得不说的故事!

2,320 阅读7分钟

浏览器事件环和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有:setTimeoutsetIntervalsetImmediate(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
  1. 先执行栈中的内容,也就是同步代码,所以2被输出出来;
  2. 然后清空微任务,所以依次输出的是 then1 then2
  3. 因代码是从上到下执行的,所以1s后 setTimeout1 被执行输出;
  4. 接着再次清空微任务,then3被输出;
  5. 最后执行输出setTimeout2

三、Node的事件环

Node是基于V8引擎的JavaScript运行环境,在处理高并发、I/O密集(文件操作、网络操作、数据库操作等)场景有明显的优势。Node的事件环机制与浏览器的是不太一样。 在Node运行环境中:

  1. 我们写的js代码会交由V8引擎进行处理
  2. 代码中可能会调用NodeApi,node会交由libuv处理
  3. libuv通过阻塞I/O和多线程实现异步I/O
  4. 然后通过事件驱动的方式,将结果放到事件队列中,最终交给我们的应用。

其实本质是在libuv(一个高性能的,事件驱动的I/O库)内部有这样一个事件环机制。在Node启动时会初始化事件环,话不多说,先上图:

图中显示的每个阶段都对应一个事件队列,当event loop执行到某个阶段时会将当前阶段对应的队列依次执行。当队列执行完毕或者执行数量超过上限时,才会转入下一个阶段。node中的微任务在切换队列时执行。

  • timers计时器:执行setTimeoutsetInterval的回调函数;
  • 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队列前面,但是setTimeoutsetImmediate没有明确的先后顺序的,这是由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队列,setImmediate1setImmediate2连续输出是因只有当前队列执行完毕后才能进去下一对列。

总结:今天的话题就先到这里了。可能本文有表述不清楚或者理解不对的地方,还请各位大佬给予指正,我会继续努力每周分享,万分感谢!如果您觉得看了这篇文章有所收获,请不要忘了动动手指点个小❤️哦!让我们一起荡起双桨,每天进步一点点,在技术的道路上越走越远!

原创不易,转载请注明出处!谢谢!