Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 运行机制详述:Event Loop #30

Open
yanyue404 opened this issue May 27, 2018 · 0 comments
Open

JavaScript 运行机制详述:Event Loop #30

yanyue404 opened this issue May 27, 2018 · 0 comments

Comments

@yanyue404
Copy link
Owner

yanyue404 commented May 27, 2018

一、单线程的 Javascript

诞生之初,JavaScript 被设计为单线程、非阻塞、异步、解释性的语言,作为客户端脚本在网页浏览器环境运行,主要用来向 HTML 页面添加交互行为。

为了并发地处理事件,JavaScript 程序输入/输出是使用事件和回调函数执行的。例如,这意味着 JavaScript 可以在等待数据库查询返回信息时处理鼠标单击。ECMAScript ES6 引入了 Promise 用于优雅地处理异步事件,其可以使得传统的基于回调的异步代码更加清晰与简单。

在最新的 HTML5 中提出了 Web-Worker,但 javascript 是单线程这一核心仍未改变。所以一切 javascript 版的"多线程"都是用单线程模拟出来的,将来也不会变。

二、任务队列

单线程模式就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

为了解决排队等待的耗时问题,聪明的 Javascript 设计者将任务分为了:

  • 同步任务
  • 异步任务

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

任务分类:

  • 同步任务:UI 渲染,alert
  • 异步任务:ajax (XMLHttpRequest),定时器,DOM 事件监听、资源下载

主线程处理任务队列的运行机制如下:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

三、什么是 Event Loop

主线程在空闲的时候从 task queue 中取出事件来执行,这个过程是循环不断的,所以整个的这种运行机制又称为 Event Loop(事件循环)。

下图(转引自 Philip Roberts 的演讲《What the heck is the event loop anyway?

bg2014100802.png

Call Stack 例子

function multiply(a, b) {
  return a * b;
}

function square(n) {
  return multiply(n, n);
}

function printSquare(n) {
  var squared = square(n);
  console.log(squared);
}

printSquare(4);

执行时发生了什么:

  • Call Stack:main 文件自身执行
  • 自上而下声明了每个函数
  • Call Stack:推入栈 printSquare(4);
  • Call Stack:推入栈 square(n);
  • Call Stack:推入栈 multiply(n,n);
  • Call Stack:弹出栈 multiply(n,n);
  • Call Stack:弹出栈 square(n);
  • Call Stack:console.log(squared);
  • Call Stack:弹出栈 printSquare(4);

Event loop 例子

$.on("button", "click", function onClick() {
  setTimeout(function timer() {
    console.log("You clicked the button!");
  }, 2000);
});

console.log("Hi!");

setTimeout(function timeout() {
  console.log("Click the button!");
}, 5000);

console.log("Welcome to loupe.");

Save + run 发生了什么:

执行的区域我们分为 Call Stack (调用栈)、Web Apis (浏览器环境的 API 线程)、Callback Queue(回调队列)

  • Call Stack:代码块依次进入主线程(以下描述省略这一步);
  • Web Apis:注册 $.on('button', 'click', ...)
  • Call Stack:console.log("Hi!");
  • Web Apis:注册 setTimeout(timeout, 5000)
  • Call Stack:console.log("Welcome to loupe.");
  • Web Apis:5S 后 setTimeout(√) 清空
  • Callback Queue:加入setTimeout(timeout, 5000)的回调函数 timeout;
  • Call Stack:主线程空闲;
  • Call Stack:执行 回调函数 timeout => console.log("Click the button!");

注意执行结束后: Web Apis 依然存在 $.on('button', 'click', ...)事件,等待执行,除非事件被销毁;

四、宏任务与微任务

当主线程空闲时(执行栈为空),主线程会先查看微任务队列,执行清空后再查看宏任务队列,并执行清空,如此反复循环。

15fdcea13361a1ec.png

micro-task(微任务):Promise.then、MutationObserver、process.nextTick(Node 环境)、setImmediate (Node 环境)

macro-task(宏任务):script 代码块、setTimeout、setInterval、I/O、UI rendering

举个例子:

new Promise((resolve) => {
  console.log(1);
  setTimeout(() => {
    console.log(2);
  }, 0);
  Promise.resolve().then(() => {
    console.log(3);
  });
  resolve();
}).then(() => {
  console.log(4);
});
console.log(5);

new Promise在实例化的过程中所执行的代码都是同步进行的,故会立即执行,而then中注册的回调是异步执行的(在resolve方法的调用下才会执行)

下面是代码的执行分析:

  • 第一次事件循环开始
    • 这段代码作为宏任务,进入主线程
    • new Promise 立即执行 log:1
    • 遇到 setTimeout,注册其回调函数后分发到 宏任务队列
    • 微任务队列 + Promise.resolve().then() 的回调函数
    • 微任务队列 + Promise.resolve().then().then() 的回调函数
    • log: 5
    • 开始执行微任务
      • Promise.resolve().then() =》 log:3
      • Promise.resolve().then().then() =》 log:4
  • 第一次事件循环结束,task queue 非空
  • 第二次事件循环开始
    • 执行 宏任务 setTimeout callback log:2
    • 没有微任务
  • 第二次事件循环结束, task queue 清空
  • 代码执行完毕

async/await 函数

因为 async/await 本质上还是基于 Promise 的一些封装,而 Promise 是属于微任务的一种。所以在实际使用上效果类似:async 函数在 await 之前的代码都是同步执行的,可以理解为 await 之前的代码属于new Promise时传入的代码,await 之后的所有代码都是在Promise.then中的回调。

setTimeout((_) => console.log(4));

async function main() {
  console.log(1);
  await Promise.resolve();
  console.log(3);
}

main();

console.log(2);

下面是代码的执行分析:

  • 第一次事件循环开始
    • 整段代码作为宏任务,进入主线程
    • 宏任务队列 + setTimeout callback
    • 立即执行 async main,相当于 new Promise(),紧接着 log: 1,微任务队列 + Promise.then()
    • log:2
    • 执行所有微任务 Promise.then() log:3
  • 第一次事件循环结束, task queue 非空
  • 第二次事件循环开始
    • 执行宏任务 setTimeout callback log:4
    • 没有微任务
  • 第二次事件循环结束, task queue 为空
  • 代码执行完毕

参考

@yanyue404 yanyue404 changed the title Javascript事件循环与异步 深入Javascript执行机制 May 30, 2018
@yanyue404 yanyue404 changed the title 深入Javascript执行机制 我所知道的JavaScript中的Event Loop Sep 25, 2019
@yanyue404 yanyue404 changed the title 我所知道的JavaScript中的Event Loop JavaScript 运行机制详述:Event Loop Jun 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant