手写一个自己的 JavaScript Promise 类库
终于有时间写这篇文章了, ES2015 推出了JS 的 Promise ,而在没有原生支持的时候,我们也可以使用诸如 Promises/A+ 的库的帮助,在我们的代码里实现Promise 的支持;
如何使用 Promise
在讲具体实现之前我们还是先了解下我们如何使用 Promise 在我们的代码中。
function getData() { return new Promise((resolve, reject)=>{ request( your_url, (error, res, movieData)=>{ if (error) reject(error); else resolve(movieData); }); }); } // 使用 getData getData().then(data => console.log(data)) .catch(error => console.log(error));
例子我们需要使用 request 模块去请求一个地址,然后拿到响应的数据。由于 request 过程是一个异步的过程。因此我们使用了 promise 来实现。在使用 promsie 的 then 方法拿到 resolve 回来的的数据。
开始基本实现
首先我们定义一个基本的 Promise 类,为了区别于原生的 Promise 我们可以自定义一个 JPromise 类。
function Promise(fn) { var callback = null; this.then = function(cb) { callback = cb; }; function resolve(value) { callback(value); } fn(resolve); }
我们这个时候先添加一些简单的测试用例。
/** test a single plugin */ import { expect } from 'chai'; import JPromise from '../../src/index'; describe('your own promise library unit tests', () => { let data = 1; const p = new JPromise((resolve) => { resolve(5); }); it('has then method', () => { expect(typeof p.then === 'function').to.equal(true); }); it('excute a then function', () => { p.then((val) => data = val); expect(data).to.equal(5); }); });
但是这样写法有一个问题就是你会发现他在 resolve() 会在 then 之前调用。意味着 callback 会是 null .暂时我们用 setTimeout 来 hack 这个问题。
function Promise(fn) { var callback = null; this.then = function(cb) { callback = cb; }; function resolve(value) { setTimeout(function() { callback(value); }, 1); } fn(resolve); }
但实际我们在测试的过程中,会发现这样只是规避 resolve 先执行的问题,后面会继续说如何解决上面的问题。
再进一步,我们需要 Promise 有状态。
- Promise 的状态可以是 pending 等待一个值,也可以是 resolved 返回一个值
- 一旦 Promise 返回一个值,则后面将不再返回
function JPromise(fn) { let state = 'pending'; let deferred = null; let value = null; function resolve(newVal) { value = newVal; state = 'resolved'; if (deferred) { handle(deferred); } } function handle(onResolved) { if (state === 'pending') { deferred = onResolved; return; } onResolved(value); } this.then = function(onResolved) { handle(onResolved); }; fn(resolve); } module.exports = JPromise;
通过状态,我们可以确保每次函数触发 then 方法的时候, resolve 始终都能够被调用到。
似乎这个时候我们的测试可以跑通了。但是,远远没有结束,我们要确保 Promise 的链式调用。
getSomeData() .then(filterTheData) .then(processTheData) .then(displayTheData);
我们可能经常会看到这样的写法。很显然刚刚的不合适了。函数会一次执行 then 方法,然后等待 传入 resolve 里的函数执行完毕,然后执行下一个 then 方法 。
如果熟悉 jQuery 链式调用 的话,可以很好的理解。
我们会在 then 的方法里面返回 Promise 对象。
function JPromise(fn) { let state = 'pending'; let deferred = null; let value = null; function resolve(newVal) { value = newVal; state = 'resolved'; if (deferred) { handle(deferred); } } function handle(handler) { if (!handler.onResolved) { handler.resolve(value); return; } const ret = handler.onResolved(value); handler.resolve(ret); } this.then = function(onResolved) { return new Promise((resolve) => { handle({ onResolved, resolve, }); }); }; fn(resolve); } module.exports = JPromise;
从上面代码我们中可以看出,我们在调用 then 的方法适合,始终都会创建一个 Promise 对象。而使用回调的时候则可以避免这样一个问题。
那么第二个 Promise resolve 传入的实际上是第一个 Promise 返回的值。
const ret = handler.onResolved(value);
我们会将函数执行的值,带入下一个 resolve 中。由于 then() 方法始终都是返回一个 Promsie
当然 then() 方法也允许你不传入 callback 进去,比如下面这样:
doSomething().then().then(function(result) { console.log('got a result', result); });
当然这些就是程序兼容性的处理。我们需要在没有传入的时候将之前保留的函数进行触发
if(!handler.onResolved) { handler.resolve(value); return; }
对 reject 进行支持
如果遇到问题了,我们需要对这些进行抛出,外部能够获取到这些问题。
doSomething().then(function(value) { console.log('Success!', value); }, function(error) { console.log('Uh oh', error); });
如同上面实现的一样我们需要将内部的 state 置位 rejected。
function doSomething() { return new Promise(function(resolve, reject) { var result = somehowGetTheValue(); if(result.error) { reject(result.error); } else { resolve(result.value); } }); }
参考 resolve 的实现,
function JPromise(fn) { let state = 'pending'; let deferred = null; let value = null; function resolve(newVal) { value = newVal; state = 'resolved'; if (deferred) { handle(deferred); } } function reject(reason) { state = 'rejected'; value = reason; if (deferred) { handle(deferred); } } function handle(handler) { if (state === 'pending') { deferred = handler; return; } let handlerCallback; if (state === 'resolved') { handlerCallback = handler.onResolved; } else { handlerCallback = handler.onRejected; } if (!handlerCallback) { if (state === 'resolved') { handler.resolve(value); } else { handler.reject(value); } return; } const ret = handlerCallback(value); handler.resolve(ret); } this.then = function(onResolved, onRejected) { return new Promise((resolve, rejected) => { handle({ onResolved, onRejected, resolve, rejected }); }); }; fn(resolve, reject); } module.exports = JPromise;
实现到这里的时候,我们还需要对异常的错误进行处理;在 Promise 用 catch 进行捕获。
function resolve(newValue) { try { // ... as before } catch(e) { reject(e); } }
除此之外,我们还得防止 在执行回调的时候,在被调用的过程中出现异常。
function handle(deferred) { // ... as before let ret; try { ret = handlerCallback(value); } catch(e) { handler.reject(e); return; } handler.resolve(ret); }
似乎感觉实现的差不多了。但是刚才的实现有一个不好的地方,就是 Promise 中用了 try…catch… 。这样 Promise 会吞噬错误。也就是:
function getSomeJson() { return new Promise(function(resolve, reject) { var badJson = "uh oh, this is not JSON at all!"; resolve(badJson); }); } getSomeJson().then(function(json) { var obj = JSON.parse(json); console.log(obj); }, function(error) { console.log('uh oh', error); });
这个 JSON 解析的时候会出现错误,但实际上我们在控制台看不到这样的错误。如果你想捕获这些错误,你需要添加一个错误处理的函数:
getSomeJson().then(function(json) { var obj = JSON.parse(json); console.log(obj); }).then(null, function(error) { console.log("an error occured: ", error); });
Promise 需要异步处理
前面我们提到了使用 setTimeout 去故意延迟执行,随后我们通过 state 去克服了这么一个问题,但是 Promises/A+ 的定义额 promise 的解决方案的确是异步执行的。这样我们可以利用 setTimeout 去包裹 handle 里的 resovle 执行;
function handle(handler) { if(state === 'pending') { deferred = handler; return; } setTimeout(function() { // ... as before }, 1); }
实际上,在实现过程中,我们可以使用 setImmediate 而不是 setTimeout。
var promise = doAnOperation(); invokeSomething(); promise.then(wrapItAllUp); invokeSomethingElse();
上面的代码,我们预期的效果执行流程
doAnOperation -> invokeSomething -> invokeSomethingElse -> wrapItAllUp
然而如果 doAnOperation 并非异步执行的函数的话,可能实际效果是:
doAnOperation -> invokeSomething -> wrapItAllUp -> invokeSomethingElse
这就是为什么 promise 始终都会去异步执行传入的回调。
相关推荐
-
如何使用24行JavaScript代码实现Redux javascript/jquery
2020-5-20
-
小技巧:字符串数组初始化 javascript/jquery
2019-8-16
-
浅谈 HTTP/2 Server Push javascript/jquery
2019-3-6
-
面试招聘:2 年招到 18 个前端教你怎么招人 javascript/jquery
2020-6-11
-
Ajax基础知识梳理 javascript/jquery
2018-10-10
-
浏览器渲染原理 javascript/jquery
2019-5-14
-
教你用开源 JS 库快速画出 GitHub 章鱼猫 javascript/jquery
2019-10-4
-
转眼人到中年:前端老程序员无法忘怀的一次百度电话面试 javascript/jquery
2019-5-12
-
你真的了解 console 吗 javascript/jquery
2018-10-11
-
xss攻击执行原理分析 javascript/jquery
2019-9-1