JavaScript运行机制:事件驱动编程详解

JavaScript运行机制:事件驱动编程详解

1.阻塞和线程

同步式I/O:

线程在执行中如果遇到磁盘读写或者网络通信(统称为I/O操作),通常要耗费较长的时间,这是操作系统会剥夺这个线程的CPU控制权,使其暂停执行,同时将资源让给其他的工作线程,这种线程的调度方式称为阻塞。当I/O操作结束,操作系统将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。这种I/O模式就是我们通常说的同步式I/O。

异步式I/O:

异步式I/O是针对上述的I/O操作,不采用阻塞模式,当线程遇到I/O操作时,不会以阻塞的方式等待I/O操作的结束,而只是将I/O操作请求发送给操作系统,然后继续执行下一条语句。当操作系统完成I/O操作,以事件的形式通知执行I/O操作的线程,线程会在特定的时间处理这个事件,可见使用异步式I/O,单线程即可胜任。

异步式I/O和同步式I/O到底有什么区别?

一个线程同时只能处理一项任务,并且I/O操作往往比CPU计算要耗时得多,所以在同步式I/O模式下,当I/O操作执行时,线程被阻塞,CPU处于空闲的状态,如果要提高CPU的利用率,必须通过多线程,一个线程因为同步式I/O被阻塞了,还有其它线程在工作,多线程可以让CPU资源不被阻塞中的线程浪费,这也是众多多线程语言采用的模式。

单线程事件驱动的异步式 I/O 比传统的多线程同步式I/O,少了多线程的开销,对于操作系统来说,创建一个线程的代价是非常昂贵的:需要给它分配内存、列入调度、在线程切换时还要执行内存换页、清空CPU缓存

2.JavaScript运行机制:消息队列和事件循环

“JavaScript是一门单线程的语言” ,所谓的单线程,是指在JS引擎中负责解释和执行JavaScript代码的线程只有一个。我们称它主线程。

处理AJAX请求的线程、处理DOM事件的线程、定时器线程、读写文件的线程(例如在Node.js中)等等。我们称它们工作线程。

JavaScript是采用异步式I/O+事件循环模式

单线程的问题:

单线程也就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为I/O很慢,不得不等着结果出来,再往下执行。

js解决方法:采用异步式I/O+事件循环模式时,当耗时的I/O的操作结束后,会以事件的形式通知主线程,这样就可以避免阻塞,那么这个通知机制是怎样实现的呢?答案是利用消息队列和事件循环。

消息队列:

当采用异步式I/O+事件循环后,所有任务可以分成两种,一种是同步任务,另一种是异步任务(此处为了理解方便,先只分为同步任务和异步任务,后面我们会细分为宏任务、微任务、正常任务)。

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

异步任务指的是,暂时不进入主线程而是进入消息队列的任务,只有主线程的同步任务全部结束后该任务才会进入主线程执行。用一句话概括就是:

工作线程将消息放到消息队列,主线程通过事件循环去取消息。

执行流程:

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个消息队列。只要异步任务有了运行结果,就在消息队列之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取消息队列,看看里面有哪些事件。那些对应的异步任务结束等待状态,进入执行栈,开始执行。
  4. 主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行,不断重复上面的第三步。

事件循环:

主线程从消息队列中读取消息,这个过程是循环不断的。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次事件循环。

宏任务 微任务:

macro-task(宏任务): setTimeout、setInterval、setImmediate、I/O, UI rendering等

micro-task(微任务): process.nextTick、Promises.then、Object.observe等

Promise,被构造时传入的回调函数,是会立即执行的,它是task,会出现在调用栈中。而Promise实例的then方法中的回调函数是micro-task,会在该Promise实例的状态改变时(resolve)被放进micro-task队列。

事件循环的顺序是从script开始第一次循环,随后全局上下文进入函数调用栈:

  1. 碰到宏任务就将其交给处理它的模块处理完之后将回调函数放进宏任务的队列之中
  2. 碰到微任务也是将其回调函数放进微任务的队列之中
  3. 直到函数调用栈清空只剩全局执行上下文,然后开始执行所有的微任务,在执行微任务时如果再碰到微任务,会将该微任务继续添加到微任务队列,当所有可执行的微任务执行完毕之后,执行栈结束并返回(return)
  4. 循环再次执行宏任务队列中的一个任务,执行完之后再执行所有的微任务,就这样一直循环
(function test() {
    setTimeout(function() { console.log(1) }, 0); //回调会被添加到macro-task队列
    new Promise(function(resolve) {
        console.log(2);
        for (var i = 0; i < 10000; i++) {
            i == 9999 && resolve();
        }
        console.log(3);
    }).then(function() { //回调会在resolve后,添加到micro-task队列
        console.log(4);
        new Promise(function(resolve) {
            setTimeout(function() { //回调会被添加到macro-task队列
                resolve();
                console.log(5);
            }, 0);
            console.log(6);
        }).then(function() {
            console.log(7); //回调会在resolve后,添加到micro-task队列
        });
    });
    console.log(8);
    return 9;
})()
//控制台打印的结果如下:
//2
//3
//8
//4
//6
//9
//1
//5
//7

转自https://zhuanlan.zhihu.com/p/30894022

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值