scopes的作用
- 在 controller,directive 和 template 之间共享 scopes 中的数据。
- 不同的 scopes 之间传递数据。
- 广播和监听事件。
- 监听数据变化。
在 AngularJS 中,引入了数据的脏检查来进行上面的第四点的实现。
Scope 上的数据
const scope = new Scope();scope.aProperty = 1;
这就是 Scope 上的数据如何工作的,只是普通的 js 对象上的属性,没有特殊的 setter 没有特殊的限制,Scope 发挥作用的秘诀在于 $watch 和 $digest 。
$watch 和 $digest
他们两个组成了一个循环,响应数据的变化,使用$watch,来创建一个叫做 watcher 的东西。watcher 可以监听 scope 上数据的变化,$watch 应该接受两个函数,
- watch 函数, 用来监听数据
- listener 函数,当被监听的数据变化时调用
$digest 函数,他会遍历 scope 上所有的 watcher ,然后找到变化了的数据来顺序运行他们的 watcher 和 listener。
'use strict';function Scope() {this.$$watcher = [];}module.exports = Scope;
我们需要一个东西来存放创建的 watcher ,两个 $ 代表着这个属性是框架内部使用的属性,不应该被应用程序所使用。
Scope.prototype.$watch = function(watchFn, listenerFn) {const watcher = {watchFn: watchFn,listenerFn: listenerFn};this.$$watchers.push(watcher);};Scope.prototype.$digest = function() {this.$$watchers.forEach(watcher => {watcher.listenerFn();});};
watcher 的作用
watcher 由 $watch 创建,现阶段我们需要手动给 scope 中的每个属性调用 $watch 编写 watchFn,listenerFn 来创建 watcher,在 angularjs 中这个阶段是在 scope 赋值的时候就已经完成。
- watcher 负责监听数据变化
- watcher 负责响应数据变化
$watch 的作用
给每个 scope 上的数据创建 watcher,关于 watch 的哲学含义,表示 angularjs 盯上你了,没有好果子吃。$digest 的作用
$digest 函数,他会遍历 scope 上所有的 watcher ,然后找到变化了的数据来顺序运行他们的 watcher 和 listener,所以 $digest 根据 watcher 的 watchFn 找到变化数据对应的 watcher,来执行 watcher 的 listenerFn。Dirty Checking
我们现在先对 scope 上的一个数据来编写 watcher
每个 watcher 应该对应 scope 上的一个数据,所以我们需要从 scope 上拿到对应的数据传给 watcher,当数据变化时调用 listenerFn ,最简单的就是之间把 scope 对象传入 watchFn。
function watchFn(scope) {return scope.firstName;}
$watcher 创建的 watcher 中的 watch 函数,应该返回变化了的数据,通常这些数据在 scope 上存在,我们可以将 scope 作为参数传入 watch 函数,这样 watch 可以轻易地拿到了数据。最简单的实现就是:
Scope.prototype.$digest = function() {this.$$watchers.forEach(watcher => {watcher.watchFn(this);watcher.listenerFn();});};
$digest 的作用是调用 watch 函数,比较返回的值和上次返回的值是否一致,不一致这个 watcher 被标记为脏,调用 listener 函数。
Scope.prototype.$digest = function () {var self = this;var newValue, oldValue;this.$$watchers.forEach(function (watcher) {newValue = watcher.watchFn(self);oldValue = watcher.last;if (newValue !== oldValue) {watcher.last = newValue;watcher.listenerFn(newValue, oldValue, self);}});};
last 第一次为 undefined,这样的话如果我们传入的值为 undefined 时第一次 $digest 不会被调用,我们希望第一次总会触发数据的 listenerFn。
function Scope() {this.$$watchers = [];}function initWatchVal() {}Scope.prototype.$watch = function (watchFn, listenerFn) {const watcher = {watchFn: watchFn,listenerFn: listenerFn,last: initWatchVal};this.$$watchers.push(watcher);};
这样的就实现了我们的目的,并且让 $digest 让 listenerFn 在第一次时以新值执行
Scope.prototype.$digest = function () {this.$$watchers.forEach((watcher) => {let newValue, oldValue;newValue = watcher.watchFn(this);oldValue = watcher.last;if (newValue !== oldValue) {watcher.last = newValue;watcher.listenerFn(newValue,(oldValue === initWatchVal ? newValue : oldValue),this);}})}
数据和 watcher
- 绑定 watcher 而不是绑定数据,只有数据没有 watcher 没有作用
- angularjs 只是在遍历 watcher
- 每个 watcher 是独立的,包含了对应数据如何是否变化和怎么更新
- watchFn 只会在 $digest 中被调用
脏检查
如果 scope 上的一个数据被改变后,有可能在它的 listenerFn 改变 scope 上的其他数据,如果已经遍历过那个 watcher 那么就会跳过这次更改。
给 scope 上的 name 和 nameUpper 创建 watcher,initial 和 nameUpper 依赖于 name 属性,如果我们调换次序程序刚好可以通过,但是 watcher 之间的依赖顺序不取决于 digest 顺序。 ```jsx Scope.prototype.$digest = function () { let dirty; do {it('triggers chained watchers in the same digest', function() {const scope = new Scope();scope.name = 'Jane';scope.$watch(function(scope) { return scope.nameUpper; },function(newValue, oldValue, scope) {if (newValue) {scope.initial = newValue.substring(0, 1) + '.';}})scope.$watch(function(scope) { return scope.name; },function(newValue, oldValue, scope) {if (newValue) {scope.nameUpper = newValue.toUpperCase();}});scope.$digest();expect(scope.initial).to.equals('J.');scope.name = 'Bob';scope.$digest();expect(scope.initial).to.equals('B.');})
} while (dirty); }dirty = this.$$digestOnce();
Scope.prototype.$$digestOnce = function () { let newValue, oldValue, dirty; this.$$watchers.forEach((watcher) => { newValue = watcher.watchFn(this); oldValue = watcher.last; if (newValue !== oldValue) { watcher.last = newValue; watcher.listenerFn(newValue, (oldValue === initWatchVal ? newValue : oldValue), this); dirty = true; } }) return dirty; }
只要有数据变化就**将整个 scope 标记为 dirty **,就执行一遍所有的 watcher ,直到所有的所有的数据都没有发生改变为止,所以在一次 digest 中每个 watcher 也许会执行不止一次,所以我们说 listenerFn 要幂等的,没有副作用,或者副作用执行有限次。<a name="tPX3A"></a>## TTL-“Time To Live”```jsxScope.prototype.$digest = function () {let ttl = 10;let dirty;do {dirty = this.$$digestOnce();if (dirty && !(ttl--)) {throw ('10 digest iterations reached');}} while (dirty);}
减少 $$watcher 遍历次数
it('ends the digest when the last watch is clean', function () {const scope = new Scope();scope.array = _.range(100);let watchExecutions = 0;_.times(100, function (i) {scope.$watch(function (scope) {watchExecutions++;return scope.array[i];},function (newValue, oldValue, scope) {});});scope.$digest();expect(watchExecutions).to.equals(200);scope.array[0] = 420;scope.$digest();expect(watchExecutions).to.equals(301);})
如果一个数据变化了,那么将会遍历 100 个 watcher ,并且标记 scope 为脏,第二次遍历时,只要我们找到上次最后为脏的 watcher,那么是等同于遍历整个数组的,建立 100 个 watcher,第一次 digest 执行 200 次,第二次我们希望它执行 301 次而不是 400 次。
每次数据变化调用 digest 会触发两次 digestOnce ,第一次不能优化始终会遍历完整个 watchers ,我们要优化的是第二轮 digestOnce ,每次 digest 开始会将 lastDirtyWatch 清空,假设我们改变了第 10 个数据,在数据改变后执行一轮 digest ,就标记第 10 个 watcher 为脏 watcher 同时标记这个 scope 为脏,如果这个 watcher 的 listnerFn 改变了第 10 个之前的数据,那么第二轮 digest 自然会遍历对应的 watcher,如果这个 watcher 的 listnerFn 改变了第 10 个之后的数据,那么第一轮 digest 还会继续遍历并且更新 $$lastDirtyWatch 所以不用担心。
Scope.prototype.$$digestOnce = function() {const self = this;let newValue, oldValue, dirty;_.forEach(this.$$watchers, function(watcher) {newValue = watcher.watchFn(self);oldValue = watcher.last;if (newValue !== oldValue) {self.$$lastDirtyWatch = watcher;watcher.last = newValue;watcher.listenerFn(newValue,(oldValue === initWatchVal ? newValue : oldValue),self);dirty = true;} else if (self.$$lastDirtyWatch === watcher) {return false;}})return dirty;}
使用 lodash 的 forEach ,这样 return 的时候会跳出循环。
引用类型数据
Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) {if (valueEq) {return _.isEqual(newValue, oldValue);} else {return newValue === oldValue;}};
angularjs 内部有自己的比较函数,我们用 lodash 的即可,而且为了察觉到旧值的变化,我们也需要进行一次深拷贝,否则两个值会同时变化,同样地 angularjs 也有自己的深拷贝函数,我们也用 lodash 的。
watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue);
NaN
如果我们不处理 NaN 那么 scope 永远为脏,我们可以在 $$areEqual 中处理
Scope.prototype.$$areEqual = function(newValue, oldValue, valueEq) {if (valueEq) {return _.isEqual(newValue, oldValue);} else {return newValue === oldValue ||(typeof newValue === 'number' && typeof oldValue === 'number' &&isNaN(newValue) && isNaN(oldValue));}};
错误处理
现在的代码出错后会停下,实际上代码应该继续向下走,angularjs 有 $exceptionHandler 来处理错误,我们简单处理即可,出错的地方无非就是 watchFn 和 listenerFn,让其中一个 watcher 出错剩下的 watcher 继续遍历即可。
Scope.prototype.$$digestOnce = function() {const self = this;let newValue, oldValue, dirty;_.forEach(this.$$watchers, function(watcher) {try {newValue = watcher.watchFn(self);oldValue = watcher.last;if (!self.$$areEqual(newValue, oldValue, watcher.valueEq)) {self.$$lastDirtyWatch = watcher;watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue);watcher.listenerFn(newValue,(oldValue === initWatchVal ? newValue : oldValue),self);dirty = true;} else if (self.$$lastDirtyWatch === watcher) {return false;}} catch (err) {console.log(err);}})return dirty;}
销毁本身的 watcher
只要 scope 存在,watcher 一般是不会销毁的,但是如果有需求在 scope 存在时销毁一个 watcher 怎么办,在angularjs 中非常巧妙的实现了,就是利用 $watch 来返回一个函数来控制相应 watcher。
it('allows destroying a $watch during digest', function () {const scope = new Scope();scope.aValue = 'abc';const watchCalls = [];scope.$watch(function (scope) {watchCalls.push('first');return scope.aValue;});const destroyWatch = scope.$watch(function (scope) {watchCalls.push('second');destroyWatch();});scope.$watch(function (scope) {watchCalls.push('third');return scope.aValue;});scope.$digest();console.log(watchCalls);expect(watchCalls).to.equals(['first', 'second', 'third', 'first', 'third']);});
[‘first’, ‘second’, ‘first’, ‘third’, ‘first’, ‘third’] 不做修改的话会得到这样一个数组,由于第一次 digest 就删除了第二个 watcher,导致指针偏移,所以第一轮的第三个 watcher 不会被遍历到,如果有第四个 watcher 就会遍历执行第四个 watcher ,解决办法很简单,将每次新的 watcher 放在起点,并且从反方向遍历数组,这样的话不会影响剩下的数组
for(let i = arr.length-1;i >= 0;i--) {} //arr.length初始化后就不会改变
[1,2,3,4] i = length-1(3)[1,2,4] i = 3 - 1(2) 遍历并删掉第三个元素[1,2,4] i = 3 -2(1)
因为 splice 数组总是会移动后面的元素,所以当 watcher 被移除,被 shift 的是已经被处理的部分,而之前的被 shift 的是没有被处理的部分。
在销毁另一个 watcher
it('allows a $watch to destroy another during digest', function () {const scope = new Scope();scope.aValue = 'abc';scope.counter = 0;scope.$watch(function (scope) {return scope.aValue;},function (newValue, oldValue, scope) {destroyWatch();});const destroyWatch = scope.$watch(function (scope) {},function (newValue, oldValue, scope) {});scope.$watch(function (scope) {return scope.aValue;},function (newValue, oldValue, scope) {scope.counter++;});scope.$digest();expect(scope.counter).to.equals(1);});
第一轮遍历,会执行第一个 watcher 的 listenerFn 销毁第二个 watcher ,[3,2,1],[3,1],第一个 watcher 往下移,导致 lastDirtyWatcher 还是它所以提前结束了,第三个永远没有执行,只需要在每次 splice 都将 lastDirtyWatcher 清零即可。
销毁所有 watcher
it('allows destroying several $watches during digest', function () {const scope = new Scope();scope.aValue = 'abc';scope.counter = 0;const destroyWatch1 = scope.$watch(function (scope) {destroyWatch1();destroyWatch2();});const destroyWatch2 = scope.$watch(function (scope) {return scope.aValue;},function (newValue, oldValue, scope) {scope.counter++;});scope.$digest();expect(scope.counter).toBe(0);});
在第一次 watcher 的时候就把 $watch 注册的所以 watcher 删掉,自然不会执行其他 watcher,但是循环会继续遍历我们也不希望它报错,所以:
Scope.prototype.$$digestOnce = function () {const self = this;let newValue, oldValue, dirty;_.forEachRight(this.$$watchers, function (watcher) {try {if (watcher) {newValue = watcher.watchFn(self);oldValue = watcher.last;if (!self.$$areEqual(newValue, oldValue, watcher.valueEq)) {self.$$lastDirtyWatch = watcher;watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue);watcher.listenerFn(newValue,(oldValue === initWatchVal ? newValue : oldValue),self);dirty = true;} else if (self.$$lastDirtyWatch === watcher) {return false;}}} catch (err) {console.log(err);}})return dirty;}
小结
- $watch 的哲学含义:观测
- 脏检查:检查到 scope 不为脏为止
- TTL 和 最后脏 watcher 来减少遍历 watcher 个数
- 销毁 watcher 只是简单的 splice,但是引发的改变 $$watchers 的 shift 副作用会比较难处理
