Event Loop

JavaScript的单线程

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题

若以多线程的方式操作这些 DOM,可能出现操作冲突,假设有两个线程同时操作一个 DOM 元素,一个线程要求删除 DOM,而另一个线程要求修改DOM,这时浏览器就无法决定采用哪个线程的操作。所以,为了避免复杂性,从一诞生,JavaScript就是单线程

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质

因为是单线程的,在某一时刻内只能执行特定的一个任务,并且会阻塞其他任务执行。那么对于类似 I/O (Input/Output的缩写,指的是计算机系统中输入输出数据的操作,比如Ajax操作从网络读取数据等耗时的任务,就没必要等待它们执行完后才继续后面的操作。在这些任务完成前,JavaScript 完全可以往下执行其他操作,当这些耗时的任务完成后则以回调的方式执行相应处理

通过事件循环机制回调函数,JavaScript可以处理大量的异步IO操作,避免了阻塞线程和影响用户体验的问题

Event Loop(事件循环)

Event Loop是一个执行模型,在不同的地方有不同的实现,浏览器和NodeJS基于不同的技术实现了各自的Event Loop

  • 浏览器的Event Loop是在html5的规范中明确定义
  • NodeJS的Event Loop是基于libuv实现的。可以参考Node的官方文档以及libuv的官方文档
  • libuv已经对Event Loop做出了实现,而HTML5规范中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商

Stack(三种含义)

第一种含义:数据结构

数据的存放方式,特点为LIFO,即后进先出(Last in, first out)

在这种数据结构中,数据像积木那样一层层堆起来,后面加入的数据就放在最上层。使用的时候,最上层的数据第一个被用掉,这就叫做"后进先出"

第二种含义:代码运行方式

调用栈(call stack),表示函数或子例程像堆积木一样存放,以实现层层调用,遵循后进先出的原则。每个执行任务被称为帧(stack of frames),函数调用形成了一个由若干帧组成的执行栈

第三种含义:内存区域

存放数据的一种内存区域,程序运行的时候,需要内存空间存放数据。一般来说,系统会划分出两种不同的内存空间:一种叫做stack(栈),另一种叫做heap(堆)

栈内存(Stack):有序存放,自动分配相对固定大小的内存空间,并由系统自动释放,用于存放基本类型和对象变量的指针(例如:Boolean、Number、String、Undefined、Null等)

堆内存(Heap):任意存放,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。heap是没有结构的,堆内存是动态分配内存,内存大小不一,也不会自动释放。用于存放JavaScript中的引用类型(例如:Object、Array、Function等)

任务队列

所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入任务队列(task queue)的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行

宏队列和微队列

宏队列(macrotask)ES6 规范中也叫task, 一些异步任务的回调会依次进入task queue,等待后续被调用,这些异步任务包括:

  • setTimeout / setInterval
  • I/O操作(例如:文件读写、网络请求等)
  • setImmediate (Node独有,用于将一个函数作为回调注册到事件循环的下一次迭代中)
  • requestAnimationFrame (浏览器独有,用于在下一帧动画渲染前执行的任务)
  • UI rendering (浏览器独有,例如:DOM操作、页面重绘等)
  • UI交互事件(例如:鼠标点击、键盘输入等)
  • postMessage(用于多个window或iframe间的通信)
  • MessageChannel(用于两个端口之间的通信)

微队列(microtask)ES6 规范中也叫jobs, 另一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:

  • process.nextTick (Node独有)
  • Promise(在promise.then()中注册的回调函数是微任务)
  • Object.observe(已废弃,用于观察对象属性变化)
  • MutationObserver(用于监听DOM变化)
  • async / await(async函数内await后面的代码是微任务)

注:宏任务和微任务的具体实现和支持情况可能会因不同的环境(浏览器、Node.js等)而有所不同。

浏览器的Event Loop

我们先来看一张图,再看完这篇文章后,请返回来再仔细看一下这张图,相信你会有更深的理解。

这张图将浏览器的Event Loop完整的描述了出来,这是执行一个JavaScript代码的具体流程:

  1. 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
  2. 全局Script代码执行完毕后,执行栈会清空;
  3. 从微队列microtask queue中取出位于队首的回调任务,放入执行栈中执行;
  4. 继续取出位于队首的任务,放入执行栈中执行,以此类推,直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行
  5. microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,执行栈也为空;
  6. 取出宏队列macrotask queue中位于队首的任务,放入执行栈中执行;
  7. 执行完毕后,执行栈为空;
  8. 重复第3-7个步骤;
  9. 重复第3-7个步骤;
  10. ......

补充:

  • 宏队列一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务
  • 微任务队列中所有的任务都会被依次取出来执行,直到microtask queue为空
  • 图中没有画UI rendering的节点,因为这个是由浏览器自行判断决定的,但是只要执行UI rendering,它的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render

当event loop 遇到 async/await

Promise 中的异步体现在 then 和 catch 中,所以写在 Promise 中的代码是被当做同步任务立即执行的。而在 async/await 中,在出现 await 出现之前,其中的代码也是立即执行的。那么出现了 await 时候发生了什么呢?

由于因为 async await 本身就是 promise + generator 的语法糖,所以 await 后面的代码是 microtask,所以对于上面代码中的

async function async1() {
	console.log('async1 start');
	await async2();
	console.log('async1 end');
}

        等价于

async function async1() {
	console.log('async1 start');
	Promise.resolve(async2()).then(() => {
        console.log('async1 end');
    })
}

以上,就是浏览器的事件循环 Event Loop

参考文章:

Stack的三种含义 - 阮一峰的网络日志

JavaScript 运行机制详解:再谈Event Loop - 阮一峰的网络日志

javascript - 带你彻底弄懂Event Loop - 前端学习 - SegmentFault 思否

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值