前端面试题:这是我理解的MVVM,请注意查收~~

栏目: 编程工具 · 发布时间: 5年前

内容简介:MVVM模式是什么?你是怎么理解MVVM原理的?理解它不只是应付面试,对VUE、Backbone.js、angular、Ember、avalon框架的设计模式也会有更进步一步的理解,有可能下一个流行框架就是你的杰作~~本篇文章最后也会实现了一个属于自己的简易MVVM库,里面实现了一个mvvm库应有基本功能~Mvvm定义MVVM是Model-View-ViewModel的简写。是一个软件架构设计模式,由微软 WPF 和 Silverlight 的架构师 Ken Cooper 和 Ted Peters 开发,是

MVVM模式是什么?你是怎么理解MVVM原理的?理解它不只是应付面试,对VUE、Backbone.js、angular、Ember、avalon框架的 设计模式 也会有更进步一步的理解,有可能下一个流行框架就是你的杰作~~本篇文章最后也会实现了一个属于自己的简易MVVM库,里面实现了一个mvvm库应有基本功能~

      现在流行的前端框架也就是 vue、react、angular了,在投递简历时,我们都可以看到任职要求会有最少熟悉这些框架中的一种,掌握这些框架就好像时多了一个轮子或者说是多了一个车,框架可以然我们快速的使用、复用处理一些问题。当然面试中不仅会问到这些只是的掌握情况,也会问些你的框架的理解,因为知其然也要知其所以然,我们这篇文章来了解MVVM框架模式~如果文章有问题,也请大家指正,不要打我啊~~

一、MVVM的概念

Mvvm定义MVVM是Model-View-ViewModel的简写。是一个软件架构设计模式,由微软 WPF 和 Silverlight 的架构师 Ken Cooper 和 Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。由 John Gossman(同样也是 WPF 和 Silverlight 的架构师)于2005年在他的博客上发表。即模型-视图-视图模型。

二、MVVM的发展史

在了解MVVM之前,我们先回顾一下前端发展的历史。下面一趴是来 自廖雪峰官方网站 的内容,了解历史就会知道为啥MVVM会被使用,使用背景,当然也可以跳过直接看下一趴~

在上个世纪的1989年,欧洲核子研究中心的物理学家Tim Berners-Lee发明了超文本标记语言(HyperText Markup Language),简称HTML,并在1993年成为互联网草案。从此,互联网开始迅速商业化,诞生了一大批商业网站。

最早的HTML页面是完全静态的网页,它们是预先编写好的存放在Web服务器上的html文件。浏览器请求某个URL时,Web服务器把对应的html文件扔给浏览器,就可以显示html文件的内容了。

如果要针对不同的用户显示不同的页面,显然不可能给成千上万的用户准备好成千上万的不同的html文件,所以,服务器就需要针对不同的用户,动态生成不同的html文件。一个最直接的想法就是利用C、C++这些编程语言,直接向浏览器输出拼接后的字符串。这种技术被称为CGI:Common Gateway Interface。

很显然,像新浪首页这样的复杂的HTML是不可能通过拼字符串得到的。于是,人们又发现,其实拼字符串的时候,大多数字符串都是HTML片段,是不变的,变化的只有少数和用户相关的数据,所以,又出现了新的创建动态HTML的方式:ASP、JSP和PHP——分别由微软、SUN和开源社区开发。

在ASP中,一个asp文件就是一个HTML,但是,需要替换的变量用特殊的<%=var%>

标记出来了,再配合循环、条件判断,创建动态HTML就比CGI要容易得多。

但是,一旦浏览器显示了一个HTML页面,要更新页面内容,唯一的方法就是重新向服务器获取一份新的HTML内容。如果浏览器想要自己修改HTML页面的内容,就需要等到1995年年底,JavaScript被引入到浏览器。

有了JavaScript后,浏览器就可以运行JavaScript,然后,对页面进行一些修改。JavaScript还可以通过修改HTML的DOM结构和CSS来实现一些动画效果,而这些功能没法通过服务器完成,必须在浏览器实现。

用JavaScript在浏览器中操作HTML,经历了若干发展阶段:

第一阶段,直接用JavaScript操作DOM节点,使用浏览器提供的原生API:

var dom = document.getElementById('name');
dom.innerHTML = 'Homer';
dom.style.color = 'red';
复制代码

第二阶段,由于原生API不好用,还要考虑浏览器兼容性,jQuery横空出世,以简洁的API迅速俘获了前端开发者的芳心:

$('#name').text('Homer').css('color', 'red');

第三阶段,MVC模式,需要服务器端配合,JavaScript可以在前端修改服务器渲染后的数据。

现在,随着前端页面越来越复杂,用户对于交互性要求也越来越高,想要写出Gmail这样的页面,仅仅用jQuery是远远不够的。MVVM模型应运而生。

MVVM最早由微软提出来,它借鉴了桌面应用程序的MVC思想,在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。

把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。

ViewModel如何编写?需要用JavaScript编写一个通用的ViewModel,这样,就可以复用整个MVVM模型了。

三、正式的MVVM理解

  • MVVM模式

MVVM 的出现促进了 GUI 前端开发与后端业务逻辑的分离,极大地提高了前端开发效率。 MVVM 的核心是 ViewModel 层 ,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用。如下图所示:

前端面试题:这是我理解的MVVM,请注意查收~~

  •   MVVM组成部分

前端面试题:这是我理解的MVVM,请注意查收~~

# View 层

View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建,为了更方便地展现 ViewModel 或者 Model 层的数据,已经产生了各种各样的前后端模板语言,比如FreeMarker、Marko、Pug、Jinja2等等,各大 MVVM 框架如 avalon,Vue,Angular 等也都有自己用来构建用户界面的内置模板语言。

# Model 层

Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,主要围绕数据库系统展开。

后端业务处理在这就不多赘述了,其实前端人员大多都不需要管,只要后端保证对外接口足够简单就行了,我请求api,你把数据返出来,咱俩就这点关系,其他都扯淡。

# ViewModel 层

ViewModel 是由前端开发人员组织生成和维护的视图数据层。mvvm模式的核心,它是连接view和model的桥梁。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,那一块展示什么这些都属于视图状态(展示),而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。由于实现了双向绑定,ViewModel 的内容会实时展现在 View 层,这是激动人心的,因为前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新,真正实现数据驱动开发。看到了吧,View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。

  • MVVM设计模式的优缺点:

     优点:

1、 当然是最主要的双向绑定技术, 单向绑定与双向绑定。

所谓 单向绑定”就是ViewModel变化时,自动更新View

所谓 双向绑定”就是在单向绑定的基础上View变化时,自动更新ViewModel

我们可以先观察下MVVM框架和jQuery操作DOM相比有什么区别?

原来的html

<p>Hello, <span id="name">LEE</span>!</p><p>You are <span id="age">18</span>.</p>

展示

前端面试题:这是我理解的MVVM,请注意查收~~

用jQuery修改name和age节点的内容:

var name = '修改';
var age =100;
 
$('#name').text(name);
$('#age').text(age);
复制代码

如果我们使用MVVM框架来实现同样的功能,我们首先并不关心DOM的结构,而是关心数据如何存储。最简单的数据存储方式是使用:

var person = {
    name: 'LEEt',
    age: 18
};
复制代码

我们把变量person看作Model,把HTML某些DOM节点看作View,并假定它们之间被关联起来了。

要把显示的name从LEE改为修改,把显示的age从18改为100,我们并不操作DOM,而是直接修改JavaScript对象:

person.name = '修改';

person.age = 100;
复制代码

这样可以看出,我们的关注点从如何操作DOM变成了如何更新JavaScript对象的状态,而操作JavaScript对象比DOM简单多了!

MVVM的设计思想:关注Model的变化,让MVVM框架去自动更新DOM的状态,从而把发者从操作DOM的繁琐步骤中解脱出来!

2、由于控制器的功能大都移动到View上处理,大大的对控制器进行了瘦身。

3、可以对View或ViewController的数据处理部分抽象出来一个函数处理model。这样它们专职页面布局和页面跳转,它们必然更一步的简化。

4、提高可维护性

5、可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。

6、低耦合可重用:视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定不同的"View"上,当View变化的时候Model不可以不变,当Model变化的时候View也可以不变。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。

缺点:

  1. Bug很难被调试。因为使用双向绑定的模式,当你看到界面异常了,有可能是你View的代码有Bug,也可能是Model的代码有问题。数据绑定使得一个位置的Bug被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。另外,数据绑定的声明是指令式地写在View的模版当中的,这些内容是没办法去打断点debug的。
  2. 一个大的模块中model也会很大,虽然使用方便了也很容易保证了数据的一致性,当时长期持有,不释放内存就造成了花费更多的内存。
  3. 对于大型的图形应用程序,视图状态较多,ViewModel的构建和维护的成本都会比较高。
  • MVVM的适用范围

从几个例子我们可以看到,MVVM最大的优势是编写前端逻辑非常复杂的页面,尤其是需要大量DOM操作的逻辑,利用MVVM可以极大地简化前端页面的逻辑。

但是MVVM不是万能的,它的目的是为了解决复杂的前端逻辑。对于以展示逻辑为主的页面,例如,新闻,博客、文档等,不能使用MVVM展示数据,因为这些页面需要被搜索引擎索引,而搜索引擎无法获取使用MVVM并通过API加载的数据。

所以,需要 SEO(Search Engine Optimization)的页面,不能使用MVVM展示数据。不需要SEO的页面,如果前端逻辑复杂,就适合使用MVVM展示数据,例如,工具类页面,复杂的表单页面,用户登录后才能操作的页面等等。当然可能现在有了ssr。

常用的MVVM框架有:

Angular:Google出品,名气大,但是学习难度有些大;适合PC,代码结构会比较清晰;

Backbone.js:入门非常困难,因为自身API太多;

Ember:一个大而全的框架,想写个Hello world都很困难。

Avalon:属于轻量级的,并且对老的浏览器支持程度较高,最低支持到IE6,所以适合兼容老刘浏览器的项目;

Vue:主打轻量级,仅作为MV*中的视图部分使用,优点轻量级,易学易用,缺点是大项目的时候还要配合其他框架或者库来使用,比较麻烦

四、实现MVVM的js库

目前实现数据双向绑定主要有一下几种方式:

  1. 脏值检测(angular):

以典型的mvvm框架angularjs为代表,angular通过检查脏数据来进行UI层的操作更新。关于angular的脏检测,有几点需要了解些:

  • l脏检测机制并不是使用定时检测。
  • l脏检测的时机是在数据发生变化时进行。
  • l angular对常用的dom事件,xhr事件等做了封装, 在里面触发进入angular的digest流程。
  • l在digest流程里面, 会从rootscope开始遍历, 检查所有的watcher。 (关于angular的具体设计可以看其他文档,这里只讨论数据绑定),那我们看下脏检测该如何去做:主要是通过设置的数据来需找与该数据相关的所有元素,然后再比较数据变化,如果变化则进行指令操作。

2.前端数据劫持(Hijacking)(vue):基本思路:通过Object.defineProperty() 去劫持数据每个属性对应的getter和setter。当有数据读取和赋值操作时则调用节点的指令,这样使用最通用的=等号赋值就可以了。

3.发布-订阅模式(backbone):通过发布消息,订阅消息进行数据和视图的绑定监听。

比较老的实现方式,使用观察者编程模式,主要思路是通过在数据对象上定义get和set方法等,调用时手动调用get或set数据,改变数据后触发UI层的渲染操作;以视图驱动数据变化的场景主要应用与input、select、textarea等元素,当UI层变化时,通过监听dom的change,keypress,keyup等事件来触发事件改变数据层的数据。整个过程均通过函数调用完成。

代码实现思路:(类似实现 VUE 的一个实现MVVM的库)

前端面试题:这是我理解的MVVM,请注意查收~~

模拟的是VUE的MVVM库使用数据劫持思路实现,MVVM,上图为基本思路图。如上图所示,我们可以看到,整体实现分为已下步骤

1、实现一个Observer,对数据进行劫持,通知数据的变化(将使用的要点为:Object.defineProperty()方法)

2、实现一个Compile,对指令进行解析,初始化视图,并且订阅数据的变更,绑定好更新函数

3、实现一个Watcher,将其作为以上两者的一个中介点,在接收数据变更的同时,让Dep添加当前Watcher,并及时通知视图进行update

4、实现一些VUE的其他功能(Computed、menthods)

5、实现MVVM,整合以上几点,作为一个入口函数

以下为代码部分:

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>
实现MVVM的js库(模拟vue实现功能)</title>

<script src="./MVVM.js"></script>
</head>
 
<body>
 
<div id="app">
<input type="text" v-model="person.name">
<p>
hello,{{person.name}}
</p>
<p>You are:{{person.age}}</p>
<!-- computed属性如果数据不变化 视图不更新 -->
<p>{{getNewName}}</p>
<button type="button" name="button" v-on:click="testToggle">
修改名字</button>

</div>
 
<script>
let vm = new Vue({
el: '#app',
data: {
person: {
name: 'lee',
age: 18
}
},
methods: {
testToggle(){
this.person.name = '修改后的名字:哈哈';
}
},
computed: {
getNewName(){
return this.person.name+' 是要成为海贼王的人'
}
},
})
</script>
</body>
</html>
复制代码

js:

// 2019-4-4
// lee 
// 草履虫的思考
// 简单模拟vue实现MVVM
/**
 * 实现一个Vue的类
 * 1、实现一个Observer,对数据进行劫持,通知数据的变化(将使用的要点为:Object.defineProperty()方法)
2、实现一个Compile,对指令进行解析,初始化视图,并且订阅数据的变更,绑定好更新函数ComplieUtil解析指令的公共方法
3、实现一个Watcher,将其作为以上两者的一个中介点,在接收数据变更的同时,让Dep添加当前Watcher,并及时通知视图进行update
4、实现一些VUE的其他功能(Computed、menthods)
 */
// 观察者模式(发布订阅)
class Dep {
    constructor() {
        this.subs = []; //存放所有watcher
    }
    // 订阅 添加watcher
    addSub(watcher) {
        this.subs.push(watcher);
    }
    // 发布
    notify() {
        this.subs.forEach(watcher => watcher.update());
    }
}
// 观察者 vm.$watch(vm,'person.name',(newVal)=>{ })
class Watcher {
    constructor(vm, expr, cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 默认存储一个老值
        this.oldValue = this.get();
    }
 
    get() {
        Dep.target = this;
        // 取值 把这个观察者和数据关联起来
        let val = ComplieUtil.getVal(this.vm,this.expr);
        Dep.target = null;
        return val;
    }
    // 更新操作 数据变化后 会调用观察者中的update方法
    update() {
        let newVal = ComplieUtil.getVal(this.vm,this.expr);
        if (newVal !== this.oldValue) {
            this.cb(newVal);
        }
    }
}
 
// 实现数据劫持作用
class Observer {
    constructor(data) {
        this.observer(data);
    }
    observer(data) {
        // 如果是对象才观察
        if (data && typeof data === 'object') {
            for (let key in data) {
                this.defineReactive(data, key, data[key])
            }
        }
    }
 
    defineReactive(obj, key, value) {
        this.observer(value);
        // 给每个属性 都加上具有发布订阅的功能
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,   // 可枚举
            configurable: true, // 可重新定义
            get() {
                // 创建watcher时 会取到对应的内容,并且把watcher放到全局上
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set: (newVal) => { // {person:{name:'lee'}
                // 数据没有变不需要更新
                if (newVal != value) {
                    // 需要递归
                    this.observer(newVal);
                    value = newVal;
                    dep.notify();
                }
            }
        })
    }
}
// 编译器
class Complier {
    constructor(el, vm) {
        // 判断el属性是不是一个元素 如果不是元素 那就获取他 (因为在vue的el中可能是el:'#app'
        // 或者document.getElementById('app')
 
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 把当前节点中的元素 获取到 放到内存中
        let fragment = this.nodeFragMent(this.el);
 
        // 把节点中的内容进行替换
 
        // 编译模板 用数据编译
        this.complie(fragment);
        // 把内容在塞到页面中
        this.el.appendChild(fragment);
    }
 
    isElementNode(node) { //是不是元素节点
        return node.nodeType === 1;
    }
    //  把节点移动到内存中
    nodeFragMent(node) {
        let frag = document.createDocumentFragment();
        let firstChild;
        while (firstChild = node.firstChild) {
            // appendChild 具有移动性 
            frag.appendChild(firstChild);
        }
        return frag;
    }
    // 是不是指令 
    isDirective(attrName) {
        return attrName.startsWith('v-');
    }
    // 编译元素
    complieElement(node) {
        let attr = node.attributes;
        [...attr].forEach(item => {
            // item 有key = value ,type="text" v-model="person.name"
            let {
                name,
                value: expr
            } = item;
            if (this.isDirective(name)) {
                // v-mode v-html v-bind...
                let [, directive] = name.split('-');
               let [directiveName,eventName] = directive.split(':');
                console.log(node, expr, this.vm, eventName);
                // ComplieUtil[directive](node, expr, this.vm);
                ComplieUtil[directiveName](node, expr, this.vm, eventName);
            }
        })
    }
    // 编译文本
    // 判断当前文本节点中内容是否包括{{}}
    complieText(node) {
        let content = node.textContent;
        var reg = /\{\{(.+?)\}\}/;
        if (reg.test(content)) {
            ComplieUtil['text'](node, content,this.vm); //{{}}
        }
    }
    // 用来编译内存中的dom节点
    complie(node) {
        let childNode = node.childNodes;
        // childNode 是类数组 转换为数组
        [...childNode].forEach(item => {
            // 元素 查找v-开头
            if (this.isElementNode(item)) {
                this.complieElement(item);
                //如果是元素的话  需要把自己传进去
                // 在去遍历子节点
                this.complie(item);
                //    文本 查找{{}}内容
            } else {
                this.complieText(item);
            }
        })
 
    }
}
// 编译工具
ComplieUtil = {
    // 解析v-model指令
    // node是节点 expr是表达式 vm是实例 person.name vm.$data 解析v-model
    model(node, expr, vm) {
        // 给输入框赋予value属性 node.value = xxx
        let fn = this.updater['modelUpdater'];
        let val = this.getVal(vm, expr);
        // 给输入框加一个观察者 如果稍后数据更i性能了会触发此方法,数据会更新
        new Watcher(vm, expr, (newVal) => {
            fn(node, newVal);
        });
        // 输入事件
        node.addEventListener('input',(e)=>{
            let val = e.target.value; //获取用户输入的内容
            this.setVal(vm, expr, val);
        });
        fn(node, val);
    },
    html() {
 
    },
    // 返回了一个全的字符串
    getContentVal(vm, expr) {
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getVal(vm, args[1]);
        });
    },
    text(node, expr, vm) { //expr {{a}} {{b}} {{person.name}}
        let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
        //给表达式{{}}都加上观察者    
            new Watcher(vm, args[1], () => {
                fn(node, this.getContentVal(vm, expr));
            });
            return this.getVal(vm, args[1]);
        });
        let fn = this.updater['textUpdater'];
        fn(node, content);
    },
    on(node, expr, vm,eventName){ //v-on:click
        console.log(node, expr, vm, eventName);
        node.addEventListener(eventName,(e)=>{
            vm[expr].call(vm,e );
        });
       
    },
    updater: {
        modelUpdater(node, value) {
            node.value = value;
        },
        htmlUpdater() {},
        // 处理文本节点
        textUpdater(node, value) {
            node.textContent = value;
        }
    },
    //根据表达式取到的对应的数据  vm.$data expr是如 'person.name'
    getVal(vm, expr) {
      return  expr.split('.').reduce((data, cur) => {
            return data[cur];
        }, vm.$data);
    },
    setVal(vm, expr,value){
        expr.split('.').reduce((data, cur,index,arr) => {
           if(index == arr.length-1){ //索引是最后一项 
               return data[cur] = value;
           }
            return data[cur];
        }, vm.$data);
    }
}
class Vue {
    constructor(options) {
        this.$el = options.el;
        this.$data = options.data;
        let computed = options.computed;
        let methods = options.methods;
        // 根元素存在在编译模板
        if (this.$el) {
            // 把数据 全部转化成用Object.defineProperty来定义
            new Observer(this.$data);
 
 
            // 实现methods中的方法
            for (let key in methods) { 
                Object.defineProperty(this, key, {
                    get() {
                        return methods[key]; //进行了转化操作
                    }
                });
            }
            // 实现computed中的方法
            for (let key in computed) { //有依赖关系
                Object.defineProperty(this.$data, key, {
                    get() {
                        return computed[key].call(this); //进行了转化操作
                    }
                });
            }
               // 把数据获取操作 都代理到vm.$data
            this.proxy(this.$data);
            new Complier(this.$el, this);
        }
 
    }
    // 代理 去掉$data
    proxy(data){
        for(let key in data){
            Object.defineProperty(this,key,{
                get(){
                    return data[key]; //进行了转化操作
                }
            });
        }
    }
}


复制代码

以上所述就是小编给大家介绍的《前端面试题:这是我理解的MVVM,请注意查收~~》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

编程之道

编程之道

杰弗雷﹒詹姆斯 / 清华大学出版社 / 1999-05 / 18.00元

本书出自美国一位善于进行哲学思考、有十多年工作经验的程序设计师——杰弗雷·詹姆斯之手,他以一种敏锐的眼光审视着发生在程序设计室里的各种各样的小故事,并利用古老的道家思想对其进行分析。简单的故事蕴含深奥的道理,是本书的最大特色。本书语言优美,比喻生动,可读性极强。一起来看看 《编程之道》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具