Это разрешимо с Promise
s:
function showTextAfterMS (text, elemSelector, ms) {
return new Promise((res, rej) => {
let elem = document.querySelector(elemSelector);
if (!elem) {
return rej(new Error(`Cannot find element by selector: ${elemSelector}`));
}
setTimeout(() => {
elem.innerHTML = text;
res(elem);
}, ms);
});
}
showTextAfterMS('bla', '#el1', 1000).
then(() => showTextAfterMS('blabla', '#el2', 1000)).
then(() => showTextAfterMS('blablabla', '#el3', 1000));
<div id="el1"></div>
<div id="el2"></div>
<div id="el3"></div>
Вы также можете сделать это с setTimeout
или setInterval
, но мой опыт показывает, что использование Promise
немного более надежно / стабильно.
Чтобы отобразить первый текст без задержки, просто измените первый вызов на showTextAfterMS
на showTextAfterMS('bla', '#el1', 0)
.
Редактировать
Причина, по которой использование Promise
s является правильным решением, в конечном счете коренится в концепциях времени выполнения JavaScript. Вкратце, это потому, что технически setTimeout
, а также setInterval
являются асинхронными действиями , потому что они оба обрабатываются циклом событий JavaScript . Подробное объяснение цикла событий и общей модели параллелизма JavaScript можно найти в MDN .
Вкратце: каждое действие, которое должно быть выполнено, переносится в конец Queue
действий, которые должна выполнить среда выполнения, ... ну, в общем, запустить. Таким образом, обрабатываются действия из пользовательского интерфейса, а также другие действия, такие как тайм-ауты и интервалы. Среда выполнения обрабатывает эти действия шаг за шагом, однако им может потребоваться другое время для завершения . Это связано с тем, что среда выполнения запускается до завершения, что означает, что каждое действие обрабатывается после драгоценное действие было обработано полностью. Поскольку действия, производимые setTimeout
и setInterval
, помещаются в Queue
, количество миллисекунд не является гарантированным временем, когда соответствующие функции называются . Это гарантированный минимальный промежуток времени , который истекает до их выполнения. Это заставляет их обоих производить асинхронных действий .
Однако, с архитектурной точки зрения, вам нужен надежный и масштабируемый способ последовательности асинхронных действий . Вот где в игру вступают Promise
.
Немного помахав рукой, мы можем сказать, что мы можем прийти к тому же решению без Promise
, используя функции обратного вызова, из-за того, как работает цикл обработки событий. Он уже вызывает одну функцию за раз , верно? Итак, вот «равное» решение, основанное на обратных вызовах:
// A "Promise equivalent", setTimeout based function with callbacks
function showTextAfter (ms, text, elemSelector, onComplete, onError) {
if (typeof ms !== 'number' || isNaN(ms)) {
return onError(new Error(`MS needs to be number, got: ${ms}`));
}
if (typeof text !== 'string') {
return onError(new Error(`Expected TEXT to be String, got: ${text}`));
}
let elem = document.querySelector(elemSelector);
if (!elem) {
return onError(new Error(`Cannot find element: ${elemSelector}`));
}
setTimeout(() => {
elem.innerHTML = text;
onComplete(elem);
}, ms);
}
showTextAfter(1000, 'bla', '#el1', (elem1) => {
showTextAfter(1000, 'blabla', '#el2', (elem2) => {
showTextAfter(1000, 'blablabla', '#el3', (elem3) => {
// do whatever you want with the elements. this example
// discards them
});
});
});
<div id="el1"></div>
<div id="el2"></div>
<div id="el3"></div>
Он работает одинаково хорошо и позволяет вам надежно связывать свои действия. Недостатки этого:
- Не очень хорошо масштабируется. Чтобы связать больше действий, вам нужно более глубокое вложение, потому что новые действия должны вызываться внутри функции обратного вызова success .
- Вы должны сделать это вложение вручную . Поэтому чем глубже вы будете вкладывать, тем сложнее будет отслеживать поток кода. Вот почему люди называют это «пирамида гибели». Представьте, что вам нужно смешать текст для 20 элементов.
- Посмотрите на его подпись:
showTextAfter :: Number -> String -> String -> Function -> Function -> undefined
. Это много вещей для этой жалкой маленькой функции! Не было бы здорово просто передать первые 3 аргумента?
Мы можем несколько смягчить последнюю проблему, вернув новую функцию из вызова на showTextAfter
, которая использует обратные вызовы onComplete
и onError
:
function showTextAfter (ms, text, elemSelector) {
return function (onComplete, onError) { // <-- this little fellow here is what it's all about
if (typeof ms !== 'number' || isNaN(ms)) {
return onError(new Error(`MS needs to be number, got: ${ms}`));
}
if (typeof text !== 'string') {
return onError(new Error(`Expected TEXT to be String, got: ${text}`));
}
let elem = document.querySelector(elemSelector);
if (!elem) {
return onError(new Error(`Cannot find element: ${elemSelector}`));
}
setTimeout(() => {
elem.innerHTML = text;
onComplete(elem);
}, ms);
}
}
const showEl1 = showTextAfter(1000, 'bla', '#el1');
const showEl2 = showTextAfter(1000, 'blabla', '#el2');
const showEl3 = showTextAfter(1000, 'blablabla', '#el3');
showEl1(elem1 => {
showEl2(elem2 => {
showEl3(elem3 => {
// whatever
});
});
});
<div id="el1"></div>
<div id="el2"></div>
<div id="el3"></div>
Да, так лучше. Но это не действительно не решает проблему, верно?
Не паникуйте, потому что это именно те проблемы, которые решает Promise
s! Они позволяют упорядочивать асинхронные действия в масштабируемом режиме с гораздо более простым отслеживанием потока управления . Все вышеперечисленные проблемы могут быть устранены с помощью (нативного) возвращаемого значения «на месте», к которому можно «связать» другие асинхронные шаги, которые будут выполнены «в будущем» (то есть они могут быть выполнены успешно или могут быть выполнены с помощью Error
). По-настоящему разумно то, что Promise
позволяет вам связать следующее асинхронное действие без вложенности .
См. Мой первоначальный (немного измененный) ответ:
function showTextAfterMS (ms, text, elemSelector) {
return new Promise((onComplete, onError) => {
// type checking stuff...
let elem = document.querySelector(elemSelector);
setTimeout(() => {
elem.innerHTML = text;
onComplete(elem);
}, ms);
});
}
showTextAfterMS(1000, 'bla', '#el1'). // <-- no more nesting!
then(() => showTextAfterMS(1000, 'blabla', '#el2')).
then(() => showTextAfterMS(1000, 'blablabla', '#el3'));
<div id="el1"></div>
<div id="el2"></div>
<div id="el3"></div>