以前写过一篇响应式原理-如何监听Array的变化,最近准备给团队同事分享,发现以前看的太粗糙了,所以决定再写一篇详细版~javascript
相信初学Vue
的同窗必定踩过这个坑,改变数组的索引,没有触发视图更新。 好比下面这个案例:html
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items[1] = 'x' // 不是响应性的
复制代码
以上案例摘抄Vue官方文档 - 数组更新检测。vue
Vue
官方文档也有给出,使用Vue.set
便可达到触发视图更新的效果。java
// Vue.set
Vue.set(vm.items, indexOfItem, newValue);
复制代码
Vue
官方给出了解释,不能检测。git
因为 JavaScript 的限制,Vue 不能检测如下数组的变更: 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
。github
那缘由是什么?我在学习的过程当中发现不少文章都在断章取义,Vue
官方给出了解释是【Vue
不能检测】,而不少文章写出的是【Object.defineProperty
不能检测】。segmentfault
但实际上Object.defineProperty
是能够检测到数组索引的变化的。以下案例:数组
let data = [1, 2];
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
console.log('我被读了,我要不要作点什么好?');
return val;
},
set: newVal => {
if (val === newVal) {
return;
}
val = newVal;
console.log("数据被改变了,我要渲染到页面上去!");
}
})
}
defineReactive(data, 0, 1);
console.log(data[0]);
data[0] = 5;
复制代码
你们能够本身在控制台中尝试一下,答案很是明显了。浏览器
Vue
只是没有使用这个方式去监听数组索引的变化,由于尤大认为性能消耗太大,因而在性能和用户体验之间作了取舍。 详细可见这边文章Vue为何不能检测数组变更。 bash
好了,终于揭开了谜底,为何Vue
为何不能检测数组变更,由于不作哈哈。
可是咱们开发者确定是有这个需求的,解决方式就是以下,使用Vue.set
。
// Vue.set
Vue.set(vm.items, indexOfItem, newValue);
复制代码
原理?很是明显,在初始的过程当中没有循环对全部数组索引监听,可是开发者须要监听哪一个索引。Vue.set
就帮你监听哪一个,核心仍是Object.defineProperty
。只是尽量的避免了无用的数组索引监听。
Object.defineProperty
虽然能检测索引的变化,但的确是监听不到数组的增长或删除的。能够阅读 Vue官方文档 - 对象变动检测注意事项 进行了解。
这个时候Vue
是怎么作的呢?
Vue
的解决方案,就是重写了数组的原型,更准确的表达是拦截了数组的原型。
首先选择了7个可以改变数组自身的几个方法。其次看下案例吧:
// 得到原型上的方法
const arrayProto = Array.prototype;
// 建立一个新对象,使用现有的对象来提供新建立的对象的__proto__
const arrayMethods = Object.create(arrayProto);
// 作一些拦截的操做
Object.defineProperty(arrayMethods, 'push', {
value(...args) {
console.log('用户传进来的参数', args);
// 真正的push 保证数据如用户指望
arrayProto.push.apply(this, args);
},
enumerable: true,
writable: true,
configurable: true,
});
let list = [1];
list.__proto__ = arrayMethods; // 重置原型
list.push(2, 3);
console.log('用户获得的list:', list);
复制代码
为何叫拦截,咱们在重写案例中的push
方法时,还须要使用真正的push
,这样才能保证数组如用户所指望的push
进去。
能够看到如下效果,咱们既能监听到用户传进来的参数,也就是监听到这个数组变化了,还能保证数组如用户所指望的push
进去。
为何使用arrayMethods
继承真正的原型,由于这样才不会污染全局的Array.prototype
,由于咱们要监听的数组只有vm.data
中的。
export class Observer {
constructor (value: any) {
// 若是是数组
if (Array.isArray(value)) {
// 若是原型上有__proto__属性, 主要是浏览器判断兼容
if (hasProto) {
// 直接覆盖响应式对象的原型
protoAugment(value, arrayMethods)
} else {
// 直接拷贝到对象的属性上,由于访问一个对象的方法时,先找他自身是否有,而后才去原型上找
copyAugment(value, arrayMethods, arrayKeys)
}
} else {
// 若是是对象
this.walk(value);
}
}
}
复制代码
以上能够看到Observer
对数组的特别处理。
咱们知道对象是在getter中收集依赖,setter中派发更新
。 那简单回忆下:
function defineReactive (obj, key, val) {
// 生成一个Dep实例
let dep = new Dep();
Object.defineProperty(obj, key, {
get: () => {
// 依赖收集
dep.depend();
},
set: () => {
// 派发更新
dep.notify();
},
})
}
复制代码
为了保证data
中每一个数据有着一对一的dep
,这里应用了闭包,保证每一个dep
实例不会被销毁。那么问题来了,dep
是一个局部变量呀~ 而监听数组变化,须要在数组拦截器中进行派发更新。那就访问不到这个dep
了,就没法知道具体要通知哪些Watcher
了!
那Vue是怎么作的呢?既然这个访问不到,那就再来一个dep
吧。
export class Observer {
constructor (value: any) {
this.value = value // data属性
this.dep = new Dep() // 挂载dep实例
// 为数据定义了一个 __ob__ 属性,这个属性的值就是当前 Observer 实例对象
def(value, '__ob__', this) // 把当前Observer实例挂在到data的__ob__上
}
}
复制代码
在Vue
初始化的过程当中,给data
中的每一个数据都挂载了当前的Observer
实例,又在这个实例上挂载了dep
。这样就能保证咱们在数组拦截器中访问到dep
了。以下:
Object.defineProperty(arrayMethods, 'push', {
value(...args) {
console.log('用户传进来的参数', args);
// 真正的push 保证数据如用户指望
arrayProto.push.apply(this, args);
// this指向当前这个数组,在初始化的时候被赋值__ob__
console.log(this.__ob__.dep)
},
enumerable: true,
writable: true,
configurable: true,
});
复制代码
如今咱们即可以在拦截器中执行dep.notify()
啦。
那如何收集依赖呢?
// 获取当前data上的 observe实例,也就是__ob__
let childOb = !shallow && observe(val);
function defineReactive (obj, key, val) {
// 生成一个Dep实例
let dep = new Dep();
Object.defineProperty(obj, key, {
get: () => {
if (Dep.target) {
// 依赖收集
dep.depend();
// 二次收集
if (childOb.dep) {
// 再收集一次依赖
childOb.dep.depend();
}
}
return val;
},
})
}
复制代码
如今要存放2个dep
,那天然是要在getter
中收集2次的,childOb
其实就是observe
中返回的__ob__
。不用在乎细节,自行查看源码就知道啦~
总结一下,针对数组在getter中收集依赖,在拦截器中触发更新
。
判断某个数组是否已Observer过,避免重复执行。
Vue.set
和Vue.del
,都是须要访问dep
的。
由于Object.defineProperty
不能检测数组的长度变化,例如:vm.items.length = newLength
。
var vm = new Vue({
data: {
items: ['a']
}
})
// 从新赋值,改变长度
vm.items = ['a, 'b', 'c']
复制代码
那vm.items = ['a, 'b', 'c']
这种状况,Vue
是如何监听的?这种状况其实监听的是对象vm
的items
属性,和数组实际上是不要紧的。由于以前发现有人误解,这里简单的提示一下~
本文主要仍是讲原理及思路,并不会涉及到不少代码,毕竟源码总会变。同时还要保证本身的js基础扎实,阅读源码才不会吃力哦~ 我就是很吃力的那种😭
若是你以为对你有帮助,就点个赞吧~
已完成:
Vue源码解读系列篇
Github博客 欢迎交流~