предисловие
В последнее время отдел набирает фронтенд. Как единственный фронтенд отдела, он провел собеседование со многими студентами, которые претендуют на работу. Один из вопросов на собеседовании с участием Promise:
20 ресурсов изображений предварительно загружаются на веб-страницу, загружаются поэтапно, 10 изображений загружаются за раз и выполняются дважды, как контролировать параллелизм запросов изображений и как определить, был ли выполнен текущий асинхронный запрос?
Однако очень немногие могут ответить на все из них.Те, кто может дать обратный звонок + считать версию, я думаю, что они квалифицированы. Затем давайте изучим и обобщим три метода борьбы с асинхронностью на основе Promise.
Примером этой статьи является чрезвычайно упрощенная программа для чтения комиксов, которая использует загрузку 4 изображений комиксов, чтобы представить реализацию и различия различных способов асинхронной обработки.Ниже приведен HTML-код:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Promise</title>
<style>
.pics{
width: 300px;
margin: 0 auto;
}
.pics img{
display: block;
width: 100%;
}
.loading{
text-align: center;
font-size: 14px;
color: #111;
}
</style>
</head>
<body>
<div class="wrap">
<div class="loading">正在加载...</div>
<div class="pics">
</div>
</div>
<script>
</script>
</body>
</html>
один запрос
Самый простой — обработать асинхронность одну за другой и превратить ее в синхронный способ обработки. Давайте просто реализуем функцию, которую можно использовать, которая загружает одно изображение, и функцию, которая обрабатывает результат, возвращаемый функцией.
function loadImg (url) {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = function () {
resolve(img)
}
img.onerror = reject
img.src = url
})
}
Идея решения от асинхронного к синхронному такова: когда первыйloadImg(urls[1])
позвоните после завершенияloadImg(urls[2])
, а затем вниз. еслиloadImg()
Это синхронная функция, поэтому естественно думать о __loop__.
for (let i = 0; i < urls.length; i++) {
loadImg(urls[i])
}
когдаloadImg()
Когда он асинхронный, мы можем использовать только цепочку обещаний для достижения и, наконец, сформировать вызов следующим образом:
loadImg(urls[0])
.then(addToHtml)
.then(()=>loadImg(urls[1]))
.then(addToHtml)
//...
.then(()=>loadImg(urls[3]))
.then(addToHtml)
Затем мы используем промежуточную переменную для хранения текущего промиса. Так же, как курсор связанного списка, измененныйfor
Код цикла выглядит следующим образом:
let promise = Promise.resolve()
for (let i = 0; i < urls.length; i++) {
promise = promise
.then(()=>loadImg(urls[i]))
.then(addToHtml)
}
Переменная промиса похожа на итератор, постоянно указывающий на последний возвращенный промис, затем мы дополнительно используем сокращение для упрощения кода.
urls.reduce((promise, url) => {
return promise
.then(()=>loadImg(url))
.then(addToHtml)
}, Promise.resolve())
В программировании операторы цикла могут быть реализованы через __recursion__ функций. Поэтому мы меняем приведенный выше код на __recursive__:
function syncLoad (index) {
if (index >= urls.length) return
loadImg(urls[index])
.then(img => {
// process img
addToHtml(img)
syncLoad (index + 1)
})
}
// 调用
syncLoad(0)
Что ж, простая асинхронная реализация в синхронную выполнена, давайте ее протестировать.
Простая версия этой реализации реализована, но верхняя все еще загружается, так как же узнать конец этой рекурсии вне функции и скрыть DOM?Promise.then()
То же самое возвращает функцию thenable, нам просто нужноsyncLoad
Внутренне передавать эту цепочку промисов, пока не вернется последняя функция.
function syncLoad (index) {
if (index >= urls.length) return Promise.resolve()
return loadImg(urls[index])
.then(img => {
addToHtml(img)
return syncLoad (index + 1)
})
}
// 调用
syncLoad(0)
.then(() => {
document.querySelector('.loading').style.display = 'none'
})
Теперь давайте улучшим эту функцию и сделаем ее более общей, она принимает __асинхронные функции__,Массив параметров, требуемых асинхронными функциями., __Функция обратного вызова асинхронной функции__ имеет три параметра. И будет записывать параметры сбоя вызова, и возвращаться за пределы функции в конце. Кроме того, можно подумать, почему уловка должна быть предпоследней тогда.
function syncLoad (fn, arr, handler) {
if (typeof fn !== 'function') throw TypeError('第一个参数必须是function')
if (!Array.isArray(arr)) throw TypeError('第二个参数必须是数组')
handler = typeof fn === 'function' ? handler : function () {}
const errors = []
return load(0)
function load (index) {
if (index >= arr.length) {
return errors.length > 0 ? Promise.reject(errors) : Promise.resolve()
}
return fn(arr[index])
.then(data => {
handler(data)
})
.catch(err => {
console.log(err)
errors.push(arr[index])
return load(index + 1)
})
.then(() => {
return load (index + 1)
})
}
}
// 调用
syncLoad(loadImg, urls, addToHtml)
.then(() => {
document.querySelector('.loading').style.display = 'none'
})
.catch(console.log)
демо1 адрес:Одиночный запрос — синхронизация нескольких промисов
До сих пор эта функция все еще имеет много необычных проблем, таких как: функция обработки должна быть согласованной, она не может быть очередью, состоящей из нескольких разных асинхронных функций, асинхронная функция обратного вызова может быть только одного типа и т. д. Более подробное описание этого подхода можно найти в статье, которую я написал ранее.Koa-compose из справочной библиотеки Koa - Nuggets.
Конечно, этот асинхронно-синхронный метод не является лучшим решением в этом примере, но при наличии подходящего бизнес-сценария это очень распространенное решение.
одновременные запросы
В конце концов, несколько HTTP-запросов могут быть сделаны одновременно под одним и тем же доменным именем.Для такого рода одновременных запросов, которые не нужно загружать по порядку, а нужно только обрабатывать по порядку,Promise.all
является лучшим решением. потому чтоPromise.all
Это нативная функция, мы будем ссылаться на документацию, чтобы объяснить ее.
Метод Promise.all(iterable) возвращает обещание, когда все обещания в параметре iterable выполнены или первое переданное обещание (отклонение) не выполняется.
отPromise.all() - JavaScript | MDN
Затем давайте изменим пример в demo1:
const promises = urls.map(loadImg)
Promise.all(promises)
.then(imgs => {
imgs.forEach(addToHtml)
document.querySelector('.loading').style.display = 'none'
})
.catch(err => {
console.error(err, 'Promise.all 当其中一个出现错误,就会reject。')
})
демо2 адрес:Параллельные запросы — Promise.all
Параллельные запросы, последовательная обработка результатов
Promise.all
Хотя несколько запросов могут быть сделаны одновременно, как только одно из обещаний не будет выполнено, все обещание будет отменено.reject
.
Обычно используемая предварительная загрузка ресурсов в веб-приложении может загружать 20 кадров кадр за кадром, При возникновении проблем с сетью один или два запроса на 20 изображений неизбежно завершатся ошибкой.resolve
Возвращаемый результат кажется немного неуместным Нам нужно только знать, какие изображения неверны, и сделать еще один запрос на неправильные изображения или компенсировать их с помощью изображения-заполнителя.
Код из предыдущего разделаconst promises = urls.map(loadImg)
После запуска все запросы изображений были отправлены, нам просто нужно обработать их один за другим, чтобыpromises
Экземпляр Promise в этом массиве в порядке, давайте сначала воспользуемся более простымfor
Зацикливание для достижения следующего, как и одиночный запрос в разделе 2, использует цепочку промисов для последовательной обработки.
let task = Promise.resolve()
for (let i = 0; i < promises.length; i++) {
task = task.then(() => promises[i]).then(addToHtml)
}
Изменить на уменьшенную версию
promises.reduce((task, imgPromise) => {
return task.then(() => imgPromise).then(addToHtml)
}, Promise.resolve())
адрес демо3:Обещание одновременных запросов, последовательная обработка результатов
Контролируйте максимальное количество параллелизма
Теперь давайте попробуем выполнить написанные выше тестовые вопросы На самом деле для этого не нужно контролировать максимальное количество параллелизма.
20 картинок, загружу два раза, потом использую дваPromise.all
Разве это не решено? но сPromise.all
Невозможно прослушать событие загрузки каждого изображения. С помощью метода из предыдущего раздела мы можем не только запрашивать одновременно, но и реагировать на событие последовательной загрузки изображения.
let index = 0
const step1 = [], step2 = []
while(index < 10) {
step1.push(loadImg(`./images/pic/${index}.jpg`))
index += 1
}
step1.reduce((task, imgPromise, i) => {
return task
.then(() => imgPromise)
.then(() => {
console.log(`第 ${i + 1} 张图片加载完成.`)
})
}, Promise.resolve())
.then(() => {
console.log('>> 前面10张已经加载完!')
})
.then(() => {
while(index < 20) {
step2.push(loadImg(`./images/pic/${index}.jpg`))
index += 1
}
return step2.reduce((task, imgPromise, i) => {
return task
.then(() => imgPromise)
.then(() => {
console.log(`第 ${i + 11} 张图片加载完成.`)
})
}, Promise.resolve())
})
.then(() => {
console.log('>> 后面10张已经加载完')
})
Приведенный выше код является хардкодом для темы.Если вы можете написать это в письменном тесте, это уже очень хорошо, но никто этого не писал, сказал... адрес demo4 (см. консоль и сетевой запрос):Обещают загрузку пошагово - 1
Затем мы абстрагируем код, пишем общий метод, эта функция возвращает промис и может продолжить обработку асинхронного обратного вызова после загрузки всех изображений.
function stepLoad (urls, handler, stepNum) {
const createPromises = function (now, stepNum) {
let last = Math.min(stepNum + now, urls.length)
return urls.slice(now, last).map(handler)
}
let step = Promise.resolve()
for (let i = 0; i < urls.length; i += stepNum) {
step = step
.then(() => {
let promises = createPromises(i, stepNum)
return promises.reduce((task, imgPromise, index) => {
return task
.then(() => imgPromise)
.then(() => {
console.log(`第 ${index + 1 + i} 张图片加载完成.`)
})
}, Promise.resolve())
})
.then(() => {
let current = Math.min(i + stepNum, urls.length)
console.log(`>> 总共${current}张已经加载完!`)
})
}
return step
}
в коде вышеfor
также можно изменить наreduce
, но вам нужно загрузитьurls
По количеству шагов он разбит на массивы, и заинтересованные друзья могут писать и читать сами.
адрес demo5 (см. консоль и сетевой запрос):Шаги обещания - 2
Но приведенная выше реализация не имеет ничего общего с тем, что мы говорили о __максимальном управлении параллелизмом__.Максимальное управление параллелизмом относится к следующему: при загрузке 20 изображений сначала запрашивается 10 изображений одновременно, а когда загружается одно изображение, и будет продолжаться инициация запроса. для изображения, сохраняя число параллелизма равным 10, пока не будут запрошены все загружаемые изображения. Можно сказать, что это относительно распространенный сценарий использования при написании поисковых роботов. Затем, основываясь на некоторых из вышеперечисленных знаний, мы реализуем эту функцию двумя способами.
использовать рекурсию
Предполагая, что наше максимальное количество параллелизма равно 4, основная идея этого метода заключается в том, что 4 асинхронных задачи Promise, эквивалентные __single request __, выполняются одновременно, а 4 одиночных запроса продолжают рекурсивно извлекать URL-адреса в URL-адресе изображения. массив для инициирования запросов, пока не будут получены все URL-адреса, и, наконец, используйтеPromise.all
Чтобы обрабатывать асинхронные задачи, которые все еще находятся в запросе в конце, мы повторно используем идею __recursive__ версии второго раздела для реализации этой функции:
function limitLoad (urls, handler, limit) {
const sequence = [].concat(urls) // 对数组做一个拷贝
let count = 0
const promises = []
const load = function () {
if (sequence.length <= 0 || count > limit) return
count += 1
console.log(`当前并发数: ${count}`)
return handler(sequence.shift())
.catch(err => {
console.error(err)
})
.then(() => {
count -= 1
console.log(`当前并发数:${count}`)
})
.then(() => load())
}
for(let i = 0; i < limit && i < urls.length; i++){
promises.push(load())
}
return Promise.all(promises)
}
Установите максимальное количество запросов на 5 и запросите временную шкалу загрузки в Chrome:
адрес demo6 (см. консоль и сетевой запрос):Обещания управляют максимальным количеством параллелизма — метод 1использоватьPromise.race
Promise.race
Принимает массив обещаний и возвращает первое обещание в массиве.resolve
Возвращаемое значение обещания. наконец-то нашелPromise.race
Сценарий использования сейчас такой, давайте сначала воспользуемся кодом функции, реализованной этим методом:
function limitLoad (urls, handler, limit) {
const sequence = [].concat(urls) // 对数组做一个拷贝
let count = 0
let promises
const wrapHandler = function (url) {
const promise = handler(url).then(img => {
return { img, index: promise }
})
return promise
}
//并发请求到最大数
promises = sequence.splice(0, limit).map(url => {
return wrapHandler(url)
})
// limit 大于全部图片数, 并发全部请求
if (sequence.length <= 0) {
return Promise.all(promises)
}
return sequence.reduce((last, url) => {
return last.then(() => {
return Promise.race(promises)
}).catch(err => {
console.error(err)
}).then((res) => {
let pos = promises.findIndex(item => {
return item == res.index
})
promises.splice(pos, 1)
promises.push(wrapHandler(url))
})
}, Promise.resolve()).then(() => {
return Promise.all(promises)
})
}
Установите максимальное количество запросов на 5 и запросите временную шкалу загрузки в Chrome:
адрес demo7 (см. консоль и сетевой запрос):Промисы контролируют максимальное количество параллелизма — метод 2В использованииPromise.race
Для достижения этой функции в основном нужно постоянно вызыватьPromise.race
вернуться былоresolve
задание, то изpromises
Удалите этот объект Promise из , и добавьте новый Promise, пока все URL-адреса не будут получены, и, наконец, используйтеPromise.all
для обработки обратного вызова после завершения всех изображений.
напиши в конце
Поскольку синтаксис ES6 широко используется в моей работе, а await/async в Koa — это синтаксический сахар Promise, для меня очень важно понимать различные элементы управления процессами Promise. Если вам что-то непонятно и есть ошибки в написании, вы можете оставить сообщение и исправить его.Кроме того, есть другие методы, которые не задействованы, пожалуйста, предоставьте новые методы и методы.
Не по теме
В настоящее время у нас есть 1 передний ХК, база Шэньчжэнь, отдел искусственного интеллекта логистической компании с 50 самолетами, требующий более трех лет опыта работы, который требуется для социального найма компании. Свяжитесь со мной, если вы заинтересованы, Электронная почта: d2hlYXRvQGZveG1haWwuY29t