Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript - 运行时 #11

Open
tomoya06 opened this issue Aug 29, 2020 · 3 comments
Open

JavaScript - 运行时 #11

tomoya06 opened this issue Aug 29, 2020 · 3 comments

Comments

@tomoya06
Copy link
Owner

模块化

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
Copy link
Owner Author

tomoya06 commented Aug 29, 2020

浏览器节点事件机制

事件触发阶段

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

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

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

注册事件

  • addEventListener 详细用法:见MDN文档
  • event.stopImmediatePropagation() 阻止阻止监听同一事件的其他事件监听器被调用。
  • event.stopPropagation() 阻止捕获和冒泡阶段中当前事件的进一步传播。

事件代理

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

@tomoya06
Copy link
Owner Author

tomoya06 commented Aug 29, 2020

垃圾回收机制

概述

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

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

回收方法

标记清除

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

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

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

引用计数

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

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

V8引擎的垃圾回收机制

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

堆结构

  • 新生代(new_space):大多数的对象开始都会被分配在这里,这个区域相对较小但是垃圾回收特别频繁,该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来。
  • 老生代(old_space):新生代中的对象在存活一段时间后就会被转移到老生代内存区,相对于新生代该内存区域的垃圾回收频率较低。
  • 此外还有大对象区、代码区、map区

image

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

新生代算法

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

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

  • 算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。
  • 当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。

老生代算法

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

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

  • 新生代中的对象已经经历过一次 Scavenge 算法
  • 新生代 To 空间的对象占比大小超过 25 %,为了不影响到内存分配,会将对象移到老生代空间中。

标记压缩

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

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

如何避免内存泄漏

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

@tomoya06 tomoya06 changed the title JavaScript - NodeJS JavaScript - 浏览器 & NodeJS Aug 29, 2020
@tomoya06 tomoya06 changed the title JavaScript - 浏览器 & NodeJS JavaScript - 浏览器 Aug 29, 2020
@tomoya06
Copy link
Owner Author

tomoya06 commented Aug 29, 2020

并发模型与事件循环

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

JS运行时描述

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

image

  • 栈:函数调用形成了一个由若干帧组成的栈。先进后出。
  • 堆:对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。
  • 队列:一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。

堆栈

  • 栈内存一般储存基础数据类型,包括 Number / String / Null / Undefined / Boolean / Symbol;按值访问,存储的值大小固定,由系统自动分配内存空间。
  • 堆内存一般储存引用数据类型,如Array / Object / Function主体,并在栈内存中存储引用类型的地址指针;按引用访问,存储的值大小不定,可动态调整。

运行过程

image

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

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

宏任务:

  • setTimeout, setInterval
  • I/O
  • script

微任务:

  • queueMicrotask(f) > MDN DOC
  • promise then/catch handler > 注意new Promise(F)F还是作为script直接执行的
  • MutationObserver > MDN DOC

完整的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 上。

@tomoya06 tomoya06 changed the title JavaScript - 浏览器 JavaScript - 运行时 Oct 4, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant