xml地图|网站地图|网站标签 [设为首页] [加入收藏]

中的神器,的异步编程实践

浅谈 JavaScript 处理树形结构的几个场景与方案

2015/06/18 · CSS · 树形结构

原文出处: 工业聚(@工业聚)   

前言

node.js下when.js 的异步编程实践,node.jswhen.js

假设一个业务场景:

通过rss地址,获取rss并保存于文件,rss地址保存于文件中。

完成该场景的业务需要完成3个任务:

1.从文件中读取rss地址。

2.获取rss。

3.保存于文件。

最后将这三个任务进行整合。

准备:

存放rss地址的文件,address.txt。


 
任务1:

读取rss地址文件的内容并通过callback返回。

复制代码 代码如下:

var getRssAddress = function(path, callback) {
  fs.readFile(path, {encoding: 'utf8'}, function (err, data) {
    callback(err, data);
  });
}

任务2:

 通过rss地址get到rss,并通过callback返回错误或数据。

复制代码 代码如下:

var getRss = function(url, callback) {
  var data = '';
  http.get(url, function(res) {
    res.on('data', function(chrunk) {
      data += chrunk;
    });
    res.on('end', function() {
      callback(null, data);
    });
  }).on('error', function(err) {
    callback(err, null);
  });
}

 

任务3:

将rss保存于文件并通过callback返回错误。

复制代码 代码如下:

var saveRss = function(data, callback) {
  fs.writeFile('rss.txt', data, 'utf8', function(err) {
    callback(err);
  });
}

整合:

复制代码 代码如下:

getRssAddress('address.txt', function(err, data) {
  if(err) {
    console.log(err);
    return;
  }
  getRss(data, function(err, data) {
    if(err) {
      console.log(err);
      return;
    }
    saveRss(data, function(err) {
      if(err) console.log(err);
    });
  });
});

上面的代码是全异步处理,使用最常见的callback处理异步逻辑的返回,好处是标准写法,大家都能容易接受;坏处是耦合性太强,处理异常麻烦,代码不直观,特别是处理业务逻辑复杂和处理任务多的场景,层层的callback会让人眼冒金星,代码难以维护。

Promise/A规范的实现之一when.js正是针对这样的问题域。

让我们来看一下改造后的代码。

任务1:

复制代码 代码如下:

var getRssAddress = function(path) {
    var deferred = when.defer();
      fs.readFile(path, {encoding: 'utf8'}, function (err, data) {
        if (err) deferred.reject(err);
        deferred.resolve(data);
      });

    return deferred.promise;
}

 
任务2:

复制代码 代码如下:

var getRss = function(url) {
  var deferred = when.defer();
    var data = '';
    http.get(url, function(res) {
      res.on('data', function(chrunk) {
        data += chrunk;
      });
      res.on('end', function() {
        deferred.resolve(data);
      });
    }).on('error', function(err) {
      deferred.reject(err);
    });

    return deferred.promise;
}

任务3:

复制代码 代码如下:

var saveRss = function(data) {
  var deferred = when.defer();
  fs.writeFile('rss.txt', data, 'utf8', function(err) {
    if(err) deferred.reject(err);
    deferred.resolve();
  });

  return deferred.promise;
}

 

整合:

复制代码 代码如下:

getRssAddress('address.txt')
  .then(getRss)
  .then(saveRss)
  .catch(function(err) {
    console.log(err);
  });

解释:

promise/A规范定义的“Deferred/Promise”模型就是“发布/订阅者”模型,通过Deferred对象发布事件,可以是完成resolve事件,或者是失败reject事件;通过Promise对象进行对应完成或失败的订阅。

在Promises/A规范中,每个任务都有三种状态:默认(pending)、完成(fulfilled)、失败(rejected)。

1.默认状态可以单向转移到完成状态,这个过程叫resolve,对应的方法是deferred.resolve(promiseOrValue);

2.默认状态还可以单向转移到失败状态,这个过程叫reject,对应的方法是deferred.reject(reason);

3.默认状态时,还可以通过deferred.notify(update)来宣告任务执行信息,如执行进度;

4.状态的转移是一次性的,一旦任务由初始的pending转为其他状态,就会进入到下一个任务的执行过程中。

按照上面的代码。

通过when.defer定义一个deferred对象。

var deferred = when.defer();
异步数据获取成功后,发布一个完成事件。

deferred.resolve(data);
异步数据获取失败后,发布一个失败事件。

deferred.reject(err);
并且返回Promise对象作为订阅使用。

return deferred.promise;
订阅是通过Promise对象的then方法进行完成/失败/通知的订阅。

getRssAddress('address.txt')
  .then(getRss)
then有三个参数,分别是onFulfilled、onRejected、onProgress

promise.then(onFulfilled, onRejected, onProgress)
上一个任务被resolve(data),onFulfilled函数就会被触发,data作为它的参数.

上一个任务被reject(reason),那么onRejected就会被触发,收到reason。

任何时候,onFulfilled和onRejected都只有其一可以被触发,并且只触发一次。

对于处理异常,when.js也提供了极其方便的方法,then能传递错误,多个任务串行执行时,我们可以只在最后一个then定义onRejected。也可以在最后一个then的后面调用catch函数捕获任何一个任务的异常。

如此写法简单明了。

复制代码 代码如下:

getRssAddress('address.txt')
  .then(getRss)
  .then(saveRss)
  .catch(function(err) {
    console.log(err);
  });

Promise给异步编程带来了巨大的方便,可以让我们专注于单个任务的实现而不会陷入金字塔厄运,以上代码仅仅是基本使用,when.js提供的功能远远不止本文提到的这些,具体参照官方API。

的异步编程实践,node.jswhen.js 假设一个业务场景: 通过rss地址,获取rss并保存于文件,rss地址保存于文件中。 完成该场景的...

姓名:岳沁

前言

近日,Mac 下著名软件 Homebrew 的作者,因为没解出来二叉树翻转的白板算法题,惨遭 Google 拒绝,继而引发推特热议。

在 JavaScript 中也有很多树形结构。比如 DOM 树,省市区地址联动,文件目录等; JSON 本身就是树形结构。

很多前端面试题也跟树形结构的有关,比如在浏览器端写遍历 DOM 树的函数,比如在 nodejs 运行时遍历文件目录等。

这里演示用 JavaScript 遍历树形结构的几种策略。

这段时间空余时间蛮少,后来特地腾出晚上的时间来开发自己的玩具。 今天讲的是为玩具所开发的一个小模块的一个功能。 具体来说它是一个仿Tree命令能够罗列给定目录的树形结构。而且我的做法是开发一个命令行工具,但这里先不提,我们就专注这个目录列表功能。 需要输出相同的结构。我们先来看下需要达到的效果

学号:17101223458

场景1:遍历 DOM 树

betway 1

转载自:

方案1:递归模式

JavaScript

