<!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>手动实现MVVM</title>
</head>
<body>
<div id="app">
<input type="text" v-model="name">
<input type="text" v-model="age">
<h1>{{name}}</h1>
<h2>{{age}}</h2>
</div>
</body>
</html>
<script>
// 实现逻辑
// @1 定义方法 获取参数
function Vue(option) {
// @2 私有属性挂载 $el ,$data
this.$el = document.querySelector(option.el);
this.$data = option.data;
observe(this.$data);// @3 数据劫持
nodeToFragment(this.$el, this);// 负责模板编译 将Vue 语法变成真实变量 @4
};
/** 数据劫持部分*/
function observe(data) {
// @3 判断是否是对象 1
if (({}).toString.call(data) !== '[object Object]') return;
// 2 获取所有属性名,方便劫持
let keys = Object.keys(data);
keys.forEach(key => {
defineReactive$$1(data, key, data[key])
})
};
function defineReactive$$1(target, key, val) {
// 实现数据劫持 @3
let dep = new Dep(); // @6 每个key 都有自己的订阅器 Dep
Object.defineProperty(target, key, {
enumerable: true,//是否可枚举
get() {
// console.log('get', val)
if (Dep.target) {
// @7 这里加判断是为了保证,只有Watcher 订阅才会触发
dep.addSub(Dep.target); // 事件池中的订阅器
}
return val // 返回值
},
set(newVal) {
if (newVal !== val) {
// console.log(newVal, 'newVal====')
val = newVal; // 设置的新值
dep.notify(); // 通知事件执行
}
}
})
}
/** 模板编译部分*/
function nodeToFragment(el, vm) {
// 文档节点转到 文档碎片上 @4
let fragment = document.createDocumentFragment(); //创建文档碎片
let child; // 每一个节点
while (child = el.firstChild) {
/** xx.appendChild: 将一个节点附加到指定父节点的子节点列表的末尾处,如果该文本节点已存在则覆盖
* xx.firstChild : 只读属性返回树中节点的第一个子节点,如果节点无子节点,则返回 null。
*/
// debugger;
compile(child, vm); // 实现将每个模板都编译 @5
fragment.appendChild(child); //将节点 移到文档碎片
}
el.appendChild(fragment); // 再将文档碎片的内容 一个一个移到接节点
}
function compile(node, vm) {
const { $data } = vm;
// 编译node节点 @5 需要判断节点类型
/**
* 1.区分文本节点还是元素节点,元素节点需要考虑 行内属性和其子节点
* 2. 如果是文本节点 直接替换 节点类型 : 1:元素节点(属性nodeType) / 3:文本节点 / 8: 注释节点 /9:根节点
* 3. node.attributes : 属性返回该元素所有属性节点的一个实时集合,返回值是对象不是数组,无法使用数组方法
* 4 node.childNodes 返回包含指定节点的子节点的集合
*/
switch (node.nodeType) {
case 1:
// 元素节点
let attrs = node.attributes; // 获取所有行内属性 type,v-model ...
// debugger;
[...attrs].forEach(e => {
// 获取到 v-xxx 行内属性 以及 v-xxx 对应的 值 xxx
if (/^v-/.test(e.nodeName)) {
// v- 属性 ,
let vName = e.nodeValue;//对应的词汇
let val = $data[vName]; // 拿到 data对应的 key对应的值
new Watcher(node, vName, vm); // @6
node.value = val;
node.addEventListener('input', (e) => { // input 事件 绑定 @8
vm.$data[vName] = e.target.value
})
}
});
// 如果存在子节点
[...node.childNodes].forEach(x => {
compile(x, vm)
})
break;
case 3:
// 获取对应文本节点 ,将文本里的小胡子 转为 变量对应的 值
let str = node.textContent;
// 获取文本
if (/\{\{(\w+)\}\}/.test(str)) {
// 存在 小胡子语法
str = str.replace(/\{\{(\w+)\}\}/, (a, b) => { // 内容替换
new Watcher(node, b, vm); // @6
return $data[b]
})
node.textContent = str;
};
break;
case 8:
// ...
break;
case 9:
// ...
break;
}
}
// 观察订阅器@6
/**
* 1 创造订阅器
*/
class Dep {
constructor () {
this.subs = []; // 事件池
};
addSub(sub) {
this.subs.push(sub); // 添加事件
}
notify() {
//
this.subs.forEach(subs => {
// 负责通知 各个事件 ,subs 是订阅者实例
subs.updated()
})
}
}
/**
* 1 创造订阅者
*/
class Watcher {
constructor (node, key, vm) {
Dep.target = this;
this.node = node;
this.key = key;
this.vm = vm;
this.getValue(); // 把当前Watcher 放进对应的事件池
Dep.target = null;
};
getValue() {
this.value = this.vm.$data[this.key]; //触发 get
};
updated() {
// 负责更新DOM
this.getValue(); // 获取新的Value值
switch (this.node.nodeType) {
case 1:
// 元素节点 input 的情况下
this.node.value = this.value;
break;
case 3:
this.node.textContent = this.value;
break;
case 8:
// ...
break;
case 9:
// ...
break;
}
}
}
</script>
<script>
// 调用
let vm = new Vue({
el: '#app',
data: {
name: '手写mvvm',
age: 18
}
})
</script>
简单手动实现 Vue2 MVVM 过程
最新推荐文章于 2024-07-09 23:44:56 发布