原理就不说明了,直接贴代码,加了很多注释
index.html
<!--
JS 有两种方法可以侦测到对象的变化:Object.defineProperty 和 Proxy
Vue 2.x 用的是 Object.defineProperty,3.x 用的是 Proxy
--------------------------------
Vue 双向绑定是通过 “数据劫持” + “发布订阅模式” 实现的
--------------------------------
这里实现的功能有:
input 和 textarea 标签的 v-model
文本的 {{ }} 数据绑定
-->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue Study Log</title>
</head>
<body>
<div id="app">
<input v-model="message">
<div>
<p>{{ message }}</p>
</div>
<input v-model="message">
<div>
<br>
<textarea v-model="message"></textarea>
</div>
{{ message }}
<div>
<p>{{ number }}</p>
<input v-model="number">
</div>
<div>
<div>
<p>{{ number }}</p>
</div>
</div>
</div>
<script src="./observer.js"></script>
<script src="./watcher.js"></script>
<script src="./compile.js"></script>
<script src="./vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
message: '我厉害吗',
number: 1234567
}
});
</script>
</body>
</html>
observer.js
// 使一个属性变为可观察的
function defineReactive(obj, key, val) {
// 收集 watcher 的容器
let dep = new Dep();
// 在这里,一个 defineProperty 会给 obj 添加三个属性
// 比如 message、set message、get message
Object.defineProperty(obj, key, {
get: function () {
dep.depend();
return val;
},
set: function (newVal) {
val = newVal;
dep.notify();
}
});
}
// 使一个对象的每一属性都变为可侦测的
function observable(obj, vm) {
Object.keys(obj).forEach(key => {
defineReactive(vm, key, obj[key]);
});
}
//----------------------------------------------
// 订阅器(依赖收集) Dep
class Dep {
constructor() {
this.subs = [];
}
// 添加订阅者
depend() {
if (Dep.target) {
this.subs.push(Dep.target);
}
}
// 通知订阅者更新
notify() {
this.subs.forEach(sub => {
sub.update();
});
}
}
watcher.js
// 订阅者 Watcher
class Watcher {
constructor(vm, node, name) {
this.vm = vm; // vue 实例
this.node = node; // 监听绑定的节点,比如监听的是 {{ }} 文本或者是 input 标签的值等
this.name = name; // 绑定的数据名称,v-model="name" 的 name
// 把这个 watcher 添加进 dep
Dep.target = this;
this.vm[this.name]; // 会自动调用绑定好的 get 方法
Dep.target = undefined;
}
// 当数据更新时 dep 会通知所有 watcher 执行这个 update 函数
update() {
// 如果是元素节点
if (this.node.nodeType === 1) {
this.node.value = this.vm[this.name];
}
// 如果是文本节点
else if (this.node.nodeType === 3){
this.node.nodeValue = this.vm[this.name];
}
}
}
compile.js
// 将目标节点下的子节点都添加进 fragment
function nodeToFragment(node, vm) {
// 创建 fragment(轻量级的 document 对象)
let fragment = document.createDocumentFragment();
let child = node.firstChild;
// 遍历根节点下的所有一级节点
while (child) {
compile(child, vm);
fragment.appendChild(child);
child = node.firstChild;
}
return fragment;
}
// 解析节点
function compile(node, vm) {
// 如果是元素节点,比如 input、div、p 标签等
if (node.nodeType === 1) {
compileElement(node, vm);
}
// 如果元素是文本节点(比如"<div>{{ xxx }}</div> 的 {{ xxx }}")
else if (node.nodeType === 3) {
compileText(node, vm);
}
}
// 解析元素节点
function compileElement(node, vm) {
// 如果是 input 标签或者 textarea 标签
if (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') {
// 遍历这个元素的所有属性
for (let attr of node.attributes) {
// 如果这个属性是 v-model
if (attr.nodeName === 'v-model') {
// 检测绑定的数据名称,即 v-model="name" 的 name
let name = attr.nodeValue;
// 给这个 input 或 textarea 添加事件
node.addEventListener('input', e => {
// 更改 vue 实例上的数据为这个 input 里的数据
vm[name] = e.target.value;
});
// 初始化此节点的值
node.value = vm[name];
// 移除 v-model 这个属性
node.removeAttribute('v-model');
// 给这个节点绑定一个订阅者,否则当别的 input 更新时收不到数据
new Watcher(vm, node, name);
}
}
}
// 如果是其他标签,比如 div、p 等
else {
// 递归解析他们的子节点
for (let child of node.childNodes) {
compile(child, vm);
}
}
}
// 解析文本节点
function compileText(node, vm) {
// 来匹配 {{ xxx }} 中的 xxx
let reg = /\{\{(.*)\}\}/;
// 如果这个文本里有 {{ xxx }} 这样的文本
if (reg.test(node.nodeValue)) {
// 获取匹配到的第一个字符串,比如 “ message”
let name = RegExp.$1;
// 去掉字符串的首尾空格,“message”
name = name.trim();
// 初始化此节点的值
node.nodeValue = vm[name];
// 绑定一个订阅者
new Watcher(vm, node, name);
}
}
vue.js
// Vue 构造函数
function Vue(options) {
// 使 data 里的属性都变为可侦测的
observable(options.data, this);
// 得到根元素,一般都是 <div id="app"></div>
let node = document.querySelector(options.el);
// 将根元素下的节点都移到 fragment 去编译,就是查找 v-model 和 {{ }}
let fragment = nodeToFragment(node, this);
// 再将这个 fragment 重新挂到根元素上
node.appendChild(fragment);
}