likes
comments
collection
share

手写简单的 MVC 模式

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

文章首发于语雀,如有问题欢迎评论指正,感谢!

🏖️ 认识 MVC

MVC的概念是从后端开发引入的,全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,是一种代码的设计模式。

  • Model:数据模型层,对数据进行增删改查的操作,操作数据库。
  • View:视图层,显示视图或者视图模版。
  • Controller:控制器层,该层主要将数据和视图进行关联挂载,和基本的逻辑操作。

它们 3 个的关系大概如下:

前端View发起请求 =>> Controller接收到请求 =>> 让Model去操作数据库 =>> 返回给Controller =>> 返回到前端

如果后端有view层那这就是服务端渲染,由后端渲染完成后返回到前端。

手写简单的 MVC 模式

🏖️ 前端中的 MVC

到了前端中,MVC的设计模式和后端基本一致:

  • Model管理视图需要的数据,数据和视图进行关联。
  • View视图层,HTML模版和视图渲染。
  • Controller管理事件的逻辑操作。

例如我们实现一个简单的MVC结构,实现一个加减乘除的案例。

我们先把整体的分层结构写出来:

(function () {
  function init() {
    model.init(); // 组织数据,监听数据(数据代理)
    view.render(); // 组织 HTML 模版 + 渲染 HTML 模版
    controller.init(); // 事件处理函数的定义和绑定
  }

  // 数据层
  var model = {
  };

  // 视图层
  var view = {
  };

  // 控制层
  var controller = {
  };

  init();
})();

然后我们先把Model层的数据定义好:

(function () {
  function init() {
    model.init(); // 组织数据,监听数据(数据代理)
    view.render(); // 组织 HTML 模版 + 渲染 HTML 模版
    controller.init(); // 事件处理函数的定义和绑定
  }

  // 数据层
  var model = {
    // 数据管理
    data: {
      a: 0,
      b: 0,
      s: "+",
      r: 0,
    },
    // 对数据进行劫持
    init: function () {
      var _this = this;

      for (const key in _this.data) {
        Object.defineProperty(_this, key, {
          // 当使用 model.a 访问数据就被劫持
          get: function () {
            return _this.data[key];
          },
          set: function (newVal) {
            _this.data[key] = newVal;
            // 去进行更新视图的渲染
            view.render({
              [key]: newVal,
            });
          }
        });
      }
    }
  };

  // 视图层
  var view = {
  };

  // 控制层
  var controller = {
  };

  init();
})();

以上代码,我们在Model层的data中定义了要被劫持的数据,在init方法中通过Object.defineProperty对数据的getset进行相关处理,当set某个属性的时候,我们就会去更新视图层。

接下来我们去书写视图层的逻辑:

(function () {
  function init() {
    model.init(); // 组织数据,监听数据(数据代理)
    view.render(); // 组织 HTML 模版 + 渲染 HTML 模版
    controller.init(); // 事件处理函数的定义和绑定
  }

  // 数据层
  var model = {
    // 数据管理
    data: {
      a: 0,
      b: 0,
      s: "+",
      r: 0,
    },
    // 对数据进行劫持
    init: function () {
      var _this = this;

      for (const key in _this.data) {
        Object.defineProperty(_this, key, {
          // 当使用 model.a 访问数据就被劫持
          get: function () {
            return _this.data[key];
          },
          set: function (newVal) {
            _this.data[key] = newVal;
            // 去进行更新视图的渲染
            view.render({
              [key]: newVal,
            });
          }
        });
      }
    }
  };

  // 视图层
  var view = {
    el: "#app",
    template: `
    <div>
        <span class="cla-a">{{ a }}</span>
        <span class="cla-s">{{ s }}</span>
        <span class="cla-b">{{ b }}</span>
        <span>=</span>
        <span class="cla-r">{{ r }}</span>
    </div>
    <div>
        <input type="text" placholder="数字a" class="cal-input a" />
        <input type="text" placholder="数字b" class="cal-input b" />
    </div>
    <div>
        <button class="cla-btn">+</button>
        <button class="cla-btn">-</button>
        <button class="cla-btn">*</button>
        <button class="cla-btn">/</button>
    </div>
    `,
    render: function (mutedData) {
      // 处理数据更改
      // 首次在 init 执行的时候就会执行这里
      if (!mutedData) {
        // 利用正则表达式去匹配模版中的 {{ xxx }}
        // 然后把匹配到的 {{ xxx }} 替换为 Model 层中真实的数据
        this.template = this.template.replace(/{{(.*?)}}/g, function (node, key) {
          return model.data[key.trim()];
        });

        // 渲染到页面中
        var container = document.createElement("div");
        container.innerHTML = this.template;
        document.querySelector(this.el).appendChild(container);
      } else {
        // 遍历 Model 中的数据替换要更新的节点数据
        for (const key in mutedData) {
          document.querySelector(".cla-" + key).textContent = mutedData[key];
        }
      }
    }
  };

  // 控制层
  var controller = {};

  init();
})();

以上代码,我们在view层的template中给标签都增加了一个类名cla-*这是为了我们方便后面去更新对应的数据。

render方法中分别对第一次初始化和Model调用进行区分处理,渲染页面。

最后,就是我们的Controller层了:

(function () {
  function init() {
    model.init(); // 组织数据,监听数据(数据代理)
    view.render(); // 组织 HTML 模版 + 渲染 HTML 模版
    controller.init(); // 事件处理函数的定义和绑定
  }

  // 数据层
  var model = {
    // 数据管理
    data: {
      a: 0,
      b: 0,
      s: "+",
      r: 0
    },
    // 对数据进行劫持
    init: function () {
      var _this = this;

      for (const key in _this.data) {
        Object.defineProperty(_this, key, {
          // 当使用 model.a 访问数据就被劫持
          get: function () {
            return _this.data[key];
          },
          set: function (newVal) {
            _this.data[key] = newVal;
            // 去进行更新视图的渲染
            view.render({
              [key]: newVal,
            });
          }
        });
      }
    }
  };

  // 视图层
  var view = {
    el: "#app",
    template: `
    <div>
        <span class="cla-a">{{ a }}</span>
        <span class="cla-s">{{ s }}</span>
        <span class="cla-b">{{ b }}</span>
        <span>=</span>
        <span class="cla-r">{{ r }}</span>
    </div>
    <div>
        <input type="text" placholder="数字a" class="cal-input a" />
        <input type="text" placholder="数字b" class="cal-input b" />
    </div>
    <div>
        <button class="cla-btn">+</button>
        <button class="cla-btn">-</button>
        <button class="cla-btn">*</button>
        <button class="cla-btn">/</button>
    </div>
    `,
    render: function (mutedData) {
      // 处理数据更改
      // 首次在 init 执行的时候就会执行这里
      if (!mutedData) {
        // 利用正则表达式去匹配模版中的 {{ xxx }}
        // 然后把匹配到的 {{ xxx }} 替换为 Model 层中真实的数据
        this.template = this.template.replace(/{{(.*?)}}/g, function (node, key) {
          return model.data[key.trim()];
        });

        // 渲染到页面中
        var container = document.createElement("div");
        container.innerHTML = this.template;
        document.querySelector(this.el).appendChild(container);
      } else {
        // 遍历 Model 中的数据替换要更新的节点数据
        for (const key in mutedData) {
          document.querySelector(".cla-" + key).textContent = mutedData[key];
        }
      }
    }
  };

  // 控制层
  var controller = {
    init: function () {
      var oCalInputs = document.querySelectorAll(".cal-input"),
        	oBtns = document.querySelectorAll(".cla-btn"),
        	inputItem,
        	btnItem;

      // 给所有的输入框框绑定 input 事件
      for (let index = 0; index < oCalInputs.length; index++) {
        inputItem = oCalInputs[index];
        inputItem.addEventListener("input", this.handleInput, false);
      }

      // 给所有的按钮绑定 click 事件
      for (let index = 0; index < oBtns.length; index++) {
        btnItem = oBtns[index];
        btnItem.addEventListener("click", this.handleClick, false);
      }
      
    },
    // 处理表单输入
    handleInput: function (event) {
      var tar = event.target,
        	value = Number(tar.value),
        	field = tar.className.split(" ")[1]; // 拿到输入框的 a 和 b

      // 然后去操作 model 中对应的值,然后就会触发 set 机制,然后就去渲染 dom 
      model[field] = value;

      // ES3 的写法,和 model.r = xxx 一个意思
      // 详见MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/with
      with (model) {
        r = eval("a" + s + "b");
      }
    },
    handleClick: function (event) {
      var type = event.target.textContent;
      model.s = type;
      
      with (model) {
        r = eval("a" + s + "b");
      }
    }
  };

  init();
})();

以上代码,我们在Controller层分别给输入框和按钮绑定了相关的事件,当事件触发的时候它们就会去操作Model的数据,然后就会触发属性的set机制,我们在set机制里面调用了View层的render方法,这个时候就产生数据模版,页面也就跟着变化。

手写简单的 MVC 模式

🏖️ MVC 的缺点

通过上面的案例我们发现,这样的设计模式还不是特点的完美,View层本应该只关注于是数据的展示。但里面却包含了reander方法,我们希望的是有一套驱动,能把数据、视图、事件处理都放在一起集中处理,这就是ViewModel

MVC 是 MVVM 模型的雏形,MVVM 解决了驱动不内聚的缺点。

Model管理数据data =>> 通过ViewModel进行连接操作 =>> View关注视图

开发者只关注于ModelView就可以啦,回到Vue框架中,Vue是关注于视图渲染的。

另外Vue允许通过ref来直接操作DOM,所以严格来说Vue并没有完全遵循MVVM的模型,MVVM是强制MV完全分离的!

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