原文:http://blog.mediumequalsmessage.com/promise-deferred-objects-in-javascript-pt2-practical-use

Introduction

In part 1 of this post, I spent a lot of time looking at the theory of promises and deferreds: what promises are and how they behave. Now it’s time to actually explore some ways to use promises and deferreds in JavaScript and dive into some best practices. I’ll start with some basic uses and examples of promises and then will dive into a number of specifics pertaining to using promises in jQuery. While the exact specifics of the API may vary from your chosen library, there is enough overlap in concepts to illustrate the use of promises.

Note: The code samples will use jQuery in spite of jQuery’s deviations from the Promise/A proposal, which is particularly noticeable in the case of error handling and when working with other promise libraries. Since part 1 discussed and included links to in-depth articles on the topic, I will refrain from saying anything else about it in this post. The use of jQuery is still wide-spread and their implementation serves as an introduction for many to the use of promises both of which lead me to believe that it is valuable to have an understanding of working with promises in jQuery.

Sequencing Patterns

A deferred is an object representing work that is not yet done and a promise is an object representing a value that is not yet known. In other words, promises / deferreds allow us to represent ‘simple’ tasks and can be easily combined to represent complex tasks and their flows, allowing for fine-grained control over sequencing. This means we can write asynchronous JavaScript parallel to how we write synchronous code. Additionally, promises make it relatively simple to abstract small pieces of functionality shared across multiple asynchronous tasks — consider as an example loading animations, progress animations etc.

Let’s begin with a global view of three common sequencing patterns that promises make possible: stacked, parallel and sequential.

  • Stacked: bind multiple handlers anywhere in the application to the same promise event.

      var request = $.ajax(url);
    
      request.done(function () {
    console.log('Request completed');
    }); // Somewhere else in the application
    request.done(function (retrievedData) {
    $('#contentPlaceholder').html(retrievedData);
    });
  • Parallel tasks: ask multiple promises to return a promise which alerts of their mutual completion.

      $.when(taskOne, taskTwo).done(function () {
    console.log('taskOne and taskTwo are finished');
    });
  • Sequential tasks: execute tasks in sequential order.

    var step1, step2, url;
    
    url = 'http://fiddle.jshell.net';
    
      step1 = $.ajax(url);
    
      step2 = step1.then(
    function (data) {
    var def = new $.Deferred(); setTimeout(function () {
    console.log('Request completed');
    def.resolve();
    },2000); return def.promise(); },
    function (err) {
    console.log('Step1 failed: Ajax request');
    }
    );
    step2.done(function () {
    console.log('Sequence completed')
    setTimeout("console.log('end')",1000);
    });

These patterns can be combined or used separately building up complex tasks and workflows.

Common Use Cases

Many examples of promise use cases pertain to Ajax requests and UI animations. In fact jQuery even returns promises by default from Ajax requests. This makes sense given that promises are ideal for asynchronous tasks whose completion needs to be handled in a unique way. However, that doesn’t mean that the use of promises should be limited to these use case. In fact, promises tend to be a tool worth considering anytime you might otherwise reach for a callback. That said, let’s have a look at some ways we can use promises.

  • Ajax

Examples of using promises with Ajax requests can be found throughout this post, so I will skip an example here.

  • Timing

We can create a promise based timeout function.

  function wait(ms) {
var deferred = $.Deferred();
setTimeout(deferred.resolve, ms); // We just need to return the promise not the whole deferred.
return deferred.promise();
} // Use it
wait(1500).then(function () {
// Do something brilliant here!
});
  • Animation

Obviously the following animation is completely useless, but it serves as an example of how promises and animations can be used together.

  var fadeIn = function (el) {

      var promise = $(el).animate({
opacity: 1
}, 1500); // Dynamically create and return an observable promise object which will be resolved when the animation completes.
return promise.promise();
}; var fadeOut = function(el) { var promise = $(el).animate({
opacity: 0
}, 1500); // Dynamically create and return an observable promise object
return promise.promise();
}; // With the setup out of the way, we can now do one of the following. // Parallel
$.when(
fadeOut('div'),
fadeIn('div')
).done(function () {
console.log('Animation finished');
$('p').css('color', 'red');
}); // OR
// Chained
fadeOut('div').then(function (el) {
fadeIn(el); // returns a promise
}).then(function (el) {
fadeOut(el); // returns a promise
});
  • Synchronizing parallel tasks with $.when()

