在 JavaScript 这种单线程事件循环模型中,同步操作与异步操作是代码依赖的核心机制。因为是单线程机制,所有任务都需要排队一个接一个地执行,为了不浪费CPU资源,避免因为一个处于长时间等待中的任务(比如setTimeout方法)而影响后面任务的执行,可以先不管setTimeout方法,将等待中的任务挂到“任务队列”中,然后优先运行后面的任务,等到setTimeout方法返回了结果再执行“任务队列”中挂起的任务。
setTimeout(()=>{
console.log(1);
},1000);
for(let i = 0;i < 1000;i++){
}
console.log(2); // 2 1
以该代码段为例,第一句为setTimeout方法,延时1000ms以后执行“console.log(1)”;第二句是一个for循环语句;第三句执行“console.log(2)”。“任务栈”在执行这三句代码时,由于第一句延时执行,所以将这个任务放进“计时线程”中,等待1000ms后将该任务放进“任务队列”中,任务栈在将第一个任务放进计时线程后立即执行第二个任务,接着执行第三个任务,执行完后从任务队列中取出第一个任务执行,所以该段代码执行的结果应该先是2,再是1。
同步行为对应内存中顺序执行的处理器指令,每条指令都会严格按照他们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地的信息。
异步行为类似于系统中断,即当前进程外部的实体就可以触发代码执行。
事实上,程序中现在运行的部分和将来运行的部分之间的关系就是异步编程的核心。
以往的异步编程模式
JavaScript 程序几乎都是由多个块构成的,这些块中只有一个是现在执行,其余的则会在将来执行。而问题是:程序中将来执行的部分并不一定在现在运行的部分执行完之后就立即执行,也就是说,现在无法完成的任务将会异步完成,因此并不会出现人们希望出现的阻塞行为。
在早期的 JavaScript 中只支持定义回调函数来表明异步操作完成。串联多个异步操作是一个很常见的问题,通常会使用深度嵌套的回调函数(俗称“回调地狱”)来解决,外部回调函数异步执行的结果嵌套的回调函数执行的条件。
setTimeout(function () {
console.log("First");
setTimeout(function () {
console.log("Second");
setTimeout(function () {
console.log("Third");
}, 3000);
}, 4000);
}, 1000); //回调地狱
显然,随着代码越来越复杂,回调策略是不具备扩展性的,嵌套回调的代码在后期维护起来就是噩梦!!!
期约基础
Promise 是异步编程的一种解决方案,它支持链式调用,可以解决回调地狱问题。
我们可以将 Promise 理解为一个容器,里面存放着某个将来才执行的事件的结果。从语法上看,Promise 是 ES6 新增的一种引用数据类型(对象),可以通过 new 操作符来实例化,通过它可以获取异步操作的消息。创建新期约时需要传入执行器函数(excutor)作为参数,excutor会在Promise 内部立即同步调用,不会进入任务队列。期约是一个有状态的对象,可能处于如下3种状态(PromiseState)之一:
- 待定 (pending)?
- 兑现?/ 解决 (fulfilled / resolved)
- 拒绝 (rejected)
待定是期约的最初始状态。在待定状态下,期约可以落定为代表成功的兑现状态,或者代表失败的拒绝状态。无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝,期约的状态就不再改变。
期约的状态是私有的,因此要控制期约状态只能通过内部操作在期约的执行器函数中完成。执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。其中,控制期约状态的转换是通过调用它的两个函数参数实现的。这两个函数参数通常都命名为resolve()和reject()。调用resolve()会把状态切换为兑现,调用reject()会把状态切换为拒绝。
下面代码在第一段调用了 Pronmise 构造函数创建了一个 Promise 实例 prom,第二段调用了Promise 实例的.then()方法,为实例 prom 注册两种状态回调函数,当 prom 的状态为 resolved 时,会触发第一个函数执行;当 prom 的状态为 rejected 时,会触发第二个函数执行。
let prom = new Promise((resolve,reject)=>{
...;
if(异步操作成功){
resolve(n); //prom的状态变为resolved,pending=>resolved,将成功的结果值n作为
//resolve函数的参数(该参数可选)
}else{
reject(n); //prom的状态变为rejected,pending=>rejected,将失败的原因值n作为
//reject函数的参数(该参数可选)
}
});
prom.then((value)=>{ //value为形参(该参数可选)
...; //prom的状态为resolved,进入这里,处理成功的结果值
},
(reason)=>{ //reason为形参(该参数可选)
...; //prom的状态为rejected,进入这里,处理失败的原因值
}
);
上面这样构造 promise 实例,然后调用 .then.then.then 的编写代码方式,就是 promise。其主要内容就是:
- 将异步过程转化成promise对象
- 对象有3种状态
- 通过.then注册状态的回调
- 已完成的状态能触发回调
采用这种方式来处理编程中的异步任务,就是在使用promise了。所以promise就是一种异步编程模式。
?
Promise的三个缺点:
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消
- 如果不设置回调函数,Promise内部抛出的错误,不会反映到外部
- 当处于pending状态时,无法得知目前进展到哪一个阶段,是刚刚开始还是即将完成
?
现在,我们用 Promise 将文章开头的那个“回调地狱”改写一下:
setTimeout(function () {
console.log("First");
setTimeout(function () {
console.log("Second");
setTimeout(function () {
console.log("Third");
}, 3000);
}, 4000);
}, 1000);
/*-------------------------------------------------------------------*/
new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("First");
resolve();
}, 1000);
}).then(function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
console.log("Second");
resolve();
}, 4000);
});
}).then(function () {
setTimeout(function () {
console.log("Third");
}, 3000);
});
Promise 实践练习
1.fs读取文件
//fs模块可以对计算机的硬盘进行读写操作,使用前需要先引入fs模块
//需求:读取resource文件夹下 content.txt 文件中的内容
/*
文件结构
|--resource
|----content.txt
|--index.js
*/
//回调函数形式
//引入fs模块
const fs = require('fs');
fs.readFile('./resource/content.txt',(err,data)=>{ //err是出现错误时的参数,data是读取的结果
//如果出错,则抛出错误
if(err) throw err;
//如果没有错误,则输出文件内容
console.log(data.toString());
});
//使用 Promise 形式进行封装
let p = new Promise((resolve,reject)=>{
fs.readFile('./resource/content.txt',(err,data)=>{
//如果出错
if(err) reject(err);
//如果成功
resolve(data);
});
});
//调用then
p.then(value=>{
console.log(value.toString());
},reason=>{
console.log(value);
})
我们还可以将其封装为一个mineReadFile函数,便于复用:
//封装为一个函数,参数:path:文件路径;返回:promise 对象
function mineReadFile(path){
return new Promise((resolve,reject)=>{
require('fs').readFile(path,(err,data)=>{
if(err) reject(err);
resolve(data);
});
});
}
mineReadFile('./resource/content.txt')
.then(value=>{
console.log(value.toString());
},reason=>{
console.log(reason);
});
2.AJAX请求
//需求:点击按钮向指定接口地址(http://127.0.0.1:8000/server)发送Ajax请求
//获取元素对象
const btn = document.getElementsByTagName("button")[0];
btn.addEventListener('click',()=>{
//创建 Promise
const p = new Promise((resolve,reject)=>{
//1.创建对象
const xhr = new XMLHttpRequest();
//2.初始化
xhr.open('GET','http://127.0.0.1:8000/server');
//3.发送
xhr.send();
//4.绑定事件,处理响应结果
xhr.onreadystatechange = ()=>{
//判断准备状态
// 0 请求未初始化(此时还没有调用open)
// 1 服务器连接已建立,已经发送请求开始监听
// 2 请求已接收,已经收到服务器返回的内容
// 3 请求处理中,解析服务器响应内容
// 4 请求已完成,且响应就绪
if(xhr.readyState === 4){
//判断响应状态码 200-300之间都算成功
if(xhr.status >= 200 && xhr.status <=300){
resolve(xhr.response); //控制台输出响应体 --输出结果:Hello Ajax GET
}else{
reject(xhr.status); //控制台输出响应状态码
}
}
};
});
//调用then方法
p.then(value=>{
console.log(value);
},reason=>{
console.log(reason);
})
});
//server.js <服务端>
//安装node.js,引入Express
const express = require('express');
//创建应用对象
const app = express();
//创建路由规则 requset是对请求报文的封装,response是对响应报文的封装
app.all('/server',()=>{
//设置响应头,允许跨域
response.setHeader('Access-Control-Allow-Origin','*');
response.setHeader('Access-Control-Allow-All','*');
//
response.send('Hello Ajax-Get!');
});
//监听端口启动服务
app.listen(8000,()=>{
console.log("服务已经启动,8000端口监听中...");
});