the promised land (in angular)

42
The Promised Land by @domenic in

Upload: domenic-denicola

Post on 06-May-2015

19.029 views

Category:

Technology


2 download

DESCRIPTION

Promises are a popular pattern for asynchronous operations in JavaScript, existing in some form in every client-side framework in widespread use today. We'll give a conceptual and practical intro to promises in general, before moving on to talking about how they fit into Angular. If you've ever wondered what exactly $q was about, this is the place to learn!

TRANSCRIPT

Page 1: The Promised Land (in Angular)

ThePromisedLand

by @domenic

in

Page 2: The Promised Land (in Angular)

http://domenic.me

https://github.com/domenic

https://npmjs.org/~domenic

http://slideshare.net/domenicdenicola

THINGS I’M DOING The Promises/A+ and ES6 promise specs Working on Q The Extensible Web Manifesto

Domenic Denicola

Page 3: The Promised Land (in Angular)

Angular is enlightened• Like most other client-side frameworks these days, Angular uses

promises for everything async:• $timeout• $http + response interceptors• $resource• $routeProvider.when

• Its built-in promise library, $q, is pretty good.

• But first, let’s take a step back and start from the beginning.

Page 4: The Promised Land (in Angular)

Promises, in General

Page 5: The Promised Land (in Angular)

Web programming is async• I/O (like XMLHttpRequest, IndexedDB, or waiting for the user to

click) takes time

• We have only a single thread

• We don’t want to freeze the tab while we do I/O

• So we tell the browser:• Go do your I/O• When you’re done, run this code• In the meantime, let’s move on to some other code

Page 6: The Promised Land (in Angular)

Async with callbacks// Ask for permission to show notificationsNotification.requestPermission(function (result) { // When the user clicks yes or no, this code runs. if (result === 'denied') { console.log('user clicked no'); } else { console.log('permission granted!'); }}); // But in the meantime, this code continues to run.console.log("Waiting for the user...");

Page 7: The Promised Land (in Angular)

Async with eventsvar request = indexedDB.open("myDatabase"); request.onsuccess = function () { console.log('opened!');};request.onerror = function () { console.log('failed');}; console.log("This code runs before either of those");

Page 8: The Promised Land (in Angular)

Async with WTFBBQvar xhr = new XMLHttpRequest();xhr.onreadystatechange = function () { if (this.readyState === this.DONE) { if (this.status === 200) { console.log("got the data!" + this.responseText); } else { console.log("an error happened!"); } }}; xhr.open("GET", "somefile.json");xhr.send();

Page 9: The Promised Land (in Angular)

These APIs are a hack• They are literally the simplest thing that could work.

• But as a replacement for synchronous control flow, they suck.

• There’s no consistency.

• There’s no guarantees.

• We lose the flow of our code writing callbacks that tie together other callbacks.

• We lose the stack-unwinding semantics of exceptions, forcing us to handle errors explicitly at every step.

Page 10: The Promised Land (in Angular)

Instead of calling a passed callback, return a promise:

var promiseForTemplate = $http.get("template.html");

