Skip to content

Instantly share code, notes, and snippets.

@RubaXa
Last active September 16, 2017 18:17
Show Gist options
  • Save RubaXa/8501359 to your computer and use it in GitHub Desktop.
Save RubaXa/8501359 to your computer and use it in GitHub Desktop.
«Promise.js» — is supported as a native interface and $.Deferred.
/**
* @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;
}
})();
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
Copy link

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
Copy link
Author

RubaXa commented Jun 22, 2014

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

@bisubus
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
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