參考文章
效果
整體步驟
- 首先是在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>
複製程式碼