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

对event loop的理解 #1

Open
se7en00 opened this issue Mar 19, 2018 · 0 comments
Open

对event loop的理解 #1

se7en00 opened this issue Mar 19, 2018 · 0 comments

Comments

@se7en00
Copy link
Owner

se7en00 commented Mar 19, 2018

最近在看到一道面试题时,困惑住了

setTimeout(function() {
  console.log(4)
}, 0);
new Promise(function(resolve) {
  console.log(1);
  for (var i = 0; i < 10000; i++) {
    i == 9999 && resolve()
  }
  console.log(2)
}).then(function() {
  console.log(5)
}).then(function() {
    console.log(6)
});
console.log(3);

out >> 1,2,3,5,6,4
out i think >> 3,4,1,2,5,6
本以为promise跟setTimeout是一样的,多是异步的,JS遇到这些代码,会先把回调函数注册到任务队列中, 等到js主线程执行完同步的代码后(也就是执行完console.log(3))再去执行刚才注册到任务队形的回调函数。但是从结果中可以看出Promise构造函数中的resolve函数的执行先于then的回调函数,then回调函数先于setTimeout回调,而又在console.log(3)后,也就是在JS主线程后。为什么会这样呢!

通过查看一些文章才明白,这些是跟JS event loop有关!上面的script(整体代码)以及其中的setTimeout都属于js event loop中macro-task, 而promise属于micro-task,micro-task的优先级要大于macro-task。
看到这里我相信有部分人会跟我一样懵B了, js event loop, macro-task, micro-task什么东东,没听说过。感觉自己真是too young too nactive! 好吧,废话说了这么多,进入正题~(以下内容仅属个人看法,有误之处希望批评指正)

JS Event loop

event loop(事件循环) 根据宿主环境可以分为浏览器下的,workers和nodeJS中的,本文主要是基于浏览器环境的。在这三个环境中,多是只能有一个event loop。

那event loop用来干什么呢?
主要用来实现js的异步机制,js的异步机制简单的说就是不停的循环(JS主线程->先执行同步的代码后->再从任务队列读取事件)的这一过程。

那为什么有这么个event loop呢?
那还不是因为JS一开始就是一门单线程语言,所以对于处理一些耗时比较长的任务,就会阻塞后面的任务,对不对,就像我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来。所以要变个法子来去阻塞化,要异步的去加载那些能阻塞主线程执行的代码。那怎么个异步法呢?

alt text

从上图中我们可以了简单描述这个event loop(事件循环):

  1. 主线程运行的时候,产生堆(内存分配发生的地方)和栈(函数调用时会形一个个栈帧,其实就是js的执行栈),主线程中同步的代码会进入栈中,当遇到setTimout, promise,用户交互、脚本、UI 渲染、网络请求等各种异步任务时,就将他丢给WebAPIs,接着执行同步任务,直到栈为空。

  2. 在此期间WebAPIs完成这个事件,把异步任务中回调函数放入CallbackQueue中等待;

  3. 当执行栈为空时,Event Loop把Callback Queue中的一个任务放入栈中,回到第1步。

    :JS的执行栈,就是我们平常所说的执行上下文,包括global执行上下文和function执行上下文, global执行上下文永远在栈底,当我们执行一个function时,会产生一个function执行上下文,放到顶部,然后进入该执行上下文并初始 变量对象this, 作用域链,初始完后并开始执行代码,遇到function时,重复上面的步骤,当执行环境中的代码执行完毕,退出这个js执行环境并销毁。

这一下子我们对event loop有了个大概的了解。如果你想深入的了解event loop, 这里有几篇深入的文章:

Macro-task

