MVVM模式

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-12-15

MVVM(Model-View-ViewModel),即模型-视图-视图模型,是为视图层(View)量身定做一套视图模型(ViewModel),并在视图模型(ViewModel)中创建属性和方法,为视图层(View)绑定数据(Model)并实现交互。

MVVM可以通过创建视图反过来控制管理器实现组件需求,创建视图即是创建页面内的视图,因此本质上就是在页面中书写HTML代码,因此如果将视图作用提升,通过在页面中直接书写HTML代码创建视图组件,让控制器或者管理器去监听这些视图组件,并处理这些组件完成预期功能。这里所说的控制器或者管理器就是MVVM模式中的VM层,即视图模型层。

比如通过以下代码直接创建对应组件:

<div class="first" data-bind="type: 'slider', data: demo1"></div>
<div class="second" data-bind="type: 'slider', data: demo2"></div>
<div class="third" data-bind="type: 'progressbar', data: demo3"></div>

上面的代码即MVVM中的View层,我们可以通过HTML元素中自定义属性data-bind的值来确定这个组件的类型type,以及组件所需的数据模型data,即通过data-bind自定义属性值为元素绑定JavaScript行为。

对于View层内的元素是要被视图模型层ViewModel监听的,因此我们在ViewModel中实现对这些元素的监听,并为它们绑定行为。

1、创建VM环境

~(function () {
  var window = this || (0, eval)('this');
  // 自执行函数
  var VM = (function(){})();
  window.VM = VM;
}())

上述代码中的 (0, eval)('this') 用来使得window永远是指向全局对象的(在浏览器环境下就是window)。

(function () {
  "use strict";
  // 在严格模式下,匿名函数中的this为undefined
  // console.log('this', this); // undefined

  // 为了防止在严格模式下window变量被赋予undefined,使用(0, eval)(‘this’)就可以把this重新指向window对象
  // (0, eval) 执行完后就是 eval
  // 所以这一行的区别是间接调用eval和直接调用eval的区别
  // 直接调用时上下文为当前执行环境,间接调用时上下文为全局环境
  // var a = eval('this');
  // console.log(a); // undefined
  // var b = (0, eval)('this');
  // console.log(b); // window
  
  // 逗号运算符是二元运算符,它能够先执行运算符左侧的操作数,然后再执行右侧的操作数,最后返回右侧操作数的值。
  
  // 通过逗号表达式对它的操作数执行了GetValue,让this的值指向了全局对象
  // 这样在es5的严格模式下,也能获得全局对象的引用,而不是undefined了
  var window = this || (0, eval)('this');
}())

// 更直观的理解,就是改变this执行使其指向window
var foo = 'global.foo';
var obj = {
  foo: 'obj.foo',
  method: function () {
    return this.foo;
  }
}

console.log(obj.method()); // 'obj.foo'
console.log((1, obj.method)()); // 'global.foo'

2、使用VM,实现业务代码

效果图如下:

代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .ui-slider, .ui-progressbar {
      position: relative;
      height: 20px;
      background: #ccc;
      margin-bottom: 20px;
      border-radius: 20px;
    }

    .ui-slider div, .ui-progressbar div {
      height: 100%;
      background: red;
      border-radius: 20px;
    }

    .ui-slider b {
      position: absolute;
      right: 0;
      transform: translateX(150%);
    }

    .ui-slider em {
      position: absolute;
      transform: translateY(-30px);
    }

    .ui-slider .ui-slider-bar {
      position: absolute;
      height: 30px;
      width: 20px;
      top: -5px;
      background: pink;
    }
  </style>
</head>
<body>
  <div style="width: 1000px; border: 1px solid pink; padding: 30px;">
    <div class="template" data-bind="type: 'slider', data: demo1"></div>
    <div class="template" data-bind="type: 'slider', data: demo2"></div>
    <div class="template" data-bind="type: 'progressbar', data: demo3"></div>
  </div>
  <script src="./index.js"></script>
