AngularJS数据绑定的工作原理

AngularJS数据绑定的工作原理

技术背景

在前端开发中,数据绑定是一种将数据模型与视图进行关联的机制,使得数据的变化能够自动反映在视图上,反之亦然。AngularJS作为一款经典的前端框架,提供了强大的数据绑定功能,其数据绑定的实现基于脏检查机制,与其他框架(如KnockoutJS和Backbone.js)采用的变化监听机制有所不同。

实现步骤

1. 定义监听器(Watchers)

AngularJS在$scope对象中维护一个简单的监听器数组$$watchers。每个监听器是一个对象,包含以下内容:

  • 一个被监听的表达式,可能是一个属性名或更复杂的表达式。
  • 表达式的最后已知值,用于与当前计算值进行比较。
  • 一个函数,当监听器检测到值变化时会被执行。

定义监听器的方式有多种:

  • 显式使用$watch方法监听$scope上的属性:
1
$scope.$watch('person.username', validateUnique);
JAVASCRIPT
  • 在模板中使用{{}}插值语法,会自动为当前$scope创建一个监听器:
1
<p>username: {{person.username}}</p>
HTML
  • 使用指令(如ng-model)定义监听器:
1
<input ng-model="person.username" />
HTML

2. 触发摘要循环(Digest Cycle)

当通过正常渠道(如ng-modelng-repeat等)与AngularJS交互时,指令会触发摘要循环。摘要循环是对$scope及其所有子$scope进行深度优先遍历。对于每个$scope对象,会遍历其$$watchers数组并计算所有表达式的值。如果新的表达式值与最后已知值不同,则调用监听器的函数。

3. 标记$scope为脏(Dirty)

如果某个监听器被触发,应用程序会知道有数据发生了变化,$scope会被标记为脏。监听器函数可能会更改$scope或其父$scope上的其他属性。由于AngularJS具有双向绑定,数据可以在$scope树中传递,因此可能会更改已经完成摘要处理的更高层级$scope上的值。

4. 重复摘要循环

只要摘要循环检测到有变化(即$scope为脏),就会再次执行整个摘要循环,直到所有$watch表达式的值与上一轮循环中的值相同(即摘要循环干净),或者达到摘要循环的最大次数(默认上限为10次)。如果达到最大次数,AngularJS会在控制台抛出错误:

1
10 $digest() iterations reached. Aborting!
BASIC

核心代码

$watch方法的粗略实现

1
2
3
4
5
6
7
8
Scope.prototype.$watch = function(watchFn, listenerFn) {
var watcher = {
watchFn: watchFn,
listenerFn: listenerFn || function() { },
last: initWatchVal // initWatchVal is typically undefined
};
this.$$watchers.push(watcher); // pushing the Watcher Object to Watchers
};
JAVASCRIPT

$digest方法的粗略实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Scope.prototype.$digest = function() {
var dirty;
do {
dirty = this.$$digestOnce();
} while (dirty);
};

Scope.prototype.$$digestOnce = function() {
var self = this;
var newValue, oldValue, dirty;
_.forEach(this.$$watchers, function(watcher) {
newValue = watcher.watchFn(self);
oldValue = watcher.last; // It just remembers the last value for dirty checking
if (newValue !== oldValue) { //Dirty checking of References
// For Deep checking the object, code of Value based checking of Object should be implemented here
watcher.last = newValue;
watcher.listenerFn(newValue,
(oldValue === initWatchVal ? newValue : oldValue),
self);
dirty = true;
}
});
return dirty;
};
JAVASCRIPT

$apply方法的粗略实现

1
2
3
4
5
6
7
Scope.prototype.$apply = function(expr) {
try {
return this.$eval(expr); //Evaluating code in the context of Scope
} finally {
this.$digest();
}
};
JAVASCRIPT

最佳实践

减少监听器数量

每个监听器在每次摘要循环中都会被评估,过多的监听器会导致性能下降。可以使用一次性绑定(One-Time Binding)来减少监听器数量,对于很少变化的数据,可以使用::语法进行一次性绑定:

1
<p>{{::person.username}}</p>
HTML

或者

1
<p ng-bind="::person.username"></p>
HTML

避免复杂的表达式

复杂的表达式在每次摘要循环中计算会消耗更多的时间,尽量保持表达式简单。

手动调用$apply

当在AngularJS上下文之外修改数据时(如使用setTimeoutXHR或第三方库),需要手动调用$apply方法来触发摘要循环,确保数据绑定更新:

1
2
3
4
5
setTimeout(function() {
$scope.$apply(function() {
// 修改$scope数据的代码
});
}, 1000);
JAVASCRIPT

常见问题

性能问题

如果创建了过多的监听器,摘要循环会变得很慢,尤其是在旧浏览器中。可以通过减少监听器数量、使用一次性绑定和避免复杂表达式来缓解性能问题。

摘要循环达到最大次数

如果摘要循环在10次迭代后仍未稳定,AngularJS会抛出错误。这通常是由于监听器函数不断更改$scope上的值,导致摘要循环无法收敛。需要检查监听器函数,确保不会无限循环地更改数据。

手动调用$apply的问题

如果在已经处于摘要循环中时手动调用$apply,会导致错误。可以使用$timeout来代替$apply,因为$timeout会自动处理摘要循环:

1
2
3
$timeout(function() {
// 修改$scope数据的代码
});
JAVASCRIPT

AngularJS数据绑定的工作原理
https://119291.xyz/posts/how-data-binding-works-in-angularjs/
作者
ww
发布于
2025年5月23日
许可协议