JS执行机制(浏览器事件环 vs Node事件环)

一、JS是单线程的

  • JavaScript作为浏览器脚本语言,从设计之初就是单线程,主要用途是与用户互动,以及操作DOM。

假设个场景,若JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?,所以,为了避免复杂性,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

  • 为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。所有任务又可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

  • 同步任务指的是:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

  • 异步任务指的是:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

  • 具体来说,异步执行的运行机制如下:

    • 1、所有同步任务都在主线程上执行,形成一个执行栈(FILO)
    • 2、主线程之外,还存在一个任务队列(FIFO)。只要异步任务有了运行结果,就在任务队列之中放置一个事件(回调函数)。
    • 3、一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
    • 4、主线程不断重复上面的第三步。这也就是我们所说的Event Loop(事件循环)
说明:
  • 所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
  • JS为什么需要异步?

如果js不存在异步,所有代码从上往下执行,如果上一行代码解析很长,那么下面的代码就会被阻塞,对于用户而言,阻塞意味着“卡死”,这样就导致了很差的用户体验

  • 队列 和 栈 堆:

堆是指程序运行时申请的动态内存,而栈只是指程序编译时一种使用堆的方法(即先进后出);栈和队列都是线性结构,栈就像一个桶,采用FILO特性(先进后出),而队列采用FIFO特性(先进先出)。

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。这种运行机制又称为Event Loop(事件循环),但是Event Loop在浏览器和Node中运行机制是有一定区别的

浏览器事件环
我们先了解下浏览器模型

  • 浏览器的user Interface (用户界面)、Browser engine(浏览器引擎)、Rendering engine(渲染引擎)、Data Persistence(数据存储)都是一个进程
  • Rendering engine(渲染引擎)下有三个线程:Net working(网络请求)、JavaScript Interpreter(js解释器)、UI线程,UI线程和JS线程会互斥。
  • 一般来说浏览器会有以下几个线程:
    • js引擎线程 (解释执行js代码、用户输入、网络请求)
    • GUI线程 (绘制用户界面、与js主线程是互斥的)
    • http网络请求线程 (处理用户的get、post等请求,等返回结果后将回调函数推入任务队列)
    • 定时触发器线程 (setTimeout、setInterval等待时间结束后把执行函数推入任务队列中)
    • 浏览器事件处理线程(将click、mouse等交互事件发生后将这些事件放入事件队列中)
浏览器事件环

  • 宏任务macrotask:setTimeout、setInterval、MessageChannel、postMessage、ajax、script(整体代码)、onClick系列事件等

  • 微任务microtask:Promise.then、MutationObserver、Object.observe(已废弃)

  • js在浏览器中执行机制(见图解):

    • 1、当一个任务进入执行栈,同步的进入主线程执行,异步的进入Event Table并注册函数。当指定的事情完成时,Event Table会将这个函数移入Event Queue,这个函数就是该任务指定的回调函数。
    • 2、Event Queue分为微任务队列(microtasks queues)和宏任务队列(macrotask queues),当主线程内的任务执行完毕为空时,会先去microtasks queues中读取对应的函数,并放入主线程执行;
    • 3、当microtasks queues中所有的任务执行完毕,再去macrotask queues中取一个宏任务放入主线程执行,并且每执行一个宏任务前,都会先将Event Queue中microtasks queues清空,即Event Queue中的微任务优先被读取执行。
    • 4、上述过程会不断重复
  • 下面我们看一个例子:

console.log('1');
setTimeout(function () { //setTimeout1
    console.log('2');
    new Promise(function (resolve) {
        console.log('3');
        resolve();
    }).then(function () { //then2
        console.log('4')
    })
})
new Promise(function (resolve) {
    console.log('5');
    resolve();
}).then(function () {//then1
    console.log('6')
})
setTimeout(function () { //setTimeout2
    console.log('7');
    new Promise(function (resolve) {
        console.log('8');
        resolve();
    }).then(function () { //then3
        console.log('9')
    })
})
// 1、5、6、2、3、4、7、8、9   (chrome浏览器)      
复制代码
  • 解析:
  • 当任务进入执行栈,同步任务进入主线程执行(promise执行其中代码为同步执行) --> 1、5,
  • 此时,微任务then1和宏任务setTimeout1、setTimeout2进入Event Queue中,微任务优先被读取执行 --> 6
  • 当前循环中微任务已被清空,开始执行第一个宏任务setTimeout1 --> 2、3
  • 此时setTimeout1中微任务then2被放入Event Queue,每执行一个宏任务前都要清空事件池中微任务,故then2优先于setTimeout2执行 --> 4
  • 执行宏任务setTimeout2,以及内部微任务 --> 7、8、9
Node事件环

node的事件环相比浏览器就不一样了,我们先了解下它的工作流程:

