Angular $apply 后视图未自动更新怎么办?
结论
通过 ng-submit 或 ng-click 向 $scope.payments 执行 push() 时,AngularJS 通常会自动运行 digest。表格会随之更新,不需要手动调用 $apply()、$digest() 或 $watch()。
如果视图没有更新,往往是下面两种情况:
- controller、service 和 directive 引用的不是同一个数组;
- 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,表格就会自动显示新增数据。
备注:内容仅供参考。
