tomoya06 / web-developer-guidance

Actually it's just a notebook for keeping down some working experience.
4 stars 0 forks source link

JavaScript - 运行时 #11

Open tomoya06 opened 4 years ago

tomoya06 commented 4 years ago

模块化

ES6

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'

common js

node独有

// a.js
module.exports = {
    a: 1
}
// or
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1

module.exports vs exports

参考内部实现:

var module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
    a: 1
}
// 基本实现
var module = {
  exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因:exports是module.exports的引用
var exports = module.exports
var load = function (module) {
    // 导出的东西
    var a = 1
    module.exports = a
    return module.exports
};

AMD

没用过。参考这里

tomoya06 commented 4 years ago

浏览器节点事件机制

事件触发阶段

  1. window 往事件触发处传播,遇到注册的捕获事件会触发
  2. 传播到事件触发处时触发注册的事件
  3. 从事件触发处往 window 传播,遇到注册的冒泡事件会触发

如果同时注册捕获和冒泡,则按注册顺序执行:

// 以下会先打印冒泡然后是捕获
node.addEventListener(
  'click',
  event => {
    console.log('冒泡')
  },
  false
)
node.addEventListener(
  'click',
  event => {
    console.log('捕获 ')
  },
  true
)

注册事件

事件代理

如果一个节点中的子节点是动态生成的、或者子节点是列表,那么子节点需要注册事件的话应该注册在父节点上。事件代理的方式相对于直接给目标注册事件来说,优点有:可以节省内存;子节点变化时也不需要给子节点注销事件。

tomoya06 commented 4 years ago

垃圾回收机制

概述

JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

简单来说,垃圾回收就是找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

回收方法

标记清除

这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。

标记清除的具体步骤如下:参考掘金博客

  1. 垃圾回收器会在内部构建一个根列表,用于从根节点出发去寻找那些可以被访问到的变量。比如在JavaScript中,window全局对象可以看成一个根节点。
  2. 然后,垃圾回收器从所有根节点出发,遍历其可以访问到的子节点,并将其标记为活动的,根节点不能到达的地方即为非活动的,将会被视为垃圾。
  3. 最后,垃圾回收器将会释放所有非活动的内存块,并将其归还给操作系统。

引用计数

所谓"引用计数"是指语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。

有个问题是循环引用,可能互相引用的两个变量已经离开作用域了但引用数都不为0,无法清除。为了避免循环引用导致的内存泄漏问题,截至2012年所有的现代浏览器均放弃了这种算法

V8引擎的垃圾回收机制

V8的垃圾回收策略主要是基于分代式垃圾回收机制,其根据对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同的分代采用不同的垃圾回收算法。

堆结构

image

上图带斜纹的区域表示暂未使用。

新生代算法

新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。

在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。

老生代算法

老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法标记压缩算法

新生代的对象在如下情况时会被移动到老生代 aka. 对象晋升:

标记压缩

标记清除算法与上一节介绍的相同。在标记清除执行之后,内存空间可能会出现不连续的状态,也就是出现内存碎片,因此需要做标记压缩。

标记压缩的过程中,会将活动的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存。

如何避免内存泄漏

  1. 少创建全局变量
  2. 手动清除定时器、清除已经卸载的DOM引用
  3. 减少闭包
tomoya06 commented 4 years ago

并发模型与事件循环

JavaScript有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。

JS运行时描述

参考MDN并发模型与事件循环

image

堆栈

运行过程

image

JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到Task队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行。本质上来说 JS 中的异步还是同步行为。

不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。

宏任务:

微任务:

完整的Event Loop顺序

以下参考来自这里,【2020-10-04】补充了更详细的事件过程,参考思否博客

  1. 执行宏任务队列中最老的一个
  2. 检查微任务队列,执行并清空微任务队列,如果在微任务的执行中又加入了新的微任务,也会在这一步一起执行。
  3. 进入更新渲染阶段,判断是否需要渲染
    • 并非每一轮 event loop 都会对应一次浏览器渲染,要根据屏幕刷新率、页面性能、页面是否在后台运行来共同决定,通常来说这个渲染间隔是固定的。
  4. 如果需要渲染:
    • 如果窗口的大小发生了变化,执行监听的 resize 方法
    • 如果页面发生了滚动,执行 scroll 方法
    • 执行 requestAnimationFrame 的回调
    • 执行 IntersectionObserver 的回调
    • 重新渲染绘制用户界面
  5. 判断 task队列和microTask队列是否都为空,如果是的话,则进行 Idle 空闲周期的算法,判断是否要执行 requestIdleCallback 的回调函数

分析

分析总结来自思否博客

  1. 事件循环不一定每轮都伴随着重渲染,但是如果有微任务,一定会伴随着微任务执行。
  2. 决定浏览器视图是否渲染的因素很多,浏览器是非常聪明的。
  3. requestAnimationFrame在重新渲染屏幕之前执行,非常适合用来做动画。
  4. requestIdleCallback在渲染屏幕之后执行,并且是否有空执行要看浏览器的调度,如果你一定要它在某个时间内执行,请使用 timeout参数。
  5. resize和scroll事件其实自带节流,它只在 Event Loop 的渲染阶段去派发事件到 EventTarget 上。