美文网首页
通过样例来理解 MVC 模式

通过样例来理解 MVC 模式

作者: downhill6 | 来源:发表于2019-05-17 01:14 被阅读0次

参考: 自制前端框架之 MVC
参考: MVC,MVP 和 MVVM 的图示

如何设计一个程序的结构,这是一门专门的学问,叫做"架构模式"(architectural pattern),属于编程的方法论。MVC 模式就是架构模式的一种,在 UI 编程领域大有大量使用 MVC 模式的开发框架 (Django等后端框架),使得开发者能够借助该模式,构建出更易于扩展和维护的应用程序。

MVC 简介

大体上可将 MVC 模式的结构分为三层,即 Model(模型)、View(视图)和Controller(控制)

  • Model: 专注数据的存取,对基础数据对象的封装。
  • Controller: 处理具体业务逻辑,即根据用户从"视图层"输入的指令,从 Model 中存取数据,并渲染到 View 中。
  • View : 负责界面视图,供用户查看和操作,可理解为【输入数据,输出界面】的模块,在其中通常不涉及的业务逻辑。

这三层是紧密联系在一起的,但又是互相独立的,每一层内部的变化不影响其他层。每一层都对外提供接口(Interface),供其他层调用。这样一来,软件就可以实现模块化,修改外观或者变更数据都不用修改其他层,大大方便了维护和升级。

需要注意的是,MVC 仅是一种模式理念,而非具体的规范。因此,根据 MVC 的理念所设计出的框架,在实现和使用上可能存在着较大的区别。

咱们的目标

常见的后端框架所封装的功能,不外乎对数据的增查改删与渲染。在前端,我们以一个非常简单的 Todo App 作为示例,来实际看看 MVC 模式到底是怎样工作的。

  • Model 模块实现 Todo 这一数据模型的存取。
  • View 模块实现将 Todo 数据模型渲染到页面。
  • Controller 模块实现对 Todo 数据的新增、编辑、删除等操作。

编写代码

Model 模块

按照 MVC 模式,Model 模块的主要工作是存取数据,并且在数据变化时将新数据传给 View 模块。Model 模块的核心是订阅-发布者模式

class Model {
    // 在构造器中实例化数据与订阅者
    // 本例中的数据就是 一个个的 todo
    constructor() {
        // 数据格式 [{id: 1, value: '123'}]
        this.todo = []
        this.todo
        // 【初始化订阅者】
        this.subscribers = [] 
    }

    // 利用 ES6 class 语法定义模型实例的 getter
    // 从而在调用 model.data 时返回正确的 Todo 数据
    get data() {
        return this.todo
    }

    // 利用 ES6 class 语法定义模型实例的 setter
    // 从而在执行形如 modle.data = newData 的赋值时
    // 能够通知订阅了 Model 的模块进行相应更新 【数据更新时,触发订阅回调】
    set data(data) {
        this.todo = data
        this.publish(this.todo)
    }

    // 由 Model 实例调用的发布方法
    // 在 Model 中的 setter 更新时,将新数据传入该方法
    // 由该方法将新数据推送到每个订阅者提供的回调中
    // 在 本项目中,订阅者为 Controller 的 render 方法 【触发所有订阅】
    publish(data) {
        // 此处的订阅者是 业务中的函数
        this.subscribers.forEach(render => render(data))
    }
}

在示例中可以发现,所谓的发布 - 订阅模式,其思路和实现均非常简单:

  1. 区分出【发布者】和【订阅者】的概念。本例中 Model 为发布者,Controller 为订阅者。
  2. 在发布者中维护【我有哪些订阅者】信息的数组,每个元素为一个订阅者提供的回调。
  3. 发布者数据更新时,依次触发所有订阅者的回调。

不过,Model 中的代码仅实现了【初始化发布者】与【触发所有订阅】,【数据更新时,触发订阅回调】的功能,并不是一个完整的发布 - 订阅模式。在完整的模式实现中,其余代码包括:

  1. 【订阅者订阅发布者】机制的实现,其代码位置为 Controller 中的最后一行 this.model.subscribers.push(this.render),在此将 render 方法作为订阅者回调,提供给了发布者。
  2. 【订阅者提供的订阅方法】的实现,在此即为 Controller 中提供的 this.render 方法。

Controller 模块

上文中已经明确,Controller 模块需要实现的功能为:

  • 与 Model / View 实例的绑定。
  • 对点击事件、DOM 选择等底层 API 的封装。
  • 用于渲染数据的 Render 方法。
class Controller {
    constructor(conf) {
        // 根据实例化参数,定义 Controller 基础配置
        // 包括 DOM 容器、Model / View 实例及 onClick 事件等
        this.el = document.querySelector(conf.el)
        this.model = conf.model
        this.view = conf.view
        // 为 容器 dom 设置事件
        this.bindEvent(this.el)
        // 给 render 函数绑定 this
        this.render = this.render.bind(this)
        // 在 Model 更新时执行 controller 的 render 方法
        this.model.subscribers.push(this.render)
    }

