文件系统模块
Node中最常见的内置模块 Filesystem(fs), 该模块提供了处理文件和目录的函数
- readFile: 读取文件,并将文件内容传递给函数
- writeFile: 将文件写到磁盘上
- readdir: 将目录中的文件以字符串数组的方式返回
- stat: 用于获取文件信息
- rename: 用于重命名文件
- unlink: 用于删除文件
// 异步版本
var fs = requier('fs')
fs.readFile('file.text', 'utf8', function(error, text) {
if(error) {
throw error
}
console.log('The file contained:', text)
})
//同步版本
fs.readFileSync('file.text', 'utf8')
异步回调函数中通常遵循异步优先, 即第一个参数接收可能错误的对象, 其余参数接收结果
使用同步函数较为节省代码,再简单的脚本中非常适用,但是异步函数会带来额外的速度提升,减少延迟
链接分类
硬链接(Hard Link)
硬链接是指通过索引节点来进行链接。在Linux的文件系统中,所有文件都会分配一个索引节点编号Inode,它是文件在系统中的唯一标识,文件的实际数据放置在数据区域(data block),INode存储着文件参数信息(元数据metadata),比如创建时间、修改时间、文件大小、属主、归属的用户组、读写权限、数据所在block号等,多个文件名可以指向同一索引节点(Inode)。
硬链接只能在同一文件系统(盘)中的文件之间进行链接,不能对目录进行创建。只要文件的索引节点还有一个以上的链接,其中一个链接更改文件内容,其余链接读取文件内容也发生改变,只删除其中一个链接并不影响索引节点本身和其他的链接(数据的实体并未删除),只有当最后一个链接被删除后,此时如果有新数据要存储到磁盘上,被删除的文件的数据块及目录的链接才会被释放,空间被新数据暂用覆盖。
软链接(Symbolic Link)
软链接(也叫符号链接),类似于windows系统中的快捷方式,与硬链接不同,软链接就是一个普通文件,只是数据块内容有点特殊,文件用户数据块中存放的内容是另一文件的路径名的指向,通过这个方式可以快速定位到软连接所指向的源文件实体。源文件删除,软链接也会失效,软链接可以跨文件系统对文件或目录创建。
拓 展
-
执行 readFile 函数 返回Promise
var fs = require('fs') function readFilePromise(...args) { return new Promise((resolve, reject) => { fs.readFile(...args, (err, data) => { if (err) { reject(err) } else { resolve(data) } }) }) } readFilePromise('a.js').then(data => { }).catch(err => { })
-
执行 writeFile 函数 返回Promise
var fs = require('fs') function writeFilePromise(...args) { return new Promise((resolve, reject) => { fs.writeFile(...args, (err) => { if (err) { resolve(err) } else { reject() } }) }) } writeFilePromise('a.js').then(data => { }).catch(err => { })
-
将一个基于回调的函数转为一个返回 Promise 的函数
function promisify(callbackBasedFunction) { return function(...args) { return new Promise((resolve, reject) => { callbackBasedFunction(...args,(err, data) => { if (err) { reject(err) } else { resolve(data) } }) }) } } readFilePromise = promisify(fs.readFile) writeFilePromise = promisify(fs.writeFile) statunlinkPromise = promisify(fs.stat) unlinkPromise = promisify(fs.unlink)
-
将一个基于 Promise 的函数转为一个返回回调的函数
function callbackify(promiseBased) { return function (...args) { var cb = args.pop() promiseBased(...args).then(val => { cb(null, val) }, reason => { cb(reason) }) } }
当然啦, 这两个函数在标准库中已经集成好了, 就是 utils
var fs = require('fs')
var utils = require('utils')
var readFilePromise = utils.promisify(fs.readFile)
var readFile = utils.callbackify(readFilePromise)
一个一个转还是有点麻烦, 现在的 node 已经提供了promise 版本的 fs 模块
fs = require('fs').promises
fs.readFile('a.txt').then().catch()
实 例
接收一个文件夹路径,返回这个文件夹里面的所有文件名,需要递归的得到所有的文件名 并放在一个一维数组里返回
需要写三个版本:
- 同步版
- 回调版
- Promise版本
同步版
const fs = require('fs')
const fsp = fs.promises
function listAllFilesSync(path) {
var stat = fs.statSync(path)
var result = []
if (stat.isFile()) {
return [path] // 如果路径类型是文件,直接返回
} else {
var entries = fs.readdirSync(path, { withFileTypes: true }) // 读取所有文件, withFileTypes 生成的数组将填充 fs.Dirent 对象,而不是字符串
entries.forEach(entry => {
var fullPath = path + '/' + entry.name // 新的路径为原来的path接上新的文件夹名
var files = listAllFilesSync(fullPath) // 递归, 返回的数组全都push到result中
result.push(...files)
});
return result
}
}
console.log(listAllFilesSync('./'))
Promise版本
function listAllFilesPromise(path) {
return fsp.stat(path).then(stat => {
if (stat.isFile()) {
return [path]
} else {
return fsp.readdir(path, {withFileTypes: true}).then(entries => {
return Promise.all(entries.map(entry => {
return listAllFilesPromise(path + '/' + entry.name)
})).then(arrays => {
return [].concat(...arrays)
})
})
}
})
}
listAllFilesPromise('./').then(console.log)
异步版本
function listAllFilesCallback(path, callback) {
fs.stat(path, (err, stat) => {
if (stat.isFile()) {
callback([path])
} else {
fs.readdir(path, {withFileTypes: true}, (err, entries) => {
var result = []
var count = 0
if (entries.length == 0) {
callback([]) // 当文件夹为空时,直接返回,不走forEach
}
entries.forEach((entry, i) => {
var fullPath = path + '/' + entry.name
listAllFilesCallback(fullPath, (files) => {
result[i] = files
count++
if (count == entries.length) {
callback([].concat(...result))
}
})
})
})
}
})
}
listAllFilesCallback('./', console.log)
提 升
以上的三种方法: 同步版本时间效率不高, promise 版本 return 过多比较繁琐, 异步版本 return 不多 但是嵌套层级不灵活
有没有一种方法可以兼具三者的优点呢?
有! 将生成器函数和promise函数组合, 即可优化promise异步代码的书写
简单回顾一下生成器函数:
- 生成器函数在执行时能暂停, 后面又能从暂停处继续执行, 用 function * 声明
- next()方法返回一个对象,包含两个属性:value 和 done,value 属性表示本次 yield 表达式的返回值,done 属性为布尔类型,表示生成器函数是否已经执行完毕并返回。
- 调用 next()方法时,如果传入了参数,那么这个参数会传给上一条执行的 yield语句左边的变量
- 当调用 return 时,会导致生成器立即变为完成状态,即 done 为 true。如果 return 后面跟了一个值,那么这个值会作为当前调用 next() 方法返回的 value 值。
function * natureNumber(n) {
for(var i = 0; i < n; i++) {
var x = yield i
console.log(x)
}
}
var iter = natureNumber(5)
iter.next() // {value: 0, done: false}
iter.next(88) // 88 {value: 1, done: false}
iter.next(99) // 00 {value: 2, done: false}
iter.return(111) // {value: 111, done: true}
生成器函数结合异步操作
我们先用一个 xhr 结合生成器函数举例
function get(url) {
return new Promise(resolve => {
var xhr = new XMLHttpRequest()
xhr.open('get', url)
xhr.onload = () => resolve(xhr.responseText)
xhr.send()
})
}
function * foo () {
var x = yield get('/')
console.log(x)
}
var iter = foo()
obj = iter.next() // {value: Promise, done: false}
obj.value.then(val => iter.next(val)) // 返回页面内容
yield的优点是可以在执行中等待, 等待的时间由函数执行时间决定
为了方便调试,接下来的举例会用setTimeout来模拟一个异步的请求,当返回多个值时:
function squareAsync(x) {
return new Promise(resolve => {
setTimeout(() => resolve(x * x), 1000 + Math.random() * 2000)
})
}
function * foo () {
var x = yield squareAsync(2)
console.log(x)
var y = yield squareAsync(3)
console.log(y)
var z = yield squareAsync(4)
console.log(z)
}
var iter = foo()
iter.next().value.then(val =>{ // yield 返回 promise
iter.next(val).value.then(val =>{ // 4
iter.next(val).value.then(val =>{ // 9
iter.next(val) // 16
})
})
})
是不是发现了点什么, 这么写重复度有点高,可以进行封装:
var iter = foo()
var generated = iter.next()
start()
function start(val) {
if (!generated.done) {
generated.value.then(val => {
generated = iter.next(val)
start(val)
})
}
}
异步递归, 把一个总是生成 promise 的生成器函数, 在 promise 执行时等待, 而后 promise 得到结果之后继续执行, 并将 promise 的结果赋值给变量。 那么, 如果 promise 返回失败了怎么办?
function * foo () {
var x = yield squareAsync(2)
console.log(x)
try {
var y = yield Promise.reject(3)
console.log(y)
} catch(e) {
console.log('error', e)
}
var z = yield squareAsync(4)
console.log(z)
}
var iter = foo()
var generated = iter.next()
step()
function step() {
if (!generated.done) {
generated.value.then(val => {
generated = iter.next(val)
step()
}, reason => {
generated = iter.throw(reason)
step()
})
}
}
利用这种方式, 可以将异步的函数写成类似同步的方式, 在代码可读性上有了很大的提升, 当然了还可以再进行封装, 方便使用, 同时可以将分步执行的函数也作为promise执行, 这样在所有分步执行的步骤结束后我们也可以拿到一个返回值:
run(function * foo () {
var x = yield squareAsync(2)
console.log(x)
try {
var y = yield Promise.reject(3)
console.log(y)
} catch(e) {
console.log('error', e)
}
var z = yield squareAsync(4)
console.log(z)
}).then((val) => {
console.log(val)
})
function run(generatorFunction) {
return new Promise((resolve, reject) => {
var iter = generatorFunction()
var generated = iter.next()
step()
function step() {
if (!generated.done) {
generated.value.then(val => {
generated = iter.next(val)
step()
}, reason => {
generated = iter.throw(reason)
step()
})
} else {
Promise.resolve(generated.value).then(resolve, reject) //此时的generated.value是生成器函数的返回值, 同时返回的函数也有可能是promise,所以返回一个promise
}
}
})
}
接下来, 还有最后一种情况需要考虑, 即在生成器函数中没有 try 应该怎么办, 比如下面这样:
run(function * foo () {
var x = yield squareAsync(2)
console.log(x)
try {
var y = yield Promise.reject(3)
console.log(y)
} catch(e) {
console.log('error', e)
}
var m = yield Promise.reject(4)// yield 抛出错误,但是没有包try
console.log(m)
var z = yield squareAsync(5)
console.log(z)
}).then((val) => {
console.log(val)
})
在这种情况下, yield的错误会抛出到foo函数的外面, 如果不处理的话很有可能就跑到控制台去了。 当然不止这一种情况可能出错, 比如 console.log(m) 不小心写成了 console.log(m(), 会在next的时候产生执行错误, 此要想解决错误, 应该在run中调用try/catch
function run(generatorFunction) {
return new Promise((resolve, reject) => {
var iter = generatorFunction()
var generated
try {
generated = iter.next()
step()
} catch(e) {
reject(e)
}
function step() {
if (!generated.done) {
generated.value.then(val => {
try {
generated = iter.next(val)
step()
} catch(e) {
reject(e)
}
}, reason => {
try {
generated = iter.throw(reason)
step()
} catch(e) {
reject(e)
}
})
} else {
Promise.resolve(generated.value).then(resolve, reject)
}
}
})
}
上面这段代码, 在面试中有可能会被问到, 需要能写出来
刚刚我们只是考虑生成器函数调用普通函数, 如果生成器函数调用的是另一个生成器函数应该怎么写呢, 那个有点复杂, 我们现在先不考虑。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。