沈公子总是只留下电话号码 發表於 2023-8-9 21:27:00

重学JavaScript Promise API

<blockquote>
<p>在这篇教程中,我们将掌握如何在JavaScript中创建并使用Promise。我们将了解Promise链式调用、错误处理以及最近添加到语言中的一些Promise静态方法。</p>
</blockquote>
<h2 id="什么是promise">什么是Promise?</h2>
<p>在JavaScript中,一些操作是异步的。这意味着当这些操作完成时,它们产出的结果或者值并不会立即生效。</p>
<p>Promise是一个特殊的JavaScript对象,它代表了异步操作的最终结果。它就像操作结果的代理。</p>
<h2 id="回调函数">回调函数</h2>
<p>在拥有JavaScript Promise之前,处理异步操作最优雅的方式是使用回调。当异步操作的结果就绪时,回调就是一个运行的函数。比如说:</p>
<pre><code class="language-jsx">setTimeout(function() {
console.log('Hello, World!');
}, 1000);
</code></pre>
<p>这里,setTimeout是一个异步函数,在指定的毫秒数后运行传递给它的回调函数。在本例中,它在一秒后将 "Hello, World!"打印到控制台。</p>
<p>此时想象我们想要在五秒之内每秒都打印一个信息。代码就会是这样的:</p>
<pre><code class="language-jsx">setTimeout(function() {
console.log(1);
setTimeout(function() {
    console.log(2);
    setTimeout(function() {
      console.log(3);
      setTimeout(function() {
      console.log(4);
      setTimeout(function() {
          console.log(5);
      }, 1000);
      }, 1000);
    }, 1000);
}, 1000);
}, 1000);
</code></pre>
<p>以这种方式使用多个嵌套回调的异步JavaScript既容易出错又难以维护。它通常被称为回调地狱,甚至有自己的网页。</p>
<p>当然,这是一个臆造的例子,但它有助于说明问题。在实际场景中,我们可能会进行Ajax调用,用结果更新DOM,然后等待动画完成。或者,我们的服务器可能从客户端接收输入,验证输入,更新数据库,写入日志文件,最后发送响应。在这两种情况下,我们还需要处理发生的任何错误。</p>
<p>使用嵌套回调来完成这样的任务是非常痛苦的。幸运的是,Promise为我们提供了一种更简洁的语法,使我们能够将异步命令串联起来,让它们一个接一个地运行。</p>
<h2 id="创建promise">创建Promise</h2>
<p>创建Promise的基本语法如下:</p>
<pre><code class="language-jsx">const promise = new Promise((resolve, reject) =&gt; {
//asynchronous code goes here
});
</code></pre>
<p>首先,我们使用<code>Promise</code>构造函数实例化一个新的Promise对象,并传递给它一个回调函数。回调接收两个参数:<code>resolve</code>和<code>reject</code>,它们都是函数。我们所有的异步代码都在回调函数中。</p>
<p>如果一切运行成功,则通过调用 <code>resolve</code> 来实现Promise。如果出现错误,则调用 <code>reject</code> 拒绝Promise。我们可以向这两个方法传递值,这些值将在消费代码中可用。</p>
<p>要了解这在实践中是如何工作的,请参考下面的代码。该代码向web服务发出异步请求,以 JSON 格式返回一个随机的笑话:</p>
<pre><code class="language-jsx">const promise = new Promise((resolve, reject) =&gt; {
const request = new XMLHttpRequest();
request.open('GET', '&lt;https://icanhazdadjoke.com/&gt;');
request.setRequestHeader('Accept', 'application/json');

request.onload = () =&gt; {
    if (request.status === 200) {
      resolve(request.response); // we got data here, so resolve the Promise
    } else {
      reject(Error(request.statusText)); // status is not 200 OK, so reject
    }
};

request.onerror = () =&gt; {
    reject(Error('Error fetching data.')); // error occurred, reject thePromise
};

request.send(); // send the request
});
</code></pre>
<h3 id="promise构造函数">Promise构造函数</h3>
<p>我们首先使用<code>Promise</code>构造函数创建一个新的Promise对象。该构造函数用于封装尚未支持Promise的函数或API,例如上面的<code>XMLHttpRequest</code>对象。传递给Promise构造函数的回调包含用于从远程服务获取数据的异步代码。(注意,我们在这里使用的是箭头函数)在回调中,我们向 https://icanhazdadjoke.com/ 创建了一个 Ajax 请求,该请求以 JSON 格式返回一个随机的笑话。</p>
<p>当从远程服务器收到成功的响应时,会传递给<code>resolve</code>方法。如果发生任何错误(无论是在服务器上还是在网络层),<code>reject</code>方法将调用一个<code>Error</code>对象。</p>
<h3 id="then方法">then方法</h3>
<p>当我们实例化一个Promise对象时,我们将得到一个未来可用数据的代理。在我们的例子中,我们期待从远程服务返回一些数据。那么,我们如何知道数据何时可用呢?这就是使用<code>Promise.then()</code>函数的地方:</p>
<pre><code class="language-jsx">const promise = new Promise((resolve, reject) =&gt; { ... });

