深入剖析JavaScript內存泄漏:識別、定位與實戰解決

在JavaScript的世界里,開發者通常不必像使用C++那樣手動管理內存的分配和釋放,這得益于JavaScript引擎內置的垃圾回收(Garbage Collection, GC)機制。然而,這并不意味著我們可以完全忽視內存管理。“自動"不等于"萬無一失”,內存泄漏仍然是JavaScript應用中一個常見且棘手的問題,它會悄無聲息地蠶食系統資源,導致應用性能下降、響應遲緩,甚至最終崩潰。

本文將帶你深入理解JavaScript內存泄漏的本質,探討常見的泄漏場景,并提供一套系統的識別、定位和解決內存泄漏問題的實戰方法。

一、什么是JavaScript內存泄漏?

簡單來說,內存泄漏指的是程序中不再需要使用的內存,由于某種原因未能被垃圾回收器正確識別并釋放,從而長期駐留在內存中,導致可用內存逐漸減少的現象。

想象一下你的房間:垃圾回收器就像一個清潔工,會定期清理掉你明確丟棄(不再引用)的垃圾。但如果有些東西你已經不用了,卻忘記扔掉,或者不小心把它藏在了一個你以為空了、但實際還連著其他東西的盒子里(間接引用),清潔工就不會把它清理掉,久而久之,房間就會被這些“遺忘的垃圾”堆滿。在JavaScript中,這些“遺忘的垃圾”就是無法被回收的內存對象。

二、JavaScript內存管理與垃圾回收(GC)基礎

要理解泄漏,得先明白內存是如何被管理的。JavaScript引擎(如V8)管理內存主要涉及:

  1. 分配內存: 當你創建變量、對象、函數等時,引擎會分配內存來存儲它們。
  2. 使用內存: 在代碼中讀取、寫入變量和對象屬性。
  3. 釋放內存: 這就是垃圾回收器的工作。GC的核心任務是找出那些“不再可達”(unreachable)的對象,并釋放它們占用的內存。

可達性(Reachability) 是關鍵概念。GC從一組已知的“根”(Roots)對象(如全局對象、當前函數調用棧中的變量等)開始,沿著引用鏈遍歷所有可以訪問到的對象。所有可達的對象都被認為是“活”的,不可達的對象則被認為是“死”的,可以被回收。

最常見的GC算法是標記-清除(Mark-and-Sweep)

  • 標記階段: 從根對象出發,遞歸地訪問所有可達對象,并給它們打上標記。
  • 清除階段: 遍歷整個內存堆,清除所有未被標記的對象,回收它們占用的空間。

內存泄漏的根本原因,往往是開發者無意中維持了對不再需要的對象的引用,使得GC誤以為這些對象仍然“可達”,從而無法回收它們。

三、常見的JavaScript內存泄漏場景及剖析

以下是一些在實際開發中非常容易踩坑的內存泄漏場景:

1. 意外的全局變量

問題描述: 在JavaScript中,如果在非嚴格模式下('use strict')忘記聲明變量(未使用var, let, const),該變量會被隱式地創建為全局對象的屬性(在瀏覽器中是window對象)。全局變量通常在頁面關閉前不會被回收。

function createData() {// 忘記使用 let/const/varleakyData = new Array(1000000).join('*'); // leakyData 成為 window.leakyData
}// 調用后,即使 createData 函數執行完畢,leakyData 依然存在于全局作用域
createData();
// window.leakyData 仍然持有對大字符串的引用

為何泄漏: leakyData 成為全局變量,被根對象window引用,因此GC認為它是可達的,即使我們后續不再需要它。

解決方案:

  • 始終使用 'use strict'; 模式,它會在試圖創建隱式全局變量時拋出錯誤。
  • 確保所有變量都使用let, const, 或 var (函數作用域內) 正確聲明。

2. 被遺忘的定時器與回調

問題描述: setIntervalsetTimeout 創建的定時器,如果其回調函數引用了外部作用域的變量(形成了閉包),并且這個定時器沒有被清除(clearInterval / clearTimeout),那么即使你認為相關的對象或DOM元素已經不再需要,定時器回調函數及其閉包所引用的內存也無法被回收。

function setupTimer() {let data = { counter: 0, largeObject: new Array(100000).fill('data') };setInterval(() => {// 這個回調函數隱式地持有了對 data 對象的引用console.log(data.counter++);// 假設在某個時刻,我們不再需要這個定時器和 data 了,// 但忘記清除定時器...}, 1000);// 忘記調用 clearInterval(timerId);
}setupTimer();

為何泄漏: 只要 setInterval 還在運行,它的回調函數就一直存活。回調函數通過閉包引用了 data 對象,導致 data 對象及其包含的 largeObject 永遠無法被GC回收,即使調用 setupTimer 的上下文已經銷毀。

解決方案:

  • 在不再需要定時器時,務必使用 clearIntervalclearTimeout 清除它們。
  • 在組件卸載或頁面離開等生命周期鉤子中清理定時器是常見的實踐(例如在React的useEffect的清理函數中,或Vue的beforeDestroy/unmounted鉤子中)。
function setupTimer() {let data = { counter: 0, largeObject: new Array(100000).fill('data') };const timerId = setInterval(() => {console.log(data.counter++);}, 1000);// 返回一個清理函數,或在適當的時候調用return function cleanup() {clearInterval(timerId);data = null; // 可選,幫助更快釋放,但主要靠clearInterval};
}const cleanupTimer = setupTimer();
// ... 在未來某個時刻 ...
cleanupTimer(); // 清除定時器,釋放閉包引用的內存

3. 閉包引起的內存泄漏

問題描述: 閉包是JavaScript的強大特性,但也容易成為內存泄漏的溫床。當一個內部函數引用了其外部函數的變量,并且這個內部函數被傳遞到外部作用域并長期存活時,外部函數的整個活動對象(包含所有局部變量)可能都無法被回收,即使只有部分變量被內部函數實際使用。

function createClosure() {const largeData = new Array(1000000).join('x'); // 一個大對象const unusedData = new Array(500000).join('y'); // 未被內部函數使用return function innerFunction() {// innerFunction 只用到了 largeData 的長度,但閉包會保持對整個外部作用域的引用console.log(largeData.length);};
}// globalClosure 持有了 innerFunction 的引用
// innerFunction 通過閉包持有了 createClosure 的作用域
let globalClosure = createClosure();// 即使我們只關心 largeData.length,unusedData 也因為閉包的存在而無法被回收
// 只有當 globalClosure 不再被引用時 (e.g., globalClosure = null),閉包作用域才可能被回收

為何泄漏: innerFunction 形成了閉包,保持了對 createClosure 函數作用域的引用。雖然 innerFunction 只直接使用了 largeData,但引擎通常會保持整個作用域鏈(或至少是優化后的部分),導致 unusedData 這樣的大對象也無法釋放。

解決方案:

  • 謹慎設計閉包: 只讓閉包引用確實需要的變量。如果可能,在不再需要時解除對閉包的引用(例如,將 globalClosure 設為 null)。
  • 避免在閉包中持有不必要的大對象引用: 如果只需要對象的某個屬性,考慮在創建閉包時只傳入該屬性值,而不是整個對象。

4. 未移除的DOM事件監聽器

問題描述: 當你給一個DOM元素添加了事件監聽器,這個監聽器函數通常會隱式或顯式地引用該DOM元素或其他變量。如果你之后通過JavaScript移除了這個DOM元素(例如,element.remove() 或通過innerHTML替換),但沒有移除附加在其上的事件監聽器,那么監聽器函數及其可能通過閉包引用的任何對象(包括那個已被移除的DOM元素本身!)都無法被回收。

function attachListener() {const button = document.getElementById('myButton');const largeData = new Array(100000).fill('event data');function handleClick() {console.log('Button clicked!', largeData.length);// handleClick 通過閉包引用了 largeData}button.addEventListener('click', handleClick);// ... 稍后 ...// 假設我們通過JS移除了按鈕,但忘記移除監聽器button.parentNode.removeChild(button);// 或者 button = null; (這只斷開了變量button對元素的引用,沒移除監聽器)// 此時,雖然按鈕不在DOM樹中,但瀏覽器內部可能仍然保持著對按鈕元素// 的引用,因為 handleClick 監聽器還附加在上面,而 handleClick 又被// (瀏覽器的事件分發機制)間接引用。同時,handleClick 還引用了 largeData。
}// 如果 attachListener 被反復調用,且每次都不清理監聽器,泄漏會累積

為何泄漏: 事件監聽器機制需要保持對回調函數 (handleClick) 和目標元素 (button) 的引用。即使 button 從DOM樹中移除,只要監聽器未被 removeEventListener 移除,這條引用鏈就存在,阻止了 button 元素和 handleClick 函數(及其閉包環境,包括 largeData)被GC回收。

解決方案:

  • 始終在元素被銷毀或不再需要監聽時,使用 removeEventListener 移除監聽器。 確保傳入 removeEventListener 的參數(事件類型、回調函數引用、捕獲/冒泡選項)與 addEventListener 時完全一致。
  • 使用 WeakMap 或框架提供的機制管理監聽器: 對于需要動態添加/移除大量元素的場景,可以考慮使用 WeakMap 來存儲與元素關聯的數據或回調,當元素被GC回收時,WeakMap 中的對應條目會自動消失。現代前端框架通常有自己的生命周期管理,會自動處理組件銷毀時的監聽器移除。

5. DOM引用泄漏(Detached DOM)

問題描述: 與事件監聽器類似,如果在JavaScript代碼中持有對DOM元素的引用,即使該元素已經從DOM樹中移除,只要這個JS引用還存在,該DOM元素及其子元素就無法被回收。

let detachedTree; // 全局變量或某個長期存活的對象屬性function createDetachedTree() {const ul = document.createElement('ul');for (let i = 0; i < 1000; i++) {const li = document.createElement('li');li.textContent = `Item ${i}`;ul.appendChild(li);}// 將這個 ul 存儲在一個變量中detachedTree = ul;// 這個 ul 從未被添加到主 DOM 樹,或者被添加后又被移除了// document.body.appendChild(ul);// ... later ...// document.body.removeChild(ul); // 從 DOM 移除// 只要 detachedTree 這個變量還在,并且可達,// 整個 ul 元素及其所有子 li 元素都無法被回收。
}createDetachedTree();
// 假設 detachedTree 一直存在,這1001個DOM節點就泄漏了

為何泄漏: detachedTree 變量直接引用了 ul 元素。即使 ul 不在可視的DOM樹中,只要這個JavaScript引用存在,GC就認為它是可達的。

解決方案:

  • 在不再需要DOM元素引用時,手動將其設為 null detachedTree = null;
  • 利用 WeakMap 如果你需要將某些數據與DOM元素關聯,但又不希望這種關聯阻止DOM元素被回收,可以使用 WeakMap,將DOM元素作為鍵。當DOM元素被回收后,WeakMap 中的條目會自動移除。

四、識別與定位內存泄漏:Chrome DevTools實戰

發現內存泄漏的存在通常源于觀察到應用性能隨時間推移而下降,或者內存占用持續增長。Chrome DevTools是診斷內存問題的強大武器。

1. 使用 Performance Monitor 實時監控

  • 打開DevTools (F12Ctrl+Shift+I/Cmd+Option+I)。
  • Esc 打開下方的抽屜(Console Drawer),點擊三個點選擇 Performance monitor
  • 勾選 JS heap size
  • 操作你的應用,執行那些你懷疑可能導致泄漏的操作(例如,反復打開/關閉某個組件、加載大量數據)。
  • 觀察 JS heap size 圖表。如果內存持續增長且從不回落到穩定水平,即使手動觸發GC(在 Memory 面板點擊垃圾桶圖標)后也是如此,那么很可能存在內存泄漏。

2. 使用 Memory 面板 - Heap Snapshot 對比

這是定位內存泄漏最核心的方法。

  • 操作流程:
    1. 打開DevTools -> Memory 面板。
    2. 拍下基線快照: 加載頁面,達到一個穩定狀態后,點擊 Take snapshot 按鈕,生成快照1(Snapshot 1)。
    3. 執行疑似泄漏的操作: 在應用中執行你懷疑導致內存泄漏的操作序列,可以重復幾次以放大效果。
    4. 拍下對比快照: 操作完成后,再次點擊 Take snapshot,生成快照2(Snapshot 2)。
    5. 對比快照:
      • 在快照列表中選中 Snapshot 2。
      • 在下方的下拉菜單中,選擇 Comparison(對比)。
      • 在其旁邊的下拉菜單中,選擇 Snapshot 1 作為對比基準。
    6. 分析對比結果:
      • 排序:#Delta(增量)或 Size Delta(大小增量)降序排序。這會顯示兩次快照之間新增了哪些對象,或者哪些類型的對象數量顯著增加。
      • 關注點:
        • (Detached DOM tree) / Detached HTMLDivElement 等: 紅色高亮顯示的通常是脫離DOM樹但未被回收的元素,這是典型的DOM泄漏。點擊展開,查看其 Retainers(持有者)面板。
        • 自定義對象/閉包: 尋找你自己代碼中定義的構造函數或對象,如果它們的數量或大小異常增長,重點排查。
        • 數組/字符串: 大量的數組或長字符串增長也值得關注。
      • 分析 Retainers(持有者)鏈: 選中一個可疑的對象,下方的 Retainers 面板會顯示一個引用鏈,告訴你是什么東西阻止了這個對象被回收。從你的代碼根源(例如 window 或某個全局變量、事件監聽器、閉包)一直追溯到這個泄漏的對象。這通常能直接定位到泄漏源頭。

3. 使用 Memory 面板 - Allocation Instrumentation on Timeline / Allocation Sampling

  • Allocation Instrumentation on Timeline (舊版,更詳細但開銷大): 記錄一段時間內所有的內存分配。可以按時間線查看內存增長情況,并關聯到具體的函數調用。適合精確定位短期內大量分配內存的代碼。
  • Allocation Sampling (新版,開銷小,基于采樣): 記錄內存分配的來源函數。運行一段時間后停止記錄,可以得到一個按函數分配內存多少排序的列表。適合查找持續分配內存且可能導致泄漏的函數。

使用這些工具可以幫助你找到哪些代碼路徑正在創建大量對象,結合 Heap Snapshot 可以確認這些對象是否最終被回收。

五、解決與預防:最佳實踐

  • 編碼習慣:
    • 始終使用 'use strict';
    • letconst 代替 var,并確保變量在使用前已聲明。
    • 注意閉包范圍,避免無意中捕獲不需要的大對象。
  • 資源清理:
    • 定時器: setInterval/setTimeout 返回的ID要保存好,在不再需要時調用 clearInterval/clearTimeout
    • 事件監聽器: 使用 addEventListener 后,務必在適當時候(元素移除、組件卸載)使用 removeEventListener 清理。注意參數需完全匹配。考慮使用 AbortController / signal 來批量取消事件監聽。
    • DOM引用: 當不再需要對DOM元素的JS引用時,將其賦值為 null
    • Web Workers / Observers 等: 任何需要手動 terminate()disconnect() 的API,都要確保在生命周期結束時調用清理方法。
  • 善用現代特性:
    • WeakMap / WeakSet 用于將數據與對象關聯,而不會阻止對象被GC回收。非常適合緩存、存儲與DOM節點相關的信息等場景。
    • WeakRef / FinalizationRegistry (ES2021+): 提供更底層的弱引用和對象被回收時的回調通知能力,適用于更復雜的內存管理場景,但使用時需謹慎。
  • 框架生命周期: 熟悉你所使用的前端框架(React, Vue, Angular等)提供的生命周期鉤子函數,在組件卸載或銷毀時執行必要的清理操作。框架通常會幫助處理很多底層的DOM和事件監聽器管理。
  • 代碼審查: 將內存管理和資源清理作為代碼審查的一部分。
  • 持續監控: 對于大型或長期運行的應用,考慮集成性能監控工具(RUM - Real User Monitoring),持續觀察線上應用的內存使用情況。

六、結語

JavaScript內存泄漏是一個潛藏的性能殺手。雖然現代JS引擎的GC已經非常智能,但不良的編碼習慣和對資源生命周期管理的疏忽仍會導致內存無法釋放。理解GC的基本原理,熟悉常見的泄漏模式,并掌握使用DevTools等工具進行診斷的方法,是每個專業JavaScript開發者必備的技能。

記住,內存優化不是一蹴而就的事情,它需要持續的關注和實踐。養成良好的編碼習慣,時刻謹記資源的申請與釋放,才能構建出健壯、高效的JavaScript應用。


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/77511.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/77511.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/77511.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

2025-04-19 Python 強類型編程

文章目錄 1 方法標注1.1 參數與返回值1.2 變參類型1.3 函數類型 2 數據類型2.1 內置類型2.2 復雜數據結構2.3 類別選擇2.4 泛型 3 標注方式3.1 注釋標注3.2 文件標注 4 特殊情形4.1 前置引用4.2 函數標注擴展4.3 協變與逆變4.4 dataclass 5 高級內容5.1 接口5.2 泛型的協變/逆變…

ETF價格相關性計算算法深度分析

1. 引言 在金融市場中&#xff0c;相關性就像是資產之間“跳舞”的默契程度。想象一下兩位舞者&#xff08;ETF&#xff09;&#xff0c;有時步伐一致&#xff0c;有時各跳各的。對于管理大規模資金的投資組合而言&#xff0c;準確理解ETF之間的“舞步同步性”對于風險管理、資…

上海人工智能實驗室:LLM無監督自訓練

&#x1f4d6;標題&#xff1a;Genius: A Generalizable and Purely Unsupervised Self-Training Framework For Advanced Reasoning &#x1f310;來源&#xff1a;arXiv, 2504.08672 &#x1f31f;摘要 &#x1f538;推進LLM推理技能引起了廣泛的興趣。然而&#xff0c;當前…

【WPF】 在WebView2使用echart顯示數據

文章目錄 前言一、NuGet安裝WebView2二、代碼部分1.xaml中引入webview22.編寫html3.在WebView2中加載html4.調用js方法為Echarts賦值 總結 前言 為了實現數據的三維效果&#xff0c;所以需要使用Echarts&#xff0c;但如何在WPF中使用Echarts呢&#xff1f; 一、NuGet安裝WebV…

2025年3月 Python編程等級考試 2級真題試卷

2025年3月青少年軟件編程Python等級考試&#xff08;二級&#xff09;真題試卷 題目總數&#xff1a;37 總分數&#xff1a;100 選擇題 第 1 題 單選題 老師要求大家記住四大名著的作者&#xff0c;小明機智地想到了可以用字典進行記錄&#xff0c;以下哪個選項的字典…

6. 話題通信 ---- 使用自定義msg,發布方和訂閱方cpp,python文件編寫

1)在功能包下新建msg目錄&#xff0c;在msg目錄下新建Person.msg,在Person.msg文件寫入&#xff1a; string name uint16 age float64 height 2)修改配置文件 2.1) 功能包下package.xml文件修改 <build_depend>message_generation</build_depend><exec_depend…

多線程使用——線程安全、線程同步

一、線程安全 &#xff08;一&#xff09;什么是線程安全問題 多個線程&#xff0c;同時操作同一個共享資源的時候&#xff0c;可能會出現業務安全的問題。 &#xff08;二&#xff09;用程序摹擬線程安全問題 二、線程同步 &#xff08;一&#xff09;同步思想概述 解決線…

4. 話題通信 ---- 發布方和訂閱方cpp文件編寫

本節對應趙虛左ROS書籍的2.1.2 以10hz,發布消息和消息的訂閱 1) 在功能包的src文件夾下&#xff0c;新建cpp文件&#xff0c;并且寫入 #include "ros/ros.h" #include "std_msgs/String.h" int main(int argc, char *argv[]) {setlocale(LC_ALL,"&…