其实上图中script(整体代码),DOM事件,AJAX, setTimout多属于Macro-task。
如果查看了HTML规范,发现Macro-task其实就是event loop里的task, 而HTML规范中的task是这么定义它的:

  1. 一个eventloop有一或多个task队列(上图中的第2步的异步任务其实是放到task队列中,wepAPI就是每个task关联的document

  2. 一个task队列是一列有序的task,而且是一个先进先出的队列,主要用来做以下工作:Events task,Parsing task, Callbacks task, Using a resource task, Reacting to DOM manipulation task等

  3. 每个task都有自己相关的document,比如我们发起AJAX异步请求时,进入到task队列,这时document就是xmlhttprequest, 其实也就是上图中的webAPI.

  4. 每个task由一个确定的task source提供, task source包括如下:

    • DOM 操作任务源:如元素以非阻塞方式插入文档
    • 用户交互任务源:如鼠标键盘事件。用户输入事件(如 click) 必须使用 task 队列
    • 网络任务源:如 XHR 回调
    • history 回溯任务源:使用 history.back() 或者类似 API

    而且从同一个task source来的task必须放到同一个task队列中,从不同源来的则被添加到不同队列中

  5. 各个task source的优先级有可能是不一样的,比如会为鼠标键盘事件(用户交互)设置更高优先级,这样会使用户感觉流畅.在、再比如自己遇到一个坑:

    //ajax
    adminAccountService.getByUserEmail(email, () => {
      console.log(1)
      });
    
     setTimeout(() => {
         console.log(2);
     }, 0);
    
     out >> 2, 1
     从结果可以看出AJAX的task source的优先级要低于setTimeout的task srouce
    

有了上面的定义后,当主线程运行到DOM事件,AJAX, setTimeout时,发现你们原来是Macro-task, 不属于我的管辖区内,然后说等你们辖区(各自的web API)处理好你们再来通知道我(回调函数),于是就把他们扔到各自的管辖区内(任务队列)

Micro-task

一般来说,microtask 包括:

  • Promise.then

    Promise 规范中提及 Promise.then 的具体实现由平台把握,可以是 microtask 或 task。当前的共识是使用 microtask 实现。

  • Object.observe
  • MutationObserver

我们例子中的promise.then其实就是Mico-task.
了解概念性的东西,我们还是去查看HTML规范,从规范中我们可以大概了解到:

  1. 每个event loop 多会有一个micro-task队列。Micro-task 会排在 microtask 队列而非 Macro-task 队列中

  2. 当当前Macro-task执行完毕后,执行栈为空时(只剩global执行上下文时)会立刻先处理所有Micro-task队列中的任务, 按顺序全部执行,直到队列为空。如果 microtask 中又添加了新的 microtask,直接放进本队列末尾。当micro-task队列多已经执行完后并再去Macro-task队列中取出task去执行...如此反复的循环。Micro-task是跟在Macro-task末尾执行,像个小跟班。

为什么只有Promise.then是Micro-task, then中的回调是什么时候进入到Micro-task队列中的呢?而new Promise()构造函数中的resolve和reject函数为什么不是Micro-task呢?

这个要查看ES的规范我们明白下面几点:

  1. 在ES规范中的job就是Micro-task,job队列也是HTML规范中的Micro-task队列, 而ES中的job queue至少包含以下两种Job队列:
Name Purpose
ScriptJobs Jobs that validate and evaluate ECMAScript Script and Module source text. See clauses 10 and 15.
PromiseJobs PromiseJobs
  1. 如果调用 then 时 promise 是 pending 状态,回调会进入 promise 的 [[PromiseFulfill/RejectReactions]] 列表里;只有该promise 被fulfill(resolve)reject 时,其中的[[PromiseFulfill/RejectReactions]]里的每个reactions会创建一个对应的PromiseJob,并被按序通过EnqueueJob 方式进入到Job队列(PromiseJobs)
  2. 如果调用 then 时promise是fulfilled或者rejected,则会直接进入Job队列(PromiseJobs),也就是Micro-task队列。
  3. 把promisJob加入到job队列,规范中只有通过EnqueueJob的方式才会进入job队列,如上面2,3两点中,而且Promise的构造函数中没有该方式, 只有PerformPromiseThen中第8,9步通过该方式加入到job队列中,所以而new Promise()构造函数中的回调是同步的代码。

Event loop的执行模型

通过上面的macro-task, micro-task定义我们知道:

  • macro-task: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
  • micro-task: Promise.then, Object.observe, MutationObserver

现在我们结合macro,micro task 看看event loop是如何来执行一个任务的流程.
当主线程开始读取完代码(整体代码),执行栈为空时,开始依次执行:

  1. 如果 macro-task A 为null (那任务队列就是空),直接跳到第5步
  2. 将 currently running task 设置为 macro-task A
  3. 执行 macro-task A (也就是执行回调函数)
  4. 将 currently running task 设置为 null 并移出 macro-task A
  5. 执行 microtask 队列
    • a:在 microtask 中选出最早的任务 micrtask X
    • b:如果 micrtask X 为null (那 microtask 队列就是空),直接跳到 g
    • c:将 currently running task 设置为 microtask X
    • d:执行 microtask X
    • e:将 currently running task 设置为 null 并移出 microtask X
    • f:在 microtask 中选出最早的任务 , 跳到 b
    • g:结束 microtask 队列
  6. 跳到第一步

通过这个简单的event loop 执行模型我们在来看最开始的例子:

//步骤1: 开始执行首个eventloop的marco task(整体代码)阶段
setTimeout(function() { //步骤2:立刻将 callback(console.log(4)) 放入 marco task 队列中
  console.log(4)
}, 0);
const promise =new Promise(function(resolve) { //步骤3: Promise 构造函数是同步代码,不会放到marco/micro task队列中,输出1
  console.log(1);
  for (var i = 0; i < 10000; i++) {
    i == 9999 && resolve()   //步骤4:遇到resolve函数此时Promise的状态变成Fulfilled
  }
  console.log(2)  //步骤5: 输出2
})
promise.then(function() { //步骤6: 立刻将 callback(console.log(5)) 放入 microtask 队列中
  console.log(5)
}).then(() => {
  console.log(6)
});
console.log(3); //步骤7: 输出3
//步骤8: 首个eventloop的 marco task(整体代码)阶段执行完毕,执行栈为空,开始执行 microtask,
        发现有一个callback(console.log(5)),执行之,**输出 5**,同时又将callback(console.log(6))放入 microtask 队列中。
//步骤9:发现 microtask 队列不为空,执行callback(console.log(6)),**输出 6**
//步骤10: microtask 队列为空,执行 UI render(根据机器负荷等环境影响,综合浏览器策略,此步骤可能执行也可能不执行)
//步骤11:首次eventloop结束。执行第二轮eventloop,取出一个 marco-task callback(console.log(4)),执行之,**输出 4**

参考:

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