promise.then((data) =&gt; {
console.log('Got data! Promise fulfilled.');
document.body.textContent = JSON.parse(data).joke;
}, (error) =&gt; {
console.error('Promise rejected.');
console.error(error.message);
});
</code></pre>
<p>该函数可以接受两个参数:成功回调和失败回调。这些回调将在Promise解决(即<code>fulfilled</code>或<code>rejected</code>)时调用。如果Promise实现,成功回调将使用我们传递给<code>resolve</code>的实际数据触发。如果Promise被拒绝,失败回调将被调用。无论我们传递给<code>reject</code>的是什么,都将作为参数传递给该回调。</p>
<h3 id="promise的状态">Promise的状态</h3>
<p>在上面代码中,我们可以通过调用<code>resolve</code>和<code>reject</code>方法来改变Promise的状态。在继续之前,花点时间看下Promise的生命周期。</p>
<p>Promise的状态会是下面值的其中一种:</p>
<ul>
<li>pending</li>
<li>fulfilled</li>
<li>rejected</li>
<li>settled</li>
</ul>
<p>Promise开始时处于<code>pending</code>的状态。这意味着它既没有<code>fulfilled</code>也没有<code>rejected</code>。如果与Promise相关的操作成功(在我们的示例中是远程 API 调用),并且调用了 <code>resolve</code> 方法,那么Promise称为<code>fulfilled</code>。另一方面,如果相关操作不成功,且 <code>reject</code> 方法被调用,则该Promise处于<code>rejected</code>状态。最后,如果Promise处于<code>fulfilled</code>或<code>rejected</code>状态,但不是<code>pending</code>状态,则称为<code>settled</code>。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/474b89d620124e82aeb46c72a0638eb8~tplv-k3u1fbpfcp-watermark.image?"></p>
<p>一旦Promise是<code>rejected</code>或者<code>fulfilled</code>,该状态将永久与之关联。这意味着Promise只能成功或失败一次。如果Promise已经<code>fulfilled</code>,并且在其后附加有两个回调的<code>then()</code>,那么成功回调会直接被调用。因此,在Promise的世界里,我们不关心Promise何时<code>settled</code>。我们只关心Promise的最终结果。</p>
<h2 id="promise链式调用">Promise链式调用</h2>
<p>有时可能需要将多个异步任务按照特定顺序链在一起。这就是所谓的Promise链式调用。让我们重温一下 <code>setTimeout</code> 示例,以了解Promise链式调用的基本工作原理。</p>
<p>我们可以像以前一样,首先创建一个新的Promise对象:</p>
<pre><code class="language-jsx">const promise = new Promise((resolve, reject) =&gt; {
setTimeout(() =&gt; { resolve() }, 1000)
});

promise.then(() =&gt; {
console.log(1);
});
</code></pre>
<p>不出所料,Promise在一秒后被执行,控制台打印"1"。</p>
<p>为了继续链式调用,我们需要在控制台语句后返回第二个Promise,并将其传递给第二个<code>then</code>:</p>
<pre><code class="language-jsx">const promise = new Promise((resolve, reject) =&gt; {
setTimeout(() =&gt; { resolve() }, 1000)
});

