从计算机知识理解JS event loop机制

238 阅读10分钟

目录

  1. 进程、线程
  2. 为什么JavaScript是单线程?
  3. 执行栈、任务队列
  4. 事件、回调函数
  5. Event Loop
  6. 宏任务、微任务
  7. 定时器 setTimeout()、setInterval()、setImmediate()
  8. Angular 生命周期函数
  9. rxjs 常用高阶函数

  1. 进程process 进程就是操作系统中执行的一个程序,操作系统以进程为单位分配存储空间,每个进程都有自己的地址空间数据栈以及其他用于跟踪进程执行的辅助数据,操作系统管理所有进程的执行,为它们合理的分配资源。

  2. 线程thread 一个进程还可以拥有多个并发的执行线索,简单的说就是拥有多个可以获得CPU调度的执行单元,这就是所谓的线程

由于线程在同一个进程下,它们可以共享相同的上下文,因此相对于进程而言,线程间的信息共享和通信更加容易

  1. 进程process VS 线程thread 当然在单核CPU系统中,真正的并发是不可能的,因为在某个时刻能够获得CPU的只有唯一的一个线程

进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。

每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)。系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源

没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

不仅进程间可以并发执行,线程之间也可以并发执行。但是由于进程的创建、撤消和切换,系统的开销比较大,所以创建的进程数目不能太多,而线程的划分尺度比进程小,所以并发性比进程高,效率和吞吐量都比较高。

  1. 执行上下文(Execution Context) 每次当控制器转到可执行代码的时候,就会进入一个执行上下文。执行上下文可以理解为当前代码的执行环境,它会形成一个作用域

JavaScript中的运行环境大概包括三种情况:

  • 全局环境:JavaScript代码运行起来会首先进入该环境
  • 函数环境:当函数被调用执行时,会进入当前函数中执行代码
  • eval(不建议使用,可忽略)

因此在一个JavaScript程序中,必定会产生多个执行上下文,JavaScript引擎会以的方式来处理它们,这个栈,我们称其为函数调用栈(call stack)栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文。

当代码在执行过程中,遇到以上三种情况,都会生成一个执行上下文,放入栈中,而处于栈顶的上下文执行完毕之后,就会自动出栈。

  • 并发:一个处理器同时处理多个任务。
  • 并行:多个处理器或者是多核的处理器同时处理多个不同的任务.

前者是逻辑上的同时发生(simultaneous),而后者是物理上的同时发生.

来个比喻:

  • 并发:一个人同时吃三个馒头,要一个馒头一个馒头轮流吃
  • 并行:三个人同时吃三个馒头,三个人同时吃,每个人只盯着一个馒头吃

下图反映了一个包含8个操作的任务在一个有两核心的CPU中创建四个线程运行的情况。

假设每个核心有两个线程,那么每个CPU中两个线程会交替并发,两个CPU之间的操作会并行运算。单就一个CPU而言两个线程可以解决线程阻塞造成的不流畅问题,其本身运行效率并没有提高,多CPU的并行运算才真正解决了运行效率问题,这也正是并发和并行的区别。

  • 同步任务(synchronous):在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  • 异步任务(asynchronous):不进入主线程、而进入任务队列(task queue)的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

详细知识看同步/异步,阻塞/非阻塞概念深度解析

JavaScript是一门依赖宿主环境的单线程弱脚本语言,这意味着什么?

JavaScript的运行环境一般由宿主环境runtime(如浏览器、Node等)和执行环境js engine(JavaScript引擎V8、JavaScript Core等) 共同构成

弱类型定义语言:数据类型可以被忽略的语言,例如计算时会在不同类型之间进行隐式转换

假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以是单线程。

JavaScript 是单线程的:一次只能运行一个任务。通常这没什么大不了的,但现在想象一下我们正在运行一个需要30秒的任务。在这个任务中,我们要等待30秒,然后才能执行接下来要做的事情(JavaScript 默认运行在浏览器的主线程上,所以整个UI都卡住了)。