function walkDom(node, callback) { if (node === null) { //判断node是否为null return } callback(node) //将node自身传入callback node = node.firstElementChild //改变node为其子元素节点 while (node) { walkDom(node, callback) //如果存在子元素,则递归调用walkDom node = node.nextElementSibling //从头到尾遍历元素节点 } } walkDom(document, function(node) {console.count()}) //包含document节点 document.querySelectorAll('*').length //数量比上面输出的少1,因为不包含document节点

1
2
3
4
5
6
7
8
9
10
11
12
13
function walkDom(node, callback) {
    if (node === null) { //判断node是否为null
        return
    }
    callback(node) //将node自身传入callback
    node = node.firstElementChild //改变node为其子元素节点
    while (node) {
        walkDom(node, callback) //如果存在子元素,则递归调用walkDom
        node = node.nextElementSibling //从头到尾遍历元素节点
    }
}
walkDom(document, function(node) {console.count()}) //包含document节点
document.querySelectorAll('*').length //数量比上面输出的少1,因为不包含document节点

将上述代码黏贴到任意页面的控制台 console 中执行。

正文

【嵌牛导读】:ES6 原生提供了 Promise 对象。

方案2:循环模式

JavaScript

function walkDom(node, callback) { if (node === null) { return } var stack = [node] //存入数组 var target while(stack.length) { //数组长度不为0,继续循环 target = stack.shift() //取出元素 callback(target) //传入callback Array.prototype.push.apply(stack, target.children) //将其子元素一股脑推入stack,增加长度 } } walkDom(document, function(node) {console.count()}) //包含document节点 document.querySelectorAll('*').length //数量比上面输出的少1,因为不包含document节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function walkDom(node, callback) {
    if (node === null) {
        return
    }
    var stack = [node] //存入数组
    var target
    while(stack.length) { //数组长度不为0,继续循环
        target = stack.shift() //取出元素
        callback(target) //传入callback
        Array.prototype.push.apply(stack, target.children) //将其子元素一股脑推入stack,增加长度
    }
}
walkDom(document, function(node) {console.count()}) //包含document节点
document.querySelectorAll('*').length //数量比上面输出的少1,因为不包含document节点

在循环模式中,shift方法可以换成pop,从尾部取出元素;push方法可以换成unshift从头部添加元素。不同的顺序,影响了是「广度优先」还是「深度优先」。

我们先来探讨一下如何获得目录结构。因为我们最终需要的是给路径->获得目录结构->数据处理(输出/另外操作)

所谓 Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理。

场景2:在 nodejs 运行时里遍历文件目录

在Node里有这么几个API

【嵌牛鼻子】:Promise

子场景1:同步模式

fs.readdir(path, callback)

【嵌牛提问】:如何提高Promise效率?

方案1:递归

JavaScript

var fs = require('fs') var Path = require('path') function readdirs(path) { var result = { //构造文件夹数据 path: path, name: Path.basename(path), type: 'directory' } var files = fs.readdirSync(path) //拿到文件目录下的所有文件名 result.children = files.map(function(file) { var subPath = Path.resolve(path, file) //拼接为绝对路径 var stats = fs.statSync(subPath) //拿到文件信息对象 if (stats.isDirectory()) { //判断是否为文件夹类型 return readdirs(subPath) //递归读取文件夹 } return { //构造文件数据 path: subPath, name: file, type: 'file' } }) return result //返回数据 } var cwd = process.cwd() var tree = readdirs(cwd) fs.writeFileSync(Path.join(cwd, 'tree.json'), JSON.stringify(tree)) //保存在tree.json中,去查看吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var fs = require('fs')
var Path = require('path')
 
function readdirs(path) {
    var result = { //构造文件夹数据
        path: path,
        name: Path.basename(path),
        type: 'directory'
    }
    var files = fs.readdirSync(path) //拿到文件目录下的所有文件名
    result.children = files.map(function(file) {
        var subPath = Path.resolve(path, file) //拼接为绝对路径
        var stats = fs.statSync(subPath) //拿到文件信息对象
        if (stats.isDirectory()) { //判断是否为文件夹类型
            return readdirs(subPath) //递归读取文件夹
        }
        return { //构造文件数据
            path: subPath,
            name: file,
            type: 'file'
        }
    })
    return result //返回数据
}
 
var cwd = process.cwd()
var tree = readdirs(cwd)
fs.writeFileSync(Path.join(cwd, 'tree.json'), JSON.stringify(tree)) //保存在tree.json中,去查看吧

将上面的代码保存在 tree.js 中,然后在当前文件夹打开命令行,输入node tree.js,目录信息保存在生成tree.json文件中。

该方法是 readdir(3) 的异步执行版本,用于读取一个目录的内容。callback 接收两个参数 (err, files),其中 files 是一个数组,数组成员为当前目录下的文件名,不包含 . 和 ..。

【嵌牛正文】:

方案2:循环

JavaScript

var fs = require('fs') var Path = require('path') function readdirs(path) { var result = { //构造文件夹数据 path: path, name: Path.basename(path), type: 'directory' } var stack = [result] //生成一个栈数组 while (stack.length) { //如果数组不为空,读取children var target = stack.pop() //取出文件夹对象 var files = fs.readdirSync(target.path) //拿到文件名数组 target.children = files.map(function(file) { var subPath = Path.resolve(target.path, file) //转化为绝对路径 var stats = fs.statSync(subPath) //拿到文件信息对象 var model = { //构造文件数据结构 path: subPath, name: file, type: stats.isDirectory() ? 'directory' : 'file' } if (model.type === 'directory') { stack.push(model) //如果是文件夹,推入栈 } return model //返回数据模型 }) } return result //返回整个数据结果 } var cwd = process.cwd() var tree = readdirs(cwd) fs.writeFileSync(Path.join(cwd, 'tree.json'), JSON.stringify(tree)) //保存在tree.json中,去查看吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var fs = require('fs')
var Path = require('path')
 
function readdirs(path) {
    var result = { //构造文件夹数据
        path: path,
        name: Path.basename(path),
        type: 'directory'
    }
    var stack = [result] //生成一个栈数组
    while (stack.length) { //如果数组不为空,读取children
        var target = stack.pop() //取出文件夹对象
        var files = fs.readdirSync(target.path) //拿到文件名数组
        target.children = files.map(function(file) {
            var subPath = Path.resolve(target.path, file) //转化为绝对路径
            var stats = fs.statSync(subPath) //拿到文件信息对象
            var model = { //构造文件数据结构
                path: subPath,
                name: file,
                type: stats.isDirectory() ? 'directory' : 'file'
            }
            if (model.type === 'directory') {
                stack.push(model) //如果是文件夹,推入栈
            }
            return model //返回数据模型
        })
    }
    return result //返回整个数据结果
}
 
var cwd = process.cwd()
var tree = readdirs(cwd)
fs.writeFileSync(Path.join(cwd, 'tree.json'), JSON.stringify(tree)) //保存在tree.json中,去查看吧

循环策略中的pop跟shift,push跟unshift也可以互换以调整优先级,甚至用可以用splice方法更精细的控制stack数组。循环模式比递归模式更可控。

fs.readdirSync(path)

Promise in js

子场景2:异步模式

该方法是 readdir(3) 的同步执行版本,返回一个不包含 . 和 .. 的文件名数组。

回调函数真正的问题在于他剥夺了我们使用 return 和 throw 这些关键字的能力。而 Promise 很好地解决了这一切。

方案1:过程式 Promise

JavaScript

var fs = require('fs') var Path = require('path') //promise包装的fs.stat方法 var stat = function(path) { return new Promise(function(resolve, reject) { fs.stat(path, function(err, stats) { err ? reject(err) : resolve(stats) }) }) } //promise包装的fs.readdir方法 var readdir = function(path) { return new Promise(function(resolve, reject) { fs.readdir(path, function(err, files) { err ? reject(err) : resolve(files) }) }) } //promise包装的fs.writeFile var writeFile = function(path, data) { return new Promise(function(resolve, reject) { fs.writeFile(path, JSON.stringify(data || ''), function(err) { err ? reject(err) : resolve }) }) } function readdirs(path) { return readdir(path) //异步读取文件夹 .then(function(files) { //拿到文件名列表 var promiseList = files.map(function(file) { //遍历列表 var subPath = Path.resolve(path, file) //拼接为绝对路径 return stat(subPath) //异步读取文件信息 .then(function(stats) { //拿到文件信息 //是文件夹类型的,继续读取目录,否则返回数据 return stats.isDirectory() ? readdirs(subPath) : { path: subPath, name: file, type: 'file' } }) }) return Promise.all(promiseList) //等待所有promise完成 }) .then(function(children) { //拿到包含所有数据的children数组 return { //返回结果 path: path, name: Path.basename(path), type: 'directory', children: children } }) } var cwd = process.cwd() readdirs(cwd) .then(writeFile.bind(null, Path.join(cwd, 'tree.json'))) //保存在tree.json中,去查看吧 .catch(console.error.bind(console)) //出错了就输出错误日志查看原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
var fs = require('fs')
var Path = require('path')
//promise包装的fs.stat方法
var stat = function(path) {
    return new Promise(function(resolve, reject) {
        fs.stat(path, function(err, stats) {
            err ? reject(err) : resolve(stats)
        })
    })
}
//promise包装的fs.readdir方法
var readdir = function(path) {
    return new Promise(function(resolve, reject) {
        fs.readdir(path, function(err, files) {
            err ? reject(err) : resolve(files)
        })
    })
}
//promise包装的fs.writeFile
var writeFile = function(path, data) {
    return new Promise(function(resolve, reject) {
        fs.writeFile(path, JSON.stringify(data || ''), function(err) {
            err ? reject(err) : resolve
        })
    })
}
 
function readdirs(path) {
    return readdir(path) //异步读取文件夹
    .then(function(files) { //拿到文件名列表
        var promiseList = files.map(function(file) { //遍历列表
            var subPath = Path.resolve(path, file) //拼接为绝对路径
            return stat(subPath) //异步读取文件信息
            .then(function(stats) { //拿到文件信息
                //是文件夹类型的,继续读取目录,否则返回数据
                return stats.isDirectory() ?
                readdirs(subPath) : {
                    path: subPath,
                    name: file,
                    type: 'file'
                }
            })
        })
        return Promise.all(promiseList) //等待所有promise完成
    })
    .then(function(children) { //拿到包含所有数据的children数组
        return { //返回结果
            path: path,
            name: Path.basename(path),
            type: 'directory',
            children: children
        }
    })
}
 
var cwd = process.cwd()
 
readdirs(cwd)
.then(writeFile.bind(null, Path.join(cwd, 'tree.json'))) //保存在tree.json中,去查看吧
.catch(console.error.bind(console)) //出错了就输出错误日志查看原因

上面的函数都能工作,但都是一个个function的调用,显得太「过程式」;

能不能用面向对象的方式来写呢?

当然可以。

其实面向对象的写法,更清晰。

为了更加语义化,以及增显逼格。

我们用 ES6 的 class 来写这个树形结构类。

...这里就列两个,其他的请自行去官网查API0-0

2015 年 6 月,ECMAScript 6 的正式版终于发布了。

方案2:ES6-class + ES6-Promise

JavaScript

import fs from 'fs' import {join, resolve, isAbsolute, basename, extname, dirname, sep} from 'path' /** * 获取目录下的所有文件 * @param {string} path * @return {promise} resolve files || reject error */ let readdir = (path) => { return new Promise((resolve, reject) => { fs.readdir(path, (err, files) => { err ? reject(err) : resolve(files) }) }) } /** * 将data写入文件 * @param {string} path 路径 * @param {data} data * @return {promise} resolve path || reject error */ let writeFile = (path, data) => { return new Promise((resolve, reject) => { fs.writeFile(path, data, (err) => { err ? reject(err) : resolve(path) }) }) } /** * 获取文件属性 * @param {string} path * @return {promise} resolve stats || reject error */ let stat = (path) => { return new Promise((resolve, reject) => { fs.stat(path, (err, stats) => { err ? reject(err) : resolve(stats) }) }) } /** * 判断path是否存在 * @param {string} path 路径 * @return {promise} resolve exists */ let exists = (path) => { return new Promise((resolve) => fs.exists(path, resolve)) } //文档类 class Document { constructor(path) { this.path = path this.name = basename(path) } //存在性判断 exists() { return exists(this.path) } //异步获取文件信息 stat() { return stat(this.path) } //输出基本数据 json() { return JSON.stringify(this) } //将基本数据保存在path路径的文件中 saveTo(path) { if (isAbsolute(path)) { return writeFile(path, this.json()) } return writeFile(resolve(this.path, path), this.json()) } } //文件类,继承自文档类 class File extends Document { constructor(path) { super(path) //必须先调用超类构造方法 this.type = 'file' //type 为 file this.extname = extname(path) //新增扩展名 } //写入数据 write(data = '') { return writeFile(this.path, data) } //其他文件特有方法如 read unlink 等 } //文件夹类,继承自文档类 class Directory extends Document { constructor(path) { super(path) //必须先调用超类构造方法 this.type = 'directory' } //读取当前文件夹 readdir() { return readdir(this.path) //读取目录 .then((files) => { //拿到文件名列表 let promiseList = files.map((file) => { let subPath = resolve(this.path, file) //拼接为绝对路径 return stat(subPath) //获取文件信息 .then((stats) => { //根据文件信息,归类为Directory或File类型 return stats.isDirectory() ? new Directory(subPath) : new File(subPath) }) }) return Promise.all(promiseList) }) .then((children) => { //拿到children数组 this.children = children //保存children属性 return children //返回children }) } //深度读取文件目录 readdirs() { return this.readdir() //读取当前文件夹 .then((children) => { //拿到children let promiseList = [] children.map((child) => { if (child instanceof Directory) { //是文件夹实例,继续深度读取文件目录 promiseList.push(child.readdirs()) } }) return Promise.all(promiseList) //等待所有子元素深度读取目录完毕 }) .then(() => this) //返回this } //其他文件夹特有方法如 addFile removeFile addDir remveDir 等 } let cwd = process.cwd() new Directory(cwd) .readdirs() .then((tree) => { tree.saveTo('tree.json') //让它自己保存在tree.json里 }) .catch(console.error.bind(console)) //输出错误日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import fs from 'fs'
import {join, resolve, isAbsolute, basename, extname, dirname, sep} from 'path'
 
/**
* 获取目录下的所有文件
* @param {string} path
* @return {promise} resolve files || reject error
*/
let readdir = (path) => {
    return new Promise((resolve, reject) => {
        fs.readdir(path, (err, files) => {
            err ? reject(err) : resolve(files)
        })
    })
}
 
/**
* 将data写入文件
* @param {string} path 路径
* @param {data} data
* @return {promise} resolve path || reject error
*/
let writeFile = (path, data) => {
    return new Promise((resolve, reject) => {
        fs.writeFile(path, data, (err) => {
            err ? reject(err) : resolve(path)
        })
    })
}
 
/**
* 获取文件属性
* @param {string} path
* @return {promise} resolve stats || reject error
*/
let stat = (path) => {
    return new Promise((resolve, reject) => {
        fs.stat(path, (err, stats) => {
            err ? reject(err) : resolve(stats)
        })
    })
}
 
/**
* 判断path是否存在
* @param {string} path 路径
* @return {promise} resolve exists
*/
let exists = (path) => {
    return new Promise((resolve) => fs.exists(path, resolve))
}
 
//文档类
class Document {
    constructor(path) {
        this.path = path
        this.name = basename(path)
    }
    //存在性判断
    exists() {
        return exists(this.path)
    }
    //异步获取文件信息
    stat() {
        return stat(this.path)
    }
    //输出基本数据
    json() {
        return JSON.stringify(this)
    }
    //将基本数据保存在path路径的文件中
    saveTo(path) {
        if (isAbsolute(path)) {
            return writeFile(path, this.json())
        }
        return writeFile(resolve(this.path, path), this.json())
    }
}
 
//文件类,继承自文档类
class File extends Document {
    constructor(path) {
        super(path) //必须先调用超类构造方法
        this.type = 'file' //type 为 file
        this.extname = extname(path) //新增扩展名
    }
    //写入数据
    write(data = '') {
        return writeFile(this.path, data)
    }
    //其他文件特有方法如 read unlink 等
}
 
//文件夹类,继承自文档类
class Directory extends Document {
    constructor(path) {
        super(path) //必须先调用超类构造方法
        this.type = 'directory'
    }
    //读取当前文件夹
    readdir() {
        return readdir(this.path) //读取目录
        .then((files) => { //拿到文件名列表
            let promiseList = files.map((file) => {
                let subPath = resolve(this.path, file) //拼接为绝对路径
                return stat(subPath) //获取文件信息
                .then((stats) => {
                    //根据文件信息,归类为Directory或File类型
                    return stats.isDirectory() ?
                    new Directory(subPath) :
                    new File(subPath)
                })
            })
            return Promise.all(promiseList)
        })
        .then((children) => { //拿到children数组
            this.children = children //保存children属性
            return children //返回children
        })
    }
    //深度读取文件目录
    readdirs() {
        return this.readdir() //读取当前文件夹
        .then((children) => { //拿到children
            let promiseList = []
            children.map((child) => {
                if (child instanceof Directory) { //是文件夹实例,继续深度读取文件目录
                    promiseList.push(child.readdirs())
                }
            })
            return Promise.all(promiseList) //等待所有子元素深度读取目录完毕
        })
        .then(() => this) //返回this
    }
    //其他文件夹特有方法如 addFile removeFile addDir remveDir 等
}
 
let cwd = process.cwd()
 
new Directory(cwd)
.readdirs()
.then((tree) => {
    tree.saveTo('tree.json') //让它自己保存在tree.json里
})
.catch(console.error.bind(console)) //输出错误日志

因为当前 JavaScript 引擎对 ES6 的支持度还不够,所以上述代码不能直接运行。可以通过以下两种方式来验证代码能不能跑起来。

第一种,先 npm install -g bable 全局安装 babel 工具,再以 babel-node tree.js的方式取代 node tree.js 来运行上述代码。

第二种,将上述代码黏贴到  ES5 的代码,将代码保存在 tree.js文件中,以 ES5 的形式执行。

一开始我试图用异步方法来获取,但是最后因为没处理好Promise和其它异步操作,导致最后数据没有存储下来。因此现在我们讲的是用同步方式获取。异步方式也将在这个模块写完之后再去写一版本。

ECMAScript 是 JavaScript 语言的国际标准,JavaScript 是 ECMAScript 的实现。ES6 的目标,是使得 JavaScript 语言可以用来编写大型的复杂的应用程序,成为企业级开发语言。

结语

以上就是我知道的一些用 JavaScript 处理树形结构的几种方法,希望看过后对你有帮助。

1 赞 4 收藏 评论

betway 2

基本上网上的方法都是递归调用方法来获取整个目录结构。

概念

这里也不例外。不过我们需要注意我们需要的将整个目录关系都缓存在一个Object里。而且因为我需要指定层级目录输出。因此每个文件/目录还将有一个属性就是deep,它作为一个告诉调用者该目录层级的深度。如果是0,说明是指定目录的根目录下同级的文件/目录。依次类推。

ES6 原生提供了 Promise 对象。

当然这里就直接贴出代码

所谓 Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理。

_getAllNames: function(level, dir) {

Promise 对象有以下两个特点。

        var filesNameArr = []

(1)对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和 Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。

        let cur = 0

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

            // 用个hash队列保存每个目录的深度

有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。

        var mapDeep = {}

Promise 也有一些缺点。首先,无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

        mapDeep[dir] = 0

var promise = new Promise(function(resolve, reject) {

            // 先遍历一遍给其建立深度索引

if (/* 异步操作成功 */){

        function getMap(dir, curIndex) {

resolve(value);

            var files = fs.readdirSync(dir) //同步拿到文件目录下的所有文件名

} else {

            files.map(function(file) {

reject(error);

                //var subPath = path.resolve(dir, file) //拼接为绝对路径

}

                var subPath = path.join(dir, file) //拼接为相对路径

});

                var stats = fs.statSync(subPath) //拿到文件信息对象

promise.then(function(value) {

                    // 必须过滤掉node_modules文件夹

// success

                if (file != 'node_modules') {

}, function(value) {

                    mapDeep[file] = curIndex + 1

// failure

                    if (stats.isDirectory()) { //判断是否为文件夹类型

});

                        return getMap(subPath, mapDeep[file]) //递归读取文件夹

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 方法和 reject 方法。

                    }

如果异步操作成功,则用 resolve 方法将 Promise 对象的状态,从「未完成」变为「成功」(即从 pending 变为 resolved);

                }

如果异步操作失败,则用 reject 方法将 Promise 对象的状态,从「未完成」变为「失败」(即从 pending 变为 rejected)。

                //console.log(subPath)

基本的 api

            })

Promise.resolve()

        }

Promise.reject()

        getMap(dir, mapDeep[dir])

Promise.prototype.then()

            //console.log(mapDeep)

Promise.prototype.catch()

        function readdirs(dir, folderName,myroot) {

Promise.all()    // 所有的完成

            var result = { //构造文件夹数据

var p = Promise.all([p1,p2,p3]);

                path: dir,

Promise.race()      // 竞速,完成一个即可

                name: path.basename(path),

进阶

                type: 'directory',

promises 的奇妙在于给予我们以前的 return 与 throw,每个 Promise 都会提供一个 then() 函数,和一个 catch(),实际上是 then(null, ...) 函数,

                deep: mapDeep[folderName]

somePromise().then(functoin(){

            }

// do something

            var files = fs.readdirSync(dir) //同步拿到文件目录下的所有文件名

});

            result.children = files.map(function(file) {

我们可以做三件事,

                //var subPath = path.resolve(dir, file) //拼接为绝对路径

  1. return 另一个 promise

  2. return 一个同步的值 (或者 undefined)

  3. throw 一个同步异常throw new Eror('');

  4. 封装同步与异步代码

                var subPath = path.join(dir, file) //拼接为相对路径

```

                var stats = fs.statSync(subPath) //拿到文件信息对象

new Promise(function (resolve, reject) {

                    //console.log(subPath)

resolve(someValue);

                if (stats.isDirectory()) { //判断是否为文件夹类型

});

                    // console.log(mapDeep[betway,file])

```

                    return readdirs(subPath, file,file) //递归读取文件夹

写成

                }

```

                return { //构造文件数据

Promise.resolve(someValue);

                    path: subPath,

```

                    name: file,

  1. 捕获同步异常

                    type: 'file'

new Promise(function (resolve, reject) {

                }

throw new Error('悲剧了,又出 bug 了');

            })

}).catch(function(err){

            return result //返回数据

console.log(err);

        }

});

        filesNameArr.push(readdirs(dir, dir))

如果是同步代码,可以写成

        return filesNameArr

Promise.reject(new Error("什么鬼"));

    },

  1. 多个异常捕获,更加精准的捕获

过滤掉node_modules是必须的,因为开发中这个目录一直存在。。。是个极大干扰源。

somePromise.then(function() {

这里我代码都加了注释应该很好理解。

return a.b.c.d();

我们来输出下缓存的目录结构

}).catch(TypeError, function(e) {

{ path: './',

//If a is defined, will end up here because

  name: '[object Object]',

//it is a type error to reference property of undefined

  type: 'directory',

}).catch(ReferenceError, function(e) {

  deep: 0,

//Will end up here if a wasn't defined at all

  children:

}).catch(function(e) {

  [ { path: '.DS_Store', name: '.DS_Store', type: 'file' },

//Generic catch-the rest, error wasn't TypeError nor

    { path: '.babelrc', name: '.babelrc', type: 'file' },

//ReferenceError

    { path: '.gitignore', name: '.gitignore', type: 'file' },

});

    { path: 'README.MD', name: 'README.MD', type: 'file' },

  1. 获取两个 Promise 的返回值

  2. .then 方式顺序调用

  3. 设定更高层的作用域

  4. spread

  5. finally

    { path: 'bin',

任何情况下都会执行的,一般写在 catch 之后

      name: '[object Object]',

  1. bind

      type: 'directory',

somethingAsync().bind({})

      deep: 1,

.spread(function (aValue, bValue) {

      children: [Object] },

this.aValue = aValue;

    { path: 'lib',

this.bValue = bValue;

      name: '[object Object]',

return somethingElseAsync(aValue, bValue);

      type: 'directory',

})

      deep: 1,

.then(function (cValue) {

      children: [Object] },

return this.aValue + this.bValue + cValue;

    { path: 'package.json', name: 'package.json', type: 'file' },

});

    { path: 'test',

或者 你也可以这样

      name: '[object Object]',

var scope = {};

      type: 'directory',

somethingAsync()

      deep: 1,

.spread(function (aValue, bValue) {

      children: [Object] } ] }

scope.aValue = aValue;

[ { path: './',

scope.bValue = bValue;

    name: '[object Object]',

return somethingElseAsync(aValue, bValue);

    type: 'directory',

})

    deep: 0,

.then(function (cValue) {

    children:

return scope.aValue + scope.bValue + cValue;

    [ [Object],

});

      [Object],

然而,这有非常多的区别,

      [Object],

你必须先声明,有浪费资源和内存泄露的风险

      [Object],

不能用于放在一个表达式的上下文中

      [Object],

效率更低

      [Object],

  1. all。非常用于于处理一个动态大小均匀的 Promise 列表

  2. join。非常适用于处理多个分离的 Promise

      [Object],

```

      [Object] ] } ]

var join = Promise.join;

针对./这个当前目录的路径,程序返回上面的结构。 这样我们就得到了第一步最基础的数据。

join(getPictures(), getComments(), getTweets(),

现在第一个步骤是为了模仿Tree工具输出命令。

function(pictures, comments, tweets) {

这里给个例子参考:

console.log("in total: " + pictures.length + comments.length + tweets.length);

├── .DS_Store

});

├── .babelrc

```

├── .gitignore

  1. props。处理一个 promise 的 map 集合。只有有一个失败,所有的执行都结束

├── README.MD

```

├── bin

Promise.props({

│   ├── .DS_Store

pictures: getPictures(),

│   ├── folderTree.js

comments: getComments(),

│   ├── lib2

tweets: getTweets()

│   │   ├── .DS_Store

}).then(function(result) {

│   │   └── testa

console.log(result.tweets, result.pictures, result.comments);

│   └── testb

});

│      └── .DS_Store

```

├── lib

  1. any 、some、race

│   ├── .DS_Store

```

│   ├── folderFactory.js

Promise.some([

│   └── testlib

ping("ns1.example.com"),

│      └── testlibfile.js

ping("ns2.example.com"),

├── package.json

ping("ns3.example.com"),

└── test

ping("ns4.example.com")

   ├── .DS_Store

], 2).spread(function(first, second) {

   ├── index.js

console.log(first, second);

    └── testFolder

}).catch(AggregateError, function(err) {

       ├── .DS_Store

err.forEach(function(e) {

       ├── a

console.error(e.stack);

       ├── b

});

       └── c

});;

它有几个注意点,一个是每当你的文件或者目录是当前层级最后一个,那么输出└──前缀如果是普通的输出├── 前缀。 如果是多层级,那么最前面依旧需要│ 来进行上下层级的联系。

```

而且还有很多小细节需要处理。

有可能,失败的 promise 比较多,导致,Promsie 永远不会 fulfilled

一开始最好的解决方法其实是递归。但是我觉得有点复杂,我想试下迭代方法。最后结果如开头的效果。基本上如果根目录中最后一个file是目录类型。那么它是正常的。但是如果根目录中最后一个文件是文件类型类型。我这段迭代并没有进行处理。

  1. .map(Function mapper [, Object options])

我们先来看下这个有缺陷的代码:

用于处理一个数组,或者 promise 数组,

    var stack = [obj[0]]

Option: concurrency 并发现

        var isFinal = false

map(..., {concurrency: 1});

        function printFolder(arr, folderName, isLastFolder) {

以下为不限制并发数量,读书文件信息

            if (arr.deep <= level) {

var Promise = require("bluebird");

                for (var i = 0; i < arr.children.length; i++) {

var join = Promise.join;

                    var isLastFile = i == arr.children.length - 1 ? true : false

var fs = Promise.promisifyAll(require("fs"));

                    var isRootLast = i == arr.children.length - 1

var concurrency = parseFloat(process.argv[2] || "Infinity");

                    isFinal = isFinal == true ? true :  arr.deep == 0 && arr.children[i].type == 'directory' && i == arr.children.length-1

var fileNames = ["file1.json", "file2.json"];

                    printFile(arr.children[i], folderName, arr.deep, isLastFile, isLastFolder,isFinal)

Promise.map(fileNames, function(fileName) {

                    if (arr.children[i].type == 'directory') {

return fs.readFileAsync(fileName)

                        //console.log('directory')

.then(JSON.parse)

                        var t = arr.children[i].path

.catch(SyntaxError, function(e) {

                        if (i == arr.children.length - 1) {

e.fileName = fileName;

                            printFolder(arr.children[i], t, true)

throw e;

                        } else {

})

                            printFolder(arr.children[i], t, false)

}, {concurrency: concurrency}).then(function(parsedJSONs) {

                        }

console.log(parsedJSONs);

                    }

}).catch(SyntaxError, function(e) {

                }

console.log("Invalid JSON in file " + e.fileName + ": " + e.message);

            }

});

        }

结果

        function printFile(file, folderName, deep, isLastFile, isLastFolder) {

$ sync && echo 3 > /proc/sys/vm/drop_caches

            if (file[0] != '.') {

$ node test.js 1

                // console.log("Folder:"+folderName)

reading files 35ms

                // console.log(deep)

$ sync && echo 3 > /proc/sys/vm/drop_caches

                //console.log(isLastFile)

$ node test.js Infinity

                //console.log(isLastFolder)

reading files: 9ms

                var name = file.path.replace(folderName + '/', '')

  1. .reduce(Function reducer [, dynamic initialValue]) -> Promise

                //console.log(isFinal)

Promise.reduce(["file1.txt", "file2.txt", "file3.txt"], function(total, fileName) {

                if (deep == 0) {

return fs.readFileAsync(fileName, "utf8").then(function(contents) {

                    if (isLastFile) {

return total + parseInt(contents, 10);

                        console.log('└── ' + name)

});

                    } else {

}, 0).then(function(total) {

                        console.log('├── ' + name)

//Total is 30

                    }

});

                }

  1. Time

                if (deep > 0) {

.delay(int ms) -> Promise

                    if (!isLastFolder) {

.timeout(int ms [, String message]) -> Promise

                        if (!isLastFile) {

Promise 的实现

                            console.log('│   '.repeat(deep) + '├── ' + name)

q

                        } else {

bluebird

                            console.log('│   '.repeat(deep) + '└── ' + name)

co

                        }

when

                    } else {

ASYNC

                        if (!isLastFile) {

async 函数与 Promise、Generator 函数一样,是用来取代回调函数、解决异步操作的一种方法。它本质上是 Generator 函数的语法糖。async 函数并不属于 ES6,而是被列入了 ES7。

                            console.log('    '.repeat(deep) + '├── ' + name)

                        } else {

                        if(isFinal){

                            console.log('    '.repeat(deep) + '└── ' + name)

                        }else{

                          var str = '    '.repeat(deep) + '└── ' + name

                          var temp = str.split('')

                          temp[0] = '│'

                            console.log(temp.join(''))

                        }

                        }

                    }

                }

            }

        }

        printFolder(obj[0], '', false)

        console.log('目录及文件罗列完毕')

我做了很多判断,但还是低估了一个目录它可能的复杂性(目录中有空目录,目录中有空文件,文件和目录的多个嵌套等等)。因为我开发的时候是根据几个example来进行调节判断。当我最后写完,重新再去测试几个目录的时候发现出现上述的file缺陷。 如图: 

betway 3

因此需要改写为递归方式。我们不可能根据别的属性来进行判断。因此我们需要依赖的还是之前的那份目录结构缓存。

这里也直接贴源码:

_showList(obj, level) {

        var sourceStruct = obj[0]

        var dirCount = 0

        var fileCount = 0

            // 字符集

        var charSet = {

            'node': '├── ', //节点

            'pipe': '│  ', // 上下链接

            'last': '└── ', // 最后的file或folder需要回勾

            'indent': '    ' // 缩进

        };

        function log(file, depth, parentHasNextSibling) {

          // console.log("log:")

            if (!parentHasNextSibling && depth.length > 1) {

                // Replace a pipe with an indent if the parent does not have a next sibling.

                depth[depth.length - 2] = charSet.indent;

            }

            if(file.lastIndexOf('/') > -1){

                file = file.substring(file.lastIndexOf('/')+1)

            }

            console.log(depth.join('') + file);

        }

        // 由于已经有缓存数据了,因此对数据进行遍历搜索

        // 如果type 是file 就不需要继续

        // 如果type 是directory 对临时数组

        function walk(path, depth, parentHasNextSibling) {

          //  console.log(path)

            var childrenLen = path.length - 1

          // console.log(childrenLen)

            var loop = true

            if (depth.length >= level) {

                loop = false

            }

            if (loop) {

                path.forEach(function walkChildren(child, index) {

                    var newdepth = !!depth ? depth.slice(0) : []

                    var isLast = (index >= childrenLen)

                    if (isLast) {

                        newdepth.push(charSet.last)

                    } else {

                        newdepth.push(charSet.node)

                    }

                    if(child.type == "file"){

                      log(child.name, newdepth, parentHasNextSibling)

                    }else{

                      log(child.path, newdepth, parentHasNextSibling)

                    }

                    if (child.type == "directory") {

                        var childPath = child.children

                        if (!isLast) {

                            newdepth.pop()

                            newdepth.push(charSet.pipe)

                        }

                        walk(childPath, newdepth, !isLast)

                    }

                })

                loop = !loop

            }

        }

        walk(sourceStruct.children, [])

        //console.log(sourceStruct)

        console.log('level:' + level)

        console.log('目录及文件罗列完毕')

    },

这里只需要判断文件类型,利用递归的特性将每个文件的路径存于newpath,然后判断长度,如果大于level就跳过。具体注释都在源码里了。

betway 4

结尾

第一个模块总算完成,四月第一篇文章居然在中旬也算对得起拖延症了~

本文由必威发布于Web前端,转载请注明出处:中的神器,的异步编程实践