惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
雷峰网
雷峰网
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Spread Privacy
Spread Privacy
H
Hacker News: Front Page
PCI Perspectives
PCI Perspectives
Webroot Blog
Webroot Blog
罗磊的独立博客
H
Heimdal Security Blog
TaoSecurity Blog
TaoSecurity Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
Last Week in AI
Last Week in AI
美团技术团队
Help Net Security
Help Net Security
The Hacker News
The Hacker News
C
Cisco Blogs
T
The Blog of Author Tim Ferriss
J
Java Code Geeks
The Register - Security
The Register - Security
IT之家
IT之家
WordPress大学
WordPress大学
Jina AI
Jina AI
Recent Commits to openclaw:main
Recent Commits to openclaw:main
H
Help Net Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Threat Research - Cisco Blogs
P
Proofpoint News Feed
NISL@THU
NISL@THU
爱范儿
爱范儿
The GitHub Blog
The GitHub Blog
Scott Helme
Scott Helme
V
Vulnerabilities – Threatpost
B
Blog
T
Tenable Blog
博客园 - 三生石上(FineUI控件)
T
The Exploit Database - CXSecurity.com
S
Security Affairs
小众软件
小众软件
Hacker News: Ask HN
Hacker News: Ask HN
Security Latest
Security Latest
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
W
WeLiveSecurity
A
Arctic Wolf
L
LINUX DO - 热门话题
Google DeepMind News
Google DeepMind News
M
MIT News - Artificial intelligence

青空之蓝

