JavaScript异步编程核心概念-Promise

580 阅读19分钟

         回调函数可以说是Javascript中所有异步编程方式的根基,但是直接使用传统回调方式去完成复杂的异步流程,就无法避免大量的回调函数嵌套产生的回调地狱。为了避免回调地狱问题,CommonJS社区提供了Promise的规范,目的是为异步编程提供一种更合理、更强大的统一解决方案。后来在ES2015中被标准化成为语言规范。

         Promise实际上就是一个对象,去表示一个异步任务最终结束后它究竟是成功还是失败。就是内部对外界作出了一个承诺,一开始这个承诺是一个待定的状态Pending。最终有可能成功Fulfilled,也有可能失败Rejected。 不管成功或者失败都有相对应的反应,在承诺状态最终明确了过后都会有相对应的任务会被自动执行。而且这种承诺会有一个很明显的特点一旦明确了结果过后,就不可能再发生改变并且相应的状态会执行相应的回调。比如成功执行 onFulfilled回调,失败执行onRejected回调。

基本用法:

        在代码层面Promise它实际上就是Es2015所提供的一个全局类型,我们可以使用它来构造一个Promise实例,也就是创建一个新的承诺。这个类型的构造函数需要接受一个函数作为参数,这个函数就可以理解为一个兑现承诺的逻辑。这个函数会在构造Promise的过程中被同步执行,在这个函数内部它可以接收到两个参数分别是:resolvereject 二者都是一个函数。Resolve函数的作用就是将这个Promise对象的状态修改为成功 Fulfilled,一般我们将异步任务的操作结果会通过这个 Resolve 参数传递出去。Reject 函数的作用就是将这个Promise的状态修改为失败 Rejected ,这个失败的参数一般传递的是一个错误对象 new Error()  用来表示这个承诺它为什么失败,也就是一个理由。Promise实例被创建成功以后,我们就可以使用这个实例的then方法分别去指定onFullfilledonRejected回调函数。then方法传入的第一个参数就是onFulfilled回调函数,可以在这个函数中定义一些成功后要执行的操作。第二参数传递的是onRejected回调函数,在这个函数内部定义一些失败过后要处理的任务。

const promise = new Promise(function(resolve, reject){
    if (Math.random(1, 10) > 5) {
        resolve(100)
    } else {
        reject(new Error('promise rejected'))
    }

})
promise.then(
    (value) => {console.log(value)}, // 100
    (error) => {console.log(error)}, // promise rejected
)

使用案例:

        使用Promise封装一个ajax,首先定义一个叫Ajax 的函数这个函数有一个url参数,用来接受外界需要请求的地址,然后在这个函数当中直接对外返回一个promise对象实例,就相当于对外作出一个承诺,在这个promise对象的执行逻辑当中使用XMLHttpRequest对象去发送一个Ajax请求。然后要设置一下 xhr 的请求方式,请求的地址为参数当中的url。往下设置一下响应类型为json,这个方式是html5当中引入的一个新特性。这样的话我们就可以在请求完成过后直接拿到一个json对象而不是字符串,接下来去注册一下xhronload事件。它同时也是html5的新特性这个事件是请求完成过后,也就是我们传统的readyState等于4的状态。在请求完成这个事件当中我们应该先去判断一下请求的状态是不是200,如果是的话就意味着请求已经成功。那么我们应该去调用Resolve去表示我们这个Promise已经成功,并且将请求回来的数据传给Resolve。反之如果请求失败的话,我们就调用Reject函数去表示Promise失败,这里我们就应该传入一个错误信息对象,这个错误信息就是当前的状态文本。完成以后去调用一下xhrsend方法开始执行这个异步请求,这样的话这个promise版本的Ajax函数就封装好了。

