有关异步和 Promise 的相关知识(JS 异步编程模型,这节内容特别硬核,你品,你细品),内容包括异步和同步的区别、回调、异步和回调的关系、举例判断同步和异步、异步任务有两个结果、怎么解决回调问题 - Promise、以 AJAX 的封装为例来解释 Promise 的用法、jQuery.ajax 和 axios、总结。
一、异步和同步的区别
1. 区别
如果能直接拿到结果,那就是同步。比如你在医院挂号,你拿到号才会离开窗口,同步任务可能消耗 10 毫秒,也可能需要 3 秒,总之不拿到结果你是不会离开的。
如果不能直接拿到结果,那就是异步。比如你在餐厅门口等位,你拿到号可以去逛街,什么时候才能真正吃饭呢?,你可以每 10 分钟去餐厅问一下(轮调),你也可以扫码用微信接收通知(回调)。
2. 异步举例 - 以 AJAX 为例
- request.sent() 之后,并不能直接得到 response
- 必须等到 readyState 变为 4 后,浏览器回头调用 request.onreadystatechange 函数
- 我们才能得到 request.response
- 这跟餐厅给你发送微信提醒的过程是类似的。
回调 callback
- 你写给自己用的函数,不是回调
- 你写给别人用的函数,就是回调
- request.onreadystatechange 就是我写给浏览器调用的
- 意思就是你(浏览器)回头调一下这个函数
- 在中文里,「回头」也有「将来」的意思,如「我回头请你吃饭」
二、回调
写了却不调用,给别人调用的函数,就是回调,「回头你调用一下呗」大家意会
1. 回调举例
// 把函数 1 给另一个函数 2
function f1(){}
function f2(){
fn()
}
f2(f1)
复制代码
- 我调用 f1 没有?答:没有调用
- 我把 f1 传给 f2(别人)了没有?答:传了
- f2 调用 f1 了没有?答:f2 调用了 f1
- 那么,f1 是不是我写给 f2 调用的函数?答:是
- 所以,f1 是回调。
2. 抬杠 1
function f1(){}
function f2(fn){
// fn()
}
f2(f1)
复制代码
如果 f2 没有调用 f1 呢?
- f2 有病啊?它不调用 f1,那它为什么要接受 fn 参数
3. 抬杠 2
function f1(){}
function f2(fn){
fn()
}
fn('字符串')
复制代码
如果我传给 f2 的参数不是函数呢?
- 你有病啊?用函数之前不看看函数的文档吗?
- 会报错:fn 不是一个函数。看到报错你不就知错了?
4. 抬杠 3
function f1(x){
console.log(x)
}
function f2(fn){
fn('你好')
}
f2(f1)
复制代码
f1 怎么会有一个 x 参数?
- fn('你好') 中的 fn 就是 f1 对吧
- fn('你好') 中的 ‘你好’ 会被赋值给参数 x 对吧
- 所以 x 就是 '你好' 啊!
- x 可以改成任意其他名字,x 表示第一个参数而已
三、异步和回调的关系
1. 回调和异步是什么
回调就是我把一个函数传给你,我可以直接传给你,也可以把它传给你全局对象上面,比如说 request 上面,然后你去 request 上面找。或者我把这个函数放在你手里
异步就是我不能马上得到结果给你,你要等一会
2. 关联
- 异步任务需要在得到结果时通知 JS 来拿结果
- 怎么通知呢?
- 可以让 JS 写一个函数地址(电话号码)给浏览器
- 异步任务完成时浏览器调用该函数地址即可(拨打电话)
- 同步时把结果作为参数传给该函数(电话可以来吃了)
- 这个函数是我写给浏览器调用的,所以是回调函数
3. 区别
- 异步任务需要用回调函数来通知结果
- 但回调函数不一定只用在异步任务里
- 回调可以用到同步任务里
- array.forEach(n=>console.log(n)) 就是同步回调
怎么知道一个函数是同步还是异步?
很简单,根据文档特征或文档
四、举例判断同步和异步
如果一个函数的返回值处于
- setTimeout
- AJAX(即 XMLHttpRequest)
- AddEventListener
这三个东西内部,那么这个函数就是异步函数
1. 摇骰子 - 举例(加深理解)
function 摇骰子(){
setTimeout(()=>{ // 箭头函数
return parseInt(Math.random()*6)+1
},1000)
// return undefined
}
复制代码
分析
- 摇骰子() 没有写 return,那就是 return undefined
- 箭头函数里有 return,返回真正的结果
- 所以这是一个异步函数/异步任务
2. 摇骰子 - 续 1
const n = 摇骰子()
console.log(n) // undefined
复制代码
那么怎么拿到异步结果?
答:可以用回调。写个函数,然后把函数地址给它
function f1(x){ console.log(x) }
摇骰子(f1)
复制代码
然后我要求摇骰子函数得到结果后把结果作为参数传给 f1
function 摇骰子(fn){
setTimeout(()=>{
fn(parseInt(Math.random()*6)+1)
},1000)
}
复制代码
3. 摇骰子 - 续 2
摇骰子函数不调用 fn 怎么办?
答:不调?不调我neng死写代码的人(包括自己)
简化为箭头函数
由于 f1 声明之后只用了一次,所以可以删掉 f1
function f1(x){ console.log(x) }
摇骰子(f1)
// 改为
摇骰子(x=>{
console.log(x)
})
// 因为传的参数和接收的参数个数一致,所以可以再简化为
摇骰子(console.log)
// 如果参数个数不一致就不能这样简化,有个面试题
复制代码
4. 一道题
问打印出什么?
const array =['1','2','3'].map(parseInt)
console.log(array)
复制代码
答:
[1,NaN,NaN]
复制代码
map 传了 3 个参数,parseInt 于是接收 3 个参数,这不是我们想要的方式
map((item,i,arr)=>{
return parseInt(item,i,arr) // 数组元素、下标、数组
// parseInt('1',0,arr) => 1 // 第二个参数 0 无效,正常解析为 1
// parseInt('2',1,arr) => NaN // 把 '2' 以一进制数字解析,NaN
// parseInt('3',2,arr) // 把 '3' 以二进制数字解析,NaN
})
复制代码
正确的写法不能简写
map((item,i,arr)=>{
return parseInt(item)
})
复制代码
五、异步任务有两个结果的处理
1. 方法一:回调接收两个参数呗
fs.readFile('./1.txt', (error, data)=>{
if(error){ console.log('失败'); return }
console.log(data.toString()) // 成功
})
复制代码
2. 方法二:搞两个回调呗
ajax('get','/1.json', data=>{}, error=>{})
// 前面函数是成功回调,后面函数是失败回调
ajax('get', '/1.json', {
success: ()=>{}, fail: ()=>{}
})
// 接收一个对象,对象有两个 key 表示成功和失败
复制代码
3. 这些方法的不足
- 不规范,名称五花八门,有人用 success + error,有人用 success + fail,有人用 done + fail
- 容易出现回调地狱,代码变得看不懂
- 很难进行错误处理
getUser( user => {
getGroups(user, (groups)=>{
groups.forEach( (g)=>{
g.filter(x => x.ownerId === user.id)
.forEach(x => console.log(x))
})
})
})
// 这还只是四层回调,你能想象20层回调吗?
复制代码
六、怎么解决回调问题 - Promise
- 规范回调的名字或顺序
- 拒绝回调地狱,让代码可读性更强
- 很方便地捕获错误
前端程序员开始翻书了
- 1976 年,Daniel P.Friedman 和 David Wise 两人提出 Promise 思想
- 后人基于此发明了 Future、Delay、Deferred 等
- 前端结合 Promise 和 JS,制定了 Promise/A + 规范
- 该规范详细描述了 Promise 的原理和使用方法
七、以 AJAX 的封装为例来解释 Promise 的用法
1. 改写成 Promise 写法
ajax = (method, url, options)=>{
const {success, fail} = options // 析构赋值
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'