JS之事件循环

前言:最近找工作发现面试官非常喜欢问事件循环,前几天做题也遇到了什么宏任务和微任务,感觉没听过,当然也喜欢问事件委托(事件代理),事件委托的原理比较简单,我这里就不赘述了,主要来整理一下事件循环的一些知识。

事件循环

1 js的单线程

我们都知道,javascript从诞生之日起就是一门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互。

单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务。

而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。

单线程是必要的,也是javascript这门语言的基石,原因之一在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的dom操作。试想一下 如果javascript是多线程的,那么当两个线程同时对dom进行一项操作,例如一个向其添加事件,而另一个删除了这个dom,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,javascript选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。

当然,现如今人们也意识到,单线程在保证了执行顺序的同时也限制了javascript的效率,因此开发出了web worker技术。这项技术号称让javascript成为一门多线程语言。

然而,使用web worker技术开的多线程有着诸多限制,例如:所有新线程都受主线程的完全控制,不能独立执行。这意味着这些“线程” 实际上应属于主线程的子线程。另外,这些子线程并没有执行I/O操作的权限,只能为主线程分担一些诸如计算等任务。所以严格来讲这些线程并没有完整的功能,也因此这项技术并非改变了javascript语言的单线程本质。

可以预见,未来的javascript也会一直是一门单线程的语言。

话说回来,前面提到javascript的另一个特点是“非阻塞”,那么javascript引擎到底是如何实现的这一点呢?答案就是今天这篇文章的主角——event loop(事件循环)
js实现单线程非阻塞就是采用的事件循环(event loop)

2 js的异步过程
2.1基本概念
线程

进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的

进程

线程是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的

异步/同步

同步:一个服务完成需要依赖其他服务时,只能等待被依赖的服务完成以后。
异步:一个服务的

阻塞/非阻塞
异步和非阻塞的区别

同步与异步是对应的,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的。
阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞。

异步:调用在发出之后,这个调用就直接返回,不管有无结果;异步是过程。
非阻塞:关注的是程序在等待调用结果(消息,返回值)时的状态,指在不能立刻得到结果之前,该调用不会阻塞当前线程
阻塞是使用同步机制的结果,非阻塞则是使用异步机制的结果。

2.2 js的异步过程

那么问题来了,js既然是单线程语言,如何实现异步过程呢?
其实,单线程和异步确实不能同时成为一个语言的特性。js选择了成为单线程的语言,所以它本身不可能是异步的,但js的宿主环境(比如浏览器,Node)是多线程的,宿主环境通过某种方式(事件驱动,下文会讲)使得js具备了异步的属性.

js是单线程语言,浏览器只分配给js一个主线程,用来执行任务(函数),但一次只能执行一个任务,这些任务形成一个任务队列排队等候执行,但前端的某些任务是非常耗时的,比如网络请求,定时器和事件监听,如果让他们和别的任务一样,都老老实实的排队等待执行的话,执行效率会非常的低,甚至导致页面的假死。所以,浏览器为这些耗时任务开辟了另外的线程,主要包括http请求线程,浏览器定时触发器,浏览器事件触发线程,这些任务是异步的

js异步过程通常是这样的:主线程发起一个异步的请求,相应的工作线程请求并告知主线程已经收到异步函数的返回;主线程可以继续执行后面的代码。同时工作线程执行异步任务。工作线程工作完成后,通知主线程;主线程收到通知后,执行一定的工作。
异步函数通常具有以下的形式:

A(args…, callbackFn)

所以,从主线程的角度看,一个异步过程包括下面两个要素:

  • 发起函数(或叫注册函数)A
  • 回调函数callbackFn

举个具体的例子:

setTimeout(fn, 1000);

其中的setTimeout就是异步过程的发起函数,fn是回调函数。

3 基本概念

首先了解基本的概念
在 JavaScript 事件循环机制中,使用到了三种数据对象,分别是栈(Stack)、堆(Heap)和队列(Queue)。

  • 栈:一种后进先出(LIFO)的数据结构。可以理解为取乒乓球时的场景,后面放进去的乒乓球反而是最先取出来的。

  • 堆:一种树状的的数据结构。可以理解为在图书馆中取书的场景,可以通过图书索引的方式直接找到需要的书。

  • 队列:一种先进先出(FIFO)的数据结构。即我们平时排队的场景,先排的人总是先出队列

