Skip to content

彻底弄懂promise实现原理 #26

@jiefancis

Description

@jiefancis

前言

promise是我们日常工作中几乎都要用到的一个api,它的出现帮助我们解决了异步问题,至于回调地狱,如果层级多了,promise还是会产生回调地狱,不过我们可以借助async/await一起使用来解决回调地狱的问题。使用了这么久的promise,对于promise如何实现的也没有一窥全貌,所以这次跟大家一起分享它里面的实现。

EventLoop执行机制

在讲解promise原理之前,我们先来了解eventLoop与宏微任务。

微任务

  • js自带api

    • queuemMicrotask
    • Promise
  • 需要判断环境

    • process.nextTick(node)
    • MutationObserver(浏览器端)

宏任务

  • js代码
  • setTimeout
  • setIntervel
  • setImmediate

事件循环机制的执行过程简单提一点:js代码从上至下执行,遇到宏(微)任务,将宏(微)任务存在宏(微)任务事件队列中等待,等待js代码执行完后,再到微任务事件队列中查找是否还有任务在等待执行,如果微任务事件队列没有任务,同理,到宏任务事件队列执行。直到宏任务微任务执行完毕。
本次分享的重点不在这,有兴趣想了解更多请查阅

从一个问题来看promise

  1. 输出下面代码的打印顺序:
Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4);
}).then((res) => {
    console.log(res)
})

Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() =>{
    console.log(6);
})

输出结果是:0 1 2 3 4 5 6

输出结果为什么跟我们预想的 0 1 4 2 3 5 6不一样呢?

promiseA+规范

new Promise().then(),如果then中返回一个promise(简称p0),(V8源码中)则会执行()=>p0.then(完成p0的状态变化),同时将() => p0.then(完成p0的状态变化) 添加到异步队列中。

题目的运行过程

以输出数字为p的标记

  1. p0(() => { console.log(0); return Promise.resolve(4)})进入异步队列中,此时异步队列[p0];

  2. p1进入异步队列中,此时异步队列[p0,p1]。

  3. 执行p0,输出0。return Promise.resolve(4)则是转换成 () => p4.then(完成p0的状态变化)添加到异步队列中,此时异步队列[p1, () => p4.then(完成p0的状态变化)]

  4. 输出1,由于p1状态变为fulfilled,p2进入异步队列,[() => p4.then(完成p0的状态变化), p2]

  5. 执行p4.then(完成p0的状态变化),(完成p0的状态变化即pres = (res) => res), pres进入异步队列,此时异步队列为[p2, pres]

  6. 输出2,p2状态变为fulfilled,p3进入异步队列,此时异步队列为[pres, p3]

  7. 执行pres,p0状态变为fulfilled,后续打印res的then回调进入异步队列中,此时[p3, () => console.log(res)]

  8. 输出3,p5进入异步队列中

  9. 输出4

  10. 输出5,p6进入异步队列中

  11. 输出6

实现一个promise

使用方式

  1. 常规
new Promise((resolve, reject) => {
    resolve(1)
})
.then(res => console.log(res))
.catch(err => console.log(err))
  1. 抛出异常
new Promise((resolve, reject) => {
    throw new Error('test')
})
.then(res => console.log(res))
.catch(err => console.log(err))
  1. 异步resolve
new Promise((resolve, reject) => {
    setTimeout(() => resolve(1))
})
.then(res => console.log(res))
.catch(err => console.log(err))
  1. 链式调用
new Promise((resolve, reject) => {
    resolve(1)
})
.then(res => console.log(`1-then`, res))
.catch(err => console.log(`1-catch`, err))
.then(res => console.log(`2-then`,res))
.catch(err => console.log(`2-catch`,err))
.then(res => console.log(`3-then`,res))
.catch(err => console.log(`3-catch`,err))
  1. then参数可选
new Promise((resolve, reject) => {
    resolve(1)
})
.then()
.then()
.then(res => console.log(`3-then`,res))

实现一个简单的Promise

new Promise((resolve, reject) => {
resolve()
}).then(res => {
console.log('then', res)
}).catch(err => {
console.log('catch', err)
})

new Promise((resolve, reject) => {})

