Promise
是 JavaScript 中异步操作的重要组成部分。你可能认为 Promise 不是那么容易理解、学习和使用的。相信我,你并不孤单!
对于许多 Web 开发人员来说,Promise 具有挑战性,即使在与他们合作多年之后也是如此。
在本文中,我想尝试改变这种看法,同时分享我在过去几年中对 JavaScript Promise 的了解。希望你觉得它有用。
JavaScript 中的 Promise 是什么?
Promise
是一个特殊的 JavaScript 对象。它在asynchronous
(又名异步)操作成功完成后产生一个值,如果由于超时、网络错误等原因未能成功完成,则产生一个错误。
成功的调用完成由resolve
函数调用指示,错误由reject
函数调用指示。
你可以像这样使用 Promise 构造函数创建一个 Promise:
let promise = new Promise(function(resolve, reject) {
// Make an asynchronous call and either resolve or reject
});
在大多数情况下,promise 可以用于异步操作。但是,从技术上讲,您可以解决/拒绝同步和异步操作。
等等,我们没有callback
异步操作的功能吗?
哦是的!那就对了。我们callback
在 JavaScript 中有函数。但是,回调在 JavaScript 中并不是什么特别的东西。它是一个常规函数,在调用完成后产生结果asynchronous
(成功/错误)。
“异步”这个词意味着将来会发生某些事情,而不是现在。通常,回调仅在执行网络调用、上传/下载内容、与数据库通信等操作时使用。
虽然callbacks
很有帮助,但它们也有很大的缺点。有时,我们可能在另一个回调中包含一个回调,而另一个回调又在另一个回调中,依此类推。我是认真的!让我们通过一个例子来理解这个“回调地狱”。
如何避免回调地狱——PizzaHub 示例
让我们从 PizzaHub 订购 Veg Margherita 披萨🍕。当我们下订单时,PizzaHub 会自动检测我们的位置,找到附近的比萨餐厅,并确定我们要的比萨是否有货。
如果有,它会检测我们免费获得的饮料和比萨饼,最后,它会下订单。
如果订单成功下单,我们会收到一条确认消息。
那么我们如何使用回调函数来编码呢?我想出了这样的事情:
function orderPizza(type, name) {
// Query the pizzahub for a store
query(`/api/pizzahub/`, function(result, error){
if (!error) {
let shopId = result.shopId;
// Get the store and query pizzas
query(`/api/pizzahub/pizza/${shopid}`, function(result, error){
if (!error) {
let pizzas = result.pizzas;
// Find if my pizza is availavle
let myPizza = pizzas.find((pizza) => {
return (pizza.type===type && pizza.name===name);
});
// Check for the free beverages
query(`/api/pizzahub/beverages/${myPizza.id}`, function(result, error){
if (!error) {
let beverage = result.id;
// Prepare an order
query(`/api/order`, {'type': type, 'name': name, 'beverage': beverage}, function(result, error){
if (!error) {
console.log(`Your order of ${type} ${name} with ${beverage} has been placed`);
} else {
console.log(`Bad luck, No Pizza for you today!`);
}
});
}
})
}
});
}
});
}
// Call the orderPizza method
orderPizza('veg', 'margherita');
让我们仔细看看orderPizza
上面代码中的函数。
它调用一个 API 来获取您附近的披萨店的 ID。之后,它会获取该餐厅可用的比萨饼列表。它检查是否找到了我们要的比萨饼,并进行另一个 API 调用以查找该比萨饼的饮料。最后,订单 API 下订单。
在这里,我们为每个 API 调用使用一个回调。这导致我们在前一个中使用另一个回调,依此类推。
这意味着我们进入了我们称之为(非常有表现力)的东西Callback Hell
。谁想要那个?它还形成了一个代码金字塔,不仅令人困惑而且容易出错。
回调地狱和金字塔演示
有几种方法可以摆脱(或不进入)callback hell
。最常见的是使用Promise
orasync
函数。但是,要async
很好地理解函数,首先需要对Promise
s 有一个公平的理解。
因此,让我们开始并深入探讨 Promise。
了解 Promise 状态
回顾一下,可以使用构造函数语法创建一个 Promise,如下所示:
let promise = new Promise(function(resolve, reject) {
// Code to execute
});
构造函数接受一个函数作为参数。此功能称为executor function
.
// Executor function passed to the
// Promise constructor as an argument
function(resolve, reject) {
// Your logic goes here...
}
executor 函数有两个参数,resolve
并且reject
. 这些是 JavaScript 语言提供的回调。您的逻辑在创建 a 时自动运行的 executor 函数内部new Promise
。
为了使 promise 有效,执行器函数应该调用回调函数中的一个,resolve
或者reject
. 稍后我们将详细了解这一点。
构造new Promise()
函数返回一个promise
对象。由于 executor 函数需要处理异步操作,因此返回的 promise 对象应该能够通知执行何时开始、何时完成(解决)或重新调整错误(拒绝)。
对象具有promise
以下内部属性:
state
– 此属性可以具有以下值:
pending
:最初当执行器函数开始执行时。fulfilled
: 当 promise 被解决时。rejected
: 当 promise 被拒绝时。
承诺状态
2. result
– 此属性可以具有以下值:
undefined
: 初始state
值为时pending
。value
: 什么时候resolve(value)
调用。error
: 什么时候reject(error)
调用。
这些内部属性是代码不可访问的,但它们是可检查的。这意味着我们将能够使用调试器工具检查state
和result
属性值,但我们将无法使用程序直接访问它们。
能够检查 Promise 的内部属性
Promise 的状态可以是pending
,fulfilled
或rejected
. 被解决或被拒绝的承诺被称为settled
。
已确定的承诺要么被履行,要么被拒绝
如何解决和拒绝承诺
fulfilled
这是一个将立即使用值解析(状态)的承诺示例I am done
。
let promise = new Promise(function(resolve, reject) {
resolve("I am done");
});
下面的承诺将被拒绝(rejected
状态)并显示错误消息Something is not right!
。
let promise = new Promise(function(resolve, reject) {
reject(new Error('Something is not right!'));
});
需要注意的重要一点:
Promise 执行者应该只调用一个resolve
或一个reject
。一旦改变了一种状态(待处理 => 已完成或待处理 => 拒绝),仅此而已。resolve
对或的任何进一步调用都reject
将被忽略。
let promise = new Promise(function(resolve, reject) {
resolve("I am surely going to get resolved!");
reject(new Error('Will this be ignored?')); // ignored
resolve("Ignored?"); // ignored
});
在上面的例子中,只有第一个要解析的会被调用,其余的会被忽略。
创建 Promise 后如何处理它
Promise
使用 executor 函数来完成任务(主要是异步的)。当执行器功能通过解决(成功)或拒绝(错误)完成时,消费者功能(使用承诺的结果)应该得到通知。
处理程序方法 、.then()
和.catch()
有助于.finally()
在执行程序和消费者函数之间创建链接,以便它们可以在 promise resolve
s 或reject
s 时保持同步。
执行者和消费者函数
如何使用.then()
Promise 处理程序
.then()
应该在 promise 对象上调用该方法来处理结果(resolve)或错误(reject)。
它接受两个函数作为参数。通常,该.then()
方法应该从您想知道 Promise 执行结果的消费者函数中调用。
promise.then(
(result) => {
console.log(result);
},
(error) => {
console.log(error);
}
);
如果你只对成功的结果感兴趣,你可以只传递一个参数给它,像这样:
promise.then(
(result) => {
console.log(result);
}
);
如果您只对错误结果感兴趣,则可以传递null
第一个参数,如下所示:
promise.then(
null,
(error) => {
console.log(error)
}
);
但是,您可以使用.catch()
我们稍后将看到的方法以更好的方式处理错误。
让我们看几个使用.then
and.catch
处理程序处理结果和错误的示例。我们将通过一些真正的异步请求使这种学习变得更有趣。我们将使用PokeAPI获取有关 Pokémon 的信息并使用 Promises 解决/拒绝它们。
首先,让我们创建一个接受 PokeAPI URL 作为参数并返回 Promise 的通用函数。如果 API 调用成功,则返回已解决的承诺。任何类型的错误都会返回被拒绝的承诺。
从现在开始,我们将在几个示例中使用这个函数来获得一个承诺并努力实现它。
function getPromise(URL) {
let promise = new Promise(function (resolve, reject) {
let req = new XMLHttpRequest();
req.open("GET", URL);
req.onload = function () {
if (req.status == 200) {
resolve(req.response);
} else {
reject("There is an Error!");
}
};
req.send();
});
return promise;
}
获取 Promise 的实用方法
示例 1:获取 50 只神奇宝贝的信息:
const ALL_POKEMONS_URL = 'https://pokeapi.co/api/v2/pokemon?limit=50';
// We have discussed this function already!
let promise = getPromise(ALL_POKEMONS_URL);
const consumer = () => {
promise.then(
(result) => {
console.log({result}); // Log the result of 50 Pokemons
},
(error) => {
// As the URL is a valid one, this will not be called.
console.log('We have encountered an Error!'); // Log an error
});
}
consumer();
示例 2:让我们尝试一个无效的 URL
const POKEMONS_BAD_URL = 'https://pokeapi.co/api/v2/pokemon-bad/';
// This will reject as the URL is 404
let promise = getPromise(POKEMONS_BAD_URL);
const consumer = () => {
promise.then(
(result) => {
// The promise didn't resolve. Hence, it will
// not be executed.
console.log({result});
},
(error) => {
// A rejected prmise will execute this
console.log('We have encountered an Error!'); // Log an error
}
);
}
consumer();
如何使用.catch()
Promise 处理程序
您可以使用此处理程序方法来处理来自 Promise 的错误(拒绝)。null
作为第一个参数传递给的语法.then()
不是处理错误的好方法。所以我们必须.catch()
用一些简洁的语法来做同样的工作:
// This will reject as the URL is 404
let promise = getPromise(POKEMONS_BAD_URL);
const consumer = () => {
promise.catch(error => console.log(error));
}
consumer();
如果我们抛出一个错误new Error("Something wrong!")
而不是从承诺执行程序和处理程序中调用reject
,它仍然会被视为拒绝。这意味着这将被.catch
处理程序方法捕获。
这对于在承诺执行程序和处理程序函数中发生的任何同步异常都是相同的。
这是一个示例,它将被视为拒绝并且.catch
将调用处理程序方法:
new Promise((resolve, reject) => {
throw new Error("Something is wrong!");// No reject call
}).catch((error) => console.log(error));
如何使用.finally()
Promise 处理程序
处理程序执行清理,.finally()
例如停止加载程序、关闭实时连接等。finally()
无论promiseresolve
是s 还是s,都会调用该方法reject
。它将结果或错误传递给下一个可以再次调用 .then() 或 .catch() 的处理程序。
这是一个示例,可以帮助您一起理解所有三种方法:
let loading = true;
loading && console.log('Loading...');
// Gatting Promise
promise = getPromise(ALL_POKEMONS_URL);
promise.finally(() => {
loading = false;
console.log(`Promise Settled and loading is ${loading}`);
}).then((result) => {
console.log({result});
}).catch((error) => {
console.log(error)
});
进一步解释一下:
- 该
.finally()
方法使加载false
。 - 如果 Promise 解决,该
.then()
方法将被调用。如果 promise 因错误而拒绝,.catch()
则将调用该方法。无论解决或拒绝如何,.finally()
都会调用遗嘱。
什么是承诺链?
该 promise.then()
调用始终返回一个承诺。这个承诺将有state
aspending
和result
as undefined
。它允许我们在新的 Promise 上调用 next.then
方法。
当第一个.then
方法返回一个值时,下一个.then
方法可以接收该值。第二个现在可以传递给第三个.then()
,依此类推。这形成了一系列.then
方法来传递承诺。这种现象被称为Promise Chain
。
承诺链
这是一个例子:
let promise = getPromise(ALL_POKEMONS_URL);
promise.then(result => {
let onePokemon = JSON.parse(result).results[0].url;
return onePokemon;
}).then(onePokemonURL => {
console.log(onePokemonURL);
}).catch(error => {
console.log('In the catch', error);
});
在这里,我们首先得到一个已解决的承诺,然后提取 URL 以到达第一个神奇宝贝。然后我们返回该值,并将其作为承诺传递给下一个 .then() 处理函数。因此输出,
https://pokeapi.co/api/v2/pokemon/1/
该.then
方法可以返回:
- 一个值(我们已经看到了)
- 一个全新的承诺。
它也可能引发错误。
这是一个示例,其中我们创建了一个带有.then
返回结果的方法和一个新的 Promise 的 Promise 链:
// Promise Chain with multiple then and catch
let promise = getPromise(ALL_POKEMONS_URL);
promise.then(result => {
let onePokemon = JSON.parse(result).results[0].url;
return onePokemon;
}).then(onePokemonURL => {
console.log(onePokemonURL);
return getPromise(onePokemonURL);
}).then(pokemon => {
console.log(JSON.parse(pokemon));
}).catch(error => {
console.log('In the catch', error);
});
在第一次.then
调用中,我们提取 URL 并将其作为值返回。这个 URL 将被传递给第二个.then
调用,我们将返回一个以该 URL 作为参数的新承诺。
这个承诺将被解决并传递给我们获取神奇宝贝信息的链。这是输出:
承诺链调用的输出
如果出现错误或 Promise 拒绝,将调用链中的 .catch 方法。
.then
需要注意的一点:多次调用不会形成 Promise 链。您最终可能会这样做,只是为了在代码中引入一个错误:
let promise = getPromise(ALL_POKEMONS_URL);
promise.then(result => {
let onePokemon = JSON.parse(result).results[0].url;
return onePokemon;
});
promise.then(onePokemonURL => {
console.log(onePokemonURL);
return getPromise(onePokemonURL);
});
promise.then(pokemon => {
console.log(JSON.parse(pokemon));
});
我们.then
在同一个 Promise 上调用该方法 3 次,但我们没有将 Promise 传递下去。这与承诺链不同。在上面的例子中,输出将是一个错误。
如何处理多个 Promise
除了处理程序方法(.then、.catch 和 .finally)之外,Promise API 中还有六个可用的静态方法。前四个方法接受一组 Promise 并并行运行它们。
- 承诺.all
- 承诺.any
- Promise.all 已解决
- 承诺竞赛
- Promise.resolve
- 承诺拒绝
让我们逐一介绍。
Promise.all() 方法
Promise.all([promises])
接受 Promise 的集合(例如,数组)作为参数并并行执行它们。
这个方法等待所有的 Promise 解决并返回 Promise 结果数组。如果任何一个 Promise 因错误而拒绝或执行失败,则所有其他 Promise 结果都将被忽略。
让我们创建三个 Promise 来获取有关三个神奇宝贝的信息。
const BULBASAUR_POKEMONS_URL = 'https://pokeapi.co/api/v2/pokemon/bulbasaur';
const RATICATE_POKEMONS_URL = 'https://pokeapi.co/api/v2/pokemon/raticate';
const KAKUNA_POKEMONS_URL = 'https://pokeapi.co/api/v2/pokemon/kakuna';
let promise_1 = getPromise(BULBASAUR_POKEMONS_URL);
let promise_2 = getPromise(RATICATE_POKEMONS_URL);
let promise_3 = getPromise(KAKUNA_POKEMONS_URL);
通过传递一组承诺来使用 Promise.all() 方法。
Promise.all([promise_1, promise_2, promise_3]).then(result => {
console.log({result});
}).catch(error => {
console.log('An Error Occured');
});
输出:
正如您在输出中看到的那样,返回了所有承诺的结果。执行所有 Promise 的时间等于 Promise 运行的最长时间。
Promise.any() 方法
Promise.any([promises])
- 与all()
方法类似,.any()
也接受一组 promise 以并行执行它们。此方法不会等待所有承诺解决。当任何一个承诺得到解决时,它就完成了。
Promise.any([promise_1, promise_2, promise_3]).then(result => {
console.log(JSON.parse(result));
}).catch(error => {
console.log('An Error Occured');
});
输出将是任何已解决的承诺的结果:
Promise.allSettled() 方法
romise.allSettled([promises])
- 此方法等待所有承诺解决(解决/拒绝)并将其结果作为对象数组返回。结果将包含状态(已完成/已拒绝)和值(如果已完成)。在拒绝状态的情况下,它将返回错误原因。
以下是所有已履行承诺的示例:
Promise.allSettled([promise_1, promise_2, promise_3]).then(result => {
console.log({result});
}).catch(error => {
console.log('There is an Error!');
});
输出:
如果任何一个 promise 拒绝了,比如 promise_1,
let promise_1 = getPromise(POKEMONS_BAD_URL);
Promise.race() 方法
Promise.race([promises])
– 它等待第一个(最快的)promise 解决,并相应地返回结果/错误。
Promise.race([promise_1, promise_2, promise_3]).then(result => {
console.log(JSON.parse(result));
}).catch(error => {
console.log('An Error Occured');
});
输出最快得到解决的承诺:
Promise.resolve/reject 方法
Promise.resolve(value)
– 它使用传递给它的值来解决一个承诺。它与以下内容相同:
let promise = new Promise(resolve => resolve(value));
Promise.reject(error)
– 它拒绝一个带有错误传递给它的承诺。它与以下内容相同:
let promise = new Promise((resolve, reject) => reject(error));
我们可以用 Promises 重写 PizzaHub 的例子吗?
当然,让我们这样做。让我们假设该query
方法将返回一个承诺。这是一个示例 query() 方法。在现实生活中,此方法可能会与数据库对话并返回结果。在这种情况下,它是非常硬编码的,但用于相同的目的。
function query(endpoint) {
if (endpoint === `/api/pizzahub/`) {
return new Promise((resolve, reject) => {
resolve({'shopId': '123'});
})
} else if (endpoint.indexOf('/api/pizzahub/pizza/') >=0) {
return new Promise((resolve, reject) => {
resolve({pizzas: [{'type': 'veg', 'name': 'margherita', 'id': '123'}]});
})
} else if (endpoint.indexOf('/api/pizzahub/beverages') >=0) {
return new Promise((resolve, reject) => {
resolve({id: '10', 'type': 'veg', 'name': 'margherita', 'beverage': 'coke'});
})
} else if (endpoint === `/api/order`) {
return new Promise((resolve, reject) => {
resolve({'type': 'veg', 'name': 'margherita', 'beverage': 'coke'});
})
}
}
接下来是我们的重构callback hell
。为此,首先,我们将创建一些逻辑函数:
// Returns a shop id
let getShopId = result => result.shopId;
// Returns a promise with pizza list for a shop
let getPizzaList = shopId => {
const url = `/api/pizzahub/pizza/${shopId}`;
return query(url);
}
// Returns a promise with pizza that matches the customer request
let getMyPizza = (result, type, name) => {
let pizzas = result.pizzas;
let myPizza = pizzas.find((pizza) => {
return (pizza.type===type && pizza.name===name);
});
const url = `/api/pizzahub/beverages/${myPizza.id}`;
return query(url);
}
// Returns a promise after Placing the order
let performOrder = result => {
let beverage = result.id;
return query(`/api/order`, {'type': result.type, 'name': result.name, 'beverage': result.beverage});
}
// Confirm the order
let confirmOrder = result => {
console.log(`Your order of ${result.type} ${result.name} with ${result.beverage} has been placed!`);
}
使用这些函数来创建所需的 Promise。这是您应该与callback hell
示例进行比较的地方。这是如此美好和优雅。
function orderPizza(type, name) {
query(`/api/pizzahub/`)
.then(result => getShopId(result))
.then(shopId => getPizzaList(shopId))
.then(result => getMyPizza(result, type, name))
.then(result => performOrder(result))
.then(result => confirmOrder(result))
.catch(function(error){
console.log(`Bad luck, No Pizza for you today!`);
})
}
最后,通过传递比萨饼的类型和名称来调用 orderPizza() 方法,如下所示:
orderPizza('veg', 'margherita');
下一步是什么?
如果您在这里并且已经阅读了上面的大部分内容,那么恭喜!您现在应该更好地掌握 JavaScript Promises。本文中使用的所有示例都在这个GitHub 存储库中。
接下来,您应该了解async
JavaScript 中进一步简化事情的函数。JavaScript Promise 的概念最好通过编写小示例并在它们之上构建来学习。
无论我们使用何种框架或库(Angular、React、Vue 等),异步操作都是不可避免的。这意味着我们必须了解让事情变得更好的承诺。
另外,我相信您现在会发现该fetch
方法的使用要容易得多:
fetch('/api/user.json')
.then(function(response) {
return response.json();
})
.then(function(json) {
console.log(json); // {"name": "tapas", "blog": "freeCodeCamp"}
});
- 该
fetch
方法返回一个承诺。所以我们可以调用.then
它的handler方法。 - 剩下的就是我们在本文中学到的承诺链。