先来思考一个问题,JS 是单线程的还是多线程的?如果是单线程,为什么JavaScript能让AJAX异步发送和回调请求,还有setTimeout也看起来像是多线程的?还有non-blocking IO, event loop等概念。


目录:

  1. 单线程的 JS 引擎
  2. 多线程的浏览器
  3. setTimeout(func, 0) 的应用场景
  4. setTimeout与setInterval
  5. [更新]参考资料
  6. [更新]深入:JS并发模型与 Event Loop

1. 单线程的 JS 引擎

浏览器无论在什么时候都只有一个线程在运行JavaScript程序。单线程,即在某个特定的时刻只有特定代码能执行,并阻塞其他代码。

而浏览器是事件驱动的(event-driven),可以将事件看做浏览器派给JS引擎的任务,

这些任务可能来自JS引擎当前正在执行的代码块,比如说,调用 setTimeout() 添加一个任务;

或者是来自浏览器内核的其他线程,比如说,界面元素鼠标点击事件、定时器时间到达通知、异步请求状态变更通知(a mouse click, a timer firing, or an XMLHttpRequest completing),

从代码角度看,任务实体就是各种回调函数。因为JS引擎是单线程的,这些任务得排队(加入JS引擎的处理队列),等待被JS引擎执行。

  • 好处:

如果多线程,那么删除或者创建dom元素,都需要在线程之间通信。

所以,单线程简单,不需要考虑线程同步;没有线程切换维护开销,省内存。


2. 多线程的浏览器

浏览器内核实现允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步。

浏览器内核可能有如下线程:

常驻线程:

  • 界面渲染线程
  • 事件响应线程

执行完就终止的线程:

  • Http请求线程
  • 定时器线程

一张图说明单线程的JS引擎如何与其他线程通信:

  • JS引擎线程 与 界面渲染线程:互斥

界面渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行.
JavaScript脚本可操纵DOM元素,如果在修改这些元素属性的同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致了。
在JavaScript引擎运行脚本期间,浏览器渲染线程都是处于挂起状态的,也就是说被”冻结”了.
所以,在脚本中对界面进行更新操作,如添加结点、删除结点或改变结点的外观等更新并不会立即体现出来,这些操作将保存在一个队列中,待JavaScript引擎空闲时才有机会渲染出来.

  • 事件触发线程

用户点击鼠标 -》 被浏览器的事件触发线程捕获 -》 形成一个鼠标点击事件添加到 JS引擎的处理队列 末尾

  • 定时触发线程

定时计数器并不是由JS引擎计数的,因为JS引擎单线程,如果处于线程阻塞状态就记不了时,需要依赖外部计数器。

定时器仅仅只是计划代码在未来的某个时间执行,而执行时机是不能保证的。

它的工作方式是:指定一个时间间隔,表示何时将定时器的代码插入到 JS引擎的处理队列,而不是何时执行代码。其等待的时间基于队列里正在等待的消息数量。

  • ajax 异步请求

浏览器新开一个线程请求,当请求状态改变时,若已设置回调(即 onreadystatechange 中设置的回调函数),该异步线程则将状态变更事件放入 JS引擎的处理队列 末尾等待处理。


3. setTimeout(func, 0) 的应用场景

它告诉js引擎,在0ms以后把func放到主事件队列中,等待当前的代码执行完毕再执行,注意:重点是改变了代码流程,把func的执行放到了等待当前的代码执行完毕再执行。

  • 让浏览器渲染当前的变化(很多浏览器UI render和js执行是放在一个线程中,线程阻塞会导致界面无法更新渲染)【待理解】
  • 重新评估”script is running too long”警告【待理解】
  • 改变执行顺序

下例中,点击按钮就会显示"calculating....",如果删除setTimeout就不会。因为reDraw事件被进入事件队列,到长时间操作的最后才能被执行,所以无法刷新。

 <button id='do'> Do long calc!</button>
