Shaun Xu

The Sheep-Pen of the Shaun


News

logo

Shaun, the author of this blog is a semi-geek, clumsy developer, passionate speaker and incapable architect with about 10 years’ experience in .NET and JavaScript. He hopes to prove that software development is art rather than manufacturing. He's into cloud computing platform and technologies (Windows Azure, Amazon and Aliyun) and right now, Shaun is being attracted by JavaScript (Angular.js and Node.js) and he likes it.

Shaun is working at Worktile Inc. as the chief architect for overall design and develop worktile, a web-based collaboration and task management tool, and lesschat, a real-time communication aggregation tool.

MVP

My Stats

  • Posts - 122
  • Comments - 616
  • Trackbacks - 0

Tag Cloud


Recent Comments


Recent Posts


Archives


Post Categories


.NET



When I'm working in my project, as well as development "sx.wizard" toolkit, I need to deal with asynchronous operation. For example, in "sx.wizard", template for each steps are loaded in parallel asynchronously. And the "$wizard" service will start to render UI once all templates are loaded successfully. In this case we need to use $q.

 

$q was explained in Angular.js document as follow.

A service that helps you run functions asynchronously, and use their return values (or exceptions) when they are done processing.

And it's inspired by Kris Kowal's Q, which is widely used in Node.js. It's also following ES6 promises.

To me, when you have a function which the result will be returned in sometime in the future, and you need to continue your procedure based on the return value, then you need to consider using $q. Let's take a very simple example.

 

Handle Asynchronous Operation

Assuming I have an angular.js application which need to display "Hello Shaun!", and "Shaun" should be appeared 2 seconds later, the code could be as below.

   1: <!DOCTYPE html>
   2: <html>
   3:   <head>
   4:     <script data-require="angular.js@1.4.0-rc.1" data-semver="1.4.0-rc.1" src="https://code.angularjs.org/1.4.0-rc.1/angular.js"></script>
   1:  
   2:   </head>
   3:  
   4:   <body ng-app="Demo" ng-controller="HomeCtrl">
   5:     <h1>Hello {{name}}!</h1>
   6:  
   7:       <script>
   8:         (function () {
   9:             var app = angular.module('Demo', []);
  10:  
  11:             app.controller('HomeCtrl', function ($scope, $timeout) {
  12:                 $timeout(function () {
  13:                     $scope.name = 'Shaun';
  14:                 }, 2000);
  15:             }); 
  16:         }());
  17:       
</script>
   5:  
   6:   </body>
   7: </html>

Here is the result.

01

If we want to continue our procedure we can simply add codes inside $timeout function as below.

   1: app.controller('HomeCtrl', function ($scope, $timeout) {
   2:     $timeout(function () {
   3:         $scope.name = 'Shaun';
   4:         alert('Continue working...');
   5:     }, 2000);
   6: }); 

In this case it seems no need to introduce $q. But let's assume the name will be retrieved from another service or component. Normally it might be retrieved some external web service through $http but in order to simplify the example let create service and let it return name value in 2 seconds.

   1: app.factory('nameService', function ($timeout) {
   2:     return {
   3:         get: function () {
   4:             $timeout(function () {
   5:                 return 'Shaun';
   6:             }, 2000);
   7:         }
   8:     };
   9: });

In controller we use this service.

   1: app.controller('HomeCtrl', function ($scope, nameService) {
   2:     $scope.name = nameService.get();
   3: }); 

Looks great. But when we launch our website we will find the name will never be shown in the page, even though in the service we returned in 2 seconds. This is because "nameService.get" function returned "undefined" after it processed $timeout. 2 seconds later we returned name value but our controller has no chance to get it.

 

image

In this case we can use $q to notify controller the name was retrieved please update scope.

   1: app.factory('nameService', function ($q, $timeout) {
   2:     return {
   3:         get: function () {
   4:             return $q(function (resolve, reject) {
   5:                 $timeout(function () {
   6:                     resolve('Shaun');
   7:                 }, 2000);
   8:             });
   9:         }
  10:     };
  11: });

We use "$q" as a constructor, which takes a function with two parameters: resolve and reject, both of them are functions. When we got the value, we can invoke "resolve" with the value as the parameter. When there's an exception, we can invoke "reject" to tell the caller it failed. In the code above I invoked "resolve" after 2 seconds with "Shaun", the name value I got.

Back to the controller, after invoked "nameService.get" we got a promise created by $q. When the result was ready it will invoke a function defined through its "then" method as following.

   1: app.controller('HomeCtrl', function ($scope, nameService) {
   2:     var getPromise = nameService.get();
   3:     
   4:     getPromise.then(function (name) {
   5:         $scope.name = name;
   6:     });
   7: }); 

 

Invocation Chaining

We can return a new promise inside "then" function from an existing promise. This makes us very easy to manage multiple asynchronous functions especially when some are depends on others. Database operation is a good example. Normally we need to open the database, then it's opened we will query records, and then we got the records we will filter them, prepare data and insert or update, then we close the connection. Below is "database" service I created, note that each function returns a promise.

   1: app.factory('database', function ($q, $timeout) {
   2:     return {
   3:         open: function (db) {
   4:             return $q(function (resolve, reject) {
   5:                 $timeout(function () {
   6:                     resolve(db + ' is connected.');
   7:                 }, 2000);
   8:             });
   9:         },
  10:         query: function (db, query) {
  11:             return $q(function (resolve, reject) {
  12:                 $timeout(function () {
  13:                     resolve(db + ' [' + query + '] processed successuflly.');
  14:                 }, 2000);
  15:             });  
  16:         },
  17:         execute: function (db, command) {
  18:             return $q(function (resolve, reject) {
  19:                 $timeout(function () {
  20:                     resolve(db + ' [' + command + '] invoked successuflly.');
  21:                 }, 2000);
  22:             });  
  23:         },
  24:         close: function (db) {
  25:             return $q(function (resolve, reject) {
  26:                 $timeout(function () {
  27:                     resolve(db + ' is disconnected.');
  28:                 }, 2000);
  29:             });  
  30:         }
  31:     };
  32: });

In controller I invoked "database.open" method and handle and append a log entry when connected. Note that in this function I invoked "database.query", which return another promise.

   1: app.controller('HomeCtrl', function ($scope, database) {
   2:     $scope.logs = [];
   3:     var db = 'shaun-db';
   4:     
   5:     database.open(db)
   6:             .then(function (result) {
   7:                 $scope.logs.push(result);
   8:                 return database.query(db, 'SELECT * FROM [Products]');
   9:             });
  10: }); 

Now I can invoke "then" right after it, which called "invocation chaining", to perform my next operation.

   1: app.controller('HomeCtrl', function ($scope, database) {
   2:     $scope.logs = [];
   3:     var db = 'shaun-db';
   4:     
   5:     database.open(db)
   6:             .then(function (result) {
   7:                 $scope.logs.push(result);
   8:                 return database.query(db, 'SELECT * FROM [Products]');
   9:             })
  10:             .then(function (result) {
  11:                 $scope.logs.push(result);
  12:                 return database.execute(db, 'INSERT INTO [Products] VALUE (5, \'Worktile\')');
  13:             })
  14:             .then(function (result) {
  15:                 $scope.logs.push(result);
  16:                 return database.close(db);
  17:             })
  18:             .then(function (result) {
  19:                 $scope.logs.push(result);
  20:             });
  21: }); 

Now let's run it you will see the operations are invoked one by one in series.

02

 

$q also supports execute asynchronous functions in parallel. In the example above we invoked one command when database was connected. It's a common case we can run some query in parallel, and when all of them are finished we will run some commands, then close the connection.

Below I tweaked "database" service a little bit in its "execute" method, allow user specify the timeout period.

   1: execute: function (db, command, timeout) {
   2:     return $q(function (resolve, reject) {
   3:         $timeout(function () {
   4:             resolve(db + ' [' + command + '] invoked successuflly.');
   5:         }, timeout); 
   6:     });  
   7: },

In controller when the database was connected and first query finished, I invoked 5 commands through "execute" method, which will be processed in parallel, and pushed the promises into an array, Then I was using "$q.all" method. This method accepts an array of promises and will invoke its "then" when all of them are resolved.

   1: .then(function (result) {
   2:     $scope.logs.push(result);
   3:     var commandPromises = [];
   4:     commandPromises.push(database.execute(db, 'CMD 1', 1000));
   5:     commandPromises.push(database.execute(db, 'CMD 2', 500));
   6:     commandPromises.push(database.execute(db, 'CMD 3', 200));
   7:     commandPromises.push(database.execute(db, 'CMD 4', 200));
   8:     commandPromises.push(database.execute(db, 'CMD 5', 3000));
   9:     return $q.all(commandPromises);
  10: })