function ajax (url) {
    return new Promise(function(resovle, reject) {
        const xhr = new XMLHttpRequest()
        xhr.open('POST', url)
        xhr.requestType = 'json'
        xhr.onLoad = function () {
            if (this.status === 200) {
                resovle(this.response)
            } else {
                reject(new Error(this.statusText))
            }
        }
    })
}
ajax('/api/users.json')
    .then(
        res => {},
        error => {}
    )

常见误区:

         通过前面的尝试我们发现从表象上来看Promise的本质也就是使用回调函数方式去定义异步任务结束过后所需要执行的任务,只不过这里的回调函数是通过then方法传递的而且Promise将我们的回调分成成功onFulfilled和失败onRejected两种,既有回调函数也就会出现回调地狱的问题。这就产生我们使用Promise的第一个误区,对此应该使用Promise对象then方法做链式调用尽量来保持异步调用的扁平化。

链式调用:

       Promise一个很大的优势就是链式调用,这样就能最大程度的避免回调地狱**Promise****then**方法就是为**Promise**对象去添加状态明确后的回调函数,它的第一个参数是 onFulfilled 回调、第二回调是 onRejected 回调。其中失败过后的回调是可以省略的,then方法最大的特点就是它的内部也会返回一个Promise对象。按照以往我们对链式调用的认知这里返回的promise应该就是当前的这个Promise对象,实际上并不是同一个对象。所以说这里的链式调用它并不是以往我们常见的那种在方法内部去返回this的方式去实现的链式调用,这里的then方法它返回的是一个全新的 Promise对象,目的就是去实现一个promise的链条也就是承诺结束过后再去返回一个新的承诺。那每一个承诺都可以负责一个异步任务,它们相互之间并没有什么影响,这就意味着如果我们不断的链式调用then方法。这里每一个then方法它实际上都是在为上一个then方法返回的Promise对象,去添加状态明确后的回调。这些promise会依次执行,这里添加的这些回调函数自然也就是从前到后依次执行。而且我们也可以在then的回调当中手动返回一个Promise对象,它的执行结果会在下一个then方法中被调用,这样我们就可以避免不必要的回调嵌套。而且以此类推,如果有多个连续的任务就可以使用这种链式调用的方式去避免回调的嵌套。从而尽量保证代码的扁平化,如果说我们的回调当中返回的不是一个Promise而是一个普通的值,这个值就会作为当前这个then方法返回的Promise中的值,在下一个then方法中接收的回调参数它实际上拿到的就是这样一个值,如果没有返回任何一个值它默认返回的就是一个 undefined

总结:1、Promise对象的then方法他会返回一个新的Promise对象,所以说我们就可以使用链式调用的方式去添加then方法;2、后面的then方法它实际上就是在为上一个then方法当中返回的Promise去注册对应的回调;3、前面then方法回调函数当中的返回值会作为后面then方法回调的参数;4、如果说我们在回调当中返回的是一个Promise对象的话那后面then方法当中的回调实际上就会等待这个Promise结束,也就是说后面的then方法实际上就是相当于为我们所返回的这个Promise对象去注册了对应的回调。