<div id='status'></div>
<div id='result'></div> $('#do').on('click', function(){ $('#status').text('calculating....'); //此处会触发redraw事件的fired,但会放到队列里执行,直到long()执行完。 // without set timeout, user will never see "calculating...."
//long();//执行长时间任务,阻塞 // with set timeout, works as expected
setTimeout(long,50);//用定时器,大约50ms以后执行长时间任务,放入执行队列,但在redraw之后了,根据先进先出原则 }) function long(){
var result = 0
for (var i = 0; i<1000; i++){
for (var j = 0; j<1000; j++){
for (var k = 0; k<1000; k++){
result = result + i+j+k
}
}
}
$('#status').text('calclation done') // has to be in here for this example. or else it will ALWAYS run instantly. This is the same as passing it a callback
}

4. setTimeout与setInterval

 setTimeout(function() {
/* 代码块... */
setTimeout(arguments.callee, 10);
}, 10); setInterval(function(){
/*代码块... */
}, 10);

这两段代码看一起效果一样,其实非也,第一段中回调函数内的 setTimeout 是 JS引擎 执行后再设置新的setTimeout定时,假定上一个回调处理完到下一个回调开始处理为一个时间间隔,理论上两个setTimeout回调的执行时间间隔>=10ms。第二段自 setInterval 设置定时后,定时触发线程就会源源不断的每隔十秒产生异步定时事件,并放到任务队列尾,理论上两个setInterval回调执行时间间隔<=10。


5. 参考资料

深入理解JavaScript定时机制

Javascript是单线程的深入分析

How JavaScript Timers Work

JavaScript 高级程序设计(第三版) 22.3 高级定时器

[更新]

MDN:并发模型与Event Loop

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

【赞!必看】Philip Roberts的演讲《Help, I'm stuck in an event-loop


6. 深入:JS并发模型与 Event Loop

JS中的并发模型基于"Event Loop"。

先来看一个理论上的模型:(下图来自MDN:并发模型与Event Loop

  • 堆(Heap):内存中一块大的、未被组织的空间,对象被创建在堆中。
  • 栈(Stack):函数调用的空间。栈结构具有的特点:先进后出,从栈顶压入、栈顶弹出,恰好可以对应函数的调用过程。换句话说,函数的调用、返回,即压栈、弹栈的过程。
  • 消息队列(Queue):也称任务队列/ JS引擎的处理队列...(随你咯)。

每个消息都与一个函数关联。

当栈为空时,从队列中取出一个消息进行处理。处理过程包含:调用与消息相关联的函数,创建一个初始栈结构。

事件循环(Event Loop):(下图来自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)

JS引擎运行时需要用到堆和栈。当栈为空时,读取消息队列,若有消息,即调用与消息相关联的函数,并初始化栈结构,执行这个函数过程中涉及到的函数调用都通过压栈弹栈来实现。而栈中代码又可能调用外部API(DOM事件,ajax,定时器),于是消息队列中又添加了新的消息,等待栈空时被JS引擎读取执行。于是构成了 Event Loop.

借助 MDN:并发模型与Event Loop 中对 Event Loop 中的阐述,个人理解是

1 while( (stack is Empty) && (queue.hasMessage) ) {
2 queue.processNextMessage;
3 } 

事件“循环”这个名字很形象~

那么,关于非阻塞I/O(non-blocking I/O),应该很清楚了,以ajax为例,通过外部API建立一个新连接,当相应状态变化时,触发状态变化事件,加入到 JS引擎 的消息队列末尾,等待执行(栈空,且队列前无其他消息等待执行)。在等待连接响应的期间,JS引擎正常、欢快的执行着其他代码呢。

[更新]单线程的JS引擎与 Event Loop的更多相关文章

  1. 为什么JS是单线程?JS中的Event Loop(事件循环)?JS如何实现异步?setimeout?

    https://segmentfault.com/a/1190000012806637 https://www.jianshu.com/p/93d756db8c81 首先,请牢记2点: (1) JS是 ...

  2. js in depth: event loop & micro-task, macro-task & stack, queue, heap & thread, process

    js in depth: event loop & micro-task, macro-task & stack, queue, heap & thread, process ...

  3. node.js中对Event Loop事件循环的理解

    javascript是单线程的,所以任务的执行都需要排队,任务分为两种,一种是同步任务,一种是异步任务. 同步任务是进入主线程上排队执行的任务,上一个任务执行完了,下一个任务才会执行. 异步任务是不进 ...

  4. Js 运行机制 event loop

    Js - 运行机制 (Even Loop) Javascript 的单线程 - 引用思否的说法: JavaScript的一个语言特性(也是这门语言的核心)就是单线程.什么是单线程呢?简单地说就是同一时 ...

  5. JS事件循环(Event Loop)机制

    前言 众所周知,为了与浏览器进行交互,Javascript是一门非阻塞单线程脚本语言. 为何单线程? 因为如果在DOM操作中,有两个线程一个添加节点,一个删除节点,浏览器并不知道以哪个为准,所以只能选 ...

  6. [Node.js] Use nodejs-dashboard event loop delay with hrtime()

    In this lesson, you will learn how to use the Formidable nodejs-dashboard event loop delay to identi ...

  7. 再次聊一聊promise settimeout asycn awiat执行顺序---js执行机制 EVENT LOOP

    首先js是单线程 分为同步和异步,异步又分为(macrotask 宏任务 和 microtask微任务 ), 这图还是很清晰嘛,再来一张 总结一下,就是遇到同步先执行同步,异步的丢到一边依次排队,先排 ...

  8. 浅析Node.js的Event Loop

    目录 浅析Node.js的Event Loop 引出问题 Node.js的基本架构 Libuv Event Loop Event Loop Phases Overview Poll Phase The ...

  9. js为什么是单线程的?10分钟了解js引擎的执行机制

    深入理解JS引擎的执行机制 1.JS为什么是单线程的? 为什么需要异步? 单线程又是如何实现异步的呢? 2.JS中的event loop(1) 3.JS中的event loop(2) 4.说说setT ...

随机推荐

  1. 03_NoSQL数据库之Redis数据库:list类型

     lists类型及操作 List是一个链表结构,主要功能室push,pop.获取一个范围的所有值等等,操作中key理解为链表的名字.Redis的list类型其实就是一个每个元素都是string类型 ...

  2. JAVA获取计算机的IP、名称、操作系统等信息

    [java] view plaincopyprint? import java.io.BufferedReader; import java.io.InputStream; import java.i ...

  3. Web Service进阶(五)SOAPBinding方式讲解

    Web Service进阶(五)SOAPBinding方式讲解 Java API for XML Web Services (JAX-WS) 2.0 (JSR 224) Standard Implem ...

  4. 利用gradle添加构建版本号

    在java的程序中,貌似都没有这个构建版本号的概念,用的诸如eclipse, idea和android studio的IDE也没有直接提供构建版本号的选项.不过我却想在android程序的版本号当中添 ...

  5. Android学习笔记之AppWidget

    什么是AppWidget?AppWidget就是我们平常在桌面上见到的那种一个个的小窗口,利用这个小窗口可以给用户提供一些方便快捷的操作. 今天的目标就是怎么创建一个简单的AppWidget. 首先我 ...

  6. UNIX环境高级编程——线程同步之互斥量

    互斥量(也称为互斥锁)出自POSIX线程标准,可以用来同步同一进程中的各个线程.当然如果一个互斥量存放在多个进程共享的某个内存区中,那么还可以通过互斥量来进行进程间的同步. 互斥量,从字面上就可以知道 ...

  7. Android项目-高考作文项目架构(二)

    1, 普通的http json请求 请看下面架构草图: 这样就抽象出了其他Activity可能需要的Http Json请求的功能. 只要其他Activity有Http Json请求的需求都可以继承Ba ...

  8. linux下重启weblogic(关闭和启动)

     ssh远程连接Linux服务器! 开启weblogic: 1.找到/Oracle/Middleware/user_projects/domains/用户_domain目录, 2.执行nohup ...

  9. PO标准form的一点疑问

    最近在修改采购订单form的时候,发现采购订单form往数据库中插数据的地方找不到,程序太多.我们又需要根据界面上的item的值在订单界面数据生成数据库数据时插值时,只能是想其他办法,一种是在on-i ...

  10. C++编译器何时为用户提供默认构造函数

    第一种是类成员中有成员是类对象,并且该成员的类含有默认构造函数,那么C++编译器会帮你给这个类也生成一个默认构造函数,用来调用其成员对象的构造函数,完成该成员的初始化构造.需要强调的是,如果这个成员的 ...