</body>
</html>
// index.js
~(function () {
  var window = this || (0, eval)('this');
  // 自执行函数
  var VM = (function(){
    // 创建策略对象
    var Method = {
      // 进度条组件创建方法
      // dom: 进度条容器
      // data: 进度条数据模型
      progressbar: function (dom, data) {
        var progress = document.createElement('div');
        var param = data.data; // 数据结构 { position: 50 }
        progress.style.width = (param.position || 100) + '%';
        dom.className += ' ui-progressbar';
        dom.appendChild(progress);
      },

      // 滑动条组件创建方法
      // dom: 滑动条容器
      // data: 滑动条数据模型
      slider: function (dom, data) {
        // 滑动条拨片
        var barEl = document.createElement('span');
        barEl.className += ' ui-slider-bar';

        // 滑动条进度容器
        var progressEl = document.createElement('div');
        // 滑动条总容量提示信息的dom
        var totalTipEl = null;
        // 滑动条拨片提示信息的dom
        var progressTipEl = null;
        // 数据模型结构 { position: 50, total: 100 }
        var param = data.data;
        // 容器元素宽度
        var width = dom.clientWidth;
        // 容器元素横坐标值
        var left = dom.offsetLeft;
        // 拨片位置 以数据的position和容器宽度来计算
        var realWidth = (param.position || 100) * width / 100;

        // 清空容器 为创建滑动条做准备
        dom.innerHTML = '';

        // 如果数据模型中提供了容器总量信息,则创建相关提示文案的dom
        if (param.total) {
          totalTipEl = document.createElement('b');
          progressTipEl = document.createElement('em');
          totalTipEl.innerHTML = param.total;
          dom.appendChild(totalTipEl);
          dom.appendChild(progressTipEl);
        }

        // 设置滑动条的方法
        function setStyle(w) {
          // 设置进度
          progressEl.style.width = w + 'px';

          // 设置进度条上的拨片
          // 将拨片的定位减20 以盖住下面的进度条展示 20为拨片的宽度 实际开发可以通过传参灵活设置 这里直接就写死
          var barLeft = w - 20;
          // 与0取最大值,确保方便不会画出容器
          barEl.style.left = Math.max(0, barLeft) + 'px';

          if (progressTipEl) {
            // 将拨片上方的提示文字即进度条信息提示 减去30 以呈现在拨片的正中间 并通过样式控制其移动到拨片的正上方
            // 需要注意的是,barEl 和 progressTipEl 都会通过样式控制其相对于组件为绝对定位
            progressTipEl.style.left = (w - 30) + 'px'; 
            // 通过当前进度条的位置 除以整个容器的位置 得到该拨片的提示信息即当前进度的百分比
            progressTipEl.innerHTML = parseFloat(w / width * 100).toFixed(2) + '%';
          }
        }

        setStyle(realWidth);
        dom.className += ' ui-slider';
        dom.appendChild(progressEl);
        dom.appendChild(barEl);

        // 让滑动条动起来
        barEl.onmousedown = function () {
          // 当鼠标在拨片上时 允许鼠标能在整个document上自由滑动并控制拨片 并改变滑动条的样式
          document.onmousemove = function (event) {
            var e = event;
            // 鼠标相对于容器的横坐标
            var w = e.clientX - left;
            // 设置滑动条
            // 滑动条大于等于20并小于等于容器宽度width  20为拨片的宽度
            setStyle(w < width ? Math.max(0, w) : width);
          }

          // 阻止页面滑动选取事件
          document.onselectstart = function () {
            return false;
          }

          // 停止滑动
          document.onmouseup = function () {
            document.onmousemove = null;
            document.onselectstart = null;
          }
        }
      }
    }

    // 获取视图层中渲染数据的映射信息
    function getBindData(dom) {
      var data = dom.dataset.bind;
      // 如果视图中是 data-bind="type: 'slider', data: demo1"
      // 那么getBindData这里返回的就是,
      // return { type: 'slider', data: demo1 } 这个对象 demo1在下面会用到 为了方便会是一个全局对象
      return !!data && (new Function("return ({" + data + "})"))();
    }

    // 组件实例化方法 VM自执行函数 返回这个实例化方法
    return function () {
      // 获取页面所有的div元素 仅示例
      var doms = document.body.getElementsByClassName('template');
      var ctx = null;

      // 遍历处理所有通过data-bind绑定了数据的dom
      for (var i = 0; i < doms.length; i++) {
        console.log(1);
        // 获取到数据
        ctx = getBindData(doms[i]);
        console.log(ctx)
        // 根据type调用对应的创建视图的方法
        ctx.type && Method[ctx.type] && Method[ctx.type](doms[i], ctx);
      }
    }
  })();
  window.VM = VM;
}())

var demo1 = { position : 60, total: 200 }; // 有拨片有提示文案的滑动条
var demo2 = { position : 20 }; // 有拨片没有提示文案的滑动条
var demo3 = { position : 50 }; // 这个数据对应的type是progress,为最简易版的进度条

window.onload = function () {
  VM();
}

MVVM模式中是视图层的作用得到了提升,通过在视图上绑定的数据然后在视图模型层取到对应的数据渲染对应的视图,MVVM模式使视图层更灵活,可以独立于数据层、视图模型层独立修改,自由创建,当然这也需要在视图模型层对其绑定的数据针对性的做“渲染”,同样也使得视图模型层做的这些“渲染”(逻辑代码)变得更加复用,只有在HTML你想在增加的位置,增加绑定了对应数据的dom结点,就可以将其渲染出来,这样约定好绑定数据的格式和规范,那么可以使那些不懂JavaScript的人,只要了解HTML内容并按照视图层规范格式即可创建一个复杂的页面视图,而让那些开发人员可专注于开发视图模型层里面的业务逻辑了。