promise.then(() =&gt; {
console.log(1);
return new Promise((resolve, reject) =&gt; {
    setTimeout(() =&gt; { resolve() }, 1000)
});
}).then(() =&gt; {
console.log(2);
});
</code></pre>
<p>虽然这个方法可行,但它已经开始变得有点笨重。让我们创建一个返回新Promise的函数,并在特定时间后解析该Promise:</p>
<pre><code class="language-jsx">function sleep(ms) {
return new Promise(resolve =&gt; setTimeout(resolve, ms));
}
</code></pre>
<p>然后,我们可以使用它来扁平化嵌套代码:</p>
<pre><code class="language-jsx">sleep(1000)
.then(() =&gt; {
    console.log(1);
    return sleep(1000);
}).then(() =&gt; {
    console.log(2);
    return sleep(1000);
}).then(() =&gt; {
    console.log(3);
    return sleep(1000);
})
...
</code></pre>
<p>由于 <code>then</code> 方法本身返回一个 Promise 对象,并且我们不会从一个异步操作传递任何值到下一个异步操作,这使得我们能够进一步简化事情:</p>
<pre><code class="language-jsx">sleep(1000)
.then(() =&gt; console.log(1))
.then(() =&gt; sleep(1000))
.then(() =&gt; console.log(2))
.then(() =&gt; sleep(1000))
.then(() =&gt; console.log(3))
...
</code></pre>
<p>这比原始代码要优雅得多。</p>
<p>请注意,如果你想了解更多有关使用JavaScript实现一个sleep函数,你可能对这篇文章感兴趣。</p>
<h3 id="向下传递数据">向下传递数据</h3>
<p>当我们需要执行多个异步操作时,我们可能希望将一个异步调用的结果传递给Promise链中的下一个<code>then</code>,这样我们就可以对该数据进行处理。</p>
<p>例如,我们可能想要获取 GitHub 仓库的贡献者列表,然后使用该信息获取第一位贡献者的姓名:</p>
<pre><code class="language-jsx">fetch('&lt;https://api.github.com/repos/eslint/eslint/contributors&gt;')
.then(res =&gt; res.json())
.then(json =&gt; {
    const firstContributor = json.login;
    return fetch(`https://api.github.com/users/${firstContributor}`)
})
.then(res =&gt; res.json())
.then(json =&gt; console.log(`The first contributor to ESLint was ${json.name}`));

// The first contributor to ESLint was Nicholas C. Zakas
</code></pre>
<p>正如我们看到的,通过返回从第二个 <code>fetch</code> 调用返回的Promise,服务器的响应 (<code>res</code>) 在下面的 <code>then</code> 中可用。</p>
<h2 id="promise错误处理">Promise错误处理</h2>
<p>我们已经知道,then函数接收两个回调函数作为参数,并且如果Promise被拒绝,第二个参数会被调用:</p>
<pre><code class="language-jsx">promise.then((data) =&gt; {
console.log('Got data! Promise fulfilled.');
...
}, (error) =&gt; {
console.error('Promise rejected.');
console.error(error.message);
});
</code></pre>
<p>然而,为每个Promise指定错误处理函数是相当繁琐的,尤其是处理Promise链式调用的时候。幸运的是,还有更好的方式。</p>
<h3 id="catch方法">catch方法</h3>
<p>我们还可以使用<code>catch</code>方法,它可以为我们处理错误。当一个Promise在Promise链的任何地方<code>rejected</code>时,控制会跳转到最近的拒绝处理函数中。这非常方便,因为它意味着我们可以在链的末尾添加一个<code>catch</code>,让它来处理发生的任何错误。</p>
<p>让我们以前面的代码为例:</p>
<pre><code class="language-jsx">fetch('&lt;https://api.github.com/repos/eslint/eslint/contributors&gt;')
.then(res =&gt; res.json())
.then(json =&gt; {
    const firstContributor = json.login;
    return fetch(`https://api.github.com/users/${firstContributor}`)
})
.then(res =&gt; res.jsn())
.then(json =&gt; console.log(`The top contributor to ESLint wass ${json.name}`))
.catch(error =&gt; console.log(error));
</code></pre>
<p>注意,除了在代码块的末尾添加错误处理函数之外,我还在第7行将<code>res.json()</code>拼错为<code>res.jsn()</code>。</p>
<p>现在运行代码,会在屏幕上看到下面的输出:</p>
<pre><code class="language-jsx">TypeError: res.jsn is not a function
&lt;anonymous&gt;&lt;http://0.0.0.0:8000/index.js:7&gt;
promise callback*&lt;http://0.0.0.0:8000/index.js:7&gt;

