vue双向绑定原理
vue的双向绑定是由数据劫持结合发布者-订阅者模式实现。
什么是数据劫持?: vue通过Object.defineProperty()来劫持对象属性的setter和getter操作,在数据变动时做你想要做的事情。
var Book = {};
Object.defineProperty(Book,'name',{
set:function(value) {
name = value;
console.log('你取了名叫:'+value);
},
get:function() {
console.log('get方法被监听到');
return '<'+name+'>';
}
});
Book.name = '张三'; //你取了名叫: 张三
console.log(Book.name); //<张三>
实现过程
我们已经知道如何实现数据的双向绑定了,那么首先要对数据进行劫持监听,所以我们首先要设置一个监听器Observer,用来监听所有的属性,当属性变化时,就需要通知订阅者Watcher,看是否需要更新。因为属性可能是多个,所以会有多个订阅者,故我们需要一个消息订阅器Dep来专门收集这些订阅者,并在监听器Observer和订阅者Watcher之间进行统一的管理。因为在节点元素上可能存在一些指令,所以我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化成一个订阅者Watcher,并替换模板数据并绑定相应的函数,这时候当订阅者Watcher接受到相应属性的变化,就会执行相对应的更新函数,从而更新视图。
整理上面的思路,我们需要实现几个步骤,来完成双向绑定:
- Observer 监听器,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
- Dep 存储依赖和派发更新,监听器和订阅者的桥梁。
- Watcher 订阅者,可以收到属性的变化通知并执行相应的函数,从而更新视图。
- Compile 解析器,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器
1、实现一个监听器
- 数据监听器的核心方法就是Object.defineProperty( ),通过遍历循环对所有属性值进行监听劫持监听
- 在属性的 get里面判断是否需要需要添加订阅者,这是为了让Watcher在初始化时触发
- 在属性的 set方法中,如果函数变化,就会通知对应的所有订阅者,订阅者们将会执行相对应的更新函数
function Observer(data) {
this.data = data;
this.walk(data);
}
Observer.prototype = {
walk: function(data) {
var self = this;
Object.keys(data).forEach(function(key) {
self.defineReactive(data, key, data[key]);
});
},
defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function getter () {
//Dep类在下一个文件详细说
//Watcher初始化触发、判断是否需要添加订阅者明
if (Dep.target) {
dep.addSub(Dep.target); //添加订阅者
}
return val;
},
set: function setter (newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
}
});
}
};
function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
}
2、实现一个Dep
- addSub方法 用于收集订阅者
- notify方法 执行相对应的更新函数
- Dep.target 的作用在Watch类中有详细说明
function Dep() {
this.subs = [];
}
//prototype 属性使您有能力向对象添加属性和方法
//prototype这个属性只有函数对象才有,具体的说就是构造函数具有.只要你声明定义了一个函数对象,这个prototype就会存在
//对象实例是没有这个属性
Dep.prototype = {
addSub:function(sub) {
this.subs.push(sub);
},
notify:function() {
this.subs.forEach(function(sub) {
sub.update(); //通知每个订阅者检查更新
})
}
}
Dep.target = null;
3、实现一个 Watcher
- 缓存Watcher中this 到 Dep.target
- 强制执行监听器里的get函数
- update用于执行更新方法
function Watcher(vm,exp,cb) {
this.vm = vm; //指向SelfVue的作用域
this.exp = exp; //绑定属性的key值
this.cb = cb; //闭包
this.value = this.get();
}
Watcher.prototype = {
update:function() {
this.run();
},
run:function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if(value !== oldVal) {
this.value = value;
this.cb.call(this.vm,value,oldVal);
}
},
get:function() {
Dep.target = this; // 缓存自己
var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
}
现在实现observer和watcher的关联
(1)定义个SelfVue类
//将Observer和Watcher关联起来
function SelfVue(data,el,exp) {
this.data = data;
observe(data);
el.innerHTML = this.data[exp];
new Watcher(this,exp,function(value) {
el.innerHTML = value;
});
return this;
}
(2)新建个Index.html
<body>
<h1 id="name">{{name}}</h1>
</body>
<script src="../js/observer.js"></script>
<script src="../js/Watcher.js"></script>
<script src="../js/SelfVue.js"></script>
<script>
var ele = document.querySelector('#name');
var selfVue = new SelfVue({
name:'hello world'
},ele,'name');
window.setTimeout(function() {
console.log('name值改变了');
selfVue.name = 'byebye world';
},2000);
</script>
至此,vue的双向绑定就实现了。
4、实现指令解析器Compile
再上面的双向绑定demo中,我们发现整个过程都没有解析dom节点,而是固定某个节点进行替换数据,所以接下来我们要实现一个解析器Compile来解析和绑定工作,分析解析器的作用,实现步骤如下:
作用:
- 解析模板指令,并替换模板数据,初始化视图
- 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器
实现:
function Compile(el, vm) {
this.vm = vm;
this.el = document.querySelector(el);
this.fragment = null;
this.init();
}
Compile.prototype = {
init: function () {
if (this.el) {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
} else {
console.log('Dom元素不存在');
}
},
//为了解析模板,首先要获得dom元素,然后对含有dom元素上含有指令的节点进行处理
//这个过程对dom元素的操作比较繁琐,所以我们可以先建一个fragment片段
//将需要解析的dom元素存到fragment片段中在做处理:
nodeToFragment:function(el) {
var fragment = document.createDocumentFragment(); //createdocumentfragment()方法创建了一虚拟的节点对象,节点对象包含所有属性和方法。
var child = el.firstChild;
while(child) {
// 将Dom元素移入fragment中
fragment.appendChild(child);
child = el.firstChild;
}
return fragment;
},
//遍历各个节点,对含有相关指定的节点进行特殊处理
compileElement:function(el) {
var childNodes = el.childNodes; //childNodes属性返回节点的子节点集合,以 NodeList 对象。
var self = this;
//slice() 方法可从已有的数组中返回选定的元素。
[].slice.call(childNodes).forEach(function(node) {
var reg = /\{\{(.*)\}\}/;
var text = node.textContent; //textContent 属性设置或返回指定节点的文本内容
if(self.isTextNode(node) && reg.test(text)) { //判断是否符合{{}}的指令
//exec() 方法用于检索字符串中的正则表达式的匹配。
//返回一个数组,其中存放匹配的结果。如果未找到匹配,则返回值为 null。
self.compileText(node,reg.exec(text)[1]);
}
if(node.childNodes && node.childNodes.length) {
self.compileElement(node); //继续递归遍历子节点
}
});
},
compileText:function(node,exp) {
var self = this;
var initText = this.vm[exp];
this.updateText(node,initText); // 将初始化的数据初始化到视图中
new Watcher(this.vm,exp,function(value) {
self.updateText(node,value);
});
},
updateText:function(node,value) {
node.textContent = typeof value == 'undefined' ? '': value;
},
isTextNode: function(node) {
return node.nodeType == 3;
}
}
修改下Seftvue和index.html,更改后,我们就不要像之前通过传入固定的元素值进行双向绑定了,可以随便命名各种变量进行双向绑定了
function SelfVue (options) {
var self = this;
this.data = options.data;
this.methods = options.methods;
Object.keys(this.data).forEach(function(key) {
self.proxyKeys(key);
});
observe(this.data);
new Compile(options.el, this);
}
SelfVue.prototype = {
proxyKeys: function (key) {
var self = this;
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get: function getter () {
return self.data[key];
},
set: function setter (newVal) {
self.data[key] = newVal;
}
});
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<h1>{{title}}</h1>
<h2>{{name}}</h2>
<h3>{{content}}</h3>
</div>
</body>
<script src="./js/Observer.js"></script>//监听器
<script src="./js/Watcher.js"></script>//订阅器
<script src="./js/Compile.js"></script>//解析器
<script src="./js/Selfvue.js"></script>
<script>
var selfVue = new SelfVue({
el:'#app',
data:{
title:'aaa',
name:'bbb',
content:'ccc'
}
});
window.setTimeout(function() {
selfVue.title = 'ddd';
selfVue.name = 'eee';
selfVue.content = 'fff'
},2000);
</script>
</html>