异常处理:

        正如前面所说Promise的结果一旦失败,它就会调用我们在then方法当中去传入的onRejected回调函数。在promise执行的过程当中,出现了异常或者手动抛出了一个异常那onRejected回调也会被执行。onRejected函数实际上就是为promise当中的异常去做一些处理,在Promise失败了或者出现了异常时都会被执行。其实关于onRejected回调的注册还有一个更常见的用法,就是使用Promise实例的catch方法去注册onRejected回调。catch方法其实就是then方法的别名,因为调用它实际上就相当于调用了then方法,然后第一个参数去传递了一个undefined。相对来说catch方法去指定失败回调要更为常见,应为这种方式会更适合于链式调用。从表象上看我们使用catch方法注册失败方法,跟直接在then方法的第二个参数去注册效果是一样的,他们都能捕获到Promise它在执行过程中的异常。但是仔细去对比这两种方式其实他们有很大的差异,因为每个then方法返回的都是新的Promise对象,这也就是说我们在后面通过链式调用的方式调用的这个catch他实际上是在给前面then方法返回的promise对象去指定失败回调,并不是直接去给第一个promise对象所指定的。因为这是同一个Promise链条,前面Promise上的异常会一直被往后传递,所以在这里能够捕获到第一个Promise当中的异常,而通过then方法的第二个参数去指定的失败回调函数只是给第一个Promise对象指定的,也就是说它只能捕获到这个Promise对象的异常。具体在表象上的差异就是,如果我们在then方法当中返回了第二个Promise而且这个promise执行过程当中出现了异常,那我们使用then的第二个参数去注册的失败回调它是捕获不到第二个promise的异常的。因为它只是给第一个promise对象注册的失败回调,对于链式调用的情况下最好使用第二种方式去分开指定成功回调和失败回调。因为Promise链条上任何一个异常都会被向后传递,直至被捕获。那也就是说这种方式更像是给整个Promise链条注册的失败回调,所以说它相对来讲要更通用一些。除此之外我们还可以在全局对象上去注册一个onhandledrejection事件,去处理那些我们代码当中没有被手动捕获的Promise异常。在浏览器环境下可以注册在window对象上,node环境中要定义在process对象上。

window.addEventListener('onhandledrejection', event => {
    const {
        reason, // Promise 失败原因
        promise // 出现异常的Promise对象
    } = event
    event.preventDefault()
}, false)
process.addEventListener('onhandledRejection', (reason, promise) => {
    console.log(reason, promise)
})

静态方法:

        在Promise类型当中还有两个静态方法,也经常会用到。首先是 Promise.resolve()这个方法的作用就是快速的把一个值转换为一个返回成功结果的Promise对象, Promise.resolve('foo') 会返回一个状态为fulfilledPromise对象。foo就是这个promise对象的返回值也就是说我们可以在它的onFulfilled的回调函数当中拿到的参数就是 foo。这种方式完全等价于我们通过 new Promise(resolve => resolve('foo'))的这种方式,如果这个方法接受到的是另一个Promise对象,这个Promise对象会被原样返回。

const promise = ajax('/api/user.json')
const promise2 = Promise.resolve(promise)
console,log(promise === promise2) // true

如果我们传入的是一个对象并且这个对象也和Promise一样有一个then方法,同时在这个then方法中可以接收onFulfilledonRejected两个回调函数,这样一个对象也可以作为一个Promise对象被执行。因为这个对象也实现了thenable的接口,换句话说它是一个可以被then的对象。支持这种对象的原因是,在原生Promise对象还没有普及之间很多时候都是使用第三方的库去实现的promise

Promise.resolve({
    then(onFulfilled, onRejected) {
        onFulfilled('foo')
    }
}).then(value => {
    console.log(value) // foo
})

         除了Promise.resolve()方法还有一个与之对应的Promisee.reject()方法。它的作用就是快速创建一个是失败结果的Promise

Promise.reject(new Error('rejected'))
    .catch(error => console.log(error)) // rejected

并行执行:

        前面介绍的操作都是通过Promise串联执行多个异步任务,也就是一个任务结束过后再去开启下一个任务。相比于传统回调的方式Promise它提供了更扁平的异步编程体验,如果同时并行执行多个异步任务。Promise也可以提供更为完善的体验,例如在页面中需要请求多个接口的情况。如果说这些接口相互间没有什么依赖,最好的选择就是同时去请求他们避免一个一个依次去请求会消耗更多的时间。这种并行请求其实很容易实现,我们只需要单独去调用这里的ajax函数就可以了。难的是如何判断所有的请求都结束了,那样一个时机。传统我们的做法是定义一个计数器然后每结束一次请求,我们让这个计数器去累加一次。直到这个计数器的数量等于任务的数量,就表示所有的任务结束了。这种方法会非常的麻烦,而且还需要考虑出现异常的情况。在这样一种情况下使用Promise类型的all方法就会简单的多,因为这个方法可以将多个Promise合并为一个Promise统一去管理。Promise.all([....])接收一个数组,数组中的每一项都是一个Promise对象。我们可以把这些promise都看作一个一个的异步任务,这个方法会返回全新的Promise对象,当内部所有的Promise都完成过后我们所返回的这个全新的Promise才会完成。此时,这个Promise对象它拿到的结果就是一个数组,在这个数组里面包含着每一个异步任务执行过后的结果。在这个任务的执行当中只有所有的任务都成功结束了,这里的新的Promise才会成功结束。如果说其中有任何一个任务失败了这个Promise就会以失败结束,这是一个很好的同步执行多个异步Promise任务。

