使⽤Promise,告别回调函数

异步编程的问题:代码逻辑不连续

⻚⾯中任务都是执⾏在主线程之上的,相对于⻚⾯来说,主线程就是它整个的世界,所以在执⾏⼀项耗时的任务 时,⽐如下载⽹络⽂件任务、获取摄像头等设备信息任务,这些任务都会放到⻚⾯主线程之外的进程或者线 程中去执⾏,这样就避免了耗时任务“霸占”⻚⾯主线程的情况。

⻚⾯主线程发起了⼀个耗时的任务,并将任务交给另外⼀个进程去处理,这时⻚⾯主线程会继续执⾏消息队列中的任务。

等该进程处理完这个任务后,会将该任务添加到渲染 进程的消息队列中,并排队等待循环系统的处理。

排队结束之后,循环系统会取出消息队列中的任务进⾏处 理,并触发相关的回调操作。

这就是⻚⾯编程的⼀⼤特点:异步回调


封装异步代码,让处理流程变得线性

重点关注的是输⼊内容(请求信息)输出内容(回复信息)

将XMLHttpRequest请求过程的代码封装起来了,重点关注输⼊数据和输出结果。

//makeRequest⽤来构造request对象
function makeRequest(request_url) {
let request = {
	method: 'Get',
	url: request_url,
	headers: '',
	body: '',
	credentials: false,
	sync: true,
	responseType: 'text',
	referrer: ''
	}
	return request
}

将所有的请求细节封装进XFetch函数

//[in] request,请求信息,请求头,延时值,返回类型等
//[out] resolve, 执⾏成功,回调该函数
//[out] reject 执⾏失败,回调该函数
function XFetch(request, resolve, reject) {
	let xhr = new XMLHttpRequest()
	xhr.ontimeout = function (e) { reject(e) }
	xhr.onerror = function (e) { reject(e) }
	xhr.onreadystatechange = function () {
	if (xhr.status = 200)
		resolve(xhr.response)
}
	xhr.open(request.method, URL, request.sync);
	xhr.timeout = request.timeout;
	xhr.responseType = request.responseType;
	//补充其他请求信息
	//...
	xhr.send();
}

实现业务代码

XFetch(makeRequest('https://time.geekbang.org'),
function resolve(data) {
	console.log(data)
}, function reject(e) {
	console.log(e)
})


新的问题:回调地狱

嵌套了太多的回调函数就很容易使得⾃⼰陷⼊了回调地狱

  • 第⼀是嵌套调⽤,下⾯的任务依赖上个任务的请求结果,并在上个任务的回调函数内部执⾏新的业务逻 辑,这样当嵌套层次多了之后,代码的可读性就变得⾮常差了。
  • 第⼆是任务的不确定性,执⾏每个任务都有两种可能的结果(成功或者失败),所以体现在代码中就需要 对每个任务的执⾏结果做两次判断,这种对每个任务都要进⾏⼀次额外的错误处理的⽅式,明显增加了代 码的混乱程度。

Promise:消灭嵌套调⽤和多次错误处理

使⽤Promise来重构XFetch的代码

function XFetch(request) {
	function executor(resolve, reject) {
		let xhr = new XMLHttpRequest()
		xhr.open('GET', request.url, true)
		xhr.ontimeout = function (e) { reject(e) }
		xhr.onerror = function (e) { reject(e) }
		xhr.onreadystatechange = function () {
			if (this.readyState === 4) {
				if (this.status === 200) {	
					resolve(this.responseText, this)
			} else {
				let error = {	
					code: this.status,
					response: this.response
				}
				reject(error, this)
			}
		}
	}
	xhr.send()
}

return new Promise(executor)
}

再利⽤XFetch来构造请求流程

var x1 = XFetch(makeRequest('https://time.geekbang.org/?category'))

var x2 = x1.then(value => {
	console.log(value)
	return XFetch(makeRequest('https://www.geekbang.org/column'))
})

var x3 = x2.then(value => {
	console.log(value)
return XFetch(makeRequest('https://time.geekbang.org'))
})

x3.catch(error => {
	console.log(error)
})

  • ⾸先引⼊了Promise,在调⽤XFetch时,会返回⼀个Promise对象
  • 构建Promise对象时,需要传⼊⼀个executor函数,XFetch的主要业务流程都在executor函数中执⾏。
  • 如果运⾏在excutor函数中的业务执⾏成功了,会调⽤resolve函数;如果执⾏失败了,则调⽤reject函 数。
  • 在excutor函数中调⽤resolve函数时,会触发promise.then设置的回调函数;⽽调⽤reject函数时,会触 发promise.catch设置的回调函数
  • 无论哪一个步骤抛错,都可以由p3的.catch来捕捉错误,是因为Promise对象的错误具有“冒泡”性质,会⼀直向后 传递,直到被onReject函数处理或catch语句捕获为⽌。

Promise主要通过下⾯两步解决嵌套回调问题的

⾸先,Promise实现了回调函数的延时绑定。回调函数的延时绑定在代码上体现就是先创建Promise对象 x1,通过Promise的构造函数executor来执⾏业务逻辑;创建好Promise对象x1之后,再使⽤x1.then来设置 回调函数。

//创建Promise对象x1,并在executor函数中执⾏业务逻辑
function executor(resolve, reject){
resolve(100)
}

let x1 = new Promise(executor)
//x1延迟绑定回调函数onResolve
function onResolve(value){
console.log(value)
}

x1.then(onResolve)



其次,需要将回调函数onResolve的返回值穿透到最外层。因为我们会根据onResolve函数的传⼊值来决定 创建什么类型的Promise任务,创建好的Promise对象需要返回到最外层,这样就可以摆脱嵌套循环。


Promise与微任务

由于Promise采⽤了回调函数延迟绑定技术,所以在执⾏resolve函数的时候,回 调函数还没有绑定,那么只能推迟回调函数的执⾏。

function Bromise(executor) {
	var onResolve_ = null
	var onReject_ = null
	//模拟实现resolve和then,暂不⽀持rejcet
	this.then = function (onResolve, onReject) {
	onResolve_ = onResolve
};
	function resolve(value) {
		setTimeout(()=>{
			onResolve_(value)
	 	},0)
}
	executor(resolve, null);
}

上⾯采⽤了定时器来推迟onResolve的执⾏,不过使⽤定时器的效率并不是太⾼,,所以 Promise⼜把这个定时器改造成了微任务了,这样既可以让onResolve_延时被调⽤,⼜提升了代码的执⾏ 效率。这就是Promise中使⽤微任务的原由了。