手写MVVM框架
创建MVVM.html
<body>
<div id="app">
<input type="text" v-model="message" />
{{message}}
</div>
<script src="./compile.js"></script>
<script src="./MVVM.js"></script>
<script>
// 数据一般都挂在在VM上
// vue中通过 Object.defineProperty 方法 给每一个数据添加get和set
// vue中实现双向绑定 1. 模板的编译 2. 数据劫持(观察数据变换) 3.Watcher
let vm = new MVVM({
el: "#app",
data: {
message: "hello wzt",
},
});
</script>
</body>
创建MVVM.js
class MVVM {
constructor(options) {
// 首先 先把可用的东西挂载在实例上
this.$el = options.el;
this.$data = optins.data;
// 通过判断 如果有需要编译的模板就开始编译
if (this.$el) {
// 用数据和元素进行编译
new Compile(this.$el, this);
}
}
}
创建compile.js(1)
class Compile {
constructor(el, vm) {
// 将参数放置在实例上
this.el = this.isElementNode(el) ? el : document.querySelector(el); // #app document
this.vm = vm;
if (this.el) {
// 如果这个元素能获取到 才开始进行编译
// 1.先把真实的DOM移入到内存中 fragment
let fragment = this.node2fragment(this.el); // 将div id为app的所有内容放入内存中
// 2.编译 => 提取想要的元素节点 v-model 和文本节点{{}}
// 3.把编译好的fragment再塞回到页面中
}
}
// 写一些辅助的方法
// 判断是否是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
// 核心的方法
node2fragment(el) {
//需要将el中的内容全部放到内存中
// 文档碎片 内存中的dom节点
let fragment = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
fragment.appendChild(firstChild);
}
return fragment; //内存中的节点
}
}
创建compile.js(2)
- 完成判断是否是元素节点,div id为app (创建了 isElementNode()函数)
- 如果是元素节点,将真实的DOM移入到内存中 (创建了 node2fragment()函数)
- 进而进行编译,从而提取想要的元素节点 (创建了compile()函数 )
- 将编译对象分为两类,一种是元素,一种是文本
- 编译元素 (创建了 compileElement()函数 )
- 编译文本(创建了 compileText()函数 )
- 将编译对象分为两类,一种是元素,一种是文本
class Compile {
constructor(el, vm) {
// 将参数放置在实例上
this.el = this.isElementNode(el) ? el : document.querySelector(el); // #app document
this.vm = vm;
if (this.el) {
// 如果这个元素能获取到 才开始进行编译
// 1.先把真实的DOM移入到内存中 fragment
let fragment = this.node2fragment(this.el); // 将div id为app的所有内容放入内存中
// 2.编译 => 提取想要的元素节点 v-model 和文本节点{{}}
this.compile(fragment);
// 3.把编译好的fragment再塞回到页面中
}
}
/*
写一些辅助的方法
*/
// 判断是否是元素节点
isElementNode(node) {
return node.nodeType === 1;
}
/*
写一些核心的方法
*/
/* 将节点放置到内存 fragment */
node2fragment(el) {
//需要将el中的内容全部放到内存中
// 文档碎片 内存中的dom节点
let fragment = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
fragment.appendChild(firstChild);
}
return fragment; //内存中的节点
}
/* 编译方法 */
compile(fragment) {
// 需要递归
let childNodes = fragment.childNodes;
// console.log(childNodes);
// 类数组格式需要转换成数组
Array.from(childNodes).forEach((node) => {
if (this.isElementNode(node)) {
// 是元素节点,还需要继续深入检查 使用递归再次调compile()
console.log("element", node);
// 这里需要编译元素方法
// this.compileElement(node);
this.compile(node);
} else {
// 是文本节点
console.log("text", node);
// 这里需要编译文本方法
// this.compileText(node);
}
});
}
}
- 编译元素方法
/* 编译元素 */
compileElement(node) {
// 带v-model的 v-text ...
let attrs = node.attributes; // 取出当前节点的属性
// console.log(attrs);
Array.from(attrs).forEach((attr) => {
// console.log(attr); //查看属性 v-model="message"
// 判断属性名字是否包含v-
let attrName = attr.name;
if (this.isDirective(attrName)) {
// 取到对应的值放到节点中
let expr = attr.value;
//node this.vm.$data expr
//todo ......................后续完善
}
});
}
判断是否是指令
/* 判断是否是指令 */
isDirective(name) {
// 判断是否包含'v-'
return name.includes("v-");
}
注:node.attributes取出当前节点的属性; node.textContent取文本中的内容
- 编译文本方法
/* 编译文本 */
compileText(node) {
// 带{{}}的
let expr = node.textContent; //取文本中的内容
console.log(expr);
let reg = /\{\{([^}]+)\}\}/g; //正则
if (reg.test(expr)) {
//node this.vm.$data expr
//todo .......................后续完善
}
}
- 编译工具
/* 编译工具 */
CompileUtil = {
text(node, vm, expr) {
// 文本处理
},
model(node, vm, expr) {
// 输入框处理
},
updater: {
// 文本更新
textUpdater(node, value) {
node.textContent = value;
},
// 输入框更新
modelUpdater(node, value) {
node.value = value;
},
},
};
至此编译元素和编译方法中的todo替换如下:
编译元素
// v-model 去除V- 留下model两种方法
// let type = attrName.slice(2); //法一:从第二个元素开始取
let [, type] = attrName.split("-"); //法二: 解构赋值
console.log(type);
//node this.vm.$data expr
CompileUtil[type](node, this.vm, expr);
}
编译文本
CompileUtil["text"](node, this.vm, expr);
上下两部分代码解耦,现在不需要修改上面的代码,只需要在编译工具CompileUtil中完善即可
完成初步创建compile.js
class Compile {
constructor(el, vm) {
// 将参数放置在实例上
this.el = this.isElementNode(el) ? el : document.querySelector(el); // #app document
this.vm = vm;
if (this.el) {
// 如果这个元素能获取到 才开始进行编译
// 1.先把真实的DOM移入到内存中 fragment
let fragment = this.node2fragment(this.el); // 将div id为app的所有内容放入内存中
// 2.编译 => 提取想要的元素节点 v-model 和文本节点{{}}
this.compile(fragment);
// 3.把编译好的fragment再塞回到页面中
this.el.appendChild(fragment);
}
}
/*
写一些辅助的方法
*/
/* 判断是否是元素节点 */
isElementNode(node) {
return node.nodeType === 1;
}
/* 判断是否是指令 */
isDirective(name) {
// 判断是否包含'v-'
return name.includes("v-");
}
/*
写一些核心的方法
*/
/* 将节点放置到内存 fragment */
node2fragment(el) {
//需要将el中的内容全部放到内存中
// 文档碎片 内存中的dom节点
let fragment = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
fragment.appendChild(firstChild);
}
return fragment; //内存中的节点
}
/* 编译方法 */
compile(fragment) {
// 需要递归
let childNodes = fragment.childNodes;
// console.log(childNodes);
// 类数组格式需要转换成数组
Array.from(childNodes).forEach((node) => {
if (this.isElementNode(node)) {
// 是元素节点,还需要继续深入检查 使用递归再次调compile()
// console.log("element", node);
// 这里需要编译元素方法
this.compileElement(node);
this.compile(node);
} else {
// 是文本节点
// console.log("text", node);
// 这里需要编译文本方法
this.compileText(node);
}
});
}
/* 编译元素 */
compileElement(node) {
// 带v-model的 v-text ...
let attrs = node.attributes; // 取出当前节点的属性
// console.log(attrs);
Array.from(attrs).forEach((attr) => {
// console.log(attr); //查看属性 v-model="message"
// 判断属性名字是否包含v-
let attrName = attr.name;
if (this.isDirective(attrName)) {
// 取到对应的值放到节点中
let expr = attr.value;
// v-model 去除V- 留下model两种方法
// let type = attrName.slice(2); //法一:从第二个元素开始取
let [, type] = attrName.split("-"); //法二: 解构赋值
console.log(type);
//node this.vm.$data expr
CompileUtil[type](node, this.vm, expr);
}
});
}
/* 编译文本 */
compileText(node) {
// 带{{}}的
let expr = node.textContent; //取文本中的内容
// console.log(expr);
let reg = /\{\{([^}]+)\}\}/g; //正则
if (reg.test(expr)) {
//node this.vm.$data expr
CompileUtil["text"](node, this.vm, expr);
}
}
}
/* 编译工具 */
CompileUtil = {
getVal(vm, expr) {
// 获取实例上对应的数据
const exprArr = expr.split("."); // [a,v,c,s]
const value = exprArr.reduce((prev, next) => {
// vm.$data.a.v... 将vm.$data指定为prev
return prev[next];
}, vm.$data);
return value;
},
getTextVal(vm, expr) {
//获取编译文本后的结果
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
return this.getVal(vm, arguments[1]);
});
},
text(node, vm, expr) {
// 文本处理
let updateFn = this.updater["textUpdater"];
// {{message.a}} => hello wzt
let value = this.getTextVal(vm, expr);
updateFn && updateFn(node, value);
},
model(node, vm, expr) {
// 输入框处理
let updateFn = this.updater["modelUpdater"];
updateFn && updateFn(node, this.getVal(vm, expr));
},
updater: {
// 文本更新
textUpdater(node, value) {
node.textContent = value;
},
// 输入框更新
modelUpdater(node, value) {
node.value = value;
},
},
};
接下来配置数据劫持 observer.js
配置MVVM.js
新增 new Observer(this.$data);
class MVVM {
constructor(options) {
// 首先 先把可用的东西挂载在实例上
this.$el = options.el;
this.$data = options.data;
// 通过判断 如果有需要编译的模板就开始编译
if (this.$el) {
//数据劫持 就是把对想的所有属性 改成 get和 set方法
new Observer(this.$data);
// 用数据和元素进行编译
new Compile(this.$el, this);
}
}
}
配置observer.js
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
// 要对这个data数据将原有的属性改成set和get的形式
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;
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 当取值是调用的方法
return value;
},
set(newValue) {
// 当给data属性中设置值的时候 更改获取的属性的值
if (newValue != value) {
that.observe(newValue); //如果是对象继续劫持
value = newValue;
}
},
});
}
}
修改compile.js
text(node, vm, expr) {
// 文本处理
let updateFn = this.updater["textUpdater"];
// {{message.a}} => hello wzt
let value = this.getTextVal(vm, expr);
//{{a}} {{b}}
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
new Watcher(vm, arguments[1], (newValue) => {
//如果数据变化了, 文本节点需要重新获取依赖的属性更新文本中的内容
updateFn && updateFn(node, this.getTextVal());
});
});
updateFn && updateFn(node, value);
},
model(node, vm, expr) {
// 输入框处理
let updateFn = this.updater["modelUpdater"];
// 这里应该加一个监控,数据变化了 因该调用这个Watch的callback
new Watcher(vm, expr, (newValue) => {
// 当值变化后会调用Cb将新的值传递过来
updateFn && updateFn(node, this.getVal(vm, expr));
});
updateFn && updateFn(node, this.getVal(vm, expr));
},
完整代码
MVVM.html
<!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">
<input type="text" v-model="message.a">
<div>{{message.a}} {{b}}</div>
<ul><li></li></ul>
{{message.a}}
</div>
</div>
<script src="./watcher.js"></script>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./MVVM.js"></script>
<script>
// 数据一般都挂在在VM上
// vue中通过 Object.defineProperty 方法 给每一个数据添加get和set
// vue中实现双向绑定 1. 模板的编译 2. 数据劫持(观察数据变换) 3.Watcher
let vm = new MVVM({
el: "#app",
data: {
message: {
a: "hello wzt",
},
b: 1,
},
});
</script>
</body>
</html>
MVVM.js
class MVVM {
constructor(options) {
// 首先 先把可用的东西挂载在实例上
this.$el = options.el;
this.$data = options.data;
// 通过判断 如果有需要编译的模板就开始编译
if (this.$el) {
//数据劫持 就是把对想的所有属性 改成 get和 set方法
new Observer(this.$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) {
// 将参数放置在实例上
this.el = this.isElementNode(el) ? el : document.querySelector(el); // #app document
this.vm = vm;
if (this.el) {
// 如果这个元素能获取到 才开始进行编译
// 1.先把真实的DOM移入到内存中 fragment
let fragment = this.node2fragment(this.el); // 将div id为app的所有内容放入内存中
// 2.编译 => 提取想要的元素节点 v-model 和文本节点{{}}
this.compile(fragment);
// 3.把编译好的fragment再塞回到页面中
this.el.appendChild(fragment);
}
}
/*
写一些辅助的方法
*/
/* 判断是否是元素节点 */
isElementNode(node) {
return node.nodeType === 1;
}
/* 判断是否是指令 */
isDirective(name) {
// 判断是否包含'v-'
return name.includes("v-");
}
/*
写一些核心的方法
*/
/* 将节点放置到内存 fragment */
node2fragment(el) {
//需要将el中的内容全部放到内存中
// 文档碎片 内存中的dom节点
let fragment = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
fragment.appendChild(firstChild);
}
return fragment; //内存中的节点
}
/* 编译方法 */
compile(fragment) {
// 需要递归
let childNodes = fragment.childNodes;
// console.log(childNodes);
// 类数组格式需要转换成数组
Array.from(childNodes).forEach((node) => {
if (this.isElementNode(node)) {
// 是元素节点,还需要继续深入检查 使用递归再次调compile()
// console.log("element", node);
// 这里需要编译元素方法
this.compileElement(node);
this.compile(node);
} else {
// 是文本节点
// console.log("text", node);
// 这里需要编译文本方法
this.compileText(node);
}
});
}
/* 编译元素 */
compileElement(node) {
// 带v-model的 v-text ...
let attrs = node.attributes; // 取出当前节点的属性
// console.log(attrs);
Array.from(attrs).forEach((attr) => {
// console.log(attr); //查看属性 v-model="message"
// 判断属性名字是否包含v-
let attrName = attr.name;
if (this.isDirective(attrName)) {
// 取到对应的值放到节点中
let expr = attr.value;
// v-model 去除V- 留下model两种方法
// let type = attrName.slice(2); //法一:从第二个元素开始取
let [, type] = attrName.split("-"); //法二: 解构赋值
console.log(type);
//node this.vm.$data expr
CompileUtil[type](node, this.vm, expr);
}
});
}
/* 编译文本 */
compileText(node) {
// 带{{}}的
let expr = node.textContent; //取文本中的内容
// console.log(expr);
let reg = /\{\{([^}]+)\}\}/g; //正则
if (reg.test(expr)) {
//node this.vm.$data expr
CompileUtil["text"](node, this.vm, expr);
}
}
}
/* 编译工具 */
CompileUtil = {
getVal(vm, expr) {
// 获取实例上对应的数据
const exprArr = expr.split("."); // [a,v,c,s]
const value = exprArr.reduce((prev, next) => {
// vm.$data.a.v... 将vm.$data指定为prev
return prev[next];
}, vm.$data);
return value;
},
getTextVal(vm, expr) {
//获取编译文本后的结果
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
// console.log(arguments[0]); //{{message.a}} {{b}}
// console.log(arguments[1]); //message.a b
return this.getVal(vm, arguments[1]);
});
},
text(node, vm, expr) {
// 文本处理
let updateFn = this.updater["textUpdater"];
// {{message.a}} => hello wzt
let value = this.getTextVal(vm, expr);
//{{a}} {{b}}
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
new Watcher(vm, arguments[1], (newValue) => {
//如果数据变化了, 文本节点需要重新获取依赖的属性更新文本中的内容
updateFn && updateFn(node, this.getTextVal(vm, expr));
});
});
updateFn && updateFn(node, value);
},
setVal(vm, expr, value) {
//[message,a]
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 Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
// 要对这个data数据将原有的属性改成set和get的形式
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) {
// 当给data属性中设置值的时候 更改获取的属性的值
if (newValue != value) {
that.observe(newValue); //如果是对象继续劫持
value = newValue;
dep.notify(); //通知所有人数据更新了
}
},
});
}
}
class Dep {
constructor() {
// 订阅的数组
this.subs = [];
}
addSub(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach((watcher) => watcher.update());
}
}
watcher.js
// 观察者的目的就是给需要变化的那个元素增加一个观察者, 当数据变化后执行对应的方法;
class Watcher {
//cb为回调函数
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先获取老的值
this.value = this.get();
}
getVal(vm, expr) {
// 获取实例上对应的数据
const exprArr = expr.split("."); // [a,v,c,s]
const value = exprArr.reduce((prev, next) => {
// vm.$data.a.v... 将vm.$data指定为prev
return prev[next];
}, vm.$data);
return value;
}
get() {
Dep.target = this;
let value = this.getVal(this.vm, this.expr);
Dep.target = null;
return value;
}
// 对外暴露的方法
update() {
let newValue = this.getVal(this.vm, this.expr);
let oldValue = this.value;
if (newValue !== oldValue) {
this.cb(newValue); //对应watch的callback
}
}
}
// 用新值和老值进行比对 如果发生变化 就调用更新方法