浏览器和Node中的Event Loop

  • 为什么javascript是单线程的?

    • 因为javascript最初设计是运行在浏览器的,首先系统分配给浏览器的内存不会很多,其次javascript运行在浏览器的目的是为了与用户进行交互,假设javascript支持多线程,那么线程A要新增一个节点,线程B要删除一个节点,浏览器就不知道到底要干嘛了。

    • 随着CPU的发展,HTML5提出了web worker。允许web worker创建子线程,但是子线程得受控于主线程,且不能操作DOM。

  • Event loop是一个执行模型,在不同的地方有不同的实现。在浏览器中遵循的是HTML的标准,在node中是基于libuv库来实现的。

浏览器中的Event Loop

HTML标准这样定义事件循环:

为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。

有两类事件循环:一种针对浏览上下文(browsing context),还有一种针对worker(web worker)。

图片来自演讲《Help,I`m stuck in an event loop》

在主线程执行的过程中会产生堆和栈。执行栈中的代码会依次去调用外部的API。这时候就会分同步代码和异步代码。同步方法就去执行栈执行,调用到异步方法的时候就会产生任务队列,异步方法的回调就去排队。执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行,在同步代码执行完了,空闲的时候才去看有没有异步任务要执行,如果这个时候任务队列正好有排队的任务,就去执行。

  • 任务队列分为macro-task和micro-task:

    • macro-task:setTimeout,setInterval,setImmediate,I/O,UI rendering

    • micro-task:process.nextTick,Promise,Object.observer,MutationObserver

  • 在执行任务队列中的方法的时候,会先去执行micro-task队列,再循环执行macro-task队列。

举个例子来说,在浏览器执行以下代码:

setTimeout(()=>{
 console.log(1);
})
console.log(2);
new Promise((resolve,reject)=>{
 console.log(3);
 resolve();
}).then(()=>{
 console.log(4)
});
console.log('end');

这样一段代码,执行结果是:2,3,end,4,1

分析一下:

1、在遇到setTimeout的时候,这个方法会去macro-task排队。

2、显而易见,同步方法,输出2。

3、输出3在resolve()之前,不用等待resolve()执行完,所以是同步的,输出3;输出4在then()中,需要等待执行,属于Promise,在micro-task排队。

4、显而易见,同步方法,输出end。

5、自此同步代码已经执行完了。

6、开始执行micro-task队列,所以输出4。

9、开始执行macro-task队列,所以输出1。

node中的Event Loop

nodejs的event loop分为6个阶段,每个阶段的作用如下:

  • timers:执行setTimeout()setInterval()中到期的callback。

  • I/O callbacks:上一轮循环中有少数的I/O callback会被延迟到这一轮的这一阶段执行

  • idle, prepare:仅内部使用(这个存疑,我也不是很懂内部在干嘛)

  • poll:最为重要的阶段,执行I/O callback,这个指的就是比如去读数据库啊,读文件之类的。在适当的条件下会阻塞在这个阶段(所谓适当的条件我要去查一查,现在不确定具体是什么样的条件叫适当)

  • check:执行setImmediate的callback

  • close callbacks:执行close事件的callback,例如socket.on("close",func)

当然还是需要注意的就是,在每一个阶段执行之前还是会先把micro-task里的任务执行掉。

              |-----------------micro tasks
   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
|             |             <----- micro tasks
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
|             |            <----- micro tasks
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘ 
|             |            <----- micro tasks
|             |
|             |                   ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
|             |                   └───────────────┘
|             |            <----- micro tasks
│  ┌──────────┴────────────┐      
│  │        check          │
│  └──────────┬────────────┘
|             |            <----- micro tasks
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

举个例子,在node下执行代码:

setTimeout(()=>{
 console.log('timer1');
 Promise.resolve().then((resolve,reject)=>{
 console.log('promise1')
 })
})
setTimeout(()=>{
 console.log('timer2');
 Promise.resolve().then((resolve,reject)=>{
 console.log('promise2')
 })
})

根据node下的Event Loop执行顺序来看,输出结果应该是:

timer1,timer2,promise1,promise2

分析一下:

首先,micro-task是空的,执行timer阶段,执行setTimeout,输出timer1,timer2。promise1的then和promise2的then去micro-task排队。

然后,micro-task队列里有方法,输出promise1,promise2

再举个例子:

setTimeout(()=>{
 console.log('timer1');
 Promise.resolve().then(()=>{
 console.log('promise1')
 })
})
setTimeout(()=>{
 console.log('timer2');
 Promise.resolve().then(()=>{
 console.log('promise2')
 })
})
Promise.resolve().then(()=>{
 console.log('promise3')
})

这段的输出是:promise3,timer1,timer2,promise1,promise2

分析一下:因为在执行timer阶段之前,会先把micro-task队列里的方法执行一下,所以promise3虽然写在后面了,也得先输出来。

再再举个栗子:

setTimeout(()=>{
 console.log('timer1');
 Promise.resolve().then(()=>{
 console.log('promise1')
 })
})
setTimeout(()=>{
 console.log('timer2');
 Promise.resolve().then(()=>{
 console.log('promise2')
 })
})
Promise.resolve().then(()=>{
 console.log('promise3')
})
process.nextTick(()=>{
 console.log('nexttick')
})

这段的输出是:nexttick,promise3,timer1,timer2,promise1,promise2

分析一下:因为在micro-task队列里的优先级,process.nextTick一定比promise先执行

需要注意的是:是异步方法的回调在任务队列排队。

/////setTimeout和setImmediate的执行顺序有待商榷,主要是会涉及到代码执行的时间,和HTML规定setTimeout默认的4ms问题,不足4ms补足4ms

参考文档:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容