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

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

进入正文以前,首先要感谢各位大佬对本人第一篇掘金文章《ES6版Promise实现,给你不同的体验》的确定及指正,可能写的不尽人意,可是大家的点赞会是我继续分享的动力之一,只要努力过,结果就不要太在乎,由于努力以后的结果会让你满意!与诸位共勉!vue

好了,话很少说,接下来让咱们进入今天的话题。今天咱们来谈一谈事件环究竟是什么?javaScript的事件环和Node的事件环有什么区别?有没有一种无从下手的感受,别捉急,只要你仔细阅读本篇文章,相信可以解开心中的疑惑。 java

1、先了解几组常见概念

俗话说,工欲善其事必先利其器。在进入浏览器事件环和Node事件环情节以前呢,咱们有必要了解如下几组常见的概念。node

一、heap(堆)和 stack(栈)

堆栈是在计算机领域不可忽视的概念,若是想要详细了解,请移步《堆栈_百度百科》。在javaScript中,栈中存的是基本数据类型,会自动分配内存空间,自动释放;堆中存的是引用数据类型,是动态分配的内存,大小不定也不会自动释放。git

  • heap堆:也能够叫堆内存;是一种队列优先,先进先出的数据结构;
  • stack栈:又名'堆栈',也是一种数据结构,不过它是按照先进后出原则存储数据的。

嘻嘻😝,本身花了半天时间(夸张)画得,自我感受良好。github

既然咱们大体理解了堆和栈的含义,咱们来看一道面试题,如何用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();
复制代码

二、线程和进程

首先,咱们应该知道进程比线程要大。一个程序至少要有一个进程,一个进程至少要有一个线程。就拿咱们常常用的浏览器为例吧,为了更直观一些,先看下这张图片:ajax

因而可知,浏览器就是多进程的,当一个网页崩溃时不会影响其余网页的正常运行。咱们主要了解下一下几个方面:

  • 渲染引擎:渲染引擎内部是多线程的,内部包含了两个最重要的线程ui线程和js线程。这里要特别注意ui线程和js线程是互斥的,由于JS运行结果会影响到ui线程的结果。ui更新会被保存在队列中等到js线程空闲时当即被执行。
  • js单线程:JavaScript最大的特色就是单线程的,其实应该说其主线程是单线程的。为何这么说呢?你想一下,若是js是多线程的,咱们在页面中这个线程要删了那个元素(不顺眼),另外一个线程呢我要保留那个元素(我罩着的),这样岂不是就乱套了。这也是为何JavaScript执行同步代码,异步代码并不会阻塞代码的运行。
  • 其余线程:
    • 浏览器事件触发线程(用来控制事件循环,存放setTimeout、浏览器事件、ajax的回调函数)
    • 定时触发器线程(setTimeout定时器所在线程)
    • 异步HTTP请求线程(ajax请求线程)

三、宏任务和微任务

宏任务和微任务能够说都是异步任务。若是了解vue源码的同窗,应该知道宏任务macrotask和微任务microtask这两个概念,他们的执行时机是不同的。vue$nextTick的源码就是经过宏任务和微任务实现的。(能够去vue的github了解一下其实现原理)。数据库

  • 常见的宏任务macrotask有:setTimeoutsetIntervalsetImmediate(ie浏览器才支持,node中本身也实现了)、MessageChannel
  • 常见的微任务microtask有:promise.then()process.nextTick(node的)

2、javaScript的事件环

浏览器中,事件环的运行机制是,先会执行栈中的内容,栈中的内容执行后执行微任务,微任务清空后再执行宏任务,先取出一个宏任务,再去执行微任务,而后在取宏任务清微任务这样不停的循环,咱们能够看下面这张图理解一下:数组

从图中能够看出,同步任务会进入执行栈,而异步任务会进入任务队列(callback queue)等待执行。一旦执行栈中的内容执行完毕,就会读取任务队列中等待的任务放入执行栈开始执行。(图中缺乏微任务)promise

那么,咱们来道面试题检验一下,当咱们在浏览器中运行下面的代码,输出的结果是什么呢?

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

3、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、setTimeout二、nextTick、setImmediate一、setImmediate二、setTimeout1,为何是这样的顺序呢?微任务nextTick的输出是由于timers队列切换到check队列,setImmediate1setImmediate2连续输出是因只有当前队列执行完毕后才能进去下一对列。

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

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

相关文章
相关标签/搜索