有哪些哲學流派適合創業二

好的&#xff0c;讓我們更深入地探討如何將?哲學與數學?深度融合&#xff0c;構建一套可落地的創業操作系統。以下從?認知框架、決策引擎、執行算法?三個維度展開&#xff0c;包含具體工具和黑箱拆解&#xff1a; ?一、認知框架&#xff1a;用哲學重構商業本質? 1. ?本體…

【后端】【python】Python 爬蟲常用的框架解析

一、總結 Python 爬蟲常用的框架主要分為 三類&#xff1a; 輕量級請求庫&#xff1a;如 requests、httpx&#xff0c;用于快速發請求。解析與處理庫&#xff1a;如 BeautifulSoup、lxml、pyquery。爬蟲框架系統&#xff1a;如 Scrapy、pyspider、Selenium、Playwright 等&am…

力扣-hot100(無重復字符的最長子串)

3. 無重復字符的最長子串 中等 給定一個字符串 s &#xff0c;請你找出其中不含有重復字符的 最長 子串 的長度。 示例 1: 輸入: s "abcabcbb" 輸出: 3 解釋: 因為無重復字符的最長子串是 "abc"&#xff0c;所以其長度為 3。暴力直觀解法一&#xff1…

六邊形棋盤格(Hexagonal Grids)的坐標

