文章目录
同步JavaScript
通常情况下,程序代码在运行的时候,同一时刻只会执行一个事件。而在程序代码中,如果一个函数依赖于另一个函数的运行结果,那么这个函数需要等待另一个函数执行完毕并返回了结果才可以继续运行并结束。
这种情况在计算机系统上并没有充分利用电脑的计算能力及多核CPU的优势。应该是电脑在一个CPU内核上完成任务,而用户可以在另一个CPU内核上进行其他的工作,而这就是异步编程的出发点。
对于浏览器,一个web应用进行了密集运算,还没把控制权返回给浏览器,那么整个浏览器就会卡住,无法进行用户交互,这种情况被称作为阻塞。
当浏览器处于阻塞状态时,浏览器无法处理用户的输入,进行其他的任务等,直到web应用结束,交回处理器的控制。
而浏览器为什么会有阻塞的情况,因为JavaScript是一种同步的、阻塞的、单线程的语言,但是web浏览器定义了函数和API来允许我们党某些事件发生时不是按照同步方式,而是异步的调用函数。这意味着代码可以同时做几件事情,而不需要停止或阻塞主线程。
异步JavaScript
由于同步JS会造成的种种问题,许多的Web API现在都使用异步的代码来运行,特别是那些需要外部设备资源的代码,例如:从网上获取文件、访问数据库并从中获取数据、从网络摄像头获取视频流或者向VR设备发送图像等等。
而为什么难以使用异步代码?举个栗子,当你从服务器请求一个较大数据的对象,通常是不可能立即得到。而且因为下载的时间未知,代码执行的过程中可能会报错,或许是请求的过程因为网络问题报错,也有可能因为获取不到,后续的代码执行因为无数据无法继续等等的一系列原因。
ES6之前的异步编程
异步callbacks
异步callbacks就是那些作为参数传入在后台运行的回调函数的函数。当后台函数结束运行时,这个函数会让你知道它的工作已经完成或者让你知道一些意外发生导致它执行失败。虽然用回调函数已经有点过时了,但是你仍旧可以在一大部分比较老但是仍在使用的API中看到它。
例子
addEventListener()这个事件监听函数就是一个典型的的使用回调函数的API。它第二个参数就是一个回调函数。当我们把回调函数作为一个参数传递给另一个函数时,回调函数并没有立刻执行,而是事件监听函数监听到对应的事件触发后在被传入的函数中调用回调函数。
const btn = document.querySelector("button");
btn.addEventListener("click", () => {
console.log("click btn.");
});
//点击绑定的按钮后
//Output: click btn,
用XMLHttpRequest API加载资源。
function loadAssert(url, type, callback){
//创建XHR对象
let xhr = new XMLHttpRequest();
//设置请求方式为Get
xhr.open('Get', url);
//定义返回类型
xhr.responseType = type;
//当请求成功完成时调用
xhr.onload = function(){
callback(xhr.response);
};
//发送请求
xhr.send();
}
function displayImage(blob){
//根据blob生成objectURL
let objectURL = URL.createObjectURL(blob);
//生成一个image元素
let image = document.createElement('img');
//把objectURL作为image的源地址
image.src = objectURL;
//加载节点显示图片,将图片在页面渲染出来
document.body.appendChild(image);
}
loadAsset('1.jpg', 'blob', displayImage);
回调函数不止允许你控制函数的执行顺序和它们之间数据的传递顺序,同样允许根据环境的不同将数据传递给不同的参数。所以你可以对下载返回的响应采取不同的方法进行处理,例如processJSON(),displayText()等。
不过要注意的是不是所有回调函数都是异步的,有些回调函数是同步的,例如Array.prototype.forEach()等参数是同步运行的。这个函数不需要等待任何事情,所以立即执行。
Promises
Promises是现在在很多Web API上实现异步代码的新方式。
比如与XMLHttpRequest相似,但是更高效的fetch()。
fetch("product.json",{
mode: "no-cors"
})
.then((response) => {
return response.json();
}).then( (json) => {
products = json;
initialize();
}).catch((err) => {
console.log('Fetch problem: ' + err.message);
});
PS:直接在控制台运行会报错,需要对fetch函数的第一个参数进行修改,将其改变为一个网络请求来对目标进行获取。如果是本地文件的话,你可以选择import…from或者require。
这里的fetch()只需要一个资源URL的参数,然后返回一个promise。它代表着一个中间。promise是表示异步操作完成或失败的对象。相当于浏览器在告诉你尽快给你答复的promise。此时fetch()并不知道它的结果是成功还是失败,目前只是一个未定的结果,等待着浏览器返回的结果。然后有三个代码块链链接到fetch()的末尾:
- 两个then():表示目标函数(fetch() )执行完毕后执行参数中的回调函数。若前一个操作成功,并且每个回调都接收前一个成功操作的结果作为输入,因此可以继续对它进行其他操作。每个.then()块返回另一个promise,这意味着可以将多个.then()块链接到另一个块上,这样就可以依次执行多个异步操作。
- catch():提供一个错误对象,可以用来报告发生的错误类型。但是同步的try…catch不能与promise一起工作。 如果其中任何一个then()代码块失败,那么会跳到结尾的catch()并运行他抛出错误。
事件队列
像promise这样的异步操作会被放入事件队列中。事件队列会在完成处理后运行,这样它们就不会阻止后续JavaScript代码的运行。排队操作将尽快向JavaScript环境返回结果。
Promises VS callbacks
两者本质上都是一个返回的对象,然后可以将回调函数附