寫於 2016.07.17
專案地址:連結描述 專案簡介:juejin.im/post/5c125a…
關於專案的用法和介紹可以檢視上面的兩個連結,這篇文章主要內容是對filemap.js
的程式碼進行一步一步的分析,詳細介紹其執行原理和優化策略。
知識點準備:
NodeJS
的基本使用方法(主要是fs
檔案系統);ES6
特性及語法(let
,const
,for...of
,arrow function
...)n叉樹先序遍歷演算法
。
知識點1和2請自行查閱資料,現在對知識點3進行分析。
N叉樹先序遍歷演算法
首先明白什麼是樹。引用資料結構與演算法JavaScript描述:
一個樹結構包含一系列存在父子關係的節點。每個節點都有一個父節點(除了頂部的第一個節點)以及零個或多個子節點:
位於樹頂部的節點叫作根節點(11)。它沒有父節點。樹中的每個元素都叫作節點,節點分為內部節點和外部節點。至少有一個子節點的節點稱為內部節點(7、5、9、15、13和20是內部節點)。沒有子元素的節點稱為外部節點或葉節點(3、6、8、10、12、14、18和25是葉節點)。 一個節點可以有祖先和後代。一個節點(除了根節點)的祖先包括父節點、祖父節點、曾祖父節點等。一個節點的後代包括子節點、孫子節點、曾孫節點等。例如,節點5的祖先有節點7和節點11,後代有節點3和節點6。 有關樹的另一個術語是子樹。子樹由節點和它的後代構成。例如,節點13、12和14構成了上圖中樹的一棵子樹。 節點的一個屬性是深度,節點的深度取決於它的祖先節點的數量。比如,節點3有3個祖先節點(5、7和11),它的深度為3。 樹的高度取決於所有節點深度的最大值。一棵樹也可以被分解成層級。根節點在第0層,它的子節點在第1層,以此類推。上圖中的樹的高度為3(最大高度已在圖中表示——第3層)。
對於一棵樹的遍歷,有先序
,中序
和後序
三種遍歷方式,在本例中使用的是先序遍歷
的方式。至於三種遍歷方式的異同,請閱讀資料結構與演算法JavaScript描述,裡面有詳細的介紹。
首先我們建立一棵樹:
let treeObj = {
'1': [
{ '2': [{ '5': [{ '11': '11' }, { '12': '12' }, { '13': '13' }, { '14': '14' }] }] },
{ '3': [{ '6': '6' }, { '7': '7' }] },
{ '4': [{ '8': '8' }, { '9': '9' }, { '10': '10' }] }
]
}
複製程式碼
為了簡單方便,我把它的key和value都設定成了相同的值。在例子中我們使用的都是key值。
然後分析先序遍歷
的原理:
先序遍歷
可以得到整棵樹的結構,這正是我們所需要的。接下來看程式碼如何實現。先看完整程式碼:
let traverseNode = (node, deep) => {
if (typeof node !== 'string') {
let key = Object.keys(node)
console.log(key, deep)
for (let i = 0; i < node[key].length; i++) {
traverseNode(node[key][i], deep + 1)
}
}
}
traverseNode(treeObj, 1)
複製程式碼
我們建立了一個traverseNode()
函式,它接收兩個物件作為引數。node
引數為傳入的節點,deep
引數為節點的起始深度。
首先使用Object.keys(obj)
方法取得節點的key值,同時輸出深度值:
let key = Object.keys(node)
console.log(key, deep)
複製程式碼
執行,在控制檯將會輸出[ '1' ] 1
。接下來我們使用遞迴來重複這個過程,進行完整的遍歷運算:
for (let i = 0; i < node[key].length; i++) {
traverseNode(node[key][i], deep + 1)
}
複製程式碼
這個遞迴就是我們前文一直在說的先序遍歷
。對於二叉樹
:
先序遍歷是以優先於後代節點的順序訪問每個節點的。先序遍歷的一種應用是列印一個結構化的文件。 先序遍歷會先訪問節點本身,然後再訪問它的左側子節點,最後是右側子節點。
在理解完上面這段話以後,不難把先序遍歷
的思路擴充套件到n叉樹
:先訪問節點本身,然後從左到右訪問它的n個子節點。
每一次完整的for迴圈都意味著“往下走一層”,所以只需要deep + 1
即可知道每一個節點對應的深度。
在本例子的遍歷過程中,node
都是一個個的物件而非字串。如果檢測到node
為字串,證明其已經到了最後一層,需要停止,否則會無限迴圈導致溢位,所以我們需要新增一個判斷:
if (typeof node !== 'string')
複製程式碼
大功告成,現在我們嘗試執行一下:
[ '1' ] 1
[ '2' ] 2
[ '5' ] 3
[ '11' ] 4
[ '12' ] 4
[ '13' ] 4
[ '14' ] 4
[ '3' ] 2
[ '6' ] 3
[ '7' ] 3
[ '4' ] 2
[ '8' ] 3
[ '9' ] 3
[ '10' ] 3
複製程式碼
完美。
filemap.js原理
filemap.js
通過遍歷一個資料夾內部的所有子檔案和子資料夾,輸出其目錄結構。我們使用fs
檔案系統來進行。
const fs = require('fs')
複製程式碼
然後來構造核心部分程式碼:
// 判斷型別。若該路徑對應的是資料夾則返回true,否則返回false
let isDic = (url) => fs.statSync(url).isDirectory()
const traverseFiles = (path, deep) => {
let files = fs.readdirSync(path)
for (let i = 0, len = files.length; i < len; i++) {
if (files[i] !== 'filemap.js') console.log(deep, files[i], '\n') // 忽略filemap.js本身
let dirPath = path + '\\' + files[i]
// 當且僅當是資料夾時才進行下一輪遍歷
if (isDic(dirPath)) traverseFiles(dirPath, deep + 1)
}
}
複製程式碼
檔案目錄結構其實就是一棵典型的n叉樹
,通過前文的例子,不難明白這段程式碼的原理。首先通過fs.readdirSync(path)
同步地獲取某路徑對應的所有檔案(夾),然後進行遞迴。可以把它理解為從第二層開始遍歷,所以在寫法上和前文例子稍有不同。
現在我們已經可以獲取檔案及其所在的深度了,接下來就是對這些資訊進行格式化,使其輸出更加直觀。為了輸出類似
|__folder
|__file1
|__file2
複製程式碼
這樣的樹狀結構,我們需要判斷不同的深度對應的縮排,所以我們來定義一個placeHolder()
函式:
const placeHolder = (num) => {
if (placeHolder.cache[num]) return placeHolder.cache[num] + '|__'
placeHolder.cache[num] = ''
for (let i = 0; i < num; i++) {
placeHolder.cache[num] += ' '
}
return placeHolder.cache[num] + '|__'
}
placeHolder.cache = {}
複製程式碼
這裡涉及到一個快取函式執行結果
的優化策略。由於該函式多次被使用,如果每一次都是從頭開始進行for迴圈,在效能上有著巨大的浪費。所以我們可以把它的執行結果快取起來,當以後遇到相同情況時只需要取出快取的結果即可,無需重新運算,大大提升了效能。
現在我們把核心程式碼改寫一下:
let isDic = (url) => fs.statSync(url).isDirectory()
const traverseFiles = (path, deep) => {
let files = fs.readdirSync(path)
for (let i = 0, len = files.length; i < len; i++) {
if (files[i] !== 'filemap.js') console.log(placeHolder(deep), files[i], '\n') // 忽略filemap.js本身
let dirPath = path + '\\' + files[i]
if (isDic(dirPath)) traverseFiles(dirPath, deep + 1)
}
}
traverseFiles('./', 1)
複製程式碼
在根目錄中執行node filemap.js
,我們就能夠得到完美的檔案目錄樹狀結構圖了。
功能進一步擴充套件
現在是“無差別”地對所有資料夾進行展開。如果想要忽略某些資料夾,比如.git
或者node_modules
之類的資料夾,應該如何做呢?參考命令列輸入引數的方法,這個需求不難實現。
首先獲取需要忽略的資料夾名:
let ignoreCase = {}
if(process.argv[2] === '-i'){
for (let i of process.argv.slice(3)) {
ignoreCase[i] = true
}
}
複製程式碼
ignoreCase
儲存著需要忽略的資料夾名。這裡使用物件而不是陣列的原因是,當判斷一個item
是否被已經被儲存的時候,item.indexOf(Array)
的效率並沒有Object[item]
來得高。使用for...of
迴圈能夠直接取得物件。
接下來我們可以在核心程式碼中多加一個判斷:
let isDic = (url) => fs.statSync(url).isDirectory()
const traverseFiles = (path, deep) => {
let files = fs.readdirSync(path)
let con = false
for (let i = 0, len = files.length; i < len; i++) {
if (files[i] !== 'filemap.js') console.log(placeHolder(deep), files[i], '\n')
con = ignoreCase[files[i]] === undefined? true: false
let dirPath = path + '\\' + files[i]
if (isDic(dirPath) && con) traverseFiles(dirPath, deep + 1)
}
}
複製程式碼
被忽略的資料夾將不會進行遞迴運算。 最後別忘了在退出程式:
process.exit()
複製程式碼
至此,完整的filemap.js
已經完成,其所有程式碼如下:
/**
* @author Jrain Lau
* @email jrainlau@163.com
* @date 2016-07-14
*/
'use strict'
const fs = require('fs')
let ignoreCase = {}
if(process.argv[2] === '-i'){
for (let i of process.argv.slice(3)) {
ignoreCase[i] = true
}
}
console.log('\n\nThe files tree is:\n=================\n\n')
const placeHolder = (num) => {
if (placeHolder.cache[num]) return placeHolder.cache[num] + '|__'
placeHolder.cache[num] = ''
for (let i = 0; i < num; i++) {
placeHolder.cache[num] += ' '
}
return placeHolder.cache[num] + '|__'
}
placeHolder.cache = {}
let isDic = (url) => fs.statSync(url).isDirectory()
const traverseFiles = (path, deep) => {
let files = fs.readdirSync(path)
let con = false
for (let i = 0, len = files.length; i < len; i++) {
if (files[i] !== 'filemap.js') console.log(placeHolder(deep), files[i], '\n')
con = ignoreCase[files[i]] === undefined? true: false
let dirPath = path + '\\' + files[i]
if (isDic(dirPath) && con) traverseFiles(dirPath, deep + 1)
}
}
traverseFiles('./', 1)
process.exit()
複製程式碼
使用時只需要帶上引數-i 資料夾1 資料夾2 ...
即可控制資料夾的展開與否。
後記
在學習資料結構與演算法JavaScript描述的過程中,有時候真的覺得特別困,後來發揮自己喜歡折騰的個性,想辦法把枯燥的東西進行實踐,不知不覺就會變得有趣了。在filemap.js
的早期版本中有著許多bug和效能問題,比如不合理使用三元表示式,沒有快取函式執行結果,判斷檔案型別考慮不周等等情況。文中所涉及到的優化策略,有很多是來自他人的指點和一次次的修改才最終得出來的,在此非常感謝給予我幫助的人。
最後感謝你的閱讀。我是Jrain,歡迎關注我的專欄,將不定期分享自己的學習體驗,開發心得,搬運牆外的乾貨。下次見啦!