我们知道浏览器中javascript程序的执行是基于变量与函数的。那么浏览器是如何保存数据,又是如何执行的呢?今天我们一起来探究一下!
0.写在前
最新的 ECMAScript 标准定义了 8 种数据类型:7种原始类型:Undefined、Null、Boolean、Number、BigInt、String、Symbol(新增)和一种复杂类型对象Object(也叫引用类型);
浏览器内存空间分为栈(stack)、堆(heap)、池(一般也会归类为栈中)。 其中栈存放变量,堆存放复杂对象,池存放常量。
1. javascript执行
1.1 在程序解析的最初阶段(预解析)
通过function定义的函数被解析器声明且定义,作为一种特殊的对象,函数的函数体会被保存在堆内存中,函数名会保存在栈内存中,其值为函数体的堆内存地址
使用var声明的变量进行变量提升,浏览器会将这些变量保存到栈中,变量默认为undefined。在这个阶段函数声明比变量声明优先。
1.2 程序执行阶段(逐行执行)
变量声明完成之后,代码开始执行。在此阶段变量被赋值,原始类型值被直接保存到栈内存中。引用类型值保存在堆内存中,并将堆内存中的引用地址赋值给栈内的变量。
随后一些函数开始被执行。首先浏览器通过函数名从堆内存中把函数体捞起来,然后开始解析、运行。我们通常使用执行上下文这么一个抽象的概念来表示JavaScript被解析和运行时的环境,JavaScript 运行任何代码都是在执行上下文环境中运行的。
函数被调用,解析但是还没有开始执行时的状态称为预解析。首先函数的执行上下文初始化arguments对象,提升函数声明和变量声明,然后创建作用域链,当前作用域没有定义该变量时,便会向上查找,取值。如果最终没有就为undefined。这种层层之间就构成了作用域链。最后会确定this指向。
当函数执行时会在内存形成了一个调用帧,随着函数的运行函数内部还调用了其他函数又会形成一个调用帧,所有的调用帧就形成一个调用栈又称为执行栈。可以把执行栈认为成一个储存函数调用的栈结构,遵循先进后出的原则。函数执行完成后,执行上下文从底部退出,等待垃圾回收。
1.3垃圾回收阶段
当函数体被执行后,JavaScript 中无用的变量会被回收,这个过程是由javascript的内存管理是自动执行的.
JavaScript 中内存管理的主要概念是可达性。简单地说,“可达性” 值就是那些以某种方式可访问或可用的值,它们被保证存储在内存中。
有一组基本的固有可达值,由于显而易见的原因无法删除。例如本地函数的局部变量和参数,当前嵌套调用链上的其他函数的变量和参数,全局变量,还有一些其他的,内部的函数及变量。这些值称为根。
如果引用或引用链可以从根访问任何其他值,则认为该值是可访问的。
JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问的对象。
垃圾回收内部算法
基本的垃圾回收算法称为“标记-清除”,定期执行以下“垃圾回收”步骤:
垃圾回收器获取根并“标记”(记住)它们。
然后它访问并“标记”所有来自它们的引用。
然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。
以此类推,直到有未访问的引用(可以从根访问)为止。
除标记的对象外,所有对象都被删除。
闭包
函数执行时产生的新的变量与函数会作为局部变量同样被保存在了内存中,若当一函数的返回值引用了局部变量,同时这个返回结果一直被其他变量引用,那么会导致函数内部的变量不会被正常的回收,形成闭包。
2. 消息队列与Event Loop
在程序执行时,我们经常讨论的一个问题就是消息队列与Event Loop(事件循环)。
一个 JavaScript 运行时包含了一个待处理的消息队列,它由处理异步任务的结果的回调函数组成。异步任务回调不进入主线程,当异步任务产生结果时其回调函数会被塞进”任务队列”(task queue)。异步任务任务包括UI事件、ajax网络请求、定时器setTimeout和setInterval等。
所以消息队列中的每一个消息都与一个函数(回调函数callback)相关联。当调用栈为空时,从队列中取出一个消息进行处理。这个处理过程包含了结果与回调函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着消息处理结束。这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
更简单的说:只要主线程空了(同步),就会去读取”任务队列”(异步),这就是JavaScript的运行机制。
2.1宏队列和微队列
- 宏队列,macrotask(tasks)。 当异步结果返回时异步任务的回调会依次进入macro task queue,等待后续被调用,这些异步任务包括:
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
- 微队列,microtask(jobs)。 当异步结果返回时异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:
- process.nextTick (Node独有)
- Promise
- Object.observe
- MutationObserver
(注:这里只针对浏览器和NodeJS)
2.2 Event Loop的执行
执行一个JavaScript代码的具体流程:
- 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
- 全局Script代码执行完毕后,调用栈Stack会清空;
- 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
- 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
- microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
- 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
- 执行完毕后,调用栈Stack为空;
- 重复第3-7个步骤;
重点:
- 宏队列macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
- 微任务队列中所有的任务都会被依次取出来执行,直到microtask queue为空;
- UI rendering的节点是由浏览器自行判断决定的。执行UI rendering的节点是在执行完所有的microtask之后,下一个macrotask之前,紧跟着执行UI render。