本文首发于公众号:合乎预期的CoyPan

写在后面

在所有的编程语言中,咱们申明一个变量时,须要零碎为咱们调配一块内存。当咱们不再须要这个变量时,须要将内存进行回收(这个过程称之为垃圾回收)。在C语言中,有malloc和free来帮助咱们进行内存治理。在JS中,开发者不须要手动进行内存治理,JS引擎会为咱们主动做这些事件。然而,这并不意味着咱们在应用JS进行编码时,不须要关怀内存问题。

JS中的内存调配与变量

内存申明周期如下:

  1. 调配你所须要的内存
  2. 应用调配到的内存(读、写)
  3. 不须要时将其开释

在JS中,这三步都是对开发者无感的,不须要咱们过多的关怀。

咱们须要留神的是,当咱们申明一个变量、失去一块内存时,须要正确区分一个变量到底是一个根本类型还是援用类型。

根本类型:String,Number,Boolean,Null,Undefined,Symbol

援用类型:Object,Array,Function

对于根本类型变量来说,零碎会为其调配一块内存,这块内存中保留的,就是变量的内容。

对于援用类型变量来说,其存储的只是一个地址而已,这个地址指向的内存块才是是变量的真正内容。援用变量的赋值,也只是把地址进行传递(复制)。举个例子:

// a 和 b 指向同一块内存var a = [1,2,3];var b = a;a.push(4);console.log(b); // [1,2,3,4]

还有一点须要留神,JS中的函数传参,其实是按值传递(按援用传递)。举个例子:

// 函数f的入参,其实是把 a 的值复制了一份。留神 a 是一个援用类型变量,其保留的是一个指向内存块的一个地址。function f(obj) {    obj.b = 1;}var a = { a : 1};f(a);console.log(a); // { a: 1, b: 1}

在平时的开发中,齐全了解JS中变量的存储形式是非常重要的。对于我本人来说,尽量避免把援用类型变量到处传递,可能一不小心在某个中央批改了变量,另一个中央逻辑没有判断好,很容易出Bug,特地是在我的项目复杂度较高,且多人开发时。这也是我比拟喜爱应用纯函数的起因。

另外,依据我之前的面试教训,有不少的小伙伴认为上面的代码会报错,这也是对JS中变量存储形式把握不熟导致的。

// const 申明一个不可扭转的变量。 // a 存储的只是数组的内存地址而已,a.push 并不会扭转 a 的值。const a = [];a.push('1'); console.log(a); // ['1']

JS中的垃圾回收

垃圾回收算法次要依赖于援用的概念。在内存治理的环境中,一个对象如果有拜访另一个对象的权限(隐式或者显式),叫做一个对象援用另一个对象。例如,一个Javascript对象具备对它原型的援用(隐式援用)和对它属性的援用(显式援用)。

在这里,“对象”的概念不仅特指 JavaScript 对象,还包含函数作用域(或者全局词法作用域)。当变量不再须要时,JS引擎会把变量占用的内存进行回收。然而怎么界定【变量不再须要】呢?次要有两种办法。

援用计数算法

把“对象是否不再须要”简化定义为“对象有没有其余对象援用到它”。如果没有援用指向该对象(零援用),对象将被垃圾回收机制回收。MDN上的例子:

var o = {   a: {    b:2  }}; // 两个对象被创立,一个作为另一个的属性被援用,另一个被调配给变量o// 很显然,没有一个能够被垃圾收集var o2 = o; // o2变量是第二个对“这个对象”的援用o = 1;      // 当初,“这个对象”只有一个o2变量的援用了,“这个对象”的原始援用o曾经没有var oa = o2.a; // 援用“这个对象”的a属性               // 当初,“这个对象”有两个援用了,一个是o2,一个是oao2 = "yo"; // 尽管最后的对象当初曾经是零援用了,能够被垃圾回收了           // 然而它的属性a的对象还在被oa援用,所以还不能回收oa = null; // a属性的那个对象当初也是零援用了           // 它能够被垃圾回收了