在某一个时刻内只能执行特定的一个任务,并且会阻塞其它任务的执行。

引入异步是,避免某个延时任务(如定时器)阻塞了整个进场,使得加载和渲染无法继续下去,如果没有异步的话,在单线程的js中一旦延时,那么必须要等到这个定时器执行完才可以继续进行渲染或者其他操作,这在客户端基本是不可容忍的。

JavaScript具有自动垃圾回收机制.对它们的模糊认知,导致了很多东西我都理解得并不明白。比如最基本的引用数据类型和引用传递到底是怎么回事儿?比如浅复制与深复制有什么不同?还有闭包,原型等等。 想要对JS的理解更加深刻,就必须对内存空间有一个清晰的认知。

在学习内存空间之前,我们需要对三种数据结构有一个直观的认知。他们分别是堆(heap),栈(stack)与队列(queue)。

bugzhang.com/2019/javasc…

Stack的三种含义

表示函数或子例程像堆积木一样存放,以实现层层调用。执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则

程序运行的时候,总是先完成最上层的调用,然后将它的值返回到下一层调用,直至完成整个调用栈,返回最后的结果。

程序运行的时候,需要内存空间存放数据。一般来说,系统会划分出两种不同的内存空间:一种叫做stack(栈),另一种叫做heap(堆)

它们的主要区别是:stack是有结构的,每个区块按照一定次序存放,可以明确知道每个区块的大小;heap是没有结构的,数据可以任意存放。因此,stack的寻址速度要快于heap

其他的区别还有,一般来说,每个线程分配一个stack,每个进程分配一个heap,也就是说,stack是线程独占的,heap是线程共用的。此外,stack创建的时候,大小是确定的,数据超过这个大小,就发生stack overflow错误,而heap的大小是不确定的,需要的话可以不断增加。

根据上面这些区别,数据存放的规则是:只要是局部的、占用空间确定的数据,一般都存放在stack里面,否则就放在heap里面

当方法执行时,该方法就会建立自己的内存栈,在这个方法内定义的局部变量都会保存在该方法所属的栈内存里,方法执行结束时,该方法从栈中弹出,该方法的栈内存也将自然销毁了。

因为栈只能存放下确定大小的简单数据,所以像变量(其实也就是一个记录了指向复杂数据结构的地址指向,所以变量也是保存在栈中的)和基本数据类型(如Undefined、Null、Boolean、Number和String等)是按值传递的都会保存在栈里,随着方法执行完毕而被销毁。

堆负责存放复杂结构的对象、数组、函数等创建成本较高并且可以重用的数据,即使方法执行完毕也不会销毁,直到系统的垃圾回收机制核实了没有任何引用才会回收。

变量对象与基础数据类型

JavaScript的执行上下文生成之后,会创建一个叫做变量对象的特殊对象,JavaScript的基础数据类型往往都会保存在变量对象中。

严格意义上来说,变量对象也是存放于堆内存中,但是由于变量对象的特殊职能,我们在理解时仍然需要将其于堆内存区分开来。

基础数据类型都是一些简单的数据段,JavaScript中有5中基础数据类型,分别是Undefined、Null、Boolean、Number、String。基础数据类型都是按值访问,因为我们可以直接操作保存在变量中的实际的值。

ES6中新加了一种基础数据类型Symbol,可以先不用考虑他

引用数据类型与堆内存

与其他语言不同,JS的引用数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。因此,引用类型的值都是按引用访问的。这里的引用,我们可以理解为保存在变量对象中的一个地址,该地址与堆内存的实际值相关联。

var a1 = 0;   // 变量对象
var a2 = 'this is string'; // 变量对象
var a3 = null; // 变量对象

var b = { m: 20 }; // 变量b存在于变量对象中,{m: 20} 作为对象存在于堆内存中
var c = [1, 2, 3]; // 变量c存在于变量对象中,[1, 2, 3] 作为对象存在于堆内存中

因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从变量对象中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。

在JavaScript中,理解队列数据结构的目的主要是为了清晰的明白事件循环(Event Loop)的机制到底是怎么回事.