var promiseOne, promiseTwo, handleSuccess, handleFailure; // Promises
promiseOne = $.ajax({ url: '../test.html' });
promiseTwo = $.ajax({ url: '../test.html' }); // Success callbacks
// .done() will only run if the promise is successfully resolved
promiseOne.done(function () {
console.log('PromiseOne Done');
}); promiseTwo.done(function () {
console.log('PromiseTwo Done');
}); // $.when() creates a new promise which will be:
// resolved if both promises inside are resolved
// rejected if one of the promises fails
$.when(
promiseOne,
promiseTwo
)
.done(function () {
console.log('promiseOne and promiseTwo are done');
})
.fail(function () {
console.log('One of our promises failed');
});
  • Decoupling events and application logic

We can also use events to trigger resolution / failure of promises, passing values through at the same time which allows us to decouple application, DOM and event logic ( jsfiddle here ).

var def, getData, updateUI, resolvePromise;

// The Promise and handler
def = new $.Deferred(); updateUI = function (data) {
$('p').html('I got the data!');
$('div').html(data);
};
getData = $.ajax({
url: '/echo/html/',
data: {
html: 'testhtml',
delay: 3
},
type: 'post'
})
.done(function(resp) {
return resp;
})
.fail(function (error) {
throw new Error("Error getting the data");
}); // Event Handler
resolvePromise = function (ev) {
ev.preventDefault();
def.resolve(ev.type, this);
return def.promise();
}; // Bind the Event
$(document).on('click', 'button', resolvePromise); def.then(function() {
return getData;
})
.then(function(data) {
updateUI(data);
})
.done(function(promiseValue, el) {
console.log('The promise was resolved by: ', promiseValue, ' on ', el);
}); // Console output: The promise was resolved by: click on <button> </button>

Gotcha’s: understanding .then() in jQuery

In order to demonstrate a couple “gotcha’s”, these final examples will walk through part of my learning process when I first started playing with promises.

Let’s assume the following two utility functions for the following examples:

// Utility Functions
function wait(ms) {
var deferred = $.Deferred();
setTimeout(deferred.resolve, ms);
return deferred.promise();
}
function notifyOfProgress(message, promise) {
console.log(message + promise.state());
}

My first attempt at chaining promises together looked something like this:

// Naive attempt at working with .then()

// Create two new deferred objects
var aManualDeferred = new $.Deferred(),
secondManualDeferred = aManualDeferred.then(function () {
console.log('1 started'); wait(3500).done(function () {
console.log('1 ended');
});
}); // After secondManualDeferred is resolved
secondManualDeferred.then(function () {
console.log('2 started'); wait(2500).done(function () {
console.log('2 ended');
});
}); // Resolve the first promise
aManualDeferred.resolve();

Upon executing this, the console output is what I would have expected had I not used promises.

1 started
2 started
2 ended
1 ended

The jQuery API says that .then() is chainable and returns a new promise, so my expectation was that whatever I wrapped in .then() and chained together would occur sequentially and wait for any tasks to finish before moving to the next. Clearly that’s not what happened. Why not?

How does .then() actually work?

Looking in the jQuery source code, we find that:

  • .then() always returns a new promise
  • .then() must be passed a function

If .then() is not passed a function:

  • the new promise will have the same behaviour as the original promise ( which means it is immediately resolved/rejected ),
  • the input inside .then() will be executed but is ignored by .then()

if .then() is passed a function which returns a promise object:

  • the new promise will have the same behaviour as the returned promise

var deferred = $.Deferred(),
secondDeferred = deferred.then(function () {
return $.Deferred(function (newDeferred) {
setTimeout(function() {
console.log('timeout complete');
newDeferred.resolve();
}, 3000);
});
}),
thirdDeferred = secondDeferred.then(function () {
console.log('thirdDeferred');
}); secondDeferred.done(function () {
console.log('secondDeferred.done');
});
deferred.resolve();
  • if .then() is passed a function which returns a value, the value becomes the value of the new object

var deferred = $.Deferred(),
filteredValue = deferred.then(function (value) {
return value * value;
}); filteredValue.done(function (value) {
console.log(value);
}); deferred.resolve(2); // 4

