likes
comments
collection
share

常说的MVC和MVVM架构是什么?用代码实现一下?

作者站长头像
站长
· 阅读数 6

相对于传统开发的区别?

MVC和MVVM,都是一种数据驱动视图的架构模型,相对于传统的面向过程式开发来说,它们是对代码规范的一种统一,方便团队进行开发。

如下俩段代码控制div是否显示:

<body>
    <div id="box">我显示的</div>
    <button id="btn">点击</button>
    
    <script>
        btn.onclick = function() {
            if(box.style.display === 'none') box.style.display = 'block';
            else box.style.display = 'none';
        }
    </script>
</body>

用数据驱动模型模型来写:

<body>
    <div id="box">我显示的</div>
    <button id="btn">点击</button>
    
    <script>
        let is_shown = true;
        function render(el, is_shown) {
            if(is_shown) el.style.display = 'block';
            else el.style.display = 'none';
        }
        btn.onclick = function() {
            is_shown = !is_shown;
            render(box, is_shown);
        }
    </script>
</body>

看起来好像多了几行代码,但是对于第二种代码来说,简单抽象封装了 render 函数,我们只需要修改 is_shownbool 值,而无需在意 render 函数内部的执行,就可以实现通过数据修改来驱动视图的更新。

MVC和MVVM?

  • MVC:Model(数据)、View(视图)、Controller(控制者)
    • 常见的MVC框架有:Angular.js、backbone...
  • MVVM:Model(数据)、View(视图)、View-Model(连接vm的桥梁)
    • 常见的MVVM框架有:vue.js、react.js...

1、mvc和mvvm都是一种设计思想。 主要就是mvc中Controller演变成mvvm中的viewModel。 mvvm主要解决了mvc中大量DOM操作使页面首次渲染性能降低,加载速度变慢的问题 。

  • 学过vue.js或者react.js的友友们就会知道,这种框架在渲染页面时会生成一个虚拟DOM树(把页面元素解析成ast抽象语法树,再转化成DocumentFragment)

  • 而我们使用框架时,实际上是在内存进行对虚拟dom的操作,最后再一次性渲染到文本文档中。

  • 这样的好处是浏览器在做初始渲染时只需一次对真实dom的操作。

常说的MVC和MVVM架构是什么?用代码实现一下?

2、MVVM与MVC最大的区别就是:它实现了View和Model的自动同步:当Model的属性改变时,我们不用再自己手动操作Dom元素来改变View的显示,它会自动变化。

  • MVC体现了面向对象编程的思维,而MVVM更是一种函数式编程的思维(后面文章会讲)
  • 对于MVC来讲,MVC操作的是真实dom,对于数据的更新需要找到对应抽象类来直接操作真实dom,这样的话,它无法完全将修改视图的操作完全封装成一个方法,然后做到修改数据直接调用该方法做到视图更新(比如上面的render函数的el参数,无法做到准确定位el参数),它会在不同方法中穿插对dom的操作
  • 而对于MVVM来讲,它操作的是虚拟dom、在数据的更新后,该框架重新生成一个虚拟dom树,与旧虚拟dom树进行比对,然后替换修改的地方,所以这里我们可以将渲染视图抽象成一个函数类
<div id="app">
   <p v-on:click="clickMes">{{mes}}</p>
</div>
  • 在上面的代码中,MVVM框架会遍历该模板生成虚拟dom,找到 v-on 等自定义属性并进行事件绑定,做到真正的视图和数据分离

3、整体看来,MVVM比MVC精简很多,我们不用再用选择器频繁地操作DOM。

MVVM并不是用VM完全取代了C,ViewModel存在目的在于抽离Controller中展示的业务逻辑,而不是替代Controller,其它视图操作业务等还是应该放在Controller中实现。

MVVM一定比MVC好吗?

从性能来说,其实非也!

对于页面首次渲染,MVVM框架可能会比MVC框架快一些,因为MVVM只会进行一次对真实dom的操作,而MVC可能会进行多次真实dom的操作

但是!在首屏渲染完毕后,用户开始对页面进行直接操作时,MVVM的性能肯定会输MVC的

  • 对于MVC构建的页面来说,用户修改数据,该框架会根据绑定的dom元素直接进行修改
  • 而对于MVVM构建的页面来说,用户修改数据,该框架会重新生成虚拟dom树与原树进行比对,再修改
  • 虽然可以进行diff(新旧虚拟dom树比对算法)优化,但是一个是直接操作,一个需要最少O(n)算法比对在进行真实dom操作。答案显而易见!vue和react官网介绍都没把虚拟dom当做框架的优势放上去

常说的MVC和MVVM架构是什么?用代码实现一下?

那MVVM究竟比MVC好在哪里?

对于程序常说的MVC和MVVM架构是什么?用代码实现一下?来讲,MVVM肯定是比MVC用的爽啊!开发效率又高,因为完全不需要考虑视图更新方面对dom树的操作,框架会自动响应绑定对视图的更新(框架使用Object.definePropertyproxy直接在数据修改时候自动调用 _render 函数进行更新)。

所以对于开发来说,用的爽才是好的,这也是为什么用 Vuereact 的人数比用 angular 的人数多的原因

简单实现MVC

MVC没啥好讲的,它其实也算不上一种模型,它只是对代码规范进行约束而已。

如下面简单实现一个点击 button 实现 div 中数字增长的效果。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>MVC Demo</title>
  </head>
  <body>
    <h1>MVC Demo</h1>
    <div id="counter">0</div>
    <button id="increment-btn">Increment</button>
    <script>
// Model
const model = {
  count: 0,
  incrementCount: function() {
    this.count++;
    return this.count;
  }
};

// View
const view = {
  updateCount: function(count) {
    const counter = document.getElementById('counter');
    counter.innerHTML = count;
  }
};

// Controller
const controller = {
  handleClick: function() {
    const newCount = model.incrementCount();
    view.updateCount(newCount);
  },
  init: function() {
    const button = document.getElementById('increment-btn');
    button.addEventListener('click', this.handleClick);
  }
};

controller.init();

    </script>
  </body>
</html>

简单实现MVVM

MVVM实现起来很复杂,只能模拟实现简单的功能,这里我们模拟实现一下 vue 框架。

我们使用 proxy 来进行响应式处理,对于 Object.definePropertyproxy碎片化文档 还有 设计模式 不熟的友友,可以查看一下文档再进行学习

Object.defineProperty() - JavaScript | MDN (mozilla.org)

Proxy - JavaScript | MDN (mozilla.org)

DocumentFragment - Web API 接口参考 | MDN (mozilla.org)

需要先了解以下算法:

实现模板字符串的替换?

//  实现一个 render(template, context) 方法,将 template 中占位符用 context 替换
var template = '{{ name }}现在{{ age }}岁'
var context = {name: 'lhy', age: 19}
console.log(render(template, context))

// 实现!直接用replace进行替换!
function render(template, context) {
    return template.replace(/{{(.*?)}}/g, (match, key) => context[key.trim()]) // match 匹配{{xxx}}, key匹配(.*?)
}

对于复杂的模板替换可以用 witheval 实现

  • 比如模板 {{ }} 里面是表达式的话!可以用 eval() 实现直接运算!
  • vue源码也是用了大量 with 进行模板解析
const data = {
    data: {
        name: 'hy',
        age: 19
    },
    school: 'cs'
};
// 对于vue来说,它传入的是一个object, 会出现以下情况
const template =
`{{ data.name }}今年{{ data.age < 17 ? '18': data.age }}岁了,在{{ school }}就读`;

/**
 * 要么用split('.')
 * @param {*} template 
 * @param {*} data
 */
function render(template, data) {
    with(data) {
        return template.replace(/{{([^}]*)}}/g, (macth, context) => {
            return eval(`${context}`)
        })
    }
}
console.log(render(template, data));

但是在实现的时候,发现使用with会报错,显示在严格模式下不让使用,不知道怎么修改

  • node代表元素节点,大概能看得懂吧!
  • 这个函数在一个 Compile 编译类中
compile_text(node) {

    function isVariable(variable) { // 判断是不是变量
        return !(variable === '' || parseInt(variable).toString() !== 'NaN' || (/[`'"]/).test(variable));
    }

    node.nodeValue = node.nodeValue.replace(/{{([^}]*)}}/g, (macth, context) => {
        context = context.trim();
        const exp = context.split(/[?:]/); // 三目运算符
        const value = exp[0].trim();
        // 变量,解决对于 data || age 这一类的,给data和age分别设置watcher
        let variables = value.split(/[^\w.'"`]/);
        variables.forEach(variable => {
            variable = variable.trim();
            if(isVariable(variable)) new Watcher(this.$vm, node, variable);
        })
        // 如果是三目运算符
        if(exp.length === 3) {
        // 运算表达式
            if(eval(`this.$vm._data.${exp[0]}`)) {
                if(isVariable(exp[1])) {
                    return eval(`this.$vm._data.${exp[1]}`);
                }
                return exp[1].trim().replace(/['"`]/g, '');
            } else {
                if(isVariable(exp[2])) {
                    return eval(`this.$vm._data.${exp[2]}`);
                }
                return exp[2].trim().replace(/['"`]/g, '');
            }
        }
        // 变量
        return eval(`this.$vm._data.${value}`)
    })
}

简单虚拟dom生成

下面参数 el 代表真实dom节点 ,比如 <div id="app">{{mes}}</div> 的 app 元素

// el 代表真实dom节点
nodeToFragment(el){ // 这里相当于生成虚拟dom(不是虚拟dom)
  let fragment = document.createDocumentFragment(); 
  // fragment 是一个指向空DocumentFragment对象的引用。是 DOM 节点。它们不是主 DOM 树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到 DOM 树。在 DOM 树中,文档片段被其所有的子元素所代替。
  // 因为文档片段存在于内存中,并不在 DOM 树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。
  let child;
  while (child = el.firstChild){
      // fragment.appendChild()具有移动性, 相当于把el中节点移动过去.nextElementSibling
      fragment.appendChild(child);//append相当于剪切的功能
  }
  return fragment;
}

把真实dom转化为类虚拟dom,然后在进行js模板修改(修改{{mes}}为文本)和操作

实现观察者模式

观察者和被观察者模式,vue的响应式原理也是通过这种模式实现绑定的。

// 请补全JavaScript代码,完成"Observer"、"Observerd"类实现观察者模式。要求如下:
// 1. 被观察者构造函数需要包含"name"属性和"state"属性且"state"初始值为"走路"
// 2. 被观察者创建"setObserver"函数用于保存观察者们
// 3. 被观察者创建"setState"函数用于设置该观察者"state"并且通知所有观察者
// 4. 观察者创建"update"函数用于被观察者进行消息通知,该函数需要打印(console.log)数据,数据格式为:小明正在走路。其中"小明"为被观察者的"name"属性,"走路"为被观察者的"state"属性
// 注意:
// 1. "Observer"为观察者,"Observerd"为被观察者
class Observerd { // 被观察者
    constructor(name) {
        this.name = name
        this.state = "走路"
        this.observers = [] // 观察者队列
    }
    setObserver(observer) {
        this.observers.push(observer)
    }
    setState(state) {
        this.state = state
        this.observers.forEach(observer => observer.update(this))
    }
}

class Observer {
    update(observerd) {
        console.log(`${observerd.name}正在${observerd.state}`)
    }
}

vue双向绑定的原理

MVVM(数据双向绑定)

职责:

  • 数据变化后更新视图
  • 视图变化后更新数据

组成:

  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

常说的MVC和MVVM架构是什么?用代码实现一下?

Vue数据双向绑定原理是通过数据劫持结合发布者-订阅者模式的方式来实现的,首先是对数据进行监听,然后当监听的属性发生变化时则告诉订阅者是否要更新,若更新就会执行对应的更新函数从而更新视图

  • Dep功能 :1.收集依赖,添加观察者(watcher) 2.通知所有观察者

编译html模板时,发现需要特殊处理的变量,比如v-model=“name”,这个name被发现以后,就准备为其创建watcher,在创建watcher的时候,先把这个watcher挂载到Dep.target这个全局静态变量上,然后触发一次get事件,这样就触发了get函数中的Dep.target && dep.addSub(Dep.target);,等get到了变量以后,也已经添加到subs队列里了,这时候在令Dep.target = null。

总结:vue先用observer劫持监听所有属性,当数据变化时,会触发setter,通知变化并调用Dep.notify(),并通知watcher(连接observer和compile的桥梁),watcher调用 _update()并更新视图 (调用 _render()方法)

最终实现MVVM!!

真的难,不过我考虑的还是挺全面的,比如深层代理响应,大家可以在浏览器开发工具中输入 app,然后进行数据修改调试实现响应式:(不过可能还是有一些bug,不过还是可以的!)

常说的MVC和MVVM架构是什么?用代码实现一下?

<!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">
        {{name}}
        <p>{{ age > 20 ? age : obj.age  }}</p>
        <p>{{ obj.name }}</p>
        <input v-model="obj.value">
        {{obj.value}}
    </div>

    <script>
        function isObject(obj) {
            return typeof obj === 'object' ? true : false;
        }

        // vue双向绑定实现:observer(观察者)、watcher(订阅对象)、notify(通知订阅对象的通报)、compiler(解析器)
        // vue2使用Object.defineProperty(),缺点:不能直接监听数组,不能监听变化(属性的添加)
        // vue3使用proxy -> 只能进行浅层的对象代理(递归循环代理)

        /**
         * 观察者
         * 
         * */
        class Obsever {
            /*
            * data: 被观察者
            */
            constructor(data, vm, prop) {
                if(isObject(data)) {
                    // 双向绑定
                    vm[prop] = this.defineReactive(data);
                }
            }
            defineReactive(obj) {
                if(!isObject(obj)) return;
                Object.keys(obj).forEach(key => { // 深层绑定
                    new Obsever(obj[key], obj, key);
                })
                const dep = new Dep();
                const handler = {
                    get(target, key, receiver) {
                        if(Dep.target) dep.addSub(Dep.target);
                        return Reflect.get(target, key, receiver)
                    },
                    set(target, key, value, receiver){
                        if(value === target[key]) return; // 值不变直接返回
                        Reflect.set(target, key, value, receiver)
                        // console.log(dep)
                        // new Obsever(value) // 替换引用类型的地址需要重新绑定响应式
                        dep.notify(); // 改变值通知所有观察者
                        return true;
                    }
                }
                return new Proxy(obj, handler)
            }
        }

        /*
        * 发布订阅模式
        */
        class Dep {
            static target = null; // 这里会存放当前的Watcher实例,并添加入Dep通知函数
            constructor() {
                this.subs = []; // 任务队列
            }
            addSub(sub) {
                return this.subs.push(sub);
            }
            notify() { // 通知所有观察者
                this.subs.forEach((sub) => { // 通知变化,此处会循环所有的依赖(Watcher实例),然后调用实例的update方法。
                    sub.update(); // 执行更新函数 (watcher 通知视图的变化)
                    // console.log(sub.update)
		        })
	        }
        }

        class Watcher {
            // 每一个Watcher都绑定一个更新函数,watcher可以收到属性的变化通知并执行相应的函数,从而更新视图。
            constructor(vm, node, prop) {
                Dep.target = this;
                this.vm = vm; // 实例
                this.node = node;
                this.prop = prop; // 要监听的属性
                this.update();
                Dep.target = null;
            }
            update() {
                this.get(); // 触发相应get
                // console.log(this.node, this)
                this.node.nodeValue = this.value //更改节点内容的关键
            }
            get() {
                this.value = eval(`this.vm._data.${this.prop}`);
            }
        }

        class Compile {
            constructor(el, vm) {
                this.$vm = vm; //vm为当前实例
                this.$el = document.querySelector(el);//获得要解析的根元素
                if(this.$el) {
                    this.$fragment = this.nodeToFragment(this.$el);
                    this.init(this.$fragment);
                    this.$el.appendChild(this.$fragment); // 将类dom添加进真实dom内
                }
            }
            nodeToFragment(el){ // 这里相当于生成虚拟dom(不是虚拟dom)
                let fragment = document.createDocumentFragment(); 
                // fragment 是一个指向空DocumentFragment对象的引用。是 DOM 节点。它们不是主 DOM 树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到 DOM 树。在 DOM 树中,文档片段被其所有的子元素所代替。
                // 因为文档片段存在于内存中,并不在 DOM 树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。
                let child;
                while (child = el.firstChild){
                    // fragment.appendChild()具有移动性, 相当于把el中节点移动过去.nextElementSibling
                    fragment.appendChild(child);//append相当于剪切的功能
                }
                return fragment;
            }
            init($fragment) {
                const childNodes = $fragment.childNodes;
                Array.from(childNodes).forEach(node => {
                    if(node.nodeType === 1) { // 元素节点
                        Array.from(node.attributes).forEach(attribute => {
                            if(attribute.nodeName === 'v-model') {
                                node.addEventListener('input', (e) => {
                                    const value = e.target.value;
                                    eval(`this.$vm._data.${attribute.nodeValue} = value`);
                                })
                            }
                        })
                        this.init(node);
                    }
                    if(node.nodeType === 3) { // 文本节点
                        this.compile_text(node);
                    }
                })
            }
            compile_text(node) {

                function isVariable(variable) { // 判断是不是变量
                    return !(variable === '' || parseInt(variable).toString() !== 'NaN' || (/[`'"]/).test(variable));
                }

                node.nodeValue = node.nodeValue.replace(/{{([^}]*)}}/g, (macth, context) => {
                    context = context.trim();
                    const exp = context.split(/[?:]/); // 三目运算符
                    const value = exp[0].trim();
                    // 变量,解决对于 data || age 这一类的,给data和age分别设置watcher
                    let variables = value.split(/[^\w.'"`]/);
                    variables.forEach(variable => {
                        variable = variable.trim();
                        if(isVariable(variable)) new Watcher(this.$vm, node, variable);
                    })
                    // 如果是三目运算符
                    if(exp.length === 3) {
                    // 运算表达式
                        if(eval(`this.$vm._data.${exp[0]}`)) {
                            if(isVariable(exp[1])) {
                                return eval(`this.$vm._data.${exp[1]}`);
                            }
                            return exp[1].trim().replace(/['"`]/g, '');
                        } else {
                            if(isVariable(exp[2])) {
                                return eval(`this.$vm._data.${exp[2]}`);
                            }
                            return exp[2].trim().replace(/['"`]/g, '');
                        }
                    }
                    // 变量
                    return eval(`this.$vm._data.${value}`)
                })
            }
        }

        class myVue {
            /*
            * options: 配置选项
            */
            constructor(options) {
                this.$options = options || {};
                const data = this._data = options.data;
                new Obsever(data, this, '_data'); // 被观察者不能是Vnode或者基本数据类型
                this.$compile = new Compile(options.el || document.body, this);
            }
        }

    </script>

<!-- 响应式不了,proxy需要深层代理 -->
    <script>
        const app = new myVue({
            el: '#app',
            data: {
                name: 'y',
                age: 19,
                obj: {
                    name: 'hy',
                    value: '',
                    age: 15
                }
            }
        });
    </script>
</body>
</html>

最后!祝大家早日拿到满意的offer!!冲冲冲!!!

“大多数优秀的程序员从事编程工作,不是因为期望获得报酬或得到公众的称赞,而是因为编程是件有趣的事儿。”——林纳斯·托瓦兹(Linus Torvalds)

转载自:https://juejin.cn/post/7206487695123185723
评论
请登录