0%

JS异步(篇一:callbacks和promise)

本想挖个小坑结果挖出一个黑洞

问题

​ 最近码代码遇到一点问题,之前请求到后端数据直接做处理,现在想要拿到数据后的返回值再分别做处理,正常调用的话返回值总是空;另外有需求要在对列表数据做操作后重新请求接口刷新列表,效果来看却总是没有刷新,打断点才发现,早在操作接口数据返回之前就已经偷偷做了刷新操作,然而太早的刷新根本达不到想要的效果…暂且用 async/await 解决了问题却留下了一个大坑,先填一部分吧。

异步调用

Ajax:“Asynchronous Javascript And XML”(异步 JavaScript 和 XML)

通过 AJAX,JavaScript 无需等待服务器的响应,而是:

  • 在等待服务器响应时执行其他脚本
  • 当响应就绪后对响应进行处理

由于向外部设备获取资源时,比如从网络获取文件、访问数据库等等,如果使用同步代码,将会在请求过程中一直处于阻塞状态——因为 JavaScript 是单线程,任何时候只能做一件事情, 只有一个主线程,其他的事情都阻塞了,直到前面的操作完成。

好在 web 浏览器定义了函数和 api,允许我们在某些事件发生时异步的调用函数(时间推移、用户通过鼠标的交互、获取网络数据等等)而不需要阻塞主线程。

Instead of immediately returning some result like most functions, functions that use callbacks take some time to produce a result. The word ‘asynchronous’, aka ‘async’ just means ‘takes some time’ or ‘happens in the future, not right now’. Usually callbacks are only used when doing I/O, e.g. downloading things, reading files, talking to databases, etc. –Callback Hell

使用同步的办法来处理异步会遇到一些问题,比如:

1
2
3
var response = fetch("myImage.png");
var blob = response.blob();
// display your image blob in the UI somehow

由于第一句的异步请求(可能)会耗费时间,会导致第二行(可能)执行报错。此时需要保证第二行的调用等到 response 返回才能继续进行。

异步编程的目标,就是让它变得更像同步编程。即是,在异步调用的过程中,可以去执行其他与本次调用函数的 response 无关的事情,但是与 response 有关的任务(或者说整个异步调用任务的第二执行阶段),需要保证在拿到 response 返回值后进行。整个任务看上去就像是同步调用一样。

其中有两个比较重要的概念:

  1. 部分代码需要在异步请求之后调用。
  2. 由于异步请求的出现,代码并不总是从上往下执行的,异步执行的代码块没有阻塞,而是继续执行接下来的代码,所以会有一些跳着执行的情况出现。

在 JavaScript 代码中,你经常会遇到两种异步编程风格:老派 callbacks(回调函数),新派 promise。

callbacks (e.g. ‘call you back later’)

callbacks 其实就是回调函数,它作为参数传递给后台执行的函数,当后台执行的函数结束后,会调用 callbacks 函数,通知你这个函数已经执行完成,可以用 callbacks 执行第二阶段了。

常见形式:

1
2
3
4
5
6
7
8
downloadPhoto("http://xxx.com/xxx", handlePhoto); //下载图片
function handlePhoto(error, response) {
if (error) {
console.log("an error!", error);
} else {
console.log("finished", response);
}
}

这段代码做了三件事:先声名handlePhoto函数;然后调用downloadPhoto函数,并将handlePhoto作为它的回调函数;最后调用handlePhoto函数。

​ 回调函数本身没有问题,但是不恰当的使用会导致回调地狱。即回调函数也是异步执行,回调函数的回调函数也是异步执行…(套娃)

​ 另外,错误处理往往非常重要,Node.js 将 error 作为回调函数的第一个参数,也是作为标准提醒码农不要忘记处理错误。(如果没有错误那么第一个参数就是 null)。不过也有另外一种说法:

一个有趣的问题是,为什么 Node.js 约定,回调函数的第一个参数,必须是错误对象 err(如果没有错误,该参数就是 null)?原因是执行分成两段,在这两段之间抛出的错误,程序无法捕捉,只能当作参数,传入第二段。 —-阮一峰的网络日志

Promise

像薛定谔的猫,本次请求成功或者失败的两种结果还没发生,就返回一个 promise 对象,表示一种中间状态像是浏览器对你说:保证尽快答复,所以叫’promise’。这个 promise 对象会在请求响应后被 resolve,并传回 Response 对象。

例如 fetch:

1
2
3
4
5
6
7
8
9
10
11
fetch("products.json")
.then(function(response) {
return response.json();
})
.then(function(json) {
products = json;
initialize();
})
.catch(function(err) {
console.log("Fetch problem: " + err.message);
});

这里 fetch()需要一个参数(请求地址)返回一个 promise,.then()代码块里对 promise 的 response 进行处理,再返回另一个 promise……其中如果任何一个.then()中的代码块执行失败,就会运行.catch(),提供一个 error 对象来报告其中发生的错误。另外同步的 try/catch 不能与 promise 一起工作,但能与async/await一起工作。

事件队列

像 promise 这样的异步操作会被放入事件队列中,事件队列在主线程完成处理后运行,这样它们就不会阻止后续 JavaScript 代码的运行。排队操作将尽快完成,然后将结果返回到 JavaScript 环境。这也是前文提到的代码不总是从上往下执行的

promise 对比 callbacks

二者很像,如果用套娃式的写法的话,promise 的代码可能更容易阅读一些,但是本质上都是一个返回的对象,promise 可以将回调函数附加到这个对象上,而不用作为参数传递给异步调用函数。

promise 的优点:

  1. 链式的,严格按照规定的顺序调用(由.then()规定)。
  2. 所有的错误都在.catch()被捕捉到,而不是每个 callback 函数中单独处理。

Promise 对象

它是一个代理对象,代理一个值(通常是一次请求的返回值),这个值在对象创建时是未知的。这种办法可以让异步方法像同步方法一样返回值,只是返回一个代表未来出现的结果的 promise 对象。

因为 Promise.prototype.thenPromise.prototype.catch 方法返回 promise 对象, 所以它们可以被链式调用。

如果一个 promise 对象处在 fulfilled 或 rejected 状态而不是 pending 状态,那么它也可以被称为settled状态。或者用resolved来表示 promise 对象处于 settled 状态。

异步代码的本质(重点来了)

当不了解代码执行顺序或者将异步代码当做同步代码写时会遇到的问题。用一段示例来展示(源代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
console.log("start");
let image;

fetch("coffee.jpg")
.then(response => {
console.log("It worked :)");
return response.blob();
})
.then(myBlob => {
let objectURL = URL.createObjectURL(myBlob);
image = document.createElement("img");
image.src = objectURL;
document.body.appendChild(image);
})
.catch(error => {
console.log(
"There has been a problem with your fetch operation: " + error.message
);
});

console.log("All done!");

首先控制台打印start,然后执行异步代码块 fetch(),这里没有阻塞,将相关代码 promise 之后就继续执行了,然后到达最后一行打印All done

在执行 fetch()完成运行,拿到返回结果给了.then()代码块后,才打印了了it worked然后将返回结果给下一个.then()。所以最后控制台打印顺序是:

  1. start
  2. All done!
  3. It worked :)

到这里,问题出现的原因算是明白了,之后找时间更新目前来讲更好地解决办法。