变化侦测分为两种类型,一种是“”推,另一种是“拉”。
Angular和react的变化侦测都属于“拉”,也就是说,当状态发生变化时,它不知道哪个状态发生变化了,只知道状态有可能改变了,然后就会发送一个信号告诉框架,框架收到信号后,就会进行一个暴力对比找到哪些DOM需要重新渲染的。这也是Angular的脏检查(基于zoom.js,利用$digest函数触发)的过程,react用的是虚拟DOM。
Vue的变化侦测属于“推”。当状态发生变化,Vue在一定程度上能马上知道哪些状态发生改变,具有更细粒度的更新;也因为粒度越细,每个状态绑定的依赖就越多,依赖追踪在内存中的开销就越大,因此,Vue也引入了虚拟DOM的概念,将一个状态的细粒度绑定到组件(Vue的另一核心:单文件组件化),这样子当状态发生改变,就会通知到组件,组件内部再使用虚拟DOM进行对比更新。
众所周知,Vue2.x的版本使用Object.defineProperty,Vue3.x使用ES6的Proxy来进行变化侦测的。下面主要讲Object和Array的变化侦测:
(注:以下代码块来自Vue2.6.10源码)
一、Object的变化侦测
1.Object通过Object.defineProperty将属性转换成getter/setter的形式来追踪变化,在getter中收集依赖,在setter中触发依赖。
第一版Object.defineProperty:
function defineReactive$$1 (obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
return val
},
set: function reactiveSetter (newVal) {
if(val === newVal){
return
}
val = newVal;
}
});
}
2.在getter中收集依赖,依赖被存储在Dep中,在Dep中对依赖进行添加,删除以及更新等操作。
Dep的封装:
var uid = 0;
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
Dep.prototype.removeSub = function removeSub (sub) {
remove(this.subs, sub);
};
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null;
var targetStack = [];
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget () {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
function remove (arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
}
}
第二版Object.defineProperty:
function defineReactive$$1 (obj,key,val) {
var dep = new Dep();//新增
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
if (Dep.target) {//新增
dep.depend();//新增
}
return val
},
set: function reactiveSetter (newVal) {
if (newVal === val) {
return
}
val = newVal;
dep.notify();//新增
}
});
}
3.所谓的依赖就是wather,只有watcher触发的getter就会去收集依赖到Dep中去,当数据发生变化时,会循环依赖列表,把所有的watcher通知一遍。
watcher原理:先把自己设置到全局唯一的指定位置(pushTarget(this)),然后读取数据( value = this.getter.call(vm, vm);)触发该数据的getter。接着,在getter中就会从全局唯一的那个位置读取正在读取数据的watcher,并把这个watcher收集到Dep中。
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
var Watcher = function Watcher (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);//parsePath读取一个字符串的keypath
}
};
/**
* Evaluate the getter, and re-collect dependencies.
*/
Watcher.prototype.get = function get () {
pushTarget(this);//this就是当前watcher实例。看上面的pushTarget,把watcher实例赋值给Dep。target
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);//读取值的时候,触发getter,就可以将this主动添加到Dep(依赖收集)
} catch (e) {
throw e
} finally {
}
return value
};
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
4.创建Observer方法递归object中的所有数据(包括子数据)都转换成响应式形式。只有Object类型才会调用walk将每个属性转换成getter/setter形式侦测,在defineReactive$$1中新增observe(new Observer(value);)来递归子属性。
var Observer = function Observer (value) {
this.value = value;
if (Array.isArray(value)) {
} else {
this.walk(value);
}
};
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
};
5.无法跟踪新增属性和删除属性,可以用Vue提供的vm.$set和vm.$delete解决
6.总结图解:
二、Array的变化侦测
1.在Observer中使用拦截器覆盖那些即将转换成响应式的Array类型数据的原型。(value.__proto__=Object.create(Array.prototype);)
function copyAugment (target, src, keys) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
def(target, key, src[key]);
}
}
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
//鉴于有些浏览器不支持__proto__属性,在这个判断对象中是否有__proto__
var hasProto = '__proto__' in {};
var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
var Observer = function Observer (value) {
this.dep = new Dep();//创建一个Dep实例,存放依赖列表
def(value, '__ob__', this);//在value上增加一个不可枚举的属性__ob__,这个属性的值是Observer实例。这样就可以通过__ob__访问Observer实例,
if (Array.isArray(value)) {
if (hasProto) {
//__proto__是指向原型的,通过它来覆盖侦测数据的原型;Object.create返回一个带有Array原型对象和属性的新对象。
value.__proto__=Object.create(Array.prototype);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
}
};
2.Array也是在getter中收集依赖,在拦截器中触发依赖,并将依赖保存在Observer实例的Dep中。
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else {
ob = new Observer(value);
}
return ob
}
function defineReactive$$1 (obj, key, val) {
var childOb = observe(val);//为val创建Observer实例
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
if (childOb) {
childOb.dep.depend();//收集依赖
}
return val
},
set: function reactiveSetter (newVal) {
if(val === newVal){
return
}
val = newVal;
}
});
}
3.向依赖发送通知(通过this.__ob__访问Observer实例,调用Observer中Dep的notify发送通知依赖)
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
def(arrayMethods, method, function mutator () {
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); }
// notify change
ob.dep.notify();//重点
return result
});
});
4.监听数组中元素的变化
var Observer = function Observer (value) {
if (Array.isArray(value)) {
this.observeArray(value);
} else {
this.walk(value);
}
};
//侦听每一项
Observer.prototype.observeArray = function observeArray (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
};
5.因为Array的变化侦测是通过拦截原型的方式实现的,对于this.lsit[0]=2或this.list.length=0;是无法拦截侦测的。
总结:
- Array和Object都是在getter触发收集依赖的
- Object通过Observer(创建实例)递归所有数据的侦测,当从外界读取数据,在watcher读取数据,触发getter,将依赖(watcher)收集到Dep中,当数据发生变化时,会触发setter,从而向Dep中的watcher依赖发送通知,watcher接收到通知之后,通知外界视图或者回调函数
- Array通过Observer(创建实例)递归所有数据并创建拦截器覆盖原型的方式进行侦测,在Obserer方法中给value(侦测得数组)新增一个不可枚举的__ob__属性,并且该属性的值就是Observer实例;在getter中将依赖收集在Observer实例中的Dep去,当数据发生变化时,通过this.__ob__.dep访问Observer实例的dep去向依赖发送通知。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。