1. 二位坐標轉六邊形棋盤的方式 1-1這是“波動式”的 這種就是把【方格子坐標】“左右各錯開半個格子”做到的 具體來說有如下幾種情況 具體到廟算平臺上&#xff0c;是很巧妙的用一個4位整數&#xff0c;前兩位為x、后兩位為y來進行表示 附上計算距離的代碼 def get_hex_di…

C++之虛函數 Virtual Function

1. 普通虛函數&#xff08;Virtual Function&#xff09; 定義&#xff1a;基類中用 virtual 聲明&#xff0c;允許派生類 覆蓋&#xff08;Override&#xff09;。特點&#xff1a; 基類可提供默認實現。派生類可選擇性覆蓋&#xff08;若不覆蓋&#xff0c;則調用基類版本&a…

基于尚硅谷FreeRTOS視頻筆記——15—系統配制文件說明與數據規范

目錄 配置函數 INCLUDE函數 config函數 數據類型 命名規范 函數與宏 配置函數 官網上可以查找 最核心的就是 config和INCLUDE INCLUDE函數 這些就是裁剪的函數 它們使用一個ifndef。如果定義了&#xff0c;就如果定義了這個宏定義&#xff0c;那么代碼就生效。 通過ifn…

HAL庫配置RS485+DMA+空閑中斷收發數據

