双向数据绑定的概念,相信大家都耳熟能详,简单来说,数据变化更新视图,视图变化更新数据。为了实现这一效果,在 Vue 中,采用了 数据劫持结合发布订阅者模式
的方式来实现。
通过 Object.defineProperty()
实现数据劫持,监听数据的变化。
通过 发布者Dep()
订阅者Watcher
实现发布订阅者模式,达到视图与数据之间相互更新的解耦。
关于如何实现一个简单的数据双向绑定,网上有很多例子,我就不列举了。我这里将我理解的双向绑定原理图画了一下:
然后,我分模块贴下代码(代码不是我写的,我也是找了别人的学习)
observer()
function observer(data) {
if(!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(key => {
var dep = new Dep();
var value = data[key];
observer(value);
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
if(Dep.target) {
dep.addSub(Dep.target)
}
return value;
},
set(newValue) {
if (value === newValue) {
return;
}
value = newValue;
dep.notify();
},
})
})
}
Dep()
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(sub => {
sub.update();
});
}
}
Dep.target = null;
Watcher
function Watcher(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get();
}
Watcher.prototype = {
get: function() {
Dep.target = this;
var value = this.vm._data;
this.exp.split('.').forEach(key => {
value = value[key]
})
Dep.target = null;
return value;
},
update: function() {
this.run();
},
run: function() {
var value = this.vm._data;
this.exp.split('.').forEach(key => {
value = value[key]
})
if (value !== this.value) {
this.value = value;
this.cb.call(this.vm, value);
}
}
}
Compile
function Compile(el, vm) {
this.el = document.querySelector(el);
this.vm = vm;
this.init();
}
Compile.prototype = {
init: function() {
this.fragment = this.node2Fragment(this.el);
this.compile(this.fragment);
this.el.appendChild(this.fragment);
},
node2Fragment: function(el) {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while(child) {
fragment.appendChild(child);
child = el.firstChild;
}
return fragment;
},
compile: function(el) {
var childNodes = el.childNodes;
var that = this;
Array.prototype.slice.call(childNodes).forEach(node => {
// if(that.isElementNode(node)) {
// that.compileElement(node)
// }
if (that.isTextNode(node)) {
that.compileText(node)
}
if(node.childNodes && node.childNodes.length) {
that.compile(node)
}
})
},
compileElement: function(node) {
var attributes = node.attributes;
var that = this;
Array.prototype.forEach.call(attributes, function(attr) {
if(that.isDirective(attr)) {
if(that.isModelDirective) {
that.compileModel()
}
if(that.isHtmlDirective) {
that.compileHtml()
}
if(that.isEventDirective) {
that.compileEvnet()
}
}
// if(that.isShortEventDirective(attr)) {
// that.compileEvnet()
// }
});
},
compileText: function(node) {
var that = this;
var reg = /\{\{(.*)\}\}/;
if(reg.test(node.textContent)) {
var exp = reg.exec(node.textContent)[1].trim()
var val = that.vm._data;
exp.split('.').forEach(key => {
val = val[key]
})
that.updateText(node, val);
new Watcher(that.vm, exp, function(value) {
that.updateText(node, value)
})
}
},
compileModel: function() {},
compileHtml: function() {},
compileEvnet: function() {},
updateText: function(node, value) {
node.textContent = value
},
isDirective: function(attr) {
return attr.indexof('v-') === 0;
},
isEventDirective: function(attr) {
return attr.indexof('on:') === 0;
},
isShortEventDirective: function(attr) {
return attr.indexof('@') === 0;
},
isHtmlDirective: function(dir) {
return dir.indexof('html') === 0;
},
isModelDirective: function(dir) {
return dir.indexof('model') === 0;
},
isElementNode: function(node) {
return node.nodeType === 1;
},
isTextNode: function(node) {
return node.nodeType === 3;
}
}
Vue
function Vue(options = {}) {
this.$options = options;
this.$el = document.getElementById(options.el);
this._data = options.data;
let data = this._data;
this._proxyData(options.data);
observer(data);
this.methods = options.methods;
new Compile(options.el, this)
}
Vue.prototype = {
_proxyData: function(data) {
var that = this;
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
configurable: true,
enumerable: false,
get() {
return that._data[key];
},
set(newValue) {
that._data[key] = newValue;
},
})
})
}
}
测试
{{ msg }}
{{ f.name }}
var vm = new Vue({
el: '#app',
data: {
msg: '11',
f: {
name: 1
},
dom: ''
},
methods: {
clickMe() {
console.log(123);
}
}
})
在 浏览器控制台 输入
vm.msg = 333;
vm.f.name = 'xxxxx'
可以看到数据变化了