这种办法有一个局限性,那就是无奈解决循环援用。在上面的例子中,两个对象被创立,并相互援用,造成了一个循环。它们被调用之后会来到函数作用域,所以它们曾经没有用了,能够被回收了。然而,援用计数算法思考到它们相互都有至多一次援用,所以它们不会被回收。

// 这种状况下,o和o2都无奈被回收。function f(){  var o = {};  var o2 = {};  o.a = o2; // o 援用 o2  o2.a = o; // o2 援用 o  return "azerty";}f();

标记-革除算法

这个算法假设设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始援用的对象,而后找这些对象援用的对象……从根开始,垃圾回收器将找到所有能够取得的对象和收集所有不能取得的对象。

对于JS中的垃圾回收算法,网上曾经有很多的文章解说,这里不再进行赘述。

JS中的内存泄露

只管JS为咱们主动解决内存的调配、回收问题,然而在某些特定的场景下,JS的垃圾回收算法并不能帮咱们去除曾经不再应用的内存。这种【因为忽略或谬误造成程序未能开释曾经不再应用的内存】的景象,被称作内存泄露。

内存占用越来越高,轻则影响零碎性能,重则导致过程解体。

可能产生内存泄露的场景有不少,包含全局变量,DOM事件,定时器等等。

上面是一段存在内存泄露的示例代码:

class Page1 extends React.Component {    events= []    componentDidMount() {        window.addEventListener('scroll', this.handleScroll.bind(this));    }    render() {        return <div>            <div><Link to={'/page2'}>返回Page2</Link></div>            <p>page1</p>                ....        </div>    }    handleScroll(e) {        this.events.push(e);    }}

当咱们点击按钮跳转到Page2后,在page2不停进行滚动操作,咱们会发现内存占用一直的上涨:

产生这个内存泄露的起因是:咱们在Page1被unmount的时候,只管Page1被销毁了,然而Page1的滚动回调函数通过eventListener仍然可“触达”,所以不会被垃圾回收。进入Page2后,滚动事件的逻辑仍然失效,外部的变量无奈被GC。如果用户在Page2进行长时间滑动等操作,页面会逐步变得卡顿。

上述的例子,在咱们开发的过程中,并不少见。不仅仅是事件绑定,也有可能是定时上报逻辑等等。如何解决呢?记得在unmount的时候,进行相应的勾销操作即可。

在平时的我的项目开发中,内存泄露还有很多其余的场景。浏览器页面还好,毕竟始终开着某个页面的用户不算太多,刷新就好。而Node.js产生内存泄露的结果就比较严重了,可能服务就间接解体了。把握JS的变量存储形式、内存管理机制,养成良好的编码习惯,能够帮忙咱们缩小内存泄露的产生。

JS中的弱援用

后面咱们讲到了JS的垃圾回收机制,如果咱们持有对一个对象的援用,那么这个对象就不会被垃圾回收。这里的援用,指的是强援用

在计算机程序设计中,还有一个弱援用的概念: 一个对象若只被弱援用所援用,则被认为是不可拜访(或弱可拜访)的,并因而可能在任何时刻被回收。

在JS中,WeakMap 和 WeakSet 给咱们提供了弱援用的能力。

WeakMap 、WeakSet

要说WeakMap,先来说一说Map。Map 对象保留键值对,并且可能记住键的原始插入程序。任何值(对象或者原始值) 都能够作为一个键或一个值。

Map对对象是强援用:

const m = new Map();let obj = { a: 1 };m.set(obj, 'a');obj = null; // 将obj置为null并不会使 { a: 1 } 被垃圾回收,因为还有map援用了 { a: 1 }

WeakMap是一组键/值对的汇合,其中的键是弱援用的。其键必须是对象,而值能够是任意的。WeakMap是对对象的弱援用:

