MVVM的框架原理
-
数据劫持
-
发布订阅模式
实现原理的过程
- 遍历data选项中的属性,添加数据的观测,执行observe的方法,使用Object.defineProperty方法转换为get和set方法,实现数据的劫持,并且添加一个compiler方法,对每个元素节点进行判断,如果是文本节点,根据指令模板去替换数据
- 当数据发生变化时,observe中的set方法被触发,会立即调用Dep.notify()方法,开始遍历所有的订阅者,调用执行者的Update方法,订阅者收到通知之后对视图进行更新
- v-model实现数据的双向绑定
在complier方法中对每个元素节点类型进行判断时,如果是标签节点,判断是否存在v-model指令,存在即创建一个订阅者,并且设置该标签节点的初始值,添加input事件监听,在输入框输入的值改变时,执行set,get方法,对视图进行更新
实现原理的代码
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
{{ message }}
<div>{{message}}</div>
<p>{{count}}</p>
<input type="text" v-model="message"/>
</div>
</div>
<!-- Vue CDN -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- MvvM js -->
<script src="./mvvm.js"></script>
<script>
var app = new MVVM({
el: '#app',
data: {
message: 'hello world',
count: 1
}
});
</script>
</body>
mvvm.js
// 发布者 publish
class Dep{
constructor(){
this.subs = [];
}
// 订阅方法
addSub(watcher){
this.subs.push(watcher);
}
// 遍历所有的订阅者,发布更新方法
notify(newVal){
this.subs.forEach(sub=>{
sub.update(newVal);
})
}
}
// 订阅者 subscribe 更新dom方法
class Watcher{
constructor(cb){
this.cb = cb;
}
update(newVal){
console.log('更新了....');
this.cb(newVal);
}
}
// 在数据劫持的时候,一个key劫持执行前,选new一个Dep
// new Dep()
// 第一次渲染前,先new一个watcher
// new Watcher()
// 第一次渲染的时候,添加订阅者
// addSub()
// 更新数据,执行发布
// notify()
let watch = null;
class MVVM{
constructor(options){
// 配置实例上的基础属性
this.$options = options;
this.$data = this._data = options.data;
// 添加数据观测
this.observer(this.$data);
// 编译模版
this.compiler(options.el);
}
// 数据观测
observer(data){
// 获得data的所有key和value
Object.entries(data).forEach(([key, value])=>{
// 创建发布者
console.log('发布者:', key);
const dep = new Dep();
// 对key进行数据观测 数据观测
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
// 设置数据
set(newVal){
// console.log('set run......');
if(newVal !== value){
// 设置新数据
value = newVal;
// 重新渲染dom
console.log('执行发布.....');
dep.notify(newVal);
}
},
// 读取数据
get(){
// console.log('get run......');
console.log('执行订阅.....');
if(watcher){
dep.addSub(watcher);
watcher = null;
}
return value;
}
});
})
}
// 编译模版
compiler(el){
// 获得实例作用的dom
const element = document.querySelector(el);
// 遍历实例作用的dom
this.compilerNode(element);
}
compilerNode(element){
// 获得需要编译的dom的每一个子节点
const childNodes = element.childNodes;
// 转为可遍历的对象,进行遍历
Array.from(childNodes).forEach(node=>{
const {nodeType, textContent} = node;
//判断是文本节点
if(nodeType === 3){
// 判断是否有插值表达式在文本节点中
let reg = /\{\{\s*(\S*)\s*\}\}/;
//有
if(reg.test(textContent)){
//数据变,dom需要更新
// 创建订阅者
console.log('订阅者:', RegExp.$1);
watcher = new Watcher((newVal)=>{
//更新数据的渲染
node.textContent = newVal;
});
// 第一次渲染dom
node.textContent = this.$data[RegExp.$1];
}
}
// 判断是标签
else if(nodeType === 1){
// 拿到标签的所有属性
let attrs = Array.from(node.attributes);
// 遍历每一属性
attrs.forEach(attr=>{
//判断属性是否是指令
if(attr.name.startsWith('v-')){
//是指令,取指令名字
let dirName = attr.name.substr(2);
if(dirName === 'model'){ //v-model="message"
let key = attr.value;
// 创建订阅者
watcher = new Watcher((newVal)=>{
node.value = newVal;
});
// 设置初始值
node.value = this.$data[key];
// 添加输入事件监听
node.addEventListener('input', (ev)=>{
this.$data[key] = ev.target.value;
});
}
else if(dirName === 'bind'){
}
}
})
}
// 有子节点,需要编译子节点
if(node.childNodes.length > 0){
// 遍历子节点
this.compilerNode(node);
}
})
}
}