[青空之蓝-2023] - 色彩 | 青空之蓝 [青空之蓝-2022] - 平静 | 青空之蓝 [青空之蓝-2021] - 远望 | 青空之蓝 浅谈垃圾回收 | 青空之蓝 浅谈泛型擦除 | 青空之蓝 浅谈单点登录 | 青空之蓝 使用 Kotlin 编写 Spring 测试 | 青空之蓝 设计模式系列文章 | 青空之蓝 从零实现一个 Java 微框架 - IoC | 青空之蓝 从零实现一个 Java 微框架 - 前言 | 青空之蓝 浅谈 JVM:类加载 | 青空之蓝 浅谈 IO | 青空之蓝 浅谈并发:synchronized & ReentrantLock | 青空之蓝 浅谈并发:CAS & AQS | 青空之蓝 浅谈并发:ThreadLocal | 青空之蓝 浅谈并发:三大特性 | 青空之蓝 浅谈组合注解 & 注解别名 | 青空之蓝 [青空之蓝-2020]-迷茫 | 青空之蓝 Java 系列文章 | 青空之蓝 HTTP 系列文章 | 青空之蓝 浅谈 EatWhatYouKill | 青空之蓝 浅谈可扩展线程池 | 青空之蓝 聊聊写框架 | 青空之蓝 聊聊现状-[2020-09] | 青空之蓝 浅谈并发:锁 | 青空之蓝 浅谈并发:基础 | 青空之蓝 浅谈缓存 | 青空之蓝 无须定义类,Spring 快速注入 Json 参数 | 青空之蓝 浅谈 Proxy 和 Aop | 青空之蓝 从零实现一个 PHP 微框架 - 初始化请求 | 青空之蓝 为 Vue3 添加一个简单的 Store | 青空之蓝 从零实现一个 PHP 微框架 - 服务提供者 | 青空之蓝 WSL2 踩坑记录 | 青空之蓝 从零实现一个 PHP 微框架 - Bootstrap 启动加载 | 青空之蓝 从零实现一个 PHP 微框架 - IoC 容器 | 青空之蓝 从零实现一个 PHP 微框架 - PSR & Composer | 青空之蓝 从零实现一个 PHP 微框架 - 前言 | 青空之蓝 MVVM 简单实现 | 青空之蓝 浅谈 DI 和 IoC | 青空之蓝 中间件实现 [PHP] | 青空之蓝 告别 Windows 终端的难看难用,打造好用的 PowerShell | 青空之蓝 VSCode Java输出中文乱码问题解决[更新] | 青空之蓝 浅谈浏览器渲染 | 青空之蓝 Vue-Cli@2 项目迁移日志 | 青空之蓝 Laragon & Scoop 集成踩坑记录 | 青空之蓝 「一行代码」优雅管理 Windows 软件 | 青空之蓝 [青空之蓝-2019]-年度总结 | 青空之蓝 为Vue添加简单的Store | 青空之蓝 为React添加简单的Store | 青空之蓝 为Vuex添加同步Action | 青空之蓝 浅谈B+树 | 青空之蓝 浅谈跳表 | 青空之蓝 浅谈数据库索引 | 青空之蓝 MySQL事务隔离 | 青空之蓝 算法复杂度分析(1) | 青空之蓝 一年来的经验总结 | 青空之蓝 Acrylic - VSCode Extension | 青空之蓝 ace编辑器设置惯性滚动 | 青空之蓝 Java二叉树实现 | 青空之蓝 Java图实现 | 青空之蓝 为apt方式安装的nginx重新编译增加WebDAV | 青空之蓝 XK-Editor - 一个支持富文本和Markdown的编辑器 | 青空之蓝 JS生成列表树 | 青空之蓝 Laravel生成目录树 | 青空之蓝 XK-Note - 集各种神奇功能的云笔记 | 青空之蓝 PHP GD生成验证码 | 青空之蓝 PHP GD图片处理[转换格式-水印-缩略图] | 青空之蓝 Origami - 简洁轻快的WordPress主题 | 青空之蓝 为WordPress启用WorkBox | 青空之蓝 [青空之蓝-2018]-年度总结 | 青空之蓝 VSCode Java手动导入jar和源码包 | 青空之蓝 Windows IP变化自动发送邮件 | 青空之蓝 C链表实现重制版 | 青空之蓝 C 结构体的定义和使用 | 青空之蓝 图的搜索(遍历) - BFS & DFS | 青空之蓝 Java链表实现 | 青空之蓝 C 快速排序 | 青空之蓝 C 插入排序 | 青空之蓝 C 归并排序 | 青空之蓝 C语言链表实现 | 青空之蓝 VSCode配置Java调试环境[Windows] | 青空之蓝 C 选择排序 | 青空之蓝 C 冒泡排序 | 青空之蓝 VSCode配置PHP调试环境[Windows] | 青空之蓝 VSCode配置C/C++ GDB调试环境[Windows] | 青空之蓝 WordPress友情链接模板 | 青空之蓝 Intel Optane 傲腾内存体验 | 青空之蓝 Mysql双机热备实战 | 青空之蓝 博客一年记录 | 青空之蓝 为WordPress启用Service Worker | 青空之蓝 Bing每日一图API | 青空之蓝 iframe延迟加载 | 青空之蓝 写在2018年高考前 | 青空之蓝 The Fox主题汉化分享 | 青空之蓝 [青空之蓝-2017]-崭新 | 青空之蓝 本博客评论规则 | 青空之蓝 世界,您好! | 青空之蓝
浅谈浏览器Event Loop [更新] | 青空之蓝
Otstar Lin · 2020-06-24 · via 青空之蓝

更新此篇文章的原因是看到一个 JSConf 关于事件循环的演讲,建议有能力的(能上 YouTube)看看这个演讲。

Jake Archibald: 在循环 - JSConf.Asia

什么是 Event Loop?

若你了解过 JavaScript,你一定知道 JavaScript 是一种单线程语言,为什么 JavaScript 是单线程呢?为什么不使用多线程呢?JavaScript 作为浏览器脚本语言(虽然现在也在后端挺流行的),JavaScript 的主要用途是与用户互动以及操作 DOM。若使用多线程就会导致一些问题,比如更新丢失等问题,当一个线程要删除 DOM 的时候另一个线程要更改它,那浏览器该如何操作呢。所以 JavaScript 为了避免多线程带来的一系列问题采用了单线程的运行机制。

