准备知识

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 属于异步事件,总是会在主线程的任务执行完后才开始执行。

顺便说一下事件循环几个原则:

  1. 一次只处理一个任务
  2. 一个任务从开始到完成,不会被其他任务所中断

这两个原则保证了浏览器任务单元的完整性,事件调用的有序性。

四、宏任务和微任务

事件循环的实现本来应该由一个用于宏任务的队列和一个用于微任务的队列进行完成,这使得事件循环要根据任务类型来进行优先处理。

宏任务:

宏任务包括:

  1. 创建文档对象、解析 HTML、执行主线程代码(script)
  2. 执行各种事件:页面加载、输入、点击
  3. setTimout,setInterval 异步事件

宏任务代表一个个离散、独立的工作单元,运行完任务后,浏览器可以进行其他的任务调度,如更新渲染或执行垃圾回收。宏任务需要多次事件循环才能执行完。

微任务:

微任务包括:

  1. Promise 回调函数
  2. 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

我们一起来分析以上代码:

  1. 进入第一次事件循环,script 这个宏任务,输出 1
  2. 第一个 setTimeout 函数本身是函数调用,属于任务源,setTimeout 的回调函数,即第一个参数,才是被分发的任务,任务被加入宏任务队列,第二次循环时调用。
  3. Promise 属于微任务,但是 Promise 初始化中代码会立即进行。所以会立即输出 6 和 8;
  4. Promise 初始化后的回调放入微任务队列
  5. 第二个 setTimeout 也属于宏任务源,回调函数的任务放入宏任务队列,第三次事件循环时调用
  6. 继续调用栈,输出 10, 没毛病
  7. 第一次事件循环的宏任务执行完毕,执行余下的所有微任务,所以输出 7,
  8. 第二次事件循环,发现有宏任务,即第一个 setTimeout 的回调,输出 2,调用 Promise 构建函数的调用栈,直接执行,所以输出3 和 5
  9. 第一个 setTimeout 的 promise 回调放入微任务队列。
  10. 第二次事件循环的宏任务调用执行完,执行刚才前一步 Promise 创建的微任务,输出 4,第二次循环执行完毕。
  11. 进入第 3 次事件循环,只有一个宏任务,即第二个 SetTimeout,所以输出 9;

关于事件循环宏任务和微任务的执行过程:

  1. 首先两个类型的任务都是逐个执行
  2. 微任务会前下一个渲染或垃圾回收前全部执行完
  3. 一次事件循环中先只执行一个宏任务,在下一次事件循环前执行完所有的微任务,包括新创建的微任务。

五、web worker

尽管 HTML5 新标准加入了 web worker 的多线程技术,但是 web worker 只能用于计算,并且 JS 的多线程 worker 无法操作 DOM, 不然就无法控制页面是在被谁操作的了。

主线程传给子线程的数据是通过拷贝复制,同样子线程传给主线程的数据也是通过拷贝复制,而不是共享同一个内存空间。

以上说明,JS 不存在线程同步,所以还是可以把 JS 看做单线程模型,把 web worker 当做 JS 的一种回调机制。

总结

事件循环是 JS 和 Nodejs 事件调用机制的核心,保证了页面可以有序无阻塞的进行。

事件循环的主要逻辑是先执行调用栈,直到清空调用栈只剩下全局上下文。

然后 JS 检查宏任务队列,如果有任务则取出一个进行调用,进行页面渲染和垃圾回收。

同时将所有的微任务源派发的任务加入微任务事件队列,最后执行余下的所有微任务。微任务执行后完,进行页面渲染和垃圾回收后进行下一轮事件循环。

