最近在阅读《你不知道的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. Hooking Android System Calls for Pleasure and Benefit

    The Android kernel is a powerful ally to the reverse engineer. While regular Android apps are hopele ...

  2. android 通过wifi 热点实现手机摄像头数据共享(转)

    原文地址:http://blog.csdn.net/sinat_35845281/article/details/52674946 最近想搞一个新奇的玩意儿~~~ 最近一直在在学习通过两个Androi ...

  3. java爬虫技术

    原博:http://983836259.blog.51cto.com/7311475/1730243 开源爬虫分类: 1.分布式爬虫:Nutch 2.JAVA单机爬虫:Crawler4j.WebMag ...

  4. sql数据库恢复 文件丢失误删除 误格式化置疑报错修复 数据库置疑修复总结/SQL SERVER 2000/2005/2008/2008R2

    数据库置疑的原因会有多种多样,不同的问题采用的步骤也会有所不同,以下的步骤不能适用所有的情况,但包括了一些基本的步骤. 数据库置疑是指数据库内部处于不一致的状态,很有可能会有数据丢失.我们推荐您从做数 ...

  5. bzoj2120: 数颜色 &&bzoj2453: 维护队列

    题目大意: 你小时候玩过弹珠吗? 小朋友A有一些弹珠,A喜欢把它们排成队列,从左到右编号为1到N.为了整个队列鲜艳美观,小朋友想知道某一段连续弹珠中,不同颜色的弹珠有多少.当然,A有时候会依据个人喜好 ...

  6. Bomb(hdu 3555)

    题意:给定一个闭区间,求区间内有多少数中含"49" /* dp[i][j]表示i位数以j为最高位位中的所有不符合数的个数. 然后把数字拆分,乱搞即可. */ #include< ...

  7. 我对CSS中的BFC的理解

       1.什么是BFC 其实在老师让我们写这篇叫BFC的时候,我跟本不知道有什么BFC的东西. 后来,我找了一些资料,知道了,BFC是Block Formatting Context (块级格式化上下 ...

  8. iOS 设置UILable字体,及计算长度和位置

    UILabel *lastLable = [[UILabel alloc] init]; UIFont *font = [UIFont fontWithName:lastLable.font.font ...

  9. jQuery插件placeholder的使用方法

    借助该插件可以轻松实现HTML5中placeholder特效: 实例代码如下: <script type="text/javascript" src="<%= ...

  10. 一个参数引起的mysql从库宕机血案

    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://suifu.blog.51cto.com/9167728/1859252 一个参数 ...