You probably already see ( if you didn’t see it right away ) why my version didn’t work. I didn’t explicitly return a promise from .then() so the new promise created by .then() had the same values as the promise it was chained to.

Avoiding the descent into callback hell

We know we need to pass .then() a function for it to be able to do it’s job and we know we need to return a promise from .then(). So we could do the following:

// Anti-pattern - Return to callback hell

var aManualDeferred = new $.Deferred();

aManualDeferred.then(function () {
console.log('1 started'); return wait(3500).then(function () {
console.log('1 ended');
}).then(function () {
console.log('2 started'); return wait(2500).done(function () {
console.log('2 ended');
});
});
}); // Resolve the first promise
aManualDeferred.resolve();

This works. Unfortunately, it’s starting the decent back into callback hell which is one of the things that promises are supposed to help us avoid. Luckily, there are a number of ways to handle this without decending into the territory of deeply nested functions. How we choose to solve it, is of course dependent on our particular situation.

Avoiding extensive use of unnamed promises

We could for example do the following:

// A chain
// Create new deferred objects
var aManualDeferred = $.Deferred(); aManualDeferred.then(function () {
console.log('1 started'); // We need to return this, we return a new promise which is resolved upon completion.
return wait(3500);
}) .then(function () {
console.log('1 ended');
}) .then(function () {
console.log('2 started');
return wait(2500);
}) .then(function () {
console.log('2 ended');
}); // Resolve the first promise
aManualDeferred.resolve();

This version admittedly reads very nicely but has the disadvantage of only one named promise which doesn’t really give us the fine-grained control over each step in the process that is desirable in many situations.

Unwinding promises and their handlers

Assuming we want to avoid deeply nested functions and that we should name our promises to give us access to each step of the process, here is a final version:

var aManualDeferred, secondManualDeferred, thirdManualDeferred;

// Create two new deferred objects
aManualDeferred = $.Deferred(); secondManualDeferred = aManualDeferred.then(function () {
console.log('1 started'); // We need to return this, we return a new promise which is resolved upon completion.
return wait(3500);
})
.done(function () {
console.log('1 ended');
}); thirdManualDeferred = secondManualDeferred.then(function () {
console.log('2 started');
return wait(2500);
})
.done(function () {
console.log('2 ended');
}); // Check current state
thirdManualDeferred.notify(
notifyOfProgress('thirdManualDeferred ', thirdManualDeferred)
); // Resolve the first promise
aManualDeferred.resolve(); // Console output
// aManualDeferred pending
// secondManualDeferred pending
// 1 started
// 1 ended
// 2 started
// 2 ended

The advantage gained by this version is that we now have 3 steps which we can clearly refer to giving the advantage of being able to ask each promise for it’s state to send notifications of progress, or later manage our sequencing as needed without having to re-write the code.

Context and passing data

In the Ajax example earlier, we saw that we can pass a value to .resolve() and .fail(). If a promise is resolved with a value, it returns that value as itself.

var passingData = function () {
var def = new $.Deferred(); setTimeout(function () {
def.resolve('50');
}, 2000); return def.promise();
}; passingData().done(function (value) {
console.log(value);
});

We can also set 'this’ when we resolve a promise.

// Create an object
var myObject = {
myMethod: function (myString) {
console.log('myString was passed from', myString);
}
}; // Create deferred
var deferred = $.Deferred(); // deferred.done(doneCallbacks [, doneCallbacks ])
deferred.done(function (method, string) {
console.log(this); // myObject // myObject.myMethod(myString);
this[method](string);
}); deferred.resolve.call(myObject, 'myMethod', 'the context'); => myString was passed from the context // We could also do this:
// deferred.resolveWith(myObject, ['myMethod', 'resolveWith']);
// but it's somewhat annoying to pass an array of arguments. // => myString was passed from resolveWith

Best Practices

I’ve attempted to illustrate some best practices along the way but for the sake of clarity, allow me to recap them under one heading. Quite frankly most of these amount to applying other best-practices when using promises: in particular: DRY and the Single Responsibility Principle. In

  • name your promises

    var step2 = step1.then()
  • separate handler functions from the promise logic by calling a named function from .then() and separate functionality into reusable bits

    var someReusableFunctionality = function () {
    // do something
    }; step2.then(someReusableFunctionality);
  • when it’s logical, return a promise instead of a deferred so that nobody else can inadvertantly resolve/reject the promise

    step2.then(function() {
    // we don't want to give resolution / rejection powers
    // to the wrong parties, so we just return the promise.
    return deferred.promise();
    });
  • don’t descend into nested callback hell or nested promise hell

By following these best practices, we can reap the most benefit from promises. We can craft decoupled applications with readable code, gain fine-gained control over asynchronous event sequencing, handle values that don’t exist yet as if they do and operations that haven’t completed yet as if they have.

jQuery Reference

I’d like to wrap-up with an overview of the jQuery API since my code examples have focused on jQuery’s implementation of promises. If you’re using a different implementation of promises, you may want to skip to the end.

Notes

  • deferred.always(), deferred.done(), deferred.fail() return the deferred object.
  • deferred.then(), deferred.when(), .promise() return a promise.
  • $.ajax() and $.get() return promise objects
  • instead of using .resolveWith() and .rejectWith(), you can call resolve with the context you want it to inherit
  • pass the deferred.promise() around instead of the deferred itself as the deferred object itself cannot be resolved or rejected through it.

$.Deferred()
A constructor that creates a new deferred object. Accepts an optional init function which will be executed immediately after the deferred is created.

deferred.always()
Returns the deferred object and executes attached functions upon resolution or rejection.

$.get("test.php")

// Execute regardless of resolution or success
.always(function() {
alertCompletedRequest();
});

deferred.then()
Adds handlers which will be called on resolved, rejected or in progress and returns a promise.

$.get("test.php")

// Execute regardless of resolution or success
.then(function() {
alertSuccess(),
alertFailure(),
alertProgress();
});

deferred.when()
Returns a new promise based on the completion of multiple promises. If any promise is rejected, .when() is rejected and if all promises are resolved, it is resolved. It is noteworthy that a non-promise can be passed to .when() and it will be treated as a resolved promise. Also of note is that it will return a single value only if all other promises resolve to a single value or an array, otherwise it resolves to an array.

deferred.resolve(optionalArgs) or deferred.reject(optionalArgs)
Resolve or reject the deferred object and call handler functions ( .done(), .fail(), .always(), .then() ) with any supplied arguments and pass their context to the handler they call.

$('body').on('button', 'click', function() {

    // Can be passed a value which will be given to handlers
deferred.resolve();
});

deferred.promise()
Returns the promise object of the deferred. If passed a target, .promise() will attach promise methods to the target instead of creating a new object.

deferred.state()
Useful for debugging and querying the state the deferred object is in. Returns: pending, resolved or rejected.

deferred.always()
Functions or an array of functions called regardless of reject and failure.deferred.done()
Functions or array of functions called upon resolution of the deferred object.

deferred.fail()
Functions or array of functions called upon rejection of the deferred object.

$.ajax()
Performs an Ajax request and returns a promise.

Conclusion

Managing asynchronous JavaScript and writing decoupled applications can be challenging. I hope by now you have a better understanding of what promises are, how you can use them and how to avoid some of the common pitfalls. There’s still a lot of ground I haven’t covered in these two posts and for that I refer you to your libraries docs and the resources mentioned at the end of both posts. Of course, if you have questions or feedback feel free to get in touch on app.net or twitter!

Author’s note:
In putting together these two posts, I am deeply indebted to the work of others. @dominic’s article You’re Missing the Point of Promises and the exchange between @dominic and @rwaldron on jQuery’s .then() really fueled my deep dive into how promises work. Trevor Burnham’s bookAsync JavaScript, Brian Cavalier’s Async Programming, Jesse Hallet’sPromise Pipeline in JavaScript, and of course the Promises/A andPromises/A+ proposals were also invaluable resources. Finally, special thanks to Rick Waldron and the author of jQuery’s .then() implementationJulian Aubourg for answering my questions in preparing this article.

Further Resources

Books

Articles

Promise & Deferred Objects in JavaScript Pt.2: in Practice的更多相关文章

  1. Promise & Deferred objects in JavaScript Pt.1: Theory and Semantics.

    原文:http://blog.mediumequalsmessage.com/promise-deferred-objects-in-javascript-pt1-theory-and-semanti ...

  2. based on Greenlets (via Eventlet and Gevent) fork 孙子worker 比较 gevent不是异步 协程原理 占位符 placeholder (Future, Promise, Deferred) 循环引擎 greenlet 没有显式调度的微线程,换言之 协程

    gevent GitHub - gevent/gevent: Coroutine-based concurrency library for Python https://github.com/gev ...

  3. A brief look at the Objects in JavaScript

    Objects  An object is a self-contained collection of data. This data comes in to forms:  properties ...

  4. Promise/Deferred

    [fydisk] 1.$.get('/api').success(onSuccess).error(onError).comlete(onComplete); 2.对同一事件加入多个Handler. ...

  5. 异步编程Promise/Deferred、多线程WebWorker

    长期以来JS都是以单线程的模式运行的,而JS又通常应用在操作用户界面和网络请求这些任务上.操作用户界面时不能进行耗时较长的操作否则会导致界面卡死,而网络请求和动画等就是耗时较长的操作.所以在JS中经常 ...

  6. Jquery AJAX如何使用Promise/Deferred实现顺序执行?

    有的时候有我有N个AJAX请求,第下个请求可能要依赖上个请求的返回值, 可以用 $.ajax("test1.php").then(function(data) { // data ...

  7. 细说Promise

    一.前言 JavaScript是单线程的,固,一次只能执行一个任务,当有一个任务耗时很长时,后面的任务就必须等待.那么,有什么办法,可以解决这类问题呢?(抛开WebWorker不谈),那就是让代码异步 ...

  8. javascript中的promise和deferred:实践(二)

    javascript中的promise和deferred:实践(二) 介绍: 在第一节呢,我花了大量的时间来介绍promises和deferreds的理论.现在呢,我们来看看jquery中的promi ...

  9. Javascript - Promise学习笔记

    最近工作轻松了点,想起了以前总是看到的一个单词promise,于是耐心下来学习了一下.   一:Promise是什么?为什么会有这个东西? 首先说明,Promise是为了解决javascript异步编 ...

随机推荐

  1. springboot 源码笔记

    1.springAppication构造器 基于getSpringFactoriesInstances方法构造如下类(获取文件内容在META-INF/spring.factories文件中) 1.1 ...

  2. .net Core学习笔记之MemoryCache

    .NET Core支持多种不同的缓存,其中包括MemoryCache,它表示存储在Web服务器内存中的缓存:     内存中的缓存存储任何对象; 分布式缓存界面仅限于byte[] 1:在.net co ...

  3. ASP.NET 关于GridView 表格重复列合并

    这几天做一个项目有用到表格显示数据的地方,客户要求重复的数据列需要合并,就总结了一下GridView 和 Repeater 关于重复数据合并的方法. 效果图如下 : GridView : 前台代码 : ...

  4. javascript中字符串常用操作总结

    String对象属性 (1) length属性 length算是字符串中非常常用的一个属性了,它的功能是获取字符串的长度.当然需要注意的是js中的中文每个汉字也只代表一个字符,这里可能跟其他语言有些不 ...

  5. SQL查询几种的区别。

    最近看了几篇SQL查询的文章做一下总结哦,大概简记如下: SQL查询的实质是,是指从数据库中取得数据的子集,可以先取列子集,然后再取符合条件的行子集. 1.单表查询: SELECT [Name] ,[ ...

  6. vps服务器搭建——Linode VPS 20美元优惠获取教程

    转载:http://www.cuishifeng.cn/linode/index.html?v=2 声明:本文旨在教大家怎么获得linode 20美元优惠,并免费使用4个月vps,请低调薅羊毛!(多张 ...

  7. 十七、curator recipes之DistributedPriorityQueue

    简介 官方文档:http://curator.apache.org/curator-recipes/distributed-priority-queue.html javaDoc:http://cur ...

  8. 百度AI人脸识别的学习总结

    本文主要分以下几个模块进行总结分析 项目要求:运用百度AI(人脸识别)通过本地与外网之间的信息交互(MQService),从而通过刷脸实现登陆.签字.会议签到等: 1.准备工作: 内网:单击事件按钮— ...

  9. PECL: configuration option "php_ini" is not set to php.ini location

    message similar to: configuration option "php_ini" is not set to php.ini locationYou shoul ...

  10. 适配器(GOF23)

    ---恢复内容开始--- 摘要:由于应用环境的变化,需要将现存的对象放到新的环境中去,但新环境的接口是现存对象不满足的. 意图:将原本接口不兼容的类通过转换,使得它们能够一起工作,复用现有的类 ada ...