Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

手写Promise、then、catch、finally、all、race #19

Open
zxyue25 opened this issue Feb 27, 2022 · 0 comments
Open

手写Promise、then、catch、finally、all、race #19

zxyue25 opened this issue Feb 27, 2022 · 0 comments
Labels
原创 Something isn't working

Comments

@zxyue25
Copy link
Owner

zxyue25 commented Feb 27, 2022

0、前提

我们想要手写一个Promise,就要遵循 Promise/A+ 规范,业界所有 Promise的类库都遵循这个规范

本篇文章写如何手写promise及其周边方法,每个方法从“定义->案例->实现”的思路展开

阅读本文前建议熟悉阮一峰老师的 ECMAScript 6 入门promise章节,熟悉promise的语法

本文提纲如下,基本覆盖到promise面试考点
image.png

1、基本封装

1.1 基本

定义

  • Promise对象是一个构造函数,用来生成Promise实例
  • Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署
  • resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
  • reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
  • Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数
    实现
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

class Promise {
    constructor(executor) {
        this.status = PENDING
        this.value = undefined // 成功的值
        this.reason = undefined // 失败原因

        // 成功函数
        let resolve = (value) => {
            if (this.status === PENDING) {
                this.value = value
                this.status = RESOLVED
                this.onFulfilledCallbacks.forEach(fn => fn());
            }
        }

        // 失败函数
        let reject = (reason) => {
            if (this.status === PENDING) {
                this.reason = reason
                this.status = REJECTED
                this.onRejectCallbacks.forEach(fn => fn())
            }
        }

        try {
            executor(resolve, reject) // 默认执行器立即执行
        }
        catch (e) {
            reject(e) // 如果立即执行函数发生错误等价于调用失败函数
        }
    }
    
    then (onFulfilled, onReject) {
        // 同步
        if (this.status === RESOLVED) {
            onFulfilled(this.value)
        }
        if (this.status === REJECTED) {
            onReject(this.reason)
        }

        // 异步订阅
        if (this.status === PENDING) { 
            this.onFulfilledCallbacks.push(() => onFulfilled(this.value))
            this.onRejectCallbacks.push(() => onReject(this.reason))
        }
    }
}

module.exports = Promise

1.2 promise参数入参是一个异步操作

现象

在 executor()中传入一个异步操作

const promise = new Promise((resolve, reject) => {
    // 异步的情况
    setTimeout(() => {
        reject(1)
    }, 1000)
})

promise.then(data => {
    console.log(data)
}, err => {
    console.log('err', err)
})
// 输出结果 err 1

实现
promise调用then方法时,当前的promise并没有成功,一直处于pending状态,所以如果当调用 then方法时,当前状态是pending,需要先将成功和失败的回调分别存放起来,在executor()的异步任务被执行时,触发resolve或reject,依次调用成功或失败的回调

class Promise {
    constructor(executor) {
        ...
        // 处理异步
        this.onFulfilledCallbacks = []
        this.onRejectCallbacks = []

        // 成功函数
        let resolve = (value) => {
            if (this.status === PENDING) {
                this.value = value
                this.status = RESOLVED
              ++this.onFulfilledCallbacks.forEach(fn => fn());
            }
        }

        // 失败函数
        let reject = (reason) => {
            if (this.status === PENDING) {
                this.reason = reason
                this.status = REJECTED
              ++this.onRejectCallbacks.forEach(fn => fn())
            }
        }

        try {
            executor(resolve, reject) // 默认执行器立即执行
        }
        catch (e) {
            reject(e) // 如果立即执行函数发生错误等价于调用失败函数
        }
    }
    
    then (onFulfilled, onReject) {
        ...
        // 异步订阅
        if (this.status === PENDING) { 
            this.onFulfilledCallbacks.push(() => onFulfilled(this.value))
            this.onRejectCallbacks.push(() => onReject(this.reason))
        }
    }
}

2、Promise.prototype.then

2.1 then的链式调用

定义

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法
现象

下面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数

const p = new Promise((resolve, reject) => {
    resolve(1001)
})

p.then(data => {
    console.log(data)
}, err => {
    console.log('err', err)
}).then(data => {
    console.log(data)
}, err => {
    console.log(err)
})
// 输出结果
// 1001
// undefined

实现

定义一个新的promise实例 promise2,并返回

then函数的返回值可能值一个普通值,也可能是一个对象,因此需要根据x的类型去处理then,引入resolvePromise方法统一处理,具体可看下一节2.2

为什么要用setTimeout:利用eventLoop,宏任务,代码块延迟执行,等new完promise2,不然resolvePromise(promise2, x, resolve, reject)取不到promise2会报错

