Angular $rootScope:inprog 问题探究

TL;DR

这是一个关于 $rootScope:inprog 错误在什么样的情况下被触发,和如何解决的故事。

场景和问题

这几天在写一个 service 。这个 service 中有个状态需要注入到 directive 中做页面展现。因为状态的改变由另一个插件控制,不在 Angular 的 event loop 中。为了触发 dirty-checking 我在 service 中调用了 $rootScope.$digest()

service 代码大概如下所示:

const STATUS = {
  A: 'A',
  B: 'B',
}

class SomeService {
  constructor($rootScope) {
    this.$rootScope = $rootScope
  }

  start() {
    this.plugin = initPlugin

    // Register plugin callbacks
    this.plugin.onStateA = () => { this._setStatus(STATUS.A) }
    this.plugin.onStateB = () => { this._setStatus(STATUS.B) }
  }

  _setStatus(status) {
    this.status = status
    this.$rootScope.$digest()
  }
}

angular.module('app.someMod').service('someService', SomeService)

目前为止一切正常,直到因为需求改动,需要加一个状态,这个状态的改变是通过 directive 中的按钮触发的,于是我在 service 中加了一个方法,在 directive 中调用,代码如下:

// In service
class someService {
  connect() {
    this._setStatus(STATUS.C)
  }
}

// In directive, the "btnClick" is bound to an element's ng-click
scope.btnClick = () => {
  someService.connect()
}

然后一点击按钮,程序就跪了…… 控制台中报的错误是 $rootScope:inprog 。

解决方法

这段错误的官方描述如下:

At any point in time there can be only one $digest or $apply operation in progress. This is to prevent very hard to detect bugs from entering your application. The stack trace of this error allows you to trace the origin of the currently executing $apply or $digest call, which caused the error.

简单来说,$digest$apply 是用来触发 dirty-checking 的方法。前者强制触发一次 dirty-checking ,后者让一段代码执行完成后触发 dirty-checking 。但是 Angular 一次只允许一个 $digest 或者 $apply 运行。上面例子里的代码会挂,是因为 scope.btnClick 本身已经在 $apply 中执行了,但 someService.connect 内部通过 _setStatus 又调用了一次 $digest ,这就触发了两次。

这让我反思为什么要手动调用 $digest ?其实我的目的只是确保所有状态改变都触发 dirty-checking 。因为这个 service 中哪些代码不会触发 dirty-checking 是很明确的,那就是插件回调。所以直接在回调中使用 $apply 就可以解决问题。

修改后的代码如下:

// In service
start() {
  // Wrap code in $apply
  this.plugin.onStateA = () => { this._wrapStatusChange(STATUS.A) }
  this.plugin.onStateB = () => { this._wrapStatusChange(STATUS.B) }
}

connect() {
  // Change status directly
  this.status = STATUS.C
}

_wrapStatusChange(status) {
  this.$rootScope.$apply(() => {
    this.status = status
  })
}

总结

只在必要的时候使用 $apply 处理那些不会触发 dirty-checking 的代码。大部分的时候 $digest 都可以被 $apply 取代。

参考资料

$rootScope:inprog
Angular 对异常的描述。这种异常附带在线文档的方式还是很方便的。顺带一提 React 的异常信息也是这样。

$rootScope.Scope
Scope 的 API ,里面可以查到 $digest 和 $apply 的详细解释。

你可能感兴趣的:(angular.js,javascript)