???個(gè)人主頁(yè):前端青山
??系列專欄:JavaScript篇
??人終將被年少不可得之物困其一生
依舊青山,本期給大家?guī)?lái)JavaScript篇專欄內(nèi)容:JavaScript-數(shù)據(jù)緩存與內(nèi)存泄露
目錄
說(shuō)說(shuō)你對(duì)事件循環(huán)的理解
一、是什么
二、宏任務(wù)與微任務(wù)
微任務(wù)
宏任務(wù)
三、async與await
async
await
四、流程分析
說(shuō)說(shuō) JavaScript 中內(nèi)存泄漏的幾種情況?
一、是什么
二、垃圾回收機(jī)制
標(biāo)記清除
引用計(jì)數(shù)
小結(jié)
三、常見內(nèi)存泄露情況
說(shuō)說(shuō)你對(duì)事件循環(huán)的理解
一、是什么
首先,JavaScript
是一門單線程的語(yǔ)言,意味著同一時(shí)間內(nèi)只能做一件事,但是這并不意味著單線程就是阻塞,而實(shí)現(xiàn)單線程非阻塞的方法就是事件循環(huán)
在JavaScript
中,所有的任務(wù)都可以分為
-
同步任務(wù):立即執(zhí)行的任務(wù),同步任務(wù)一般會(huì)直接進(jìn)入到主線程中執(zhí)行
-
異步任務(wù):異步執(zhí)行的任務(wù),比如
ajax
網(wǎng)絡(luò)請(qǐng)求,setTimeout
定時(shí)函數(shù)等
同步任務(wù)與異步任務(wù)的運(yùn)行流程圖如下:
從上面我們可以看到,同步任務(wù)進(jìn)入主線程,即主執(zhí)行棧,異步任務(wù)進(jìn)入任務(wù)隊(duì)列,主線程內(nèi)的任務(wù)執(zhí)行完畢為空,會(huì)去任務(wù)隊(duì)列讀取對(duì)應(yīng)的任務(wù),推入主線程執(zhí)行。上述過程的不斷重復(fù)就事件循環(huán)
二、宏任務(wù)與微任務(wù)
如果將任務(wù)劃分為同步任務(wù)和異步任務(wù)并不是那么的準(zhǔn)確,舉個(gè)例子:
console.log(1)
?
setTimeout(()=>{
? ?console.log(2)
}, 0)
?
new Promise((resolve, reject)=>{
? ?console.log('new Promise')
? ?resolve()
}).then(()=>{
? ?console.log('then')
})
?
console.log(3)
如果按照上面流程圖來(lái)分析代碼,我們會(huì)得到下面的執(zhí)行步驟:
-
console.log(1)
,同步任務(wù),主線程中執(zhí)行 -
setTimeout()
,異步任務(wù),放到Event Table
,0 毫秒后console.log(2)
回調(diào)推入Event Queue
中 -
new Promise
,同步任務(wù),主線程直接執(zhí)行 -
.then
,異步任務(wù),放到Event Table
-
console.log(3)
,同步任務(wù),主線程執(zhí)行
所以按照分析,它的結(jié)果應(yīng)該是 1
=> 'new Promise'
=> 3
=> 2
=> 'then'
但是實(shí)際結(jié)果是:1
=>'new Promise'
=> 3
=> 'then'
=> 2
出現(xiàn)分歧的原因在于異步任務(wù)執(zhí)行順序,事件隊(duì)列其實(shí)是一個(gè)“先進(jìn)先出”的數(shù)據(jù)結(jié)構(gòu),排在前面的事件會(huì)優(yōu)先被主線程讀取
例子中 setTimeout
回調(diào)事件是先進(jìn)入隊(duì)列中的,按理說(shuō)應(yīng)該先于 .then
中的執(zhí)行,但是結(jié)果卻偏偏相反
原因在于異步任務(wù)還可以細(xì)分為微任務(wù)與宏任務(wù)
微任務(wù)
一個(gè)需要異步執(zhí)行的函數(shù),執(zhí)行時(shí)機(jī)是在主函數(shù)執(zhí)行結(jié)束之后、當(dāng)前宏任務(wù)結(jié)束之前
常見的微任務(wù)有:
-
Promise.then
-
MutaionObserver
-
Object.observe(已廢棄;Proxy 對(duì)象替代)
-
process.nextTick(Node.js)
宏任務(wù)
宏任務(wù)的時(shí)間粒度比較大,執(zhí)行的時(shí)間間隔是不能精確控制的,對(duì)一些高實(shí)時(shí)性的需求就不太符合
常見的宏任務(wù)有:
-
script (可以理解為外層同步代碼)
-
setTimeout/setInterval
-
UI rendering/UI事件
-
postMessage、MessageChannel
-
setImmediate、I/O(Node.js)
這時(shí)候,事件循環(huán),宏任務(wù),微任務(wù)的關(guān)系如圖所示
按照這個(gè)流程,它的執(zhí)行機(jī)制是:
-
執(zhí)行一個(gè)宏任務(wù),如果遇到微任務(wù)就將它放到微任務(wù)的事件隊(duì)列中
-
當(dāng)前宏任務(wù)執(zhí)行完成后,會(huì)查看微任務(wù)的事件隊(duì)列,然后將里面的所有微任務(wù)依次執(zhí)行完
回到上面的題目
console.log(1)
setTimeout(()=>{
? ?console.log(2)
}, 0)
new Promise((resolve, reject)=>{
? ?console.log('new Promise')
? ?resolve()
}).then(()=>{
? ?console.log('then')
})
console.log(3)
流程如下
// 遇到 console.log(1) ,直接打印 1
// 遇到定時(shí)器,屬于新的宏任務(wù),留著后面執(zhí)行
// 遇到 new Promise,這個(gè)是直接執(zhí)行的,打印 'new Promise'
// .then 屬于微任務(wù),放入微任務(wù)隊(duì)列,后面再執(zhí)行
// 遇到 console.log(3) 直接打印 3
// 好了本輪宏任務(wù)執(zhí)行完畢,現(xiàn)在去微任務(wù)列表查看是否有微任務(wù),發(fā)現(xiàn) .then 的回調(diào),執(zhí)行它,打印 'then'
// 當(dāng)一次宏任務(wù)執(zhí)行完,再去執(zhí)行新的宏任務(wù),這里就剩一個(gè)定時(shí)器的宏任務(wù)了,執(zhí)行它,打印 2
三、async與await
async
是異步的意思,await
則可以理解為 async wait
。所以可以理解async
就是用來(lái)聲明一個(gè)異步方法,而 await
是用來(lái)等待異步方法執(zhí)行
async
async
函數(shù)返回一個(gè)promise
對(duì)象,下面兩種方法是等效的
function f() {
? ?return Promise.resolve('TEST');
}
?
// asyncF is equivalent to f!
async function asyncF() {
? ?return 'TEST';
}
await
正常情況下,await
命令后面是一個(gè) Promise
對(duì)象,返回該對(duì)象的結(jié)果。如果不是 Promise
對(duì)象,就直接返回對(duì)應(yīng)的值
async function f(){
? ?// 等同于
? ?// return 123
? ?return await 123
}
f().then(v => console.log(v)) // 123
不管await
后面跟著的是什么,await
都會(huì)阻塞后面的代碼
async function fn1 (){
? ?console.log(1)
? ?await fn2()
? ?console.log(2) // 阻塞
}
?
async function fn2 (){
? ?console.log('fn2')
}
?
fn1()
console.log(3)
上面的例子中,await
會(huì)阻塞下面的代碼(即加入微任務(wù)隊(duì)列),先執(zhí)行 async
外面的同步代碼,同步代碼執(zhí)行完,再回到 async
函數(shù)中,再執(zhí)行之前阻塞的代碼
所以上述輸出結(jié)果為:1
,fn2
,3
,2
四、流程分析
通過對(duì)上面的了解,我們對(duì)JavaScript
對(duì)各種場(chǎng)景的執(zhí)行順序有了大致的了解
這里直接上代碼:
async function async1() {
? ?console.log('async1 start')
? ?await async2()
? ?console.log('async1 end')
}
async function async2() {
? ?console.log('async2')
}
console.log('script start')
setTimeout(function () {
? ?console.log('settimeout')
})
async1()
new Promise(function (resolve) {
? ?console.log('promise1')
? ?resolve()
}).then(function () {
? ?console.log('promise2')
})
console.log('script end')
分析過程:
-
執(zhí)行整段代碼,遇到
console.log('script start')
直接打印結(jié)果,輸出script start
-
遇到定時(shí)器了,它是宏任務(wù),先放著不執(zhí)行
-
遇到
async1()
,執(zhí)行async1
函數(shù),先打印async1 start
,下面遇到await
怎么辦?先執(zhí)行async2
,打印async2
,然后阻塞下面代碼(即加入微任務(wù)列表),跳出去執(zhí)行同步代碼 -
跳到
new Promise
這里,直接執(zhí)行,打印promise1
,下面遇到.then()
,它是微任務(wù),放到微任務(wù)列表等待執(zhí)行 -
最后一行直接打印
script end
,現(xiàn)在同步代碼執(zhí)行完了,開始執(zhí)行微任務(wù),即await
下面的代碼,打印async1 end
-
繼續(xù)執(zhí)行下一個(gè)微任務(wù),即執(zhí)行
then
的回調(diào),打印promise2
-
上一個(gè)宏任務(wù)所有事都做完了,開始下一個(gè)宏任務(wù),就是定時(shí)器,打印
settimeout
所以最后的結(jié)果是:script start
、async1 start
、async2
、promise1
、script end
、async1 end
、promise2
、settimeout
說(shuō)說(shuō) JavaScript 中內(nèi)存泄漏的幾種情況?
一、是什么
內(nèi)存泄漏(Memory leak)是在計(jì)算機(jī)科學(xué)中,由于疏忽或錯(cuò)誤造成程序未能釋放已經(jīng)不再使用的內(nèi)存
并非指內(nèi)存在物理上的消失,而是應(yīng)用程序分配某段內(nèi)存后,由于設(shè)計(jì)錯(cuò)誤,導(dǎo)致在釋放該段內(nèi)存之前就失去了對(duì)該段內(nèi)存的控制,從而造成了內(nèi)存的浪費(fèi)
程序的運(yùn)行需要內(nèi)存。只要程序提出要求,操作系統(tǒng)或者運(yùn)行時(shí)就必須供給內(nèi)存
對(duì)于持續(xù)運(yùn)行的服務(wù)進(jìn)程,必須及時(shí)釋放不再用到的內(nèi)存。否則,內(nèi)存占用越來(lái)越高,輕則影響系統(tǒng)性能,重則導(dǎo)致進(jìn)程崩潰
在C
語(yǔ)言中,因?yàn)槭鞘謩?dòng)管理內(nèi)存,內(nèi)存泄露是經(jīng)常出現(xiàn)的事情。
char * buffer;
buffer = (char*) malloc(42);
?
// Do something with buffer
?
free(buffer);
上面是 C 語(yǔ)言代碼,malloc
方法用來(lái)申請(qǐng)內(nèi)存,使用完畢之后,必須自己用free
方法釋放內(nèi)存。
這很麻煩,所以大多數(shù)語(yǔ)言提供自動(dòng)內(nèi)存管理,減輕程序員的負(fù)擔(dān),這被稱為"垃圾回收機(jī)制"
二、垃圾回收機(jī)制
Javascript 具有自動(dòng)垃圾回收機(jī)制(GC:Garbage Collecation),也就是說(shuō),執(zhí)行環(huán)境會(huì)負(fù)責(zé)管理代碼執(zhí)行過程中使用的內(nèi)存
原理:垃圾收集器會(huì)定期(周期性)找出那些不在繼續(xù)使用的變量,然后釋放其內(nèi)存
通常情況下有兩種實(shí)現(xiàn)方式:
-
標(biāo)記清除
-
引用計(jì)數(shù)
標(biāo)記清除
JavaScript
最常用的垃圾收回機(jī)制
當(dāng)變量進(jìn)入執(zhí)行環(huán)境是,就標(biāo)記這個(gè)變量為“進(jìn)入環(huán)境“。進(jìn)入環(huán)境的變量所占用的內(nèi)存就不能釋放,當(dāng)變量離開環(huán)境時(shí),則將其標(biāo)記為“離開環(huán)境“
垃圾回收程序運(yùn)行的時(shí)候,會(huì)標(biāo)記內(nèi)存中存儲(chǔ)的所有變量。然后,它會(huì)將所有在上下文中的變量,以及被在上下文中的變量引用的變量的標(biāo)記去掉
在此之后再被加上標(biāo)記的變量就是待刪除的了,原因是任何在上下文中的變量都訪問不到它們了
隨后垃圾回收程序做一次內(nèi)存清理,銷毀帶標(biāo)記的所有值并收回它們的內(nèi)存
舉個(gè)例子:? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
引用計(jì)數(shù)
語(yǔ)言引擎有一張"引用表",保存了內(nèi)存里面所有的資源(通常是各種值)的引用次數(shù)。如果一個(gè)值的引用次數(shù)是0
,就表示這個(gè)值不再用到了,因此可以將這塊內(nèi)存釋放
如果一個(gè)值不再需要了,引用數(shù)卻不為0
,垃圾回收機(jī)制無(wú)法釋放這塊內(nèi)存,從而導(dǎo)致內(nèi)存泄漏
const arr = [1, 2, 3, 4];
console.log('hello world');
上面代碼中,數(shù)組[1, 2, 3, 4]
是一個(gè)值,會(huì)占用內(nèi)存。變量arr
是僅有的對(duì)這個(gè)值的引用,因此引用次數(shù)為1
。盡管后面的代碼沒有用到arr
,它還是會(huì)持續(xù)占用內(nèi)存
如果需要這塊內(nèi)存被垃圾回收機(jī)制釋放,只需要設(shè)置如下:
arr = null
通過設(shè)置arr
為null
,就解除了對(duì)數(shù)組[1,2,3,4]
的引用,引用次數(shù)變?yōu)?0,就被垃圾回收了
小結(jié)
有了垃圾回收機(jī)制,不代表不用關(guān)注內(nèi)存泄露。那些很占空間的值,一旦不再用到,需要檢查是否還存在對(duì)它們的引用。如果是的話,就必須手動(dòng)解除引用
三、常見內(nèi)存泄露情況
意外的全局變量
function foo(arg) {
? ?bar = "this is a hidden global variable";
}
另一種意外的全局變量可能由 this
創(chuàng)建:
function foo() {
? ?this.variable = "potential accidental global";
}
// foo 調(diào)用自己,this 指向了全局對(duì)象(window)
foo();
上述使用嚴(yán)格模式,可以避免意外的全局變量
定時(shí)器也常會(huì)造成內(nèi)存泄露
var someResource = getData();
setInterval(function() {
? ?var node = document.getElementById('Node');
? ?if(node) {
? ? ? ?// 處理 node 和 someResource
? ? ? ?node.innerHTML = JSON.stringify(someResource));
? }
}, 1000);
如果id
為Node的元素從DOM
中移除,該定時(shí)器仍會(huì)存在,同時(shí),因?yàn)榛卣{(diào)函數(shù)中包含對(duì)someResource
的引用,定時(shí)器外面的someResource
也不會(huì)被釋放
包括我們之前所說(shuō)的閉包,維持函數(shù)內(nèi)局部變量,使其得不到釋放
function bindEvent() {
?var obj = document.createElement('XXX');
?var unused = function () {
? ?console.log(obj, '閉包內(nèi)引用obj obj不會(huì)被釋放');
};
?obj = null; // 解決方法
}
沒有清理對(duì)DOM
元素的引用同樣造成內(nèi)存泄露文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-776924.html
const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom刪除了
console.log(refA, 'refA'); // 但是還存在引用能console出整個(gè)div 沒有被回收
refA = null;
console.log(refA, 'refA'); // 解除引用
包括使用事件監(jiān)聽addEventListener
監(jiān)聽的時(shí)候,在不監(jiān)聽的情況下使用removeEventListener
取消對(duì)事件監(jiān)聽文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-776924.html
到了這里,關(guān)于JavaScript中的數(shù)據(jù)緩存與內(nèi)存泄露:解密前端性能優(yōu)化與代碼健康的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!