then (onFulfilled, onReject) {
    let promise2 = new Promise((resolve, reject) => {
        // 同步
        if (this.status === RESOLVED) {
            // 写setTimeout,利用eventLoop,宏任务,代码块延迟执行,等new完promise2
            setTimeout(() => {
                try {
                    let x = onFulfilled(this.value)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (e) {
                    reject(e)
                }
            }, 0)
        }
        if (this.status === REJECTED) {
            setTimeout(() => {
                try {
                    let x = onReject(this.reason)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (e) {
                    reject(e)
                }
            }, 0)
        }

       // 异步订阅
       if (this.status === PENDING) {
           this.onFulfilledCallbacks.push(() => {
               setTimeout(() => {
                   try {
                       let x = onFulfilled(this.value)
                       resolvePromise(promise2, x, resolve, reject)
                   } catch (e) {
                       reject(e)
                   }
               }, 0)
           })
           this.onRejectCallbacks.push(() => {
               setTimeout(() => {
                   try {
                       let x = onReject(this.reason)
                       resolvePromise(promise2, x, resolve, reject)
                   }
                   catch (e) {
                       reject(e)
                   }
               }, 0)
           })
       }
    })
    return promise2
}

2.2 then函数返回值类型

前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用

(1)x跟promise2不能只一个东西

现象

如下代码,promise2的then函数返回值为promise2

输出结果是[TypeError: Chaining cycle detected for promise #<Promise>]

// x跟promise2不能是一个东西
const p = new Promise((resolve, reject) => {
    resolve()
})

let promise2 = p.then(() => {
    return promise2
})

promise2.then(null, err => {
    console.log(err)
})

实现

可以比喻为,A等A买菜回来,这是不可能的,所以直接报错

const resolvePromise = (promise2, x, resolve, reject) => {
    if (promise2 === x) {
        // 判断x的值与promise是否是同一个,如果是同一个,就不用等待了,直接出错即可
        return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
    }
}
(2)x返回值可能是promise也可能是普通值

x可能的值

  • promise
    • 执行x的then方法,返回相应值
  • 普通值
    • 直接resolve
      现象
// then函数的返回值还是一个promise
const p = new Promise((resolve, reject) => {
    resolve(100)
})

p.then(data => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('success')
        }, 100)
    })
}, err => {
    console.log(err)
}).then(data => {
    console.log(data)
}, err => {
    console.log(err)
})
// 输出结果:success

// then函数中的resolve还是promise
const p = new Promise((resolve, reject) => {
    resolve(100)
})

p.then(data => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve('success')
                }, 0)
            }))
        }, 100)
    })
}, err => {
    console.log(err)
}).then(data => {
    console.log(data)
}, err => {
    console.log(err)
})
// 输出结果:success

实现

先判断x是否是object或者function

    • x上的then
      • 取成功
        • 判断then是否是function
          • 是,执行thenresolve成功函数yreject失败函数r成功函数y可能还是一个promise,递归执行resolvePromise(promise2, y, resolve, reject)
          • 不是,说明x只是普通的对象,比如{then: 1},直接reslove(x)
      • 取失败
        • 直接reject(e)
  • 不是
    • x普通值,直接resolve(x)
const resolvePromise = (promise2, x, reslove, reject) => {
    ...
     if (typeof x === 'object' && x !== null || typeof x === 'function') {
        try {
            let then = x.then // 取then可能这个then属性是通过defineProperty来定义的,可能报错
            if (typeof then === 'function') { // 当有then方法,则认为x是一个promise
                then.call(x, y => {
                     // y可能还是一个promise值,递归,直到解析出来的值是一个普通值
                    resolvePromise(promise2, y, resolve, reject) // 采用promise的成功结果将值下传递
                }, reject => {
                    reject(x) // 采用失败结果将值向下传递
                })
            } else {
                resolve(x) // x是一个普通对象,比如{then: 1}
            }
        } catch (e) {
            reject(e)
        }
    } else {
        resolve(x) // x是一个普通值,直接成功即可
    }
}
(3)then的两个参数是可选的,如果不写默认resolve(data)

定义

then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的
现象

如下代码,p.then().then()省略参数

  • reslove时,最后一个thenresolve输出data依旧可以获取到数据

    • 其实相当于p.then(data => {return data}).then(data => {return data})
  • reject时,最后一个thenreject输出err依旧可以获取数据

    • 其实相当于p.then(null, err => { throw err }).then(null, err => { throw err })
// 案例
// then函数中的resolve和reject是可选参数
const p = new Promise((resolve, reject) => {
    resolve(100)
})

p.then().then().then(data => {
    console.log(data)
})

// p.then(data => {return data}).then(data => {return data}).then(data => {
//     console.log(data)
// })

// p.then(null, err => { throw err }).then(null, err => { throw err }).then(null, err => {
//     console.log('err', err)
// })

