实现一个简单版本 Vue,仅实现了 数据响应式、依赖收集、compile编译中的html和文本编译,起名为nvue,即新 vue。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">
<p n-text="counter"></p>
<p n-html="desc"></p>
<div>{{desc}}</div>
</div>
<script src="./nvue.js"></script>
<script>
const app = new NVue({
el: "#app",
data: {
counter: 1,
desc: '<p>这是一个新 vue<span style="color:red">demo</span></p>',
name: "xiaohong",
},
methods: {
add() {
this.counter++;
},
},
});
setInterval(() => {
app.counter++;
}, 2000);
</script>
</body>
</html>
nvue.js
// 一共需要实现:
// 1. Vue 类
// 2. Dep 类
// 3. Watcher 类
// 4. Compile 类 编译类
// 5. observer 函数
// 6. proxy 函数
// 7. reactive函数
// 一个对象代表一个 Observer
// 一个 key 代表 dep 实例
// 一个对象 key 的使用代表一个 watcher
// 实现数据响应式
function defineReactive(obj, key, val) {
// key如果是对象,则需要递归绑定响应式
observer(val);
// 一个 key 代表一个 dep,这里是给每一个 key 做响应式,所以创建一个 dep 实例对象
let dep = new Dep();
Object.defineProperty(obj, key, {
get() {
console.log("get", key);
// get 获取值,则创建 dep,传入的为 watcher,即 Dep.target
Dep.target && dep.addDep(Dep.target);
return val;
},
set(v) {
if (v != val) {
// 可能设置的还是为对象,需要递归传入做响应式
observer(v);
// set 修改值,则通知 dep 修改
val = v;
console.log("set", key, v);
// 通知 dep 更新
dep.notify();
}
},
});
}
function observer(data) {
// 保证仅对对象做响应式
if (typeof data !== "object" || !data) {
return data;
}
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key]);
});
}
// 给 vm 实例设置代理,可以通过 vue 实例直接获取到 data 对象属性
function proxy(vm) {
// 因为仅对 vm.$data 上的 key 做拦截,所以需要遍历 vm.$data 的 keys
Object.keys(vm.$data).forEach((key) => {
// 拦截 vm
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(v) {
if (v !== vm.$data[key]) {
vm.$data[key] = v;
}
},
});
});
}
class NVue {
// 传入 el,及配置对象
constructor(options) {
this.el = options.el;
this.$options = options; // 保存当前配置对象
this.$data = options.data; // 保存 当前data
this.$vm = this; // 保存当前 vue 实例
// 创建 Dep 的实例
// this.dep = new Dep();
// Dep.target && this.dep.addDep(this)
// 1.将所有 data 对象 变为响应式
observer(this.$data);
proxy(this);
// 2.为 当前 vue 实例 this 增加代理,让用户访问的 data 可以直接从vue 实例上获取,而不是必须从 this.$data上获取
// 3.data对象已变为响应式, 一切准备就绪,进行模版编译
new Compile(this.el, this.$vm);
}
}
class Compile {
constructor(el, vm) {
this.el = el;
this.vm = vm;
this.compile(document.querySelector(el));
}
// 编译函数
compile(el) {
// 传入的节点一定有 childNodes 子节点,对子节点进行遍历
el.childNodes.forEach((node) => {
// 元素
if (node.nodeType === 1) {
// 如果 nodeType=1 则为元素节点
this.compileElements(node);
// 如果 node 节点有 子节点
if (node.childNodes.length > 0) {
// 递归编译
this.compile(node);
}
} else if (this.isInterText(node)) {
// 如果 nodeTyppe=3 则为文本节点
this.compileText(node);
}
});
}
isInterText(node) {
// 判断插值文本,还要通过正则表达式验证 {{}} 格式
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
// 编译元素节点
compileElements(node) {
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach((attr) => {
const key = attr.name; // 获取属性 name
const exp = attr.value; // 获取属性值
// 如果属性 key 是以 n-开头,说明为指令表达式
if (key.startsWith("n-")) {
// 获取指令
const dir = key.substring("2");
this[dir] && this[dir](node, exp);
}
});
}
// 编译文本节点
compileText(node) {
// 注意:RegExp.$1,这里是临时取法,如果有别的正则不可以直接这样获取
this.update(node, RegExp.$1, "text");
}
// 统一更新函数,在这里创建 watcher
update(node, exp, dir) {
const fn = this[dir + "Updater"];
console.log("update", this.vm[exp]);
fn && fn(node, this.vm[exp]);
new Watcher(this.vm, exp, (val) => {
fn && fn(node, val);
});
}
// 文本更新函数
textUpdater(node, val) {
node.textContent = val;
}
// html 更新函数
htmlUpdater(node, val) {
node.innerHTML = val;
}
// 文本
text(node, exp) {
this.update(node, exp, "text");
}
// html
html(node, exp) {
this.update(node, exp, "html");
}
}
// 依赖类,一个 watcher 实例代表一个依赖
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
// 通过 key 和 vm 来获取最新的 value 值
this.key = key;
// 传入 updateFn 更新函数,需要再依赖被更新的时候调用,并传入最新的 value 值
this.updateFn = updateFn;
// 全局变量设为 Dep.target
Dep.target = this;
// 获取 vm[key],触发 对象 key get 方法,进行依赖收集
this.vm[key];
// 依赖收集后将 Dep.target 置空
Dep.target = null;
}
update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
// 依赖收集类
class Dep {
constructor() {
// 定义依赖数组,存放依赖,每个依赖就是一个 watcher
this.deps = [];
}
addDep(watcher) {
// 依赖收集,即 watcher 收集
this.deps.push(watcher);
}
notify() {
// 触发依赖更新,将依赖中所有的 watcher 的 update 方法都遍历一遍
this.deps.forEach((dep) => {
dep.update();
});
}
}
本代码参考自前端杨村长。