在开发过程中如何应用mvvm思想(非现有的框架)?

提出这个问题,旨在学习、理解mvvm的思想,所以希望答主能讲解一下如何一步一步把一个需求按照mvvm的方式解决掉,有个小例子(比如说一个组件)就最好了。 提前感谢各位的不吝赐教,谢谢!!!
关注者
254
被浏览
5860

2 个回答

谢邀。。。



我们写界面的时候,经常会碰到一些功能,比如,从某些数据动态生成界面,然后从输入控件采集数据,经过变换,更新另外的界面。

整个这个过程,其实归纳来看,就两件事:

- 根据数据的变动,生成或者变更界面
- 根据界面的操作,变更数据

所有的界面相关操作流程,都可以化为若干这两种操作的组合。写多了之后,我们就可以总结出,两者又都是包含一些模式的,比如说,数据的变动,它包含:

- 简单类型的创建赋值
- 引用类型的创建和赋值
- 数组元素的添加,移除,交换、赋值
- 对象属性的添加、删除、赋值

界面的变更,包含:

- 元素的创建、交换、移位、移除
- 属性的创建、赋值、移除

再总结下去,就会发现,数据和界面的变更之间,一般都是存在对应关系的。

比如说,一个数组跟界面一个列表对应,两者始终是同步的,一个变了,另外一个也要跟着变。一个变量或者对象的键值,对应到一个元素的属性上,一个变了,另外一个也要跟着变。

既然这样,如果我们能够引入一种绑定关系,经过一系列的配置过程,使得以后每次数据发生变更,界面都会自动跟着作对应变动;界面上的操作,也会自动更新到数据,那开发过程就会非常省事了,绝大部分此类操作都会转化为配置,供绑定框架用来建立数据和界面之间的关联关系。

所以,数据绑定是一个非常广泛而迫切的需求,任何一个现代界面开发体系,如果不提供某种数据绑定机制,其开发过程就必定是低效的。

那么,界面,很好理解,肯定是View,数据,如果说是Model,那ViewModel又是什么?

以购物车结算页面为例,包含的内容有:

- 已选商品列表(每种商品可以添加或者移除数量,并且有每种商品的总价)
- 商品总数和总价

从View的角度,一般不会有太多疑问,看到的东西,可以视业务场景做分块,每块单独实现,但是Model呢?

我们知道Model是一种数据,但并非每个数据都属于Model,一般来说,在页面上的数据中,用于跟服务端交互的数据,可以大致视为Model,回到我们这个例子中看,那就是每种商品的已选数量。

但另外还有一些东西是要用于展示或者中间步骤的,那就是每种商品的总价,还有商品总数、总价,这些东西由于不提交,仅供视图使用,所以可以视为ViewModel。

ViewModel是Model和View之间的桥梁,它的设计原则是:

- 为Model和View提供适配
- 如果有需要转换的过程,尽可能在ViewModel中做,保持Model的纯洁,View的清晰。

我们上面这个例子中,数据的转换并不明显。大家都见过行政区划树吧,省市县乡村,可能Model给的数据都是平级,然后根据展示的需要,转换成树状的,转换之后的数据,就是专供展示用的ViewModel了。

综上,MVVM所包含的Model,View,ViewModel三层,在实践的时候,主要包含几个要点:

- 以不同的角色分别考虑每个层次
- 先考虑Model和View,最后考虑ViewModel

这个ViewModel,责任可不轻,就像中年男人,是婆媳关系的纽带、桥梁,要想两头满意,就得自己多干,脏活累活全都我来,你们歇着……

--------------------
欠两个例子代码,闲了用Vue补个
class ConcreteComponent {
    constructor() {
        this.element = document.createElement('div');

        this.state = {
            status: 'loading'
            content: '',
            disabled: false
        };
    }

    render() {
        let classNames = ['wrapper'];

        if (this.state.status === 'loaded' && !this.state.disabled) {
            classNames.push('enabled');
        }

        let content;

        switch (this.state.status) {
            case 'loading':
                content = 'Loading...';
                break;
            case 'loaded':
                content = this.state.content;
                break;
            case 'error':
            default:
                content = 'Something wrong happened.';
                break;
        }

        this.element = `
            <div>Hello, Whatever!</div>
            <div class="${classNames.join(' ')}">
                ${content}
            </div>
        `;
    }
}

let component = new ConcreteComponent();

document.body.appendChild(component.element);

component.state.content = 'Useless content.';
component.render();

component.state.disabled = true;
component.render();

上面的代码示例中, 最重要的就是 state 的具体变化和 render 的分离. render 不关心到底是什么具体的操作或事件使得 state 发生了变化, 只负责根据变化后的 state 重新渲染. 当然这会产生一些不必要的开销, 但这部分的调优应该交给框架来搞定.