在 JavaScript 事件循环机制中,使用的栈数据结构便是执行上下文栈,每当有函数被调用时,便会创建相对应的执行上下文并将其入栈;使用到堆数据结构主要是为了表示一个大部分非结构化的内存区域存放对象;使用到的队列数据结构便是任务队列,主要用于存放异步任务

执行栈(执行上下文栈)

在js代码运行的过程当中,会进入到不同的执行环境中,一开始执行时最先进入到全局环境,此时全局上下文首先被创建并入栈,之后当调用函数时则进入相应的函数环境,此时相应的函数上下文被创建并入栈,当处于栈顶的执行上下文代码执行完毕后,则将其出栈。

function fn2() {    console.log('fn2')
}function fn1() {    console.log('fn1')
    fn2();
}

fn1();

代码中的执行上下文栈如图所示:
在这里插入图片描述

主线程

要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。
主线程循环:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。
当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。
当主线程将执行栈中所有的代码执行完之后,主线程将会去查看任务队列是否有任务。如果有,那么主线程会依次执行那些任务队列中的回调函数。

任务队列

当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)
在js的事件循环机制中,存在多种任务队列,其分为宏任务(macro-task)和微任务(micor-task)两种。

宏任务与微任务:

异步任务分为 宏任务(macrotask) 与 微任务 (microtask),不同的API注册的任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。

宏任务(macrotask)::
script(整体代码)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 环境)

微任务(microtask):
Promise、 MutaionObserver、process.nextTick(Node.js环境)

不同的任务源对应的任务队列其执行顺序优先级是不同的,上述宏任务和微任务的先后顺序代表了其任务队列执行顺序的优先级。

即在宏任务队列中,各个队列的优先级为
setTimeout > setInterval > I/O
在微任务队列中,各个队列的优先级为
Promise > Object.observe > MutationObserver

4 Event Loop(事件循环)

事件循环机制:
step1:主线程读取JS代码,此时为同步环境,形成相应的堆和执行栈;

step2: 主线程遇到异步任务,指给对应的异步进程进行处理(WEB API);

step3: 异步进程处理完毕(Ajax返回、DOM事件处罚、Timer到等),将相应的异步任务推入任务队列;

step4:主线程查询任务队列,执行microtask queue,将其按序执行,全部执行完毕;

step5:主线程查询任务队列,执行macrotask queue,取队首任务执行,执行完毕;

step6:重复step4、step5。

简单来说,事件循环机制的流程就是,主线程执行 JavaScript 整体代码后将遇到的各个任务源所指定的任务分发到各个任务队列中,然后微任务队列和宏任务队列交替入栈执行直到清空所有的任务队列,全局上下文出栈
在这里插入图片描述

这里要注意的是,任务源所指定的异步任务,并不是立即被放入任务队列中的,而是在接收到响应结果后才会将其放入任务队列中排队。如 setTimeout 中指定延迟事件为 1s,则在 1s 后才会将该任务源所指定的任务队列放入队列中;I/O 交互只有接收到响应结果后才将其异步任务放入队列中排队等待执行

宏任务 > 所有微任务 > 宏任务,如下图所示:
在这里插入图片描述
例子:

    setTimeout(function () {
        console.log(1);    });
    new Promise(function(resolve,reject){
        console.log(2)
        resolve(3)
    }).then(function(val){
        console.log(val);
    })
    console.log(4);

根据本文的解析,我们可以得到:
①先执行script同步代码
先执行new Promise中的console.log(2),then后面的不执行属于微任务
然后执行console.log(4)
②执行完script宏任务后,执行微任务,console.log(3),没有其他微任务了。
③执行另一个宏任务,定时器,console.log(1)。
根据本文的内容,可以很轻松,且有理有据的猜出写出正确答案:2,4,3,1.

参考文献

https://www.imooc.com/article/43693
https://juejin.im/post/5b498d245188251b193d4059
https://blog.youkuaiyun.com/qq_39207948/article/details/81671304

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值