而若只是单纯的同步单线程的执行便会导致 JS 运行到某个需要等待的位置时就会造成假死状态,比如当 JS 要从网络中获取一张巨大的图片,发起了 HTTP 请求,在等待 HTTP 请求中若是不采用某种机制来处理的话就会导致卡住的假死状态,我们可以用 Java 来模拟一下:

import java.util.Date;

public class Main {
   public static void main(String[] args) {
      // 打印该句执行的时间
      System.out.println("begin: " + new Date().getTime());
      try {
         // 利用sleep模拟请求时的状态
         Thread.sleep(5000);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      // 打印该句执行的时间
      System.out.println("end: " + new Date().getTime());
   }
}
import java.util.Date;

public class Main {
   public static void main(String[] args) {
      // 打印该句执行的时间
      System.out.println("begin: " + new Date().getTime());
      try {
         // 利用sleep模拟请求时的状态
         Thread.sleep(5000);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
      // 打印该句执行的时间
      System.out.println("end: " + new Date().getTime());
   }
}

从上面的运行结果可以看到,同步运行的时候 Java 在 begin 和 end 中间隔了非常久的时间,程序也在那时候被阻塞住了,而 UI 是不能被阻塞的否则会严重影响用户体验,所以 JS 采用异步来防止这种情况发生。

当 JS 线程执行到需要异步的操作的时候就会把该任务发到任务队列,然后继续向下执行,当所有的同步代码都执行完毕的时候,JS 线程就会从任务队列读取任务并执行,若遇到异步操作就继续入队。。。如此往复,这就是Event Loop(事件循环)

Task

在浏览器中存在着一个任务队列,如上图,左侧绿色的部分,任务队列中存放着将要执行的任务,当任务队列中没有任务的时候,事件循环会进入空转的状态,但这并不代表浏览器是休眠的,从上图我们可以看到,除了任务队列外,事件循环还要处理渲染相关的任务。一旦有任务进入任务队列了,浏览器会在可以执行任务的时机从任务队列中取出任务并执行,执行完一个任务后就进入下一个循环,而不是逐个取出任务执行直到任务队列为空。

为了容易理解 Task,我们通常把 Task 分成 MacroTask (宏任务)MicroTask (微任务),宏任务很好理解,就是一些异步的任务,如 setTimeout 等等,而微任务比较常见的就是 Promise。微任务会在宏任务执行完后,并且 JS 调用栈为空的时候执行。具体的过程下面会分析。

Promise

Promise 是 ES6 新增的一种异步解决方案,它的运行方式是当需要进行 I/O,等待等异步操作的时候,不返回结果而是返回一个 Promise("承诺"),当这个承诺完成的时候,即状态变为 fulfilled 或者 rejected ,这个 promise 就定格了,也可以认为返回值是一样的了,你可以在任何位置任何时间利用 then 得到这个结果(返回值),或许这有点难以理解,那就举个例子吧:

var re = "foo";

var promise1 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    resolve(re);
  }, 300);
});

promise1.then(function (value) {
  console.log("one call value: " + value);
  console.log("one vall re prev: " + re);
  re = "foo2";
  console.log("one call re: " + re);
});

promise1.then(function (value) {
  console.log("two call value: " + value);
  console.log("two call re: " + re);
});

console.log(promise1);

// > [object Promise]
// > "one call value: foo"
// > "one vall re prev: foo"
// > "one call re: foo2"
// > "two call value: foo"
// > "two call re: foo2"
var re = "foo";

var promise1 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    resolve(re);
  }, 300);
});

promise1.then(function (value) {
  console.log("one call value: " + value);
  console.log("one vall re prev: " + re);
  re = "foo2";
  console.log("one call re: " + re);
});

promise1.then(function (value) {
  console.log("two call value: " + value);
  console.log("two call re: " + re);
});

console.log(promise1);