    // 根据点击 btn 的 class 属性绑定不同的事件回调
    bindEvent() {
        this.el.addEventListener('click', (event) => {
            let el = event.target
            let id = el.dataset.id
            if (el.classList.contains('todo-delete')) {
                deleteTodo(id)
            } else if (el.classList.contains('todo-update')) {
                updateTodo(el, id)
            } else if (el.classList.contains('todo-add')) {
                addTodo(el)
            }
        })

        // 点击 add 时,把新的 todo 添加到 model 中
        const addTodo = (el) => {
            let input = el.parentElement.querySelector('.input-add')
            let value = input.value
            if (value !== '') {
                let data = this.model.data
                data.push({
                    id: data.length,
                    value: value
                })
                this.model.data = data
            }
        }

        // 点击 delete 时,把对应 todo 从 model 中删除
        const deleteTodo = (id) => {
            this.model.data = this.model.data.filter((todo) => {
                return id !== String(todo.id)
            })
        }

        // 点击 update 时,把对应 todo 在 model 中更新
        const updateTodo = (el, id) => {
            this.model.data = this.model.data.map((todo) => {
                return ({
                    id: todo.id,
                    value: setValue(id, todo)
                })
            })
        }
        
        // 辅助 更新函数
        const setValue = (id, todo) => {
            if (id === String(todo.id)) {
                let updateInput = document.querySelector(`input[data-id="${todo.id}"]`)
                return updateInput.value
            } else {
                return todo.value
            }
        }
    }

    // 全量重置 DOM 的 naive render 实现
    render() {
        // 由于 view 是纯函数,故而直接对其传入 Model 数据
        // 将输出的 HTML 模板作为 Controller DOM 内的新状态
        this.el.innerHTML = this.view(this.model.todo)
    }
}

可以看到 Controller 模块在点击事件触发时,没有直接修改 dom, 只是修改了 model 中的数据, dom 视图的改变是在 model 中数据变化时自动 render 的。

View 模块

如前文所述, View 模块实质上就是一个在 Model 中数据变更时,由 Controller 在 render 方法中执行的一个纯函数。

function view(todos) {
    const todosList = todos.map(todo => `
    <div>
      <span data-id="${todo.id}">
        ${todo.value}
      </span>
      <button data-id="${todo.id}" class="todo-delete">
        删除
      </button>
      <span>
        <input data-id="${todo.id}"/>
        <button data-id="${todo.id}" class="todo-update">
          Update
        </button>
      </span>
    </div>
  `).join('')

    return (`
    <main>
      <input class="input-add"/>
      <button class="todo-add">Add</button>
      <div>${todosList}</div>
    </main>
  `)
}

index.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>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="./mvc.js"></script>
    <script>
        const model = new Model()
        let conf = {
            model,
            view,
            el: '#app',
        }
        const controller = new Controller(conf)
        controller.render()
    </script>
</body>
</html>

总结

在实现 MVC 模式的 todo app 的过程中,MVC 模式的特性得到了体现,【Model, View】模块直接只对外提供接口, 【Controller】模块则将两者连接了起来,而 ES6 所提供的 class 高级特性则大大简化这些特性的实现复杂度(setter getter 等)。

最后的最后,咱们再梳理下流程:

点击按钮 -> 触发 controller 里的事件 -> 更改 model 数据 -> 触发 render 函数 -> 更新视图

相关文章

  • 通过样例来理解 MVC 模式

    参考: 自制前端框架之 MVC参考: MVC,MVP 和 MVVM 的图示 如何设计一个程序的结构,这是一门专门的...

  • 面试题

    非作者原著 来自摘抄 yahoouchen 设计模式 MVC模式 MVVM模式 单例模式: 通过static关键字...

  • python面试题-2018.1.30

    问题:如何实现单例模式? 通过new方法来实现单例模式。 变体: 通过装饰器来实现单例模式 通过元类来创建单例模式...

  • 6. iOS面试题其他篇1

    常用的设计模式 单例模式 组合模式 观察者模式 代理模式 享元模式 工厂方法模式 抽象工厂模式 MVC的理解 数据...

  • MVC 单例模式的理解

    1.什么是MVC mvc是model view controller的简称, view-存放视图使用的 model...

  • Flux架构模式

    Flux架构模式 在说flux模式之前,我们先说说mvc和mvvm模式 MVC模式 通过关注数据界面分离,来鼓励改...

  • 设计模式(一)

    一 单例模式 二 MVC(Model View Controller)mvc_pattern_uml_diagra...

  • Java EE 极简教程(四):MVC 模式

    MVC 模式 MVC 模式是一种软件框架模式,被广泛应用在 JavaEE 项目的开发中。MVC 模式很好理解,但也...

  • iOS知识梳理3:设计模式

    iOS有哪些常见的设计模式?单例模式/委托模式/观察者模式/MVC模式 单例模式 单例保证了应用程序的生命周期内仅...

  • IOS 设计模式

    IOS开发中几种设计模式:单例模式、观察者模式、MVC模式、代理模式 一、单例模式 场景:确保程序运行期某个类,只...

网友评论

      本文标题:通过样例来理解 MVC 模式

      本文链接:https://www.haomeiwen.com/subject/fuugaqtx.html