vue中引用js_从JS中的内存管理说起 —— JS中的弱引用
點擊上方“藍字”關(guān)注本公眾號
寫在前面
在所有的編程語言中,我們聲明一個變量時,需要系統(tǒng)為我們分配一塊內(nèi)存。當我們不再需要這個變量時,需要將內(nèi)存進行回收(這個過程稱之為垃圾回收)。在C語言中,有malloc和free來協(xié)助我們進行內(nèi)存管理。在JS中,開發(fā)者不需要手動進行內(nèi)存管理,JS引擎會為我們自動做這些事情。但是,這并不意味著我們在使用JS進行編碼時,不需要關(guān)心內(nèi)存問題。
JS中的內(nèi)存分配與變量
內(nèi)存聲明周期如下:
分配你所需要的內(nèi)存
使用分配到的內(nèi)存(讀、寫)
不需要時將其釋放
在JS中,這三步都是對開發(fā)者無感的,不需要我們過多的關(guān)心。
我們需要注意的是,當我們聲明一個變量、得到一塊內(nèi)存時,需要正確區(qū)分一個變量到底是一個基本類型還是引用類型。
基本類型:String,Number,Boolean,Null,Undefined,Symbol
引用類型:Object,Array,Function
對于基本類型變量來說,系統(tǒng)會為其分配一塊內(nèi)存,這塊內(nèi)存中保存的,就是變量的內(nèi)容。
對于引用類型變量來說,其存儲的只是一個地址而已,這個地址指向的內(nèi)存塊才是是變量的真正內(nèi)容。引用變量的賦值,也只是把地址進行傳遞(復制)。舉個例子:
// a 和 b 指向同一塊內(nèi)存var a = [1,2,3];var b = a;a.push(4);console.log(b); // [1,2,3,4]還有一點需要注意,JS中的函數(shù)傳參,其實是按值傳遞(按引用傳遞)。舉個例子:
// 函數(shù)f的入?yún)?#xff0c;其實是把 a 的值復制了一份。注意 a 是一個引用類型變量,其保存的是一個指向內(nèi)存塊的一個地址。function f(obj) { obj.b = 1;}var a = { a : 1};f(a);console.log(a); // { a: 1, b: 1}在平時的開發(fā)中,完全理解JS中變量的存儲方式是十分重要的。對于我自己來說,盡量避免把引用類型變量到處傳遞,可能一不小心在某個地方修改了變量,另一個地方邏輯沒有判斷好,很容易出Bug,特別是在項目復雜度較高,且多人開發(fā)時。這也是我比較喜歡使用純函數(shù)的原因。
另外,根據(jù)我之前的面試經(jīng)驗,有不少的小伙伴認為下面的代碼會報錯,這也是對JS中變量存儲方式掌握不熟導致的。
// const 聲明一個不可改變的變量。// a 存儲的只是數(shù)組的內(nèi)存地址而已,a.push 并不會改變 a 的值。const a = [];a.push('1'); console.log(a); // ['1']JS中的垃圾回收
垃圾回收算法主要依賴于引用的概念。在內(nèi)存管理的環(huán)境中,一個對象如果有訪問另一個對象的權(quán)限(隱式或者顯式),叫做一個對象引用另一個對象。例如,一個Javascript對象具有對它原型的引用(隱式引用)和對它屬性的引用(顯式引用)。
在這里,“對象”的概念不僅特指 JavaScript 對象,還包括函數(shù)作用域(或者全局詞法作用域)。當變量不再需要時,JS引擎會把變量占用的內(nèi)存進行回收。但是怎么界定【變量不再需要】呢?主要有兩種方法。
引用計數(shù)法
把“對象是否不再需要”簡化定義為“對象有沒有其他對象引用到它”。如果沒有引用指向該對象(零引用),對象將被垃圾回收機制回收。MDN上的例子:
var o = { a: { b:2 }}; // 兩個對象被創(chuàng)建,一個作為另一個的屬性被引用,另一個被分配給變量o// 很顯然,沒有一個可以被垃圾收集var o2 = o; // o2變量是第二個對“這個對象”的引用o = 1; // 現(xiàn)在,“這個對象”只有一個o2變量的引用了,“這個對象”的原始引用o已經(jīng)沒有var oa = o2.a; // 引用“這個對象”的a屬性 // 現(xiàn)在,“這個對象”有兩個引用了,一個是o2,一個是oao2 = "yo"; // 雖然最初的對象現(xiàn)在已經(jīng)是零引用了,可以被垃圾回收了 // 但是它的屬性a的對象還在被oa引用,所以還不能回收oa = null; // a屬性的那個對象現(xiàn)在也是零引用了 // 它可以被垃圾回收了這種方法有一個局限性,那就是無法處理循環(huán)引用。在下面的例子中,兩個對象被創(chuàng)建,并互相引用,形成了一個循環(huán)。它們被調(diào)用之后會離開函數(shù)作用域,所以它們已經(jīng)沒有用了,可以被回收了。然而,引用計數(shù)算法考慮到它們互相都有至少一次引用,所以它們不會被回收。
//?這種情況下,o和o2都無法被回收function f(){ var o = {}; var o2 = {}; o.a = o2; // o 引用 o2 o2.a = o; // o2 引用 o return "azerty";}f();標記-清除 算法
這個算法假定設(shè)置一個叫做根(root)的對象(在Javascript里,根是全局對象)。垃圾回收器將定期從根開始,找所有從根開始引用的對象,然后找這些對象引用的對象……從根開始,垃圾回收器將找到所有可以獲得的對象和收集所有不能獲得的對象。
關(guān)于JS中的垃圾回收算法,網(wǎng)上已經(jīng)有很多的文章講解,這里不再進行贅述。
JS中的內(nèi)存泄露
盡管JS為我們自動處理內(nèi)存的分配、回收問題,但是在某些特定的場景下,JS的垃圾回收算法并不能幫我們?nèi)コ呀?jīng)不再使用的內(nèi)存。這種【由于疏忽或錯誤造成程序未能釋放已經(jīng)不再使用的內(nèi)存】的現(xiàn)象,被稱作內(nèi)存泄露。
內(nèi)存占用越來越高,輕則影響系統(tǒng)性能,重則導致進程崩潰。
可能產(chǎn)生內(nèi)存泄露的場景有不少,包括全局變量,DOM事件,定時器等等。
下面是一段存在內(nèi)存泄露的示例代碼:
class Page1 extends React.Component { events= [] componentDidMount() { window.addEventListener('scroll', this.handleScroll.bind(this)); } render() { return <div> <div><Link to={'/page2'}>前往Page2Link>div> <p>page1p> .... div> } handleScroll(e) { this.events.push(e); }}當我們點擊按鈕跳轉(zhuǎn)到Page2后,在page2不停進行滾動操作,我們會發(fā)現(xiàn)內(nèi)存占用不斷的上漲:
產(chǎn)生這個內(nèi)存泄露的原因是:我們在Page1被unmount的時候,盡管Page1被銷毀了,但是Page1的滾動回調(diào)函數(shù)通過eventListener依然可“觸達”,所以不會被垃圾回收。進入Page2后,滾動事件的邏輯依然生效,內(nèi)部的變量無法被GC。如果用戶在Page2進行長時間滑動等操作,頁面會逐漸變得卡頓。
上述的例子,在我們開發(fā)的過程中,并不少見。不僅僅是事件綁定,也有可能是定時上報邏輯等等。如何解決呢?記得在unmount的時候,進行相應(yīng)的取消操作即可。
在平時的項目開發(fā)中,內(nèi)存泄露還有很多其他的場景。瀏覽器頁面還好,畢竟一直開著某個頁面的用戶不算太多,而且刷新就好。而Node.js發(fā)生內(nèi)存泄露的后果就比較嚴重了,可能服務(wù)就直接崩潰了。掌握JS的變量存儲方式、內(nèi)存管理機制,養(yǎng)成良好的編碼習慣,可以幫助我們減少內(nèi)存泄露的發(fā)生。
JS中的弱引用
前面我們講到了JS的垃圾回收機制,如果我們持有對一個對象的引用,那么這個對象就不會被垃圾回收。這里的引用,指的是強引用。
在計算機程序設(shè)計中,還有一個弱引用的概念:一個對象若只被弱引用所引用,則被認為是不可訪問(或弱可訪問)的,并因此可能在任何時刻被回收。
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 是可枚舉的話,其列表將會受垃圾回收機制的影響,從而得到不確定的結(jié)果。
WeakSet可以視為 WeakMap 中所有值都是布爾值的一個特例,這里就不再贅述了。
JavaScript 的 WeakMap 并不是真正意義上的弱引用:實際上,只要鍵仍然存活,它就強引用其內(nèi)容。WeakMap 僅在鍵被垃圾回收之后,才弱引用它的內(nèi)容。這種關(guān)系更準確地稱為 ephemeron 。
WeakRef
WeakRef是一個更高級的API,它提供了真正的弱引用。我們直接借助上文的內(nèi)存泄露的例子來看一看WeakRef的效果:
import React from 'react';import { Link } from 'react-router-dom';// 使用WeakRef將回調(diào)函數(shù)“包裹”起來,形成對回調(diào)函數(shù)的弱引用。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'}>前往Page2Link>div> <p>page1p> .... div> } handleScroll(e) { this.events.push(e); }}export default Page1;我們再來看看點擊按鈕跳轉(zhuǎn)到page2后的內(nèi)存表現(xiàn):
可以很直觀的看到,在跳轉(zhuǎn)到page2后,持續(xù)滾動一段時間后,內(nèi)存平穩(wěn)。這是因為隨著page1被unmount,真正的滾動回調(diào)函數(shù)( Page1的 handleScroll 函數(shù))被GC掉了。其內(nèi)部的變量也最終被GC。
但其實,這里還有一個問題,雖然我們通過weakRef.deref() 拿不到 handleScroll 滾動回調(diào)函數(shù)了(已被GC),但是我們的包裹函數(shù) wrapper 依然會執(zhí)行。因為我們沒有執(zhí)行removeEventListener。理想情況是:我們希望滾動監(jiān)聽函數(shù)也被取消掉。
可以借助FinalizationRegistry來實現(xiàn)這個功能。看下面的示例代碼:
// FinalizationRegistry構(gòu)造函數(shù)接受一個回調(diào)函數(shù)作為參數(shù),返回一個示例。我們把實例注冊到某個對象上,當該對象被GC時,回調(diào)函數(shù)會觸發(fā)。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時,會觸發(fā)回調(diào)函數(shù)。回調(diào)函數(shù)傳參由我們自己控制。 gListenersRegistry.register(listener, { window, wrapper }); window.addEventListener('scroll', wrapper);}WeakRef 和 FinalizationRegistry 屬于高級Api,在Chrome v84 和 Node.js 13.0.0 后開始支持。一般情況下不建議使用。因為容易用錯,導致更多的問題。
寫在后面
本文從JS中的內(nèi)存管理講起,說到了JS中的弱引用。雖然JS引擎幫我們處理了內(nèi)存管理問題,但是我們在業(yè)務(wù)開發(fā)中并不能完全忽視內(nèi)存問題,特別是在Node.js的開發(fā)中。
關(guān)于JS內(nèi)存管理的更多細節(jié),可以移步我之前翻譯的一篇文章:
V8引擎的內(nèi)存管理
參考資料:
1、https://www.youtube.com/watch?v=TPm-UhWkiq8
2、https://www.infoq.cn/article/lKsmb2tlGH1EHG0*bbYg
3、https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management
總結(jié)
以上是生活随笔為你收集整理的vue中引用js_从JS中的内存管理说起 —— JS中的弱引用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python教材目录_Python实用教
- 下一篇: 经济学中的定量分析python_(转)P