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

当面试官问你的时候他是想知道什么--事件循环机制 #1

Open
hanxiaosss opened this issue Jun 19, 2020 · 0 comments
Open
Labels
interview deal with problem

Comments

@hanxiaosss
Copy link
Owner

hanxiaosss commented Jun 19, 2020

当面试官问你的时候他是想知道什么--事件循环机制

不知道大家面试的时候有没有和我一样的困惑,明明知识点复习过,自己工作中也会用到,但是当面试官问道“你能不能讲下.../你对...的原理了解吗/...”的时候我就一下蒙了,三言两语草草回答了,自己也深知面试官不是为了考倒你才问这样的问题,主要目的还是通过你的回答来看你的知识体系,思维方式以及掌握程度,他想要的答案绝对不是三两句说一下概念。

我总结了一下造成这种的原因有:

  1. 知识没有系统的连接起来,碎片化,导致一时很难组织语言回答。
  2. 理论知识没有与实践结合。
  3. 没有理解面试官问这个的意图

那么,当面试官问你一个问题的时候,他想得到的回答是什么呢。为了巩固一下我自己的知识,决定写一系列面试角度来讲解 js 基础知识知识点的文章。

第一篇是关于 js 事件循环机制。

你对 Javascript 的事件循环机制了解吗?说一下你的看法。

单线程

单线程是 JavaScript 的主要特点之一。这一特点与很多语言 (C/C++,Java) 不同。而设计这一特点的原因与 JavaScript 的用途密切相关 --作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM 。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。这对于性能和用户体验都是非常不友好的,为此 JavaScript 的设计者就设计了一系列策略来解决并发模型的执行顺序 --也就是我们这篇要讲的事件循环机制

概念

在学习事件循环的机制前先了解其中的一些关于 javascript 的前置知识:

执行栈与消息队列

image

执行栈:函数调用形成了一个由若干帧组成的栈。

function foo(b) {
  let a = 10;
  return a + b + 11;
}

function bar(x) {
  let y = 3;
  return foo(x * y);
}

console.log(bar(7)); // 返回 42

当调用 bar 时,第一个包含了 bar 的参数和局部变量。 当 bar 调用 foo 时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含 foo 的参数和局部变量。当 foo 执行完毕然后返回时,第二个帧就被弹出栈(剩下 bar 函数的调用帧 )。当 bar 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了。

消息队列:一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。
在 事件循环 期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。
函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。

:对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。

同步任务与异步任务

同步任务 指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。同步任务位于主线程执行栈。

异步任务 指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。
"任务队列"中的事件,常见有IO设备的事件、一些用户产生的事件(比如鼠标点击、页面滚动等等)。以及 setTimeOut/setInterval 的回调事件,promise 的then函数事件。值得一提的是放入任务队列的任务通常是回调里面的任务。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

setTimeout(()=>{
  console.log('执行异步代码')
})
console.log('执行同步代码')
while(true){
}

setTimeout 的回调函数就是 JavaScript 提供的一种异步操作,上面的代码执行你会会发现即使 setTimeout 的代码位于整个代码的最前面,也没有被立即执行,而是被挂起来等待主线程执行完毕,由于主线程的同步代码陷入死循环,一直无法将线程让给异步任务,导致 setTimeout 回调函数一直都不会被执行。这个例子也可以看出 JavaScript 的单线程特性。

EventLoop

JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其它语言中的模型截然不同,比如 C 和 Java。 --mdn

可以看出 EventLoop 是一种处理并发执行时顺序的模型,主要就是提高基于单线程的 JavaScript 遇到 I/O 设备等耗时很长的任务时的效率和性能

宏任务与微任务

浏览器端事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个。(我的理解微任务队列只有一个是因为每次主线程任务一执行完栈空,都会将微任务全部出队列压入执行栈,清空微任务队列)

常见的 macro-task 比如:setTimeout、setInterval、script(整体代码)、 I/O 操作、UI 渲染等。

常见的 micro-task 比如: new Promise().then(回调)、MutationObserver(html5新特性) 等。

执行宏任务出队列进入执行栈时是一个任务一个任务的出队列,而执行微任务出队列是一队出队列直至微任务队列为空

原理

  1. 当执行一整段代码 (script) 的时候其实相当于一个宏任务的执行,到执行下一个宏任务为一轮事件循环

  2. 执行时将同步任务压入执行栈,进行立即执行。异步任务则分别添加到宏任务队列和微任务队列的头部(队列的访问规则是先进先出 (FIFO) ,从尾部弹出)。执行栈中同步任务的执行会产生新的宏任务和微任务分别加到对应消息队列

  3. 同步任务执行完毕出栈,栈空后,如果微任务队列有任务,则将微任务依次出队列压入执行栈,进行第二步的执行。如果在微任务里创建了新的微任务,会一直执行微任务里创建的微任务,你可以在浏览器里试试,setTimeout里不断创建 setTimeout 不会造成浏览器“假死”,Promise.then() 里不断执行Promise.then()会导致浏览器 “假死” ,也就是会一直阻塞事件循环

  4. 当微任务全部出队列,队列为空,执行栈执行完毕栈空后,宏任务出队压入执行栈中,宏任务一次执行一个任务,进行下一轮的事件循环。