// 组合使用串行和并行
ajax('/api/urls.json')
    .then(value => {
        const urls = Object.values(value)
        const tasks = urls.map(url => ajax(url))
        return Promise.all(tasks)
    }).then(values => {
        console.log(values)
    })

除了提供Promise.all()方法以外,还提供了一个Promise.race()方法。这个方法它同样可以把多个Promise对象组合为一个全新的Promise对象,但是与Promise.all()方法不同。Promise.all()它是等待所有的任务结束过后才会结束,而Promise.race()它是跟着所有任务当中第一个完成的任务一起结束,也就是说只要有任何一个任务完成了,那这个所返回的新的Promise对象也就会完成。

const request = ajax('/api/users.json')
const timeOut = setTimeout(() => throw(new Error('Time Out')), 500)
Promise.race([
    request,
    timeOut
]).then(
    value => console.log(value)
).catch (
    error => console.log(error)
)

执行时序:

        最后来深入了解一下关于Promise执行时序的问题,也就是执行的顺序。正如开始所见到的,即便说Promise内部没有传递异步任务,它的回调函数仍然会进入到回调队列当中去排队。也就是说我么必须要等待当前所有的同步代码执行完成以后,才会去执行Promise当中的回调。如果继续在后面使用链式调用的方式去传递多个回调,这里每一个回调也应该是依次执行。但是如果Promise前面有个setTimeout的话会先执行Promise,以为setTimeout属于宏任务、Promise属于微任务。在回调队列当中的任务一般被称为宏任务,宏任务执行执行过程中可以额外加上一些额外需求。这时候对于这些临时额外的需求,可以选择作为一个新的宏任务重新进入到回调队列当中去排队。也可以作为当前任务的微任务,就是可以直接在当前这个任务结束过后就立即去执行,而不是到整个队伍的末尾在重新排队。这就是宏任务和微任务之间的差异,Promise的回调就是作为微任务执行的,所以说他会在本轮调用结束的末尾去自动执行。微任务的概念实际上是在后来才被引入到JS当中的,它的目的就是为了提高JS应用的响应能力。目前接触到的大部分异步调用的API都会作为宏任务进入到回调队列,而Promise对象和MutationObserver与node当中的 process.nextTick会作为微任务,直接在本轮调用的末尾就执行。

console.log('global start')
setTimeout(() => {
    console.log('Time out')
}, 0)
Promise.resolve(10)
    .then(value => {
        console.log(value) 
        return ++value
    })
    .then(value => {
        console.log(value)
        return ++value
    })
console.log('global end')
// global start
// global end
// 10
// 11
// Time out

核心逻辑剖析:

Promise就是一个类,在实例化的时候传递一个函数作为执行器,并且会立即执行。这个执行器有两个参数:resolve reject,这两个参数实际上是一个函数。它们的目的就是为了更改Promise的状态。Promise中有三种状态:等待pending、成功fulfilled、失败rejected,一旦状态确定就不可更改。pending --> fulfilled 、 pending --> rejected 、fulfilled -/-> rejectedresolvereject函数是用来更改状态的,resolve: fulfilled、reject:rejectedresolve接受一个成功的返回值,reject 接受一个失败的原因。

