单线程JavaScript

来源:互联网 发布:淘宝潮牌旗舰店 编辑:程序博客网 时间:2024/05/16 23:40

以下我在一个博客中的摘抄,他所参考的《你不知道的javacript》也是我正在读的一本书,这篇也算是我久违的的读书笔记吧

什么是线程

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

JavaScript中的线程包括函数调用、I/O设备(如向服务器发送请求获取响应等)、定时器、用户操作的事件(click、keyup、scroll等)。

由于每个线程需要排队执行,因此涉及堆(Heap)、栈(Stack)、队列(Queue)的概念。

Heap、Stack、Queue

堆(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. 主线程不断重复第二、第三个步骤。

Event Loop(事件循环)

主线程中的所有同步任务执行完毕,再读取任务队列中的异步任务,这个过程是循环不断的。所以,整个的这种运行机制称为Event Loop(事件循环)。

在Event Loop运行机制中,存在两种任务,正常任务(task)和微任务(microtask),它们的区别在于,task会下下一轮Event Loop执行(如定时器),而microtask会在本轮Event Loop的所有任务执行完毕后执行(如Promise对象)。

这里写图片描述

上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种webAPIs,它们在任务队列中加入各种事件(click,load,keyup等)。只要栈中的代码执行完毕,主线程就会去读取任务队列,依次执行那些事件所对应的回调函数。

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

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

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

异步任务的执行与函数的调用位置无关,只取决于执行栈的任务数量当同步任务执行完毕之后,才会开始执行异步任务,并且遵循先进入任务队列的事件先执行的原则。

0 0
原创粉丝点击