说明
浏览器工作原理与实践专栏学习笔记
Promise 到底解决了什么问题呢?
Promise 解决的是异步编码风格的问题,而不是一些其他的问题
异步编程的问题:代码逻辑不连续
Web 应用的异步编程模型
Web 页面的单线程架构决定了异步回调
下面的 XMLHttpRequest
例子:出现多次回调,这导致逻辑不连贯、不线性,非常不符合人的直觉
//执行状态
function onResolve(response){console.log(response) }
function onReject(error){console.log(error) }
let xhr = new XMLHttpRequest()
xhr.ontimeout = function(e) { onReject(e)}
xhr.onerror = function(e) { onReject(e) }
xhr.onreadystatechange = function () { onResolve(xhr.response) }
//设置请求类型,请求URL,是否同步信息
let URL = 'https://time.geekbang.com'
xhr.open('Get', URL, true);
//设置参数
xhr.timeout = 3000 //设置xhr请求的超时时间
xhr.responseType = "text" //设置响应返回的数据格式
xhr.setRequestHeader("X_TEST","time.geekbang")
//发出请求
xhr.send();
封装异步代码,让处理流程变得线性
将上面的 XMLHttpRequest 请求封装一下:
封装请求过程
1.输入的 HTTP 请求信息全部保存到一个 request 的结构中
//makeRequest用来构造request对象
function makeRequest(request_url) {
let request = {
method: 'Get',
url: request_url,
headers: '',
body: '',
credentials: false,
sync: true,
responseType: 'text',
referrer: ''
}
return request
}
2.封装 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();
}
3.业务代码编写
XFetch(makeRequest('https://time.geekbang.org'),
function resolve(data) {
console.log(data)
}, function reject(e) {
console.log(e)
})
新的问题:回调地狱
先看一个例子:该例子是基于上面的
XFetch(makeRequest('https://time.geekbang.org/?category'),
function resolve(response) {
console.log(response)
XFetch(makeRequest('https://time.geekbang.org/column'),
function resolve(response) {
console.log(response)
XFetch(makeRequest('https://time.geekbang.org')
function resolve(response) {
console.log(response)
}, function reject(e) {
console.log(e)
})
}, function reject(e) {
console.log(e)
})
}, function reject(e) {
console.log(e)
})
我们可以看到这个代码看起来很乱,不直观,它用了嵌套调用,并且都要进行错误的处理。
那么怎么处理这种问题?
- 消灭嵌套调用
- 合并多个任务的错误处理
Promise:消灭嵌套调用和多次错误处理
1.使用 Promise 来重构 XFetch
// 引入了 Promise,在调用 XFetch 时,会返回一个 Promise 对象
// 业务流程都在 executor 函数中执行
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 函数,触发 promise.then 设置的回调函数
resolve(this.responseText, this)
} else {
let error = {
code: this.status,
response: this.response
}
// 执行失败了,则调用 reject 函数时,触发 promise.catch 设置的回调函数
reject(error, this)
}
}
}
xhr.send()
}
return new Promise(executor)
}
2.利用 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 是如何消灭嵌套回调和合并多个错误处理
解决嵌套回调
1.Promise 实现了回调函数的延时绑定
//创建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)
2.将回调函数 onResolve 的返回值穿透到最外层
合并多个错误处理
function executor(resolve, reject) {
let rand = Math.random();
console.log(1)
console.log(rand)
if (rand > 0.5)
resolve()
else
reject()
}
var p0 = new Promise(executor);
var p1 = p0.then((value) => {
console.log("succeed-1")
return new Promise(executor)
})
var p3 = p1.then((value) => {
console.log("succeed-2")
return new Promise(executor)
})
var p4 = p3.then((value) => {
console.log("succeed-3")
return new Promise(executor)
})
p4.catch((error) => {
console.log("error")
})
console.log(2)
将代码放到控制台运行,随机的一个结果如下:无论哪个对象里面抛出异常,都可以通过最后一个对象 p4.catch
来捕获异常。
因为 Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止。
参考:promise 内部有 resolved_ 和 rejected_ 变量保存成功和失败的回调,进入
.then(resolved,rejected)
时会判断 rejected 参数是否为函数,若是函数,错误时使用 rejected 处理错误;若不是,则错误时直接 throw 错误,一直传递到最后的捕获,若最后没有被捕获,则会报错。可通过监听unhandledrejection
事件捕获未处理的 promise 错误。
拓展:unhandledrejection
基本的异常上报
防止默认处理
模拟实现一个 Promise
下面将模拟的对象称为 Bromise
1.Bromise 的实现代码:
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);
}
2.使用 Bromise 来实现业务代码
function executor(resolve, reject) {
resolve(100)
}
//将Promise改成我们自己的Bromsie
let demo = new Bromise(executor)
function onResolve(value){
console.log(value)
}
demo.then(onResolve)
3.执行代码
把代码放到控制台执行发现报错了:
由于 Bromise 的延迟绑定导致的,在调用到
onResolve_
函数的时候,Bromise.then
还没有执行
4.改造 Bromise 中的 resolve 方法
让 resolve 延迟调用 onResolve_
:
比如:采用定时器(效率并不是太高)来推迟 onResolve 的执行,
实现如下
function resolve(value) {
setTimeout(()=>{
onResolve_(value)
},0)
}
Promise 把这个定时器改造成了微任务了,这样既可以让 onResolve_
延时被调用,又提升了代码的执行效率。
重点理解
Promise 通过回调函数延迟绑定、回调函数返回值穿透、错误“冒泡”技术这三个点。
其他
到时手写系列将会在面试专栏那里出现:比如(手写一个 Promise )。