面试官:请手写一个Promise

简介: 面试官:请手写一个Promise

前端面试题库 (面试必备)            推荐:★★★★★

地址:前端面试题库

前言

面试官:请手写一个Promise?(开门见山)

我:既然说到Promise,那我肯定得先介绍一下JavaScript异步编程的发展史吧,这样就理解为啥Promise会出现以及Promise解决了什么问题了吧。

  • 阶段一:回调函数
  • 阶段二:事件发布/订阅模型
  • ...

面试官:我不关心什么异步编程发展史(不耐烦),这年头谁都知道Promise是解决了回调地狱的问题,我关心的是你的编码能力,你直接show you code,直接写!!!

我:好吧!(其实手写代码才是我的强项,嘻嘻!)

手写promise

先说下promise的三种状态:

  • PENDING:等待态,promise的初始状态
  • FULFILLED:成功态,promise调用resolve函数后即会从PENDING等待态变为FULFILLED成功态
  • REJECTED:失败态:promise调用reject函数后即会从PENDING等待态变为REJECTED失败态

注意:

  1. promise的状态一旦发生变更,便无法再更改。比如调用resolvePEDING变为FULFILLED,它的状态就永远是FULFILLED了,再调用reject也无法从FULFILLED变成REJECTED
  2. 状态只能从PENDING变为FULFILLEDREJECTED,不能从FULFILLEDREJECTED返回到PENDING,这个也很好理解,状态只能前进不能倒退。

先看用法:

const p = new Promise((resolve, reject) => {
    resolve(111);
})
p.then((value) => {
    console.log(value)
}, (error) => {
    console.log(error)
})

首先,Promise肯定是一个类,所以我们才可以new它,然后Promise实例化的时候给它传入一个回调我们叫它executor方法,Promise内部会立即调用这个executor方法,并且会传入resolvereject两个函数作为调用参数,另外在Promise类的原型上应该提供一个then方法,它里面可以传入两个回调,分别为Promise成功的回调Promise失败的回调。调用resolve后会走入成功的回调中,调用reject后会走入失败的回调中

const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
class Promise {
    constructor(executor) {
        this.value = undefined
        this.reason = undefined
        this.status = PENDING
        const resolve = (value) => {
            if (this.status === PENDING) {
                this.value = value
                this.status = FULFILLED
                this.onResolvedCallbacks.forEach(fn => fn())
            }
        }
        const reject = (reason) => {
            if (this.status === PENDING) {
                this.reason = reason
                this.status = REJECTED
                this.onRejectedCallbacks.forEach(fn => fn())
            }
        }
        executor(resolve, reject);
    }
    then(onFulfilled, onRejected) {
       if (this.status === FULFILLED) {
        onFulfilled && onFulfilled(this.value)
       }
       if (this.status === REJECTED) {
        onRejected && onRejected(this.reason)
       }
    }
}
module.exports = Promise;

面试官:如果是异步调用resovle或者reject呢?

我:简单,用两个数组充当队列把then里边的回调存起来不就好了。

class Promise {
    constructor(executor) {
        // ...
        // 定义两个数组
        this.onResolvedCallbacks = [];
        this.onRejectedCallbacks = [];
        const resolve = (value) => {
            if (this.status === PENDING) {
                this.value = value
                this.status = FULFILLED
                this.onResolvedCallbacks.forEach(fn => fn())
            }
        }
        const reject = (reason) => {
            if (this.status === PENDING) {
                this.reason = reason
                this.status = REJECTED
                this.onRejectedCallbacks.forEach(fn => fn())
            }
        }
        // 默认执行executor函数,并传入resolve和reject函数
        executor(resolve, reject)
    }
    then(onFulfilled, onRejected) {
       if (this.status === FULFILLED) {
        onFulfilled && onFulfilled(this.value)
       }
       if (this.status === REJECTED) {
        onRejected && onRejected(this.reason)
       }
       if (this.status === PENDING) {
        this.onResolvedCallbacks.push(() => {
            onFulfilled(this.value)
        })
        this.onRejectedCallbacks.push(() => {
            onRejected(this.reason)
        })
       }
    }
}

