不看源码还不知道Vue的变化侦测居然还分Object和Array。很多地方还是不太理解,通过自己写一遍梳理下思路。
Array由于可以通过其原型上的方法来改变数组的内容,因此与Object不同,不会触发getter/setter。
1 如何追踪变化
既然数组可以用原型上的方法改变内容,那我们就可以对原型方法进行一些改造,来实现和Object一样的效果。
用一个拦截器覆盖原生的原型方法,之后,看似使用的是原型上的方法在操作数组,实质却是拦截器中的方法。
2 实现拦截器
既然要覆盖原型方法,则必然要先继承上面的方法,再对它进行改造。
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function(method){
// 缓存原始方法
const original = arrayProto[method];
Object.defineProperty(arrayMethods,method,{
value:function mutator(...args){
return original.apply(this,args)
},
enumerable:false,
writable:true,
configurable:true
})
})
当使用push方法时,实际调用的是arrayMethods.push,而arrayMethods.push是函数mutator,最后在mutator中执行original来进行push
3 使用拦截器覆盖Array原型
拦截器实现完了,就要用来覆盖Array原型,但不能直接覆盖,会污染全局的Array。
从上一章知道数据是通过Observer转换成响应式的,所以只需要在Observer中使用拦截器覆盖那些即将被转换成响应式Array类型数据的原型
export class Observer{
constructor(value){
this.value = value;
if(Array.isArray(value)){
value._proto_ = arrayMethods;//新增
}else{
this.walk.(value)
}
}
}
4 将拦截器方法挂载到数据属性上
不是所有浏览器都支持使用_proto_来访问原型,因此需要处理不能使用的情况,粗暴的做法------将arrayMethods设置到被侦测的数组上
// 判断_proto_是否可用
const hasProto = '_proto_' in {}
const arraykeys = Object.getOwnPropertyNames(arrayMethods)
export class Observer{
constructor(value){
this.value = value;
if(Array.isArray(value)){
const augment = hasProto
?protoAugment
:copyAugment
augment(value,arrayMethods,arrayKeys);
}else{
this.walk.(value)
}
}
function protoAugment(target,src,keys){
target._proto_ = src
}
function copyAugment(target,src,keys){
for(let i=0,l=keys.length;i<l;i++){
const key = keys[i];
def(target,key,src[key])
}
}
}
如果浏览器支持_proto_,则使用protoAugment函数来覆盖原型,如果不支持,则调用copyAugment。
5 如何收集依赖
在getter中收集依赖,在拦截器中触发依赖
function defineReactive(data,key,val){
if(typeof val==='object'){
new Observer(val)
}
let dep = new Dep();
Object.defineProperty(data,key,{
enumerable:true,
get:function(){
dep.depend()
//这里收集Array的依赖
return val
}
set:function(newVal){
if(val==newVal){
return;
}
val = newVal;
dep.notify();
})
}
6 依赖存放在哪里
为了满足在getter和拦截器中都可以访问到依赖,因此把依赖保存在Observer实例上
export class Observer{
constructor(value){
this.value = value;
//新增dep
this.dep = new Dep();
if(Array.isArray(value)){
const augment = hasProto
?protoAugment
:copyAugment
augment(value,arrayMethods,arrayKeys);
}else{
this.walk.(value)
}
}
.......
}
7 收集依赖
新增一个函数observe,它尝试创建一个Observer实例。
function defineReactive(data,key,val){
let childOb = observer(val); //修改
let dep = new Dep();
Object.defineProperty(data,key,{
enumerable:true,
get:function(){
dep.depend();
// 新增
if(childOb){
childOb.dep.depend()
}
return val
}
set:function(newVal){
if(val==newVal){
return;
}
dep.notify();
val = newVal;
}
})
}
<!-- 尝试为value创建一个Observer实例,
如果创建成功,直接返回新创建的Observer实例。
如果value已经存在一个Observer实例,则直接返回它 -->
export function observer(value,asRootData){
if(!isObject(value)){
return
}
let ob;
if(hasOwn(value,'_ob_')&&value._ob_instanceof Observer){
ob = value._ob_
}else{
ob = new Observer(value);
}
return ob;
}
8 在拦截器中获取Observer实例
dep保存在Observer中,所以需要在拦截器中的this上读取Observer的实例
<!-- 工具函数 -->
function def(obj,key,val,enumerable){
Object.defineProperty(obj,key,{
value:val,
enumerable:!!enumerable,
writable:true,
configurable:true
})
}
export class Observer{
constructor(value){
this.value = value;
this.dep = new Dep();
<!-- 新增 -->
def(value,'_ob_',this)
}
if(Array.isArray(value)){
const augment = hasProto
?protoAugment
:copyAugment
augment(value,arrayMethods,arrayKeys);
}else{
this.walk.(value)
}
....
}
_ob_属性的值即当前Observer的实例
9 向数组的依赖发送通知
在Observer实例中拿到dep属性,调用ob.dep.notify()去通知依赖(Watcher)数据发生了改变
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function(method){
// 缓存原始方法
const original = arrayProto[method];
def(arrayMethods,method,function mutator(...args){
const ob = this._ob_;
ob.dep.notify() //向依赖发送通知
return original.apply(this,args)
}
}
总结:
Vue无法侦测到数组的以下变化:
- 直接通过索引设置某个值 this.list[0] = 2
- 修改数组的长度 this.list.length = 0
这是因为数组的变化侦测是通过其原型上的方法来实现的,包括push、pop、shift、unshift、splice、sort、reverse,只能侦测通过这七种方法而造成的数据变化。
解决方法:
- 使用vue.$set(),手动通知vue完成观察(推荐这种方法)
- 将这两种变动换成原型上的七种方法实现