const wm = new WeakMap();let obj = { b: 2 };wm.set(obj, '2');obj = null; // 将obj置为 null 后,只管 wm 仍然援用了{ b: 2 },然而因为是弱援用,{ b: 2 } 会在某一时刻被GC。

正因为这样的弱援用,WeakMap 的 key 是不可枚举的 (没有办法能给出所有的 key)。如果key 是可枚举的话,其列表将会受垃圾回收机制的影响,从而失去不确定的后果。

WeakSet能够视为 WeakMap 中所有值都是布尔值的一个特例,这里就不再赘述了。

JavaScript 的 WeakMap 并不是真正意义上的弱援用:实际上,只有键依然存活,它就强援用其内容。WeakMap 仅在键被垃圾回收之后,才弱援用它的内容。这种关系更精确地称为 ephemeron 。

WeakRef

WeakRef是一个更高级的API,它提供了真正的弱援用。咱们间接借助上文的内存泄露的例子来看一看WeakRef的成果:

import React from 'react';import { Link } from 'react-router-dom';// 应用WeakRef将回调函数“包裹”起来,造成对回调函数的弱援用。function addWeakListener(listener) {    const weakRef = new WeakRef(listener);    const wrapper = e => {        if (weakRef.deref()) {            return weakRef.deref()(e);        }    }    window.addEventListener('scroll', wrapper);}class Page1 extends React.Component {    events= []    componentDidMount() {        addWeakListener(this.handleScroll.bind(this));    }    componentWillUnmount() {        console.log(this.events);    }    render() {        return <div>            <div><Link to={'/page2'}>返回Page2</Link></div>            <p>page1</p>            ....        </div>    }    handleScroll(e) {        this.events.push(e);    }}export default Page1;

咱们再来看看点击按钮跳转到page2后的内存体现:

能够很直观的看到,在跳转到page2后,继续滚动一段时间后,内存安稳。这是因为随着page1被unmount,真正的滚动回调函数( Page1的 handleScroll 函数)被GC掉了。其外部的变量也最终被GC。

但其实,这里还有一个问题,尽管咱们通过weakRef.deref() 拿不到 handleScroll 滚动回调函数了(已被GC),然而咱们的包裹函数 wrapper 仍然会执行。因为咱们没有执行removeEventListener。现实状况是:咱们心愿滚动监听函数也被勾销掉。

能够借助FinalizationRegistry来实现这个性能。看上面的示例代码:

// FinalizationRegistry构造函数承受一个回调函数作为参数,返回一个示例。咱们把实例注册到某个对象上,当该对象被GC时,回调函数会触发。const gListenersRegistry = new FinalizationRegistry(({ window, wrapper }) => {    console.log('GC happen!!');    window.removeEventListener('scroll', wrapper);});function addWeakListener(listener) {    const weakRef = new WeakRef(listener);    const wrapper = e => {        console.log('scroll');        if (weakRef.deref()) {            return weakRef.deref()(e);        }    }    // 新增这行代码,当listener被GC时,会触发回调函数。回调函数传参由咱们本人管制。    gListenersRegistry.register(listener, { window, wrapper });    window.addEventListener('scroll', wrapper);}
WeakRef 和 FinalizationRegistry 属于高级Api,在Chrome v84 和 Node.js 13.0.0 后开始反对。个别状况下不倡议应用。因为容易用错,导致更多的问题。

写在前面

本文从JS中的内存治理讲起,说到了JS中的弱援用。尽管JS引擎帮咱们解决了内存治理问题,然而咱们在业务开发中并不能齐全漠视内存问题,特地是在Node.js的开发中。

对于V8的内存策略的更多细节,能够移步我之前翻译的一篇文章:

V8引擎的内存治理

参考资料:

1、https://www.youtube.com/watch...

2、https://www.infoq.cn/article/...*bbYg

3、https://developer.mozilla.org...