NodeJS 树结构遍历 —— 深度优先和广度优先

在这里插入图片描述

原文阅读


树的基本概念

树(Tree)是 n 个结点的有限集,n0 时,称为空树,在任意一棵非空树中有且仅有一个特定的被称为根(Root)的结点,当 n 大于 1 时,其他结点可分为 m 个互不相交的有限集 T1T2......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

三、异步 Promise 的实现

在异步回调函数的实现方式中,回调嵌套层级很是多,这在对代码的可读性和维护性上都形成困扰,在 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.allthen 直接删除上一级目录。

四、异步 async/await 的实现

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/await 的实现

参数为删除根文件夹的路径,由于 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 循环实现的。

总结

深度优先和广度优先的两种遍历方式应该是考虑具体场景选择最适合的方式使用,上面这么多实现递归删除文件目录的方法中,重点在于体会深度遍历和广度遍历的不一样,其实在相似于递归删除文件目录的这种功能使用深度优先更适合一些。

相关文章
相关标签/搜索