说明
浏览器工作原理与实践专栏学习笔记
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 )。