写在前面
现在我们有两种策略来确定值是否改变,根据值或者根据引用,我们在 $watch 传递一个布尔值来分辨,现在我们引入第三种完全独立的函数来注册一个 watcher - $watchCollection。这个函数可以监听对数组和对象的添加和删除以及修改,之前我们使用的 deepWatch 会在 digest 中监听对象的任意层级,并且会进行深拷贝。但是 $watchCollection 只会监听对象的浅层属性,这样会更加节省内存和加快速度。
什么是 collection
在修改里面元素不影响外部这个元素,这个外部元素就叫 collection
函数雏形
首先要明白这个函数主要是为了节省监听对象和数组的时候每次执行 digestOnce 都会进行深度遍历的时间,所以我们对传入的 watchFn 和 listenerFn 进行改造。
Scope.prototype.$watchCollection = function (watchFn, listenerFn) {const internalWatchFn = function (scope) {};const internalListenerFn = function () {};return this.$watch(internalWatchFn, internalListenerFn);};
非集合类型
$watchCollection 的目的是为了监听对象和数组,但是当 $watchCollection 里的 watchFn 返回一个值的时候我们也需要让程序可以运行。
Scope.prototype.$watchCollection = function (watchFn, listenerFn) {let newValue,oldValue,changeCount = 0;const self = this;const internalWatchFn = function (scope) {newValue = watchFn(scope);if (!self.$$areEqual(newValue, oldValue, false)) {changeCount++;}oldValue = newValue;return changeCount;};const internalListenerFn = function () {listenerFn(newValue, oldValue, self);};return this.$watch(internalWatchFn, internalListenerFn);};
将 newValue 和 oldValue 直接写在外面,让两个函数都可以直接引用到。
internalWatchFn 函数可以用 changeCount 代替了原来 watchFn 返回的值,从而代替原本 $$digestOnce 函数中对引用类型的判断,这样的话如果被监听的值为引用类型的话返回的就会变成基本类型就省去了拷贝过程。
由于我们返回的是 changeCount 所以 internalListenerFn 函数接受的参数不再有意义我们直接用$watchCollection 函数内部的值。
集合分类
Scope.prototype.$watchCollection = function (watchFn, listenerFn) {const self = this;let newValue,oldValue,changeCount = 0;const internalWatchFn = function (scope) {newValue = watchFn(scope);+ if (_.isObject(newValue)) {+ if (_.isArray(newValue)) {+ } else {++ }+ } else {+ if (!self.$$areEqual(newValue, oldValue, false)) {+ changeCount++;+ }+ oldValue = newValue;+ }return changeCount;};const internalListenerFn = function () {listenerFn(newValue, oldValue, self);};return this.$watch(internalWatchFn, internalListenerFn);};
oldValue = newValue 只有在基本类型的时候才会执行,为引用类型时要进行一次简单拷贝代替掉深拷贝。
if (_.isArray(newValue)) {if(!_.isArray(oldValue)) {changeCount++;oldValue = [];}}
如果新值为数组,但是老值不为数组就会将 changeCount 加 1 ,从而 $digest 中会调用 internalListenerFn 函数
检测到数组个数的变化
if (_.isArray(newValue)) {if(!_.isArray(oldValue)) {changeCount++;oldValue = [];}if (newValue.length !== oldValue.length) {changeCount++;oldValue.length = newValue.length;}}
检测数组元素的变化
if (_.isArray(newValue)) {if(!_.isArray(oldValue)) {changeCount++;oldValue = [];}if (newValue.length !== oldValue.length) {changeCount++;oldValue.length = newValue.length;}_.forEach(newValue, function(newItem, i) {const bothNaN = _.isNaN(newItem) && _.isNaN(oldValue[i]);if (!bothNaN && newItem !== oldValue[i]) {changeCount++;oldValue[i] = newItem;}});}
对数组处理完之后将 oldValue 将会和 newValue 一样,相当于进行一次浅拷贝
类数组
满足以下三个条件,即可认为类数组。
- 不为 null 和 undefind。
- 是对象。
- 不为 function ,因为函数有length属性,表示形参个数。
- 拥有 length 属性,且值合法。
函数的 arguments 和使用原生 dom api 取到的 domList 都是类数组,但是他们 isArray 返回的是 false,需要我们创建一个函数来兼容 isArray 使它能判断参数是类数组。
function isArrayLike(obj) {if (_.isNull(obj) || _.isUndefined(obj)) {return false;}if (typeof obj === 'function') {return false}const length = obj.length;return _.isNumber(length);}
isArrayLike 替换掉 isArray 即可,但是字符串类型会通过测试,但是它通过不了 isObject 的检测,由于 string 是 immutable 类型无法改变它的内容把它当成 collection 来测没有什么意义。
为什么不跳出流程
为什么不在 changeCount 变化后立即跳出流程?因为接下来的流程刚好可以将 newValue 浅拷贝为 oldValue,性能算下来差不多
检测对象
if (!_.isObject(oldValue) || isArrayLike(oldValue)) {changeCount++;oldValue = {};}_.forOwn(newValue, function(newVal, key) {const bothNaN = _.isNaN(newVal) && _.isNaN(oldValue[key]);if (bothNaN && oldValue[key] !== newVal) {changeCount++;oldValue[key] = newVal;}});_.forOwn(oldValue, function(oldVal, key) {if (!newValue.hasOwnProperty(key)) {changeCount++;delete oldValue[key];}});
- oldValue 由不是对象或者是类数组转换为对象
- 对新对象进行遍历,可以检测到对象值的改变和增加,会在遍历中赋值给 oldValue
- 对旧对象进行遍历,可以检测到对象值的删除,然后 oldValue 删除对应属性
减少循环次数
```jsx Scope.prototype.$watchCollection = function (watchFn, listenerFn) { const self = this; let newValue,oldValue,
oldLength, changeCount = 0;
const internalWatchFn = function (scope) {
- let newLength; newValue = watchFn(scope);
if (_.isObject(newValue)) {
if (isArrayLike(newValue)) {/* 类数组和数组 */} else {if (!_.isObject(oldValue) || isArrayLike(oldValue)) {changeCount++;oldValue = {};oldLength = 0;}newLength = 0;_.forOwn(newValue, function (newVal, key) {newLength++;if (oldValue.hasOwnProperty(key)) {const bothNaN = _.isNaN(newVal) && _.isNaN(oldValue[key]);if (!bothNaN && oldValue[key] !== newVal) {changeCount++;oldValue[key] = newVal;}} else {changeCount++;oldLength++;oldValue[key] = newVal;}});if (oldLength > newLength) {changeCount++;_.forOwn(oldValue, function (oldVal, key) {if (!newValue.hasOwnProperty(key)) {oldLength--;delete oldValue[key];}});}}
} else { / 基本类型 / } return changeCount; };
const internalListenerFn = function () { listenerFn(newValue, oldValue, self); };
return this.$watch(internalWatchFn, internalListenerFn); }; ```
- 第一次 digest 的时候 oldValue 为 undefined,所以赋值 oldValue = {},oldLength = 0,newLength = 0
- 之后会对 newValue 进行遍历,遍历结束后 newLength 就是 newValue key 的个数,每次重新执行 digestOnce 时 newLength 会重新设置为
来判断之前不为对象的值转化为对象,而且第一次 digestOnce 调用 watchFn 时 oldValue 为 undefined,会做初始化。if (!_.isObject(oldValue) || isArrayLike(oldValue)) {changeCount++;oldValue = {};oldLength = 0;}
第一次 digestOnce 之后由于 oldValue 为 {},所以 oldValue.hasOwnProperty(key) 都为 false 遍历完之后 newLength 就是最新的对象 key 的个数,oldLength 就是没有经过增删改查的对象 key 的个数。_.forOwn(newValue, function (newVal, key) {newLength++;if (oldValue.hasOwnProperty(key)) {const bothNaN = _.isNaN(newVal) && _.isNaN(oldValue[key]);if (!bothNaN && oldValue[key] !== newVal) {changeCount++;oldValue[key] = newVal;}} else {changeCount++;oldLength++;oldValue[key] = newVal;}});
第二次之后,oldValue.hasOwnProperty(key) 如果为 true 且 !bothNaN && oldValue[key] !== newVal 那么说明对象的属性值被修改,oldValue.hasOwnProperty(key) 如果为 false 说明对象增加来新的属性,当增加新属性的时候,oldLength 会加 1,这是为了下一步检测对象是否被删除来属性同步 oldLength 和 newLength。
if (oldLength > newLength) {changeCount++;_.forOwn(oldValue, function (oldVal, key) {if (!newValue.hasOwnProperty(key)) {oldLength--;delete oldValue[key];}});}
第一次 digestOnce 之后 oldLength 和 newLength 值相同,并且添加新属性的时候两个值会同时增加,所以这样我们可以检测出是否被删减了对象属性。
含有 length 属性的对象
我们的 isArrayLike 函数需要兼容含有 length 属性的对象
function isArrayLike(obj) {if (_.isNull(obj) || _.isUndefined(obj)) {return false;}if (typeof obj === 'function') {return false}const length = obj.length;+ return length === 0 ||+ (_.isNumber(length) && length > 0 && (length - 1) in obj);}
如果 length 为 42 ,那么必须有值为 41 的属性,但是只对 length 大于零的对象有限,我们可以适当加宽条件,这不是非常安全的检测,但是比较实用。
veryOldValue
const internalListenerFn = function () {if (firstRun) {listenerFn(newValue, newValue, self);firstRun = false;} else {listenerFn(newValue, veryOldValue, self);}if (trackVeryOldValue) {veryOldValue = _.clone(newValue);}};
小结
第三种监听方式,节省了 deepWatch 所消耗的时间,以及 deepclone 所消耗的内存
