JavaScript专题之事件循环
准备知识
1. 进程(process)
进程是系统资源分配一个独立单位,一个程序至少有一个进程。比方说:一个工厂代表一个 CPU, 一个车间就是一个进程,任一时刻,只能有一个进程在运行,其他进程处于非运行状态。
2. 线程(Thread)
线程是CPU调度和分派的基本单位,一个线程只能属于一个进程,一个进程可以有多个线程且至少有一个。比方说一个车间的工人,可以有多个工人一起工作。
生活中常常能看到,某某电脑 CPU 的 4 核 4 线程,其意思是指,这款 CPU 同一时间最多只能运行 4 个线程,所以有些线程会处于工作状态,有的线程会处于中断,堵塞,睡眠状态。
经常看到有很多任务同时在进行,一边工作,一边听歌,还一边下载电影。那是因为这些线程在以闪电般的速度不断的切换主要的几个线程,所以,人的体验上感觉是很多很多任务在同时进行。
3. 栈(stack)
栈是一种数据结构,具有后进先出的特点,最开始进入栈结构的数据反而最后才能出来。
4. 队列(queue)
队列也是一种数据结构,数据只能从一边进,一边出,先进去的自然就先出来。
5. 同步和异步(sync async)
同步和异步关注的消息通信机制,同步在函数调用时,如果调用者没有拿到响应结果,程序会继续等待,知道拿到结果为止。而异步会执行其后的代码,等到有响应结果后,才处理响应。
6. 阻塞和非阻塞(blocking & non-blocking)
阻塞和非阻塞关注的是程序等待调用结果时的状态,阻塞的意思是,在调用结果返回响应前,线程会被挂起占用,程序无法继续往下走,而非阻塞的线程则不会挂起,后面的代码能够继续往下执行。
比方说:我去超市买包薯片,老板告诉我货架上没货了,马上去库房拿,这过程中,老板要我站着等他,直到他拿到货出来给我。这个过程就是阻塞。
如果老板告诉我,可以先回去,他一会去库房拿,拿到了之后打电话给我。这个过程,就是非阻塞的,我不用等待,还可以干其他的事情。
7. 执行栈(execution stack)
js 代码在执行代码时,JS 会给调用代码生成一个执行上下文对象,并将其压入执行上下文栈,首先进入栈底的是全局上下文,然后是函数的执行上下文(Execution Context),函数执行完之后,函数上下文从栈中弹出,直到退出浏览器,全局上下文才从栈底弹出。
用代码举个例子:
var globalName = "window";
var foo1 = function() {
console.log("foo1");
}
var foo2 = function() {
console.log("foo2");
foo1();
}
foo2();
上面的图片大致能够描述执行上下文栈的实现逻辑,有关执行上下文的知识,大家可以翻看我之前的文章 - 《JavaScript 之执行上下文》
二、为什么 JS 是单线程模型?
JavaScript 的一个非常有趣的特性是事件循环模型,与许多其他语言不同,它永不阻塞。 处理 I/O 通常通过事件和回调来执行 – MDN
浏览器主要任务是给用户是视觉和交互上的体验,如果页面使用过程中,偶尔出现阻塞、挂起、无响应的体验一定是非常糟糕的。同时,如果采用多线程同步的模型,那么如何保证同一时间修改了 DOM, 到底是哪个线程先生效呢。
浏览器执行环境的核心思想在于任务调度方式的特别:
哪个任务的优先级高,先来就先运行,直到执行完了才执行下一个,并且同一时刻只能执行一个代码片段,即所谓的单线程模型。
比方说,银行的柜台只开启了一个柜台,每个人想要办理业务,就得先拿号排队,叫到了你的号码,你才能上去办理业务。不能多个人同时在一个柜台办理业务,不然就很容易出差错。
三、事件循环
事件循环是 JS 处理各种事件的核心,由于多个线程同时操作 DOM, 造成不可控的问题,所以 JS 采用了单线程模型。另外,由于所有的事件同步执行,执行完一个才能执行下一个,会造成页面渲染的堵塞。JS 中存在异步事件,用户可以在点击页面的时候,请求网络响应的同事,还可以进行其他的点击操作,保证了页面不会因为网络请求,多种 IO 接口响应慢造成代码执行的堵塞和挂起。
事件循环的顺序是:
一个
接下来我们用代码来解释:
console.log("script start!");
function foo1() {
console.log("foo1");
}
foo1();
setTimeout(function () {
console.log("setTimeout!");
}, 1000);
function foo2() {
console.log("foo2");
}
foo2();
console.log("script end!");
打印:
// script start!
// foo1
// foo2
// script end!
// setTimeout!
那我们尝试把 setTimeout 的延迟时间改为 0,想要立即执行,看会不会立即执行:
console.log("script start!");
function foo1() {
console.log("foo1");
}
foo1();
setTimeout(function () {
console.log("setTimeout!");
}, 0);
function foo2() {
console.log("foo2");
}
foo2();
console.log("script end!");
打印:
// script start!
// foo1
// foo2
// script end!
// setTimeout!
可以看出 setTimeout 属于异步事件,总是会在主线程的任务执行完后才开始执行。
顺便说一下事件循环几个原则:
- 一次只处理一个任务
- 一个任务从开始到完成,不会被其他任务所中断
这两个原则保证了浏览器任务单元的完整性,事件调用的有序性。
四、宏任务和微任务
事件循环的实现本来应该由一个用于宏任务的队列和一个用于微任务的队列进行完成,这使得事件循环要根据任务类型来进行优先处理。
宏任务:
宏任务包括:
- 创建文档对象、解析 HTML、执行主线程代码(script)
- 执行各种事件:页面加载、输入、点击
- setTimout,setInterval 异步事件
宏任务代表一个个离散、独立的工作单元,运行完任务后,浏览器可以进行其他的任务调度,如更新渲染或执行垃圾回收。宏任务需要多次事件循环才能执行完。
微任务:
微任务包括:
- Promise 回调函数
- new MutaionObserver()
微任务是更小的任务,微任务需要尽可能地、通过异步方式执行,微任务更新浏览器的状态,但必须在浏览器执行其他任务之前执行。微任务使得我们避免不必要的 UI 重绘。微任务在一次事件循环中必须全部执行完。
宏任务和微任务的执行优先级原则是:
完成一个宏任务后,执行余下的微任务
同一次事件循环中,宏任务永远在微任务之前执行。
ok,知道了优先级原则后,我们来看一段代码:
console.log(1);
setTimeout(function() {
console.log(2);
new Promise(resolve => {
console.log(3);
resolve(4);
console.log(5);
}).then(data => {
console.log(data);
});
}, 0);
new Promise(resolve => {
console.log(6);
resolve(7);
console.log(8);
}).then(data => {
console.log(data);
});
setTimeout(function() {
console.log(9);
}, 0);
console.log(10);
output:
第一次循环:
// 1
// 6
// 8
// 10
// 7
第二次循环:
// 2
// 3
// 5
// 4
第三次循环
// 9
我们一起来分析以上代码:
- 进入第一次事件循环,script 这个宏任务,输出 1
- 第一个 setTimeout 函数本身是函数调用,属于任务源,setTimeout 的回调函数,即第一个参数,才是被分发的任务,任务被加入宏任务队列,第二次循环时调用。
- Promise 属于微任务,但是 Promise 初始化中代码会立即进行。所以会立即输出 6 和 8;
- Promise 初始化后的回调放入微任务队列
- 第二个 setTimeout 也属于宏任务源,回调函数的任务放入宏任务队列,第三次事件循环时调用
- 继续调用栈,输出 10, 没毛病
- 第一次事件循环的宏任务执行完毕,执行余下的所有微任务,所以输出 7,
- 第二次事件循环,发现有宏任务,即第一个 setTimeout 的回调,输出 2,调用 Promise 构建函数的调用栈,直接执行,所以输出3 和 5
- 第一个 setTimeout 的 promise 回调放入微任务队列。
- 第二次事件循环的宏任务调用执行完,执行刚才前一步 Promise 创建的微任务,输出 4,第二次循环执行完毕。
- 进入第 3 次事件循环,只有一个宏任务,即第二个 SetTimeout,所以输出 9;
关于事件循环宏任务和微任务的执行过程:
- 首先两个类型的任务都是逐个执行
- 微任务会前下一个渲染或垃圾回收前全部执行完
- 一次事件循环中先只执行一个宏任务,在下一次事件循环前执行完所有的微任务,包括新创建的微任务。
五、web worker
尽管 HTML5 新标准加入了 web worker 的多线程技术,但是 web worker 只能用于计算,并且 JS 的多线程 worker 无法操作 DOM, 不然就无法控制页面是在被谁操作的了。
主线程传给子线程的数据是通过拷贝复制,同样子线程传给主线程的数据也是通过拷贝复制,而不是共享同一个内存空间。
以上说明,JS 不存在线程同步,所以还是可以把 JS 看做单线程模型,把 web worker 当做 JS 的一种回调机制。
总结
事件循环是 JS 和 Nodejs 事件调用机制的核心,保证了页面可以有序无阻塞的进行。
事件循环的主要逻辑是先执行调用栈,直到清空调用栈只剩下全局上下文。
然后 JS 检查宏任务队列,如果有任务则取出一个进行调用,进行页面渲染和垃圾回收。
同时将所有的微任务源派发的任务加入微任务事件队列,最后执行余下的所有微任务。微任务执行后完,进行页面渲染和垃圾回收后进行下一轮事件循环。
JavaScript专题之事件循环的更多相关文章
- 深入理解javascript中的事件循环event-loop
前面的话 本文将详细介绍javascript中的事件循环event-loop 线程 javascript是单线程的语言,也就是说,同一个时间只能做一件事.而这个单线程的特性,与它的用途有关,作为浏览器 ...
- JavaScript中的事件循环机制跟函数柯里化
一.事件循环机制的理解 test();//按秒输出5个5 function test() { for (var i = 0; i < 5; i++) { setTimeout(() => ...
- JavaScript中的事件循环
JavaScript是单线程单并发语言 单线程:主程序只有一个线程,即同一时间片段内其只能执行单个任务. 引发的问题: 单线程,意味着任务都需要排队,前一个任务结束,才会执行后一个任务.若前一个任务耗 ...
- 总结:JavaScript异步、事件循环与消息队列、微任务与宏任务
本人正在努力学习前端,内容仅供参考.由于各种原因(不喜欢博客园的UI),大家可以移步我的github阅读体验更佳:传送门,喜欢就点个star咯,或者我的博客:https://blog.tangzhen ...
- (转)总结:JavaScript异步、事件循环与消息队列、微任务与宏任务
前言 Philip Roberts 在演讲 great talk at JSConf on the event loop 中说:要是用一句话来形容 JavaScript,我可能会这样: “JavaSc ...
- 聊一聊JavaScript中的事件循环
一.概念:事件循环 JavaScript是单线程的 1.整片 script 整体代码(第一个宏任务)放到执行栈中,执行之后,会触发很多方法 这些方法只能一个个的顺序执行,不能并发 2.这些要执行的方法 ...
- 浅谈Javascript单线程和事件循环
单线程 Javascript 是单线程的,意味着不会有其他线程来竞争.为什么是单线程呢? 假设 Javascript 是多线程的,有两个线程,分别对同一个元素进行操作: function change ...
- JavaScript:理解事件循环
话说js是单线程的,它通过浏览器事件循环轮询事件队列,来实现异步.然而,事件循环的时机是什么时候?浏览器是如何帮助JS引擎线程实现异步的? 浏览器页面进程的四个线程 首先说一下,chrome会为每一个 ...
- javascript 运行机制 事件循环 浏览器缓存 (慕课网 前段跳槽面试必备 4-1,4-2,4-3)
4-1 渲染机制:-1-,什么是DOCTYPE及其作用?DTD(document type definition,文档类型定义)是一系列的语法规则,用来定义XML或(X)HTML的文件类型,浏览器会使 ...
随机推荐
- rabbitmqctl: Error unable to connect to node 'rabbit@xxxxx' nodedown
RabbitMQ使用rabbitmqctl add_user的时候报以下错误: Creating user "xxxx" ... Error: unable to connect ...
- 宝锋UV-5R说明书下载
宝锋UV-5R说明书 百度网盘下载地址: 链接: https://pan.baidu.com/s/1QJXEJ2YyO7ovMAQG7Uur4A 提取码: j8d2 BI8EJM 73 ...
- Flink 从 0 到 1 学习 —— Flink 配置文件详解
前面文章我们已经知道 Flink 是什么东西了,安装好 Flink 后,我们再来看下安装路径下的配置文件吧. 安装目录下主要有 flink-conf.yaml 配置.日志的配置文件.zk 配置.Fli ...
- linux 操作系统级别监控 TOP命令
Top命令是Linux下一个实时的.交互式的,对操作系统整体监控的命令,可以对CPU.内存.进程监控. 是Linux下最常用的监控命令. 第一行是任务队列信息 1 user 当前登录用户数load a ...
- Scala 系列(十三)—— 隐式转换和隐式参数
一.隐式转换 1.1 使用隐式转换 隐式转换指的是以implicit关键字声明带有单个参数的转换函数,它将值从一种类型转换为另一种类型,以便使用之前类型所没有的功能.示例如下: // 普通人 clas ...
- 理解setState
近来在学习react源码, 最初是直接从入口一行一行的看, 结果跟着调用的函数跳转来跳去头都晕了. 后来决定带着一个目的去看源码, 每次看只研究一个东西. 一开始最想了解的就是充满魔性的setStat ...
- Selenium+python操作id为动态变化的frame(iframe)
先定位到一组frame:ele = dr.find_elements_by_tag_name('iframe')此时获得一组frame 再通过index取需要切进去的frame并取到该frame的id ...
- Ubuntu server16.04安装配置驱动418.87、cuda10.1、cudnn7.6.4.38、anaconda、pytorch超详细解决
目录 安装GCC 安装NVIDIA驱动 1. 卸载原有驱动(没装跳过) 2. 禁用nouveau 3. 安装NVIDIA显卡驱动 安装CUDA10.1 安装cudnn 安装anaconda 安装ten ...
- 基于Babylon.js编写宇宙飞船模拟程序1——程序基础结构、物理引擎使用、三维罗盘
计划做一个宇宙飞船模拟程序,首先做一些技术准备. 可以访问https://ljzc002.github.io/test/Spacetest/HTML/PAGE/spacetestwp2.html查看测 ...
- Spring boot - 梳理 - 根本上说,Spring Boot项目只不过是一个普通的Spring项目,只是使用了Spring Boot的起步依赖和自动配置
根本上说,Spring Boot项目只不过是一个普通的Spring项目,只是使用了Spring Boot的起步依赖和自动配置