整个流程大致如下图:

image

范例

我们根据以上的执行原理讲解来解释一下执行的顺序。示例:

setTimeout(() =>//异步宏任务
console.log(4)
)

new Promise(resolve => {
  resolve()
  console.log(1) //同步任务1
}).then(_ => {//异步微任务
  console.log(3)
})

console.log(2)  //同步任务2

分析一下根据前面讲解的 eventLoop 执行顺序分析一下上述代码事项过程:

  • 首先开始时执行栈空,将同步任务一次压入执行栈中(关于执行栈中的执行机制后续文章也会讲解),即代码注释中的同步任务1和同步任务2 。遇到 setTimeout 回调函数将他添加到消息队列里的宏任务队列, promise的then函数添加到消息队列的微任务队列。同步任务执行完将输出1,2,执行栈空

注意:我们说的异步队列里面存放的是回调事件以及promise的then函数,所以promise主体内容还是同步任务。

  • 执行栈空后,会查看微任务队列,里面存放着promise的then函数任务(注释中的异步微任务),将其出队列压入执行栈中,执行完毕输出3,执行栈空。
  • 执行栈空后,微任务队列也空了,从宏任务队列出队列一个宏任务(注释中的异步宏任务)压入执行栈中,执行完毕,输出4,执行栈空

注意:微任务队列存在多个微任务时也会一次出队列直至队列空

浏览器输出结果:

image

一个更复杂的执行过程例子:

setTimeout(()=>{     //macro队列0
  console.log('异步宏任务1')
  new Promise(resolve=>{
    resolve()
    console.log('promise1')
    })
    .then(()=>{//micro队列1
      console.log('异步微任务1 ')
      })
  })

  new Promise(resolve=>{
    resolve()
    console.log('promise2') //执行栈stack0
    })
    .then(()=>{//micro队列0
      console.log('异步微任务2')
      setTimeout(()=>{  //macro队列1
      console.log('异步宏任务2')  
        })
      })

console.log('主线任务')//执行栈stack0

分析:

  • 开始时执行栈空,将同步任务压入执行栈(即注释中执行栈stack0代码),遇到异步微任务将其添加的micro队列(即注释中micro队列0),将宏任务添加到macro队列(即注释中micro队列0)。主线程任务执行完输出 promise2 主线任务。执行栈空。

  • 执行栈空后,会查看微任务队列,将刚才压入微任务(即注释中micro队列0)出队列压入执行栈中,开始执行时解析到异步宏任务任务(即注释中macro队列1)将其添加到宏任务队列头部。执行完毕输出,异步微任务2,执行栈空。

注意消息队列的队列决定了先进先出(FIFO)的执行顺序,为了便于理解后添加的任务将描述为添加到队列头部,从尾部按顺序出队列

  • 执行栈空后,查看微任务队列为空。从宏任务队列中取出尾部第一个宏任务(即注释中micro队列0),压入执行栈中,开始执行时遇到异步任务(即注释中的micro队列1)将其添加到微任务队列。主线任务执行完输出 异步宏任务1 promise1.执行栈空

执行栈空后,查看微任务队列发现微任务(即注释中的micro队列1),将其出队列压入执行栈中,执行完毕输出 异步微任务1

  • 执行栈
    ,将其出队列压入执行栈中,执行完毕输出 异步微任务1。执行栈空
  • 执行栈空后,查看微任务队列为空,查看宏任务队列将第二个宏任务(即注释中macro队列1)出队列压入执行栈中,执行完毕输出 异步宏任务2 ,执行栈空

浏览器运行输出结果:
image

零延迟

零延迟并不意味着回调会立即执行。以 0 为第二参数调用 setTimeout 并不表示在 0 毫秒后就立即调用回调函数。

其等待的时间取决于队列里待处理的消息数量。在下面的例子中,"这是一条消息" 将会在回调获得处理之前输出到控制台,这是因为延迟参数是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间。

基本上,setTimeout 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间。

const s = new Date().getSeconds();

setTimeout(function() {
  // 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
  console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);

while(true) {
  if(new Date().getSeconds() - s >= 2) {
    console.log("Good, looped for 2 seconds");
    break;
  }
}

//TODO 应用 1.手写一个promise的实现 2.vue.nextTick原理及源码

参考链接:

并发模型与事件循环

JavaScript 运行机制详解:再谈Event Loop

浏览器与Node的事见循环(EventLoop)有何区别

@hanxiaosss hanxiaosss added the interview deal with problem label Jun 19, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interview deal with problem
Projects
None yet
Development

No branches or pull requests

1 participant