最近碰到奇怪的问题,翻阅 MDN 文档,发现其可真是个宝藏哈。内容都是些基础的东西,包括事件循环、类型数组、内存管理和浏览器进程
事件循环
JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。
运行时概念
- 栈:函数调用形成了一个由若干帧组成的栈
- 堆:对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语
- 队列:一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。
事件循环
- 每一个消息完整地执行后,其它消息才会被执行。这为程序的分析提供了一些优秀的特性,包括:当一个函数执行时,它不会被抢占,只有在它运行完毕之后才会去运行任何其他的代码,才能修改这个函数操作的数据。
- 这个模型的一个缺点在于当一个消息需要太长时间才能处理完毕时,Web 应用程序就无法处理与用户的交互,例如点击或滚动。
- 一个良好的习惯是缩短单个消息处理时间,并在可能的情况下将一个消息裁剪成多个消息。
- 添加消息:每当一个事件发生并且有一个事件监听器绑定在该事件上时,一个消息就会被添加进消息队列。如果没有事件监听器,这个事件将会丢失。
- 多个运行时互相通信:一个 web worker 或者一个跨域的 iframe 都有自己的栈、堆和消息队列。两个不同的运行时只能通过 postMessage 方法进行通信。如果另一个运行时侦听 message 事件,则此方法会向该运行时添加消息。
类型化数组
JavaScript 类型化数组是一种类似数组的对象,并提供了一种用于访问原始二进制数据的机制。
不要把类型化数组与正常数组混淆,因为在类型数组上调用 Array.isArray()
会返回 false
。此外,并不是所有可用于正常数组的方法都能被类型化数组所支持(如 push
和 pop
)。
类型数组架构,为了达到最大的灵活性和效率,JavaScript 类型数组将实现拆分为缓冲和视图两部分。
- 缓冲:一个缓冲(由 ArrayBuffer 对象实现)描述的是一个数据块。缓冲没有格式可言,并且不提供机制访问其内容。
- 视图:为了访问在缓冲对象中包含的内存,你需要使用视图。视图提供了上下文 — 即数据类型、起始偏移量和元素数 — 将数据转换为实际有类型的数组。
ArrayBuffer
- 一种数据类型,用来表示一个通用的、固定长度的二进制数据缓冲区
- 不能直接操纵一个 ArrayBuffer 中的内容;你需要创建一个类型化数组的视图或一个描述缓冲数据格式的 DataView,使用它们来读写缓冲区中的内容
读写缓冲区中内容的两种方式
- 类型数组视图:类型化数组视图具有自描述性的名字和所有常用的数值类型像 Int8,Uint32,Float64 等等。
- 数据视图:DataView 是一种底层接口,它提供有可以操作缓冲区中任意数据的读写接口。这对操作不同类型数据的场景很有帮助
内存管理
垃圾回收的两种算法
引用计数垃圾收集:最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
存在限制:循环引用,看下面的例子
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。
标记-清除算法:这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。
假定设置一个叫做根(root)的对象(在 Javascript 里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
从 2012 年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。
循环引用不再是问题了:在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。
渲染进程
渲染进程包含了哪些线程
- GUI 渲染线程
- 负责页面渲染,布局和绘制
- 页面需要重绘和回流时,该线程就会执行
- 与 JS 引擎线程互斥,防止渲染结果不可预测
- JS 引擎线程
- 负责处理解析和执行 JS 脚本程序
- 只有一个 JS 引擎线程
- 与 GUI 渲染线程互斥,防止渲染结果不可预期
- 事件触发线程
- 用来控制事件循环(鼠标点击、setTimeout、ajax等)
- 当事件满足触发条件时,将事件放入到 JS 引擎所在的执行队列中
- 定时触发器线程
- setTimeout 和 setInterval 所在的线程
- 定时器并不是由 JS 引擎计时的,是由定时触发线程来计时的
- 计时完毕,通知事件触发线程
- 异步 HTTP 请求线程
- 浏览器有一个单独的线程用于处理 ajax 请求
- 当请求完成时,若有回调函数,通知事件触发线程