双向绑定的原理——Object.defineProperty和发布订阅者模式
这两个分别的概念:
(1)Object.defineProperty:其实是定义对象的属性,其实并不是为一个对象做双向数据绑定的,而是去给对象属性标签,只不过属性里的get和set实现了响应式。
(2)发布订阅者模式:订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。
上图,网上找的便于理解的图解释observer和compile,watcher三者的关系
接下来通过源码(减少很多理解复杂的代码)解析双向绑定的原理:
1、创建观察者observer
遍历data的属性,利用Object.defineProperty给对象每个属性添加get和set方法,当数据改变,触发set,获取数据又触发了get的方法。
2、创建订阅器Dep
此时我们还需要一个以来收集器(订阅器)dep来收集依赖,在添加get属性时,使用dep.depend()就把各自添加进dep里,在set中,每次改变数据时,触发dep.notify(),去更新依赖。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.prototype.walk = function walk(obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
};
function defineReactive$$1(obj, key, val, customSetter, shallow) {
var dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
}
return value
},
set: function reactiveSetter(newVal) {
val = newVal;
dep.notify();
}
});
}
3、定义订阅器Dep
var Dep = function Dep() {
this.id = uid++;
this.subs = [];
};
Dep.prototype.addSub = function addSub(sub) {
this.subs.push(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();
}
};
4、Watcher就是一个订阅者。
用于将dep发来的update消息处理,通过dep.addSub()和watcher.addSub()关联起来,执行Watcher绑定的更新函数,触发run(),进而触发this.cb.call(this.vm, value, oldValue);更新视图
Watcher.prototype.addDep = function addDep(dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
Watcher.prototype.update = function update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
Watcher.prototype.run = function run() {
if (this.active) {
var value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
var oldValue = this.value;
this.value = value;
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
};
5、实现一个Compile
compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
new Watcher(vm, exp, function(value, oldValue) {
// 一旦属性值有变化,会收到通知执行此更新函数,更新视图
updaterFn && updaterFn(node, value, oldValue);
});
涉及到比较重要一点,vue中数组只有push,pop等方法更新,才能双向数据绑定,下标的方式arr[0]则不能的原因:
源码中对数组的方法进行了封装
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/**
* Intercept mutating methods and emit events
*/
//装饰者模式
//既保持原有方法,又触发更新
methodsToPatch.forEach(function (method) {
// cache original 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); }
// notify change
//触发更新
ob.dep.notify();
return result
});
});
✳✳✳另外一提,vue3已经改用proxy来实现双向绑定原理,defineProperty只能监听某个属性,不能对全局对象监听,因为要循环,而proxy可以对全局对象进行监听,省去for in提升效率,还可以监听数组,不用再去单独对数组做特异性操作
(如有错误,欢迎指正)