PAYATHON 2026

Angular $apply 后视图未自动更新怎么办?

支付小周

结论

通过 ng-submit 或 ng-click 向 $scope.payments 执行 push() 时,AngularJS 通常会自动运行 digest。表格会随之更新,不需要手动调用 $apply()、$digest() 或 $watch()。

如果视图没有更新,往往是下面两种情况:

  1. controller、service 和 directive 引用的不是同一个数组;
  2. directive 使用了独立作用域,但没有接收 payments。

另外,$scope.$apply() 本身就会触发 $digest(),不要在它后面再次调用 $scope.$digest()。

保持同一个数组引用

下面的写法会替换数组引用:

var payments = [];

this.getPayments = function () {
  return payments;
};

this.newPayment = function (payment) {
  payments = payments.concat(payment);
};

controller 保存的是原来的数组,而 service 新增数据时,通过 concat() 创建并赋值了一个新数组。即使 digest 正常运行,controller 看到的仍然是旧数组。

可以改为直接修改原数组:

app.service('payment', function () {
  var payments = [];

  this.getPayments = function () {
    return payments;
  };

  this.addPayment = function (newPayment) {
    payments.push({
      id: newPayment.id,
      amount: newPayment.amount,
      cause: newPayment.cause
    });
  };
});

在 controller 中用包装函数调用 service:

app.controller('mainController', [
  '$scope',
  'payment',
  function ($scope, payment) {
    $scope.payments = payment.getPayments();
    $scope.formPayment = {};

    $scope.submitPayment = function () {
      payment.addPayment($scope.formPayment);

      $scope.formPayment = {};
    };
  }
]);

不要直接赋值:

$scope.newPayment = payment.newPayment;

如果 newPayment() 内部用到了 this,直接传递函数会丢失原来的调用上下文。用 controller 包一层更稳妥:

$scope.newPayment = function () {
  payment.newPayment($scope.formPayment);
};

通过 ng-submit 提交表单

<form ng-submit="submitPayment()">
  <label>
    Amount
    <input type="number" ng-model="formPayment.amount" required>
  </label>

  <label>
    Cause
    <input type="text" ng-model="formPayment.cause" required>
  </label>

  <button type="submit">Pay</button>
</form>

<p>payments: {{ payments.length }}</p>

<table>
  <thead>
    <tr>
      <th>Id</th>
      <th>Amount</th>
      <th>Cause</th>
    </tr>
  </thead>

  <tbody>
    <tr ng-repeat="item in payments track by item.id">
      <td>{{ item.id }}</td>
      <td>{{ item.amount }}</td>
      <td>{{ item.cause }}</td>
    </tr>
  </tbody>
</table>

ng-submit 的处理函数运行在 AngularJS 上下文中。函数执行完毕后,AngularJS 会自动检查 payments.length 和 ng-repeat,所以调用 push() 后,新行会立即显示。

如果 id 可能重复或暂时不存在,可以先去掉 track by item.id,也可以改成:

<tr ng-repeat="item in payments track by $index">

当数据有稳定且唯一的 id 时,优先使用 track by item.id。

检查 directive 的作用域

如果表格放在 directive 中,而且 directive 使用了独立作用域:

scope: {}

它无法直接访问父级 controller 中的 payments,需要显式接收这个数组:

app.directive('paymentTable', function () {
  return {
    restrict: 'E',
    scope: {
      payments: '<'
    },
    template: [
      '<table>',
      '  <thead>',
      '    <tr>',
      '      <th>Id</th>',
      '      <th>Amount</th>',
      '      <th>Cause</th>',
      '    </tr>',
      '  </thead>',
      '  <tbody>',
      '    <tr ng-repeat="item in payments track by item.id">',
      '      <td>{{ item.id }}</td>',
      '      <td>{{ item.amount }}</td>',
      '      <td>{{ item.cause }}</td>',
      '    </tr>',
      '  </tbody>',
      '</table>'
    ].join('')
  };
});

使用时传入 payments:

<payment-table payments="payments"></payment-table>

< 是 AngularJS 1.5 及以上版本常用的单向绑定。项目版本较早时,可以使用 =:

scope: {
  payments: '='
}

属性名也必须对应。如果 directive 内部读取的是 items,外部传入的却是 payments,就会出现 controller 中数组长度已经变化,但表格仍不更新的情况。

只有外部异步回调才需要接入 digest

原生 setTimeout()、第三方库回调和某些非 AngularJS 事件不在 AngularJS 的管理范围内。这种情况下可以使用 $timeout():

$timeout(function () {
  $scope.payments.push(newPayment);
}, 0);

也可以使用 $evalAsync():

externalApi.onPayment(function (newPayment) {
  $scope.$evalAsync(function () {
    $scope.payments.push(newPayment);
  });
});

确实需要 $apply() 时,把数据修改放进回调:

externalApi.onPayment(function (newPayment) {
  $scope.$apply(function () {
    $scope.payments.push(newPayment);
  });
});

不要这样调用:

$scope.$apply();
$scope.$digest();

$apply() 已经包含一次从根作用域开始的 digest。再次调用 $digest() 没有必要,还可能触发:

$digest already in progress

排查时应重点检查数组引用、service 方法的调用方式,以及 directive 的 scope 绑定。只要表单通过 ng-submit 提交,service 对同一个数组执行 push(),directive 也正确接收了 payments,表格就会自动显示新增数据。

备注:内容仅供参考。