index.js:9:27
</code></pre>
<p>我正在运行的文件名为<code>index.js</code>。第7行包含错误,第9行是捕获错误的<code>catch</code>块。</p>
<h3 id="finally方法">finally方法</h3>
<p><code>Promise.finally</code>方法在Promise <code>settled</code>后运行,也就是<code>resolved</code>或者<code>rejected</code>。与<code>catch</code>一样,该方法有助于防止代码重复,并且在执行清理任务时非常有用,例如关闭数据库连接或从UI中移除加载动画。</p>
<p>下面是一个使用我们之前代码的示例:</p>
<pre><code class="language-jsx">function getFirstContributor(org, repo) {
showLoadingSpinner();
fetch(`https://api.github.com/repos/${org}/${repo}/contributors`)
.then(res =&gt; res.json())
.then(json =&gt; {
    const firstContributor = json.login;
    return fetch(`https://api.github.com/users/${firstContributor}`)
})
.then(res =&gt; res.json())
.then(json =&gt; console.log(`The first contributor to ${repo} was ${json.name}`))
.catch(error =&gt; console.log(error))
.finally(() =&gt; hideLoadingSpinner());
};

getFirstContributor('facebook', 'react');
</code></pre>
<p>它不接收任何参数并返回一个Promise,因此我们可以在它的返回值上链式调用更多的<code>then</code>、<code>catch</code>和<code>finally</code>调用。</p>
<h2 id="更多promise方法">更多Promise方法</h2>
<p>到此为止,我们已经对JavaScript Promise有了一个很好的基本了解,但在结束之前,我们需要注意各种Promise实用方法。</p>
<h3 id="promiseall">Promise.all()</h3>
<p>在前面的示例中,我们需要在第一个 Ajax 调用完成后才能进行第二个 Ajax 调用。与此不同的是,有时我们会有一堆完全不相互依赖的异步操作。这时就需要使用 <code>Promise.all</code>。</p>
<p>该方法接收一个Promise数组,等待所有Promise <code>resolved</code>或其中任何一个Promise <code>rejected</code>。如果所有的Promise都成功<code>resolved</code>,<code>all</code>实现一个数组,该数组包含各个Promise的履行值:</p>
<pre><code class="language-jsx">Promise.all([
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; resolve(1), 0)),
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; resolve(2), 1500)),
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; resolve(3), 3000)),
])
.then(values =&gt; console.log(values))
.catch(err =&gt; console.error(err));
</code></pre>
<p>上述代码会在三秒后在控制台打印<code></code>。</p>
<p>然而,如果任何Promise <code>rejected</code>,<code>all</code>将拒绝该Promise的值,而不会考虑任何其他Promise。</p>
<h3 id="promiseallsettled">Promise.allSettled()</h3>
<p>不像<code>all</code>,<code>Promise.allSettled</code> 将等待传递给它的每一个Promise的实现或拒绝。如果一个Promise被拒绝,它不会停止执行:</p>
<pre><code class="language-jsx">Promise.allSettled([
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; resolve(1), 0)),
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; reject(2), 1500)),
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; resolve(3), 3000)),
])
.then(values =&gt; console.log(values))
.catch(err =&gt; console.error(err));
</code></pre>
<p>这将返回状态和值(如果<code>fulfilled</code>)或者原因(如果<code>rejected</code>)的列表:</p>
<pre><code class="language-jsx">[
{ status: "fulfilled", value: 1 },
{ status: "rejected", reason: 2 },
{ status: "fulfilled", value: 3 },
]
</code></pre>
<h3 id="promiseany">Promise.any()</h3>
<p><code>Promise.any()</code>返回第一个状态为<code>fulfilled</code>的Promise的值。如果有任何Promise <code>rejected</code>,都会被忽略:</p>
<pre><code class="language-jsx">Promise.any([
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; reject(1), 0)),
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; resolve(2), 1500)),
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; resolve(3), 3000)),
])
.then(values =&gt; console.log(values))
.catch(err =&gt; console.error(err));
</code></pre>
<p>在1.5秒后,控制台会打印"2"。</p>
<h3 id="promiserace">Promise.race()</h3>
<p><code>Promise.race</code>也接收一个Promise数组,并(像上面列出的其他方法一样)返回一个新的Promise。只要它接收到的一个Promise实现或者拒绝,<code>race</code>本身就会使用刚刚<code>settled</code>的Promise的值或原因来实现或拒绝:</p>
<pre><code class="language-jsx">Promise.race([
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; reject('Rejected with 1'), 0)),
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; resolve(2), 1500)),
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; resolve(3), 3000)),
])
.then(values =&gt; console.log(values))
.catch(err =&gt; console.error(err));
</code></pre>
<p>这会在控制台打印”Rejected with 1”,因为数组中的第一个Promise会被立即拒绝,并且拒绝会被我们的<code>catch</code>块捕获。</p>
<p>我们可以这么改:</p>
<pre><code class="language-jsx">Promise.race([
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; resolve('Resolved with 1'), 0)),
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; resolve(2), 1500)),
new Promise((resolve, reject) =&gt; setTimeout(() =&gt; resolve(3), 3000)),
])
.then(values =&gt; console.log(values))
.catch(err =&gt; console.error(err));
</code></pre>
<p>这会在控制台打印”Resolved with 1”。</p>
<p>这两个例子中,其他两个Promise都会被忽略。</p>
<h2 id="应该使用哪个">应该使用哪个</h2>
<p>到目前为止,我们已经了解了回调和Promise,但值得一提的还有较新的<code>async ... await</code>语法。虽然它实际上只是Promise之上的语法糖,但在很多情况下,它可以让基于Promise的代码更容易阅读和理解。</p>
<p>例如,我们可以这样重写之前的代码:</p>
<pre><code class="language-jsx">async function getFirstContributor(org, repo) {
showLoadingSpinner();
try {
    const res1 = awaitfetch(`https://apiy.github.com/repos/${org}/${repo}/contributors`);
    const contributors = await res1.json();
    const firstContributor = contributors.login;
    const res2 = await fetch(`https://api.github.com/users/${firstContributor}`)
    const details = await res2.json();
    console.log(`The first contributor to ${repo} was ${details.name}`);
} catch (error) {
    console.error(error)
} finally {
    hideLoadingSpinner();
}
}

getFirstContributor('facebook', 'react');
</code></pre>
<p>正如我们所看到的,我们使用<code>try ... catch</code>语法来处理错误,并且我们可以在<code>finally</code>块中进行任何修整。</p>
<p>我发现上述代码比基于Promise的版本更容易解析。不过,我鼓励你熟悉<code>async ... await</code>语法,看看哪种最适合你。</p>
<h2 id="总结">总结</h2>
<p>在本文中,我们了解了如何创建和使用 JavaScript Promise。我们学习了如何创建一个Promise链,并将数据从一个异步操作传递到下一个异步操作。我们还研究了错误处理以及各种Promise实用方法。</p>
<p>如上所述,下一步应该是开始学习<code>async ...await</code>,加深对JavaScript程序内部流程控制的理解。</p>
<p>以上就是本文的全部内容。如果对你有所帮助,欢迎点赞、收藏、转发~</p><br><br>
来源:https://www.cnblogs.com/chuckQu/p/17618046.html
頁: [1]
查看完整版本: 重学JavaScript Promise API