Буквально недавно передо мной стала задача загрузка нескольких картинок. Казалось бы, плёвое дело, но мне захотелось сделать это ещё и удобно. Поэтому я решил, что перед загрузкой надо показать пользователю превью добавленных (но, ещё не загруженных) картинок.
Ниже приведена часть кода ответственная за это:
readFile = file => {
let fileReader = new FileReader()
return new Promise((resolve, reject) => {
fileReader.onload = e => {
let dataURI = e.target.result
resolve(<ItemPreview src={dataURI} preview={true}/>)
}
fileReader.onerror = () => reject('Ошибка чтения файла')
if (/^image/.test(file.type)) {
fileReader.readAsDataURL(file)
} else {
reject('Можно загружать только изображения!')
}
})
}
onAddFiles = e => {
const {files} = e.dataTransfer
this.setState({files: [...files]})
let a = [].map.call(files, file => this.readFile(file))
Promise.all(a)
.then(previews => this.setState({items: [...previews, ...this.state.items]}))
.catch(alert)
}
Пара замечаний:
- Во-первых, загрузка происходит как с помощью классического окна выбора файлов, так и с помощью драг'н'дропа.
- Во-вторых, как некоторые уже поняли, это код на React. Компонент на который происходит дроп файлов я опустил ибо его реализация не суть этой статьи. В коде также виден компонент который отображает как уже загруженнвые файлы, так и только что добавленные. Его реализацию я тоже опущу ибо это просто dumb-компонент
Что происходит в коде?
Начну с функции readFile()
Это обычное замыкание, которое возвращает промис, который, в свою очередь, читает файл через новый инстанс интерфейса FileReader (почитать о нём можно на MDN) и возвращает его dataURI, чтобы потом подставить полученный результат в атрибут src тэга <img>.
Внутри этого промиса к инстансу FileReader'а добавляется два обработчика: на успешное чтение файла (onload) и на ошибку чтения (onerror). В них вызываются функции переданные как параметры в промис: resolve() и reject(). В качестве последнего, для примера, я использовал обычный alert(). В качестве resolve() выступает встроенная функция компонентов React: setState(). Вот тут есть важный нюанс о котором я расскажу ниже
Дополнительно происходит проверка на тип файла, но это, что очевидно, не обязательно.
Обработчик события добавления файлов
— он же onAddFiles().
Первые две строчки моджно смело пропустить, потому, что в них берутся выбранные файлы и добавляются в список для последующей загрузки.
Дальше мы собираем в массив a[] пачку промисов которые будут читать добавленные файлы и резолвиться/режектиться в зависимости от результата.
И вот тут наступает тот момент о котором я обещал рассказать выше.
Для начала предлагаю задуматься как можно написать код который будет обрабатывать список файлов с помощью имеющейся у нас функции readFile()? Первое что приходит на ум это перебор файлов в цикле с вызовом для каждого файла этой функции, а затем обновлять стейт компонента через setState().
Только вот беда, на каждую загрузку файла будет происходить обновление стейта и, как следствие, перерисовка компонента включая его потомков, а это минус к производительности, которой так известен React.
И вот тут к нам на помощь приходит волшебная функция Promise.all которая принимает на вход массив промисов и возвращает массив результатов, но только после того, как разрешаться с каким-либо результатом ВСЕ переданные в него промисы. Именно для этого я собрал их в массив на третьей строке функции onSelectFiles(). После того, как промисы отработали, в качестве resolve() вызывается функция setState() которая перерисовывает компонент лишь единожды.
Как говорили раньше в интернетах: PROFIT!!1