// > [object Promise]
// > "one call value: foo"
// > "one vall re prev: foo"
// > "one call re: foo2"
// > "two call value: foo"
// > "two call re: foo2"

运行重置输入

从上面的运行结果可以看到 re 确实在第一次调用 promise1 的时候被修改为 foo2,但是当第二次调用 promise 时的 value 并没有跟着改变,也就是说 promise 不会再调用第二次,而是直接返回结果。

async/await

ES7 中添加了 asyncawait 的关键字,async 返回的必定是 Promise,可以理解为就是异步函数,await 是等待 Promise。

async 可以使一个函数成为异步函数,返回 Promise,我们可以把 async 认为使new Promise 的语法糖,所以当函数 return 值的时候 return 的不是值而是 Promise,若要得到 async 中 return 的值就需要使用 then

var fun = async function foo() {
  return "hello";
};

var re = fun();
console.log(re);
re.then((value) => console.log(value));

// > [object Promise]
// > "hello"
var fun = async function foo() {
  return "hello";
};

var re = fun();
console.log(re);
re.then((value) => console.log(value));

// > [object Promise]
// > "hello"

运行重置输入

await 是等待后面东西,可以是 Promise,可以是值,可以是表达式,当可以直接得到值的时候 await 会立即返回值,但若是 Promise,await 就会将 JS 阻塞住,直到 Promise 兑现,有了 await,我们可以把异步的 JS 写成同步的 JS,可以有效的解决回调地狱。

一个有用的例子

console.log("sync1");

setTimeout(function () {
  //1
  console.log("sync_timeout1");
}, 0);

var promise = new Promise(function (resolve, reject) {
  setTimeout(function () {
    //2
    console.log("pro_new_timeout");
  }, 0);
  console.log("pro_new");
  resolve();
});

promise.then(() => {
  console.log("pro_then");
  setTimeout(() => {
    //3
    console.log("pro_timeout");
  }, 0);
});

setTimeout(function () {
  //4
  console.log("sync_timeout2");
}, 0);
console.log("sync2");
console.log("sync1");

setTimeout(function () {
  //1
  console.log("sync_timeout1");
}, 0);

var promise = new Promise(function (resolve, reject) {
  setTimeout(function () {
    //2
    console.log("pro_new_timeout");
  }, 0);
  console.log("pro_new");
  resolve();
});

promise.then(() => {
  console.log("pro_then");
  setTimeout(() => {
    //3
    console.log("pro_timeout");
  }, 0);
});

setTimeout(function () {
  //4
  console.log("sync_timeout2");
}, 0);
console.log("sync2");

运行重置输入

看到这个代码,是不是很晕,先不要放到 JS 中运行,让我们一起来看看这个的输出,你可以先不看以下的步骤自己思考一下,或许能得到不小收获。

首先,sync1 的 console.log 肯定是第一个输出的,调用栈先进了 console,然后出栈,控制台输出sync1

然后程序来到了第一个 setTimeout,这是一个异步宏任务,所以放到宏任务队列中,然后跳过该 setTimeout。MacroTask Queue:[setTimeout-1]

接下来程序来到了 new promise,new promise 是同步的,所以会进入到 function 中,遇到第二个 setTimeout,此时将这个 setTimeout 放到宏任务队列中,然后跳过 setTimeout,执行到 console.log,输出pro_new,接着遇到 resolve,是微任务将其放到微任务队列中,然后退出 function。MacroTask Queue:[setTimeout-1,setTimeout-2],MicroTask Queue:[resolve]

接着遇到了 promise 的 then,这是属于 resolve 的回调,当 resolve 状态改变的时候才执行,所以跳过该部分。MacroTask Queue:[setTimeout-1,setTimeout-2],MicroTask Queue:[resolve]

然后又遇到了一个 setTimeout,同样将其放到宏任务队列中。MacroTask Queue:[setTimeout-1,setTimeout-2,setTimeout-4],MicroTask Queue:[resolve]

接着遇到了最后一个 console.log,输出sync2,此时同步代码已经执行完毕。

