代码功能流程图
源代码
index.html
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="app">
<!-- 双向数据绑定 靠的是表单 -->
<input type="text" v-model="message.a" />
<div>{{message.a}}</div>
{{message.a}} {{b}}
</div>
<script src="./src/watcher.js"></script>
<script src="./src/observer.js"></script>
<script src="./src/compile.js"></script>
<script src="./src/MVVM.JS"></script>
<script>
// 我们的数据一般都挂载在vm上
let vm = new MVVM({
el: "#app",
data: {
message: { a: "foo" },
b: "bar",
},
});
</script>
</body>
</html>
MVVM.JS
class MVVM {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
if (this.$el) {
new Observer(this.$data);
// 将数据代理到实例上直接操作实例即可,不需要通过vm.$data来进行操作
this.proxyData(this.$data);
new Compile(this.$el, this);
}
}
proxyData(data) {
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newValue) {
data[key] = newValue;
},
});
});
}
}
compile.js
class Compile {
constructor(el, vm) {
// 看看传递的元素是不是DOM,不是DOM我就来获取一下~
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {
// 如果这个元素能获取到 我们才开始编译
// 1.先把这些真实的DOM移入到内存中 fragment (性能优化)
let fragment = this.node2fragment(this.el);
// 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{}}
this.compile(fragment);
// 3.把编译好的fragment在塞回到页面里去
this.el.appendChild(fragment);
}
}
node2fragment(el) {
// 需要将el中的内容全部放到内存中
// 文档碎片 内存中的dom节点
let fragment = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
fragment.appendChild(firstChild);
// appendChild具有移动性
}
return fragment; // 内存中的节点
}
compile(fragment) {
// 需要递归 每次拿子元素
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach((node) => {
if (this.isElementNode(node)) {
// 是元素节点,还需要继续深入的检查
// 这里需要编译元素
this.compileElement(node);
this.compile(node);
} else {
// 文本节点
// 这里需要编译文本
this.compileText(node);
}
});
}
compileElement(node) {
// 带v-model v-text
let attrs = node.attributes; // 取出当前节点的属性
Array.from(attrs).forEach((attr) => {
// 判断属性名字是不是包含v-model
let attrName = attr.name;
if (this.isDirective(attrName)) {
// 取到对应的值放到节点中
let expr = attr.value;
let [, type] = attrName.split("-"); //
// 调用对应的编译方法 编译哪个节点,用数据替换掉表达式
CompileUtil[type](node, this.vm, expr);
}
});
}
compileText(node) {
let expr = node.textContent; // 取文本中的内容
let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} {{c}}
if (reg.test(expr)) {
// 调用编译文本的方法 编译哪个节点,用数据替换掉表达式
CompileUtil["text"](node, this.vm, expr);
}
}
/* 专门写一些辅助的方法 */
isElementNode(node) {
return node.nodeType === 1;
}
/* 核心的方法 */
/*辅助的方法*/
// 是不是指令
isDirective(name) {
return name.includes("v-");
}
}
CompileUtil = {
text(node, vm, expr) {
// 文本处理
let updateFn = this.updater["textUpdater"];
// 文本比较特殊 expr可能是'{{message.a}} {{b}}'
// 调用getTextVal方法去取到对应的结果
let value = this.getTextVal(vm, expr);
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
new Watcher(vm, arguments[1], (newValue) => {
// 如果数据变化了,文本节点需要重新获取依赖的属性更新文本中的内容
updateFn && updateFn(node, this.getTextVal(vm, expr));
});
});
updateFn && updateFn(node, value);
},
getTextVal(vm, expr) {
// 获取编译文本后的结果
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
// 依次去去数据对应的值
return this.getVal(vm, arguments[1]);
});
},
getVal(vm, expr) {
// 获取实例上对应的数据
expr = expr.split("."); // {{message.a}} [message,a] 实现依次取值
// vm.$data.message => vm.$data.message.a
return expr.reduce((prev, next) => {
return prev[next];
}, vm.$data);
},
setVal(vm, expr, value) {
expr = expr.split(".");
return expr.reduce((prev, next, currentIndex) => {
if (currentIndex === expr.length - 1) {
return (prev[next] = value);
}
return prev[next];
}, vm.$data);
},
model(node, vm, expr) {
// 输入框处理
let updateFn = this.updater["modelUpdater"];
// 这里应该加一个监控 数据变化了 应该调用这个watch的callback
new Watcher(vm, expr, (newValue) => {
// 当值变化后会调用cb 将新的值传递过来
updateFn && updateFn(node, this.getVal(vm, expr));
});
node.addEventListener("input", (e) => {
let newValue = e.target.value;
// 监听输入事件将输入的内容设置到对应数据上
this.setVal(vm, expr, newValue);
});
updateFn && updateFn(node, this.getVal(vm, expr));
},
// ------------------------------------
updater: {
// 文本更新
textUpdater(node, value) {
node.textContent = value;
},
// 输入框更新
modelUpdater(node, value) {
node.value = value;
},
},
};
observer.js
class Dep {
constructor() {
// 订阅的数组
this.subs = [];
}
addSub(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach((watcher) => watcher.update());
}
}
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
// 要对这个data数据将原有的属性改成set和get的形式
// defineProperty针对的是对象
if (!data || typeof data !== "object") {
return;
}
// 要将数据 一一劫持 先获取取到data的key和value
Object.keys(data).forEach((key) => {
// 定义响应式变化
this.defineReactive(data, key, data[key]);
this.observe(data[key]); // 深度递归劫持
});
}
// 定义响应式
defineReactive(obj, key, value) {
let that = this;
let dep = new Dep(); // 每个变化的数据 都会对应一个数组,这个数组是存放所有更新的操作
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 当取值时调用的方法
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue) {
if (newValue != value) {
that.observe(newValue);
value = newValue;
dep.notify(); // 通知所有人 数据更新了
}
},
});
}
}
// 更新Observer中的defineReactive
watcher.js
class Watcher {
// 因为要获取老值 所以需要 "数据" 和 "表达式"
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先获取一下老的值 保留起来
this.value = this.get();
}
// 老套路获取值的方法,这里先不进行封装
getVal(vm, expr) {
expr = expr.split(".");
return expr.reduce((prev, next) => {
return prev[next];
}, vm.$data);
}
get() {
Dep.target = this;
let value = this.getVal(this.vm,this.expr); // 会调用属性对应的get方法
Dep.target = null;
return value;
}
// 对外暴露的方法,如果值改变就可以调用这个方法来更新
update() {
let newValue = this.getVal(this.vm, this.expr);
let oldValue = this.value;
this.value = newValue;
if (newValue != oldValue) {
this.cb(newValue); // 对应watch的callback
}
}
}
参考文章:
看完这篇关于MVVM的文章,面试通过率提升了80%
属于自己的文字,理解,观点,欢迎交流