邹明潮 Notes: YDKJS Promise

邹明潮
KevinZou
Published in
7 min readJun 1, 2017

JavaScript

JavaScript is a single-threaded, non-blocking, asynchronous and concurrent language, because DOM-tree is not thread-safe.

  • All our JavaScript code should run in the same execution context such as stack
  • the code inside of a function is atomic, which means this function will be executed completely before any code can run.

Concurrency

Concurrency is when two or more “processes” are executing simultaneously over the same period, regardless of wether their individual constituent operations happen in parallel(at the same instant on separate processors or cores) or not.

Interaction

Concurrent “processes” will by necessity interact, indirectly through scope or the DOM.

Case 1: Using if statements as a ‘gate’.

var res = [];

function response(data) {
if (data.url == "http://some.url.1") {
res[0] = data;
}
else if (data.url == "http://some.url.2") {
res[1] = data;
}
}

ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

Case 2: Breaking a long-running process into pieces.

var res = [];

// `response(..)` receives array of results from the Ajax call
function response(data) {
// let's just do 1000 at a time
var chunk = data.splice( 0, 1000 );

// add onto existing `res` array
res = res.concat(
// make a new transformed array with all `chunk` values doubled
chunk.map( function(val){
return val * 2;
} )
);

// anything left to process?
if (data.length > 0) {
// async schedule next batch
setTimeout( function(){
response( data );
}, 0 );
}
}

ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

Event Loop

JavaScript employs event loop in the browser to implement the mechanism of concurrency and asynchrony.

邹明潮

As described in the picture, the main parts of JavaScript Event loop are:

  • The Call stack, which is a usual call stack that contains all called functions.
  • The Event handlers queue, a queue of event handlers that are waiting to be executed.
  • The Event loop, which is a process that checks whether events handlers queue is not empty and if it is — calls top event handler and removes it from queue.
  • The JavaScript Web APIs: those APIs that are provided by the browser that are responsible for filling the Event handlers queue, providing many features such as the ability to make an AJAX request.

Animation: Event Loop

One more thing worth mentioning is that page render has higher priority than any handler waiting in the Event handler queue.

while(true){   if(renderEngine.isItTimeToRender(){     renderEngine.render();   }  if(eventHandlersQueue.isNotEmpty()){     eventHandlersQueue.processTopEventHandler();  }}

Job Queue

As of ES6, there is a new concept layered on top of the event loop queue, called “Job queue”.

If an event is waiting on the event loop queue, it’s taken off and executed for each tick. “Job queue” is a queue hanging off the end of every tick in the event loop queue. Certain async-implied actions that may occur during a tick of the event loop will not cause a whole new event to be added to the event loop queue, but will instead add an item (aka Job) to the end of the current tick’s Job queue.

In Promise, the

Callbacks

Callbacks suffer from inversion of control in that they implicitly give control over to another party (often a third-party utility not in your control!) to invoke the continuation of your program. This control transfer leads us to a troubling list of trust issues, such as whether the callback is called more times than we expect.

Promises

Promise Value

function add(xPromise,yPromise) {
// `Promise.all([ .. ])` takes an array of promises,
// and returns a new promise that waits on them
// all to finish
return Promise.all( [xPromise, yPromise] )

// when that promise is resolved, let's take the
// received `X` and `Y` values and add them together.
.then( function(values){
// `values` is an array of the messages from the
// previously resolved promises
return values[0] + values[1];
} );
}

// `fetchX()` and `fetchY()` return promises for
// their respective values, which may be ready
// *now* or *later*.
add( fetchX(), fetchY() )

// we get a promise back for the sum of those
// two numbers.
// now we chain-call `then(..)` to wait for the
// resolution of that returned promise.
.then( function(sum){
console.log( sum ); // that was easier!
} );

Once a Promise is resolved, it stays that way forever — it becomes an immutable value at that point — and can then be observed as many times as necessary.

Promise Events

function get(url) {
return new Promise(function(resolve, reject) {
var req = new XMLHttpRequest();
req.open("GET", url, true);
req.addEventListener("load", function() {
if (req.status < 400)
resolve(req.responseText);
else
reject(new Error("Request failed: " + req.statusText));
});
req.addEventListener("error", function() {
reject(new Error("Network error"));
});
req.send(null);
});
}

Promise Trust

  • Call the callback too early

The callbacks you provide to then(..) will always called asynchronously, because they are jobs in the job queue.

  • Call the callback too late (or never)

When a Promise is resolved, all then(..) registered callbacks(jobs) on it will be called in order(FIFO) immediately.

p.then( function(){
p.then( function(){
console.log( "C" );
} );
console.log( "A" );
} );
p.then( function(){
console.log( "B" );
} );
// A B C

Never calling the callback

// a utility for timing out a Promise
function timeoutPromise(delay) {
return new Promise( function(resolve,reject){
setTimeout( function(){
reject( "Timeout!" );
}, delay );
} );
}

// setup a timeout for `foo()`
Promise.race( [
foo(), // attempt `foo()`
timeoutPromise( 3000 ) // give it 3 seconds
] )
.then(
function(){
// `foo(..)` fulfilled in time!
},
function(err){
// either `foo()` rejected, or it just
// didn't finish in time, so inspect
// `err` to know which
}
);
  • Call the callback too few or too many times

A Promise can only be resolved once, any then(..) registered callbacks will only ever be called once(each).

  • Swallow any errors/exceptions that may happen

A JS exception will force the Promise to become rejected.

Chain Flow

  • Every time you call then(..) on a Promise, it creates and returns a new Promise, which we can chain with.
  • Whatever value you return from the then(..) call's fulfillment callback (the first parameter) is automatically set as the fulfillment of the chained Promise.
var p = Promise.resolve( 21 );

p.then( function(v){
console.log( v ); // 21

/* create a promise to return, which overrides the promise created by the first then(..) */
return new Promise( function(resolve,reject){
/* Since a setTimeout is a webApi, it spawns a new item that will be stored in the event handler queue */
setTimeout( function(){
// fulfill with value `42`
resolve( v * 2 );
}, 100 );
} );
} )
.then( function(v){
// runs after the 100ms delay in the previous step
console.log( v ); // 42
} );

The Promise chain we construct is not only a flow control that expresses a multistep async sequence, but it also acts as a message channel to propagate messages from step to step.

Promise API

  • Promise.resolve(..)

1. If you pass an immediate, non-Promise, non-thenable value to Promise.resolve(..), you get a promise that's fulfilled with that value. In other words, these two promises p1 and p2 will behave basically identically:

var p1 = new Promise( function(resolve,reject){
resolve( 42 );
} );
var p2 = Promise.resolve( 42 );

2. If you pass a genuine Promise to Promise.resolve(..), you just get the same promise back:

var p1 = Promise.resolve( 42 );var p2 = Promise.resolve( p1 );p1 === p2; // true

3. If you pass a non-Promise thenable value to Promise.resolve(..), it will attempt to unwrap that value, and the unwrapping will keep going until a concrete final non-Promise-like value is extracted.

var p = {
then: function(cb) {
cb( 42 );
}
};
Promise.resolve( p )
.then(
function fulfilled(val){
console.log( val ); // 42
},
function rejected(err){
// never gets here
}
);
  • Promise.reject(..)

reject(..) does not do the unwrapping that resolve(..) does. If you pass a Promise/thenable value to reject(..), that untouched value will be set as the rejection reason.

  • new Promise(..)

The revealing constructor Promise(..) must be used with new, and must be provided a function callback that is synchronously/immediately called. This function is passed two function callbacks that act as resolution capabilities for the promise. We commonly label these resolve(..) and reject(..):

var p = new Promise( function(resolve,reject){
// `resolve(..)` to resolve/fulfill the promise
// `reject(..)` to reject the promise
} );

Variations

  • A static helper utility that lets us observe (without interfering) the resolution of a Promise:
// polyfill-safe guard check
if (!Promise.observe) {
Promise.observe = function(pr,cb) {
// side-observe `pr`'s resolution
pr.then(
function fulfilled(msg){
// schedule callback async (as Job)
Promise.resolve( msg ).then( cb );
},
function rejected(err){
// schedule callback async (as Job)
Promise.resolve( err ).then( cb );
}
);

// return original promise
return pr;
};
}
  • first([ .. ]) is that it ignores any rejections and fulfills as soon as the first Promise fulfills.
/ polyfill-safe guard check
if (!Promise.first) {
Promise.first = function(prs) {
return new Promise( function(resolve,reject){
// loop through all promises
prs.forEach( function(pr){
// normalize the value
Promise.resolve( pr )
/* whichever one fulfills first wins, and gets to resolve the main promise */
.then( resolve );
} );
} );
};
}
  • An asynchronous map(..) utility that takes an array of values (could be Promises or anything else), plus a function (task) to perform against each. map(..) itself returns a promise whose fulfillment value is an array that holds (in the same mapping order) the async fulfillment value from each task
if (!Promise.map) {
Promise.map = function(vals,cb) {
// new promise that waits for all mapped promises
return Promise.all(
/* note: regular array `map(..)`, turns the array of values into an array of promises */
vals.map( function(val){
/* replace `val` with a new promise that resolves after `val` is async mapped */
return new Promise( function(resolve){
cb( val, resolve );
} );
} )
);
};
}
  • Refactor to promise-aware code
// polyfill-safe guard check
if (!Promise.wrap) {
Promise.wrap = function(fn) {
return function() {
var args = [].slice.call( arguments );

return new Promise( function(resolve,reject){
fn.apply(
null,
args.concat( function(err,v){
if (err) {
reject( err );
}
else {
resolve( v );
}
} )
);
} );
};
};
}

--

--