读取文件的 IO 操作,为什么要先实例化一个 FileReader 呢?

比如页面上有一个 <input type="file" onChange="todo"> 的选择器,我选择完以后绑定相对应的 onChange 事件。

function todo(){}
const fileReader = new FileReader()
const file=inputElement.files[0]
//接下来处理文件
//fileReader 的一些文件处理方法

  if (file) {
    fileReader.readAsDataURL(file)
  }

  fileReader.addEventListener(
    'load',
    () => {
      const result = fileReader.result
      const resultContainer = document.getElementById('result')
      const img = document.createElement('img')
      img.src = result
      resultContainer.append(img)
    },
    { once: true }
  )

这种 FilerReader 实例化以后再进行读取操作的方法我在学习 go 这门语言中也见到过类似的设计。

问题:我好奇的点在于,为什么我们总要创建一个 fileReader事例再去做一些事情?(为什么不直接给构造函数传参数 fileReader= new FileReader([file]) 然后假设就可以在 fileReader 实例的属性上进行读取操作。比如:fileReader.resultfileReader.readAsDataUrl

这种设计模式有什么好处吗?或者说 new FileReader(file) 直接传参有什么弊端吗?

阅读 1.7k
avatarAI BotBETA

回答

在 JavaScript 中,FileReader 对象的设计是为了异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 FileBlob 对象指定要读取的文件或数据。这种设计模式遵循了 JavaScript 中常见的异步编程模式,并且与 Web API 的其他部分保持一致。

为什么 FileReader 需要先实例化,然后再调用其方法来读取文件,而不是直接在构造函数中传入文件并立即读取,主要有以下几个原因:

  1. 异步性:文件读取可能是一个耗时的操作,特别是当文件很大或者网络条件不佳时。因此,JavaScript 提供了 FileReader API 来异步处理这些操作,以避免阻塞主线程。如果 FileReader 允许在构造函数中直接传入文件并立即读取,那么它就需要同步地等待读取完成,这可能会导致用户界面无响应或延迟。
  2. 可复用性:通过实例化 FileReader 对象,你可以多次使用同一个对象来读取不同的文件,或者对同一个文件执行不同的读取操作(如 readAsTextreadAsDataURL 等)。这使得 FileReader 对象更加灵活和可复用。
  3. 事件驱动FileReader API 是基于事件的。当你调用 readAsDataURLreadAsText 等方法时,文件读取操作会立即开始,但结果并不会立即返回。相反,当读取操作完成时,会触发一个 load 事件,你可以通过监听这个事件来获取读取结果。这种基于事件的设计模式允许你在文件读取过程中执行其他操作,并在读取完成时得到通知。
  4. 错误处理:除了 load 事件外,FileReader 还提供了 errorprogress 等其他事件。通过监听这些事件,你可以处理读取过程中的错误或监视读取进度。

如果将文件直接作为参数传递给 FileReader 构造函数,并在构造函数中立即读取文件,那么上述的所有优点都将不复存在。此外,直接在构造函数中读取文件也会破坏 JavaScript 的异步编程模型和事件驱动的设计模式。

因此,尽管直接在构造函数中传入文件并立即读取看起来可能更简洁,但这种设计并不符合 JavaScript 的异步编程模型和 Web API 的设计原则。通过先实例化 FileReader 对象,再调用其方法来读取文件,我们可以更好地利用 JavaScript 的异步性和事件驱动的特性,从而创建更加高效、灵活和可维护的代码。

3 个回答

为什么要实例化FileReader再用?
实例化再用,更容易扩展逻辑。
比如你的例子,

const fileReader = new FileReader()
fileReader.readAsText(file)
fileReader.addEventListener('load', () => {
  console.log(fileReader.result)
})

如果后续有人反馈,文件很大,读取文件需要很长时间,希望加进度条,有了FileReader实例,我们就能方便地再加一个监听函数

const fileReader = new FileReader()
fileReader.readAsText(file)
fileReader.addEventListener('load', () => {
  console.log(fileReader.result)
})

// 新增进度监听
fileReader.addEventListener('progress', (ev) => {
  console.log(`${ev.loaded}/${ev.total}`)
})

如果后续又有人提出增加可以取消按钮,我们可以继续利用FileReader实例,调用abort()方法中断读取。

const fileReader = new FileReader()
fileReader.readAsText(file)
fileReader.addEventListener('load', () => {
  console.log(fileReader.result)
})

// 新增进度监听
fileReader.addEventListener('progress', (ev) => {
  console.log(`${ev.loaded}/${ev.total}`)
})

// 新增取消按钮
cancel.addEventListener('click', () => {
  fileReader.abort()
}, { once: true })

由上可见,实例化FileReader再操作,可以更灵活地利用FileReader提供的接口,做更多的事。


然后再说说为什么不是直接new FileReader(file)
如果做过java开发,会发现java确实是这么干的。

public static void main(String[] args) {
  char[] array = new char[100];
  FileReader input = new FileReader("file.txt");

  // Reads characters
  input.read(array);

  // Closes the reader
  input.close();
}

以下是我的推测了,不一定正确。
首先JS天生是异步的,文件的加载异步过程,一般不会设计在构造函数里。你肯定不会见过、也不允许写出这样的构造函数:

class MyClass {
  // 报错 'async' modifier cannot appear on a constructor declaration
  async constructor() {
    
  }
}

当然,你也可以说,构造函数里并不立刻加载文件,可以在调用fileReader.readAsDataUrl()再加载嘛。
这样的设计确实可行。但这需要考虑一点,就是避免不必要的理解成本。
new FileReader(file)fileReader.readAsDataUrl(),会给人造成错误的理解,误认为文件加载发生在构造函数中。如果换成new FileReader()fileReader.readAsDataUrl(file),那就能明确提示文件加载是发生在后一步里。

另外先new FileReader()fileReader.readAsDataUrl(file)还有一点好处,就是可以单例复用,比如:

const input = document.getElementById('input')

// 只用一个实例
const fileReader = new FileReader()

input.onchange = () => {
  const file = input.files?.item(0)
  if (!file) return

  fileReader.readAsText(file)
  fileReader.onload = () => {
    console.log(fileReader.result)
  }
}

这样可以减少FileReader的构造次数。


另外再提一下,FileReaderXMLHttpRequest差不多是一个年代的产物,那时候非常推崇OOP(面向对象),所以“先实例化再调用方法”这种API设计也很流行。比如XMLHttpRequest

const req = new XMLHttpRequest()
req.open("GET", "http://www.example.org/example.txt")
req.addEventListener("load", () => {
  console.log(req.responseText)
})
req.send()

是不是感觉跟FileReader有几分相像?
不过现在有了fetch,就不需要那么麻烦了。

const result = await fetch('http://www.example.org/example.txt').then(r => r.text())
console.log(result)

你可能会好奇FileReader有没有替代的方案?
有,但要具体分析,取决于你要获取怎样的数据。
还是以<input type="file" id="input">为例。
如果是加载文本:

const input = document.getElementById('input')

input.addEventListener('change', async () => {
  const file = input.files?.item(0)
  if (!file) return

  // 没错,File继承自Blob,自带text()方法了
  const text = await file.text()

  console.log(text)
})

如果是加载图片:

/** @type {HTMLInputElement} */
const input = document.getElementById('input')
/** @type {HTMLImageElement} */
const img = document.getElementById('img')

input.addEventListener('change', () => {
  const file = input.files?.item(0)
  if (!file) return

  // 创建一个虚拟URL给img标签使用
  const blobURL = URL.createObjectURL(file)
  img.src = blobURL

  // img标签加载完成后,释放虚拟URL
  img.addEventListener('load', () => {
    URL.revokeObjectURL(blobURL)
  }, { once: true })
})

类与实例的设计,方便我们将类的状态保存在实例中,以备将来使用。并提供多样性的方法,满足不同需求。

换句话说,如果一个需求比较简单,一个动作就能完成,也没有太多衍生操作,那么做成函数就比较合适。比如 atobbtoa。反之,如果需求比较复杂,或者要覆盖更多的场景,可能类与实例的模式就更合适。比如你问题中的文件。你只用到一个方法,所以觉得函数化更方便;但是实际上需求会更多样,所以目前这样的设计更有价值。

回答:应该是JavaScript设计的WebApi和Java那种不一样,这个fileReader.read的相关操作都是异步的,写成调用函数传参的时候代入读取的内容,如file对象,可以比较好的在回调函数中使用这个对象,不然如果直接放在构造函数中去传入,重复使用这个fileReader就需要不停的重新调用构造函数,不免有些浪费了;而且这种基于事件回调类型的api很少通过构造函数直接传入,使用作用域大了,得考虑的事情会变多。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题