转自面试官眼中的Promise
说明
本文假设你有一定的Promise基础知识,不涉及api的讲解,但是对你深入理解Promise有一定益处。
写在前面
在公司顶过几天面试官,一道手写Promise就卡主了不少人(受困于这道题的人别打我。。。我是不会告诉你我就职的公司的),其实这道题的主要目的是考察对Promise的理解,顺便的才是考察js逻辑,写出来是加分项,能表达出你对Promise的理解才是最重要的。
但是现实情况是有挺多人直接白卷,把加分项变成了减分项。写不写是态度问题,写出几个点已经足以让面试官高看你一眼。下面我就把面试官希望看到几个点拆解出来,以面试题的方式去理解Promise,希望对你们有所帮助。
不想看长篇大论的可以直接查看总结:总结
Promise 特性
Promise捕获错误与 try catch 等同
- 请写出下列代码的输出
var p1 = new Promise(function(resolve, reject) {throw Error('sync error')
}).then(res => {console.log(res)}).catch(err => {console.log(err)})
复制代码复制代码
2.请写出下列代码的输出
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') })
复制代码复制代码
错误三连,你知道正确答案吗??
正确答案是:
- Error被catch到,最后console.log输出
- 错误无法被catch,控制台报错
- promise没有catch,错误被捕获后又被抛出,控制台报错
这里考查的主要是Promise的错误捕获,其实仔细想想js中能用的错误捕获也只能是try catch了,而try catch只能捕获同步错误,并且在没有传入错误监听的时候会将捕获到的错误抛出。
所以在手写promise中,你至少要写出try catch包裹回调代调
function Promise(fn) {...doResolve(fn, this)}
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">doResolve</span>(<span class="hljs-params">fn, self</span>) </span>{<span class="hljs-keyword">try</span> {fn(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">value</span>) </span>{...},<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">reason</span>) </span>{...})} <span class="hljs-keyword">catch</span>(err) {reject(self, err)}
}<span class="hljs-built_in">Promise</span>.prototype.then = <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">onFulfilled, onRejected</span>) </span>{<span class="hljs-keyword">try</span> {...onFulfilled(value)} <span class="hljs-keyword">catch</span>(err) {reject(err)}
};<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">reject</span>(<span class="hljs-params">self, newValue</span>) </span>{...if (!self._handled) {<span class="hljs-built_in">Promise</span>._unhandledRejectionFn(self._value);}
}
复制代码
复制代码
复制代码复制代码
Promise 拥有状态变化
把上面的面试题改写一下:
- 请写出下列代码的输出
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)})
复制代码复制代码
正确答案是:
- 输出 1
- 输出 2
- console.log输出错误
Promise是一个有状态的容器,当状态被凝固了,后面的resolve或reject就不会被触发。简单的说就是同一个Promise只能触发一个状态监听(onFulfilled或onRejected)。所以在手写Promise中需要有一个状态标记:
function Promise(fn) {...this._state = 0 // 状态标记doResolve(fn, this)}
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">doResolve</span>(<span class="hljs-params">fn, self</span>) </span>{<span class="hljs-keyword">var</span> done = <span class="hljs-literal">false</span> <span class="hljs-comment">// 保证只执行一个监听</span><span class="hljs-keyword">try</span> {fn(<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">value</span>) </span>{<span class="hljs-keyword">if</span> (done) <span class="hljs-keyword">return</span>done = <span class="hljs-literal">true</span>resolve(self, value)},<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">reason</span>) </span>{<span class="hljs-keyword">if</span> (done) <span class="hljs-keyword">return</span>;done = <span class="hljs-literal">true</span>reject(self, value)})} <span class="hljs-keyword">catch</span>(err) {<span class="hljs-keyword">if</span> (done) <span class="hljs-keyword">return</span>done = <span class="hljs-literal">true</span>reject(self, err)}
}<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">resolve</span>(<span class="hljs-params">self, newValue</span>) </span>{<span class="hljs-keyword">try</span> {self._state = <span class="hljs-number">1</span>;...}<span class="hljs-keyword">catch</span>(err) {reject(self, err)}
}<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">reject</span>(<span class="hljs-params">self, newValue</span>) </span>{self._state = <span class="hljs-number">2</span>;...if (!self._handled) {<span class="hljs-built_in">Promise</span>._unhandledRejectionFn(self._value);}
}
复制代码
复制代码
复制代码复制代码
Promise 方法中的回调是异步的
- 请写出下列代码的输出
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)...
}
复制代码
复制代码复制代码
Promise 会存储返回值
- 请写出下列代码的输出
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)}
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">resolve</span>(<span class="hljs-params">self, newValue</span>) </span>{<span class="hljs-keyword">try</span> {...if (newValue <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Promise</span>) {self._state = <span class="hljs-number">3</span>;self._value = newValue;finale(self);<span class="hljs-keyword">return</span>;} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> then === <span class="hljs-string">'function'</span>) {doResolve(bind(then, newValue), self);<span class="hljs-keyword">return</span>;}self._state = <span class="hljs-number">1</span>;self._value = newValue;...}<span class="hljs-keyword">catch</span>(err) {reject(self, err)}
}<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">reject</span>(<span class="hljs-params">self, newValue</span>) </span>{self._state = <span class="hljs-number">2</span>;self._value = newValue;...if (!self._handled) {<span class="hljs-built_in">Promise</span>._unhandledRejectionFn(self._value);}
}
复制代码
复制代码
复制代码复制代码
Promise 方法每次都返回一个新的Promise
- 请写出下列代码的输出
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方法,特殊的是fanilly会返回上一个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捕获错误与 try catch 等同
- Promise 拥有状态变化
- Promise 方法中的回调是异步的
- Promise 方法每次都返回一个新的Promise
- Promise 会存储返回值
文中案例皆取自 promise-polyfill,有美玉在前,作者就不亮出自己的板砖了,同时也提醒各位面试者多看优秀作品的源码,何必看那些不太正规的第三方的实现。
毕竟公司的目标不是造重复的轮子,如果你已经能清晰明了地表述出上述部分知识,我们就能相信你已经是一个能够正确并灵活使用Promise的开发者了,及格分双手奉上(以我们公司的招聘目标为例,相信大部分公司要求也是如此)。
最后:
马上快到2019年了,祝大家都能找到称心如意的工作!???
-- The End