异步与Promise

芝麻凛 2021年08月31日 144次浏览

异步与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 表示成功或失败

这些方法的不足

不管方法一还是方法二,都有问题

  1. 不规范,名称五花八稳,有人用success+error,有人用success+fail,有人用done+fail
  2. 容易出现回调地狱,代码变得看不懂
  3. 很难进行错误处理

回调地狱举例

getUser(user => {
    getGroups(user,(groups)=>{
        groups.forEach((g)=>{
            g.filter(x => x.ownerId === user.id).forEach(x => console.log(x))
        })
    })
})
这只是四层回调,如果再多那就完全看不懂了

image20210908215015804.png

怎么解决回调问题

有什么办法能解决这三个问题

  • 规范回调的名字或顺序
  • 拒绝回调地狱,让代码可读性更强
  • 很方便的捕获错误

前端程序员开始翻书了

  • 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是好习惯

请求拦截器

  • 可以在所有请求里加东西,比如加查询参数

响应拦截器

  • 可以在所有响应里加些东西,甚至改内容

可以生成不同的实例(对象)

  • 不同的实例可以设置不同的配置,用于复杂场景