const PENDING = 'pending' // 等待
const FULFILLED = 'fulfilled' // 成功
const REJECTED = 'rejected' // 失败
class Promise {
    constructor (executor) {
        executor(this.resolve, this.reject) // 立即执行执行器
    }
    status = PENDING // Promise状态
    resolve = () => { // 将Promise状态改为成功
        if (this.status !== PENDING) return // 当非PENDING状态时返回
        this.status = FULFILLED
    }
    reject = () => { // 将Promise状态改为失败
        if (this.status !== PENDING) return // 当非PENDING状态时返回
        this.status = REJECTED
    }
}

then方法做的事情就是判断状态,如果状态成功则调用成功回调函数;反之,调用失败回调函数。then方法是被定义在原型对象上的。then的成功回调函数 onFulfilled(value) value表示成功之后的值、onRejected(reason) reason 是失败的原因。

class Promise {
    ....
    value = undefined // 成功之后的值
    reason = undefined // 失败之后的原因
    resolve = value => {
        ...
        this.value = value // 保存成功之后的值
    }
    rejected = reason => {
        ...
        this.reason = reason // 保存失败之后的值
    }
    then (onFulfilled, onRejected) {
        /** 判断Promise状态 */
        if (this.status === FULFILLED) { // 成功状态
            onFulfilled(this.value)
        } else if (this.status === REJECTED) { // 失败状态
            onRejected(this.reason)
        }
    }
}

Promise内部处理异步函数的流程是:在then函数被调用执行时,判断Promise的状态,当状态还是pending时就把成功回调函数和失败回调函数缓存起来。并在resolvereject函数体内部判断是否有成功回调函数或者失败回调函数的缓存,有则执行。

class Promise {
    ...
    fulfilledCallback = undefined // 实例属性成功回调
    rejectedCallback = undefined // 实例属性失败回调
    resolve = value => {
        ....
        this.fulfilledCallback
            && this.fulfilledCallback(value) // 判断回调是否存在,存在就调用
    }
    reject = reason => {
        ...
        this.fulfilledCallback // 判断回调是否存在,存在就调用
            && this.rejectedCallback(reason)
    }
    then (onFulfilled, onRejected) {
        if (this.status === FULFILLED) {
            ...
        } else if (this.status === REJECTED) {
            ...
        } else { // 异步延迟执行下的等待状态
            this.fulfilledCallback = onFulfilled
            this.rejectedCallback = onRejected
        }
    }
}

在同一个Promise对象下的then方法是可以多次被调用的,这就需要将异步情况下成功回调函数或者失败回调函数缓存在数组中,并在resolvereject函数体中按序弹出 while(this.successCallback.length) this.successCallback.shift()(this.value)

class Promise {
    ...
    fulfilledCallbacks = [] // 实例属性成功回调
    rejectedCallbacks = [] // 实例属性失败回调
    resolve = value => {
        ....
        while(this.fulfilledCallbacks.length) // 判断回调是否存在,存在就调用
            this.fulfilledCallbacks.shift()(value)
    }
    reject = reason => {
        ...
        while(this.fulfilledCallbacks.length) // 判断回调是否存在,存在就调用
            this.rejectedCallbacks.shift()(reason)
    }
    then (onFulfilled, onRejected) {
        if (this.status === FULFILLED) {
            ...
        } else if (this.status === REJECTED) {
            ...
        } else { // 异步延迟执行下的等待状态
            this.fulfilledCallbacks.push(onFulfilled)
            this.rejectedCallbacks.push(onRejected)
        }
    }
}

then方法是可以被链式调用的,并且后面的then方法的回调函数拿到的值是这个then方法回调函数的返回值。要实现then方法的链式调用就是在then方法中return一个新的Promise实例,并将then的执行体传入新的Promise中作为执行器。这样就可以通过将本次成功回调函数的执行返回值传给新执行器的 resolve 来实现将上一个then回调函数的返回值传给下一个then的目的。

