Skip to content

Instantly share code, notes, and snippets.

@RubaXa RubaXa/Promise.js
Last active Sep 16, 2017

Embed
What would you like to do?
«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');
});
/**/
});
@josdejong

This comment has been minimized.

Copy link

josdejong commented May 12, 2014

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.

@RubaXa

This comment has been minimized.

Copy link
Owner Author

RubaXa commented Jun 22, 2014

@josdejong Updated solution.
Now a full-fledged polyfill + is compatible with jQuery.

@bisubus

This comment has been minimized.

Copy link

bisubus commented Apr 28, 2015

@RubaXa Great tiny library, thanks. It also got a favourable comparison here. Do you have plans to move it to a repo? It would be nice to have it at hand as bower dependency .

@mr-moon

This comment has been minimized.

Copy link

mr-moon commented Sep 16, 2015

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
You can’t perform that action at this time.