某人

此前素未谋面、此后遥遥无期

0%

angularjs1.x双向绑定

angularjs双向绑定

双向绑定:界面的操作能实时反映到数据,数据的改变能实时反映界面,如下图:文本框的操作

image

双向绑定实现

  • 脏值检测 例如AngularJS1.x
  • Getter/Setter:重定义model的getter与setter方法,在数据变动的时候重新渲染页面,例如Vue.js

使用Getter/Setter的时候,数据的每次修改都会激活刷新模版的方法,

脏值检测,可以在完成所有数值变动后,统一刷新到Dom,当监听元素变多的时候 watcher 列表会变得很长,查询变动的数据元素将耗费更多的资源。

脏检查

angular 必须去挨个检查一定作用域上(如 ,$scope)的这些元素对应绑定表达式的值是否有被改变。这就是脏数据检查的由来.

  • 脏检查是一种模型到视图的数据映射机制,由 $apply 或 $digest 触发。
  • 脏检查的范围是整个页面,不受区域或组件划分影响
  • 使用尽量简单的绑定表达式提升脏检查执行速度
  • 尽量减少页面上绑定表达式的个数(单次绑定和ng-if)
  • 给 ng-repeat 添加 track by 让 angular 复用已有元素

构造函数Scope

在angular框架中,缀$$表示这个变量被当作私有的来考虑,不应当在外部代码中调用

在写angularjs时到处都能遇到$scope,而$scope 就是 Scope 的实例

源码:angular@1.5.0:

image

在Scope的原型(Scope.prototype)中共定义了大概14个函数,如下图:

image
image

其中有3个函数起者至关重要的作用域(译的不准):

  • $watch(译:监听、监视)
  • $digest(译:迭代、循环、遍历)
  • $apply(译:传播、应用)

$watch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$watch: function(watchExp, listener, objectEquality, prettyPrintExpression) {
var get = $parse(watchExp);

if (get.$$watchDelegate) {
return get.$$watchDelegate(this, listener, objectEquality, get, watchExp);
}
var scope = this,
array = scope.$$watchers,
watcher = {
fn: listener,
last: initWatchVal,
get: get,
exp: prettyPrintExpression || watchExp,
eq: !!objectEquality
};

lastDirtyWatch = null;

if (!isFunction(listener)) {
watcher.fn = noop;
}

if (!array) {
array = scope.$$watchers = [];
}
// we use unshift since we use a while loop in $digest for speed.
// the while loop reads in reverse order.
array.unshift(watcher);
incrementWatchersCount(this, 1);

return function deregisterWatch() {
if (arrayRemove(array, watcher) >= 0) {
incrementWatchersCount(scope, -1);
}
lastDirtyWatch = null;
};
},

$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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
$digest: function() {
var watch, value, last, fn, get,
watchers,
length,
dirty, ttl = TTL,
next, current, target = this,
watchLog = [],
logIdx, logMsg, asyncTask;

beginPhase('$digest');
// Check for changes to browser url that happened in sync before the call to $digest
$browser.$$checkUrlChange();

if (this === $rootScope && applyAsyncId !== null) {
// If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
// cancel the scheduled $apply and flush the queue of expressions to be evaluated.
$browser.defer.cancel(applyAsyncId);
flushApplyAsync();
}

lastDirtyWatch = null;

do { // "while dirty" loop
dirty = false;
current = target;

while (asyncQueue.length) {
try {
asyncTask = asyncQueue.shift();
asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
} catch (e) {
$exceptionHandler(e);
}
lastDirtyWatch = null;
}

traverseScopesLoop:
do { // "traverse the scopes" loop
if ((watchers = current.$$watchers)) {
// process our watches
length = watchers.length;
while (length--) {
try {
watch = watchers[length];
// Most common watches are on primitives, in which case we can short
// circuit it with === operator, only when === fails do we use .equals
if (watch) {
get = watch.get;
if ((value = get(current)) !== (last = watch.last) &&
!(watch.eq
? equals(value, last)
: (typeof value === 'number' && typeof last === 'number'
&& isNaN(value) && isNaN(last)))) {
dirty = true;
lastDirtyWatch = watch;
watch.last = watch.eq ? copy(value, null) : value;
fn = watch.fn;
fn(value, ((last === initWatchVal) ? value : last), current);
if (ttl < 5) {
logIdx = 4 - ttl;
if (!watchLog[logIdx]) watchLog[logIdx] = [];
watchLog[logIdx].push({
msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp,
newVal: value,
oldVal: last
});
}
} else if (watch === lastDirtyWatch) {
// If the most recently dirty watcher is now clean, short circuit since the remaining watchers
// have already been tested.
dirty = false;
break traverseScopesLoop;
}
}
} catch (e) {
$exceptionHandler(e);
}
}
}

// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $broadcast
if (!(next = ((current.$$watchersCount && current.$$childHead) ||
(current !== target && current.$$nextSibling)))) {
while (current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while ((current = next));

// `break traverseScopesLoop;` takes us to here

if ((dirty || asyncQueue.length) && !(ttl--)) {
clearPhase();
throw $rootScopeMinErr('infdig',
'{0} $digest() iterations reached. Aborting!\n' +
'Watchers fired in the last 5 iterations: {1}',
TTL, watchLog);
}

} while (dirty || asyncQueue.length);

clearPhase();

while (postDigestQueue.length) {
try {
postDigestQueue.shift()();
} catch (e) {
$exceptionHandler(e);
}
}
},

$apply:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$apply: function(expr) {
try {
beginPhase('$apply');
try {
return this.$eval(expr);
} finally {
clearPhase();
}
} catch (e) {
$exceptionHandler(e);
} finally {
try {
$rootScope.$digest();
} catch (e) {
$exceptionHandler(e);
throw e;
}
}
},

脏检查的触发

angularjs 会在视图(view)或model(模型)发生变更的时候触发脏检查,即脏检查是$digest$apply的执行。

angularjs使用$watch函数为scope添加一个监视器。当这个scope中有变化发生时,监视器便会提醒你。
Scope中有一个对象数组$$watchers,里面保存着我们定义的所有的监视器对象watcher

$apply方法会触发 $digest,它将会在 $rootScope中运行digest循环,然后向下遍历每一个作用域并在每个作用域上运行循环.

angular中的一些所有方法都 scope.apply()里面,像ng−click http的回调函数等:

image

在这些情况下,我们不需要自己调用,实际上我们也不能自己调用,否则在apply()方法里面再调用apply()方法会抛出错误,仅当这个新的执行序列不是被angular JS的库的方法创建的,这个时候我们需要将代码用scope.apply()包起来,如下:

1
2
3
4
5
6
$scope.message ="等2000ms后更新";
setTimeout(function () {
  $scope.$apply(function () {
  $scope.message ="hello world";
});
}, 2000);

$digest 方法的执行即是遍历 scope 上绑定的所有 watcher,并执行相应的 watch(指定想要监控的对象) 和 listener(当数据改变时触发的回调) 方法。$watch这个函数的前两个,它指明了要观察什么(watchExp),在变化时要发生什么(listener)。第三个参数是否深度监听:

1
$watch(watchExpression, listener, objectEquality);

循环进行脏值检查(脏值)

digest循环运行时,它将会遍历所有的监听器然后再次循环,只要这次循环发现了”脏值”,循环就会继续下去。如果watchExp的值和最新的值不相同,那么这次循环就会被认为发现了“脏值”。

如果两个 watcher 之间互相影响彼此,则会导致无限循环的问题。把digest的运行控制在一个可接受的迭代数量内。如果这么多次之后,作用域还在变更,在这个点上,会抛出一个异常,因为不管作用域的状态变成怎样,它都不太可能是用户想要的结果。
迭代的最大值称为TTL(short for Time To Live)。这个值默认是10,可能有点小,但是记住这是一个性能敏感的地方,因为digest经常被执行,而且每个digest运行了所有的监听器。用户也不太可能创建10个以上链状的监听器。

最后

脏检查其实它像闪电般快,如果你在一个模版里有2000-3000个watch,它会开始变慢。

Angular 1.x 的机制下,脏检查的执行范围过大以及频率过频繁

相关链接

  1. AngularJS源码分析之
  2. Scopes and DirtyChecking
  3. 理解$watch ,$apply 和 $digest — 理解数据绑定过程
  4. AngularJS开发指南04:核心概览
  5. AngularJS开发指南05:指令
  6. AngularJS开发指南14:依赖注入
  7. Angular 学习笔记:$digest 实现原理
  8. Angular 1 深度解析:脏数据检查与 angular 性能优化
  9. AngularJS的脏检查深入分析