前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
前言
面试官:请手写一个Promise
?(开门见山)
我:既然说到Promise
,那我肯定得先介绍一下JavaScript异步编程
的发展史吧,这样就理解为啥Promise会出现
以及Promise解决了什么问题
了吧。
- 阶段一:回调函数
- 阶段二:事件发布/订阅模型
- ...
面试官:我不关心什么异步编程发展史(不耐烦),这年头谁都知道Promise
是解决了回调地狱
的问题,我关心的是你的编码能力
,你直接show you code
,直接写!!!
我:好吧!(其实手写代码才是我的强项,嘻嘻!)
手写promise
先说下promise
的三种状态:
PENDING
:等待态,promise的初始状态FULFILLED
:成功态,promise调用resolve
函数后即会从PENDING等待态
变为FULFILLED成功态
。REJECTED
:失败态:promise调用reject
函数后即会从PENDING等待态
变为REJECTED失败态
注意:
- promise的状态一旦发生变更,便无法再更改。比如调用
resolve
从PEDING
变为FULFILLED
,它的状态就永远是FULFILLED
了,再调用reject也无法从FULFILLED
变成REJECTED
- 状态只能从
PENDING
变为FULFILLED
或REJECTED
,不能从FULFILLED
或REJECTED
返回到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方法
,并且会传入resolve
和reject
两个函数作为调用参数,另外在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) }) } } }
这里定义了两个数组onResolvedCallbacks
和onRejectedCallbacks
分别存储 then 里面成功的回调
和失败的回调
,然后再调用resolve
和reject
时分别循环执行这两个数组里存储的回调函数。
面试官:可以,那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) } }
- 首先,先判断新返回的一个promise
promise2
是不是等于x
,抛出错误UnhandledPromiseRejectionWarning: TypeError: Chaining cycle detected for promise #<Promise>
,这一步是防止内部的循环引用。 - 声明一个变量
called
,相当于加了一把锁,让promise
只能调用一次成功或者失败回调,防止死循环。 - 解析x,如果它的类型是
object
并且不为null
,或者它是一个函数
,并且它有then
方法,我们认为这是一个promise
- 递归解析,
then
里面再次调用resolvePromise
手写最后
因为promise
在EventLoop
里面是个微任务,不过我们可以简单通过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) } }
executor
执行时增加try catch
,防止执行用户传入的函数直接就报错了,这时我们应该直接reject
promise。- 调用
onFulfilled
和onRejected
时,需要包裹setTimeout
。 ok,这样就大功告成了。最后我们来测试下我们写的promise
是否符合规范。 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
。
完美,最后面试官向你伸出了大拇指!
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库