异步与同步

在理解promise之前,我们首先要搞清楚,什么是异步?什么是回调?

网上的解释经常混淆异步与回调。

一句话解释

如果能直接拿到结果,就是同步。

例如在医院窗口挂号,拿到号你才会离开窗口。同步任务可能消耗10ms,也可能需要3s。总之不拿到结果你是不会离开的。

如果不能直接拿到结果,那就是异步。

比如你在餐厅门口等位,你拿到号可以去逛街。什么时候才能真正吃饭呢?

你可以每10分钟去餐厅问一下,这种方法就是轮询。

也可以扫码用微信接受通知,而这就是我们所说的回调。

AJAX为例

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

这跟餐厅给你发送微信提醒的过程是类似的。

回调callback

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

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

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

写了却不调用,给别人调用的函数,就是回调。“回头你调用一下呗”。

举例

把函数1给另一个函数2

1
2
3
4
5
function f1(){}
function f2(fn){
	fn()
}
f2(f1)

分析

  1. 我调用f1没有?答:没有
  2. 我把f1传给f2(别人)了没有?答:传了
  3. f2调用f1了没有?答:f2调用了f1

综上,f1是我写给f2调用的函数,f1是回调。

异步与回调的关系

异步任务需要用到回调函数,但是回调函数不一定只用在异步任务里,也可以用到同步任务里。

array,forEach(n⇒console.log(n))就是同步回调

异步任务需要在得到结果时通知JS来拿结果。怎么通知呢?

可以让JS写留一个函数地址(电话号码)给浏览器,异步任务完成时浏览器调用该函数地址即可(拨打电话)。同时把结果作为参数传给该函数(电话通知可以来吃了)

这个函数是我写给浏览器调用的,所以是回调函数。

判断异步同步

如果一个函数的返回值处于

  • setTimeout
  • AJAX(即XMLHttpRequest)
  • AddEventListener

这三个东西内部,那么这个函数就是异步函数。

注意,不要把AJAX设置为同步,这样会使请求期间页面卡住。

摇骰子

1
2
3
4
5
6
function 摇骰子(){
	setTimeout(()=>{
		return parseInt(Math.random()*6)+1
	},1000)
	//return undefined
}

分析

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

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

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

摇骰子续

1
2
const n = 摇骰子()
console.log(n)//undefined

那怎么拿到异步结果呢?

可以用回调,写个函数,然后把函数地址给它。

1
2
function f1(x){console.log(x)}
摇骰子(f1)

然后我要求摇骰子函数得到结果后把结果作为参数传给f1

1
2
3
4
5
function 摇骰子(fn){
	setTimeout(()=>{
		fn(parseInt(Math.random()*6)+1)
	},1000)
}

这个代码可以进行简化

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

1
2
3
4
5
6
7
function f1(x){console.log(x)}
摇骰子(f1)
//改为
摇骰子(x = >{console.log(x)})
//再简化为
摇骰子(console.log)
//如果参数不一致就不能这样简化

总结

  1. 异步任务不能直接立刻拿到结果
  2. 于是我们传一个回调给异步任务
  3. 异步任务完成时调用回调
  4. 调用的时候把结果作为参数

但是异步任务有两个结果,如果失败,怎么办?

方法一:回调接受两个参数,node.js就是用这种方法

1
2
3
4
fs.readFile('./1.txt',(error,data)=>{
		if(error){console.log('失败');return}//失败
		console.log(data.toString())//成功
})

方法二:搞两个回调

1
2
3
4
5
6
ajax('get','/1.json',data=>{},error=>{})
//前面函数是成功回调,后面函数是失败回调
ajax('get','/1.json',{
	success:()=>{},fail:()=>{}
})
//接受一个对象,对象有两个key表示成功和失败

这些方法的不足

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

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

回调地狱举例

1
2
3
4
5
6
7
8
getUser( user => {
	getGroups(user,(groups)=>{
		groups.forEach( (g)=>{
			g.filter(x =>x.ownerID === user.id)
				.forEach(x =>console.log(x))
			})
		})
	})

这还只是四层回调,你能想象20层回调吗?

怎么解决回调问题?怎么让回调变得更好用?

Promise

为了实现这三个目标

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

前端程序员开始翻书了

  • 1976年,Daniel P.Fridman和David Wise提出Promise思想
  • 后人基于此发明了Future、Delay 、Deferred等
  • 前端结合Promise和JS,制订了Promise/A+规范,该规范详细描述了Promise的原理和使用方法

以AJAX的封装为例来解释Promise的用法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
ajax = (method,url,options) =>{
	const {sucess,fail} = options//ES6析构赋值
	const request = new XMLHttpRequset()
	request.open(method,url)
	request.onreadystatechange = () =>{
		if(request.readyState === 4){
		//成功就调用success,失败就调用fail
			if(request.status<400){
				success.call(null,request.response)
			}else if(requset.status >=400){
				fail.call(null,requset,requset,status)
			}
		}
	}
	request,send()
}

ajax('get','/xxx',{
	success(response){},fail:(request,status)=>{}
})//左边是function缩写,右边是箭头函数缩写

Promise说这代码太傻了,我们改成Promise写法

1
2
3
4
5
6
7
ajax('get','/xxx',{
	success(response){},fail:{request,status)=>{}
})
//上面用到了两个回调,还使用了success和fail
//改成Promise写法
ajax('get','/xxx')
	.then((response)=>{},(request,status)=>{})

虽然也是回调,但是不需要记success和fail了 then的第一个参数就是success,then的第二个参数就是fail

请问ajax()返回了个啥?

返回了一个含有.then()方法的对象呗

那么再请问如何得到这个含有.then()的对象呢?

那就要改造ajax的源码了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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()
})
}

return new Promise((resolve,reject)⇒{})

小结

  1. return new Promise((resolve,reject)⇒{…})
  2. 任务成功调用resolve(result),任务失败调用reject(error)
  3. resolve和reject会再去调用成功和失败函数
  4. 使用.then(success,fail)传入成功和失败函数