本文假设你有必定的Promise基础知识,不涉及api的讲解,可是对你深刻理解Promise有必定益处。git
在公司顶过几天面试官,一道手写Promise就卡主了很多人(受困于这道题的人别打我。。。我是不会告诉你我就任的公司的),其实这道题的主要目的是考察对Promise的理解,顺便的才是考察js逻辑,写出来是加分项,能表达出你对Promise的理解才是最重要的。github
可是现实状况是有挺多人直接白卷,把加分项变成了减分项。写不写是态度问题,写出几个点已经足以让面试官高看你一眼。下面我就把面试官但愿看到几个点拆解出来,以面试题的方式去理解Promise,但愿对大家有所帮助。面试
不想看长篇大论的能够直接查看总结:总结api
var p1 = new Promise(function(resolve, reject) {
throw Error('sync error')
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
复制代码
2.请写出下列代码的输出promise
var p1 = new Promise(function(resolve, reject) {
setTimeout(() => {
throw Error('async error')
})
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
复制代码
var p1 = new Promise(function(resolve, reject) {
resolve()
})
.then(res => {
throw Error('sync error')
})
复制代码
错误三连,你知道正确答案吗😏?浏览器
正确答案是:bash
这里考查的主要是Promise的错误捕获,其实仔细想一想js中能用的错误捕获也只能是try catch了,而try catch只能捕获同步错误,而且在没有传入错误监听的时候会将捕获到的错误抛出。app
因此在手写promise中,你至少要写出try catch包裹回调代调异步
function Promise(fn) {
...
doResolve(fn, this)
}
function doResolve(fn, self) {
try {
fn(function(value) {
...
},
function(reason) {
...
})
} catch(err) {
reject(self, err)
}
}
Promise.prototype.then = function(onFulfilled, onRejected) {
try {
...
onFulfilled(value)
} catch(err) {
reject(err)
}
};
function reject(self, newValue) {
...
if (!self._handled) {
Promise._unhandledRejectionFn(self._value);
}
}
复制代码
把上面的面试题改写一下:async
var p1 = new Promise(function(resolve, reject) {
resolve(1)
throw Error('sync error')
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
复制代码
var p1 = new Promise(function(resolve, reject) {
reject(2)
resolve(1)
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
复制代码
var p1 = new Promise(function(resolve, reject) {
resolve(1)
})
.then(res => {
throw Error('sync error')
console.log(res)
})
.catch(err => {
console.log(err)
})
复制代码
正确答案是:
Promise是一个有状态的容器,当状态被凝固了,后面的resolve或reject就不会被触发。简单的说就是同一个Promise只能触发一个状态监听(onFulfilled或onRejected)。因此在手写Promise中须要有一个状态标记:
function Promise(fn) {
...
this._state = 0 // 状态标记
doResolve(fn, this)
}
function doResolve(fn, self) {
var done = false // 保证只执行一个监听
try {
fn(function(value) {
if (done) return
done = true
resolve(self, value)
},
function(reason) {
if (done) return;
done = true
reject(self, value)
})
} catch(err) {
if (done) return
done = true
reject(self, err)
}
}
function resolve(self, newValue) {
try {
self._state = 1;
...
}
catch(err) {
reject(self, err)
}
}
function reject(self, newValue) {
self._state = 2;
...
if (!self._handled) {
Promise._unhandledRejectionFn(self._value);
}
}
复制代码
var p1 = new Promise(function(resolve, reject) {
resolve()
setTimeout(() => {
console.log(1)
})
console.log(2)
})
.then(res => {
console.log(3)
})
console.log(4)
复制代码
正确答案是:
依次输出:
2
4
3
1
复制代码
或
2
4
1
3
复制代码
首先 promise 中then、catch、finally中的回调都是异步执行的,因此前面输出2 4
的同步代码是没有疑问的。
那为何两种答案都认为是对的呢,实际上是由于polyfill的锅。正确的Promise输出应该是 2 4 3 1,缘由在于Promise.then是微任务执行的,微任务优先于宏任务执行(setTimeout就是宏任务)。
可是在polyfill中,浏览器环境是无法主动注册微任务的,因此一样是使用setTimeout调用then中的fn,一样是宏任务的状况下就只是队列的先进先出原则了,那么在promise-polyfill环境中输出 2 4 1 3也认为是正确的。
那么手写Promise中,应该将resolve,reject回调设为异步:
function handle(self, deferred) {
...
setTimeout(function() {
var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
if (cb === null) {
(self._state === 1 ? resolve : reject)(deferred.promise, self._value);
return;
}
var ret;
try {
ret = cb(self._value);
} catch (e) {
reject(deferred.promise, e);
return;
}
resolve(deferred.promise, ret);
}, 0)
...
}
复制代码
var p1 = new Promise(function(resolve, reject) {
reject(1)
})
.catch(err => {
console.log(err)
return 2
})
setTimeout(() => {
p1
.then(res => console.log(res))
}, 1000)
复制代码
正确答案是:
先输出 1
1秒后输出 2
Promise会将最后的值存储起来,若是在下次使用promise方法的时候回直接返回该值的promise。
因此手写一个Promise,你应该保存返回值:
function Promise(fn) {
...
this._state = 0 // 状态标记
this._value = undefined; // 存储返回值
doResolve(fn, this)
}
function resolve(self, newValue) {
try {
...
if (newValue instanceof Promise) {
self._state = 3;
self._value = newValue;
finale(self);
return;
} else if (typeof then === 'function') {
doResolve(bind(then, newValue), self);
return;
}
self._state = 1;
self._value = newValue;
...
}
catch(err) {
reject(self, err)
}
}
function reject(self, newValue) {
self._state = 2;
self._value = newValue;
...
if (!self._handled) {
Promise._unhandledRejectionFn(self._value);
}
}
复制代码
var p1 = new Promise(function(resolve, reject) {
reject(1)
})
.then(
res => {
console.log(res)
return 2
},
err => {
console.log(err)
return 3
}
)
.catch(err => {
console.log(err)
return 4
})
.finally(res => {
console.log(res)
return 5
})
.then(
res => console.log(res),
err => console.log(err)
)
复制代码
正确答案是:
依次输出:
1
undefined
3
复制代码
Promise可以链式调用的缘由是它的每个方法都返回新的promise,哪怕是finally方法,特殊的是finlly会返回上一个promise的值包装成的新promise,而且finally也不接收参数,由于不管Promise是reject仍是fulfill它都会被调用。
因此你须要在promise方法中返回新的promise:
function bind(fn, thisArg) {
return function() {
fn.apply(thisArg, arguments);
};
}
function resolve(self, newValue) {
...
try {
if (newValue instanceof Promise) {
self._state = 3;
self._value = newValue;
finale(self);
return;
} else if (typeof then === 'function') {
doResolve(bind(then, newValue), self);
return;
}
self._state = 1;
...
} catch (e) {
reject(self, e);
}
}
复制代码
上述总共表达了五个Promise知识点:
文中案例皆取自 promise-polyfill,有美玉在前,做者就不亮出本身的板砖了,同时也提醒各位面试者多看优秀做品的源码,何须看那些不太正规的第三方的实现。
毕竟公司的目标不是造重复的轮子,若是你已经能清晰明了地表述出上述部分知识,咱们就能相信你已是一个可以正确并灵活使用Promise的开发者了,及格分双手奉上(以咱们公司的招聘目标为例,相信大部分公司要求也是如此)。
最后:
立刻快到2019年了,祝你们都能找到趁心如意的工做!🎉🎉🎉
-- The End