资讯专栏INFORMATION COLUMN

Angular 根据 service 的状态更新 directive

Y3G / 2133人阅读

摘要:分别是表达式,事件传递,和的计算属性。问题我有一个,其中包含一些状态信息比如连接状态和电量。现在我需要做一个去展示这些状态。因为渲染的时候也许已经更新了。

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: `
      
{{statusDisplay}}
`, 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: `
    
{{vm.statusDisplay}}
` }

这样一来,大部分逻辑都可以挪到 controller 中。如果没有 DOM 操作我们甚至可以不写 link 方法。也没必要加入额外的 $watch$on 。只是因为 dirty-checking 的特性,绑定到 template 的属性往往会多计算几次。所以属性必须非常简单。大部分情况下这不会有什么问题。

参考资料

$rootScope.Scope
Angular API ,可以看看里面对 $watch$broadcast$emit , $on 的描述。

$rootScope.$emit() vs $rootScope.$broadcast()
$emit$broadcast 的性能比较。注意后来的 Angular 已经解决了性能差异,两者相差无几。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79084.html

相关文章

  • [译] Controller间数据共享?最佳实践:使用Service

    摘要:共享数据的最佳策略是什么呢用一些变态的控制器继承方案吗当然不是,最简单容易的方式就是使用服务。概括创建一个服务去存放你的数据,并给数据创建和的方法。 原文链接 : Sharing Data Between Controllers? Best Practice: Use a Service原文作者 : DAVE CEDDIA译者 : 李林璞(web前端领域)译者注:翻译如有疏漏,欢迎指出...

    Salamander 评论0 收藏0
  • Angular1.x + TypeScript 编码风格

    摘要:组件还包含数据事件的输入与输出,生命周期钩子和使用单向数据流以及从父组件上获取数据的事件对象备份。 说明:参照了Angular1.x+es2015的中文翻译,并将个人觉得不合适、不正确的地方进行了修改,欢迎批评指正。 架构,文件结构,组件,单向数据流以及最佳实践 来自@toddmotto团队的实用编码指南 Angular 的编码风格以及架构已经使用ES2015进行重写,这些在Angul...

    ytwman 评论0 收藏0
  • Angular $rootScope:inprog 问题探究

    摘要:场景和问题这几天在写一个。因为状态的改变由另一个插件控制,不在的中。前者强制触发一次,后者让一段代码执行完成后触发。所以直接在回调中使用就可以解决问题。参考资料对异常的描述。这种异常附带在线文档的方式还是很方便的。 TL;DR 这是一个关于 $rootScope:inprog 错误在什么样的情况下被触发,和如何解决的故事。 场景和问题 这几天在写一个 service 。这个 servi...

    shiguibiao 评论0 收藏0
  • Angular中使用类Redux工具—ngrx/store

    摘要:因为我们正好要使用,此时我们就可以用的服务来监听了当然,我们不能忘了清空订阅最后一步就是在有返回的时候调用表单的现在我们可以在组件里显示提醒了 原文:Connect Angular Forms to @ngrx/store showImg(https://segmentfault.com/img/bVRpdX?w=1000&h=674); 这篇文章中,我们将要讨论如何用ngrx/eff...

    bitkylin 评论0 收藏0

发表评论

0条评论

Y3G

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<