Last active
September 16, 2017 18:17
-
-
Save RubaXa/8501359 to your computer and use it in GitHub Desktop.
«Promise.js» — is supported as a native interface and $.Deferred.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @author RubaXa <[email protected]> | |
* @license MIT | |
*/ | |
(function () { | |
"use strict"; | |
function _then(promise, method, callback) { | |
return function () { | |
var args = arguments, retVal; | |
/* istanbul ignore else */ | |
if (typeof callback === 'function') { | |
try { | |
retVal = callback.apply(promise, args); | |
} catch (err) { | |
promise.reject(err); | |
return; | |
} | |
if (retVal && typeof retVal.then === 'function') { | |
if (retVal.done && retVal.fail) { | |
retVal.__noLog = true; | |
retVal.done(promise.resolve).fail(promise.reject); | |
retVal.__noLog = false; | |
} | |
else { | |
retVal.then(promise.resolve, promise.reject); | |
} | |
return; | |
} else { | |
args = [retVal]; | |
method = 'resolve'; | |
} | |
} | |
promise[method].apply(promise, args); | |
}; | |
} | |
/** | |
* «Обещания» поддерживают как [нативный](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) | |
* интерфейс, так и [$.Deferred](http://api.jquery.com/category/deferred-object/). | |
* | |
* @class Promise | |
* @constructs Promise | |
* @param {Function} [executor] | |
*/ | |
var Promise = function (executor) { | |
var _completed = false; | |
function _finish(state, result) { | |
dfd.done = | |
dfd.fail = function () { | |
return dfd; | |
}; | |
dfd[state ? 'done' : 'fail'] = function (fn) { | |
/* istanbul ignore else */ | |
if (typeof fn === 'function') { | |
fn(result); | |
} | |
return dfd; | |
}; | |
var fn, | |
fns = state ? _doneFn : _failFn, | |
i = 0, | |
n = fns.length | |
; | |
for (; i < n; i++) { | |
fn = fns[i]; | |
/* istanbul ignore else */ | |
if (typeof fn === 'function') { | |
fn(result); | |
} | |
} | |
fns = _doneFn = _failFn = null; | |
} | |
function _setState(state) { | |
return function (result) { | |
if (_completed) { | |
return dfd; | |
} | |
_completed = true; | |
dfd.resolve = | |
dfd.reject = function () { | |
return dfd; | |
}; | |
if (state && result && result.then && result.pending !== false) { | |
// Опачки! | |
result.then( | |
function (result) { _finish(true, result); }, | |
function (result) { _finish(false, result); } | |
); | |
} | |
else { | |
_finish(state, result); | |
} | |
return dfd; | |
}; | |
} | |
var | |
_doneFn = [], | |
_failFn = [], | |
dfd = { | |
/** | |
* Добавляет обработчик, который будет вызван, когда «обещание» будет «разрешено» | |
* @param {Function} fn функция обработчик | |
* @returns {Promise} | |
* @memberOf Promise# | |
*/ | |
done: function done(fn) { | |
_doneFn.push(fn); | |
return dfd; | |
}, | |
/** | |
* Добавляет обработчик, который будет вызван, когда «обещание» будет «отменено» | |
* @param {Function} fn функция обработчик | |
* @returns {Promise} | |
* @memberOf Promise# | |
*/ | |
fail: function fail(fn) { | |
_failFn.push(fn); | |
return dfd; | |
}, | |
/** | |
* Добавляет сразу два обработчика | |
* @param {Function} [doneFn] будет выполнено, когда «обещание» будет «разрешено» | |
* @param {Function} [failFn] или когда «обещание» будет «отменено» | |
* @returns {Promise} | |
* @memberOf Promise# | |
*/ | |
then: function then(doneFn, failFn) { | |
var promise = Promise(); | |
dfd.__noLog = true; // для логгера | |
dfd | |
.done(_then(promise, 'resolve', doneFn)) | |
.fail(_then(promise, 'reject', failFn)) | |
; | |
dfd.__noLog = false; | |
return promise; | |
}, | |
notify: function () { // jQuery support | |
return dfd; | |
}, | |
progress: function () { // jQuery support | |
return dfd; | |
}, | |
promise: function () { // jQuery support | |
// jQuery support | |
return dfd; | |
}, | |
/** | |
* Добавить обработчик «обещаний» в независимости от выполнения | |
* @param {Function} fn функция обработчик | |
* @returns {Promise} | |
* @memberOf Promise# | |
*/ | |
always: function always(fn) { | |
dfd.done(fn).fail(fn); | |
return dfd; | |
}, | |
/** | |
* «Разрешить» «обещание» | |
* @param {*} result | |
* @returns {Promise} | |
* @method | |
* @memberOf Promise# | |
*/ | |
resolve: _setState(true), | |
/** | |
* «Отменить» «обещание» | |
* @param {*} result | |
* @returns {Promise} | |
* @method | |
* @memberOf Promise# | |
*/ | |
reject: _setState(false) | |
} | |
; | |
/** | |
* @name Promise#catch | |
* @alias fail | |
* @method | |
*/ | |
dfd['catch'] = function (fn) { | |
return dfd.then(null, fn); | |
}; | |
dfd.constructor = Promise; | |
// Работеам как native Promises | |
/* istanbul ignore else */ | |
if (typeof executor === 'function') { | |
try { | |
executor(dfd.resolve, dfd.reject); | |
} catch (err) { | |
dfd.reject(err); | |
} | |
} | |
return dfd; | |
}; | |
/** | |
* Дождаться «разрешения» всех обещаний | |
* @static | |
* @memberOf Promise | |
* @param {Array} iterable массив значений/обещаний | |
* @returns {Promise} | |
*/ | |
Promise.all = function (iterable) { | |
var dfd = Promise(), | |
d, | |
i = 0, | |
n = iterable.length, | |
remain = n, | |
values = [], | |
_fn, | |
_doneFn = function (i, val) { | |
(i >= 0) && (values[i] = val); | |
/* istanbul ignore else */ | |
if (--remain <= 0) { | |
dfd.resolve(values); | |
} | |
}, | |
_failFn = function (err) { | |
dfd.reject([err]); | |
} | |
; | |
if (remain === 0) { | |
_doneFn(); | |
} | |
else { | |
for (; i < n; i++) { | |
d = iterable[i]; | |
if (d && typeof d.then === 'function') { | |
_fn = _doneFn.bind(null, i); // todo: тест | |
d.__noLog = true; | |
if (d.done && d.fail) { | |
d.done(_fn).fail(_failFn); | |
} else { | |
d.then(_fn, _failFn); | |
} | |
d.__noLog = false; | |
} | |
else { | |
_doneFn(i, d); | |
} | |
} | |
} | |
return dfd; | |
}; | |
/** | |
* Дождаться «разрешения» всех обещаний и вернуть результат последнего | |
* @static | |
* @memberOf Promise | |
* @param {Array} iterable массив значений/обещаний | |
* @returns {Promise} | |
*/ | |
Promise.race = function (iterable) { | |
return Promise.all(iterable).then(function (values) { | |
return values.pop(); | |
}); | |
}; | |
/** | |
* Привести значение к «Обещанию» | |
* @static | |
* @memberOf Promise | |
* @param {*} value переменная или объект имеющий метод then | |
* @returns {Promise} | |
*/ | |
Promise.cast = function (value) { | |
var promise = Promise().resolve(value); | |
return value && typeof value.then === 'function' | |
? promise.then(function () { return value; }) | |
: promise | |
; | |
}; | |
/** | |
* Вернуть «разрешенное» обещание | |
* @static | |
* @memberOf Promise | |
* @param {*} value переменная | |
* @returns {Promise} | |
*/ | |
Promise.resolve = function (value) { | |
return (value && value.constructor === Promise) ? value : Promise().resolve(value); | |
}; | |
/** | |
* Вернуть «отклоненное» обещание | |
* @static | |
* @memberOf Promise | |
* @param {*} value переменная | |
* @returns {Promise} | |
*/ | |
Promise.reject = function (value) { | |
return Promise().reject(value); | |
}; | |
/** | |
* Дождаться «разрешения» всех обещаний | |
* @param {Object} map «Ключь» => «Обещание» | |
* @returns {Promise} | |
*/ | |
Promise.map = function (map) { | |
var array = [], key, idx = 0, results = {}; | |
for (key in map) { | |
array.push(map[key]); | |
} | |
return Promise.all(array).then(function (values) { | |
/* jshint -W088 */ | |
for (key in map) { | |
results[key] = values[idx++]; | |
} | |
return results; | |
}); | |
}; | |
// Версия модуля | |
Promise.version = "0.3.1"; | |
/* istanbul ignore else */ | |
if (!window.Promise) { | |
window.Promise = Promise; | |
} | |
// exports | |
if (typeof define === "function" && (define.amd || /* istanbul ignore next */ define.ajs)) { | |
define('Promise', [], function () { | |
return Promise; | |
}); | |
} else if (typeof module != "undefined" && module.exports) { | |
module.exports = Promise; | |
} | |
else { | |
window.Deferred = Promise; | |
} | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
define(['Promise', 'jquery'], function (MyPromise, $) { | |
/* jshint asi: true */ | |
module('Promise'); | |
requireTest('core', function (Promise) { | |
var promise = new Promise, name; | |
for (name in { resolve: 1, reject: 1, all: 1, race: 1, cast: 1 }) { | |
equal(typeof Promise[name], 'function', name); | |
} | |
for (name in { done: 1, fail: 1, then: 1, 'catch': 1, resolve: 1, reject: 1, always: 1 }) { | |
equal(typeof promise[name], 'function', name); | |
} | |
}); | |
function _checkStatus(actual, expected) { | |
return function (args) { | |
equal(actual, expected); | |
deepEqual(args, ["foo", "bar"]); | |
}; | |
} | |
// Test for 'resolve' and 'reject' | |
for (var key in { resolve: 1, reject: 1 }) { | |
/*jshint loopfunc: true */ | |
(function (method) { | |
asyncTest(method, function () { | |
expect(4); | |
var dfd = MyPromise(); | |
dfd | |
.done(_checkStatus(method, 'resolve')) | |
.fail(_checkStatus(method, 'reject')) | |
.then(_checkStatus(method, 'resolve'), _checkStatus(method, 'reject')) | |
; | |
dfd[method](["foo", "bar"]); | |
dfd.always(function () { | |
setTimeout(start, 0); | |
}); | |
}); | |
asyncTest(method + ' x 2', function () { | |
expect(8); | |
var dfd = MyPromise(); | |
dfd | |
.done(_checkStatus(method, 'resolve')) | |
.fail(_checkStatus(method, 'reject')) | |
.then(_checkStatus(method, 'resolve'), _checkStatus(method, 'reject')) | |
; | |
dfd[method](["foo", "bar"]); | |
dfd[method](["bar", "foo"]); | |
dfd | |
.done(_checkStatus(method, 'resolve')) | |
.fail(_checkStatus(method, 'reject')) | |
.then(_checkStatus(method, 'resolve'), _checkStatus(method, 'reject')) | |
; | |
dfd.always(function () { | |
setTimeout(start, 0); | |
}); | |
}); | |
asyncTest(method + '..' + method, function () { | |
expect(4); | |
var dfd = MyPromise(); | |
dfd[method](["foo", "bar"]); | |
dfd | |
.done(_checkStatus(method, 'resolve')) | |
.fail(_checkStatus(method, 'reject')) | |
.then(_checkStatus(method, 'resolve'), _checkStatus(method, 'reject')) | |
; | |
dfd[method](["bar", "foo"]); | |
dfd.always(function () { | |
setTimeout(start, 0); | |
}); | |
}); | |
})(key); | |
} | |
asyncTest('then', function () { | |
var log = []; | |
MyPromise() | |
.done(function (a) { | |
log.push(a) | |
}) | |
.resolve(1) | |
.then(function (a) { | |
return MyPromise().reject(a + 2) | |
}) | |
.done(function () { | |
log.push('fail') | |
}) | |
.fail(function (a) { | |
log.push(a) | |
}) | |
.then(null, function (a) { | |
return MyPromise().resolve(a + 3); | |
}) | |
.done(function (a) { | |
log.push(a) | |
}) | |
.fail(function () { | |
log.push('fail') | |
}) | |
.then(function (a) { | |
return a * 2; | |
}) | |
.done(function (a) { | |
log.push(a) | |
}) | |
.fail(function () { | |
log.push('fail') | |
}) | |
; | |
equal(log.join('->'), '1->3->6->12'); | |
var fail = false; | |
MyPromise(function (resolve, reject) { | |
setTimeout(function () { | |
reject('reject'); | |
}, 50); | |
}) | |
.then(null, function (val) { | |
return MyPromise(function (resolve) { | |
setTimeout(function () { | |
resolve(val + ' resolve'); | |
}, 50); | |
}); | |
}) | |
.always(function (res) { | |
ok(!fail); | |
equal(res, 'reject resolve'); | |
MyPromise.reject() | |
.then(function () { | |
return 'fail'; | |
}, function () { | |
return { | |
then: function (done, fail) { | |
fail('ok'); | |
} | |
}; | |
}) | |
.always(function (res) { | |
equal(res, 'ok'); | |
new MyPromise(function () { | |
throw '1'; | |
})['catch'](function (x) { | |
throw x + '2' | |
})['catch'](function (x) { | |
return x; | |
}).then(function (x) { | |
equal(x, '12'); | |
start(); | |
}); | |
}) | |
; | |
}) | |
; | |
}); | |
asyncTest('then + jquery', function () { | |
MyPromise.resolve().notify().progress().then(); // для покрытия jQuery методов | |
$.Deferred() | |
.reject('foo') | |
.then(null, function (val) { | |
return MyPromise.resolve(val + ' bar'); | |
}) | |
.always(function (res) { | |
equal(res, 'foo bar'); | |
start(); | |
}) | |
; | |
}); | |
asyncTest('then + fail', function () { | |
var n = [], m = []; | |
Promise.reject() | |
.then(function () { n.push('1Y'); }, function () { n.push('1N'); }) | |
.then(function () { n.push('2Y'); }, function () { n.push('2N'); }) | |
.then(function () { | |
}) | |
; | |
setTimeout(function () { | |
Promise.resolve() | |
.then(function () { n.push('3Y'); }, function () { n.push('3N'); }) | |
.then(function () { n.push('4Y'); }, function () { n.push('4N'); }) | |
; | |
}, 3); | |
MyPromise.reject() | |
.then(function () { m.push('1Y'); }, function () { m.push('1N'); }) | |
.then(function () { m.push('2Y'); }, function () { m.push('2N'); }) | |
; | |
setTimeout(function () { | |
MyPromise.resolve() | |
.then(function () { m.push('3Y'); }, function () { m.push('3N'); }) | |
.then(function () { m.push('4Y'); }, function () { m.push('4N'); }) | |
; | |
}, 3); | |
setTimeout(function () { | |
deepEqual(m, n); | |
start(); | |
}, 10); | |
}); | |
asyncTest('all: done', function () { | |
expect(4); | |
var foo = MyPromise(), | |
bar = MyPromise(), | |
baz = MyPromise(), | |
qux = { // like Promise | |
dfd: MyPromise(), | |
resolve: function (val){ this.dfd.resolve(val); }, | |
then: function (done){ this.dfd.then(done); } | |
}, | |
empty | |
; | |
setTimeout(foo.resolve.bind(null, 1), 100); | |
bar.resolve(2); | |
setTimeout(baz.resolve.bind(null, 3), 150); | |
qux.resolve(4); | |
MyPromise.all([]).always(function (){ | |
empty = true; | |
}); | |
MyPromise.all([foo, bar, '|', baz, qux, 'YES!', null, false, 100500]) | |
.done(function () { ok(true, "done"); }) | |
.then(function (values) { ok(true, "then"); return values }) | |
.fail(function () { ok(false, "fail"); }) | |
.then(function (values) { return values }, function () { ok(false, "then(null, fail)"); }) | |
.always(function (values) { | |
ok(empty, 'empty'); | |
equal(values.join('->'), '1->2->|->3->4->YES!->->false->100500'); | |
setTimeout(start, 0); | |
}) | |
; | |
}); | |
asyncTest('all: fail', function () { | |
expect(2); | |
var foo = MyPromise(), | |
bar = MyPromise(), | |
baz = MyPromise(), | |
qux = MyPromise() | |
; | |
setTimeout(foo.resolve, 100); | |
bar.resolve(); | |
setTimeout(baz.reject, 150); | |
qux.resolve(); | |
MyPromise.all([foo, bar, baz, qux]) | |
.done(function () { ok(false, "done"); }) | |
.then(function () { ok(false, "then"); }) | |
.fail(function () { ok(true, "fail"); }) | |
.then(null, function () { ok(true, "then(null, fail)"); }) | |
.always(function () { | |
setTimeout(start, 0); | |
}) | |
; | |
}); | |
asyncTest('like Native', function () { | |
MyPromise(function (resolve) { | |
resolve('foo'); | |
}).always(function (val) { | |
equal(val, 'foo'); | |
start(); | |
}); | |
}); | |
test('static: resolve/reject', function () { | |
expect(2); | |
MyPromise.resolve('foo').done(function (val) { | |
equal(val, 'foo') | |
}); | |
MyPromise.reject('bar').fail(function (val) { | |
equal(val, 'bar') | |
}); | |
}); | |
test('race', function () { | |
expect(1); | |
MyPromise.race([ | |
MyPromise.resolve(1), | |
MyPromise.resolve(2) | |
]).then(function (value) { | |
return value * 3; | |
}).always(function (value){ | |
return equal(value, 6); | |
}); | |
}); | |
test('cast', function () { | |
expect(3); | |
MyPromise.cast(3).then(function (result) { | |
equal(result, 3); | |
}); | |
MyPromise.cast($.Deferred().resolve('resolve')).then(function (result) { | |
equal(result, 'resolve'); | |
}); | |
MyPromise.cast($.Deferred().reject('reject'))['catch'](function (result) { | |
equal(result, 'reject'); | |
}); | |
}); | |
asyncTest('stress', function () { | |
var log = {}; | |
function testMe(method) { | |
var dfd = MyPromise(); | |
var name = method == 'resolve' ? 'done' : 'fail'; | |
dfd[name](function () { | |
log[name] = true; | |
}); | |
dfd[name](dfd[method]); | |
dfd[method](); | |
return dfd; | |
} | |
MyPromise.all([testMe('resolve'), testMe('reject')]).always(function () { | |
ok(log.done); | |
ok(log.fail); | |
start(); | |
}); | |
}); | |
asyncTest('resolve + reject', function () { | |
new MyPromise(function (resolve, reject) { | |
resolve(true); | |
setTimeout(function () { | |
try { | |
reject(false); | |
ok(true); | |
} catch (err) { | |
equal(err+'', null); | |
} | |
start(); | |
}); | |
}); | |
}); | |
asyncTest('catch', function () { | |
new Promise(function () { | |
throw "foo"; | |
})['catch'](function (foo) { | |
return new MyPromise(function () { | |
throw "bar"; | |
})['catch'](function (bar) { | |
return [foo, bar]; | |
}); | |
}).then(function (values) { | |
deepEqual(values, ['foo', 'bar']); | |
start(); | |
}, function (err) { | |
equal(err+'', null, 'fail'); | |
start(); | |
}); | |
}); | |
promiseTest('map', function () { | |
return MyPromise.map({ | |
foo: MyPromise.resolve(1), | |
bar: MyPromise.resolve(2) | |
}).then(function (data) { | |
deepEqual(data, { foo: 1, bar: 2 }); | |
}); | |
}); | |
promiseTest('executer + promise', function () { | |
return new MyPromise(function (resolve) { | |
resolve(new MyPromise(function (resolve, reject) { | |
reject('ok'); | |
})); | |
}).then(function () { | |
ok(false, 'done'); | |
}, function () { | |
ok(true, 'fail'); | |
}); | |
}); | |
promiseTest('statics:resolve|reject', function () { | |
return MyPromise.resolve(MyPromise.reject(123))['catch'](function (val) { | |
equal(val, 123); | |
return MyPromise.reject(MyPromise.resolve(321)).then(function () { | |
ok(false, 'должен быть catch'); | |
}, function (promise) { | |
ok(promise.then); | |
return promise.then(function (val) { | |
equal(val, 321); | |
}); | |
}) | |
}); | |
}); | |
/** | |
test('bench', function () { | |
var NativePromise = window.Promise, i; | |
var ts = new Date; | |
for (i = 0; i < 1e4; i++) { | |
MyPromise().resolve().then(function () {}); | |
} | |
ts = (new Date) - ts; | |
var $ts = new Date; | |
for (i = 0; i < 1e4; i++) { | |
$.Deferred().resolve().then(function () {}); | |
} | |
$ts = (new Date) - $ts; | |
var nts = new Date; | |
if (NativePromise) { | |
for (i = 0; i < 1e4; i++) { | |
NativePromise.resolve().then(function () {}); | |
} | |
} | |
nts = (new Date) - nts; | |
NativePromise && ok(ts / nts < 2, 'Native, RubaXa win: ' + (nts / ts)); | |
ok($ts / ts > 5, 'jQuery, RubaXa win: ' + ($ts / ts)); | |
console.log('Promise: ' + ts + 'ms'); | |
console.log('Native.Promise: ' + nts + 'ms'); | |
console.log('jQuery.Deferred: ' + $ts + 'ms'); | |
}); | |
/**/ | |
}); |
@josdejong Updated solution.
Now a full-fledged polyfill + is compatible with jQuery.
By any chance, do you have a TypeScript definitions for this gist? Thanks!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks for this nice, compact Deferred solution.
I encountered an issue with
Deferred.when
: when resolved, the results of the resolved Deferred objects are not passed.