单线程JavaScript


什么是线程

由于JavaScript是单线程语言,因此,在一个进程上,只能运行一个线程,而不能多个线程同时运行。也就是说JavaScript不允许多个线程共享内存空间。因此,如果有多个线程想同时运行,则需采取排队的方式,即只有当前一个线程执行完毕,后一个线程才开始执行。

Heap 、Stack、 Queue

堆:对象被分配在一个堆中,用以表示一个内存中未被组织的区域。我们知道,在函数被调用之前,JavaScript引擎会对函数进行编译(词法分析、语法分析、代码生成)的工作。当完成编译时会将函数(这里不限于函数,JavaScript所有皆对象,除了undefined,null)放入堆中,分配内存空间,等待执行或者调用。
栈: 当函数调用时,会形成一个"执行栈"。我们看一个简单的例子。

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

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

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

  • 当主线程开始执行同步任务时,会创建一个"执行栈",每一个同步任务排队执行,只有前一个任务执行完毕,才会执行下一个任务。同时,执行栈与汗水的调用位置有关。
  • 当主线程上的所有同步任务执行完毕之后,主线程会读取任务队列上的异步任务,并将异步任务推入执行栈中开始执行。
  • 主线程不断重复以上2个步骤。

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会等到同步任务执行完毕之后,再等到任务队列中的异步任务执行完毕之后才开始执行。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

从上面的两种运行结果可以看出,

同步任务取决于函数的调用位置,不同的调用位置,进入执行栈的位置就不同,主线程执行的顺序就不同

异步任务的执行与函数的调用位置无关,只取决于执行栈的任务数量,当同步任务执行完毕之后,才会开始执行异步任务,并且遵循先进入任务队列的事件先执行的原则。
参考文献:http://www.cnblogs.com/Uncle-Keith/p/6436047.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 从哪说起呢? 单纯讲多线程编程真的不知道从哪下嘴。。 不如我直接引用一个最简单的问题,以这个作为切入点好了 在ma...
    Mr_Baymax阅读 2,831评论 1 17
  • Object C中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?如果想延时执行代码、方法又是什么? 1...
    AlanGe阅读 1,792评论 0 17
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 173,286评论 25 708
  • 蓬莱仙山长岛行, 旖旎飘渺摄人心。最是黄渤交接处,欲化大鵟啸天清。 三十号晚七点在牡丹园上的大巴车,没有避开如浪的...
    安静不失文艺的数据师阅读 821评论 6 6
  • 文/小马乖乖乖 01 和他在一起已经六年了。 我们相识于网络,约见在严肃而又庄重的人民大礼堂前的一颗大树下。 他,...
    小马乖乖乖阅读 9,037评论 6 15