node 初步 (二)

文件系統模塊

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 =>{
  iter.next(val).value.then(val =>{
    iter.next(val).value.then(val =>{
      iter.next(val)
    })
  })
})

是不是發現了點什麼, 這麼寫重複度有點高,可以進行封裝:

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 {
    console.log('error', e)
  }
  var z = yield squareAsync(4)
  console.log(z)
}

var iter = foo()
var generated = iter.next()
start()
function start(val) {
  if (!generated.done) {
    generated.value.then(val => {
      generated = iter.next(val)
      start(val)
    }, reason => {
      generated = iter.throw(reason)
      start()
    })
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章