TL;DR
这篇文章讲解了三种根据 service 的状态更新 directive 的做法。分别是 $watch
表达式,事件传递,和 controller 的计算属性。
问题
我有一个 readerService
,其中包含一些状态信息(比如连接状态和电量)。现在我需要做一个 directive 去展示这些状态。因为它只需要从 readerService
中获取数据,不需要任何外部传值,所以我直接把 service 注入进去。但如何更新就成了一个问题。
service 的代码如下。
const STATUS = {
DETACH: 'DETACH',
ATTACH: 'ATTACH',
READY: 'READY'
}
class ReaderService {
constructor() {
this.STATUS = STATUS
// The status will be changed by some callbacks
this.status = STATUS.DETACH
}
}
angular.module('app').service('readerService', readerService)
directive 代码如下:
angular.module('app').directive('readerIndicator', (readerService) => {
const STATUS = readerService.STATUS
const STATUS_DISPLAY = {
[STATUS.DETACH]: 'Disconnected',
[STATUS.ATTACH]: 'Connecting...',
[STATUS.READY]: 'Connected',
}
return {
restrict: 'E',
scope: {},
template: `
<div class="status">
{{statusDisplay}}
</div>
`,
link(scope) {
// Set and change scope.statusDisplay here
}
}
})
我尝试过以下几种办法,下面一一介绍。
方法一:$watch
第一个想到的方法就是在 directive 中用 $watch
去监视 readerService.status
。因为它不是 directive scope 的属性,所以我们需要用一个函数来包裹它。Angular 会在 dirty-checking 时计算和比较新旧值,只有状态真的发生了改变才会触发回调。
// In directive
link(scope) {
scope.$watch(() => readerService.status, (status) => {
scope.statusDisplay = STATUS_DISPLAY[status]
})
}
这个做法足够简单高效,只要涉及 readerService.status
改变的代码会触发 dirty-checking ,directive 就会自动更新。service 不需要修改任何代码。
但如果有多个 directive 的属性都受 service status 的影响,那 $watch
代码就看得比较晦涩了。尤其是 $watch
修改的值会影响其他的值的时候。比如:
// In directive
link(scope) {
scope.$watch(() => readerService.status, (status) => {
scope.statusDisplay = STATUS_DISPLAY[status]
scope.showBattery = status !== STATUS.DETACH
})
scope.$watch('showBattery', () => {
// some other things depend on showBattery
})
}
这种时候声明式的编程风格会更容易看懂,比如 Ember 或 Vue 里面的 computed property 。这个待会讨论。
方法二:$broadcast/$emit + $on
这种思路是 service 每次状态改变都发送一个事件,然后 directive 监听事件来改变状态。因为 directive 渲染的时候也许 status 已经更新了。所以我们需要在 link
中计算一个初始值。
我最开始是用 $broadcast
去做的。代码如下:
// In service
setStatus(value) {
this.status = value
// Need to inject $rootScope
this.$rootScope.$broadcast('reader.statusChanged', this.status)
}
// In directive
link(scope) {
scope.statusDisplay = STATUS_DISPLAY[nfcReaderService.status]
scope.$on('reader.statusChanged', (event, status) => {
scope.statusDisplay = STATUS_DISPLAY[status]
})
}
但马上发现 $broadcast
之后 UI 更新总要等 1 秒多(不过 $on
回调倒是很快)。Google 一番后知道原因是 $broadcast
是向下层所有 scope 广播,广播完成后再 dirty-checking 。一个更好的做法是使用 $emit
,它只会向上传递事件,不过不管发送事件还是监听事件都得用 $rootScope
。
修改后的代码如下:
// In service
setStatus(value) {
this.status = value
// Use $emit instead of $broadcast
this.$rootScope.$emit('reader.statusChanged', this.status)
}
// In directive
link(scope) {
scope.statusDisplay = STATUS_DISPLAY[nfcReaderService.status]
// Use $rootScope instead of scope
$rootScope.$on('reader.statusChanged', (event, status) => {
scope.statusDisplay = STATUS_DISPLAY[status]
})
}
如果因为某些原因不得不用 $broadcast
的话,你可以在 $on
回调最后用 $digest
或 $apply
强制触发 dirty-checking ,这也可以达到快速更新 UI 的目的。
方法三:controller + property
我个人觉得前两个方法能解决问题,但代码维护性都不太好。$watch
在属性相互关联的情况下非常难看懂,$emit/$on
需要把一些逻辑写两次(初始化 directive 时和回调执行时)。方法一中我提到了有些时候声明式的属性比 $watch
更容易看懂。这个方法就是使用 controller 。directive 可以设置自己的 controller 作为数据来源(或者说 view model),我们可以把那些需要计算的属性作为 controller 的属性。这样 dirty-checking 时它们就会自动计算。
// In directive
class ReaderController {
constructor($scope, readerService) {
this.readerService = readerService
}
get statusDisplay() {
return STATUS_DISPLAY[this.readerService.status]
}
}
return {
// ...
controller: ReaderController,
controllerAs: 'vm',
template: `
<div class="status">
{{vm.statusDisplay}}
</div>
`
}
这样一来,大部分逻辑都可以挪到 controller 中。如果没有 DOM 操作我们甚至可以不写 link
方法。也没必要加入额外的 $watch
和 $on
。只是因为 dirty-checking 的特性,绑定到 template 的属性往往会多计算几次。所以属性必须非常简单。大部分情况下这不会有什么问题。
参考资料
$rootScope.Scope
Angular API ,可以看看里面对 $watch
,$broadcast
,$emit
, $on
的描述。
$rootScope.$emit() vs $rootScope.$broadcast()$emit
和 $broadcast
的性能比较。注意后来的 Angular 已经解决了性能差异,两者相差无几。