单线程JavaScript
最近在阅读《你不知道的JavaScript中卷》,当我看到第二部分介绍异步和回调函数的一些知识时,由于该书在第二部分1、2章对线程、事件循环的概念介绍的并非详细,因此引发了我的一系列思考。于是写下这篇小文章,记录自己对该知识点的学习和思考。
什么是线程
由于JavaScript是单线程语言,因此,在一个进程上,只能运行一个线程,而不能多个线程同时运行。也就是说JavaScript不允许多个线程共享内存空间。因此,如果有多个线程想同时运行,则需采取排队的方式,即只有当前一个线程执行完毕,后一个线程才开始执行。JavaScript中的线程包括函数调用、I/O设备(如向服务器发送请求获取响应等)、定时器、用户操作的事件(click、keyup、scroll等)。
由于每个线程需要排队执行,因此涉及堆(Heap)、栈(Stack)、队列(Queue)的概念。
Heap、Stack、Queue
在MDN上的一篇文章《并发模型与Event Loop》,介绍了关于这三个概念
堆(Heap):对象被分配在一个堆中,一个用以表示一个内存中未被组织的区域。我们知道,函数是第一等对象,同时函数是“可调用的对象”。因此,当函数在被调用之前,JavaScript引擎会对函数进行编译(词法分析、语法分析、代码生成)的工作。当完成编译时会将函数(这里不限于函数,JavaScript所有皆为对象,除了undefined、null)放入堆中,分配内存空间,等待执行或调用。
栈(Stack):当函数调用时,会形成一个“执行栈”。我们看一个简单的例子。
function bar(b) {
return b * 2;
}
function foo(a) {
return bar(a * 3);
}
console.log(foo(1)); //6
当JavaScript引擎在编译阶段,会将foo、bar置于堆中,分配内存空间。当调用foo()时,引擎创建了一个执行栈,包含了foo函数的参数和局部变量。当在foo的词法作用域中调用bar时,会将bar函数推入执行栈,并置于foo函数之上,同时包含bar函数的参数和局部变量。当bar返回时(此例中bar函数调用并返回结果是瞬间完成的),bar函数出栈。当foo函数返回结果时,整个执行栈就空了。此时,如果任务队列中存在异步任务,则主线程会读取任务队列中的任务。待会介绍任务队列。
如果对词法作用域不熟悉的朋友,可以参考如下:闭包与词法作用域
任务队列
单线程就意味着,所有任务(线程)需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务不得不一直等待。
因此,所有任务可以分为两种,一种是同步任务,一种是异步任务。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,后一个任务才会执行;异步任务指的是不进入主线程、而进入任务队列的任务,只有当主线程上的所有同步任务执行完毕之后,主线程才会读取任务队列,开始执行异步任务。
任务队列是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
“任务队列”中的事件,除了IO设备(ajax获取服务器数据)的事件以外,还包括一些用户产生的事件(mousehover、click、scroll、keyup等)和定时器等。只要在事件中指定了回调函数,这些事件发生时就会进入“任务队列”,等待主线程读取。而主线程读取任务队列中的异步任务,主要就是读取回调函数。
当主线程的所有同步任务执行(排队执行)完毕之后,就会读取任务队列中的异步任务,将异步任务推入执行栈中执行。任务队列是一个先进先出的数据结构,即排在前面的事件,优先被主线程读取。如果存在定时器,时间越短的越先进入执行栈。
因此,可以做一个简单的总结:
- JS将任务分为两种,同步任务和异步任务。
- 当主线程开始执行同步任务时,会创建一个“执行栈”,每一个同步任务排队执行,只有前一个任务执行完毕,才会执行下一个任务。同时,执行栈与函数的调用位置相关。
- 当主线程上的所有同步任务执行完毕之后,也就是当“执行栈”为空时,主线程会去读取任务队列上的异步任务(回调函数),并将异步任务推入执行栈中开始执行。
- 主线程不断重复第二、第三个步骤。
setTimeout
明白了主线程执行相关任务的思路后,来看看定时器。上面介绍到,定时器是属于任务队列中的异步任务。因此会等待“执行栈”上的所有同步任务执行完毕之后,主线程计算定时器的执行时间,再将事件推入“执行栈”。看一个简单的例子。
function foo() {
setTimeout(function() {
console.log(1);
}, 0)
console.log(2);
}
function bar() {
setTimeout(function() {
console.log(3);
}, 0);
console.log(4);
}
foo();
bar();
这段函数的输出结果为2, 4, 1, 3。做一个简单的分析。
foo、bar函数的内部有相同的结构,都有一个定时器和console.log()函数。当foo、bar函数调用时,会形成一个“执行栈”,主线程会先执行“执行栈”中的同步任务,即console.log(2), console.log(4),而两个定时器会被推入任务队列中,等待执行。当主线程上的同步任务执行完毕之后,结束定时器的等待,将任务队列中的两个异步任务推入“执行栈”中执行,因此输出的顺序为2, 4, 1, 3。
定时器的第一个参数是一个函数,第二个参数是推迟执行的毫秒数。从函数的定义上看,如果将时间设定为0,此时应该是立即执行定时器才对,为什么输出顺序会不同呢?
需要注意的是,setTimeout()只是将回调函数插入到“任务队列”中,因此必须等到主线程上的同步任务全部执行完毕,主线程才会执行任务队列中的异步任务。setTimeout的第二个参数只能确保任务在指定的时间之后执行,而不能保证一定就在该时间之后立即执行,是否能够立即执行,取决于“执行栈”中的任务数量。
看一段代码。
function foo() {
setTimeout(function() {
console.log(1);
}, 2000)
console.log(2);
}
function bar() {
setTimeout(function() {
console.log(3);
}, 1000);
console.log(4);
}
function baz() {
setTimeout(function() {
console.log(5);
}, 0)
console.log(6);
}
foo();
bar();
baz();
//结果: 2, 4, 6, 5, 3, 1;
主线程上的同步任务按照执行栈排队执行,任务队列上的定时器按照时间长短排队执行。时间越短,越早进入“执行栈”,越早被主线程执行。也就是说,先进入任务队列的任务先执行。
如果换一种函数的调用位置
baz();
foo();
bar();
//此时的结果: 6, 2, 4, 5, 3, 1
从上面的两种运行结果可以看出,
同步任务取决于函数的调用位置,不同的调用位置,进入执行栈的位置就不同,主线程执行的顺序就不同
异步任务的执行与函数的调用位置无关,只取决于执行栈的任务数量,当同步任务执行完毕之后,才会开始执行异步任务,并且遵循先进入任务队列的事件先执行的原则。
单线程JavaScript的更多相关文章
- JavaScript到底是不是单线程
JavaScript到底是不是单线程 JavaScript引擎 在了解计时器内部运作前,我们必须清楚一点,触发和执行并不是同一概念,计时器的回调函数一定会在指定delay的时间后被触发,但并不一定立即 ...
- javascript的单线程
1.什么是javascript的单线程javascript是单线程的语言,所以在一个进程上,只能运行一个县城,不能多个线程同时运行.也就是说javascript不允许多个线程共享内存空间.如果多个线程 ...
- 从JavaScript的单线程执行说起
先看一段代码: 1 2 3 4 5 setTimeout(function(){ alert("a"); }, 0); while(1); alert("b&qu ...
- 浅谈Javascript单线程和事件循环
单线程 Javascript 是单线程的,意味着不会有其他线程来竞争.为什么是单线程呢? 假设 Javascript 是多线程的,有两个线程,分别对同一个元素进行操作: function change ...
- 理解JavaScript中的事件轮询
原文:http://www.ruanyifeng.com/blog/2014/10/event-loop.html 为什么JavaScript是单线程 JavaScript语言的一大特点就是单线程,也 ...
- JavaScript定时器及相关面试题
在单线程JavaScript这篇文章中,在介绍JavaScript单线程的同时,也介绍了setTimeout是如何工作的.但是对于定时器的一些内容,并没有做深入的讨论.这篇文章,会详细说说JS的两种定 ...
- JavaScript异步编程
前言 如果你有志于成为一个优秀的前端工程师,或是想要深入学习JavaScript,异步编程是必不可少的一个知识点,这也是区分初级,中级或高级前端的依据之一.如果你对异步编程没有太清晰的概念,那么我建议 ...
- javascript内存管理(堆和栈)和javascript运行机制
内存基本概念 内存的生命周期: 1.分配所需的内存 2.内存的读与写 3.不需要时将其释放 所有语言的内存生命周期都基本一致,不同的是最后一步在低级语言中很清晰,但是在像JavaScript 等高级语 ...
- javascript的异步编程
同步与异步 介绍异步之前,回顾一下,所谓同步编程,就是计算机一行一行按顺序依次执行代码,当前代码任务耗时执行会阻塞后续代码的执行. 同步编程,即是一种典型的请求-响应模型,当请求调用一个函数或方法后, ...
随机推荐
- IOS开发中数据持久化的几种方法--NSUserDefaults
IOS开发中数据持久化的几种方法--NSUserDefaults IOS 开发中,经常会遇到需要把一些数据保存在本地的情况,那么这个时候我们有以下几种可以选择的方案: 一.使用NSUserDefaul ...
- java数据结构整理(二)
一.List接口,有序的Collection接口,能够精确地控制每个元素插入的位置,允许有相同的元素 1.链表,LinkedList实现了List接口,允许null元素,提供了get().remove ...
- Iphone安装铃声
PP助手 应用列表中打开铃声多多文档. 5点击铃声下载,找到下载的铃声,按下图所示步骤导出至电脑. 6在PP助手界面内,找到"视频音乐"标签,然后进入视频音乐分类下的铃声分类,点击 ...
- 从移动硬盘开机,引导VHD(Win10)
STEP 1 USB隨身碟能順利Boot Win10,點擊[主引導記錄]來設定Windows NT6.x引導程序與啟動記錄. STEP 2 事實上,格式化時MBR的類型預設就是Windows NT 6 ...
- poj 3641 ——2016——3——15
传送门:http://poj.org/problem?id=3461 题目大意:给你两个字符串p和s,求出p在s中出现的次数. 题解:这一眼看过去就知道是KMP,作为模板来写是最好不过了.... 这道 ...
- C#子窗口与父窗口交互(使用委托和事件)
目标:在子窗口Form2上单击按钮时向Form1传递一组自定义参数,并显示在父窗口Form1上. 方法:有很多方法,这里只介绍委托和事件的实现方式. 思路:Form2中定义事件,Form1创建Form ...
- JAVA语言中冒号的用法
近来由于本人要介入android平台的开发,所以就买了本JAVA语言的书学习.学习一段时间来,我的感觉是谭浩强就是厉害,编写的<C编程语言>系列丛书不愧是经典.书中对C语言的介绍既系统又全 ...
- jQuery原型技术分解
jQuery原型技术分解 起源----原型继承 用户过javascript的都会明白,在javascript脚本中到处都是 函数,函数可以归置代码段,把相对独立的功能封闭在一个函数包中.函数也可以实现 ...
- jquery换肤
<script src="script/jquery-2.1.0.js"></script> <link href="style/ ...
- 时钟(AnalogClock和DigitalClock)的功能与用法
时钟UI组件是两个非常简单的组件,DigitalClock本身就继承了TextView——也就是说它本身就是文本框,只是它里面显示的内容总是当前时间.与TextView不同的是为DigitalCloc ...