# 浅析 MVC

# 意大利面条式代码(❌), 模块化(⭕)

当我们在 coding 一个 web 应用时, 代码量肯定不少, 然而把全部逻辑都写在一个 JS 文件里面会引起巨大的混乱, 稍有编程常识的人都会马上意识到这是一个需要解决的问题. 通常我们将自己的项目代码模块化, 即把应用拆为各自高度独立的模块, 模块间可以通过接口来通信, 有时甚至不需要通信. 然后将各个模块引入到一个文件里使用即可.

# MVC - 是先有鸡 🥠 还是先有蛋 🥚

在模块化后, 有人从后端里搬来一个 MVC 的概念, 把 web 应用也划分为了三个部分(Model, View, Controller).下面说说各自的功能

# Model - 操作数据

Model 层的代码负责获取到 web 应用所需要的数据, 在一些场景下还要负责处理数据的改动(增, 删, 改, 查)

const Model = {
  data: {
    x: 20
  },
  update(data) {
    Object.assign(Model.data, data);
  }
};
1
2
3
4
5
6
7
8

# View

View 层负责与用户能看见的部分, 比如页面的模板, 渲染函数等.

const View = {
  html: `
    <h1>hello</h1>
  `,
  render() {
    // render the template
  },
  mount() {}
};
1
2
3
4
5
6
7
8
9

# Controller

Controller 层负责 View 和 Model 都不处理的部分. 如事件绑定等

const Controller = {
  events: {
    fn1: "click #button1",
    fn2: "click #button2"
  },
  bindEvents() {
    for (const key in Controller.events) {
      if (Controller.events.hasOwnProperty(key)) {
        const [event, selector] = Controller.events[key].split(" ");
        $(View.el).on(event, selector, Controller[key]);
      }
    }
  },
  fn1() {},
  fn2() {}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在上面的代码里, 我们使用了表驱动的概念来绑定事件. 我们抽象出有对应关系的数据作为一个抽象的表:

fn1 fn2
click #button1 click #button2

然后循环遍历地绑定事件, 这样就能避免笨重且重复地选中元素, 监听事件, 操作函数这一系列的操作. 现在我只需要写一次就行.

# MVC 的逻辑

在 MVC 的视角下, 一个 web 应用应当这样做: View 调用 mount函数开始初始化, 将 View 的模板添加到指定的元素里, 再调用 Controller 进行事件绑定, 使用 eventBus 进行监听状态变化. 当我们在 web 应用里改变状态时, 会被 eventBus 监听到, 然后触发重新渲染页面的render函数. 当然, 有人发现我在 View 里调用了 Controller, 实际上, MVC 并不一定要彼此独立分开, 甚至全部组合在一起也是可以的. 那么这和不用 MVC 又有什么区别. 虽然代码组合在一起, 但是结构依然要清晰, 如果我们能够定义好el,template,mount,render,data. 那么这个 web 应用依然能够大大地解决代码结构混乱, 意大利面条代码等问题的.MVC 并不是最终的解决方案, 它后来也开始分化(由于理念的不同), 演变出了 Angular, Vue, React 等框架与库.

# eventBus🚌

eventBus用于在对象之间的通信. 是根据发布-订阅模式来实现的.下面会 cover 到它的 api 和实现方法.

# API: on, off, trigger

on(msg, fn) 接受两个参数, 第一个参数msg表示监听的事件名, 然后触发fn函数.

off(msg) 接受到msg后删除所有监听msg的事件

trigger(msg, [params]) 接受到msg后调用对应的fn, params为可选参数

# 手写 eventBus🚌

class EventBus {
  constructor() {
    this.events = {};
  }

  on(msg, fn) {
    if (!this.events[msg]) {
      this.events[msg] = [];
    }
    this.events[msg].push(fn);
  }

  trigger(msg, params) {
    this.events[msg].forEach(item => {
      item.apply(this, params);
    });
  }

  off(msg) {
    if (!!this.events[msg]) {
      delete this.events[msg];
      console.log(`DELETE ${msg} COMPLETED!`);
    } else {
      console.error("CAN'T DELETE PROPERTY OF UNDEFINED");
    }
  }
}

export default EventBus;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

实际上, eventBus 里有一个events对象, 我们监听的事件名是对象的key, 而value是一个数组, 数组里的元素就是fn. 最后, 要实现 eventBus 的功能, 必须要在全局的环境下调用.所以通常把 eventBus 放在顶部, 如果你的应用里使用到 Class, 那么 eventBus 应当作为 class 的祖先类.如class MVC extends eventBus{}. 实际上, DOM API也是这样做的:

回调地狱

DOM的原型链上层能发现eventTarget, 它的三个API和我们这里的三个实际上是一样的.