前言
VUE是当下比较热门的一个前端框架,其显著特点就是双向数据绑定,即data更新view,view更新data,但其具体实现一直模模糊糊,这次就来搞个明明白白。
核心原理
其核心原理就是数据劫持
,数据劫持是用Object.definePorperty()
来实现的,看代码
let data = {}
let num = 0;
Object.defineProperty(data, 'num', {
enumerable: true,
configurable: true,
get() {
return num;
},
set(v) {
num = v;
}
});
每当获取data['num']
的值时就会触发它的get方法
,设置data['num']
的值时就会触发它的set方法
,这样就实现了数据劫持,一旦数据发生一些改变,就可以监听到,然后去实现一些自定义的功能,例如:更新view
第一步,确定初始化方式
就模仿VUE的初始化好了,首先我们写HTML
<div id="app">
<h1>{{info}}</h1>
<input type="text" v-model="info">
<button v-on:click="btnClick">点击</button>
</div>
接着,对它进行初始化
let ymvue = new YMVue({
el: '#app',
data: {
info: 'hello world'
},
methods: {
mounted() {
console.log(this)
},
btnClick() {
this.hello = 'Hello Vue'
}
}
});
第二步,生成观察者
VUE用的是观察者模式,观察者的主要功能就是数据劫持,那我们定义一个观察者Guard
,当我们初始化一个YMVue
对象之后,就把它交给观察者Guard
,然后遍历它的data
集合,并对里面的数据进行劫持。
function Guard(obj) {
this.obj = obj; // YMVue对象
this.start(obj.data);
}
Guard.prototype = {
start(data) {
if (data && typeof data === 'object') {
Object.keys(data).forEach((key) => {
this.addGuard(data, key, data[key]);
});
}
},
addGuard(data, key, val) {
let self = this;
this.start(data[key]);
// let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
//if (Dep.target) {
// dep.addSub(Dep.target);
//}
return val;
},
set(v) {
if (val === v) {
return;
}
val = v;
//dep.update();
}
})
}
};
上面代码中注释代码仅作暂时注释,待下面进行说明
第三步,生成订阅者
如果给观察者传入一个回调函数callback
,那么在触发set
方法后执行回调,好像是实现了所有功能。
但是,but,一个数据的更新可能会触发无数个方法,如果通过给观察者传入回调,那就必须对同一个数据进行多次劫持,那自然是不得行了,所以需要个订阅者,把数据更新后带来的连锁反应订阅到这个数据上去。
function BindCallbackToGuard(obj, name, callback) {
this.obj = obj; // YMVue对象
this.callback = callback; // 回调函数
this.name = name; // 订阅的数据
this.value = this.get();
}
BindCallbackToGuard.prototype = {
excute() {
let val = this.obj.data[this.name];
let oldVal = this.value;
if (val !== oldVal) {
this.value = val;
this.callback.call(this.obj, val, oldVal);
}
},
get() {
Dep.target = this; // 把自己设为订阅对象
let value = this.obj.data[this.name]; // 触发观察者的get函数
Dep.target = null;
return value;
}
};
第四步,创建一个订阅者容器
第二步和第三步的代码中都有一个Dep
,那Dep
到底是什么呢
Dep
其实是一个订阅者容器,每当有一个订阅者订阅了自己,观察者就把它放进订阅者容器里面,然后当观察者监听到数据有变的时候,就去遍历订阅者容器,然后执行每个订阅者订阅的方法。
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub);
},
update() {
this.subs.forEach((sub) => { // sub是一个订阅者
sub.excute();
})
}
};
这时候,就可以取消第二步代码中的注释了
第五步,生成DOM解析器
上面我们已经把功能都搞定了,现在就需要把那些特殊的HTML跟这些代码联系到一起,这时候就需要一个DOM解析器,其中对DOM元素的操作我们用到了文档碎片Fragment
function Analysis(containerId, obj) {
this.obj = obj; // YMVue对象
this.dom = document.querySelector(containerId);
this.fragment = null;
this.init();
}
Analysis.prototype = {
init() {
if (this.dom) {
this.fragment = this.switchToFragment(this.dom); // 将NODE节点转换为文档碎片
this.analysisElement(this.fragment); // 开始解析
this.dom.appendChild(this.fragment); // 用文档碎片替换node节点
} else {
console.log('Dom 元素不存在');
}
},
switchToFragment() {
let fragment = document.createDocumentFragment();
let child = this.dom.firstChild;
while (child) {
fragment.append(child);
child = this.dom.firstChild;
}
return fragment;
},
analysisElement(dom) {
let childNodes = dom.childNodes;
[].slice.call(childNodes).forEach((node) => {
let reg = /\{\{(.*)\}\}/;
let text = node.textContent;
if (this.isElementNode(node)) { // 元素节点
this.analysisAttr(node);
} else if (this.isTextNode(node) && reg.test(text)) { // 文本节点
this.bindText(node, reg.exec(text)[1]);
}
if (node.childNodes && node.childNodes.length) {
this.analysisElement(node);
}
});
},
analysisAttr(node) {
let nodeAttrs = node.attributes;
Array.prototype.forEach.call(nodeAttrs, (attr) => {
let name = attr.name; // 属性名称
if (this.isCommand(name)) { // 属性名以'v-'开头
let value = attr.value; // 属性值
let command = name.substring(2);
if (this.isEventCommand(command)) { // 事件指令,比如v-on:click
this.bindEvent(node, value, command);
} else { // v-model 指令
this.bindModel(node, value)
}
node.removeAttribute(name); // 移除后,页面上不显示
}
})
},
bindText(node, name) {
let text = this.obj[name];
node.textContent = text || '';
// 添加一个订阅者
new BindCallbackToGuard(this.obj, name, function (v) {
node.textContent = v;
});
},
bindEvent(node, name, command) {
let eventType = command.split(':')[1];
let method = this.obj.methods && this.obj.methods[name];
if (eventType && method) {
node.addEventListener(eventType, method.bind(this.obj), false);
}
},
bindModel(node, name) {
let self = this;
let text = this.obj[name];
node.value = text || '';
// 添加一个订阅者
new BindCallbackToGuard(this.obj, name, function (v) {
node.value = v;
});
// 监听input事件,当它的value改变时,同时更新其绑定值
node.addEventListener('input', function (e) {
let newVal = e.target.value;
if (text === newVal) {
return;
}
self.obj[name] = newVal;
text = newVal;
}, false);
},
isCommand(attr) {
return attr.indexOf('v-') === 0;
},
isEventCommand(dir) {
return dir.indexOf('on:') === 0;
},
isElementNode(node) {
return node.nodeType === 1;
},
isTextNode(node) {
return node.nodeType === 3;
}
};
第六步,定义初始化类
现在所有的功能基本完成,就差一个初始类YMVue
了
function YMVue(opts) {
this.data = opts.data;
this.methods = opts.methods || {};
Object.keys(this.data).forEach((key) => {
this.proxy(key); // 设置代理
});
new Guard(this); // 初始化观察者
new Analysis(opts.el, this); // dom解析
this.methods.mounted && this.methods.mounted.call(this); // 初始化完成后,执行mounted方法
}
YMVue.prototype = {
proxy(key) {
let self = this;
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return self.data[key];
},
set(v) {
self.data[key] = v;
}
})
}
};
初始化类有一个代理proxy
,它有什么用呢?
因为VUE改变的数据的操作是this.info='xxxx'
,但是this
指向的是VUE
对象,info
又在data
集合里,所以应该是this.data.info='xxx'
,那取消中间的data
就需要用到代理。
结束
OK,大功告成,虽然功能距离真正的VUE还差的远,但是简单的数据双向绑定就这么实现了,写成一个插件,随便在哪都可以用起来。
完整代码在这