Javascript 异步编程
主要包含以下几个内容:
- 同步和异步的概念
- 实现异步的几种方式
- 回调函数
- promise
- 生成器和迭代器
- ES7 async 和 await
1. 同步和异步的概念
在计算机中,所谓同步,就是指同一时间只能做一件事情。比如看下面的代码:
console.log("Hello");
console.log("world");
console.log("F69");
同步任务的特点在于,一次执行一个任务,后面的任务只能排队,只有等待前面的任务结束之后,才能执行下一个同步任务。
所谓异步,有两种解决方案:1. 多线程 2. 单线程非阻塞
所谓单线程非阻塞,就是指将耗时间的任务(异步任务)交给其他模块来进行处理,处理完成之后,再由js的单线程进行处理。
整个js引擎的同步异步任务执行顺序如下:
- 执行同步任务
- 如果遇到异步任务,交给异步模块
- 如果异步处理模块已经处理异步任务,扔到任务队列里面排队,因为必须要等到所有的同步任务执行完之后,才会执行异步任务的结果
- 所有的同步任务执行完成,从任务队列里面获取异步的执行结果
常见的异步任务有:
setTimeout / setInterval、文件的读写(I/O 操作),网络请求
2. 实现异步的几种方式
有些时候,我们需要等待某一个异步操作结束之后,再执行下一个步骤。
举例如下:每隔2秒输出一个数,下面的代码是不正确的:
setTimeout(function(){
console.log(1);
},2000);
setTimeout(function(){
console.log(2);
},2000);
setTimeout(function(){
console.log(3);
},2000);
因为上面的3个setTimeout都是异步代码,都会被放到异步处理模块,而且由于时间都是2秒,几乎同时完成,然后被放入到任务队列。
如果要解决上面的问题,可以采取以下的几种方案:
- 回调函数
- promise
- 生成器
- async await
首先我们来看一下回调函数的处理方式,代码如下:
setTimeout(function () {
console.log(1);
setTimeout(function () {
console.log(2);
setTimeout(function () {
console.log(3);
}, 2000);
}, 2000);
}, 2000);
读取文件的例子:
const fs = require('fs'); // 引入 fs 模块
fs.readFile('./file1.txt','utf8',function(err,data){
if(err) throw err;
console.log(data);
fs.readFile('./file2.txt','utf8',function(err,data){
if(err) throw err;
console.log(data);
fs.readFile('./file3.txt','utf8',function(err,data){
if(err) throw err;
console.log(data);
});
});
});
使用回调来获取网络请求的示例:
let classID, teacherId;
let xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
xhr.open("get", "./stu.json");
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) { // 服务器正常的响应数据回来
let arr = JSON.parse(xhr.responseText).student;
for (let i = 0; i < arr.length; i++) {
if (arr[i].name === "韩梅梅") {
classID = arr[i].classId;
}
}
// 继续发送请求
xhr.open("get", "./classes.json");
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) { // 服务器正常的响应数据回来
let arr = JSON.parse(xhr.responseText).classes;
for (let i = 0; i < arr.length; i++) {
if (arr[i].id === classID) {
teacherId = arr[i].teacherId;
}
}
// 继续发送请求
xhr.open("get", "./teacher.json");
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) { // 服务器正常的响应数据回来
let arr = JSON.parse(xhr.responseText).teachers;
for (let i = 0; i < arr.length; i++) {
if (arr[i].id === teacherId) {
console.log(arr[i].name)
}
}
// 继续发送请求
}
}
}
}
}
}
但是,使用回调有一个最大的问题,就是会带来回调地狱(callback hell)
所谓回调地狱,就是指代码在可读性上面非常的差,代码逐渐的在往右边延伸。例如:
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);
setTimeout(function () {
console.log(6);
setTimeout(function () {
console.log(7);
setTimeout(function () {
console.log(8);
}, 2000);
}, 2000);
}, 2000);
}, 2000);
}, 2000);
}, 2000);
}, 2000);
}, 2000);
随着 ES6 的到来,出现了解决方案,promise。