Promise callbacks возвращает promises
В отношении этих великих двух источников: NZakas - Возвращение Promises в Promise Chains и MDN Promises, я хотел бы спросить следующее:
Каждый раз, когда мы возвращаем значение из обработчика выполнения обещаний, как это значение передается новому обещанию, возвращенному от этого же обработчика?
Например,
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = p1.then(function(value) {
// first fulfillment handler
console.log(value); // 42
return p2;
});
p3.then(function(value) {
// second fulfillment handler
console.log(value); // 43
});
В этом примере p2
является обещанием. p3
также является обещанием, исходящим из обработчика выполнения p1
. Однако p2 !== p3
. Вместо этого p2
как-то волшебным образом решается на 43
(как?), И это значение затем передается в обработчик выполнения p3
. Даже предложение здесь путается.
Не могли бы вы объяснить мне, что именно здесь происходит? Я полностью смущен этой концепцией.
Ответы
Ответ 1
Давайте скажем, что вызов внутри then()
обратного вызова отклоняет обещание результата с отказом, а возврат из обратного вызова then()
выполняет обещание результата с успешным значением.
let p2 = p1.then(() => {
throw new Error('lol')
})
// p2 was rejected with Error('lol')
let p3 = p1.then(() => {
return 42
})
// p3 was fulfilled with 42
Но иногда даже внутри продолжения мы не знаем, преуспели мы или нет. Нам нужно больше времени.
return checkCache().then(cachedValue => {
if (cachedValue) {
return cachedValue
}
// I want to do some async work here
})
Однако, если я выполняю асинхронную работу там, было бы слишком поздно для return
или throw
, не так ли?
return checkCache().then(cachedValue => {
if (cachedValue) {
return cachedValue
}
fetchData().then(fetchedValue => {
// Doesn’t make sense: it’s too late to return from outer function by now.
// What do we do?
// return fetchedValue
})
})
Вот почему Promises не будет полезен, если вы не сможете разрешить другое обещание.
Это не означает, что в вашем примере p2
станет p3
. Это отдельные объекты Promise. Однако, возвращая p2
из then()
, который выражает p3
, вы говорите "Я хочу, чтобы p3
разрешал все, что p2
разрешает, удастся или не удастся".
Как это происходит, его конкретная реализация. Внутренне вы можете думать о then()
как о создании нового обещания. Реализация будет способна выполнять или отклонять ее, когда захочет. Обычно он автоматически выполняет или отклоняет его при возврате:
// Warning: this is just an illustration
// and not a real implementation code.
// For example, it completely ignores
// the second then() argument for clarity,
// and completely ignores the Promises/A+
// requirement that continuations are
// run asynchronously.
then(callback) {
// Save these so we can manipulate
// the returned Promise when we are ready
let resolve, reject
// Imagine this._onFulfilled is an internal
// queue of code to run after current Promise resolves.
this._onFulfilled.push(() => {
let result, error, succeeded
try {
// Call your callback!
result = callback(this._result)
succeeded = true
} catch (err) {
error = err
succeeded = false
}
if (succeeded) {
// If your callback returned a value,
// fulfill the returned Promise to it
resolve(result)
} else {
// If your callback threw an error,
// reject the returned Promise with it
reject(error)
}
})
// then() returns a Promise
return new Promise((_resolve, _reject) => {
resolve = _resolve
reject = _reject
})
}
Опять же, это очень псевдо-код, но показывает, как then()
может быть реализован в реализациях Promise.
Если мы хотим добавить поддержку для решения Promise, нам просто нужно изменить код, чтобы иметь специальную ветвь, если callback
вы переходите на then()
, возвращает Promise:
if (succeeded) {
// If your callback returned a value,
// resolve the returned Promise to it...
if (typeof result.then === 'function') {
// ...unless it is a Promise itself,
// in which case we just pass our internal
// resolve and reject to then() of that Promise
result.then(resolve, reject)
} else {
resolve(result)
}
} else {
// If your callback threw an error,
// reject the returned Promise with it
reject(error)
}
})
Позвольте мне еще раз пояснить, что это не настоящая реализация обещаний и имеет большие дыры и несовместимости. Однако это должно дать вам интуитивное представление о том, как библиотеки Promise реализуют разрешение на обещание. После того, как вы будете довольны этой идеей, я бы рекомендовал вам взглянуть на то, как реальные реализации Promise обрабатывают это.
Ответ 2
В основном p3
есть return
- другое обещание: p2
. Это означает, что результат p2
будет передан как параметр для следующего обратного вызова then
, в этом случае он решает 43
.
Всякий раз, когда вы используете ключевое слово return
, вы передаете результат в качестве параметра для следующего обратного вызова then
.
let p3 = p1.then(function(value) {
// first fulfillment handler
console.log(value); // 42
return p2;
});
Ваш код:
p3.then(function(value) {
// second fulfillment handler
console.log(value); // 43
});
Является равным:
p1.then(function(resultOfP1) {
// resultOfP1 === 42
return p2; // // Returning a promise ( that might resolve to 43 or fail )
})
.then(function(resultOfP2) {
console.log(resultOfP2) // '43'
});
Btw, я заметил, что вы используете синтаксис ES6, у вас может быть более легкий синтаксис с использованием синтаксиса толстых стрелок:
p1.then(resultOfP1 => p2) // the `return` is implied since it a one-liner
.then(resultOfP2 => console.log(resultOfP2));
Ответ 3
В этом примере p2 является обещанием. p3 также является обещанием, исходящим из обработчика выполнения p1. Однако p2! == p3. Вместо этого p2 как-то волшебным образом разрешается до 43 (как?), И это значение затем передается обработчику выполнения p3. Даже предложение здесь путается.
упрощенная версия, как это работает (только псевдокод)
function resolve(value){
if(isPromise(value)){
value.then(resolve, reject);
}else{
//dispatch the value to the listener
}
}
все это довольно сложно, так как вам нужно позаботиться о том, чтобы обещание уже было разрешено и еще несколько вещей.
Ответ 4
Я постараюсь ответить на вопрос "почему then
обратные вызовы могут возвращать Promise
сами" более канонические. Чтобы поднять другой угол, я сравниваю Promise
с менее сложным и запутанным типом контейнера - Array
s.
A Promise
- это контейнер для будущего значения.
Array
- контейнер для произвольного количества значений.
Мы не можем применять обычные функции к типам контейнеров:
const sqr = x => x * x;
const xs = [1,2,3];
const p = Promise.resolve(3);
sqr(xs); // fails
sqr(p); // fails
Нам нужен механизм, чтобы поднять их в контексте конкретного контейнера:
xs.map(sqr); // [1,4,9]
p.then(sqr); // Promise {[[PromiseValue]]: 9}
Но что происходит, когда сама предоставленная функция возвращает контейнер одного и того же типа?
const sqra = x => [x * x];
const sqrp = x => Promise.resolve(x * x);
const xs = [1,2,3];
const p = Promise.resolve(3);
xs.map(sqra); // [[1],[4],[9]]
p.then(sqrp); // Promise {[[PromiseValue]]: 9}
sqra
действует как ожидаемый. Он просто возвращает вложенный контейнер с правильными значениями. Очевидно, это не очень полезно.
Но как интерпретировать результат sqrp
? Если мы будем следовать своей собственной логике, это должно быть что-то вроде Promise {[[PromiseValue]]: Promise {[[PromiseValue]]: 9}}
- но это не так. Итак, какая здесь магия?
Чтобы восстановить механизм, нам просто нужно немного адаптировать наш метод map
:
const flatten = f => x => f(x)[0];
const sqra = x => [x * x];
const sqrp = x => Promise.resolve(x * x);
const xs = [1,2,3];
xs.map(flatten(sqra))
flatten
просто принимает функцию и значение, применяет функцию к значению и разворачивает результат, тем самым уменьшая структуру вложенного массива на один уровень.
Проще говоря, then
в контексте Promise
эквивалентен map
в сочетании с flatten
в контексте Array
s. Такое поведение чрезвычайно важно. Мы можем применять не только нормальные функции к Promise
, но также и функции, которые сами возвращают a Promise
.
Фактически это область функционального программирования. A Promise
- это конкретная реализация monad, then
is bind
/chain
, а функция, возвращающая символ Promise
, является монадической. Когда вы понимаете API Promise
, вы в основном понимаете все монады.