什么是异步?什么是同步?什么是回调?
异步
如果能直接拿到结果那就是同步
- 比如在医院挂号,你拿到号才会离开
- 同步任务可能消耗10毫秒,也可能需要3秒
- 总之拿不到结果不会离开
如果不能直接拿到结果那就是异步
- 比如在餐厅门口等位,拿到号后可以去逛街
- 什么时候才能吃饭?
- 可以每10分钟去餐厅问一下(轮询)
- 也可以扫码用微信接收通知(回调)
异步举例
以 AJAX 为例
- request.send() 之后,并不能直接得到 response
- 必须等到 readyState 变为4后,浏览器回头调用 reques.onreadystatechange 函数
- 才能得到 request.response
回调 callback
- 你写个自己用的函数, 不是回调
- 你写给别人用的函数, 就是回调
- request.onreadystatechange 就是写给浏览器调用的
- 意思就是你(浏览器)回头调一下这个函数
回调举例
把函数1给另一个函数2
function f1(){}
function f2(fn){
fn()
}
f2(f1)
异步和回调的关系
关联
- 异步任务需要在得到结果时通知 JS 来拿结果
- 让 JS 留一个函数地址给浏览器
- 异步任务完成时浏览器调用该函数地址即可
- 同事把结果作为参数传递给该函数
- 这个函数是我写个浏览器的, 所以是回调函数
区别
- 异步任务需要用到回调函数来通知结果
- 但回调函数不一定只用在异步任务里
- 回调可以用到同步任务里
- 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,返回真正的结果
- 所以这是一个异步函数/异步任务
如何得到结果
function 摇骰子(fn){
setTimeout(()=>{
fn(parseInt(Math.random() * 6) + 1)
}, 1000)
// return undefined
}
// 用回调函数
function f1(x){ console.log(x) }
摇骰子(f1)
// 简化为箭头函数
摇骰子(x => {
console.log(x)
})
// 再简化为, 如果函数不一致就不能这样简化
摇骰子(console.log)
如果异步任务有两个结果,成功和失败,怎么办
两个结果
方法一:回调接受两个参数
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
- 容易出现回地狱,代码看不懂
- 很难进行错误处理
回调地狱举例
getUser( user => {
getGroups( user, (groups)=>{
groups.forEach( (g)=>{
g.filter(x => x.ownerId === user.id).forEach(x => console.log(x))
})
})
})
以 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 写法
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.status)
}
}
}
request.send()
})
}
ajax('get', '/xxx').then((response)=>{}, (request)=>{})
记忆
return new Promise((resolve, reject)=>{...})
小结:如何让一个回调函数,变成 Promise 的异步函数
第一步
return new Promise((resolve, reject)=>{...})- 任务成功则调用 resolve(result)
- 任务失败则调用 reject(error)
- resolve 和 reject 会再去调用成功和失败函数
第二步
- 使用 .then(success, fail) 传入成功和失败函数
此封装的 AJAX 的缺点
post 无法上传数据
- request.send(这里可以上传数据)
不能设置请求头
- request.setRequestHeader(key, value)
怎么解决呢?
- 花时间把 ajax 写完美(有时间可以做)
- 使用 jQuery.ajax(这个可以)
- 使用 axios(这个库比 jQuery 逼格高)
axios 代码示例
axios.get('/5.json')
.then( response =>
console.log(response)
)
axios 高级用法
JSON 自动处理
- axios 发现响应的 Content-Type 是 json
- 就会自动调用 JSON.parse
请求拦截器
- 你可以在所有请求里加些东西,比如加查询参数
响应拦截器
- 你可以在所有响应里加些东西,甚至改内容
可以生成不同实例(对象)
- 不同是实例可以设置不同的配置,用于复杂场景