发布于: 代码组织 > Deferreds

Deferred 示例

link 更多 Deferreds 示例

Deferreds 在 Ajax 背后被使用,但这并不意味着它们不能用于其他地方。本节描述了 Deferreds 有助于抽象异步行为并解耦代码的情形。

link 缓存

link 异步缓存

对于异步任务来说,缓存可能会有些苛刻,因为您必须确保对于给定的键,任务只执行一次。因此,代码必须以某种方式跟踪入站任务。

1
2
$.cachedGetScript( url, callback1 );
$.cachedGetScript( url, callback2 );

缓存机制必须确保 URL 只被请求一次,即使脚本尚未被缓存。这展示了一些逻辑,用于跟踪绑定到给定 URL 的回调,以便缓存系统正确处理已完成和入站的请求。

1
2
3
4
5
6
7
8
9
10
var cachedScriptPromises = {};
$.cachedGetScript = function( url, callback ) {
if ( !cachedScriptPromises[ url ] ) {
cachedScriptPromises[ url ] = $.Deferred(function( defer ) {
$.getScript( url ).then( defer.resolve, defer.reject );
}).promise();
}
return cachedScriptPromises[ url ].done( callback );
};

每个 URL 缓存一个 promise。如果给定 URL 还没有 promise,则创建一个 deferred 并发出请求。当请求完成时,deferred 被解决(使用 defer.resolve);如果发生错误,deferred 被拒绝(使用 defer.reject)。如果 promise 已经存在,回调被附加到现有的 deferred;否则,首先创建 promise,然后附加回调。这种解决方案的巨大优势在于它将透明地处理已完成和入站的请求。另一个优势是基于 deferred 的缓存将优雅地处理失败。promise 最终会被拒绝,可以通过提供一个错误回调来测试。

1
$.cachedGetScript( url ).then( successCallback, errorCallback );

link 通用异步缓存

也可以使代码完全通用,构建一个缓存工厂,当键尚未在缓存中时,它将抽象出要执行的实际任务。

1
2
3
4
5
6
7
8
9
10
11
$.createCache = function( requestFunction ) {
var cache = {};
return function( key, callback ) {
if ( !cache[ key ] ) {
cache[ key ] = $.Deferred(function( defer ) {
requestFunction( defer, key );
}).promise();
}
return cache[ key ].done( callback );
};
};

现在请求逻辑被抽象掉了,$.cachedGetScript() 可以重写如下:

1
2
3
$.cachedGetScript = $.createCache(function( defer, url ) {
$.getScript( url ).then( defer.resolve, defer.reject );
});

这将起作用,因为每次调用 $.createCache() 都会创建一个新的缓存存储库并返回一个新的缓存检索函数。

link 图像加载

缓存可用于确保同一图像不会被多次加载。

1
2
3
4
5
6
7
8
9
10
11
12
$.loadImage = $.createCache(function( defer, url ) {
var image = new Image();
function cleanUp() {
image.onload = image.onerror = null;
}
defer.then( cleanUp, cleanUp );
image.onload = function() {
defer.resolve( url );
};
image.onerror = defer.reject;
image.src = url;
});

同样,以下代码片段

1
2
$.loadImage( "my-image.png" ).done( callback1 );
$.loadImage( "my-image.png" ).done( callback2 );

将起作用,无论 my-image.png 是否已加载,或者它是否正在加载过程中。

link 缓存数据 API 响应

在页面的生命周期内被认为是不可变的 API 请求也是完美的候选者。例如,以下代码

1
2
3
4
5
6
7
8
9
10
11
$.searchTwitter = $.createCache(function( defer, query ) {
$.ajax({
url: "http://search.twitter.com/search.json",
data: {
q: query
},
dataType: "jsonp",
success: defer.resolve,
error: defer.reject
});
});

将允许您在 Twitter 上执行搜索并同时缓存它们。

1
2
$.searchTwitter( "jQuery Deferred", callback1 );
$.searchTwitter( "jQuery Deferred", callback2 );

link 计时

这种基于 deferred 的缓存不限于网络请求;它也可以用于计时目的。

例如,您可能需要在给定时间后在页面上执行一个操作,以吸引用户对他们可能不知道的特定功能的注意力,或者处理超时(例如测验问题)。虽然 setTimeout() 对于大多数用例都很好,但它不能处理稍后请求计时器的情况,即使它理论上已经过期了。我们可以使用以下缓存系统来处理这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var readyTime;
$(function() {
readyTime = jQuery.now();
});
$.afterDOMReady = $.createCache(function( defer, delay ) {
delay = delay || 0;
$(function() {
var delta = $.now() - readyTime;
if ( delta >= delay ) {
defer.resolve();
} else {
setTimeout( defer.resolve, delay - delta );
}
});
});

新的 $.afterDOMReady() 辅助方法在 DOM 准备就绪后提供适当的计时,同时确保使用最少的计时器。如果延迟已过期,任何回调都会立即被调用。

link 一次性事件

虽然 jQuery 提供了所有可能需要的事件绑定,但处理只需要处理一次的事件可能会变得有点麻烦。

例如,您可能希望有一个按钮,第一次点击时打开一个面板,之后保持打开状态,或者在第一次点击该按钮时执行特殊的初始化操作。处理这种情况时,通常会得到这样的代码:

1
2
3
4
5
6
7
8
9
var buttonClicked = false;
$( "#myButton" ).click(function() {
if ( !buttonClicked ) {
buttonClicked = true;
initializeData();
showPanel();
}
});

然后,稍后,您可能希望执行操作,但前提是面板已打开:

1
2
3
4
5
if ( buttonClicked ) {
// Perform specific action
}

这是一个高度耦合的解决方案。如果您想添加其他操作,您必须编辑绑定代码或只是复制所有内容。如果您不这样做,您唯一的选择是测试 buttonClicked,并且您可能会丢失该新操作,因为 buttonClicked 变量可能为 false,并且您的新代码可能永远不会执行。

使用 deferreds 我们可以做得更好(为了简化起见,以下代码只适用于单个元素和单个事件类型,但可以轻松推广到具有多种事件类型的完整集合):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$.fn.bindOnce = function( event, callback ) {
var element = $( this[ 0 ] ),
defer = element.data( "bind_once_defer_" + event );
if ( !defer ) {
defer = $.Deferred();
function deferCallback() {
element.unbind( event, deferCallback );
defer.resolveWith( this, arguments );
}
element.bind( event, deferCallback )
element.data( "bind_once_defer_" + event , defer );
}
return defer.done( callback ).promise();
};

代码工作原理如下:

  • 检查元素是否已经为给定事件附加了 deferred:
  • 如果没有,则创建它,并使其在事件第一次触发时得到解决:
  • 然后将给定的回调附加到 deferred 并返回 promise:

虽然代码肯定更冗长,但它使得以一种模块化和解耦的方式处理手头的问题变得更简单。但让我们首先定义一个辅助方法:

1
2
3
$.fn.firstClick = function( callback ) {
return this.bindOnce( "click", callback );
};

然后逻辑可以重构如下:

1
2
3
4
var openPanel = $( "#myButton" ).firstClick();
openPanel.done( initializeData );
openPanel.done( showPanel );

如果稍后只有在面板打开时才执行操作:

1
2
3
4
5
openPanel.done(function() {
// Perform specific action
});

如果面板尚未打开,则不会丢失任何内容,操作只会推迟到按钮被点击。

link 组合辅助方法

所有上述示例单独看可能显得有些有限。然而,当您将它们混合在一起时,promise 的真正力量就会发挥作用。

link 首次点击时请求面板内容并打开该面板

以下是当点击时打开面板的按钮代码。它通过网络请求其内容,然后淡入内容。使用前面定义的辅助方法,它可以定义为:

1
2
3
4
5
6
7
8
9
$( "#myButton" ).firstClick(function() {
var panel = $( "#myPanel" );
$.when(
$.get( "panel.html" ),
panel.slideDownPromise()
).done(function( ajaxResponse ) {
panel.html( ajaxResponse[ 0 ] ).fadeIn();
});
});

link 首次点击时在面板中加载图像并打开该面板

另一个可能的目标是让面板淡入,前提是按钮已被点击并且所有图像都已加载。

为此的 HTML 代码看起来像这样:

1
2
3
4
5
6
<div id="myPanel">
<img data-src="image1.png" alt="">
<img data-src="image2.png" alt="">
<img data-src="image3.png" alt="">
<img data-src="image4.png" alt="">
</div>

我们使用 data-src 属性来跟踪真实的图像位置。使用我们的 promise 辅助方法处理我们的用例的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$( "#myButton" ).firstClick(function() {
var panel = $( "#myPanel" ),
promises = [];
panel.find( "img" ).each(function() {
var image = $( this ),
src = element.attr( "data-src" );
if ( src ) {
promises.push(
$.loadImage( src ).then(function() {
image.attr( "src", src );
}, function() {
image.attr( "src", "error.png" );
})
);
}
});
promises.push( panel.slideDownPromise() );
$.when.apply( null, promises ).done(function() {
panel.fadeIn();
});
});

这里的诀窍是跟踪所有 $.loadImage() promise。我们稍后使用 $.when() 将它们与面板 .slideDown() 动画结合起来。因此,当按钮第一次被点击时,面板将向下滑动,图像将开始加载。一旦面板完成向下滑动并且所有图像都已加载,那么,也只有到那时,面板才会淡入。

link 在特定延迟后加载页面上的图像

为了在整个页面上实现延迟图像显示,可以在 HTML 中使用以下格式。

1
2
3
4
<img data-src="image1.png" data-after="1000" src="placeholder.png" alt="">
<img data-src="image2.png" data-after="1000" src="placeholder.png" alt="">
<img data-src="image1.png" src="placeholder.png" alt="">
<img data-src="image2.png" data-after="2000" src="placeholder.png" alt="">

它所说的非常简单:

  • 加载 image1.png 并立即显示第三个图像,第一个图像在一秒后显示。
  • 加载 image2.png 并在第二个图像一秒后显示,第四个图像两秒后显示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$( "img" ).each(function() {
var element = $( this ),
src = element.attr( "data-src" ),
after = element.attr( "data-after" );
if ( src ) {
$.when(
$.loadImage( src ),
$.afterDOMReady( after )
).then(function() {
element.attr( "src", src );
}, function() {
element.attr( "src", "error.png" );
}).done(function() {
element.fadeIn();
});
}
});

为了延迟图像本身的加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$( "img" ).each(function() {
var element = $( this ),
src = element.attr( "data-src" ),
after = element.attr( "data-after" );
if ( src ) {
$.afterDOMReady( after, function() {
$.loadImage( src ).then(function() {
element.attr( "src", src );
}, function() {
element.attr( "src", "error.png" );
}).done(function() {
element.fadeIn();
});
});
}
});

在这里,在延迟满足后,图像才被加载。当您想限制页面加载时的网络请求数量时,这很有意义。