从Javascript单线程谈Event Loop

假如面试回答js的运行机制时,你可能说出这么一段话:“Javascript的事件分同步任务和异步任务,遇到同步任务就放在执行栈中执行,而碰到异步任务就放到任务队列之中,等到执行栈执行完毕之后再去执行任务队列之中的事件。”但你能说出背后的原因吗?

先理解相关概念

线程与进程

进程:是系统资源分配和调度的单元。一个运行着的程序就对应了一个进程。一个进程包括了运行中的程序和程序所使用到的内存和系统资源。

线程:线程是进程下的执行者,一个进程至少会开启一个线程(主线程),也可以开启多个线程。

同步和异步

同步和异步关注的是:消息结果通信机制

同步:发出调用后,在没有得到结果前,该调用不返回。但是一旦调用返回,就得到返回值

异步:发出调用后,调用直接返回,没有返回结果。但结果由回调函数给出,至于什么时候给出,不知道。(这个回调函数肯定是在当前js执行完后才执行)

阻塞与非阻塞

阻塞和非阻塞关注的是:程序在等待调用结果时的状态.

阻塞调用:调用结果返回之前,当前线程被挂起。调用线程只有在得到结果后才会返回。
非阻塞调用:在不能立刻得到结果之前,该调用不会阻塞当前线程。

为什么JavaScript是单线程?

JavaScript是单线程,程序按照顺序排列,前面的必须处理好,后面的才会执行。JavaScript的设计初衷是作为浏览器脚本语言,主要是简单用户交互、操作DOM等,所以这门语言要围绕单线程来设计,否则出现复杂的同步问题。

由于JavaScript是单线程的,对于耗时的或者时间不确定的操作,我们可以使用异步编程,因为异步可以实现非阻塞操作。当然也可以用HTML5标准的Web Worker。本文不作讨论,详细参考MDN文档:点击这里

既然js是单线程执行的,那谁去轮询大的任务队列?这不矛盾了吗?

Js的单线程与异步矛盾吗?

不矛盾!!!首先记住这句话:Js执行是单线程,但浏览器是多线程

1)JS的单线程

一个浏览器进程中只有一个JS的执行线程,同一时刻内只会有一段代码在执行

2)浏览器多线程

查阅资料,有些文章也说是模块,本文就以浏览器是多线程来说,它有以下常驻线程:

渲染引擎线程:负责页面的渲染

JS引擎线程:负责JS的解析和执行(本文说的主线程就指js引擎线程)

定时器触发线程:处理定时事件,比如setTimeout, setInterval

事件触发线程:处理DOM事件

异步http请求线程:处理http请求

......

浏览器是Js的使用场景,浏览器本身是典型的 GUI 工作线程(GUI 工作线程在绝大多数系统中都实现为事件处理,避免阻塞交互)。故浏览器是事件驱动的(Event driven),浏览器中很多行为是异步,会创建事件并放入任务队列中。

由于Javascript 是单线程,它需要借助异步完成耗时或者时间不确定的操作,这些操作由浏览器的其它线程执行,这形成了异步事件驱动。异步事件驱动往往由浏览器的两个或以上常驻线程共同完成的。例如ajax异步请求是由JS执行线程和异步http请求线程,事件触发线程共同完成的。

事件循环机制(Event Loop)

相关概念

函数调用形成一个栈帧。

 1 function foo(b) {
 2   let a = 10;
 3   return a + b + 11;
 4 }
 5
 6 function bar(x) {
 7   let y = 3;
 8   return foo(x * y);
 9 }
10
11 console.log(bar(7));

当调用 bar 时,创建了第一个帧 ,帧中包含了 bar 的参数和局部变量。

当 bar 调用 foo 时,第二个帧就被创建,并被压到第一个帧之上,帧中包含了 foo 的参数和局部变量。

当 foo 返回时,最上层的帧就被弹出栈(剩下 bar 函数的调用帧 )。

当 bar 返回的时候,栈就空了。

对象被分配在一个堆中,一个用以表示一个内存中大的未被组织的区域。

每一个线程只有一个栈,每一个程序只有一个堆。

队列

一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都与一个函数相关联。

当栈为空时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数。

当栈再次为空的时候,也就意味着消息处理结束。

任务队列消息队列

任务队列是一个先进先出的数据结构,当主线程执行栈一清空,任务队列的回调函数就自动进入主线程。任务分成两种:

1、同步任务:在主线程上排队执行的任务。只有执行完当前任务,才能执行后一个任务。

2、异步任务:该任务不进入主线程、而进入任务队列。当执行栈清空后,才去执行任务队列中的任务。

异步执行的运行机制

由于JavaScript只能一次执行一段代码(由于其单线程性质),这些代码块中的每一个都“阻止”其他异步事件的进度。这意味着当异步事件发生时(如鼠标点击,定时器触发或XMLHttpRequest完成),它将排队等待稍后执行(这种排队实际发生的确定会因浏览器到浏览器而异)。

