前言
vue框架实现了数据的双向绑定,即Data和Dom之间的双向通信,这两者之间的通信则需要一个Directive来连接,即:
表现得更复杂一点:
模块叙述
下面我结合一个具体的代码案例来介绍各个模块的功能以及最终的执行流程。
Observer/Dep
Observer的作用为对传入的data设置了 getter 和 setter即为一个可以获取和修改的观察者。
Dep 存在意义就是,他通过一个记录了 Watcher 和 Observer 之间的依赖关系,是二者的一个桥梁。
observer.js代码:
class Observer {
constructor(data) {
this.data = data;
this.walk(data);
}
walk = (data) => {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
});
}
defineReactive = (data, key, val) => {
const dep = new Dep();
observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: () => {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set: (newVal) => {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
}
});
}
}
function observe(value) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
};
class Dep {
constructor() {
this.subs = [];
}
addSub = (sub) => {
this.subs.push(sub);
}
notify = () => {
this.subs.forEach((sub) => {
sub.update();
});
}
}
Dep.target = null;
详细来说,首先Observer中通过创建Dep对象并在其getter和setter中绑定与Dep的关系,而Watch中通过全局属性Dep.target来绑定与Dep的关系,当Observer触发setter时就能直接触发Watch中的update方法。
Watch
Watch的作用就是当观察到Observer中的数据变化时会执行Directive里给Watch传递的回调函数。
watch.js代码:
class Watcher {
constructor(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.get(); // 将自己添加到订阅器的操作
}
update = () => {
this.run();
}
run = () => {
const value = this.vm.data[this.exp];
const oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb(value);
}
}
get = () => {
Dep.target = this; // 缓存自己
this.value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
}
}
Directive(Compile)
Compile是一个简化版本的Directive,Compile的作用就是解析传入的Data并且与之绑定相应的Watch。
compile.js代码:
class Compile {
constructor(el, vm) {
this.vm = vm;
this.el = document.querySelector(el);
this.fragment = null;
this.init();
}
init = () => {
if (this.el) {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
} else {
console.log('Dom元素不存在');
}
}
nodeToFragment = () => {
const fragment = document.createDocumentFragment();
let child = this.el.firstChild;
while (child) {
// 将Dom元素移入fragment中
fragment.appendChild(child);
child = this.el.firstChild;
}
return fragment;
}
compileElement = (el) => {
const childNodes = el.childNodes;
childNodes.forEach((node) => {
const reg = /\{\{(.*)\}\}/;
const text = node.textContent;
if (this.isElementNode(node)) {
this.compile(node);
} else if (this.isTextNode(node) && reg.test(text)) {
this.compileText(node, reg.exec(text)[1]);
}
if (node.childNodes && node.childNodes.length) {
this.compileElement(node);
}
});
}
compile = (node) => {
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach((attr) => {
const attrName = attr.name;
if (this.isDirective(attrName)) {
const exp = attr.value;
const dir = attrName.substring(2);
if (this.isEventDirective(dir)) { // 事件指令
this.compileEvent(node, exp, dir);
} else { // v-model 指令
this.compileModel(node, exp);
}
node.removeAttribute(attrName);
}
});
}
compileText = (node, exp) => {
const initText = this.vm[exp];
this.updateText(node, initText);
new Watcher(this.vm, exp, (value) => {
this.updateText(node, value);
});
}
compileEvent = (node, exp, dir) => {
const eventType = dir.split(':')[1];
const cb = this.vm.methods && this.vm.methods[exp];
if (eventType && cb) {
node.addEventListener(eventType, cb, false);
}
}
compileModel = (node, exp) => {
const val = this.vm[exp];
this.modelUpdater(node, val);
new Watcher(this.vm, exp, (value) => {
this.modelUpdater(node, value);
});
node.addEventListener('input', (e) => {
const newValue = e.target.value;
if (val === newValue) {
return;
}
this.vm[exp] = newValue;
val = newValue;
});
}
updateText = (node, value) => {
node.textContent = typeof value == 'undefined' ? '' : value;
}
modelUpdater = (node, value) => {
node.value = typeof value == 'undefined' ? '' : value;
}
isDirective = (attr) => attr.indexOf('v-') == 0
isEventDirective = (dir) => dir.indexOf('on:') === 0
isElementNode = (node) => node.nodeType == 1
isTextNode = (node) => node.nodeType == 3
}
入口文件
index.js:
class SelfVue {
constructor(options) {
this.data = options.data;
this.methods = options.methods;
Object.keys(this.data).forEach((key) => {
this.proxyKeys(key);
});
observe(this.data);
new Compile(options.el, this);
options.mounted.call(this); // 所有事情处理好后执行mounted函数
}
proxyKeys = (key) => {
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get: () => this.data[key],
set: (newVal) => this.data[key] = newVal
});
}
}
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>self-vue</title>
</head>
<style>
#app {
text-align: center;
}
</style>
<body>
<div id="app">
<h2>{{title}}</h2>
<input v-model="name">
<h1>{{name}}</h1>
<button v-on:click="clickMe">click me!</button>
</div>
</body>
<script src="js/observer.js"></script>
<script src="js/watcher.js"></script>
<script src="js/compile.js"></script>
<script src="js/index.js"></script>
<script type="text/javascript">
new SelfVue({
el: '#app',
data: {
title: 'hello world',
name: 'canfoo'
},
methods: {
clickMe: () => {
this.title = 'hello world';
}
},
mounted: function () {
setTimeout(() => {
this.title = '你好';
}, 1000);
}
});
</script>
</html>
流程概述
以上就是此案例的全部代码,其中h1里面的内容会实时与input框内的内容保持双向一致,这里我详细描述此过程执行流程。
初始化时在SelfVue里为这两个Data分别设置了setter和getter,然后在Compile里分别创建了Watch。当用户改变input里的内容时,首先会由input的data触发其Observer里的setter方法,然后由Dep触发Watch里的Compile传入的回调函数(注意,这里Dep里面有两个Watch一个是input一个是h1),从而改变input的Dom,紧接着又会触发第二个Watch里的Compile传入的回调函数,进而改变了h1的Dom。
结语
到此,代码和流程都已介绍完毕,有兴趣的朋友可以自己进行实现,或者也可以直接用此代码进行运行调试。