MVVM基本原理
MVVM(Model-View-ViewModel)本质上就是MVC 的改进版,MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。
MVVM相比与MVC模式主要是分离了试图和模型,所有的交互都通过ViewModel进行了数据的通知与交互,从而达到了低耦合的优点,View的修改可独立于Model的修改,可提高重用性,可在View中重用独立的视图逻辑。归根结底可总结为数据的双向绑定的过程,即ViewModel的数据与Model的绑定,ViewModel的数据与View中的数据绑定,从而达到数据低耦合高重用性的过程。
MVVM的原理的基础
MVVM的基础模式其实不复杂,通过该原理主要就是需要实现ViewModel层到Model层与View层的交互过程。在Vue的官方示例图中,给出了如下图示;
从图示所致,DOM就是视图,Model就是JS对象,Vue就是作为ViewModel存在将双向的数据进行绑定,那如何来实现一个简单的双向数据绑定呢?
MVVM的流程梳理
根据js的相关内容来实现的思考,基本流程如下;
大致的思路逻辑如上所示,其中在Model修改数据的时候主要就是通过addEventListener事件来注册回调函数,ViewModel到View的事件或者View到ViewModel的事件主要就是通过Object.defineProperty属性来设置值。
假如数据格式如下;
视图(View)
<input v-model="c" type="text">
MVVM框架(ViewModel)
将视图的内容和数据当做参数传入Mvvm中生成一个实例
数据(Model)
{c: 2}
MVVM实例初始化过程
- 首先,根据传入的Model,调用Object.defineProperty来劫持数据,并设置set和get方法,并注册回调watch更新视图函数
- 通过传入的视图信息编译初始化一个视图,并根据传入的编译后的视图添加监听函数,监听html中输入输入的响应事件,如果触发则回调设置到Mvvm中的对应的实例值
- 如果是通过其他事件修改了Model的值则执行注册的watch方法,重新生成对应的视图内容,渲染出新的页面值,从而完成从数据到视图的更新
以上,就基本是MVVM框架中从Model层到View层,和View层到Model层的数据的交流的概述流程。
实现简易的MVVM框架
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<h1>{{singer}}</h1> <!-- 模板渲染 -->
<h1>{{song.first}}</h1> <!-- 递归嵌套 -->
<input v-model="singer" type="text"> <!-- 双向绑定 -->
</div>
</body>
<script >
function Mvvm(options = {}) {
// vm.$options Vue上是将所有属性挂载到上面
this.$options = options;
let data = this._data = this.$options.data;
this.init() // 初始化设置熟悉
// 数据劫持
observe(data);
new Compile(options.el, this) // 编译内容
}
// Dep 订阅发布模式实现类
function Dep() {
// 一个数组(存放函数的事件池)
this.subs = [];
}
Dep.prototype.addSub = function(sub){
this.subs.push(sub);
},
Dep.prototype.notify = function(val) {
// 绑定的方法,都有一个update方法
this.subs.forEach(sub => sub.update(val));
}
// 数据劫持
function observe(data){
// 如果传入为空或者不是object则返回
if (!data || typeof data !== "object"){
return
}
for (let key in data){
let dep = new Dep()
let val = data[key]
observe(val) // 嵌套调用
Object.defineProperty(data, key, {
configurable: true,
get() {
// 通过该函数注册回调函数到dep
if (Dep.target){
dep.addSub(Dep.target)
}
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
// 更新回调视图
val = newVal;
dep.notify(newVal)
}
})
}
}
Mvvm.prototype = {
init() {
for (let key in this._data){
Object.defineProperty(this, key, {
configurable: true,
get() {
return this._data[key];
},
set(newVal) {
this._data[key] = newVal;
}
})
}
}
}
function Compile(el, vm){
vm.$el = document.querySelector(el);
// 在el范围里将内容都拿到,当然不能一个一个的拿
let fragment = document.createDocumentFragment();
while (child = vm.$el.firstChild) {
fragment.appendChild(child); // 此时将el中的内容放入内存中
}
// 对el里面的内容进行替换
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent;
let reg = /\{\{(.*?)\}\}/g; // 正则匹配{{}}
if (node.nodeType === 3 && reg.test(txt)) { // 即是文本节点又有大括号的情况{{}}
let arr = RegExp.$1.split('.'); // 匹配到的第一个分组 如: a.b, c
let val = vm;
arr.forEach(key => {
val = val[key];
});
let w = new Watcher(vm, RegExp.$1, function(newVal) {
node.textContent = newVal; // 当watcher触发时会自动将内容放进输入框中
});
node.textContent = txt.replace(reg, val).trim();
}
if (node.nodeType == 1) {
// 检查是否是v-model属性值
let nodeAttr = node.attributes
Array.from(nodeAttr).forEach(attr => {
let name = attr.name; // v-model type
let exp = attr.value; // data key
if (name == "v-model"){
node.value = vm[exp]
let w = new Watcher(vm, exp, function(newVal) {
node.value = vm[exp]; // 当watcher触发时会自动将内容放进输入框中
});
}
node.addEventListener('input', e => {
let newVal = e.target.value;
// 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
vm[exp] = newVal;
});
})
}
if (node.childNodes && node.childNodes.length) {
replace(node);
}
});
}
replace(fragment); // 替换内容
vm.$el.appendChild(fragment); // 再将文档碎片放入el中
}
// 监听函数
function Watcher(vm, exp, fn) {
this.vm = vm
this.exp = exp
this.fn = fn; // 将fn放到实例上
let arr = exp.split('.'); // 检查是否是深层次递归嵌套
let data = this.vm._data
if (arr.length > 1){
let length = arr.length
for(i=0;i<length-1;i++){
data = data[arr[i]]
}
// 只注册最后一个回调函数
Dep.target = this
const value = data[arr[length-1]]
} else {
Dep.target = this
const value = data[exp]
}
Dep.target = null
}
Watcher.prototype.update = function(val){
this.fn(val)
};
let app = new Mvvm({el: "#app",data: {"singer": "singer1", "song": {"first": "contry", "second": "home"}}})
</script>
</html>
该段代码运行在浏览器之后,在输入框中输入数据,可看到第一行的数据也会跟着改变,此时打开浏览器调试窗口的终端,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5SBlO8AR-1578382678865)(/Users/wuzi/Desktop/blog/mvvm_test.png)]
从终端打印信息可看出,基本都是实现了set get方法,并且会根据数据的改变从而改变视图。
本段代码参考了网上一些实现的代码,并做了修改,代码的实现思路跟上图中的流程图相似。在实现的过程中也借用了闭包的原理,为每一个属性值在defineProperty的时候,创建一个订阅数组dep,然后当数据有更改之后,就通知所有订阅该dep的所有接受着,这样确保每次的数据修改都只修改到订阅的局部的订阅者。
主要做的两件事情就是将数据进行双向的绑定,具体的实现大家可自行深入学习,本文的示例代码基于网上实例进行修改,原文来自不好意思!耽误你的十分钟,让MVVM原理还给你。
总结
本文主要就是简单的探究了一下MVVM框架的基本原理,仅仅是作为在使用vue框架的时候的基础回顾,原理并不算复杂,主要就是通过数据的双向绑定从而提高开发效率与代码复用率。由于本人才疏学浅,如有错误请批评指正。