// 作为MyPromise的形参,这形参是一个函数,又有自己的形参,实参在函数执行的时候传入
function executor(resolve, reject) {}
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
const PENDING = 'pending'
function MyPromise(executor) {
    // 上下文共享状态
    const ctx = this.ctx = {
        value: '',
        reason: '',
        status: 'pending'
    }

    // resolve实现状态改变及保存数据到实例属性中
    const resolve = (value) => {
        // 状态变化只能从pending转变为fulfilled或者rejected,且更改后无法再次更改
        if(ctx.status === PENDING) {
            ctx.value = value
            ctx.status = FULFILLED
        }
    }

    // reject实现状态改变及保存数据到实例属性中
    const reject = (reason) => {
        // 状态变化只能从pending转变为fulfilled或者rejected,且更改后无法再次更改
        if(ctx.status === PENDING) {
            ctx.reason = reason
            ctx.status = REJECTED
        }
    }

    executor(resolve, reject)
}
MyPromise.prototype.then = function(res){
    if(this.ctx.status === PENDING) {
        console.log('then', res)
    }
}

MyPromise.prototype.catch = function(err){
    if(this.ctx.status === PENDING) {
        console.log('catch', err)
    }
}

异常捕获(try-catch)

关于异常捕获,有很多方式,比较被我们大家熟知的是window.onerror,try-catch,这里我们用try-catch。

  1. new Promise()抛出异常
new MyPromise((resolve, reject) => {
    throw new Error() 
})

function MyPromise(executor) {
    ...
    try {
        executor(resolve, reject)
    } catch(err) {
        reject(err)
    }
    
}

  1. then中onFulfilled及onRejected执行抛出异常
MyPromise.prototype.then = function(onFulfilled, onRejected) {
    if(this.ctx.status === FULFILLED) {
        try{
            onFulfilled(this.ctx.value)
        } catch(err) {
            console.log('catch error', err)
        }
    }
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {
    // 实现链式调用,同时确保每次返回的都是一个新的promise。
    const _promise = new MyPromise((resolve, reject) => {
        if(this.ctx.status === PENDING) {
            try{
                const result = onFulfilled(this.ctx.value)
                resolvePromise(promise2, x, resolve, reject);
            } catch(err) {
                reject(err)
            }
            
        } else if(this.ctx.status === FULFILLED) {
            onRejected(this.ctx.reason)
        } else if(this.ctx.status === REJECTED) {

        }
    })
    return _promise
}

P.then().catch()

异步resolve

我们在初始化Promise实例的时候,会遇到这种情况,我们不会立即resolve,而是等待一段时间的延迟或者等待接口请求完成后才resolve,而我们前面的实现都是同步的方式。

  • resolve职责
    在改动resolve实现逻辑之前,需要先明确resolve的职责:
    判断当前状态为pending,才去更改实例的状态为fulfilled(rejected);

  • then的职责:
    状态为fulfilled(rejected)才执行onFulfilled(onRejected)。

实例化时没有resolve()走到then函数中执行时,当前实例的状态是pending,这时then是不会执行onFulfilled函数的。

  • 框架的处理方式
    对于这种异步执行的场景,前端框架里面常用的方式是增加事件队列+异步处理来实现延迟,这些场景大多是同步的方式下加入宏微任务队列来达到延迟的目的。

  • 上下文共享状态(实例属性)
    但promise这里已经是异步,如果再加入异步,我们如何确保这两个异步的执行顺序?至于是否可以加入发布订阅模式来处理,这里不深入。回过头来看说明then在resolve前面执行,我们可以先将onFulfilled(onRejected)保存在this.ctx属性中到resolve中执行,在其前面加个判断条件执行。

const p = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve(1)
    }, 1000)
})
p.then(res => {

})

==========resolve改动===============
const onFulfilledQueue = []

const resolve = (value) => {
    if(ctx.status === PENGDING) {
        ctx.status = FULFILLED
        ctx.value = value
        if(onFulfilledQueue.length) {
            onFulfilledQueue.forEach(fn => fn())
        }
    }
}

==========then改动===============
MyPromise.prototype.then = function(onFulfilled, onRejected) {
    const _promise = new MyPromise((resolve, reject) => {
        if(this.ctx.status === PENDING) {
            onFulfilled && this.ctx.onFulfilledQueue.push(onFulfilled)
            onRejected && this.ctx.onRejectedQueue.push(onRejected)
        } else if(this.ctx.status === FULFILLED) {
            const res = onFulfilled(this.ctx.value)
            resolvePromise(x, resolve, reject)
        } else if(this.ctx.status === REJECTED) {
            onRejected(this.ctx.reason)
        }
    })
    return _promise
}

function resolvePromise(res, resolve, reject) {
    if(res instanceof MyPromise || (res?.then && type res.then === 'function')) {
        res.then(resolve)
    } else {
        resolve(res)
    }
}

then链式调用

  1. promiseA+规范中规定then返回一个新的promise实例
  • 关于链式调用,在js里面,我们的做法是在所有的方法中返回当前实例。但是在promise这里行不通;是因为返回当前promise实例,当前promise实例状态从pending->fulfilled,或者pending->rejected后无法再改变,后面将会一直走then的逻辑。

promise本身也不支持onFulfilled(onRejected)返回当前实例,返回则是会报错

var p = new Promise((resolve, reject) => {resolve(1)}).then(res => {console.log(res); return p})

// 报错信息
Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
  1. onFulfilled(onRejected)返回值
  • 返回一个promise实例
  • 返回一个类promise对象(拥有then属性)
  • 返回一个基本数据
  • 抛出一个异常
// 例子
new Promise((resolve, reject) => {
    resolve(1)
}).then(res => console.log('then', res)).catch(err => {console.log('catch', err); return err}).then(res => console.log('then2', res)).catch(err => {console.log('catch2', err)})

// then改版
MyPromise.prototype.then = function(onFulfilled, onRejected){
    const _promise = new Promise((resolve, reject) => {
        const { status, value, onFulfilledQueue, onRejectedQueue } = this.ctx;

        if (status === FULFILLED) {
            try{
                const x = onFulfilled(value)
                resolvePromise(x, resolve, reject)
            } catch (err) {
                reject(err)
            }
        } else if(status === REJECTED) {
            try{
                const x = onRejected(value)
                resolvePromise(x, resolve, reject)
            } catch (err) {
                reject(err)
            }
        } else if(status === PENDING) {
            onFulfilled && onFulfilledQueue.push(onFulfilled)
            onRejected && onRejectedQueue.push(onRejected)
        }
    })
    
}

then参数可选

// 例子
new Promise((resolve, reject) => {
 resolve(1)   
}).then().then().then().then().then(res => console.log('then5', res))

// catch
new Promise((resolve, reject) => {
    reject(1)
}).then(res => console.log('then', res)).catch().then(res => {console.log('then2', res); return res}).catch(err => {console.log('catch2', err)})


// then改动
MyPromise.prototype.then = function(onFulfilled, onRejected){
    onFulfilled = onFulfilled && typeof onFulfilled === 'function' ? onFulfilled : (value) => value;
    onRejected = onRejected && typeof onRejected === 'function' ? onRejected : (reason) => { throw reason };

    const _promise = new Promise((resolve, reject) => {
        

        if (status === FULFILLED) {
            try{
                const x = onFulfilled(value)
                resolvePromise(x, resolve, reject)
            } catch (err) {
                reject(err)
            }
        } else if(status === REJECTED) {
            try{
                const x = onRejected(value)
                resolvePromise(x, resolve, reject)
            } catch (err) {
                reject(err)
            }
        } else if(status === PENDING) {
            onFulfilled && onFulfilledQueue.push(onFulfilled)
            onRejected && onRejectedQueue.push(onRejected)
        }
    })
    
}

Promise.resolve()

MyPromise.prototype.resove = function(params) {
    const _promise = new MyPromise((resolve, reject) => {
        if(params instanceof MyPromise || typeof params?.then === 'function') {
            params.then(resolve)
        } else {
            resolve(params)
        }
    })
    return _promise
}

refrence

从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节
V8源码补充篇
Promise V8 源码分析(一)
chrome v8 promise源码
promiseA+规范

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions