解析Vue2.0双向绑定实现原理
什么是双向绑定
在开发中我们经常需要将数据动态的改变,并且改变后的数据还需要重新展现到页面上。在传统的开发模式下,我们需要手动更新视图,这个操作比较繁琐,代码比较复杂。双向绑定机制的引入,使得开发者不需要手动的去更新DOM,只需要关注数据的状态,页面会自动根据数据的变化来更新页面,这样开发效率大大提高。
Vue的双向绑定实现原理
Vue.js实现数据的双向绑定主要是通过数据劫持结合发布者-订阅者模式的设计思想来实现的。其中,数据劫持指的是通过 Object.defineProperty()
方法来对数据进行劫持,也就是监听对象属性的变化,当数据变动时,劫持器会通知订阅者,再由订阅者去更新视图。
在Vue.js中,实现双向绑定的关键在于UI层的模板解析(Compile),数据绑定(Observer),还有数据监听者(Watch)等操作。
Compile模板解析
Compile模板解析主要作用于 Vue.js 中的模板处理,同时也是解析器的核心。它的作用是对每个元素节点(Node)以及它们含有的指令(Directive)进行分析,将模板中的变量替换成实际的值。在解析过程中,Compile还需要实现一些其他功能,例如指令的解析、模板解析等。
Compile模板解析处理的是Vue.js实例中的所有DOM节点,包括了v-model、{{}}等指令。Compile负责将模板解析成AST(抽象语法树),由AST生成渲染函数。
Observer数据劫持
Observer数据劫持主要是通过 Object.defineProperty()
方法来对数据进行劫持,也就是对对象属性赋值进行监听,当数据变动时,劫持器会通知订阅者,再由订阅者去更新视图。
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
return val;
},
set: function(newVal) {
if (val === newVal) return;
val = newVal;
console.log(`属性${key}已经被监听,现在值为:“${newVal}”。`);
}
});
}
Observer会遍历vue中的所有属性,对每个属性进行劫持,这样当属性发生变化的时候就会自动触发getter和setter方法,在setter方法中可以做一些其他操作,例如通知更新以及开发者自定义的操作。
Watch数据监听者
Watch数据监听者主要是监听 Observer 发布的消息,并触发指令的回调函数去重新渲染页面。Watcher分其中主要分为两个类型,委托Watcher和自己Watcher。
VM实例
function Dep () {
this.subs = []
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub)
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update()
})
}
}
function defineReactive(data, key, val) {
var dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
if (Dep.target) {
dep.addSub(Dep.target)
}
return val;
},
set: function(newVal) {
if (val === newVal) return;
val = newVal;
dep.notify()
console.log(`属性${key}已经被监听,现在值为:“${newVal}”。`);
}
});
}
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 = {
node2Fragment: function(el) {
var fragment = document.createDocumentFragment();
var child;
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
init: function() {
this.compileElement(this.$fragment);
},
// 指令解析
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) {
var attrName = attr.name;
if (me.isDirective(attrName)) {
var exp = attr.value;
var dir = attrName.substring(2);
if (me.isEventDirective(dir)) {
// 事件指令
compileUtil.eventHandler(node, me.$vm, exp, dir);
} else {
// v-model 指令
compileUtil[dir] && compileUtil[dir](node, me.$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;
}
};
var compileUtil = {
// v-model
model: function(node, vm, exp) {
this.bind(node, vm, exp, 'model');
var me = this,
val = this._getVmVal(vm, exp);
node.addEventListener('input', function(e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
me._setVmVal(vm, exp, newValue);
val = newValue;
});
},
// 事件处理
eventHandler: function(node, vm, exp, dir) {
var eventType = dir.split(':')[1],
fn = vm.$options.methods && vm.$options.methods[exp];
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false);
}
},
// v-text
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, this._getVmVal(vm, exp));
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},
_getVmVal: function(vm, exp) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k) {
val = val[k];
});
return val;
},
_setVmVal: function(vm, exp, value) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k, i) {
// 非最后一个key,更新val的值
if (i < exp.length - 1) {
val = val[k];
} else {
val[k] = value;
}
});
}
};
var updater = {
modelUpdater: function(node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
},
textUpdater: function(node, value) {
console.log('text:', value)
node.textContent = typeof value == 'undefined' ? '' : value;
}
};
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get();
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldValue = this.value;
if (value !== oldValue) {
this.value = value;
this.cb.call(this.vm, value, oldValue);
}
},
get: function() {
Dep.target = this;
var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
Dep.target = null;
return value;
}
};
function Vue(data, el) {
this.$data = data;
new Observer(data);
this.$el = el;
this.$compile = new Compile(el, this);
}
var vm = new Vue({
data: {
text: 'demo test',
text1: 'A',
text2: 'B'
}
});
在上述代码中,定义了vue中的 Dep 类,Dep 类主要实现了订阅者的统计和通知。而 Compile 类主要进行解析vue的模板。
双向绑定实现原理的两条示例说明
示例1
<div id="app">
<input type="text" v-model="text"/>
<h1>{{text}}</h1>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
text: 'Hello, ZhuangZhuang!'
}
})
</script>
在这个例子中,我们通过 v-model
指令将 input
标签和 text
变量进行了绑定,只要我们输入内容就会自动更新到页面上。
这是因为在上述代码中,我们取到了dom对象,给dom添加了绑定事件,对于这个事件的回调函数我们传入了watcher中的callback。当我们的input发生了变化后会执行callback中的update方法,该方法最终执行是 updater['modelUpdate']
,该方法负责更新我们的视图。
示例2
<div id="app">
<input type="text" v-model="text1"/>
<input type="text" v-model="text2"/>
<h1>{{text1}} {{text2}}</h1>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
text1: 'A',
text2: 'B'
}
})
</script>
在这个例子中,我们通过 v-model
指令将2个 input
标签和 text1
、text2
两个变量分别进行了双向绑定,只要我们输入内容就会自动更新到页面上。
在实例的生命周期中,会先执行Observer,Observer会对vue中的所有属性进行遍历,对每个属性进行劫持;接着执行Compile,Compile负责模板解析,对v-model进行解析后生成getter和setter,与Observer进行关联,并且在数据更新的时候通知watcher进行更新,从而实现了双向绑定。 当我们的输入框发生变化时,监听器会检测到这一变化,并触发setter进行状态更新,更新完成后,监听器又会自动将新的状态通知给订阅它的节点,从而完成了整个数据流的自动处理过程。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:解析Vue2.0双向绑定实现原理 - Python技术站