diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..9a874b5 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad46b30 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..179a6bf --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 ZHIMIN.ZHANG + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9bd0bde..1729f8d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,21 @@ -# Promise3 -Try to implement a Promise which compliance to promise A/+ Spec. +# Promise +Learning Promise by actually implementing it. -It's my third implementation thus it's called Promise3. +## Study +1. `promise3.js` is the original version of the [Promise3](https://github.com/xieranmaya/Promise3) +2. `promise.js` is the revised version with verbose comments, which is easy to read. -And now, it's done! +## Test +```bash +npm i +npm test +``` -# How to run tests +## Reference -`npm install -g promises-aplus-tests` +[剖析Promise内部结构](https://github.com/xieranmaya/blog/issues/3) -- Article for Promise -`promises-aplus-tests ./Promise3.js` +[Promise3](https://github.com/xieranmaya/Promise3) -- source code + +[promises/A+](https://promisesaplus.com/) -- The standard +[promises-tests](https://github.com/promises-aplus/promises-tests) -- -- The test diff --git a/package.json b/package.json new file mode 100644 index 0000000..471f5c8 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "promise", + "version": "1.0.0", + "description": "Learning Promise by actually implementing it.", + "main": "index.js", + "scripts": { + "test": "promises-aplus-tests src/promise.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nzhl/promise.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/nzhl/promise/issues" + }, + "homepage": "https://github.com/nzhl/promise#readme", + "dependencies": { + "promises-aplus-tests": "^2.1.2" + } +} diff --git a/src/promise.js b/src/promise.js new file mode 100644 index 0000000..643c644 --- /dev/null +++ b/src/promise.js @@ -0,0 +1,192 @@ +var Promise = (function () { + // resolve 和 reject 都是异步的 + // 因为 executor 中就算执调用行了resolve + // 仍然要执行完executor之后的代码 + // 这点和throw 的机制不同 + function resolve (value) { + // 如果value是promise, 则一直展开 + // 直至value为非promise为止 + if (value instanceof Promise) { + return value.then(resolve.bind(this), reject.bind(this)) + } + setTimeout(() => { + if (this.status !== 'pending') return + this.status = 'resolved' + this.data = value + for (let each of this.onResolvedCallback) { + each(value) + } + }, 0) + } + + function reject (reason) { + // reject 和resolve 不同, + // 直接原样将值抛出 + setTimeout(() => { + if (this.status !== 'pending') return + this.status = 'rejected' + this.data = reason + for (let each of this.onRejectedCallback) { + each(reason) + } + }, 0) + } + + function resolvePromise (promise, x, resolve, reject) { + // 如果resolve 返回的是then刚返回的promise,直接报错 + // 因为这相当于链上连续两个相同的promise + // 具体情况可以看测试代码 + if (promise === x) { + return reject(new TypeError('Chaining cycle detected for promise!')) + } + + // 因为我们只能假设它是thenable, 但实际上这个then不知道是什么 + // 所以当then同时调用了 res, rej 的情况下 + // 我们以第一次调用的结果为准, 这也是为什么当 + // thenAlreadyCalledOrThrow 为true 时我们直接返回 + let thenAlreadyCalledOrThrow = false + if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) { + try { + let then = x.then + if (typeof then === 'function') { + // 一个非空的对象, 假设它是thenable来保证promise的兼容性 + // 如果有then, 说明他是 + then.call(x, function res (y) { + if (thenAlreadyCalledOrThrow) return + thenAlreadyCalledOrThrow = true + return resolvePromise(promise, y, resolve, reject) + }, function rej (r) { + if (thenAlreadyCalledOrThrow) return + thenAlreadyCalledOrThrow = true + return reject(r) + }) + } else { + // x对象没有then,说明不是,相当于then里面返回了一个 + // 正常的值, 所以直接resolve即可 + resolve(x) + } + } catch (e) { + if (thenAlreadyCalledOrThrow) return + thenAlreadyCalledOrThrow = true + return reject(e) + } + } else { + resolve(x) + } + } + + function Promise (executor) { + if (typeof executor !== 'function') { + // 非标准 但与Chrome谷歌保持一致 + throw new TypeError('Promise resolver ' + executor + ' is not a function') + } + + this.status = 'pending' + this.data = undefined + this.onResolvedCallback = [] + this.onRejectedCallback = [] + + // 实现过程中如果出现 Error, 直接reject. + try { + executor(resolve.bind(this), reject.bind(this)) + } catch (e) { + reject.bind(this)(e) + } + } + + Promise.prototype.then = function (onfulfilled, onrejected) { + // 返回值穿透 + if (typeof onfulfilled !== 'function') onfulfilled = data => data + // 错误穿透, 注意这里要用throw而不是return, 因为如果是return的话 + // 那么这个then返回的promise状态将变成resolved但是我们想要的是rejected + // 这样才能调用之后的onrejected + if (typeof onrejected !== 'function') onrejected = reason => { throw reason } + + if (this.status === 'resolved') { + let thenPromise = new Promise((resolve, reject) => { + // 使用箭头函数来保证this一直指向原Promise对象 + // 源代码中使用了this + // then函数返回时promise是同步的, 但执行then回调必须是异步的 + setTimeout(() => { + try { + // 这个onfulfilled 就是then的回调函数 + // 无论什么时候他必须异步 + // 当前this.status 是 resolved (rejeted 也一样) + // 说明此时的event loop已经不是promise状态改变的 + // 那个event loop了,所以此时要想 then代码异步, + // 必须加上setTimeout + // 而下面 this.state 是 ‘pending’ 则不同 + // 因为起码在该次event loop之内,promise的状态不会 + // 改变,所以已经确保了这个then起码会在下一个 + // event loop 才被调用, 也就是已经确保了异步 + var x = onfulfilled(this.data) + resolvePromise(thenPromise, x, resolve, reject) + } catch (e) { + reject(e) + } + }) + }) + return thenPromise + } + if (this.status === 'rejected') { + let thenPromise = new Promise((resolve, reject) => { + setTimeout(() => { + try { + var x = onrejected(this.data) + resolvePromise(thenPromise, x, resolve, reject) + } catch (e) { + reject(e) + } + }) + }) + return thenPromise + } + if (this.status === 'pending') { + // 这里之所以没有异步执行,是因为这些函数必然会被resolve或reject调用,而resolve或reject函数里的内容已是异步执行,构造函数里的定义 + // 以上是原解释, 其实是不完整的, 只是解释了为什么没必要添加setTimerout + // 但是并没有解释为什么添加之后是错的 (不信的可以拿源代码添加之后跑测试) + // 原因在于, 如果源代码在下一个event loop 完成了, 那么他会立即调用回调, + // 但是此时回调还没有被push到新的promise上 + let thenPromise = new Promise((resolve, reject) => { + this.onResolvedCallback.push(() => { + try { + var x = onfulfilled(this.data) + resolvePromise(thenPromise, x, resolve, reject) + } catch (e) { + reject(e) + } + }) + + this.onRejectedCallback.push(() => { + try { + var x = onrejected(this.data) + resolvePromise(thenPromise, x, resolve, reject) + } catch (e) { + reject(e) + } + }) + }) + return thenPromise + } + } + + // for prmise A+ test + Promise.deferred = Promise.defer = function () { + var dfd = {} + dfd.promise = new Promise(function (resolve, reject) { + dfd.resolve = resolve + dfd.reject = reject + }) + return dfd + } + + // for nodejs + if (typeof module !== 'undefined') { + module.exports = Promise + } + + return Promise +})() // IIFE for old browser + +// ES6 +// export default Promise diff --git a/Promise3.js b/src/promise3.js similarity index 64% rename from Promise3.js rename to src/promise3.js index 591707e..aef7de5 100644 --- a/Promise3.js +++ b/src/promise3.js @@ -1,5 +1,5 @@ -var Promise = (function() { - function Promise(resolver) { +var Promise = (function () { + function Promise (resolver) { if (typeof resolver !== 'function') { throw new TypeError('Promise resolver ' + resolver + ' is not a function') } @@ -9,8 +9,11 @@ var Promise = (function() { self.callbacks = [] self.status = 'pending' - function resolve(value) { - setTimeout(function() { + function resolve (value) { + if (value instanceof Promise) { + return value.then(resolve.bind(this), reject.bind(this)) + } + setTimeout(function () { if (self.status !== 'pending') { return } @@ -23,8 +26,8 @@ var Promise = (function() { }) } - function reject(reason) { - setTimeout(function(){ + function reject (reason) { + setTimeout(function () { if (self.status !== 'pending') { return } @@ -37,14 +40,14 @@ var Promise = (function() { }) } - try{ + try { resolver(resolve, reject) - } catch(e) { + } catch (e) { reject(e) } } - function resolvePromise(promise, x, resolve, reject) { + function resolvePromise (promise, x, resolve, reject) { var then var thenCalledOrThrow = false @@ -56,11 +59,11 @@ var Promise = (function() { try { then = x.then if (typeof then === 'function') { - then.call(x, function rs(y) { + then.call(x, function rs (y) { if (thenCalledOrThrow) return thenCalledOrThrow = true return resolvePromise(promise, y, resolve, reject) - }, function rj(r) { + }, function rj (r) { if (thenCalledOrThrow) return thenCalledOrThrow = true return reject(r) @@ -68,7 +71,7 @@ var Promise = (function() { } else { return resolve(x) } - } catch(e) { + } catch (e) { if (thenCalledOrThrow) return thenCalledOrThrow = true return reject(e) @@ -78,19 +81,19 @@ var Promise = (function() { } } - Promise.prototype.then = function(onResolved, onRejected) { - onResolved = typeof onResolved === 'function' ? onResolved : function(v){return v} - onRejected = typeof onRejected === 'function' ? onRejected : function(r){throw r} + Promise.prototype.then = function (onResolved, onRejected) { + onResolved = typeof onResolved === 'function' ? onResolved : function (v) { return v } + onRejected = typeof onRejected === 'function' ? onRejected : function (r) { throw r } var self = this var promise2 if (self.status === 'resolved') { - return promise2 = new Promise(function(resolve, reject) { - setTimeout(function() { + return promise2 = new Promise(function (resolve, reject) { + setTimeout(function () { try { var x = onResolved(self.data) resolvePromise(promise2, x, resolve, reject) - } catch(e) { + } catch (e) { return reject(e) } }) @@ -98,12 +101,12 @@ var Promise = (function() { } if (self.status === 'rejected') { - return promise2 = new Promise(function(resolve, reject) { - setTimeout(function() { + return promise2 = new Promise(function (resolve, reject) { + setTimeout(function () { try { var x = onRejected(self.data) resolvePromise(promise2, x, resolve, reject) - } catch(e) { + } catch (e) { return reject(e) } }) @@ -111,21 +114,21 @@ var Promise = (function() { } if (self.status === 'pending') { - return promise2 = new Promise(function(resolve, reject) { + return promise2 = new Promise(function (resolve, reject) { self.callbacks.push({ - onResolved: function(value) { + onResolved: function (value) { try { var x = onResolved(value) resolvePromise(promise2, x, resolve, reject) - } catch(e) { + } catch (e) { return reject(e) } }, - onRejected: function(reason) { + onRejected: function (reason) { try { var x = onRejected(reason) resolvePromise(promise2, x, resolve, reject) - } catch(e) { + } catch (e) { return reject(e) } } @@ -134,72 +137,72 @@ var Promise = (function() { } } - Promise.prototype.valueOf = function() { + Promise.prototype.valueOf = function () { return this.data } - Promise.prototype.catch = function(onRejected) { + Promise.prototype.catch = function (onRejected) { return this.then(null, onRejected) } - Promise.prototype.finally = function(fn) { + Promise.prototype.finally = function (fn) { // 为什么这里可以呢,因为所有的then调用是一起的,但是这个then里调用fn又异步了一次,所以它总是最后调用的。 // 当然这里只能保证在已添加的函数里是最后一次,不过这也是必然。 // 不过看起来比其它的实现要简单以及容易理解的多。 // 貌似对finally的行为没有一个公认的定义,所以这个实现目前是跟Q保持一致,会返回一个新的Promise而不是原来那个。 - return this.then(function(v){ + return this.then(function (v) { setTimeout(fn) return v - }, function(r){ + }, function (r) { setTimeout(fn) throw r }) } - Promise.prototype.spread = function(fn, onRejected) { - return this.then(function(values) { + Promise.prototype.spread = function (fn, onRejected) { + return this.then(function (values) { return fn.apply(null, values) }, onRejected) } - Promise.prototype.inject = function(fn, onRejected) { - return this.then(function(v) { - return fn.apply(null, fn.toString().match(/\((.*?)\)/)[1].split(',').map(function(key){ - return v[key]; + Promise.prototype.inject = function (fn, onRejected) { + return this.then(function (v) { + return fn.apply(null, fn.toString().match(/\((.*?)\)/)[1].split(',').map(function (key) { + return v[key] })) }, onRejected) } - Promise.prototype.delay = function(duration) { - return this.then(function(value) { - return new Promise(function(resolve, reject) { - setTimeout(function() { + Promise.prototype.delay = function (duration) { + return this.then(function (value) { + return new Promise(function (resolve, reject) { + setTimeout(function () { resolve(value) }, duration) }) - }, function(reason) { - return new Promise(function(resolve, reject) { - setTimeout(function() { + }, function (reason) { + return new Promise(function (resolve, reject) { + setTimeout(function () { reject(reason) }, duration) }) }) } - Promise.all = function(promises) { - return new Promise(function(resolve, reject) { + Promise.all = function (promises) { + return new Promise(function (resolve, reject) { var resolvedCounter = 0 var promiseNum = promises.length var resolvedValues = new Array(promiseNum) for (var i = 0; i < promiseNum; i++) { - (function(i) { - Promise.resolve(promises[i]).then(function(value) { + (function (i) { + Promise.resolve(promises[i]).then(function (value) { resolvedCounter++ resolvedValues[i] = value if (resolvedCounter == promiseNum) { return resolve(resolvedValues) } - }, function(reason) { + }, function (reason) { return reject(reason) }) })(i) @@ -207,43 +210,43 @@ var Promise = (function() { }) } - Promise.race = function(promises) { - return new Promise(function(resolve, reject) { + Promise.race = function (promises) { + return new Promise(function (resolve, reject) { for (var i = 0; i < promises.length; i++) { - Promise.resolve(promises[i]).then(function(value) { + Promise.resolve(promises[i]).then(function (value) { return resolve(value) - }, function(reason) { + }, function (reason) { return reject(reason) }) } }) } - Promise.resolve = function(value) { - var promise = new Promise(function(resolve, reject) { + Promise.resolve = function (value) { + var promise = new Promise(function (resolve, reject) { resolvePromise(promise, value, resolve, reject) }) return promise } - Promise.reject = function(reason) { - return new Promise(function(resolve, reject) { + Promise.reject = function (reason) { + return new Promise(function (resolve, reject) { reject(reason) }) } - Promise.fcall = function(fn){ + Promise.fcall = function (fn) { // 虽然fn可以接收到上一层then里传来的参数,但是其实是undefined,所以跟没有是一样的,因为resolve没参数啊 return Promise.resolve().then(fn) } - Promise.done = Promise.stop = function(){ - return new Promise(function(){}) + Promise.done = Promise.stop = function () { + return new Promise(function () {}) } - Promise.deferred = Promise.defer = function() { + Promise.deferred = Promise.defer = function () { var dfd = {} - dfd.promise = new Promise(function(resolve, reject) { + dfd.promise = new Promise(function (resolve, reject) { dfd.resolve = resolve dfd.reject = reject }) @@ -252,7 +255,7 @@ var Promise = (function() { try { // CommonJS compliance module.exports = Promise - } catch(e) {} + } catch (e) {} return Promise })()