Vue-双向绑定原理
在文末所附的参考资料中极尽详细地剖析了Vue实现双向绑定的原理。本文可以视为那篇文章的梗概版。
vue.js 实现双向绑定是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调
整体思路
- 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
- 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
- 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
- mvvm入口函数,整合以上三者
流程描述
以下我根据理解对整个流程做一个描述,可能出现理解有误。具体的逻辑到最后的参考资料去看。
首先我们整个流程的目的是要实现,将数据和视图绑定在一个vm实例中。然后改变数据的时候也动态改变试图上的数据。这仅仅只是单向绑定,即由更新数据->更新视图。但是双向绑定是在单向的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。
好以下我来描述整个实现流程
<div id="mvvm-app">{{ word }}</div>
var vm = new MVVM({
el: '#mvvm-app',
data: {
word: 'Hello World!'
},
});
我们最终要实现类似上面这样的一个ViewModel,其中el绑定了HTML也就是视图那边而data代表着数据,我们现在想要的是当我们改变数据
vm.word = 'Hello New World'!
HTML上的视图也会相应的更新
首先我们需要用Object.defineProperty()来进行数据劫持,大致原理就是我们使用
var dep = new Dep();
Object.defineProperty(data, word, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function() {
return val;
},
set: function(newVal) {
console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
val = newVal;
dep.notify();
}
});
这样我们就劫持了data这个数据的word属性,每次我们使用
vm.data.word = 'New world'
这样的方式为word赋上一个新值的时候,我们就可以触发上面set这个函数,执行setter函数里定义的逻辑。那么在setter的逻辑里我们应该写些什么呢。首先对于data里的每一个数据,比如这里的word,我们应该为他实例化一个相对应的Dep实例dep
var dep = new Dep();
然后在setter函数里我们调用
dep.notify();
这个Dep类型的构造函数如下
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
我们的每一个数据,都有一个dep,这个dep里面有一个数组,这个数组里面包含了所有监听这个数据(我们这里是word)的视图(我们这里是#mvvm-app里的)
好了现在我们完成的逻辑就是下面这样,一旦word被 = 赋上新值了,他会触发dep.notify。我们可以在下面看到notify的逻辑,它遍历自己的订阅者然后执行他们的update()函数,等于是通知所有监听/依赖我的视图,告诉这些视图”喂(#`O′)我的数据已经更新啦,你们视图也该更新一下了”。
那么现在问题时我们怎么往dep里添加订阅者呢?
首先我们应该知道dep里面的订阅者应该是Watcher的实例,我们应该将我们每一个监听这个word数据的视图(双大括号插值、特性上绑定指令等等)都实例化一个watcher然后添加到数据word的dep内的sub数组上来
所以我们去遍历el绑定的元素和他的子元素,然后搜索所有绑定word这个数据的视图,为每个视图new一个Watcher
//这里的Compile我们只考虑双大括号,其他的绑定指令啥的方法类似
//我们把绑定的那个el先提取出来
var el = document.querySelector(el)
var reg = /\{\{(.*)\}\}/ // {{}}表达式文本
var childNodes = el.childNodes
//将类数组对象转化为数组
[].slice.call(childNodes).forEach(function(node){
var text = node.textContent;
if(reg.test(text)){
//我猜这里的RegExp.$1应该是指word
new Watcher(vm,exp,callback);
}
})
在new这个Watcher的过程中我们试图劫持word数据的getter函数,在word数据的getter函数里为word数据相关联的这个dep添加一个新订阅者,也就是我们在扫描el中扫描到的这个视图所new出来的Watcher
// Observer.js
Object.defineProperty(data, key, {
get: function() {
// 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
Dep.target && dep.addDep(Dep.target);
return val;
}
// ... 省略
});
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
// 此处为了触发属性的getter,从而在dep添加自己,结合Observer更易理解
this.value = this.get();
}
Watcher.prototype = {
update: function() {
this.run(); // 属性值变化收到通知
},
run: function() {
var value = this.get(); // 取到最新值
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
}
},
get: function(key) {
Dep.target = this;
this.value = data[key]; // 这里会触发属性的getter,从而添加订阅者
Dep.target = null;
}
}
好了,现在订阅者也添加好了。
现在我们每一次更改word这个数据的时候都会触发setter函数
setter函数会去通知自己的所有订阅者wather触发自己的update()函数。我们的单向数据绑定到此完毕了。