文中代码是从vue2.6源码中摘抄
Vue实现数据双向绑定的原理是基于数据劫持结合发布-订阅者模式实现的,通过Object.defineProperty()来劫持各个属性,并在数据变动时发布消息给订阅者,触发相应的监听回调,更新视图
Vue要实现数据的双向绑定,就必须要具备以下几点
1:Observer数据监听器:对数据对象的所有属性进行监听,如有变动则获取最新值并通知订阅者更新视图
2:Compile指令解析器:对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,并绑定相应的更新视图函数
3:Dep事件调度中心:如当Observer数据监听器监听的数据有变动,则通过Dep通知订阅者Watcher
4:Watcher订阅者:连接Observer和Compile的桥梁,能够订阅并接收每个属性变动的通知,执行每个指令绑定的回调函数,进而更新视图
如图
Observer数据监听器
将需要观察的数据对象进行递归遍历,通过Obeject.defineProperty()来监听每个属性的变动,当给某个属性赋值时,就会触发set方法,那么就能监听到数据变化了,部分源码
function observe(value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
//....省略
ob = new Observer(value)
return ob
}
class Observer {
value: any;
constructor(value: any) {
this.value = value
this.vmCount = 0
this.walk(value)
//....省略
}
walk(obj: Object) {
//....省略
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
//....省略
const dep = new Dep()//事件调用中心,存放订阅者Watcher
observe(val)
Object.defineProperty(obj, key, {
get: function reactiveGetter() {
//....省略
if (Dep.target) {
dep.depend()//往事件队列中添加订阅者Watcher
}
return value
},
set: function reactiveSetter(newVal) {
//如果值相等就直接返回 不更新视图
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
//....省略
val = newVal //赋新值
//通知所有订阅者
dep.notify()
}
})
}
Observer主要作用
1:对需要观察的数据对象进行递归遍历,为每个属性添加get、set方法
2:往Dep订阅器中添加订阅者Watcher
3:当数据变化时,通知所有订阅者更新数据
创建数据监听器后,通过for循环遍历给每个属性 并通过Object.defineProperty给每个属性添加get、set方法
在get方法中,除了返回我们所要的值以外,还往Dep事件队列中添加Watcher订阅者,用于当数据变化时通知订阅者
在set方法中,对比新旧值,如若不同,则通过Dep的notify方法通知订阅者,进而更新视图
Dep事件调度中心
Dep主要作用
1:维护一个数组,用于存放订阅者Watcher(在Watcher自身实例化时,通过一系列调用,往订阅器Dep中添加自己)
2:当数据变化时,能够调用自身notify方法,从而触发所有订阅者Watcher
class Dep {
static target: ?Watcher;
subs: Array<Watcher>;
//....省略
constructor() {
//内部维护一个数组,用于存放订阅者Watcher
this.subs = []
}
addSub(sub: Watcher) {
this.subs.push(sub)
}
depend() {
if (Dep.target) { //Dep.target就是订阅者Watcher
//调用Watcher的addDep方法,然后在回调到Dep的addSub方法从而添加订阅者
Dep.target.addDep(this)
}
}
notify() {
for (let i = 0, l = subs.length; i < l; i++) {
//通知所有订阅者
subs[i].update()
}
}
}
Dep内部维护着一个数组,通过depend往数组中添加订阅者Watcher,当属性有变化时会调用属性的set方法,进而调用Dep的notify方法,在notify内部通过循环的方式调用订阅者Watcher的update方法
Watcher订阅者
Watcher主要作用
1:在自身实例化时,通过一系列调用,往订阅器Dep中添加自己
2:当数据变化时,通过一系列调用,能够调用自身update方法,从而触发Compile中绑定的回调,更新视图
class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
) {
this.vm = vm
//回调函数,用于更新视图
this.cb = cb
//调用自己的get方法,目的是触发属性的get方法
this.value = this.lazy
? undefined
: this.get()
}
get() {
//把当前对象赋值给Dep.target
Dep.target = this
//....省略
//触发属性的get方法,从而在Dep中添加自己
value = this.getter.call(vm, vm)
//设置Dep.target为null
Dep.target = null
return value
}
//Dep对象会调用此方法
addDep(dep: Dep) {
if (!this.depIds.has(id)) {
dep.addSub(this) //添加Watcher到Dep中
}
}
//属性变化调用Dep的notify方法,进而调用此方法
update() {
this.run()
}
run() {
const value = this.get()
const oldValue = this.value
if (value !== oldVal) {
this.value = value;
//属性值变化,调用回调函数,更新视图
this.cb.call(this.vm, value, oldValue)
}
}
}
//Observer中代码
Object.defineProperty(obj, key, {
get: function reactiveGetter() {
//....省略
if (Dep.target) {
dep.depend()//往事件队列中添加订阅者Watcher
}
return value
}
}
实例化Watcher的时候,调用其内部的get方法,此方法首先把当前对象赋值给Dep.target,然后调用监听属性的get方法,进而往调用中心Dep中添加订阅者,从而当属性值有变化时,就会通知Dep调度中心,进而通知Watcher,而Watcher又会通过回调函数,通知视图更新
Compile指令解析器
因vue2.6源码这部分还没看太懂,所以直接转载了
Compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者(Watcher),一旦数据有变动,收到通知,更新视图
如图所示:
因为遍历解析的过程有多次操作DOM节点,为提高性能和效率,会先将根节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实DOM节点中
function Compile(el) {
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
init: function() { this.compileElement(this.$fragment); },
node2Fragment: function(el) {
var fragment = document.createDocumentFragment(), child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
}
};
compileElement将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对弈的指令更新函数进行绑定
Compile.prototype = {
// ... 省略
compileElement: function(el) {
var childNodes = el.childNodes, me = this;
[].slice.call(childNodes).forEach(function(node) {
var text = node.textContent;
var reg = /\{\{(.*)\}\}/; // 表达式文本
// 按元素节点方式编译
if (me.isElementNode(node)) {
me.compile(node);
} else if (me.isTextNode(node) && reg.test(text)) {
me.compileText(node, RegExp.$1);
}
// 遍历编译子节点
if (node.childNodes && node.childNodes.length) {
me.compileElement(node);
}
});
},
compile: function(node) {
var nodeAttrs = node.attributes, me = this;
[].slice.call(nodeAttrs).forEach(function(attr) {
// 规定:指令以 v-xxx 命名
// 如 <span v-text="content"></span> 中指令为 v-text
var attrName = attr.name; // v-text
if (me.isDirective(attrName)) {
var exp = attr.value; // content
var dir = attrName.substring(2); // text
if (me.isEventDirective(dir)) {
// 事件指令, 如 v-on:click
compileUtil.eventHandler(node, me.$vm, exp, dir);
} else {
// 普通指令
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
}
});
}
};
// 指令处理集合
var compileUtil = {
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
// ...省略
bind: function(node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater'];
// 第一次初始化视图
updaterFn && updaterFn(node, vm[exp]);
// 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
new Watcher(vm, exp, function(value, oldValue) {
// 一旦属性值有变化,会收到通知执行此更新函数,更新视图
updaterFn && updaterFn(node, value, oldValue);
});
}
};
// 更新函数
var updater = {
textUpdater: function(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
}
// ...省略
};
通过new Watcher()添加回调来接收数据变化的通知,以此来达到数据的双向绑定
观察者模式和发布订阅模式的区别
从图中可以看出两者的区别
1:发布订阅模式多了一个事件调度中心
2:观察者模式中被观察者和观察者有依赖关系:
3:发布订阅模式因中间有事件调度中心,所以没有依赖关系
4:观察者模式多应用于同步事件通知上
5:发布订阅模式因中间有事件调度中心,多应用于异步事件通知上