1、所有同步任务都在主线程上执行,形成一个执行栈

2、当遇到异步任务时(IO设备操作等),就在任务队列中添加一个事件,这个事件对应着该异步任务的回调函数。

3、执行栈中的所有同步任务执行完毕,系统就会读取任务队列,进入执行栈,开始执行。

4、主线程不断重复第三步。这就形成了事件循环

结论:Javascript的事件分同步任务和异步任务,遇到同步任务就放在执行栈中执行,而碰到异步任务就放到任务队列之中,等到执行栈执行完毕之后再去执行任务队列之中的事件。

事件和回调函数的概念必要说明

  • 工作线程:是本文对除了js引擎线程之外的其它线程的统称
  • 回调函数:在一个函数中调用另外一个函数。这里指异步场景下为了非阻塞那些被主线程挂起来的代码。
  • 主线程读取任务队列,就是读取里面有哪些事件,执行对应的回调函数。

工作线程完成一项任务,就向任务队列中添加一个事件。这里的完成任务是指完成操作(click、mouse、touch,ajax的数据完全请求回来......),并非指执行它的回调函数

a.onclick = function () {
  console.log("roro")
}

如上段代码,仅是操作了click,但并没有执行回调函数打印roro

事件循环

事件循环是:主线程重复从任务队列中取消息(事件),执行对应回调函数的过程。

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在任务队列中加入各种事件(click,load,done)。只要执行引擎栈栈中的代码执行完毕,主线程就会去读取任务队列,依次执行那些事件所对应的回调函数。

定时器

首先参考这篇外国人的文章:how-javascript-timers-work,定时器的执行原理及细节。

setTimeout()怎么执行?

setTimeout(function () {
  console.log(‘a‘);
}, 5000)

Javascript执行引擎(主线程)运行的时候,产生堆和栈。程序中代码依次进入栈中等待执行,当调用setTimeout()方法时,在浏览器的定时器线程下处理延时方法,当setTimeout方法执行5秒后,到达触发条件,方法被添加到用于回调的任务队列。

当执行引擎的执行栈为空,执行引擎开始轮询检查任务队列是否有任务需要被执行,当检查到已经符合执行条件的延时方法时,将延时方法console.log(a)压入执行栈,引擎发现调用了log()方法,于是又将log()方法入栈。然后对执行栈依次出栈执行,输出‘a’,清空执行栈,整个执行完毕。

setTimeout(fn,0)是立即执行吗?

在javascript权威指南中:当setTimeout的延迟时间设置为0的时候,回调函数不会马上执行,而是进入 事件队列。

btn.onclick = function () {
  setTimeout(function () {
    console.log(‘a‘)
  }, 0);
}

setTimeout(fn,0)的含义是:指定某个任务在主线程的空闲时间下,尽可能早地执行。它被添加进任务队列,因此要等到同步任务和任务队列中的前一个事件都处理完,才会执行。

HTML5标准规定了setTimeout()的第二个参数的最小值不得低于4ms,如果低于这个值,就会自动增加。老版本的浏览器允许最短间隔设为10ms。详细参考MDN文档:最小延迟和超时嵌套

所以setTimeout(fn,0)并不是立即执行。假若你想实现0ms 的timeout可以用window.postMessage(),本文不作讨论。

两道经典的面试题:

一)以下代码输出什么?

function foo() {
  console.log(‘a‘)
  setTimeout(function () {
    console.log(‘b‘);
  }, 500)
}

for (let i = 0; i < 10000; i++) {
  foo()
}

执行结果:首先全部输出a,中间等待500ms,然后全部输出b。上图是个人理解,不恰当的地方请指出!

二)ajax异步请求是否真的异步?

1、JS的执行线程(主线程)发起异步请求,浏览器会开一条新的HTTP请求线程来执行请求,继续执行栈中剩下的任务,

2、在新线程(HTTP请求线程)中,在执行请求的同时,浏览器会正常处理其他任务的执行。

3、在未来的某一时刻,当数据完全请求回来以后,事件触发线程监视到之前发起的HTTP请求已完成,会将指定的回调函数放入任务队列中。

4、当浏览器执行栈空闲时,去扫描任务队列中的回调函数,依次压入执行栈中处理。

所以:ajax请求是异步。由浏览器新开一个线程请求,事件回调的时候放入Event loop任务队列等候处理。详细的例子,可以参考MDN文档对ajax的描述:同步和异步

误解:事件循环类似栈或队列

这里顺带提一下:事件循环虽然涉及到类似队列的结构,并不是采用栈的方式处理任务。事件循环作为一个进程被划分为多个阶段,每个阶段处理一些特定任务,各阶段轮询调度。这些阶段可以是定时器处理,dom事件处理,ajax异步处理......

结语

JavaScript引擎只有一个线程,强制异步事件排队等待执行,Javascript语言的事件循环,是浏览器的处理和行为。另外,本文是我个人的学习笔记,通篇结合个人的理解,在某些地方表述不严谨,如有错误,希望指出。

