童鞋们都应该知道Vue2.x很重要的特性(数据双向绑定、虚拟DOM),所以在面试的时候就经常有面试官问小白同学说数据双向绑定原理是什么,虚拟DOM是什么,给我实现一个呗。所以给大家展示一个最简的双向绑定的案例,也参考了网上一些资料。至于虚拟DOM的实现后面再说吧,网上资料也很多很全因为毕竟react都出来好久了。(都知道网上资料一大堆,大都不着调。)直接上代码!!!
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="msg">
<span>{{msg}}</span>
</div>
<script src="mvvm/watcher.js"></script>
<script src="mvvm/observer.js"></script>
<script src="mvvm/compile.js"></script>
<script src="mvvm/index.js"></script>
<script>
let vm = new Vue({
el: "app",
data: {
msg: 'hello world'
}
})
</script>
</body>
</html>
- index.js
// 这个文件为入口文件,也就是Vue的构造函数
function Vue(options) {
// 传递过来的对象
this.data = options.data;
this.id = options.el;
// 1.第一步先将data中所有的数据进行监听
// 这个函数一般使用递归的方式完成所有属性的监听
observer(this.data, this);
// 2.第二步将所有DOM节点的翻译出来,也就是说将v-model,
// {{}}等翻译成你想要的数据。其次还有将v-model的数据进行监听,使用观察者模式,完成双向绑定
getAllNode(document.getElementById(this.id), this);
}
- observer.js
// 定义一个pubsub,这个作用是将所有的观察者加入其中,
// 并且出发事件
function pubsub() {
this.subs = [];
}
pubsub.prototype = {
// 将需要观察的数据加入subs中
addSub: function(sub){
this.subs.push(sub);
},
// 执行观察的数据上绑定的事件update事件。
pub: function(){
console.log(this.subs);
this.subs.forEach(function(sub){
sub.update();
})
}
}
// 将数据都进行监听
function active(obj, key, val) {
var pubsub1 = new pubsub();
Object.defineProperty(obj.data, key, {
// getter,如果获取数据时,会判断是有需要观察的数据,如果有就添加到subs中,没有不添加
// Pubsub是一个全局的变量,这个变量必须是全局才能判断是有需要观察的数据
get() {
if(Pubsub.target) {
// 添加订阅
pubsub1.addSub(Pubsub.target);
}
return val;
},
set(newVal) {
// 如果数据被setter,那么就涉及到及时的更新数据,
// 这时只需要进行发布事件,观察的数据就会执行update函数来执行更新操作
if(val == newVal) {
return;
}
val = newVal;
// 发布
pubsub1.pub();
}
})
}
// 监听data中所有的数据
function observer(obj, vm) {
// obj = data
// vm = 实例对象
for(var key in obj) {
active(vm, key, obj[key]);
}
}
- comiple.js
// 获取到所有节点,并且进行翻译
function getAllNode(node, vm) {
console.log(vm);
var length = node.childNodes.length;
for(var i = 0; i < length; i++) {
compile(node.childNodes[i], vm)
}
}
// 翻译
function compile(node, vm) {
// 匹配到{{}},将其中的值进行观察。
var reg = /\{\{(.*)\}\}/;
// 如果节点存在并且节点类型为1时(查看一下为1时一般都是什么节点)
if(node!=undefined && node.nodeType == 1) {
// 获取到节点的属性
var attr = node.attributes;
if(attr.length) {
// 对节点的属性循环处理
for(var i = 0; i < attr.length; i++) {
// 如果为v-model时,进行处理
if(attr[i].nodeName == "v-model") {
// 获取到v-model里面的写的变量名
var name = attr[i].nodeValue;
// 给该input增加事件处理,如果内容改变,并及时更新data中的数据
node.addEventListener('input', function(e) {
vm.data[name] = e.target.value;
})
// 修改dom上的数据,并移除指令
console.log(vm.data[name]);
node.value = vm.data[name];
node.removeAttribute('v-model')
}
}
}else {
// 匹配到{{}}的dom节点
if(reg.test(node.outerText)) {
var name = RegExp.$1;
// 拿到变量的名称
name = name.trim();
// 将变量加入到watcher中
new Watcher(vm, node, name)
}
}
}
}
- watcher.js
// 全局的变量,这个变量来控制是否当前有观察的数据
var Pubsub = {
target: null
}
// 观察者定义
function Watcher(vm, node, name) {
Pubsub.target = this;
this.name = name;
this.node = node;
this.vm = vm;
// 将数据观察时就要进行更新操作一次
this.update();
Pubsub.target = null;
}
Watcher.prototype = {
// 将更新的数据渲染到页面中去
update() {
this.node.innerHTML = this.vm.data[this.name];
}
}
大家想测试,就将五个文件的代码复制下来,运行一下。
运行的同时给大家配一张图,让大家容易理解。(这张是盗图,不知哪位大神用visio画的,这里我就直接引用了)