前言&#xff1a; &#xff08;1&#xff09;DMA是單片機集成在芯片內部的一個數據搬運工&#xff0c;它可以代替單片機對數據進行傳輸、存儲&#xff0c;節約CPU資源。一般應用場景&#xff0c;ADC多通道采集&#xff0c;串口收發&#xff08;頻繁進入接收中斷&#xff09;&a…

從零開始解剖Spring Boot啟動流程:一個Java小白的奇幻冒險之旅

大家好呀&#xff01;今天我們要一起探索一個神奇的話題——Spring Boot的啟動流程。我知道很多小伙伴一聽到"啟動流程"四個字就開始頭疼&#xff0c;別擔心&#xff01;我會用最通俗易懂的方式&#xff0c;帶你從main()方法開始&#xff0c;一步步揭開Spring Boot的…

下載HBuilder X,使用uniapp編寫微信小程序

到官網下載HBuilder X 地址&#xff1a;HBuilderX-高效極客技巧 下載完成后解壓 打開解壓后的文件夾找到HBuilderX.exe 打開顯示更多&#xff0c;發送到桌面快捷方式 到桌面上啟動HBuilderX.exe啟動應用 在工具點擊插件安裝 選擇安裝Vue3編譯器 點擊新建創建Vue3項目 編寫項目…