Node工作流程

  • Node工作流程:
    • 1、V8引擎解析JavaScript脚本,若解析后的代码,调用了Node API,Node就会将代码交给libuv库处理,这个libuv库是别人写的,也就是node的事件环。
    • 2、libuv库负责Node API的执行,它将不同的任务分配给不同的工作线程(work threads),通过多线程同步阻塞执行,模拟异步处理机制,成功后将回调函数放入Event Queue。
    • 3、等到work threads队列中有执行完成的事件,就会通过execute callback回调给Event queue队列,把它放入队列中。
    • 4、最后通过事件驱动(发布订阅)的方式,取出EVENT QUEUE队列的事件,再通过V8引擎将结果返回给应用
  • node中的event loop是在libuv里面的,libuv里面有个事件环机制,他会在启动node时,初始化事件环
Node事件环

  • timers队列:setTimeout、setInterval、

  • poll队列:异步I/O

  • check队列:setImmediate

  • 微任务:Promise.then、process.nextTick

  • js在Node中执行机制(见图解):

    • Event Loop中每一个阶段都对应着一个事件队列:时间队列、poll(轮询)队列、check(setimmediate)队列等,以及微任务队列(process.nextTick优先于Promise.then执行)
    • 每当其中一个队列执行完毕或者执行数量超过上限,event loop就会执行下一个阶段,即切换到下一个队列
    • 每当event loop切换一个执行队列之前,就会先去清空microtasks queues,然后再切换到下个队列去执行,如此反复
  • 注意:

    • setImmediate是属于check队列的,还有poll队列主要是异步的I/O操作,比如node中的fs.readFile()
    • process.nextTick优先于Promise.then执行
    • node事件环初始化是有启动时间的
    • setTimeout的0毫秒实际上也是达不到的;根据HTML的标准,最低是4毫秒。

上代码,加深下理解:

let fs = require('fs');
setTimeout(function(){
    Promise.resolve().then(()=>{
        console.log('then2');
    })
},0);
Promise.resolve().then(()=>{
    console.log('then1');
});
fs.readFile('./gitigore',function(){
    setTimeout(function () {
        console.log('setTimeout')
    }, 0);
    setImmediate(()=>{
        console.log('setImmediate')
    });
});
// then1 then2 setImmediate setTimeout
复制代码
  • 说明:
    • 微任务优先执行:then1
    • 然后setTimeout放入时间队列,fs.readFile放入poll队列,setTimeout先执行,并把then2房屋微任务队列
    • 切换任务队列到poll队列,fs.readFile执行之前会先清空微任务队列,Promise.then执行:then2
    • fs.readFile执行,setTimeout放入时间队列,等待时间0s,setImmediate放入check队列,poll队列执行完毕后,下一队列是check队列,setImmediate执行:setImmediate
    • check队列执行完毕,进入下个循环,时间队列在执行:setTimeout

但是下面这种情况setImmediate和setTimeout谁先谁后就不得而知了:

setImmediate(function(){
    console.log('setImmediate')
});
setTimeout(function(){
    console.log('setTimeout')
},0); // ->4ms
//setImmediate setTimeout 或 setTimeout setImmediate
复制代码
  • 说明:
    • node事件环初始化是有时间的
    • 正常情况下走完时间队列才会走check队列,但是setTimeout-0会有4ms延迟,如果node启动时间大于4ms,那么OK一切正常,setTimeout先放入执行栈执行,
    • 若node启动时间小于4ms。此时setTimeout-0并未启动,所以会先将setImmediate放入执行栈执行。因此才会出现两种结果

我们再看一种情况:

setTimeout(() => {
    console.log('timeout1');
    process.nextTick(()=>{
        console.log('nextTick1');
    })
}, 1000);
// 虽然都写1000毫秒 是有误差
process.nextTick(function(){ // nextTick执行的时候用了0.8ms
    setTimeout(() => {
        console.log('timeout2'); 
    }, 1000); // 1000.02ms
    console.log('nextTick2');
})
// nextTick2 timeout1 nextTick1 timeout2 或 nextTick2 timeout1 timeout2 nextTick1
复制代码
  • 说明
    • setTimeout1等待1000ms后才放入timers队列,微任务首先执行:nextTick2,假设微任务执行时间m,

    • 此时setTimeout2释放,等待1000ms,即(1000+m)ms后放入timers队列,

    • 1000ms后,setTimeout1执行,执行时间n,执行完毕后nextTick1放入微任务队列

    • 如果n> m,则此时setTimeout2已被放入timers队列,timers队列中setTimeout2继续执行

    • 如果n< m,则此时setTimeout2还没有放入timers队列,libuv进入下一次循环,然后进入下次循环中的timers队列前要清空微任务,所以nextTick1先被执行:nextTick1

    • 再加上setTimeout本身就是存在误差的,更加不确定timeout2与nextTick1的执行顺序

    • 注意node事件环是切换队列时才清空microtasks queues。

    • 总之,由于setTimeout的误差或者同步代码和微任务的执行时间不确定性,某些场景下的异步任务可能还未被放入队列中,当队列为空时就会执行下一阶段队列任务,按照node事件环机制继续运行。

转载于:https://juejin.im/post/5b794606e51d4538b063db95

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值