JavaScript专题之事件循环的更多相关文章

  1. 深入理解javascript中的事件循环event-loop

    前面的话 本文将详细介绍javascript中的事件循环event-loop 线程 javascript是单线程的语言,也就是说,同一个时间只能做一件事.而这个单线程的特性,与它的用途有关,作为浏览器 ...

  2. JavaScript中的事件循环机制跟函数柯里化

    一.事件循环机制的理解 test();//按秒输出5个5 function test() { for (var i = 0; i < 5; i++) { setTimeout(() => ...

  3. JavaScript中的事件循环

    JavaScript是单线程单并发语言 单线程:主程序只有一个线程,即同一时间片段内其只能执行单个任务. 引发的问题: 单线程,意味着任务都需要排队,前一个任务结束,才会执行后一个任务.若前一个任务耗 ...

  4. 总结:JavaScript异步、事件循环与消息队列、微任务与宏任务

    本人正在努力学习前端,内容仅供参考.由于各种原因(不喜欢博客园的UI),大家可以移步我的github阅读体验更佳:传送门,喜欢就点个star咯,或者我的博客:https://blog.tangzhen ...

  5. (转)总结:JavaScript异步、事件循环与消息队列、微任务与宏任务

    前言 Philip Roberts 在演讲 great talk at JSConf on the event loop 中说:要是用一句话来形容 JavaScript,我可能会这样: “JavaSc ...

  6. 聊一聊JavaScript中的事件循环

    一.概念:事件循环 JavaScript是单线程的 1.整片 script 整体代码(第一个宏任务)放到执行栈中,执行之后,会触发很多方法 这些方法只能一个个的顺序执行,不能并发 2.这些要执行的方法 ...

  7. 浅谈Javascript单线程和事件循环

    单线程 Javascript 是单线程的,意味着不会有其他线程来竞争.为什么是单线程呢? 假设 Javascript 是多线程的,有两个线程,分别对同一个元素进行操作: function change ...

  8. JavaScript:理解事件循环

    话说js是单线程的,它通过浏览器事件循环轮询事件队列,来实现异步.然而,事件循环的时机是什么时候?浏览器是如何帮助JS引擎线程实现异步的? 浏览器页面进程的四个线程 首先说一下,chrome会为每一个 ...

  9. javascript 运行机制 事件循环 浏览器缓存 (慕课网 前段跳槽面试必备 4-1,4-2,4-3)

    4-1 渲染机制:-1-,什么是DOCTYPE及其作用?DTD(document type definition,文档类型定义)是一系列的语法规则,用来定义XML或(X)HTML的文件类型,浏览器会使 ...

随机推荐

  1. .Net基础篇_学习笔记_第五天_流程控制while循环003

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...

  2. python安装第三方包的安装路径, dist-packages和site-packages区别

    简单来说 如果是系统自带的python,会使用dist-packages目录 如果你手动安装python,它会直接使用目录site-packages 这允许你让两个安装隔离开来 dist-packag ...

  3. 进击的.NET 在云原生时代的蜕变

    你一定看过这篇文章 <进击的 Java ,云原生时代的蜕变>,  本篇文章的灵感来自于这篇文章.明天就将正式发布.NET Core 3.0, 所以写下这篇文章让大家全面认识.NET Cor ...

  4. C#面试题目整理(一)

    1.您在什么情况下会用到虚方法?它与接口有什么不同?当子类需要重新定义父类的一个方法时,父类的方法需要定义为虚方法:在定义接口的时候不能又方法体,但是虚方法可以有方法体,实现时,子类可以不实现父类的虚 ...

  5. librosa语音信号处理

    librosa是一个非常强大的python语音信号处理的第三方库,本文参考的是librosa的官方文档,本文主要总结了一些重要,对我来说非常常用的功能.学会librosa后再也不用用python去实现 ...

  6. [翻译] ASP.NET Core 3.0 的新增功能

    ASP.NET Core 3.0 的新增功能 全文翻译自微软官方文档英文版 What's new in ASP.NET Core 3.0 本文重点介绍了 ASP.NET Core 3.0 中最重要的更 ...

  7. 校园网打开IEEE 显示未登录

    校园网访问IEEE 显示未登录,如图 解决办法 1.打开网络和共享中心 2.如图 3.把ipv6的钩去掉 4.把host文件(在C:\Windows\System32\drivers\etc)复制到桌 ...

  8. Mac 安装python 3.*新版本的详细步骤

    Mac 系统自带python,不过自带的python版本都是2.*版本.虽然不影响老版本项目的运行, 但是python最新的3.*版本的一些语法与2.*版本并不相同,我们不论是学习还是使用,当然用最新 ...

  9. LeetCode 430. Faltten a Multilevel Doubly Linked List

    题目链接:LeetCode 430. Faltten a Multilevel Doubly Linked List class Node { public: int val = NULL; Node ...

  10. Flask中的路由、实例化参数和config配置文件

    Flask中的路由 endpoint 别名不能重复,对应的视图函数,默认是视图函数名.endpoint 才是路由的核心.视图函数与路由的对应关系.可以通过url_for 反向创建url # metho ...