詳解與HTTP服務器相關操作

HTTP 服務器是一種遵循超文本傳輸協議&#xff08;HTTP&#xff09;的服務器&#xff0c;用于在網絡上傳輸和處理網頁及其他相關資源。以下是關于它的詳細介紹&#xff1a; 工作原理 HTTP 服務器監聽指定端口&#xff08;通常是 80 端口用于 HTTP&#xff0c;443 端口用于 HT…

2. ubuntu20.04 和VS Code實現 ros的輸出 (C++,Python)

本節對應趙虛左ROS書籍的1.4.2 1)創建工作空間 mkdir -p catkin_ws/src cd catkin_ws catkin_make 2) 終端進入VS Code code . 3) vscoe 的基本配置 3.1&#xff09;修改.vscode/tasks.json ,修改內容如下&#xff1a; { // 有關 tasks.json 格式的文檔&#xff0c;請參見…

SAP系統中MD01與MD02區別

知識點普及&#xff0d;MD01與MD02區別 1、從日常業務中&#xff0c;我們都容易知道MD01是運行全部物料&#xff0c;MD02是運行單個物料 2、在做配置測試中&#xff0c;也出現過MD02可以跑出物料&#xff0c;但是MD01跑不出的情況。 3、MD01與MD02的差異: 3.1、只要在物料主數…