异步与Promise

异步

什么是异步?什么是同步?

同步——直接拿到结果

异步——不能直接拿到结果

异步举例

以AJAX为例

request.send()之后,并不能直接得到response,必须等到readyState变为4后,浏览器回头调用request.onreadystatechange函数,才能得到request.response

image-20210515162556817.png

回调 callback

写给自己用的函数,不是回调

写给别人用的函数,就是回调

request.onreadystatechange就是写给浏览器调用的,意思是浏览器回头调用一下这个函数。

回调举例
  • 把函数1给另一个函数2

    function f1(){}
    function f2(fn){
    	fn()
    }
    f2(f1)
    复制代码
  • 分析

    没有调用f1,但是把f1传给了f2,f2调用了f1,f1是写给f2调用的函数,f1就是回调。

  • 抬杠

    function f1(x){
    	console.log(x)
    }
    function f2(fn){
    	fn('你好')
    }
    f2(f1)
    复制代码

    f1怎么会有一个x参数?

    fn('你好')中的fn就是f1,'你好'会被赋值给参数x

异步和回调的关系

关联

异步任务需要在得到结果时通知JS来拿结果,可以让JS留一个函数地址给浏览器,异步任务完成时,浏览器调用该函数地址即可,同时把结果作为参数传给该函数,这个函数是写给浏览器调用的,所以是回调函数。

区别

异步任务需要用到回调函数来通知结果,但回调函数不一定只用在异步任务里

回调也可以用到同步任务里——

array.forEach(n=>console.log(n))就是同步回调

判断同步异步

判断方法

如果一个函数的返回值处于下列三种情况,那么这个函数就是异步函数

setTimeout
AJAX //(即XMLHttpRequest)
AddEventListener
复制代码

没错,AJAX可以设置成同步,但是没必要,这样做会使请求期间页面卡住。

举个例子
摇骰子
function 摇骰子(){
	setTimeout(()=>{
	 return parseInt(Math.random()*6) + 1 //随意1-6
	},1000)
	//return undefined
}
复制代码
分析

摇骰子()没有写return,那就是return undefined

箭头函数里有return,返回真正的结果

所以这是一个异步函数/异步任务

摇骰子优化——回调
function f1(x){console.log(x)} //利用回调,将摇骰子函数得到的结果作为参数传给f1
function 摇骰子(fn){
	setTimeout(() => {
		fn(parseInt(Math.random()*6) + 1)
	},1000)
}
复制代码
摇骰子——简化箭头函数

由于f1声明之后只用了一次,所以可以删掉f1

function f1(x){console.log(x)}
摇骰子(f1)
复制代码

改为

摇骰子(x=>{console.log(x)})
复制代码

再简化为

摇骰子(console.log) //如果参数个数不一致,就不能这样简化,比如下面这个面试题
复制代码

面试题

image-20210515213322686.png

还原这个完整的代码

parseInt(string,radix)解析一个字符串并返回指定基数的十进制整数

image-20210515214006131.png

const array = ['1','2','3'].map((item,i,arr)=>{
	return parseInt(item,i,arr)
	// parseInt('1',0,arr) => 1  //传入0是无效参数,直接传出1
    // parseInt('2',1,arr) => NaN   //传入1代表1进制,1进制里只有0啊,所以只能得出NaN
    // parseInt('3',2,arr) => NaN  //传入2代表2进制,2进制只有0和1,没有3,传出NaN
})
复制代码

这样写才可以得到想要的答案

image-20210515213900883.png

简写的话可以写成

image-20210515215057612.png

Promise的用法

如果异步任务有两个结果——成功或失败,怎么办?

方法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表示成功和失败
复制代码

以上方法有些缺点:

  1. 不规范,名称五花八门,没有成文规定。有人用success+error,有人用success+fail,有人用done+fail。

  2. 容易出现回调地狱,代码变得看不懂

    例如

    getUser(user => {
    	getGroups(user.(groups)=>{
    		groups.forEach((g)=>{
    			g.filter(x => x.ownerId === user.id)
    			.forEach(x => console.log(x))
    		})
    	})
    })
    复制代码
  3. 很难进行错误处理

怎么解决回调问题?

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

Promise

1976年,Daniel P.Friedman 和 David Wise 提出Promise思想

后人基于此发明了FutureDelayDeferred

前端结合PromiseJS,制定了Promise/A+规范

该规范详细描述了Promise的原理和使用方法

以AJAX的封装为例
ajax = (method,url,optiopns) => {
	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',{
    success(response){}, fail:(request,status) =>{}
}) //左边success是function缩写,右边是箭头函数
复制代码
改成Promise写法
ajax = (method,url,options) => {
	return new Promise((resolve,reject)=>{
		const {sucess,fail} = options
		cosnt 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()
	})
}

ajax('get','/xxx')
	.then((response)=>{}, (request,status)=>{})
//虽然改写也是回调,但是不需要记success和fail
//then 的第一个参数就是success, then 的第二个参数就是fail
//ajax()返回了一个含有 .then()方法的对象
复制代码

两步走——

  1. return new Promise((resolve,reject)=>{...})

    任务成功则调用resolve(result)

    任务失败则调用result(error)

    resolvereject 会再去调用成功和失败函数

  2. 使用.then(success,fail)传入成功和失败函数

上面封装ajax的缺点——

  • post无法上传数据

    request.send(这里可以上传数据)

  • 不能设置请求头

    request.setRequestHeader(key,value)

  • 怎么解决呢?

    继续完善ajax

    使用jQuery.ajax

    使用axios

jQuery.ajax

中文文档

优点:

支持更多形式的参数

支持Promise

支持更多功能

axios

目前最新的AJAX库

代码示例

axios.get('5.json')
	.then(response =>
	console.log(response)	
)
复制代码

高级用法——

  • JSON自动处理

    axios发现响应的Content-Type是json,就会自动调用JSON.parse,所以正确设置Content-Type是好习惯

  • 请求拦截器

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

  • 响应拦截器

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

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

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

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享