帮助查找JavaScript内存泄漏的功能

2 分•作者: EGreg•大约 1 年前•原帖
我在理解内存泄漏的来源时遇到了很大的困难,尤其是在 iOS Safari 上。我会进入开发者工具的时间线标签,看到内存不断增加,但不确定是如何或在哪里发生的。因此,我编写了这个函数来遍历所有由各种软件添加的全局对象,避免重复访问同一对象。这个函数是异步的,以免过多阻塞用户体验。你可以运行它来开始查看引用泄漏的地方。 ```javascript Q = {}; Q.globalNames = Object.keys(window); // 快照基线 Q.globalNamesAdded = function () { const current = Object.keys(window); const baseline = Q.globalNames; const added = []; for (let i = 0; i < current.length; i++) { if (!baseline.includes(current[i])) { added.push(current[i]); } } return added; }; Q.walkGlobalsAsync = function (filterFn, options = {}) { const seen = new WeakSet(); const found = new Set(); const pathMap = new WeakMap(); const maxDepth = options.maxDepth || 5; const includeStack = options.includeStack || false; const logEvery = options.logEvery || 100; const startingKeys = Q.globalNamesAdded ? Q.globalNamesAdded() : Object.keys(window); let totalChecked = 0; let matchesFound = 0; function walk(obj, path = 'window', depth = 0) { if (!obj || typeof obj !== 'object') return; if (seen.has(obj)) return; seen.add(obj); totalChecked++; if (totalChecked % logEvery === 0) { console.log(`检查了 ${totalChecked} 个对象,发现了 ${matchesFound} 个`); } if (filterFn(obj)) { found.add(obj); matchesFound++; if (includeStack) { pathMap.set(obj, path); console.log(`[发现] ${path}`, obj); } else { console.log(`[发现]`, obj); } } if (depth >= maxDepth) return; const skipKeys = obj instanceof HTMLElement ? new Set([ 'parentNode', 'parentElement', 'nextSibling', 'previousSibling', 'firstChild', 'lastChild', 'children', 'childNodes', 'ownerDocument', 'style', 'classList', 'dataset', 'attributes', 'innerHTML', 'outerHTML', 'nextElementSibling', 'previousElementSibling' ]) : null; for (const key in obj) { if (skipKeys && skipKeys.has(key)) continue; try { walk(obj[key], path + '.' + key, depth + 1); } catch (e) {} } } let i = 0; function nextBatch() { const batchSize = 10; const end = Math.min(i + batchSize, startingKeys.length); for (; i < end; i++) { try { walk(window[startingKeys[i]], 'window.' + startingKeys[i], 0); } catch (e) {} } if (i < startingKeys.length) { setTimeout(nextBatch, 0); // 安排下一批 } else { console.log(`完成。发现 ${matchesFound} 个保留对象。`); if (includeStack) { console.log([...found].map(obj => ({ object: obj, path: pathMap.get(obj) }))); } else { console.log([...found]); } } } nextBatch(); }; ``` 使用方法如下: ```javascript Q.walkGlobalsAsync( obj => obj instanceof HTMLElement && !document.contains(obj), { includeStack: true, maxDepth: 4, logEvery: 50 } ); ``` 但是请注意,这不会找到被闭包保留的对象,即使你能找到闭包本身,你仍然需要手动检查它们的代码。
查看原文
I had a tough time understanding where memory leaks are coming from, especially on iOS safari. I&#x27;d go into Dev Tools &gt; Timelines tab and see the memory go up, but not sure how or where. So I wrote this function to traverse all the global objects that have been added by various software, avoiding revisiting the same objects more than once. The function is async so as not to tie up the UX too much. You can run it to start seeing where the references are being leaked.<p><pre><code> Q = {}; Q.globalNames = Object.keys(window); &#x2F;&#x2F; snapshot baseline Q.globalNamesAdded = function () { const current = Object.keys(window); const baseline = Q.globalNames; const added = []; for (let i = 0; i &lt; current.length; i++) { if (!baseline.includes(current[i])) { added.push(current[i]); } } return added; }; Q.walkGlobalsAsync = function (filterFn, options = {}) { const seen = new WeakSet(); const found = new Set(); const pathMap = new WeakMap(); const maxDepth = options.maxDepth || 5; const includeStack = options.includeStack || false; const logEvery = options.logEvery || 100; const startingKeys = Q.globalNamesAdded ? Q.globalNamesAdded() : Object.keys(window); let totalChecked = 0; let matchesFound = 0; function walk(obj, path = &#x27;window&#x27;, depth = 0) { if (!obj || typeof obj !== &#x27;object&#x27;) return; if (seen.has(obj)) return; seen.add(obj); totalChecked++; if (totalChecked % logEvery === 0) { console.log(`Checked ${totalChecked} objects, found ${matchesFound}`); } if (filterFn(obj)) { found.add(obj); matchesFound++; if (includeStack) { pathMap.set(obj, path); console.log(`[FOUND] ${path}`, obj); } else { console.log(`[FOUND]`, obj); } } if (depth &gt;= maxDepth) return; const skipKeys = obj instanceof HTMLElement ? new Set([ &#x27;parentNode&#x27;, &#x27;parentElement&#x27;, &#x27;nextSibling&#x27;, &#x27;previousSibling&#x27;, &#x27;firstChild&#x27;, &#x27;lastChild&#x27;, &#x27;children&#x27;, &#x27;childNodes&#x27;, &#x27;ownerDocument&#x27;, &#x27;style&#x27;, &#x27;classList&#x27;, &#x27;dataset&#x27;, &#x27;attributes&#x27;, &#x27;innerHTML&#x27;, &#x27;outerHTML&#x27;, &#x27;nextElementSibling&#x27;, &#x27;previousElementSibling&#x27; ]) : null; for (const key in obj) { if (skipKeys &amp;&amp; skipKeys.has(key)) continue; try { walk(obj[key], path + &#x27;.&#x27; + key, depth + 1); } catch (e) {} } } let i = 0; function nextBatch() { const batchSize = 10; const end = Math.min(i + batchSize, startingKeys.length); for (; i &lt; end; i++) { try { walk(window[startingKeys[i]], &#x27;window.&#x27; + startingKeys[i], 0); } catch (e) {} } if (i &lt; startingKeys.length) { setTimeout(nextBatch, 0); &#x2F;&#x2F; Schedule next batch } else { console.log(`Done. Found ${matchesFound} retained objects.`); if (includeStack) { console.log([...found].map(obj =&gt; ({ object: obj, path: pathMap.get(obj) }))); } else { console.log([...found]); } } } nextBatch(); }; </code></pre> Here is how you use it:<p><pre><code> Q.walkGlobalsAsync( obj =&gt; obj instanceof HTMLElement &amp;&amp; !document.contains(obj), { includeStack: true, maxDepth: 4, logEvery: 50 } ); </code></pre> However -- note that this will NOT find objects retained by closures, even if you can find the closures themselves you&#x27;re going to have to check their code manually.