You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了。
当面试官问你的时候他是想知道什么--事件循环机制
不知道大家面试的时候有没有和我一样的困惑,明明知识点复习过,自己工作中也会用到,但是当面试官问道“你能不能讲下.../你对...的原理了解吗/...”的时候我就一下蒙了,三言两语草草回答了,自己也深知面试官不是为了考倒你才问这样的问题,主要目的还是通过你的回答来看你的知识体系,思维方式以及掌握程度,他想要的答案绝对不是三两句说一下概念。
我总结了一下造成这种的原因有:
那么,当面试官问你一个问题的时候,他想得到的回答是什么呢。为了巩固一下我自己的知识,决定写一系列面试角度来讲解 js 基础知识知识点的文章。
第一篇是关于 js 事件循环机制。
单线程
单线程是
JavaScript
的主要特点之一。这一特点与很多语言 (C/C++,Java) 不同。而设计这一特点的原因与JavaScript
的用途密切相关 --作为浏览器脚本语言,JavaScript
的主要用途是与用户互动,以及操作 DOM 。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript
同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript
就是单线程,这已经成了这门语言的核心特征,将来也不会改变。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。这对于性能和用户体验都是非常不友好的,为此
JavaScript
的设计者就设计了一系列策略来解决并发模型的执行顺序 --也就是我们这篇要讲的事件循环机制概念
在学习事件循环的机制前先了解其中的一些关于
javascript
的前置知识:执行栈与消息队列
执行栈:函数调用形成了一个由若干帧组成的栈。
当调用 bar 时,第一个包含了 bar 的参数和局部变量。 当 bar 调用 foo 时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含 foo 的参数和局部变量。当 foo 执行完毕然后返回时,第二个帧就被弹出栈(剩下 bar 函数的调用帧 )。当 bar 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了。
消息队列:一个
JavaScript
运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。在 事件循环 期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。
函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。
堆:对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。
同步任务与异步任务
同步任务 指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。同步任务位于主线程执行栈。
异步任务 指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。
"任务队列"中的事件,常见有IO设备的事件、一些用户产生的事件(比如鼠标点击、页面滚动等等)。以及
setTimeOut/setInterval
的回调事件,promise
的then函数事件。值得一提的是放入任务队列的任务通常是回调里面的任务。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。setTimeout
的回调函数就是JavaScript
提供的一种异步操作,上面的代码执行你会会发现即使setTimeout
的代码位于整个代码的最前面,也没有被立即执行,而是被挂起来等待主线程执行完毕,由于主线程的同步代码陷入死循环,一直无法将线程让给异步任务,导致setTimeout
回调函数一直都不会被执行。这个例子也可以看出JavaScript
的单线程特性。EventLoop
可以看出
EventLoop
是一种处理并发执行时顺序的模型,主要就是提高基于单线程的JavaScript
遇到I/O
设备等耗时很长的任务时的效率和性能宏任务与微任务
浏览器端事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个。(我的理解微任务队列只有一个是因为每次主线程任务一执行完栈空,都会将微任务全部出队列压入执行栈,清空微任务队列)
执行宏任务出队列进入执行栈时是一个任务一个任务的出队列,而执行微任务出队列是一队出队列直至微任务队列为空
原理
当执行一整段代码 (script) 的时候其实相当于一个宏任务的执行,到执行下一个宏任务为一轮事件循环
执行时将同步任务压入执行栈,进行立即执行。异步任务则分别添加到宏任务队列和微任务队列的头部(队列的访问规则是先进先出 (FIFO) ,从尾部弹出)。执行栈中同步任务的执行会产生新的宏任务和微任务分别加到对应消息队列
同步任务执行完毕出栈,栈空后,如果微任务队列有任务,则将微任务依次出队列压入执行栈,进行第二步的执行。如果在微任务里创建了新的微任务,会一直执行微任务里创建的微任务,你可以在浏览器里试试,setTimeout里不断创建
setTimeout
不会造成浏览器“假死”,Promise.then()
里不断执行Promise.then()
会导致浏览器 “假死” ,也就是会一直阻塞事件循环当微任务全部出队列,队列为空,执行栈执行完毕栈空后,宏任务出队压入执行栈中,宏任务一次执行一个任务,进行下一轮的事件循环。
整个流程大致如下图:
范例
我们根据以上的执行原理讲解来解释一下执行的顺序。示例:
分析一下根据前面讲解的
eventLoop
执行顺序分析一下上述代码事项过程:setTimeout
回调函数将他添加到消息队列里的宏任务队列,promise
的then函数添加到消息队列的微任务队列。同步任务执行完将输出1,2,执行栈空浏览器输出结果:
一个更复杂的执行过程例子:
分析:
开始时执行栈空,将同步任务压入执行栈(即注释中执行栈stack0代码),遇到异步微任务将其添加的micro队列(即注释中micro队列0),将宏任务添加到macro队列(即注释中micro队列0)。主线程任务执行完输出 promise2 主线任务。执行栈空。
执行栈空后,会查看微任务队列,将刚才压入微任务(即注释中micro队列0)出队列压入执行栈中,开始执行时解析到异步宏任务任务(即注释中macro队列1)将其添加到宏任务队列头部。执行完毕输出,异步微任务2,执行栈空。
,将其出队列压入执行栈中,执行完毕输出 异步微任务1。执行栈空
浏览器运行输出结果:
零延迟
其等待的时间取决于队列里待处理的消息数量。在下面的例子中,"这是一条消息" 将会在回调获得处理之前输出到控制台,这是因为延迟参数是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间。
基本上,setTimeout 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间。
参考链接:
并发模型与事件循环
JavaScript 运行机制详解:再谈Event Loop
浏览器与Node的事见循环(EventLoop)有何区别
The text was updated successfully, but these errors were encountered: