参考文章
效果
整体步骤
- 首先是在Vue的构造函数中对data中的数据响应化(使用的是defineProperty)
- 然后对html中相关指令和{{变量}}形式代码编译(compile), 因为这些变量与Vue.data里的数据有关,需要绑定在一起。
- 绑定过程是每一个与Vue.data中数据相关的节点,都会创建Watcher对象,然后这个代表节点的Watcher会被收集到数据自己的dep中,当这个数据变化时,会遍历dep中的Watcher。
- 数据变化的通知过程,使用defineProperty实现,通过劫持set操作,在其中通知Watcher,通过劫持get操作,来收集依赖。
代码地址
主要代码
复制保存为vmodel.js
// vm == view model
class Dep{
constructor(){
this.subscribers = [];
}
addSubscriber(sub) {
this.subscribers.push(sub);
}
notify() {
this.subscribers.forEach((sub) => {
sub.update();
});
}
}
class Watcher {
constructor(vm, node, name, nodeType) {
Dep.target = this;
this.vm = vm;
this.node = node;
this.name = name;
this.nodeType = nodeType;
this.update();
Dep.target = null;
}
update() {
this.get();
if (this.nodeType == 'text') {
this.node.data = this.value;
}
if (this.nodeType == 'input') {
this.node.value = this.value;
}
}
get() {
this.value = this.vm.data[this.name];
}
}
class Vue{
constructor(options) {
this.data = options.data;
toObserve(this.data);
let root = document.querySelector(options.el);
var newChild = nodeToFragment(root, this);
root.appendChild(newChild);
}
}
function toReactive(key, value, obj) {
let dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSubscriber(Dep.target);
}
return value;
},
set(newValue) {
if (newValue == value) {
return;
}
value = newValue;
dep.notify();
}
});
}
function toObserve(obj) {
Object.keys(obj).forEach((key) => {
toReactive(key, obj[key], obj);
});
}
function nodeToFragment(node, vm) {
let flag = document.createDocumentFragment();
let child = node.firstChild;
while (child) { // 遍历child
compile(child, vm);
flag.appendChild(child);
child = node.firstChild;
}
return flag;
}
function compile(node, vm) {
let reg = /\{\{(.*)\}\}/;
if (node.nodeType == Node.ELEMENT_NODE) {
let attrNode = node.attributes;
for (let i = 0; i < attrNode.length; i++) {
let attr = attrNode[i];
if (attr.nodeName == 'v-model') {
let name = attr.nodeValue;
node.addEventListener('input', (e) => {
console.log(e);
vm.data[name] = e.target.value; // 这里会通知name的subscribers进行update
console.log(vm);
});
node.value = vm.data[name]; // 通知
node.removeAttribute('v-model');
new Watcher(vm, node, name, 'input');
}
}
}
else if (node.nodeType == Node.TEXT_NODE) {
if (reg.test(node.nodeValue)) {
let name = RegExp.$1;
name = name.trim();
node.nodeValue = vm.data[name];
new Watcher(vm, node, name, 'text');
}
}
}
使用
复制保存为index.html,和vmodel.js放在同一文件夹。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="vueData">
{{ vueData }}
<button type="button" onclick="ssClick()">change</button>
</div>
<script src="vmodel.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
vueData: 'ssss'
}
})
function ssClick() {
vm.data['vueData'] = 'shaoshuai';
}
</script>
</body>
</html>