最近在阅读《你不知道的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等)和定时器等。只要在事件中指定了回调函数,这些事件发生时就会进入“任务队列”,等待主线程读取。而主线程读取任务队列中的异步任务,主要就是读取回调函数。

当主线程的所有同步任务执行(排队执行)完毕之后,就会读取任务队列中的异步任务,将异步任务推入执行栈中执行。任务队列是一个先进先出的数据结构,即排在前面的事件,优先被主线程读取。如果存在定时器,时间越短的越先进入执行栈。

因此,可以做一个简单的总结:

  1. JS将任务分为两种,同步任务和异步任务。
  2. 当主线程开始执行同步任务时,会创建一个“执行栈”,每一个同步任务排队执行,只有前一个任务执行完毕,才会执行下一个任务。同时,执行栈与函数的调用位置相关。
  3. 当主线程上的所有同步任务执行完毕之后,也就是当“执行栈”为空时,主线程会去读取任务队列上的异步任务(回调函数),并将异步任务推入执行栈中开始执行。
  4. 主线程不断重复第二、第三个步骤。

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的更多相关文章

  1. JavaScript到底是不是单线程

    JavaScript到底是不是单线程 JavaScript引擎 在了解计时器内部运作前,我们必须清楚一点,触发和执行并不是同一概念,计时器的回调函数一定会在指定delay的时间后被触发,但并不一定立即 ...

  2. javascript的单线程

    1.什么是javascript的单线程javascript是单线程的语言,所以在一个进程上,只能运行一个县城,不能多个线程同时运行.也就是说javascript不允许多个线程共享内存空间.如果多个线程 ...

  3. 从JavaScript的单线程执行说起

    先看一段代码: 1 2 3 4 5 setTimeout(function(){     alert("a"); }, 0); while(1); alert("b&qu ...

  4. 浅谈Javascript单线程和事件循环

    单线程 Javascript 是单线程的,意味着不会有其他线程来竞争.为什么是单线程呢? 假设 Javascript 是多线程的,有两个线程,分别对同一个元素进行操作: function change ...

  5. 理解JavaScript中的事件轮询

    原文:http://www.ruanyifeng.com/blog/2014/10/event-loop.html 为什么JavaScript是单线程 JavaScript语言的一大特点就是单线程,也 ...

  6. JavaScript定时器及相关面试题

    在单线程JavaScript这篇文章中,在介绍JavaScript单线程的同时,也介绍了setTimeout是如何工作的.但是对于定时器的一些内容,并没有做深入的讨论.这篇文章,会详细说说JS的两种定 ...

  7. JavaScript异步编程

    前言 如果你有志于成为一个优秀的前端工程师,或是想要深入学习JavaScript,异步编程是必不可少的一个知识点,这也是区分初级,中级或高级前端的依据之一.如果你对异步编程没有太清晰的概念,那么我建议 ...

  8. javascript内存管理(堆和栈)和javascript运行机制

    内存基本概念 内存的生命周期: 1.分配所需的内存 2.内存的读与写 3.不需要时将其释放 所有语言的内存生命周期都基本一致,不同的是最后一步在低级语言中很清晰,但是在像JavaScript 等高级语 ...

  9. javascript的异步编程

    同步与异步 介绍异步之前,回顾一下,所谓同步编程,就是计算机一行一行按顺序依次执行代码,当前代码任务耗时执行会阻塞后续代码的执行. 同步编程,即是一种典型的请求-响应模型,当请求调用一个函数或方法后, ...

随机推荐

  1. IOS block 对象强引用和若引用

    1. 在block外面这样:__weak MyController *weakSelf = self 或者 __weak __typeof(self) weakSelf = self;是为了防止强引用 ...

  2. Cookie 简单使用记录浏览记录

    ItemsDAO.java package dao; import java.util.* ; import java.sql.* ; import util.DBHelper; import ent ...

  3. 轻轻的扩展了一下IEnumerable<T>

    今天用EF写东西玩,觉得IEnumerable里面除了where().select(),是不是能添加点其他方法呢. 想做就做,F12到方法定义: public static IEnumerable&l ...

  4. JS 响应式编程

    <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <script ...

  5. 十二生肖&天干地支

    看相:http://baike.baidu.com/view/833122.htm?fr=aladdin 八字:http://baike.baidu.com/view/17127.htm?fr=ala ...

  6. HDU2066:一个人的旅行(Dijkstra)

    Problem Description 虽然草儿是个路痴(就是在杭电待了一年多,居然还会在校园里迷路的人,汗~),但是草儿仍然很喜欢旅行,因为在旅途中 会遇见很多人(白马王子,^0^),很多事,还能丰 ...

  7. 我用Cocos2d-x模拟《Love Live!学院偶像祭》的Live场景(一)

    同样从CSDN搬过来 博客开这么久了,就发过一篇很水的文章,一直想写点正式的东西.这次准备开工一个仿其他游戏的简单小游戏,于是开博客把开发过程记录下来.这一系列文章主要讲,我是如何从零开始使用Coco ...

  8. adb server无法启动方法,结束占用端口的进程

    adb server is out of date.  killing...ADB server didn't ACK* failed to start daemon *error: unknown ...

  9. SpringMvc项目中使用GoogleKaptcha 生成验证码

    前言:google captcha 是google生成验证码的一个工具类,其原理是将随机生成字符串保存到session中,同时以图片的形式返回给页面,之后前台页面提交到后台进行对比. 1.jar包准备 ...

  10. SuperSocket入门(三)-Telnet多服务实例和服务实例交互配置详解

        在SuperSocket入门(二)中我们已经简单了解了通过配置App.config文件使用BootStrap启动SuperSocket服务.我们先来看一下上个案例中的基本配置文件示例: < ...