«Promise.js» — is supported as a native interface and $.Deferred.
/** | |
* @author RubaXa <trash@rubaxa.org> | |
* @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; | |
} | |
})(); |
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'); | |
}); | |
/**/ | |
}); |
This comment has been minimized.
This comment has been minimized.
@josdejong Updated solution. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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
This comment has been minimized.
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.