微任务列表不为空,所以需要在这个 tick 中执行,不能先取宏任务,调用 resolve 的 then,输出pro_then,同时将 setTimeout 放入宏任务队列。MacroTask Queue:[setTimeout-1,setTimeout-2,setTimeout-4,setTimeout-3],MicroTask Queue:[]

微任务队列空了,从宏任务中取出队首的任务,即 setTimeout-1,执行后输出sync_timeout1

取出队首任务,setTimeout-2,输出pro_new_timeout

取出队首任务,setTimeout-4,输出sync_timeout2

取出队首任务,setTimeout-3,输出pro_timeout,此时宏任务列表和微任务队列为空,js 引擎进入等待状态。

  • sync1
  • pro_new
  • sync2
  • pro_then
  • sync_timeout1
  • pro_new_timeout
  • sync_timeout2
  • pro_timeout

你们猜对了吗?反正我第一次是没猜对(逃

一些值得了解的地方

很多文章会把 Task 分成 MacroTask 和 MicroTask,并说明 MicroTask 是在 MacroTask 的末尾执行完毕,其实这并不准确。我们来看看以下代码:

button.addEventListener("click", () => {
  Promise.resolve().then(() => console.log("Microtask 1"));
  console.log("Listener 1");
});
button.addEventListener("click", () => {
  Promise.resolve().then(() => console.log("Microtask 2"));
  console.log("Listener 2");
});
button.addEventListener("click", () => {
  Promise.resolve().then(() => console.log("Microtask 1"));
  console.log("Listener 1");
});
button.addEventListener("click", () => {
  Promise.resolve().then(() => console.log("Microtask 2"));
  console.log("Listener 2");
});

代码很简单,为一个按钮注册两个点击事件,当用户点击后,控制台会输出一些日志:

  • Listener 1
  • Microtask 1
  • Listener 2
  • Microtask 2

按照微任务在宏任务完成后执行的流程,对照这输出,似乎并没有错误,但是,如果我们手动触发点击事件会如何呢?

button.addEventListener("click", () => {
  Promise.resolve().then(() => console.log("Microtask 1"));
  console.log("Listener 1");
});
button.addEventListener("click", () => {
  Promise.resolve().then(() => console.log("Microtask 2"));
  console.log("Listener 2");
});
button.click();
button.addEventListener("click", () => {
  Promise.resolve().then(() => console.log("Microtask 1"));
  console.log("Listener 1");
});
button.addEventListener("click", () => {
  Promise.resolve().then(() => console.log("Microtask 2"));
  console.log("Listener 2");
});
button.click();

此时的输出就变成了:

  • Listener 1
  • Listener 2
  • Microtask 1
  • Microtask 2

从以上输出可以看到,微任务一定会在该宏任务下执行完毕的说法是错误的,那么,是什么原因造成二者的差异呢?其实,这是就是我在 Task 章节里说到的,微任务会在宏任务执行完后并且 JS 调用栈为空的时候执行。在第二段代码中,执行完 Listener 1 宏任务的时候,JS 调用栈还存在着 button.click() 这个函数的栈帧,所以 Microtask 1 微任务就无法被执行,直到 Listener 2 完成的时候,button.click() 函数调用帧才会出栈,此时 JS 调用栈才为空,微任务才可开始执行,所以就有了以上的输出。

结语

学了好久的前端,之前对异步只处于会用的状态,前几天刚了解了一下 JS 的异步,刚好好久没写过文章了,便自己整理了写成一篇文章,输出才是最好的学习,其实是为了水文章(逃。( ̄ y▽, ̄)╭

浅谈浏览器Event Loop [更新]

https://blog.ixk.me/post/talking-about-browser-event-loop
  • 许可协议

    BY-NC-SA

  • 本文作者

    Otstar Lin

  • 发布于

    2020/06/24

转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!

WSL2 踩坑记录从零实现一个 PHP 微框架 - Bootstrap 启动加载