博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
浏览器和Node.js中的Event Loop
阅读量:7163 次
发布时间:2019-06-29

本文共 5260 字,大约阅读时间需要 17 分钟。

前言

众所周知,javascript是一门单线程语言,而当我们使用ajax和服务端进行通信的时候是需要一定时间的,这样当前线程就会被阻塞,使浏览器失去相应。因此,当js执行执行一些长时间的任务时,我们希望有一种异步的方式处理这种任务。事件循环(event loop)就是如何处理异步执行顺序的一种机制。

$.get(url, function (data) {    //do something});复制代码

浏览器中的事件循环

接下来会一一介绍,事件循环中的执行栈事件队列宏任务微任务等概念

什么是执行栈

执行栈就是js代码运行的地方,上图call stack所示。当下面程序运行时,会推送的调用栈中被执行。

console.log('Hi');setTimeout(function cb1() {     console.log('cb1');}, 500);console.log('Bye');复制代码

什么是事件队列

当浏览器中的事件监听函数被触发(DOM)、网络请求的相应(ajax)、定时器被触发(setTimeout)相对应的回调函数就会被推送到事件队列中,等待执行;如上图中的Callback Queue。

什么是事件循环

事件循环是一个这样的过程:当执行栈中的任务结束之后,会将事件队列中的第一个任务推入到执行栈中执行,当任务处理完毕,又会取事件队列中的第一个任务,如此往复,便构成了事件循环。

对应到下面代码中。

console.log('Hi');setTimeout(function cb1() {     console.log('cb1');}, 500);console.log('Bye');复制代码
  • 程序推送到执行栈中被执行
  • 执行console语句、输出Hi
  • 执行setTimeou语句
  • 执行console语句、输出Bye
  • 500ms的时候,setTimeout的回调函数被推送到事件队列中
  • 此时事件队列中只有setTimeout的回调函数这一个任务,会被推到执行栈中执行
  • console语句执行、输出cb1

通过上面的例子会对执行栈和事件队列有个基本的认识。由于JS是单线程的,同步任务会造成浏览器阻塞,我们把任务分成一个一个的异步任务,通过事件循环来执行事件队列中的任务。这就使得当我们挂起某一个任务的时候可以去做一些其他的事情,而不需要等待这个任务执行完毕。所以事件循环的运行机制大致分为以下步骤:

1、检查事件队列是否为空,如果为空,则继续检查;如不为空,则执行 2;

2、取出事件队列的首部,压入执行栈;

3、执行任务;

4、检查执行栈,如果执行栈为空,则跳回第 1 步;如不为空,则继续检查;

浏览器渲染时机

我们知道DOM操作会触发浏览器渲染,如增、删节点,改变背景颜色。那么这类操作是如何在浏览器当中奏效的?

至此我们已经知道了事件循环是如何执行的,事件循环器会不停的检查事件队列,如果不为空,则取出队首压入执行栈执行。当一个任务执行完毕之后,事件循环器又会继续不停的检查事件队列,不过在这间,浏览器会对页面进行渲染。这就保证了用户在浏览页面的时候不会出现页面阻塞的情况,这也使 JS 动画成为可能。

function move() {    setTimeout(() => {        dom.style.left = dom.offsetLeft + 10 + 'px'        move()    }, 15);}move()复制代码

现在用事件循环的机制说明js动画的过程。上面代码会在执行栈中执行,move函数被调用,setTimeout的回调函数15ms之后会被推送到事件队列中。此时执行栈中的任务结束,浏览器渲染、检查事件队列不断循环。当15ms之后事件队列中有任务时,会被推送到执行栈中执行,这时dom节点向右偏移10px,move函数执行、执行栈结束,浏览渲染、检查事件队列。如此往复就形成了动画。

宏任务和微任务(microtask)

先看一段代码,是如何输出的;

console.log('script start');setTimeout(function () {    console.log('setTimeout');}, 0);Promise.resolve().then(function () {    console.log('promise1');}).then(function () {    console.log('promise2');});console.log('script end');复制代码

答案是:'script start''script end''promise1''promise2''setTimeout'

setTimeout的回调函数是宏任务、Promise的回调函数是微任务。微任务和宏任务一样遵循事件循环机制,但是他们还是有些差别。

1、宏任务和微任务的事件队列是相互独立的;

2、微任务队列的检查时机早于宏任务。(执行栈中任务结束就会马上清空微任务事件队列)

根据上面的规则,解释代码的输出。

  • 执行栈中的代码执行,宏任务推入宏任务事件队列、微任务推入微任务事件队列,执行栈任务结束

  • 检查微任务事件队列,此时已经有Promise的回调函数,推入执行栈,输出promise1。Promise还有回调函数,推入微任务事件队列,执行栈结束。

  • 检查微任务事件队列,推入执行栈,输出promise2,执行栈结束。

  • 检查微任务事件队列,此时被清空

  • 检查宏任务事件队列,推入执行栈,输出setTimeout,执行栈结束。

    宏任务有: **setTimeout** 、**setImmediate** 、 **MessageChannel**  微任务有: **setTimeout** 、**setImmediate** 、 **MessageChannel**复制代码

Node.js中的事件循环

Node中的事件循环是和浏览器有很大区别的

当Node.js启动时,会初始化event loop;每个event loop都会包含按如下顺序六个循环阶段

┌───────────────────────┐┌─>│        timers         ││  └──────────┬────────────┘│  ┌──────────┴────────────┐│  │     I/O callbacks     ││  └──────────┬────────────┘│  ┌──────────┴────────────┐│  │     idle, prepare     ││  └──────────┬────────────┘      ┌───────────────┐│  ┌──────────┴────────────┐      │   incoming:   ││  │         poll          │<─────┤  connections, ││  └──────────┬────────────┘      │   data, etc.  ││  ┌──────────┴────────────┐      └───────────────┘│  │        check          ││  └──────────┬────────────┘│  ┌──────────┴────────────┐└──┤    close callbacks    │   └───────────────────────复制代码
  • timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;
  • I/O callbacks 阶段: 执行除了 close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks;
  • idle, prepare 阶段: 仅node内部使用;
  • poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
  • check 阶段: 执行setImmediate() 设定的callbacks;
  • close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行。

每一个阶段都有一个装有callbacks的fifo queue(队列),当event loop运行到一个指定阶段时, node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时,event loop会转入下一下阶段。

Node.js中的宏任务和微任务

宏任务:setTimeout和setImmediate复制代码
  • setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行;但其在timer阶段执行
  • setImmediate 设计在check阶段执行;

谁先输出,谁后输出?

setTimeout(function timeout () {  console.log('timeout');},0);setImmediate(function immediate () {  console.log('immediate');});复制代码

答案是不确定的。有两个前提我们是需要清楚的;

  • event loop初始化是需要一定时间的
  • setTimeout有最小毫秒数的,通常是4ms。

当:event loop准备时间 > setTimeout最小毫秒数。从timers阶段检查,此时队列中已经有setTimeout的任务,所以timeout先输出;

当:event loop准备时间 < setTimeout最小毫秒数。从timers阶段检查,此时队列是空的就下检查接下来的阶段,到check阶段,已经有setImmediate的任务,所以immediate先输出;

微任务:process.nextTick()和Promise.then()复制代码

微任务不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行;nextTick比Promise.then()先执行

下面代码是如何执行的。

setImmediate(() => {  console.log('setImmediate1')  setTimeout(() => {    console.log('setTimeout1')  }, 0);})setTimeout(()=>{  process.nextTick(()=>console.log('nextTick'))  console.log('setTimeout2')  setImmediate(()=>{    console.log('setImmediate2')  })},0);复制代码
  • 从前面的知识知道,此时setTimeout和setImmediate执行顺序是不确定的。
  • 假设setImmediate先执行,输出setImmediate1,setTimeout的任务添加到timer阶段
  • 检查timer阶段,这时已经有两个任务。先执行之前的第一个任务,nextTick添加到微任务队列,输出setTimeout2,setImmediate的任务添加到check阶段。
  • timer中还有一个任务,执行输出setTimeout1
  • 切换阶段,微任务执行,输出nextTick
  • 检查check阶段,输出setImmediate2

思考题

let fs = require('fs')fs.readFile('./1.txt', 'utf8', function (err, data) {    setTimeout(() => {        console.log('setTimeout')    }, 0);    setImmediate(() => {        console.log('setImmediate')    })})复制代码

这种情况下的setTimeout和setImmediate执行的顺序确定吗?readFile的回调函数是在poll阶段执行 答案是setImmediatesetTimeout先执行

结语

浏览器中和Node.js中的事件循环可以说是两套不同的机制,做个总结,希望有所帮助。

转载地址:http://aojwm.baihongyu.com/

你可能感兴趣的文章
WCF配置文件注释
查看>>
获取SqlConnection的统计信息
查看>>
puppet cert maintain
查看>>
团队绩效评估计划
查看>>
BZOJ1486:[HNOI2009]最小圈——题解
查看>>
网络对抗 Exp0 Kali安装 Week1
查看>>
【Python第六篇】Python面向对象(进阶篇)及相关(异常处理、反射)
查看>>
Java DOM方式解析XML(模板)
查看>>
TextUtils.isEmpty() 和equals方法。
查看>>
yii2修改默认控制器和布局视图
查看>>
python 基础知识(一)
查看>>
检测应用版本
查看>>
EditText会自动获取焦点并弹出输入法的问题
查看>>
知识体系
查看>>
jquery把表单数据序列成json,支持多维数组
查看>>
virtualbox mac-debian共享文件夹
查看>>
[转载]当web配置文件 appSettings配置的东西越来越多时,可以拆开了
查看>>
int *i = new int;
查看>>
CCF计算机认证——字符串匹配问题(运行都正确,为什么提交后只给50分?)...
查看>>
POST提交的四种类型
查看>>