异步与Promise
JS异步编程模型
什么是异步?什么是同步?
异步
如果能直接拿到结果,那就是异步同步
- 比如在医院挂号,拿到号才会离开窗口
- 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)
分析
- 我调用f1没有? 答:没有调用
- 我把f1传给了f2(别人)? 答:传了
- f2调用f1了没有?答:f2调用了f1
- 那么,f1 是不是 写给 f2调用的函数?答:是
- 所以 f1 是回调
异步和回调的关系
关联
- 异步任务需要在得到结果时,通知JS来拿结果
- 怎么通知呢?可以让JS留一个函数地址(电话号码)给浏览器
- 异步任务完成时浏览器调用该函数地址即可(拨打电话)
- 同时把结果作为参数传给该函数(电话里说可以吃饭了)
- 这个函数是写给浏览器调用的,所以是回调函数
区别
- 异步任务需要用到回调函数来通知结果,但不一定要回调也可以用轮询
- 但回调函数不一定只用在异步任务里
- 回调可以用到同步任务里,array.forEach(n=>console.log(n)) 就是回调函数。
怎么知道一个函数是同步还是异步:根据特征或文档
判断同步异步
如果一个函数的返回值处于以下几种
- setTimeout
- AJAX(即XMLHttpRequest)(不要把AJAX设置为同步的,这样做会使请求期间页面卡住)
- AddEventListener,这三个东西内部,那么这个函数就是异步函数
摇骰子
举例1
function 摇骰子(){
setTimeout(()=>{//箭头函数
return parseInt(Math.random() * 6) + 1
},1000)
// return undefined
}
分析
- 摇骰子() 没有写return,那就是return undefined
- 箭头函数里有return,返回真正的结果
- 所以这是一个异步函数/异步任务
摇骰子续
const n = 摇骰子()
console.log(n) //undefined
那么怎么拿到异步结果?
- 可以用回调。写个函数,然后把函数地址给它
function f1(x){console.log(x)}
摇骰子(f1)
然后要求摇骰子函数得到结果后把结果作为参数传给f1
function 摇骰子(fn){
setTimeout(fn){//箭头函数
fn(parseInt(Math.random()* 6) +1)
},1000)
}
总结
- 异步任务不能拿到结果,于是我们传一个回调给异步任务
- 异步任务完成时调用回调,调用的时候把结果作为参数。
如果异步任务有两个结果成功或失败,怎么办?进一步思考
两个结果
方法一:回调接收两个参数
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))
})
})
})
这只是四层回调,如果再多那就完全看不懂了
怎么解决回调问题
有什么办法能解决这三个问题
- 规范回调的名字或顺序
- 拒绝回调地狱,让代码可读性更强
- 很方便的捕获错误
前端程序员开始翻书了
- 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 //析构赋值 把success和fail拿出来,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)=>{}
}) //左边是有this函数 function 缩写,右边是没有this的箭头函数
改成Promise的写法
//先改一调用的姿势
ajax('get','/xxx',{
success(response){},fail:(request,status)=>{}
})
//上面用到了两个回调,还是用了success 和 fail
//改成Promise写法
ajax('get','/xxx').then((response)=>{},(request)=>{})
//虽然也是回调,但是不需要记success 和 fail 了
//then 的第一个参数就是 success ,第二个参数就是fail
//ajax 返回了一个含有.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){
//成功就调用 resolve,失败就调用 reject
if(request.status < 400){
resolve.call(null,request.response)
}else if(request.status >= 400){
reject.call(null,request)
}
}
}
request.send()
})
}
promise核心: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 目前前端主流
axios高级用法
JSON自动处理
- axios如果发现响应的Content-Type是json
- 就会自动调用JSON.parse,所以说正确设置Content-Type是好习惯
请求拦截器
- 可以在所有请求里加东西,比如加查询参数
响应拦截器
- 可以在所有响应里加些东西,甚至改内容
可以生成不同的实例(对象)
- 不同的实例可以设置不同的配置,用于复杂场景