这里定义了两个数组onResolvedCallbacksonRejectedCallbacks分别存储 then 里面成功的回调失败的回调,然后再调用resolvereject时分别循环执行这两个数组里存储的回调函数。

面试官:可以,那promise的链式调用是怎么实现的呢?

比如:下面这段代码:

const p = new Promise((resolve, reject) => {
   setTimeout(() => {
    resolve(111)
   }, 1000)
})
p.then((value1) => {
    console.log('value1', value1)
    return 222
}, (error1) => {
    console.log('error1', error1)
}).then((value2) => {
    console.log('value2', value2)
}, (error2) => {
    console.log('error2', error2)
})

它的打印结果为:

这个是如何实现的呢?

我:这个也简单,它内部调用then方法时,返回了一个新的promise,并让这个新的promise接管了它下一个then方法。

注意:这里不能返回this,这样会导致多个then方法全部受同一个promise控制。

class Promise {
    // ...
    then(onFulfilled, onRejected) {
       const promise2 = new Promise((resolve, reject) => {
        if (this.status === FULFILLED) {
            // onFulfilled方法可能返回值或者promise
            const x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
           }
           if (this.status === REJECTED) {
            // onRejected方法可能返回值或者promise
            const x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
           }
           if (this.status === PENDING) {
            this.onResolvedCallbacks.push(() => {
                const x = onFulfilled(this.value)
                resolvePromise(promise2, x, resolve, reject)
            })
            this.onRejectedCallbacks.push(() => {
                const x = onRejected(this.reason)
                resolvePromise(promise2, x, resolve, reject)
            })
           }
       })
       return promise2
    }
}

最核心的就是resolvePromise,来看下它做了什么:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        return reject(new TypeError('UnhandledPromiseRejectionWarning: TypeError: Chaining cycle detected for promise #<Promise>'))
    }
    let called
    // 判断x的类型 x是对象或函数才有可能是一个promise
    if (typeof x === 'object' && x !== null || typeof x === 'function') {
        try {
            const then = x.then
            if (typeof then === 'function') {
                // 只能认为它是一个promise
                then.call(x, (y) => {
                    if (called) return
                    called = true
                    resolvePromise(promise2, y, resolve, reject)
                }, (r) => {
                    if (called) return
                    called = true
                    reject(r)
                })
            }else {
                resolve(x)
            }
        } catch (e) {
            if (called) return
            called = true
            reject(e)
        }
    } else {
        resolve(x)
    }
}
  1. 首先,先判断新返回的一个promisepromise2是不是等于x,抛出错误UnhandledPromiseRejectionWarning: TypeError: Chaining cycle detected for promise #<Promise>,这一步是防止内部的循环引用。
  2. 声明一个变量called,相当于加了一把锁,让promise只能调用一次成功或者失败回调,防止死循环。
  3. 解析x,如果它的类型是object并且不为null,或者它是一个函数,并且它有then方法,我们认为这是一个promise
  4. 递归解析,then里面再次调用resolvePromise

手写最后

因为promiseEventLoop里面是个微任务,不过我们可以简单通过setTimout模拟。

然后我们再加上一些报错的捕获代码以及一些参数的兼容代码,以及实现catch方法。