promiseForTemplate.then( function (template) { // use template }, function (err) { // couldn’t get the template });

Promises are the right abstraction

Page 11: The Promised Land (in Angular)

function getPromiseFor5() { var d = $q.defer(); d.resolve(5); return d.promise;} getPromiseFor5().then(function (v) { console.log('this will be 5: ' + v);});

Creating a promise

Page 12: The Promised Land (in Angular)

function getPromiseFor5After1Second() { var d = $q.defer(); setTimeout(function () { d.resolve(5); }, 1000); return d.promise;} getPromiseFor5After1Second().then(function (v) { // this code only gets run after one second console.log('this will be 5: ' + v);});

Creating a promise (more advanced)

Page 13: The Promised Land (in Angular)

promiseForResult.then(onFulfilled, onRejected);

• Only one of onFulfilled or onRejected will be called.

• onFulfilled will be called with a single fulfillment value (⇔ return value).

• onRejected will be called with a single rejection reason (⇔ thrown exception).

• If the promise is already settled, the handlers will still be called once you attach them.

• The handlers will always be called asynchronously.

Promise guarantees

Page 14: The Promised Land (in Angular)

var transformedPromise = originalPromise.then(onFulfilled, onRejected);

• If the called handler returns a value, transformedPromise will be resolved with that value:• If the returned value is a promise, we adopt its state.

• Otherwise, transformedPromise is fulfilled with that value.

• If the called handler throws an exception, transformedPromise will be rejected with that exception.

Promises can be chained

Page 15: The Promised Land (in Angular)

var result; try { result = process(getInput());} catch (ex) { result = handleError(ex);}

var resultPromise = getInputPromise() .then(processAsync) .then(undefined, handleErrorAsync);

The sync ⇔ async parallel

Page 16: The Promised Land (in Angular)

var result; try { result = process(getInput());} catch (ex) { result = handleError(ex);}

var resultPromise = getInputPromise() .then(processAsync) .catch(handleErrorAsync);

The sync ⇔ async parallel

Page 17: The Promised Land (in Angular)

Case 1: simple functional transform var user = getUser(); var userName = user.name; // becomes  var userNamePromise = getUser().then(function (user) { return user.name; });

Page 18: The Promised Land (in Angular)

Case 2: reacting with an exception var user = getUser(); if (user === null) throw new Error("null user!"); // becomes  var userPromise = getUser().then(function (user) { if (user === null) throw new Error("null user!"); return user; });

Page 19: The Promised Land (in Angular)

Case 3: handling an exception try { updateUser(data); } catch (ex) { console.log("There was an error:", ex); } // becomes  var updatePromise = updateUser(data).catch(function (ex) { console.log("There was an error:", ex); });

Page 20: The Promised Land (in Angular)

Case 4: rethrowing an exception try { updateUser(data); } catch (ex) { throw new Error("Updating user failed. Details: " + ex.message); } // becomes  var updatePromise = updateUser(data).catch(function (ex) { throw new Error("Updating user failed. Details: " + ex.message); });

Page 21: The Promised Land (in Angular)

var name = promptForNewUserName(userId); updateUser({ id: userId, name: name }); refreshUI(); // becomes  promptForNewUserName(userId) .then(function (name) { return updateUser({ id: userId, name: name }); }) .then(refreshUI);

Bonus async case: waiting

Page 22: The Promised Land (in Angular)

Key features In practice, here are some key capabilities promises give you:

• They are guaranteed to always be async.

• They provide an asynchronous analog of exception propagation.

• Because they are first-class objects, you can combine them easily and powerfully.

• They allow easy creation of reusable abstractions.

Page 23: The Promised Land (in Angular)

Always asyncfunction getUser(userName, onSuccess, onError) { if (cache.has(userName)) { onSuccess(cache.get(userName)); } else { $.ajax("/user?" + userName, { success: onSuccess, error: onError }); }}

Page 24: The Promised Land (in Angular)

Always asyncconsole.log("1"); getUser("ddenicola", function (user) { console.log(user.firstName);}); console.log("2");

// 1, 2, Domenic

Page 25: The Promised Land (in Angular)

Always asyncconsole.log("1"); getUser("ddenicola", function (user) { console.log(user.firstName);}); console.log("2");

// 1, Domenic, 2

Page 26: The Promised Land (in Angular)

Always asyncfunction getUser(userName) { if (cache.has(userName)) { return $q.when(cache.get(userName)); } else { return $http.get("/user?" + userName); }}

Page 27: The Promised Land (in Angular)

Always asyncconsole.log("1"); getUser("ddenicola“).then(function (user) { console.log(user.firstName);}); console.log("2");// 1, 2, Domenic (every time!)

Page 28: The Promised Land (in Angular)

getUser("Domenic", function (user) { getBestFriend(user, function (friend) { ui.showBestFriend(friend); });});

Async “exception propagation”

Page 29: The Promised Land (in Angular)

getUser("Domenic", function (err, user) { if (err) { ui.error(err); } else { getBestFriend(user, function (err, friend) { if (err) { ui.error(err); } else { ui.showBestFriend(friend, function (err, friend) { if (err) { ui.error(err); } }); } }); }});

Async “exception propagation”

Page 30: The Promised Land (in Angular)

getUser("Domenic") .then(getBestFriend) .then(ui.showBestFriend) .catch(ui.error);

Async “exception propagation”

Page 31: The Promised Land (in Angular)

Because promises are first-class objects, you can build simple operations on them instead of tying callbacks together:

// Fulfills with an array of results when both fulfill, or rejects if either reject all([getUserData(), getCompanyData()]);   // Fulfills with single result as soon as either fulfills, or rejects if both reject any([storeDataOnServer1(), storeDataOnServer2()]);   // If writeFile accepts promises as arguments, and readFile returns one: writeFile("dest.txt", readFile("source.txt"));

Promises as first-class objects

Page 32: The Promised Land (in Angular)

Building promise abstractionsfunction timer(promise, ms) { var deferred = $q.defer(); promise.then(deferred.resolve, deferred.reject); setTimeout(function () { deferred.reject(new Error("oops timed out")); }, ms); return deferred.promise;} function httpGetWithTimer(url, ms) { return timer($http.get(url), ms);}

Page 33: The Promised Land (in Angular)

Building promise abstractionsfunction retry(operation, maxTimes) { return operation().catch(function (reason) { if (maxTimes === 0) { throw reason; } return retry(operation, maxTimes - 1); });} function httpGetWithRetry(url, maxTimes) { return retry(function () { return $http.get(url); }, maxTimes);}

Page 34: The Promised Land (in Angular)

Promises, in Angular

Page 35: The Promised Land (in Angular)

The digest cycle

Page 36: The Promised Land (in Angular)

function MyController($scope) { $scope.text = "loading"; $scope.doThing = function () { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (this.readyState === this.DONE && this.status === 200) { $scope.text = this.responseText; } }; xhr.open("GET", "somefile.json"); xhr.send(); };} // Doesn’t work, because the callback function is outside the digest cycle!

Page 37: The Promised Land (in Angular)

function MyController($scope) { $scope.text = "loading"; $scope.doThing = function () { jQuery.get("somefile.json").then(function (responseText) { $scope.text = responseText; }); };}

// Still doesn't work: same problem 

Page 38: The Promised Land (in Angular)

function MyController($scope) { $scope.text = "loading"; $scope.doThing = function () { jQuery.get("somefile.json").then(function (responseText) { $scope.apply(function () { $scope.text = responseText; }); }); };}

// Works, but WTF

Page 39: The Promised Land (in Angular)

function MyController($scope, $http) { $scope.text = "loading"; $scope.doThing = function () { $http.get("somefile.json").then(function (response) { $scope.text = response.data; }); };}

// Works! Angular’s promises are integrated into the digest cycle

Page 40: The Promised Land (in Angular)

Useful things• $q.all([promise1, promise2, promise3]).then(function (threeElements) { … });

• $q.all({ a: promiseA, b: promise }).then(function (twoProperties) { … });

• Progress callbacks:• deferred.notify(value)• promise.then(undefined, undefined, onProgress)• But, use sparingly, and be careful

• $q.when(otherThenable), e.g. for jQuery “promises”

• promise.finally(function () { // happens on either success or failure});

Page 41: The Promised Land (in Angular)

Gotchas• Issue 7992: catching thrown errors causes them to be logged anyway

• Writing reusable libraries that vend $q promises is hard• $q is coupled to Angular’s dependency injection framework• You have to create an Angular module, which has limited audience

• Angular promises are not as full-featured as other libraries:• Check out Q or Bluebird• But to get the digest-cycle magic, you need

qPromise.finally($scope.apply).

• Deferreds are kind of lame compared to the ES6 Promise constructor.

• Progress callbacks are problematic.