时间: 2024-10-10 14:55:25

从Javascript单线程谈Event Loop的相关文章

深入理解Javascript单线程谈Event Loop

假如面试回答js的运行机制时,你可能说出这么一段话:"Javascript的事件分同步任务和异步任务,遇到同步任务就放在执行栈中执行,而碰到异步任务就放到任务队列之中,等到执行栈执行完毕之后再去执行任务队列之中的事件."但你能说出背后的原因吗? 1.线程与进程 进程:是系统资源分配和调度的单元.一个运行着的程序就对应了一个进程.一个进程包括了运行中的程序和程序所使用到的内存和系统资源. 线程:线程是进程下的执行者,一个进程至少会开启一个线程(主线程),也可以开启多个线程. 2.同步和异

【朴灵评注】JavaScript 运行机制详解:再谈Event Loop

PS: 我先旁观下大师们的讨论,得多看书了~ 别人说的:“看了一下不觉得评注对到哪里去,只有吹毛求疵之感. 比如同步异步介绍,本来就无大错:比如node图里面的OS operation,推敲一下就可以猜到那是指同步操作(自然不走event loop了):至于watcher啥的,显然只是实现上的特色,即使用同一个queue实现也未尝不可” [原帖: http://www.ruanyifeng.com/blog/2014/10/event-loop.html 作者:阮一峰] 一年前,我写了一篇<什么

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

一年前,我写了一篇<什么是 Event Loop?>,谈了我对Event Loop的理解. 上个月,我偶然看到了Philip Roberts的演讲<Help, I'm stuck in an event-loop>.这才尴尬地发现,自己的理解是错的.我决定重写这个题目,详细.完整.正确地描述JavaScript引擎的内部运行机制.下面就是我的重写. 进入正文之前,插播一条消息.我的新书<ECMAScript 6入门>出版了(版权页,内页1,内页2),铜版纸全彩印刷,非常

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

原文地址:http://www.ruanyifeng.com/blog/2014/10/event-loop.html 一年前,我写了一篇<什么是 Event Loop?>,谈了我对Event Loop的理解. 上个月,我偶然看到了Philip Roberts的演讲<Help, I'm stuck in an event-loop>.这才尴尬地发现,自己的理解是错的.我决定重写这个题目,详细.完整.正确地描述JavaScript引擎的内部运行机制.下面就是我的重写. 进入正文之前,

(转载)JavaScript 运行机制详解:再谈Event Loop

source: http://www.ruanyifeng.com/blog/2014/10/event-loop.html  一年前,我写了一篇<什么是 Event Loop?>,谈了我对Event Loop的理解. 上个月,我偶然看到了Philip Roberts的演讲<Help, I'm stuck in an event-loop>.这才尴尬地发现,自己的理解是错的.我决定重写这个题目,详细.完整.正确地描述JavaScript引擎的内部运行机制.下面就是我的重写. 进入正

我看朴灵评注阮一峰的《JavaScript 运行机制详解:再谈Event Loop》

阮一峰和朴灵对我来说都是大牛,他们俩的书我都买过,阮老师的译作<软件随想录>和朴灵的<深入浅出node.js>.这个事情已经过了4个月了,所以我拿来讲应该也没啥问题. 这件事情是这样的,阮一峰在自己的博客写了篇文章<JavaScript 运行机制详解:再谈Event Loop>,然后朴灵看见了,发现了很多问题,然后在印象笔记又写了篇文章<[朴灵评注]JavaScript 运行机制详解:再谈Event Loop>,由于印象笔记现在已经不能访问了(尼玛也太烂了)

浅谈 Event Loop

之前看了一篇关于 Event Loop的文章 做了下面的笔记 Event Loop 是解决JavaScript 单线程问题的一种运行机制 当程序接受到了请求之后. 就会把请求交给Event Loop 然后继续往下执行 这样就不用等待请求返回,继续执行下面的代码, 当Event Loop完成后会把结果返回给程序 程序再调用已经设定好的回调函数,完成任务 但是就会出现了如下的问题 程序继续往下跑是一个进程 那Event Loop跑的难道不是另一个线程吗? 如果Event Loop 接受到很多个请求,

JavaScript执行顺序Event Loop

javascript是一门单线程语言,为了实现主线程的不阻塞,但可以用Event Loop模拟多线程操作 Event Loop中同步异步任务执行顺序: 所有异步任务都是在Event Table中注册函数,当指定的时间完成时,Event Table会将函数放入Event Queue,主线程的同步任务执行完会去Event Queue读取对应函数,进入主线程执行. js引擎monitoring process进程,当发现主进程执行栈为空,会去执行Event Queue中的函数 let data = [

[Javascript] Task queue &amp; Event loop.

Javascript with Chorme v8 engine works like this : For Chorme engine, v8, it has call stack. And all the async opreations functions are stay in webapis. So everytime  you call 'setTimeout()' or http call, it will always call webapis. So, in the pictu