Concept - Distilling how a promise works?
Can someone implement the most basic promise in a few lines?
Here it is:
function Promise(exec) {
// takes a function as an argument that gets the fullfiller
var callbacks = [], result;
exec(function fulfill() {
if (result) return;
result = arguments;
for (let c;c=callbacks.shift();)
c.apply(null, arguments);
});
this.addCallback = function(c) {
if (result)
c.apply(null, result)
else
callbacks.push(c);
}
}
Additional then
with chaining (which you will need for the answer):
Promise.prototype.then = function(fn) {
return new Promise(fulfill => {
this.addCallback((...args) => {
const result = fn(...args);
if (result instanceof Promise)
result.addCallback(fulfill);
else
fulfill(result);
});
});
};
How are these two snippets related?
ajax
is called from the getPromiseForAjaxResult
function:
function getPromiseForAjaxResult(ressource) {
return new Promise(function(callback) {
ajax({url:ressource}, callback);
});
}
How an extremely simple implementation of a Promise works
onResolve
andonReject
have to be bound in order to prevent theexecutionFunction
to apply them with another context or without any context. (if for whatever reason you callresolve
orreject
with another context, it has to be bound toPromiseSimple
, otherwizethis
will refer to something else or won't refer to anything if no context).
Here is an example that won't work if you don't bind onResolve
or onReject
:
const makeApiCall = () => {
return new PromiseSimple((resolve, reject) => {
Promise.resolve().then(resolve); // Using native promise here : resolve is called without context and won't work if not bound
});
}
- This implementation does block the main thread but you won't probably see it since you're using a setTimeout which delays the execution in the event loop. The true Promise implementation delays the tasks you define in
then
orcatch
callback as microtask in the event loop. (There is this very interresting article about browsers event loop).
Hope this helps,
Simple Promise and Then implementation
Here's the shortened code for creating a promise class,
class MyPromise { constructor(executor) { this.callbacks = [];
const resolve = res => { for (const { callback } of this.callbacks) { callback(res); } };
executor(resolve); }
then(callback) { return new MyPromise((resolve) => { const done = res => { resolve(callback(res)); }; this.callbacks.push({ callback: done }); }); }}
promise = new MyPromise((resolve) => { setTimeout(() => resolve(2), 1000);});
promise.then(result => { console.log(result); return 2 * result;}).then(result => console.log(result));
How is a promise/defer library implemented?
I find it harder to explain than to show an example, so here is a very simple implementation of what a defer/promise could be.
Disclaimer: This is not a functional implementation and some parts of the Promise/A specification are missing, This is just to explain the basis of the promises.
tl;dr: Go to the Create classes and example section to see full implementation.
Promise:
First we need to create a promise object with an array of callbacks. I'll start working with objects because it's clearer:
var promise = {
callbacks: []
}
now add callbacks with the method then:
var promise = {
callbacks: [],
then: function (callback) {
callbacks.push(callback);
}
}
And we need the error callbacks too:
var promise = {
okCallbacks: [],
koCallbacks: [],
then: function (okCallback, koCallback) {
okCallbacks.push(okCallback);
if (koCallback) {
koCallbacks.push(koCallback);
}
}
}
Defer:
Now create the defer object that will have a promise:
var defer = {
promise: promise
};
The defer needs to be resolved:
var defer = {
promise: promise,
resolve: function (data) {
this.promise.okCallbacks.forEach(function(callback) {
window.setTimeout(function () {
callback(data)
}, 0);
});
},
};
And needs to reject:
var defer = {
promise: promise,
resolve: function (data) {
this.promise.okCallbacks.forEach(function(callback) {
window.setTimeout(function () {
callback(data)
}, 0);
});
},
reject: function (error) {
this.promise.koCallbacks.forEach(function(callback) {
window.setTimeout(function () {
callback(error)
}, 0);
});
}
};
Note that the callbacks are called in a timeout to allow the code be always asynchronous.
And that's what a basic defer/promise implementation needs.
Create classes and example:
Now lets convert both objects to classes, first the promise:
var Promise = function () {
this.okCallbacks = [];
this.koCallbacks = [];
};
Promise.prototype = {
okCallbacks: null,
koCallbacks: null,
then: function (okCallback, koCallback) {
okCallbacks.push(okCallback);
if (koCallback) {
koCallbacks.push(koCallback);
}
}
};
And now the defer:
var Defer = function () {
this.promise = new Promise();
};
Defer.prototype = {
promise: null,
resolve: function (data) {
this.promise.okCallbacks.forEach(function(callback) {
window.setTimeout(function () {
callback(data)
}, 0);
});
},
reject: function (error) {
this.promise.koCallbacks.forEach(function(callback) {
window.setTimeout(function () {
callback(error)
}, 0);
});
}
};
And here is an example of use:
function test() {
var defer = new Defer();
// an example of an async call
serverCall(function (request) {
if (request.status === 200) {
defer.resolve(request.responseText);
} else {
defer.reject(new Error("Status code was " + request.status));
}
});
return defer.promise;
}
test().then(function (text) {
alert(text);
}, function (error) {
alert(error.message);
});
As you can see the basic parts are simple and small. It will grow when you add other options, for example multiple promise resolution:
Defer.all(promiseA, promiseB, promiseC).then()
or promise chaining:
getUserById(id).then(getFilesByUser).then(deleteFile).then(promptResult);
To read more about the specifications: CommonJS Promise Specification. Note that main libraries (Q, when.js, rsvp.js, node-promise, ...) follow Promises/A specification.
Hope I was clear enough.
Edit:
As asked in the comments, I've added two things in this version:
- The possibility to call then of a promise, no matter what status it has.
- The possibility to chain promises.
To be able to call the promise when resolved you need to add the status to the promise, and when the then is called check that status. If the status is resolved or rejected just execute the callback with its data or error.
To be able to chain promises you need to generate a new defer for each call to then
and, when the promise is resolved/rejected, resolve/reject the new promise with the result of the callback. So when the promise is done, if the callback returns a new promise it is bound to the promise returned with the then()
. If not, the promise is resolved with the result of the callback.
Here is the promise:
var Promise = function () {
this.okCallbacks = [];
this.koCallbacks = [];
};
Promise.prototype = {
okCallbacks: null,
koCallbacks: null,
status: 'pending',
error: null,
then: function (okCallback, koCallback) {
var defer = new Defer();
// Add callbacks to the arrays with the defer binded to these callbacks
this.okCallbacks.push({
func: okCallback,
defer: defer
});
if (koCallback) {
this.koCallbacks.push({
func: koCallback,
defer: defer
});
}
// Check if the promise is not pending. If not call the callback
if (this.status === 'resolved') {
this.executeCallback({
func: okCallback,
defer: defer
}, this.data)
} else if(this.status === 'rejected') {
this.executeCallback({
func: koCallback,
defer: defer
}, this.error)
}
return defer.promise;
},
executeCallback: function (callbackData, result) {
window.setTimeout(function () {
var res = callbackData.func(result);
if (res instanceof Promise) {
callbackData.defer.bind(res);
} else {
callbackData.defer.resolve(res);
}
}, 0);
}
};
And the defer:
var Defer = function () {
this.promise = new Promise();
};
Defer.prototype = {
promise: null,
resolve: function (data) {
var promise = this.promise;
promise.data = data;
promise.status = 'resolved';
promise.okCallbacks.forEach(function(callbackData) {
promise.executeCallback(callbackData, data);
});
},
reject: function (error) {
var promise = this.promise;
promise.error = error;
promise.status = 'rejected';
promise.koCallbacks.forEach(function(callbackData) {
promise.executeCallback(callbackData, error);
});
},
// Make this promise behave like another promise:
// When the other promise is resolved/rejected this is also resolved/rejected
// with the same data
bind: function (promise) {
var that = this;
promise.then(function (res) {
that.resolve(res);
}, function (err) {
that.reject(err);
})
}
};
As you can see, it has grown quite a bit.
Related Topics
Why Doesn't Decodeuri("A+B") == "A B"
Get Number Days in a Specified Month Using JavaScript
Anything Similar in JavaScript to Ruby's #{Value} (String Interpolation)
Template Language That Works on Both Server and Client
Rails 5 - Turbolinks 5,Some Js Not Loaded on Page Render
Uncaught Referenceerror: Reactdom Is Not Defined
Rails Specify Load Order of JavaScript Files
Using Jquery Hover with HTML Image Map
Decryption of Aes Created with Sjcl.Js in Ruby
How to Listen for Keyboard Open/Close in JavaScript/Sencha
Differencebetween Compile and Link Function in Angularjs
Jquery Scrolltop() Doesn't Seem to Work in Safari or Chrome (Windows)
Why How to Not Define Functions in Jquery's Document.Ready()