class Promise {
    constructor(executor) {
        // ...
        // 这里增加try catch
        try {
            executor(this.resolve, this.reject)
        } catch (e) {
            reject(e)
        }
    }
    then(onFulfilled, onRejected) {
        // 这里兼容下 onFulfilled 和 onRejected 的传参
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
        onRejected = typeof onRejected === 'function' ? onRejected : err => {
            throw err
        }
        const promise2 = new Promise((resolve, reject) => {
            if (this.status === FULFILLED) {
                // 用 setTimeout 模拟异步
                setTimeout(() => {
                    try {
                        const x = onFulfilled(this.value)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                }, 0)
            }
            if (this.status === REJECTED) {
                // 用 setTimeout 模拟异步
                setTimeout(() => {
                    try {
                        const x = onRejected(this.reason)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                }, 0)
            }
            if (this.status === PENDING) {
                this.onResolvedCallbacks.push(() => {
                    // 用 setTimeout 模拟异步
                    setTimeout(() => {
                        try {
                            const x = onFulfilled(this.value)
                            resolvePromise(promise2, x, resolve, reject)
                        } catch (e) {
                            reject(e)
                        }
                    }, 0)
                })
                this.onRejectedCallbacks.push(() => {
                    // 用 setTimeout 模拟异步
                    setTimeout(() => {
                        try {
                            const x = onRejected(this.reason)
                            resolvePromise(promise2, x, resolve, reject)
                        } catch (e) {
                            reject(e)
                        }
                    }, 0)
                })
            }
        })
        return promise2
    }
    // catch函数实际上里面就是调用了then方法
    catch (errCallback) {
        return this.then(null, errCallback)
    }
}
  1. executor执行时增加try catch,防止执行用户传入的函数直接就报错了,这时我们应该直接rejectpromise。
  2. 调用onFulfilledonRejected时,需要包裹setTimeout。 ok,这样就大功告成了。最后我们来测试下我们写的promise是否符合规范。
  3. catch函数实际上里面就是调用了then方法,然后第一个参数传null

测试promise

promise是有规范的,即Promises/A+,我们可以跑一段脚本测试写的promise是否符合规范。

首先,需要在我们的promise增加如下代码:

// 测试脚本
Promise.defer = Promise.deferred = function () {
    let dfd = {}
    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve
        dfd.reject = reject
    })
    return dfd
}

然后安装promises-aplus-tests包,比如用npm可以使用命令npm install -g promises-aplus-tests安装到全局,然后使用命令promises-aplus-tests 文件名即可进行测试,里面有872测试用例,全部通过即可以认为这是一个标准的promise

完美,最后面试官向你伸出了大拇指!

前端面试题库 (面试必备)            推荐:★★★★★

地址:前端面试题库

相关文章
|
6月前
|
存储 前端开发 JavaScript
【面试题】Promise只会概念远远不够,还需这17道题目巩固!
【面试题】Promise只会概念远远不够,还需这17道题目巩固!
|
6月前
|
存储 前端开发 JavaScript
【面试题】面试官问:如果有100个请求,你如何使用Promise控制并发?
【面试题】面试官问:如果有100个请求,你如何使用Promise控制并发?
143 0
|
6月前
|
前端开发 JavaScript API
【面试题】说说 Promise是什么?如何使用
【面试题】说说 Promise是什么?如何使用
|
6月前
|
前端开发
【面试题】吃透Promise?先实现一个再说(包含所有方法)(二)
【面试题】吃透Promise?先实现一个再说(包含所有方法)(二)
|
6月前
|
存储 运维 前端开发
【面试题】吃透Promise?先实现一个再说(包含所有方法)(一)
【面试题】吃透Promise?先实现一个再说(包含所有方法)(一)
|
6月前
|
前端开发 JavaScript
【面试题】async/await、promise和setTimeout的执行顺序
【面试题】async/await、promise和setTimeout的执行顺序
|
6月前
|
存储 前端开发 JavaScript
面试官问:如果有100个请求,你如何使用Promise控制并发?
面试官问:如果有100个请求,你如何使用Promise控制并发?
323 0
|
6月前
|
前端开发 JavaScript
No101.精选前端面试题,享受每天的挑战和学习(Promise)
No101.精选前端面试题,享受每天的挑战和学习(Promise)
|
6月前
|
前端开发 JavaScript API
【面试题】面试官:为什么Promise中的错误不能被try/catch?
【面试题】面试官:为什么Promise中的错误不能被try/catch?
|
6月前
|
前端开发 JavaScript 开发者
【面试题】前端人70%以上 不了解的promise/async await
【面试题】前端人70%以上 不了解的promise/async await
135 0

热门文章

最新文章