// 输出结果:
// resolbe 100
// reject err 100

实现

then(onFulfilled, onReject){
    // onFulfilled、onReject是可选参数
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : data => data
    onReject = typeof onReject === 'function' ? onReject : err => { throw err }
    ...
}

3、检测Promise是否符合规范

Promise/A+规范提供了一个专门的测试脚本,可以测试所编写的代码是否符合Promise/A+的规范

step1 全局安装包promises-aplus-tests

npm i -g promises-aplus-tests

step2 在primes文件最下方写入以下内容

Promise.defer = Promise.deferred = function () {
    let dfd = {}
    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve
        dfd.reject = reject
    })
    return dfd
}

step3 执行命令检测

promises-aplus-tests promise/promise.js

可以看到检测全部通过
image.png

4、Promise.all

定义

  • Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例
  • Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理
  • p的状态由p1p2p3决定,分成两种情况
    • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。
    • 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数
const p = Promise.all([p1, p2, p3]);

现象

Promise.all([1, 2, 3, 4]).then(data => {
    console.log(data)
})
// 输出 [ 1, 2, 3, 4 ]

Promise.all([1, 2, new Promise((resolve) => { resolve(300) }), 4]).then(data => {
    console.log(data)
})
// 输出 [ 1, 2, 300, 4 ]

实现

const isPromise = (data) => {
    if (typeof data === 'object' && data !== null || typeof data === 'function') {
        if (typeof data.then === 'function') {
            return true
        }
    }
    return false
}

Promise.all = function (promiseArr) {
    return new Promise((resolve, reject) => {
        let arr = []
        let index = 0

        function processData (i, data) {
            arr[i] = data
            if (++index === promiseArr.length) { // 不能用arr.length === promiseArr.length;因为promiseArr中有异步promise的话,arr不会按照顺序被塞进返回值
                resolve(arr)
            }
        }
        for (let i = 0; i < promiseArr.length; i++) {
            let current = promiseArr[i]
            if (isPromise(current)) {
                // 如果是promis,执行then
                current.then(data => {
                    processData(i, data)
                }, err => {
                    console.log(err)
                    reject(err)
                })
            } else {
                // 如果不是promise,直接返回
                processData(i, current)
            }
        }
    })
}

5、Promise.resolve & Promise.reject

resolve

将现有对象转为Promise对象

Promise.resolve = function (value) {
    if (value instanceof Promise) return value
    return new Promise(resolve => resolve(value))
}

reject

会返回一个新的 Promise 实例,该实例的状态为rejected

Promise.reject = function (reason) {
    return new Promise(_, reject => reject(reason))
}

6、Promise.prototype.finally

定义

  • finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作
  • finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果

finally本质上是then方法的特例

promise
.finally(() => {
  // 语句
});

// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

现象

const p = new Promise((resolve, reject) => {
    resolve(100)
})

p.finally(() => {
    console.log('finally')
}).then(data => {
    console.log(data)
}, err => {
    console.log('err', err)
})
// 输出结果
// finally
// 100

const p = new Promise((resolve, reject) => {
    reject(100)
})

p.finally(() => {
    console.log('finally')
}).then(data => {
    console.log(data)
}, err => {
    console.log('err', err)
})
// 输出结果
// finally
// err 100


const p = new Promise((resolve) => {
    resolve(100)
})

p.finally(() => {
    console.log('finally')
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve()
        }, 4000)
    })
}).then(data => {
    console.log(data)
})
// 输出结果
// finally
// 等四秒钟
// 100

实现

Promise.prototype.finally = function(callback){
    return this.then(
        data => Promise.resolve(callback()).then(() => data),
        err => Promise.resolve(callback()).then(() => {throw err} )
    )
}

7、Promise.prototype.catch

Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数
现象

const p = new Promise((_, resolve) => {
    throw 'err'
}).catch((e) => {
    console.log(e)
})
// 输出 err

实现

Promise.prototype.catch  = function (onRejected) {
    return this.then(null, onRejected);
}

8、Promise.race

现象
注意,下面代码第二个例子输出的是300,而非1

Promise.race([1, new Promise((resolve) => { resolve(300) }), 2, 4]).then(data => {
    console.log(data)
})
// 输出 1


Promise.race([new Promise((resolve) => { resolve(300) }), 1, 2, 4]).then(data => {
    console.log(data)
})
// 输出 300

实现

Promise.race = function (promiseArr) {
    return new Promise((resolve, reject) => {
        for (let i = 0; i < promiseArr.length; i++) {
            const current = promiseArr[i];
            Promise.resolve(current).then(resolve, reject);
        }
    });
}

写在最后

本篇文章所有的代码在github/zxyue25

参考

@zxyue25 zxyue25 added the 原创 Something isn't working label Feb 27, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
原创 Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant