MVC
在 MVC 框架中,视图、控制器和模型是三个独立的组件。视图负责渲染数据以及用户界面的交互,控制器处理用户输入并更新模型,模型存储数据并提供对数据的访问方法。这些组件之间的通信是通过事件或直接调用方法实现的。
// Model
class Model {
constructor() {
this.data = '';
}
setData(data) {
this.data = data;
}
getData() {
return this.data;
}
}
// View
class View {
constructor() {
this.element = document.getElementById('output');
}
render(data) {
this.element.innerHTML = data;
}
}
// Controller
class Controller {
constructor(model, view) {
this.model = model;
this.view = view;
}
updateData(newData) {
this.model.setData(newData);
this.renderData();
}
renderData() {
const data = this.model.getData();
this.view.render(data);
}
}
// Usage
const model = new Model();
const view = new View();
const controller = new Controller(model, view);
controller.updateData('Hello, MVC!');
MVVM
Model(模型)负责管理和存储数据,View(视图)负责呈现用户界面,而ViewModel(视图模型)则是连接模型和视图之间的桥梁。ViewModel通过监听模型的变化并更新视图,同时也提供了一些用于处理用户交互的方法。
MVVM 框架通常采用数据驱动的方式,即数据的改变会自动反映在视图上,减少了手动操作和处理的复杂性。与 MVC 框架相比,MVVM 框架更加注重数据的状态管理和数据绑定的能力,使得开发者能够更专注于数据和业务逻辑的处理,提高了代码的可维护性和可测试性。
// Model
class Model {
constructor() {
this.data = '';
}
setData(data) {
this.data = data;
}
getData() {
return this.data;
}
}
// 视图模型(ViewModel)
class ViewModel {
constructor(model) {
this.model = model;
this.data = '';
}
updateData(data) {
this.data = data;
}
handleClick() {
const newData = 'Updated data';
this.model.setData(newData);
}
}
// 视图(View)
class View {
constructor(viewModel) {
this.viewModel = viewModel;
const button = document.getElementById('button');
button.addEventListener('click', () => {
this.viewModel.handleClick();
});
}
render() {
console.log(`Rendering view with data: ${this.viewModel.data}`);
}
}
const model = new Model();
const viewModel = new ViewModel(model);
const view = new View(viewModel);
viewModel.updateData(model.getData());
view.render();
MVC中的Controller和MVVM的ViewModel的区别是什么呢?
以vue为例, 个人认为,Controller是需要用户(指开发)直接去修改页面和修改数据的,如同通过ref获取到指定元素,并直接将新的数据赋值给该元素以及更新data中的数据。
MVVM只需要去更新data中的数据即可,data中的数据由于是响应式数据(被监听了),所以,后续的视图更新和数据更新都是由框架内部完成,不需要手动修改。
而对于vue而言,它是怎么做到MVVM的呢?
对于vue而言,它只需要在框架内部实现对data的监听,然后将data同步更新到页面上,不需要再去主动更新数据即可。而实现Vue中的MVVM模型需要四个大类,分别是:
- 模板编译
- 数据劫持
- watcher监听
- 发布订阅
它们之间的关系如下:
数据实现响应式的过程是在初始化时实现的,而具体的实现过程和这四个大类的创建以及相互之间的调度息息相关,所以,我根据源码demo画了以下时序图:
如果对上面的流程还是一知半解,接下来,看具体的代码实现:
首先创建MVVM实例,将模板和data作为参数传入
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
}
})
})
}
}
数据劫持
- 通过深度遍历,使用Object.defineProperty对整个$data以及每一层进行劫持
- 对每一个key劫持过程中,会为这个key创建一个调度中心(Dependency)
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){ // 当给data属性中设置值的适合 更改获取的属性的值
if(newValue!=value){
// 这里的this不是实例
that.observe(newValue);// 如果是设置的是对象继续劫持
value = newValue;
dep.notify(); // 通知所有人 数据更新了
}
}
});
}
}
模板编译
模板编译的过程分为两个部分:
- 解析变量并将初始值赋值到模板中并渲染:
- 将模板复制到内存中
- 深度遍历模板并对每一个节点进行解析,直到解析到变量
- 将变量解析后,并获取$data中的变量对应的值,将初始值替换模板中的变量
- 将包含初始值的模板解析渲染到页面上
- 给每一个包含变量的元素创建一个wathcer对象,并添加到该变量的dependecy中,当变量发生改变时,则触发wathcer的update方法,对该元素进行重新编译
这里用代码展示了解析到了文本节点,如{{meassage.a}}时,将会创建一个属于该元素wathcer实例
// node是当前节点,vm是整个mvvm实例,expression是文本:{{message.a}}
text(node, vm, expr) { // 文本处理
let updateFn = this.updater['textUpdater'];
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)
},
watcher监听
- 首次创建wathcer实例时,它会将自身放到公共作用域上,并通过调用该wathcer所对应的变量(如message.a)的get方法,触发数据劫持,并将watcher自身添加到dependecy中;
- 当watcher的update方法被调起时,它会比对新旧两值的区别,并且将最新的值赋值给模板元素
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(){
// 在取值前先将watcher保存到Dep上
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;
if(newValue != oldValue){
this.cb(newValue); // 对应watch的callback
}
}
}
发布订阅
上面已经说完了收集过程,那什么时候触发更新呢?当修改一个变量时,通过数据劫持触发set方法,set方法会比对新旧值是否一致,不一致的前提下,调用调度中心的所有wathcer的update方法进行更新。
class Dep{
constructor(){
// 订阅的数组
this.subs = []
}
addSub(watcher){
this.subs.push(watcher);
}
notify(){
this.subs.forEach(watcher=>watcher.update());
}
}
总而言之,本文主要是介绍了MVC和MVVM的区别,这个区别就是MVVM不需要开发者手动去更新视图,更新视图这个功能交给了框架底层去实现。