Vue2.x 不能检测以下数组的变动:1.直接修改数组长度;2.通过数组下标赋值。第一点主要是收到defineProperty
的限制,第二点要究其原因,还是得从源码分析。
直接修改数组长度
Vue2.x 利用Object.defineProperty
劫持对象的访问器,在属性值发生变化时我们可以获取变化,实现对数据变化的双向绑定。但是Object.defineProperty
并不能监控数组长度的变化,因为数组对象中的 length 属性的 configurable
为 false
,不允许访问器对该属性进行操作
关于数据双向绑定,可以参考基于 defineProperty 和 proxy 实现数据双向绑定
通过数组下标赋值
vue 怎么监听数组
Observer,是实现 vue 响应式的核心模块,用于监听对象和依赖追踪
// Observer 监听对象
var Observer = function Observer(value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, "__ob__", this);
if (Array.isArray(value)) {
// 将重写的数组方法挂载到数组原型对象上
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
可以看到,observer 遇到数组会走单独的处理逻辑,首先将重写的数组方法挂到数组原型方法上,然后进一步通过observeArray
处理数组
// observeArray
Observer.prototype.observeArray = function observeArray(items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
// 如果items[i]是数组,会新建 Observer 对象,然后继续执行observe, 直到遍历完毕
}
};
// observe
function observe(value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
// 如果要观测的数据不是一个对象或者是 VNode 实例,则不做监听
return;
}
var ob;
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
// 已经添加过observer实例,直接返回
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob;
}
可以看到,vue 并没有对数组的所有元素进行监听,所以通过数组下标修改或增加元素,会发现依赖该数组元素的视图并不会更新
重写数组方法
Vue 2.x 针对数组的部分原型方法做了特殊处理,通过调用这些重写的方法,可以对新增的元素的索引进行监听,确保新增元素是响应式的
var methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
methodsToPatch.forEach(function (method) {
var original = arrayProto[method];
def(arrayMethods, method, function mutator() {
var args = [],
len = arguments.length;
while (len--) args[len] = arguments[len];
var result = original.apply(this, args);
var ob = this.__ob__;
var inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) {
ob.observeArray(inserted);
}
// 重新触发依赖更新
ob.dep.notify();
return result;
});
});
通过源码可以看到,在操作数组时,如果使用 push、unshift、splice 添加或修改元素,会重新触发依赖的更新函数,渲染对应视图或对象
Vue.set
除了上述方法,我们还可以通过Vue.set
来实现对数组的更新,并渲染对应视图。贴一波 set 函数源码,源码分析可以看下注释
function set (target, key, val) {
......
if (Array.isArray(target) && isValidArrayIndex(key)) { // 处理数组元素
target.length = Math.max(target.length, key)
// 使用重写过的splice更新数组
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
// 是对象原有属性,赋值即可触发更新函数
target[key] = val
return val
}
const ob = target.__ob__
// __ob__指向target的observer实例
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
// 对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性
// 所以会直接返回值
return val
}
// target非响应式对象,不用对属性监听,直接赋值并返回
if (!ob) {
target[key] = val
return val
}
// 给新增的对象属性添加依赖
defineReactive(ob.value, key, val)
// 触发依赖,重新渲染页面
ob.dep.notify()
return val
}