浏览器和Node中的Event Loop

作者: SuperSnail | 来源:发表于2018-03-20 20:05 被阅读56次
    • 为什么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

    参考文档:

    相关文章

      网友评论

        本文标题:浏览器和Node中的Event Loop

        本文链接:https://www.haomeiwen.com/subject/nbxiqftx.html