深入理解js中的promise

135 阅读5分钟

Promise解决了什么问题

Promise 解决的是异步编码风格的问题

Promise没有出现之前异步编程存在什么问题,是怎么解决的

代码逻辑不连续

参考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();

我们执行上面这段代码,可以正常输出结果的。但是,这短短的一段代码里面竟然出现了五次回调,这么多的回调会导致代码的逻辑不连贯、不线性,非常不符合人的直觉,这就是异步回调影响到我们的编码方式。

如何解决:封装异步代码,让处理流程变得线性

我们重点关注的是输入内容(请求信息)和输出内容(回复信息),至于中间的异步请求过程,我们不想在代码里面体现太多,因为这会干扰核心的代码逻辑。整体思路如下图所示:

image.png

//makeRequest用来构造request对象
function makeRequest(request_url) {
    let request = {
        method: 'Get',
        url: request_url,
        headers: '',
        body: '',
        credentials: false,
        sync: true,
        responseType: 'text',
        referrer: ''
    }
    return request
}
//[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)
    })

但是这又会产生新的问题:回调地狱

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的出现解决了这个两个问题:消灭嵌套调用和多次错误处理


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)
}
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)
})
  1. 首先我们引入了 Promise,在调用 XFetch 时,会返回一个 Promise 对象。
  2. 构建 Promise 对象时,需要传入一个 executor 函数,XFetch 的主要业务流程都在 executor 函数中执行。
  3. 如果运行在 excutor 函数中的业务执行成功了,会调用 resolve 函数;如果执行失败了,则调用 reject 函数。
  4. 在 excutor 函数中调用 resolve 函数时,会触发 promise.then 设置的回调函数;而调用 reject 函数时,会触发 promise.catch 设置的回调函数。
  5. Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止。具备了这样“冒泡”的特性后,就不需要在每个 Promise 对象中单独捕获异常了

首先,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)

其次,需要将回调函数 onResolve 的返回值穿透到最外层。

image.png

现在我们知道了 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);
}

function executor(resolve, reject) {
    resolve(100)
}
//将Promise改成我们自己的Bromsie
let demo = new Bromise(executor)
//执行到这一步就会报错了,因为在执行executor函数的时候,then还没有执行,onResolve_还是null
//此时的解决方案就是用setTimeout或者微任务让onResolve_延迟执行
function onResolve(value){
    console.log(value)
}
demo.then(onResolve)

async 和 await

Promise虽然解决了回调地狱的问题,但是代码里面包含了大量的 then 函数,使得代码依然不是太容易阅读

基于这个原因,ES7 引入了 async/await,这是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰

使用js原生api Fetch看一下

async function foo() {
	try {
		let response1 = await fetch('https://www.geekbang.org')
		console.log('response1')
		console.log(response1)
		let response2 = await fetch('https://www.geekbang.org/test')
		console.log('response2')
		console.log(response2)
	} catch (err) {
		console.error(err)
	}
}
foo()

思考:

1、Promise 中为什么要引入微任务?

解答:因为 Promise 回调函数延迟绑定导致 执行的时候onResolve_还未绑定,所以onResolve_要使用微任务延迟执行

由于promise采用.then延时绑定回调机制,而new Promise时又需要直接执行promise中的方法,即发生了先执行方法后添加回调的过程,此时需等待then方法绑定两个回调后才能继续执行方法回调,便可将回调添加到当前js调用栈中执行结束后的任务队列中,由于宏任务较多容易堵塞,则采用了微任务

2、Promise 中是如何实现回调函数返回值穿透的?

解答:回调函数返回一个新的promise

首先Promise的执行结果保存在promise的data变量中,然后是.then方法返回值为使用resolved或rejected回调方法新建的一个promise对象,即例如成功则返回new Promise(resolved),将前一个promise的data值赋给新建的promise

3、Promise 出错后,是怎么通过“冒泡”传递给最后那个捕获异常的函数?

解答:promise内部有resolved_和rejected_变量保存成功和失败的回调,进入.then(resolved,rejected)时会判断rejected参数是否为函数,若是函数,错误时使用rejected处理错误;若不是,则错误时直接throw错误,一直传递到最后的捕获,若最后没有被捕获,则会报错。可通过监听unhandledrejection事件捕获未处理的promise错误

参考文档:浏览器工作原理与实践