异步、回调和Promise
JS 异步编程模型
同步、异步、轮询和回调
如果能直接拿到结果
- 那就是同步
- 比如你在医院挂号,你拿到号才会离开窗口
- 同步任务可能消耗 10 毫秒,也可能需要 3 秒
- 总之不拿到结果你是不会离开的
如果不能直接拿到结果
- 那就是异步
- 比如你在餐厅等位置,你拿到号可以去逛街
- 什么时候才能真正吃饭呢?
- 你可以每 10 分钟去餐厅问一下(轮询)
- 你也可以扫码用微信接收通知(回调)
异步举例
以 AJAX 为例
request.send()
之后,并不能直接得到response
不信
console.log(request.response)
试试- 必须等到
readyState
变为4
后,浏览器回头调用request.onreadystatechange
函数 - 我们才能得到
request.response
- 这跟餐厅给你发送微信提醒的过程是类似的
回调 callback
- 你写给自己用的函数,不是回调
- 你写给别人用的函数,就是回调
request.onreadystatechange
就是我写给浏览器调用的- 意思就是你(浏览器)回头调用一下这个函数
在中文里,’回头’ 也有 ‘将来’ 的意思, 如 ‘我回头请你吃饭’
回调举例
把函数 1 给另一个函数 2
function f1() {}
function f2(fn) {
fn();
}
f2(f1);
分析
- Q: 我调用
f1
没有?A: 没有调用 - Q: 我把
f1
传给f2
(别人)了没有?A: 传了 - Q:
f2
调用f1
了没有?A:f2
调用了f1
- Q: 那么,
f1
是不是我写给f2
调用的函数?A: 是
所以,
f1
是回调
抬杠
- Q: 如果
f2
没有调用f1
呢? - A:
f2
有病啊?它不调用f1
那它为什么要接收fn
参数
异步和回调的关系
关联
- 异步任务需要在得到结果时通知 JS 来拿结果
- 怎么通知呢?
- 可以让 JS 留一个函数地址(电话号码)给浏览器
- 异步任务完成时浏览器调用该函数地址即可(拨打电话)
- 同时把结果作为参数传给该函数(电话里说可以来吃了)
- 这个函数是我写给浏览器调用的,所以是回调函数
回调就是我们先把一个函数留给它,等它完成了它就会调这个函数,调的时候它会把结果放到函数的参数里,这样就可以实现异步的通知。
区别
- 异步任务需要用到回调函数来通知结果
但它不一定非要用回调函数,它也可以用轮询
- 回调函数不一定只用在异步任务里,回调可以用到同步任务里
array.forEach(n=>console.log(n))
就是同步回调
异步不一定要用到回调,回调也不一定要用到异步里。异步和回调他们两个只是合作关系。
判断同步异步
判断一个函数的返回值是否处于
setTimeout
AJAX
(即XMLHttpRequest
)AddEventListener
如果一个函数的返回值处于这三个东西内部,那么这个函数就是异步函数
如果还有其他 API 是异步的,之后的博客会另行说明
等一下
Q: 我听说 AJAX 可以设置为同步的
A: 傻*前端才把 AJAX 设置为同步的,因为这样做会使请求期间页面卡住
总结
- 异步任务不能拿到结果
- 于是我们传一个回调给异步任务
- 异步任务完成时调用回调
- 调用的时候把结果作为参数
Promise(前端解决异步问题的统一方案)
进一步思考: 如果异步任务有两个结果成功或失败,怎么办?
两个结果
- 方法一: 回调接收两个参数
fs.readFile("./1.txt", (error, data) => {
if (error) {
console.log("失败");
return;
}
console.log(data.toString()); // 成功
});
- 方法二: 搞两个回调
ajax(
"get",
"/1.json",
(data) => {},
(error) => {}
);
// 前面函数是成功回调,后面函数是失败回调
ajax("get", "/1.json", {
success: () => {},
fail: () => {},
});
// 接收一个对象,对象有两个key表示成功和失败
这些方法的不足
- 不规范,成功回调和失败回调的名称五花八门,比如 success+error、success+fail、done+fail
- 容易出现回调地狱(callback hell),代码变得看不懂
- 很难进行错误处理
A guide to writing asynchronous JavaScript programs: Callback Hell
怎么解决回调问题
- 1976 年,Daniel P.Friedman 和 David Wise 提出 Promise 思想
- 后人基于此发明了 Future、Delay、Deferred 等
- 前端结合 Promise 和 JS,制定了Promise/A+ 规范
该规范详细描述了 Promise 的原理和使用方法
以 AJAX 的封装为例
来解释 Promise 的用法
ajax = (method, url, options) => {
const { success, fail } = options; // 析构赋值
// const success = options.success
// const fail = options.fail
const request = new XMLHttpRequest();
request.open(method, url);
request.onreadystatechange = () => {
if (request.readyState === 4) {
//成功就调用success,失败就调用fail
if (request.status < 400) {
success.call(null, request.response);
} else if (request.status >= 400) {
fail.call(null, request, request.status);
}
}
};
request.send();
};
ajax("get", "/xxx", {
success(response) {},
fail: (request, status) => {},
}); // 左边是 function 缩写,右边是箭头函数
Promise 说这代码太傻了
因为这代码不规范、容易出现回调地狱和很难进行错误处理。所以太傻了。
让我们改成 Promise 写法:
// 先改一下调用的姿势
ajax("get", "/xxx", {
success(response) {},
fail: (request, status) => {},
});
// 上面用到了两个回调,还是用了success和fail
// 改成Promise写法
ajax("get", "/xxx").then(
(response) => {},
(request) => {}
);
虽然也是回调,但是不需要记 success 和 fail 了,then 的第一个参数就是 success,then 的第二个参数就是 fail
Q: 请问
ajax()
返回了个啥?A: 返回了一个含有 .then() 方法的对象
那么如何得到这个含有 .then() 的对象呢?
那就要改造 ajax 的源码了:
// 与之前的版本相比,改动的三行已经标出。
ajax = (method, url, options) => {
// 改动
return new Promise((resolve, reject) => {
const { success, fail } = options;
const request = new XMLHttpRequest();
request.open(method, url);
request.onreadystatechange = () => {
if (request.readyState === 4) {
//成功就调用success,失败就调用fail
if (request.status < 400) {
resolve.call(null, request.response); // 改动
} else if (request.status >= 400) {
reject.call(null, request); // 改动
}
}
};
request.send();
});
};
把这个形式背下来:
return new Promise((resolve,reject)=>{})
小结
第一步
return new Promise((resolve,reject)=>{ ... })
- 任务成功则调用
resolve(result)
- 任务失败则调用
reject(error)
resolve
和reject
会再去调用成功和失败函数
第二步
- 使用
.then(success, fail)
传入成功和失败函数
Promise 的其他用法: 使用 Promise | MDN
我们封装的 ajax 的缺点
- post 无法上传数据
request.send(这里可以上传数据)
- 不能设置请求头
request.setRequestHeader(key, value)
- 解决办法
花时间把
ajax
写到完美(不推荐)使用
jQuery.ajax
(这个可以)使用
axios
(这个库比jQuery
逼格高)
jQuery.ajax
- 已经非常完美
- 封装优点
支持更多形式的参数,Promise 等超多功能
Q: 需要掌握
jQuery.ajax
吗?A: 不用,时代变了,现在用
axios
就好了:)
axios
- 目前最新的 AJAX 库(显然它抄袭了 jQuery 的封装思路)
axios
的用法: Axios Cheat Sheet
- 代码示例
axios.get("/5.json").then((response) => console.log(response));
axios 高级用法
JSON 自动处理
axios
如果发现响应的Content-Type
是json
,就会自动调用JSON.parse
所以说正确设置
Content-Type
是好习惯
请求拦截器
- 你可以在所有请求里加些东西,比如加查询参数
响应拦截器
- 你可以在所有响应里加些东西,甚至改内容
可以生成不同实例(对象)
- 不同的实例可以设置不同的配置,用于复杂场景
扩展阅读: