樹(Tree)是 n
個結點的有限集,n
爲 0
時,稱爲空樹,在任意一棵非空樹中有且僅有一個特定的被稱爲根(Root)的結點,當 n
大於 1
時,其他結點可分爲 m
個互不相交的有限集 T1
、T2
、......
、Tm
,其中每個集合自己又是一棵樹,而且稱爲 SubTree
,即根的子樹。數組
須要強調的是,n>0
時根結點是惟一的,不可能存在多個根結點,m>0
時,子樹的個數沒有限制,但它們必定是互不相交的。異步
從根開始定義起,根爲第一層,根的孩子爲第二層,若某結點在第 l
層,則其子樹就在第 l+1
層,其雙親在同一層的結點互爲 「堂兄弟」,樹中結點的最大層級數稱爲樹的深度(Depth)或高度。async
在對樹結構進行遍歷時,按順序可分爲先序、中序和後續,按遍歷的方式可分爲深度優先和廣度優先,咱們這篇文章就經過使用先序深度優先和先序廣度優先來實現 NodeJS 中遞歸刪除目錄結構,體會對樹結構的遍歷,文章中會大量用到 NodeJS 核心模塊 fs
的方法,能夠經過 NodeJS 文件操做 —— fs 基本使用 來了解文中用到的 fs
模塊的方法及用法。函數
深度優先的意思就是在遍歷當前文件目錄的時候,若是子文件夾內還有內容,就繼續遍歷子文件夾,直到遍歷到最深層再也不有文件夾,則刪除其中的文件,再刪除這個文件夾,而後繼續遍歷它的 「兄弟」,直到內層文件目錄都被刪除,再刪除上一級,最後根文件夾爲空,刪除根文件夾。性能
咱們要實現的函數參數爲要刪除的根文件夾的路徑,執行函數後會刪除這個根文件夾。ui
// 深度優先 —— 同步 // 引入依賴模塊 const fs = require("fs"); const path = require("path"); // 先序深度優先同步刪除文件夾 function rmDirDepSync(p) { // 獲取根文件夾的 Stats 對象 let statObj = fs.statSync(p); // 檢查該文件夾的是不是文件夾 if (statObj.isDirectory()) { // 查看文件夾內部 let dirs = fs.readdirSync(p); // 將內部的文件和文件夾拼接成正確的路徑 dirs = dirs.map(dir => path.jion(p, dir)); // 循環遞歸處理 dirs 內的每個文件或文件夾 for (let i = 0; i < dirs.length; i++) { rmDirDepSync(dirs[i]); } // 等待都處理完後刪除該文件夾 fs.rmdirSync(p); } else { // 如果文件則直接刪除 fs.unlinkSync(p); } } // 調用 rmDirDepSync("a");
上面代碼在調用 rmDirDepSync
時傳入 a
,先判斷 a
是不是文件夾,不是則直接刪除文件,是則查看文件目錄,使用 map
將根文件路徑拼接到每個成員的名稱前,並返回合法的路徑集合,循環這個集合並對每一項進行遞歸,重複執行操做,最終實現刪除根文件夾內全部的文件和文件夾,並刪除根文件夾。spa
同步的實現會阻塞代碼的執行,每次執行一個文件操做,必須在執行完畢以後才能執行下一行代碼,相對於同步,異步的方式性能會更好一些,咱們下面使用異步回調的方式來實現遞歸刪除文件目錄的函數。code
函數有兩個參數,第一個參數一樣爲根文件夾的路徑,第二個參數爲一個回調函數,在文件目錄被所有刪除後執行。對象
// 深度優先 —— 異步回調 // 引入依賴模塊 const fs = require("fs"); const path = require("path"); // 先序深度優先異步(回調函數)刪除文件夾 function rmDirDepCb(p, callback) { // 獲取傳入路徑的 Stats 對象 fs.stat(p, (err, statObj) => { // 判斷路徑下是否爲文件夾 if (statObj.isDirectory()) { // 是文件夾則查看內部成員 fs.readdir(p, (err, dirs) => { // 將文件夾成員拼接成合法路徑的集合 dirs = dirs.map(dir => path.join(p, dir)); // next 方法用來檢查集合內每個路徑 function next(index) { // 若是全部成員檢查並刪除完成則刪除上一級目錄 if (index === dirs.length) return fs.rmdir(p, callback); // 對路徑下每個文件或文件夾執行遞歸,回調爲遞歸 next 檢查路徑集合中的下一項 rmDirDepCb(dirs[index], () => next(index + 1)); } next(0); }); } else { // 是文件則直接刪除 fs.unlink(p, callback); } }); } // 調用 rmDirDepCb("a", () => { console.log("刪除完成"); }); // 刪除完成
上面方法也遵循深度優先,與同步相比較主要思路是相同的,異步回調的實現更爲抽象,並非經過循環去處理的文件夾下的每一個成員的路徑,而是經過調用 next
函數和在成功刪除文件時遞歸執行 next
函數並維護 index
變量實現的。blog
在異步回調函數的實現方式中,回調嵌套層級很是多,這在對代碼的可讀性和維護性上都形成困擾,在 ES6 規範中,Promise 的出現就是用來解決 「回調地獄」 的問題,因此咱們也使用 Promise 來實現。
函數的參數爲要刪除的根文件夾的路徑,此次之因此不須要傳 callback
參數是由於 callback
中的邏輯能夠在調用函數以後鏈式調用 then
方法來執行。
// 深度優先 —— 異步 Promise // 引入依賴模塊 const fs = require("fs"); const path = require("path"); // 先序深度優先異步(Promise)刪除文件夾 function rmDirDepPromise(p) { return new Promise((resolve, reject) => { // 獲取傳入路徑的 Stats 對象 fs.stat(p, (err, statObj) => { // 判斷路徑下是否爲文件夾 if (statObj.isDirectory()) { // 是文件夾則查看內部成員 fs.readdir(p, (err, dirs) => { // 將文件夾成員拼接成合法路徑的集合 dirs = dirs.map(dir => path.join(p, dir)); // 將全部的路徑都轉換成 Promise dirs = dirs.map(dir => rmDirDepPromise(dir)); // 數組中路徑下全部的 Promise 都執行了 resolve 時,刪除上級目錄 Promise.all(dirs).then(() => fs.rmdir(p, resolve)); }); } else { // 是文件則直接刪除 fs.unlink(p, resolve); } }); }); } // 調用 rmDirDepPromise("a").then(() => { console.log("刪除完成"); }); // 刪除完成
與異步回調函數的方式不一樣的是在調用 rmDirDepPromise
時直接返回了一個 Promise 實例,而在刪除文件成功或在刪除文件夾成功時直接調用了 resolve
,在一個子文件夾下直接將這些成員經過遞歸 rmDirDepPromise
都轉換爲 Promise 實例,則能夠用 Primise.all
來監聽這些成員刪除的狀態,若是都成功再調用 Primise.all
的 then
直接刪除上一級目錄。
Promise 版本相對於異步回調版本從代碼的可讀性上有所提高,可是實現邏輯仍是比較抽象,沒有同步代碼的可讀性好,若是想要 「魚」 和 「熊掌」 兼得,既要性能又要可讀性,可使用 ES7 標準中的 async/await
來實現。
因爲 async
函數的返回值爲一個 Promise 實例,因此參數只須要傳被刪除的根文件夾的路徑便可。
// 深度優先 —— 異步 async/await // 引入依賴模塊 const fs = require("fs"); const path = require("path"); const { promisify } = require("util"); // 將用到 fs 模塊的異步方法轉換成 Primise const stat = promisify(fs.stat); const readdir = promisify(fs.readdir); const rmdir = promisify(fs.rmdir); const unlink = promisify(fs.unlink); // 先序深度優先異步(async/await)刪除文件夾 async function rmDirDepAsync(p) { // 獲取傳入路徑的 Stats 對象 let statObj = await stat(p); // 判斷路徑下是否爲文件夾 if (statObj.isDirectory()) { // 是文件夾則查看內部成員 let dirs = await readdir(p); // 將文件夾成員拼接成合法路徑的集合 dirs = dirs.map(dir => path.join(p, dir)); // 循環集合遞歸 rmDirDepAsync 處理全部的成員 dirs = dirs.map(dir => rmDirDepAsync(dir)); // 當全部的成員都成功 await Promise.all(dirs); // 刪除該文件夾 await rmdir(p); } else { // 是文件則直接刪除 await unlink(p); } } // 調用 rmDirDepAsync("a").then(() => { console.log("刪除完成"); }); // 刪除完成
在遞歸 rmDirDepAsync
時,全部子文件夾內部的成員必須都刪除成功,才刪除這個子文件夾,在使用 unlink
刪除文件時,必須等待文件刪除結束才能讓 Promise 執行完成,因此也須要 await
,全部遞歸以前的異步 Promise 都須要在遞歸內部的異步 Promise 執行完成後才能執行完成,因此涉及到異步的操做都使用了 await
進行等待。
廣度優先的意思是遍歷文件夾目錄的時候,先遍歷根文件夾,將內部的成員路徑一個一個的存入數組中,再繼續遍歷下一層,再將下一層的路徑都存入數組中,直到遍歷到最後一層,此時數組中的路徑順序爲第一層的路徑,第二層的路徑,直到最後一層的路徑,因爲要刪除的文件夾必須爲空,因此刪除時,倒序遍歷這個數組取出路徑進行文件目錄的刪除。
在廣度優先的實現方式中一樣按照同步、異步回調、和 異步 async/await
這幾種方式分別來實現,由於在拼接存儲路徑數組的時候沒有異步操做,因此單純使用 Promise 沒有太大的意義。
參數爲根文件夾的路徑,內部的 fs
方法一樣都使用同步方法。
// 廣度優先 —— 同步 // 引入依賴模塊 const fs = require("fs"); const path = require("path"); // 先序廣度優先同步刪除文件夾 function rmDirBreSync(p) { let pathArr = [p]; // 建立存儲路徑的數組,默認存入根路徑 let index = 0; // 用於存儲取出數組成員的索引 let current; // 用於存儲取出的成員,即路徑 // 若是數組中能找到當前指定索引的項,則執行循環體,並將該項存入 current while ((current = arr[index++])) { // 獲取當前從數組中取出的路徑的 Stats 對象 let statObj = fs.statSync(current); // 若是是文件夾,則讀取內容 if (statObj.isDirectory()) { let dirs = fs.readdir(current); // 將獲取到的成員路徑處理爲合法路徑 dirs = dirs.map(dir => path.join(current, dir)); // 將原數組的成員路徑和處理後的成員路徑從新解構在 pathArr 中 pathArr = [...pathArr, ...dirs]; } } // 逆序循環 pathArr for (let i = pathArr.length - 1; i >= 0; i--) { let pathItem = pathArr[i]; // 當前循環項 let statObj = fs.statSync(pathItem); // 獲取 Stats 對象 // 若是是文件夾則刪除文件夾,是文件則刪除文件 if (statObj.isDirectory()) { fs.rmdirSync(pathItem); } else { fs.unlinkSync(pathItem); } } } // 調用 rmDirBreSync("a");
經過 while
循環廣度遍歷,將全部的路徑按層級順序存入 pathArr
數組中,在經過 for
反向遍歷數組,對遍歷到的路徑進行判斷並調用對應的刪除方法,pathArr
後面的項存儲的都是最後一層的路徑,從後向前路徑的層級逐漸減少,因此反向遍歷不會致使刪除非空文件夾的操做。
函數有兩個參數,第一個參數爲根文件夾的路徑,第二個爲 callback
,在刪除結束後執行。
// 廣度優先 —— 異步回調 // 引入依賴模塊 const fs = require("fs"); const path = require("path"); // 先序廣度優先異步(回調函數)刪除文件夾 function rmDirBreCb(p, callback) { let pathArr = [p]; // 建立存儲路徑的數組,默認存入根路徑 function next(index) { // 若是已經都處理完,則調用刪除的函數 if (index === pathArr.length) return remove(); // 取出數組中的文件路徑 let current = arr[index]; // 獲取取出路徑的 Stats 對象 fs.stat(currrent, (err, statObj) => { // 判斷是不是文件夾 if (statObj.isDirectory()) { // 是文件夾讀取內部成員 fs.readdir(current, (err, dirs) => { // 將數組中成員名稱修改成合法路徑 dirs = dirs.map(dir => path.join(current, dir)); // 將原數組的成員路徑和處理後的成員路徑從新解構在 pathArr 中 pathArr = [...pathArr, ...dirs]; // 遞歸取出數組的下一項進行檢測 next(index + 1); }); } else { // 若是是文件則直接遞歸獲取數組的下一項進行檢測 next(index + 1); } }); } next(0); // 刪除的函數 function remove() { function next(index) { // 若是所有刪除完成,執行回調函數 if (index < 0) return callback(); // 獲取數組的最後一項 let current = pathArr[index]; // 獲取該路徑的 Stats 對象 fs.stat(current, (err, statObj) => { // 不論是文件仍是文件夾都直接刪除 if (statObj.isDirectory()) { fs.rmdir(current, () => next(index - 1)); } else { fs.unlink(current, () => next(index - 1)); } }); } next(arr.length - 1); } } // 調用 rmDirBreCb("a", () => { console.log("刪除完成"); }); // 刪除完成
在調用 rmDirBreCb
時主要執行兩個步驟,第一個步驟是構造存儲路徑的數組,第二個步驟是逆序刪除數組中對應的文件或文件夾,爲了保證性能,兩個過程都是經過遞歸 next
函數並維護存儲索引的變量來實現的,而非循環。
在構造數組的過程當中若是構造數組完成後,調用的刪除函數 remove
,在 remove
中在刪除完成後,調用的 callback
,實現思路是相同的,都是在遞歸時設置判斷條件,若是構造數組或刪除結束之後不繼續遞歸,而是直接執行對應的函數並跳出。
參數爲刪除根文件夾的路徑,由於 async
最後返回的是 Promise 實例,因此不須要 callback
,刪除後的邏輯能夠經過調用返回 Promise 實例的 then
來實現。
// 廣度優先 —— 異步 async/await // 引入依賴模塊 const fs = require("fs"); const path = require("path"); const { promisify } = require("util"); // 將用到 fs 模塊的異步方法轉換成 Primise const stat = promisify(fs.stat); const readdir = promisify(fs.readdir); const rmdir = promisify(fs.rmdir); const unlink = promisify(fs.unlink); // 先序廣度優先異步(async/await)刪除文件夾 async function rmDirBreAsync(p) { let pathArr = [p]; // 建立存儲路徑的數組,默認存入根路徑 let index = 0; // 去數組中取出路徑的索引 // 若是存在該項則繼續循環 while (index !== pathArr.length) { // 取出當前的路徑 let current = pathArr[index]; // 獲取 Stats 對象 let statObj = await stat(current); // 判斷是不是文件夾 if (statObj.isDirectory()) { // 查看文件夾成員 let dirs = await readdir(current); // 將路徑集合更改成合法路徑集合 dirs = dirs.map(dir => path.join(current, dir)); // 合併存儲路徑的數組 pathArr = [...pathArr, ...dirs]; } index++; } let current; // 刪除的路徑 // 循環取出路徑 while ((current = pathArr.pop())) { // 獲取 Stats 對象 let statObj = await stat(current); // 不論是文件仍是文件夾都直接刪除 if (statObj.isDirectory()) { await rmdir(current); } else { await unlink(current); } } } // 調用 rmDirBreAsync("a").then(() => { console.log("刪除完成"); }); // 刪除完成
上面的寫法都是使用同步的寫法,但對文件的操做都是異步的,並使用 await
進行等待,在建立路徑集合的數組和倒序刪除的過程都是經過 while
循環實現的。
深度優先和廣度優先的兩種遍歷方式應該是考慮具體場景選擇最適合的方式使用,上面這麼多實現遞歸刪除文件目錄的方法中,重點在於體會深度遍歷和廣度遍歷的不一樣,其實在相似於遞歸刪除文件目錄的這種功能使用深度優先更適合一些。