Vue高级--双向绑定原理
1.原理
vue数据双向绑 定是通过数据劫持结合发布者-订阅者模式的方式,通过 **Object.defineProperty() ** 劫持各个属性的 setter , getter , 在数据变动时发布消息给订阅者,触发相应的监听回调。
2. 实现思路
要实现mvvm 的双向绑定,必须实现以下几点:
- 1 实现一个数据监听器 Observer,能够对数据对象的所有属性进行监听,如果变动,可拿到最新值并通知订阅者。
- 2 实现一个指令解析器 Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
- 3 实现一个 Watcher,作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
- 4 实现一个可以容纳订阅者的消息订阅器 Dep ,订阅器 Dep 主要负责收集订阅器,然后在属性变化的时候执行对象的 订阅者的更新函数。
- 5 实现MVVM 入口函数,整合以上三者。
上述流程如图所示:
image.png
3. 实现Observer 数据监听器
Observer 是一个数据监听器,其核心方法就是 Object.defineProperty()。如果要对所有属性都进行监听的话,那么可以通过递归方法遍历所有属性值,并对其进行 Object.defineProperty()处理,如下代码:
- Observer.js
// 1.实现Observer观察者
function Observer(data) {
this.data = data;
this.walk(data);
}
Observer.prototype = {
constructor: Observer,
walk: function (data) {
let self = this;
Object.keys(data).forEach(key => {
self.defineReacctive(self.data, key, data[key])
})
},
defineReacctive: function (data, key, val) {
observe(val);//监听子属性
Object.defineProperty(data, key, {
enumerable: true,//可枚举
configurable: false,//不能再define
get: function () {
return val;
},
set: function (newVal) {
if (val === newVal) return;
console.log('监听到的值发生变化了', val, '-->', newVal);
val = newVal;
}
})
}
}
function observe(data, vm) {
if (!data || typeof data !== 'object') {
return;
}
return new Observer(data)
}
4. 实现Dep 消息订阅器
设计过程中,需要创建一个可以容纳订阅者的消息订阅器 Dep,订阅器 Dep主要负责 收集订阅器Watcher,然后在属性变化的时候执行对象订阅者的更新函数。
- Dep.js
// 实现消息订阅器
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub)
},
notify: function () {
this.subs.forEach(function (sub) {
sub.update();
})
}
}
Dep.target = null;
- 结合Dep 和Observer
将订阅器添加的订阅者 设计在Observer的 getter里面,这是为了让 Watcher 初始化进行触发,在 Observer的setter函数里面,如果数据变化,就会通知所有订阅者,订阅者们就会执行对象的更新函数。一个比较完整的Observer 已经实现了。
Observer.prototype = {
constructor: Observer,
walk: function (data) {
let self = this;
Object.keys(data).forEach(key => {
self.defineReacctive(self.data, key, data[key])
})
},
defineReacctive: function (data, key, val) {
let dep = new Dep();
observe(val);//监听子属性
Object.defineProperty(data, key, {
enumerable: true,//可枚举
configurable: false,//不能再define
get: function () {
//将订阅者Wather赋予给 Dep.target,每个订阅者都是不一样的
Dep.target && dep.addSub(Dep.target);//在这里添加一个订阅器
return val;
},
set: function (newVal) {
if (val === newVal) return;
console.log('监听到的值发生变化了', val, '-->', newVal);
val = newVal;
dep.notify();//通知所有订阅者
}
})
}
}
5. 实现Compile 编译指令
image.pngCompile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面,并将每个指定对应的节点绑定更新函数,添加监听数据的Watcher订阅者,一旦数据有变动,收到通知,更新视图,如图所示:
- Compile.js 代码如下:
// 编译指令
function Compile(el, vm) {
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.$fragment = this.nodeFragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
constructor: Compile,
nodeFragment: function (el) {
let fragment = document.createDocumentFragment();
let child;
//将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
init: function () {
this.compileElement(this.$fragment);
},
compileElement: function (el) {
let childNodes = el.childNodes;
let self = this;
[].slice.call(childNodes).forEach(function (node) {
let text = node.textContent;
let reg = /\{\{(.*)\}\}/; //匹配 {{}}
if (self.isElementNode(node)) {//元素节点
self.compile(node)
} else if (self.isTextNode(node) && reg.test(text)) { //文本节点且 {{}}
self.compileText(node, RegExp.$1.trim());
}
if (node.childNodes && node.childNodes.length) {//拥有孩子节点,继续递归
self.compileElement(node)
}
})
},
compile: function (node) {
let nodeAttrs = node.attributes;
let self = this;
[].slice.call(nodeAttrs).forEach(function (attr) {
let attrName = attr.name;
if (self.isDirective(attrName)) {
let exp = attr.value;
let dir = attrName.substring(2);
if (self.isEventDirective(dir)) {//事件指令
compileUtil.eventHandler(node, self.$vm, exp, dir);
} else {//普通指令
compileUtil[dir] && compileUtil[dir](node, self.$vm, exp)
}
node.removeAttribute(attrName);
}
})
},
compileText: function (node, exp) {
compileUtil.text(node, this.$vm, exp);
},
isDirective: function (attr) {//普通指令
return attr.indexOf('v-') == 0;
},
isEventDirective: function (dir) {//事件指令
return dir.indexOf('on') === 0;
},
isElementNode: function (node) {//元素节点
return node.nodeType == 1;
},
isTextNode: function (node) {//文本节点
return node.nodeType == 3;
},
}
//指定处理集合
let compileUtil = {
text: function (node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
html: function (node, vm, exp) {
this.bind(node, vm, exp, 'html');
},
model: function (node, vm, exp) {
this.bind(node, vm, exp, 'model');
let self = this;
let val = this._getVMVal(vm, exp);
node.addEventListener('input', function (e) {
let newVal = e.target.value;
if (val === newVal) return;
console.log(newVal);
self._setVMVal(vm, exp, newVal);
val = newVal
})
},
class: function (node, vm, exp) {
this.bind(node, vm, exp, 'class');
},
bind: function (node, vm, exp, dir) {
let updateFn = updater[dir + 'Updater'];
updateFn && updateFn(node, this._getVMVal(vm, exp));
new Watcher(vm, exp, function (value, oldValue) {
updateFn && updateFn(node, value, oldValue)
})
},
//事件处理
eventHandler: function (node, vm, exp, dir) {
let eventType = dir.split(':')[1];
let fn = vm.$options.methods && vm.$options.methods[exp];
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false);
}
},
_getVMVal:function(vm,exp) {
let val = vm;
exp = exp.split('.');
exp.forEach(function(k) {
val = val._data[k]
})
return val;
},
_setVMVal: function(vm,exp,value) {
let val = vm;
exp = exp.split('.');
exp.forEach(function(k,i) {
//非最后一个key,更新val的值
if(i<exp.length -1) {
val = val._data[k];
} else {
val._data[k] = value
}
})
},
}
let updater = {
textUpdater: function(node,value) {
node.textContent = typeof value == 'undefined'? '' : value
},
htmlUpdater: function(node,value) {
node.innerHtml = typeof value == 'undefined'? '' : value
},
classUpdater: function(node,value,oldValue) {
let className = node.className;
className = className.replace(oldValue,'').replace(/\s$/,'');
let space = className && String(value)? ' ': '';
node.className = className + space +value;
},
modelUpdater: function(node,value) {
node.value = typeof value == 'undefined'? '' : value
},
}
6. 实现Watcher 订阅者
Watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
1.在自身实例化时往属性订阅器Dep 里面添加自己:在Dep.target上缓存订阅器,通过触发 getter方法,把自己添加到getter方法里面,添加成功后去掉Dep.target。
2.自身必须有一个update方法。
3.待属性变动dep.notice()通知时,能调用自身的update方法,并触发Compil中绑定的回调。
function MVVM(options) {
this.$options = options || {};
let data = this._data = this.$options.data;
let self = this;
observe(data,self);
this.$compile = new Compile(options.el || document.body, self)
}
从上面代码可看出监听的数据对象是options.data,每次需要更新视图,则必须通过let vm = new MVVM({data:{name: 'zs'}}); vm._data.name = 'li';,这样的方式来改变数据。
显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的:
let vm = new MVVM({data:{name: 'zs'}}); vm.name = 'li';
所以这里我们需要给MVVM实例添加一个属性代理的方法,使访问vm的属性代理可访问 vm._data的属性,改造后的代码如下:
function MVVM(options) {
this.$options = options || {};
console.log(this);
let data = this._data = this.$options.data;
let self = this;
//数据代理
//实现 vm.xxx -> vm._data.xxx
Object.keys(data).forEach(function (key) {
self._proxyData(key)
});
this._initComputed();
observe(data);
this.$compile = new Compile(options.el || document.body, this)
}
MVVM.prototype = {
constructor: MVVM,
$watch: function (key, options, cb) {
new Watcher(this, key, cb);
},
_proxyData: function (key, setter, getter) {
let self = this;
setter = setter ||
Object.defineProperty(self, key, {
enumerable: true,
configurable: false,
get: function proxyGetter() {
return self._data[key]
},
set: function proxySetter(newVal) {
self._data[key] = newVal;
}
})
},
_initComputed: function () {//添加计算属性
let self = this;
let computed = this.$options.computed;
if (typeof computed === 'object') {
Object.keys(computed).forEach(function (key) {
Object.defineProperty(self, key, {
get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
set: function() {}
})
})
}
},
}
这里主要利用了 Object.defineProperty() 这个方法来劫持 vm实例对象的属性的读写权,使读写vm实例的属性 转成了 vm._data的属性值。
7. html
<body>
<div id="app">
<h1 id="name">{{name}}</h1>
<input type="text" v-model="msg">
<p>{{msg}}</p>
<button v-on:click="clickHandle">change</button>
</div>
</body>
<script src="./Dep.js"></script>
<script src="./Observer.js"></script>
<script src="./Watcher.js"></script>
<script src="./Compile.js"></script>
<script src="./MVVM.js"></script>
<script>
let vm = new MVVM({
el: '#app',
data: {
msg: 'hello'
},
methods: {
clickHandle: function() {
this.msg = 'hi';
console.log(this);
}
},
})
</script>
效果图:
image.png
最后,源码都放在gitee上面了,请点击源码