class Promise {
    ...
    then (onFulfilled, onRejected) {
        return new Promise((resolve, reject) => {
            if (this.status === FULFILLED) {
                const result = onFulfilled(this.value)
                resolve(result)
            } else if (this.status === REJECTED) {
                ...
            } else {
                ...
            }
        })
    }
}

如果在上一个then的回调中又返回了一个Promise对象,我们在处理then函数时要对返回值进行类型的判断。

class Promise {
    ...
    then (onFulfilled, onRejected) {
        return new Promise((resolve, reject) => {
            if (this.status === FULFILLED) {
                const result = onFulfilled(this.value)
                resolvePromise(result, resolve, reject)
            } else if (this.status === REJECTED) {
                ...
            } else {
                ...
            }
        })
    }
}
function resolvePromise (result, resolve, reject) {
    if (result instanceof Promise) {
        result.then(resolve, rejiect)
    } else {
        resolve(result)
    }
}

如果在then的回调中又返回了自身的这个Promise的实例就会发生循环调用的错误,为了避免这中错误,要在then的链式调用中进行识别判断该promise对象是否是自返回。

class Promise {
    ...
    then (...) {
        const promise = new Promise((...) => {
            if (this.status === FULFILLED) {
                setTimeout(() => {
                    ....
                    resolve(promise, result, resolve, reject)
                }, 0)
            } else if (this.status === REJECTED) {
                ...
            } else {
                ...
            }
        })
        return promise
    }
}
function resolvePromise (promise, result, resolve, reject) {
    if (promise === result)
        return reject(
            new TypeError('Chaining cycle detected for promise #<Promise>')
        )
    ...
}

错误捕获要考虑两个情况,第一个是在执行体内部发生错误,当执行器当中的代码在执行的过程中。发生错误的时候,就让Promise的状态变成失败状态。也就是在then方法的第二个参数要捕获到这个错误,可以在构造函数中执行器的外部添加 try catch

class Promise {
    constructor (executor) {
        try {
            executor(this.resolve, this.reject)
        } catch (error) {
            this.reject(error)
        }
    }
    ...
}

第二个是在then方法的回调函数内部发生错误,这个错误要在下一个then方法执行时捕获到。这个捕获的实现是在then方法体中successCallbackfailCallback调用的外部添加try catch这里也要考虑异步的情况。

class Promise {
    resovle = value => {
        ...
        this.onFulfilledCallback.shift()()
    }
    reject = reason => {
        ...
        this.onRejectedCallback.shift()()
    }
    then (onFulfilled, onRejected) {
        const promise = new Promise((resolve, reject) => {
            if (this.status === FULFILLED) {
                setTimeout(_ => {
                    try {
                        const result = onFulfilled(this.value)
                        resolvePromise(promise, result, resovle, reject)
                    } catch (error) {
                        reject(error)
                    }
                }, 0)
            } else if (this.status ==== REJECTED) {
                setTimeout(_ => {
                    try {
                        const result = onRejected(this.reason)
                        resolvePromise(promise, result, resovle, reject)
                    } catch (error) {
                        reject(error)
                    }
                }, 0)
            } else {
                this.fulfilledCallbacks.push(_ => {
                    setTimeout(_ => {
                        try {
                            const result = onFulfilled(this.value)
                            resolvePromise(promise, result, resovle, reject)
                        } catch (error) {
                            reject(error)
                        }
                    }, 0)
                })                this.rejectedCallbacks.push(_ => {
                    setTimeout(_ => {
                        try {
                            const result = onRejected(this.reason)
                            resolvePromise(promise, result, resovle, reject)
                        } catch (error) {
                            reject(error)
                        }
                    }, 0)
                })            }
        })
        return promise
    }
}
...

then方法的参数变成可选参数:successCallback ? successCallback : value => value failCallback ? failCallback : reason => throw reason