这篇文章主要是来扒一扒VUE的双向绑定原理。
vue是属于MVVM(modal-view-viewmodal)模型,它其中一大特点就是view和viewmodal的双向绑定。
vue的双向绑定的做法主要是数据劫持和发布者-订阅者模式结合。
一、主要用到的知识点
documentFragment:
documentFragment主要就是用来数据劫持。用白话说就是:根据id将一个根节点下的所有子孙节点都劫持到一个新建的documentFragment中,然后再将documentFragment整体挂载到根节点下。劫持的过程中可以做一些操作,比如编译HTML(后面细说)。
var element = document.createDocumentFragment();
var node = document.getElementById("app");
var child;
while(child = node.firstChild) {
//do sth compile
element.appendChild(child);
//appendChild()会将child移到element,而不是复制,所以这个while不是死循环哈
}
//循环完之后,node已经没有子孙节点了
document.getElementById("app").appendChild(element);
Object.defineProperty(obj, val, des):
设置data中的数据的访问器属性,访问器属性是一种特殊的属性,它不能直接在对象中设置,必须通过Object.defineProperty()来实现。
var data = {
a: 'test',
};
Object.defineProperty(data, 'a', {
get: function() {
//do sth observe
return sth;
},
set: function(val) {
//do sth observe
}
});
get和set内部的this都指向data。
当读取data.a的时候就会调用get函数;当给data.a赋值的时候就会调用set函数。因此当熟读data.a或者复制data.a的时候可以监听(observe)到。
发布者-订阅者模式:发布者publisher和订阅者subscriber是一对多的关系。
二、双向绑定的实现
viewmodal -> view:
上面知识点中有说到数据劫持,并且在数据劫持的过程中,可以做一些编译(compile),主要是两个任务:
-
数据初始化绑定:页面首次打开之后将data渲染到HTML;
-
数据响应式绑定:用defineProperty()给data设置访问器属性,当data中的属性被赋值时,就会触发set方法,对data进行监听。
function Vue(option) {
//数据响应式绑定,observe监听
this.data = option.data;
var data = this.data;
observe(data, this);
//数据初始化绑定,编译compile
var id = option.el;
var node = document.getElementById(id);
nodeToFragment(node, this);
}
//数据初始化绑定
function nodeToFragment(node, vm) {
var element = document.createDocumentFragment();
var node = document.getElementById("app");
var child;
while(child = node.firstChild) {
compile(child, vm);
element.appendChild(child);
//appendChild()会将child移到element,而不是复制,所以这个while不是死循环哈
}
//循环完之后,node已经没有子孙节点了
document.getElementById("app").appendChild(element);
}
function compile(node, vm) {
if(node.nodeType == 1) {//表示是element
let attrs = node.attributes;//获取node的属性
for(let i = 0; i < attrs.length; i ++) {
if(attrs[i].nodeName == 'v-model') {
let name = attrs[i].nodeValue;
node.value = vm.data[name]; //渲染element
node.removeAttribute('v-model');
}
}
}
if(node.nodeType == 3) {//表示是text
let reg = /\{\{(.*)\}\}/;
if(reg.test(node.nodeValue)) {
let name = RegExp.$1; //获取reg第一个括号中匹配到数据
name = name.trim();
node.nodeValue = vm.data[name]; //渲染text
}
}
}
//数据响应式绑定
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm.data, key, obj[key]);
})
}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
//...
set: function(newVal) {
if(newVal == val)
return;
val = newVal;
console.log(obj + ':' + key + '被更新了');
//通知更新试图
}
})
}
触发set方法之后,如何响应到文本节点?
这里用到了上文所讲的知识点之一————发布者-订阅者模式。set方法触发后,第二件事就是作为发布者发通知,告知订阅者“我变了”,文本节点就是订阅者,收到消息之后,更新试图。
发布者和订阅者是一对多的关系。
function Dep() {
this.subs = [];//订阅者数组
}
Dep.prototype = {
addSub: function(sub) { //添加订阅者
this.subs.push(sub);
},
notify: function() { //发布者通知订阅者更新
this.subs.forEach(function(sub) {
sub.update();
})
}
}
接下来要做的就是在合适的时候添加订阅者,合适的时候自然是get方法中;然后在set方法中添加notify方法,当set被触发之后通知订阅者进行更新试图。
function defineReactive(obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
//...
get: function() {
if(Dep.target) {//Dep.target是全局变量,当前订阅者
dep.addSub(Dep.target); //添加订阅者
}
return val;
},
set: function(newVal) {
if(newVal == val)
return;
val = newVal;
dep.notify();//发布者发出通知,dep对象收到通知后推送给订阅者
}
})
}
Dep.target是什么呢?Dep.target是一个全局变量,会把当前订阅者复制给Dep.target,以便在get方法中调用。
订阅者Watcher
function Watcher(vm, node, name) {
Dep.target = this; //将自己复制给Dep.target;
this.vm = vm;
this.node = node;
this.name = name;
this.update();//添加订阅者和更新订阅者value
Dep.target = null; //将全局变量Dep.target赋值为空
}
Watcher.prototype = {
update: function() {
this.get();
this.node.nodeValue = this.value;//
},
get: function() {
this.value = this.vm.data[this.name];//触发get方法
}
}
function compile(node, vm) {
//...
if(node.nodeType == 3) {//表示是text
let reg = /\{\{(.*)\}\}/;
if(reg.test(node.nodeValue)) {
let name = RegExp.$1; //获取reg第一个括号中匹配到数据
name = name.trim();
//node.nodeValue = vm.data[name]; //渲染text
new Watcher(vm, node, name);
}
}
}
view -> viewmodal
指的是input,select,textarea文本框内的值被改变之后,也要同步到data中的数据。 解决办法就是在数据劫持的时候,给每个节点添加监听,这一步也是在compile中实现。
function compile(node, vm) {
//...
if(node.nodeType == 1) {//表示是element
let attrs = node.attributes;//获取node的属性
for(let i = 0; i < attrs.length; i ++) {
if(attrs[i].nodeName == 'v-model') {
let name = attrs[i].nodeValue;
//node.value = vm.data[name]; //渲染element
new Watcher(vm, node, name);
node.removeAttribute('v-model');
node.addEventListener('keydown', function(e) {
vm.data[name] = node.value;
})
}
}
}
}
这样就实现了view <-> viewmodal的双向绑定了。
整体代码↓↓↓,不过这只是简易版的双向绑定,实际会复杂很多。
<html>
<head>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
{{text}}
</div>
<script type="text/javascript">
function Dep() {
this.subs = [];//订阅者数组
}
Dep.prototype = {
addSub: function(sub) { //添加订阅者
this.subs.push(sub);
},
notify: function() { //发布者通知订阅者更新
this.subs.forEach(function(sub) {
sub.update();
})
}
}
function Watcher(vm, node, name) {
Dep.target = this; //将自己复制给Dep.target;
this.vm = vm;
this.node = node;
this.name = name;
this.update();//添加订阅者和更新订阅者value
Dep.target = null; //将全局变量Dep.target赋值为空
}
Watcher.prototype = {
update: function() {
this.get();
this.node.nodeValue = this.value;//
},
get: function() {
this.value = this.vm.data[this.name];//触发get方法
}
}
function Vue(option) {
//数据响应式绑定,observe监听
this.data = option.data;
var data = this.data;
observe(data, this);
//数据初始化绑定,编译compile
var id = option.el;
var node = document.getElementById(id);
nodeToFragment(node, this);
}
//数据初始化绑定
function nodeToFragment(node, vm) {
var element = document.createDocumentFragment();
var node = document.getElementById("app");
var child;
while(child = node.firstChild) {
compile(child, vm);
element.appendChild(child);
//appendChild()会将child移到element,而不是复制,所以这个while不是死循环哈
}
//循环完之后,node已经没有子孙节点了
document.getElementById("app").appendChild(element);
}
function compile(node, vm) {
if(node.nodeType == 1) {//表示是element
let attrs = node.attributes;//获取node的属性
for(let i = 0; i < attrs.length; i ++) {
if(attrs[i].nodeName == 'v-model') {
let name = attrs[i].nodeValue;
node.value = vm.data[name]; //渲染element
node.removeAttribute('v-model');
node.addEventListener('keyup', function(e) {//对文本节点进行更新
vm.data[name] = node.value;
})
}
}
}
if(node.nodeType == 3) {//表示是text
let reg = /\{\{(.*)\}\}/;
if(reg.test(node.nodeValue)) {
let name = RegExp.$1; //获取reg第一个括号中匹配到数据
name = name.trim();
//node.nodeValue = vm.data[name]; //渲染text
new Watcher(vm, node, name);
}
}
}
//数据响应式绑定
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm.data, key, obj[key]);
})
}
function defineReactive(obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
//...
get: function() {
if(Dep.target) {//Dep.target是全局变量,当前订阅者
dep.addSub(Dep.target); //添加订阅者
}
return val;
},
set: function(newVal) {
if(newVal == val)
return;
val = newVal;
console.log('我被更新了');
dep.notify();//发布者发出通知,dep对象收到通知后推送给订阅者
}
})
}
var vm = new Vue({
el: 'app',
data: {
text: 'aaa',
}
});
</script>
</body>
</html>
参考文章 小白我看了几篇类似的文章觉得这一篇比较通俗易懂,所以我在这基础上稍微整理了下。
请各位大神赐教。