After that I executed two commands in parallel in the same way, and close the database connection when all of them are finished.

   1: .then(function (result) {
   2:     $scope.logs.push(result);
   3:     var commandPromises = [];
   4:     commandPromises.push(database.execute(db, 'CMD 6', 200));
   5:     commandPromises.push(database.execute(db, 'CMD 7', 300));
   6:     return $q.all(commandPromises);
   7: })

When we run our application you will see the procedure was waiting for all parallel commands finished then process the next one.

03

 

Examples in Shaun's Angular Toolkits

Now let's take a look on how I used $q in the code of my angular toolkits. In "sx.wizard" user can specify step template URL or template inline code. When "template" property was specified I will use it directly, when "templateUrl" was specified I will load the content through $http, which is an asynchronous operation. Hence I have a function responsible for creating template promise.

   1: var _getTemplatePromise = function(step) {
   2:     if (step.template) {
   3:         step.template = '' +
   4:             '<div class="sx-wizard-step" sx-wizard-step-id="' + step.id + '">' +
   5:             step.template +
   6:             '</div>';
   7:         return $q.when(step);
   8:     } else {
   9:         return $http.get(step.templateUrl).then(function(response) {
  10:             step.template = '' +
  11:                 '<div class="sx-wizard-step" sx-wizard-step-id="' + step.id + '">' +
  12:                 response.data +
  13:                 '</div>';
  14:             return step;
  15:         });
  16:     }
  17: };

In the code above I checked if "template" property was specified. If so I will wrap some HTML code and return a new promise through "$q.when". "$q.when" can be used when we are not pretty sure if an object is a normal object or a promise. It create a new promise wrapping this kind of object so that we can dealing it in $q. If the value passed in "$q.when" is a normal object it will be resolved immediately.

If "templateUrl" was specified I invoked "$http.get" to retrieve template content. Since "$http.get" returns a promise I use its "then" method to assign the content to step's template property.

Finally I returned the promise, no matter created by "$q.when" or "$http.get", into a local array.

   1: var _stepTemplatePromises = [];
   2:  
   3: ... ...
   4:  
   5: wizard.addStep = function(step) {
   6:     ... ...
   7:     _stepTemplatePromises.push(_getTemplatePromise(step));
   8:     return wizard;
   9: };

Then when the wizard is going to be shown, it will wait until all template are loaded successfully through "$q.all", then start to prepare scope and render UI.

   1: wizard.open = function(data, success, cancel) {
   2:     ... ...
   3:     $q.all(_stepTemplatePromises).then(function() {
   4:         var instance = $modal.open({
   5:             ... ...
   6:         });
   7:         instance.result.then(function(data) {
   8:             return success(data);
   9:         }, function() {
  10:             return cancel();
  11:         });
  12:     });
  13:     return wizard;
  14: };

 

Another example in "sx.tabs", which I'm going to introduce in the near future. User can specify tab content through inline template or URL. So I have the similar function for tab content loading.

   1: var _setTemplatePromise = function (tab) {
   2:     if (tab.template) {
   3:         tab.$templatePromise = $q.when(tab);
   4:     }
   5:     else {
   6:         tab.$templatePromise = $http.get(tab.templateUrl, {cache: $templateCache}).then(function (response) {
   7:             tab.template = response.data;
   8:             return tab;
   9:         });
  10:     }
  11: };

Then a tab was clicked, I checked the template promise status, when it's resolved I will start execute the tab's controller, compile and render.

   1: scope.switchTab = function (options, callback) {
   2:     ... ...
   3:     if (id && (!scope.activeTab || !scope.activeTab.id || scope.activeTab.id !== id)) {
   4:         var tab = scope.$tabs[id];
   5:         if (tab) {
   6:             $q.when(tab.$templatePromise).then(function () { 
   7:                 ... ...
   8:             });
   9:         }
  10:         else {
  11:             return callback(false);
  12:         }
  13:     }
  14:     else {
  15:         return callback(false);
  16:     }
  17: };

 

Summary

Asynchronous function is very common in JavaScript. There are many libraries handles this scenario. Angular.js support it by using "$q". Comparing with others pattern "$q" is lightweight and simple, with enough functionalities. And all asynchronous components in Angular.js are built based on "$q" such as "$http", "$timeout", etc.

When your operation is asynchronous, and you need to invoke some following operations after it finished, you should think about "$q" firstly.

 

Hope this helps,

Shaun

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.

Comments

No comments posted yet.
Post A Comment
Title:
Name:
Email:
Comment:
Verification: