来源:http://www.bennadel.com/blog/2800-forcing-q-notify-to-execute-with-a-no-op-in-angularjs.htm
强制 $q .notify() 执行
.notify() 事件的美妙之处在于,我们的数据访问层可以使用它来提供“立即可用但陈旧”的数据,同时仍然使用 .resolve() 事件来获取实时数据。这为调用上下文(您的控制器)提供了很好的洞察力,并可以控制缓存的数据集以及它 [控制器] 是否甚至想要合并缓存的数据。
但是,我们遇到了一点竞争条件。拥有缓存数据的数据访问服务需要调用 .notify() 才能将 Promise 返回给调用上下文。这意味着您的控制器在 .notify() 被调用后绑定到通知事件。从哲学的角度来看,这应该没问题 - Promise(以及几乎所有事件驱动的)旨在异步调用绑定以创建访问的统一性。
然而,从实际的角度来看,它并不是那么简单。虽然 AngularJS 遵循这一理念,但它还添加了一些优化以减少处理。具体在我们的例子中,AngularJS 不会在延迟对象中安排回调处理,除非它看到至少一个回调被绑定(否则它认为世界没有在监听)。因此,我们的控制器将永远不会收到有关缓存数据的通知。
为了解决这个问题,我们可以让我们的服务层在调用 .notify() 之前将一个无操作(无操作)函数绑定到通知事件。这样,当它调用 .notify() 时,AngularJS 将看到至少注册了一个回调,并且它会安排在下一个滴答中刷新挂起的队列(这是通过 $rootScope.$evalAsync() 实现的)。这使我们的控制器即使在调用 .notify() 之后绑定到通知事件,也可以收到缓存数据的通知。
为了看到这一点,我创建了一个friendService,它通过两种不同的方法返回数据。这两种方法都尝试通过 .notify() 返回缓存数据,然后通过 .resolve() 返回“实时”数据。这两种方法的唯一区别是在调用 .notify() 之前将 no-op 绑定到 notify 事件
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Forcing $q .notify() To Execute With A No-Op In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">
<h1>
Forcing $q .notify() To Execute With A No-Op In AngularJS
</h1>
<h2>
Friends
</h2>
<div ng-switch="isLoading">
<!-- Show while friends are being loaded. -->
<p ng-switch-when="true">
<em>Loading...</em>
</p>
<!-- Show once the friends have loaded and are available in the view-model. -->
<ul ng-switch-when="false">
<li ng-repeat="friend in friends track by friend.id">
{{ friend.name }}
</li>
</ul>
</div>
<p>
<a ng-click="load()">Load</a>
|
<a ng-click="loadWithNoop()">Load With No-Op</a>
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.13.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope, friendService ) {
$scope.isLoading = false;
$scope.friends = [];
// Load the friend data (defaults to "get" method vs. "getWithNoop").
loadRemoteData();
// ---
// PUBLIC METHODS.
// ---
// I reload the list of friends using friendService.get().
$scope.load = function() {
loadRemoteData( "get" );
};
// I reload the list of friends using friendService.getWithNoop().
$scope.loadWithNoop = function() {
loadRemoteData( "getWithNoop" );
};
// ---
// PRIVATE METHODS.
// ---
// I load the friends from the friend repository. I am passing-in the
// method name to demonstrate that, from the Controller's point-of-view,
// nothing here is different other than the name of the method. The real
// substantive difference exists in the implementation of the friend-
// Service method and how it interacts with $q / Deferred.
function loadRemoteData( loadingMethod ) {
console.info( "Loading friends with [", loadingMethod, "]" );
// Indicate that we are in the loading phase.
$scope.isLoading = true;
// When we make the request, we expect the service to try to use
// cached-data, which it will make available via the "notify" event
// handler on the promise. As such, we're going to wire up the same
// event handler to both the "resolve" and the "notify" callbacks.
friendService[ loadingMethod || "get" ]
.call( friendService )
.then(
handleResolve, // Resolve.
null,
handleResolve // Notify.
)
;
function handleResolve( friends ) {
// Indicate that the data is no longer being loaded.
$scope.isLoading = false;
$scope.friends = friends;
console.log( "Friends loaded successfully at", ( new Date() ).getTime() );
}
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I provide access to the friend repository.
app.factory(
"friendService",
function( $q, $timeout ) {
// Our friend "repository".
var friends = [
{
id: 1,
name: "Tricia"
},
{
id: 2,
name: "Heather"
},
{
id: 3,
name: "Kim"
}
];
// Return the public API.
return({
get: get,
getWithNoop: getWithNoop
});
// ---
// PUBLIC METHODS.
// ---
// I return the list of friends. If the friends are cached locally, the
// cached collection will be exposed via the promise' .notify() event.
function get() {
var deferred = $q.defer();
// Notify the calling context with the cached data.
deferred.notify( angular.copy( friends ) );
$timeout(
function networkLatency() {
deferred.resolve( angular.copy( friends ) );
},
1000,
false // No need to trigger digest - $q will do that already.
);
return( deferred.promise );
}
// I return the list of friends. If the friends are cached locally, the
// cached collection will be exposed via the promise' .notify() event.
function getWithNoop() {
var deferred = $q.defer();
// -- BEGIN: Hack. ----------------------------------------------- //
// CAUTION: This is a work-around for an optimization in the way
// AngularJS implemented $q. When we go to invoke .notify(),
// AngularJS will ignore the event if there are no pending callbacks
// for the event. Since our calling context can't bind to .notify()
// until after we invoke .notify() here (and return the promise),
// AngularJS will ignore it. However, if we bind a No-Op (no
// operation) function to the .notify() event, AngularJS will
// schedule a flushing of the deferred queue in the "next tick,"
// which will give the calling context time to bind to .notify().
deferred.promise.then( null, null, angular.noop );
// -- END: Hack. ------------------------------------------------- //
// Notify the calling context with the cached data.
deferred.notify( angular.copy( friends ) );
$timeout(
function networkLatency() {
deferred.resolve( angular.copy( friends ) );
},
1000,
false // No need to trigger digest - $q will do that already.
);
return( deferred.promise );
}
}
);
</script>
</body>
</html>
如您所见,控制器将相同的处理程序绑定到 promise 的“resolve”和“notify”事件。这样就可以统一处理缓存数据和直播数据。唯一的区别在于它调用的服务层方法 - get() 与 getWithNoop()。而且,如果我们调用 .get() 几次,然后调用 .getWithNoop() 几次,我们可以在控制台中看到差异。