web前端mvc库实现

朱建

web前端mvc库实现

前言

随着前端应用日趋复杂,如今如angular,vue的mvvm框架,基于virtual dom的react等前端库基本成为了各个公司的首选。而以当初最流行的头号大哥backbone为代表的mvc库基本退出了历史舞台。

在现如今人人都说mvvm/react多好,backbone多差的时代。笔者看别人文章,看的时候总是感觉好像有点道理,看完之后如耳边风一般左耳朵进,右耳朵出。

so,痛定思痛之后,笔者定了个小目标,实现了份简易版的backbone库。以设计,实现的角度来对比其它类型库的差异。

ok,废话不多说,上正菜。

思路整理

MVC即将前端应用抽象为Model,View,Control三大模块。View为用户视图,通过浏览器事件接受用户输入。Model为数据模型,他可以随时和后端同步数据。Control则是具体实现View派发的事件,计算并改变Model的数据。

UI可以被抽象为模版+数据,随着用户不断的触发浏览器提供的各种事件,交互不断的进行,Control接受了View指令改变着Model的数据,而View则随着Model的改变做出响应,最终展现在用户面前。

流程图:

图片描述

模块划分

本篇文章的思路来自于backbone,并屏弃了耦合的后端操作。早期MVC并没有对Control做严格的划分,也许是数据的改变计算并不那么复杂,所以Control功能在View的事件内完成了,也就是说View模块里面耦合了Control的功能。

但近几年flux的action,store的出现,View调用action,具体数据变化计算则在store内部实现,也算是把Control功能从View内部抽象出来了吧。

Event模块

为对象提供对事件的处理和回调,内部实现了观察者(订阅者)模式,如view订阅了model的变化,model变化之后则通知view。

基本方法。
  • on函数通过event名,在object上绑定callback函数,将回调函数存储在数组里。

  • off函数移除在object上绑定的callback函数

    • 通过event名移除指定callback。如object.off("change", onChange)

    • 通过event名移除所有callback。如object.off("change")

    • 移除所有指定callback。如object.off(null, onChange);

    • 移除所有指定context的callback。如object.off(null, null, context);

    • 清空object所有的callback。如object.off()

  • trigger函数通过event名,找到object对应的数组,并触发所有数组内回调函数。

注意事项

其所有方法应该支持类似on(name,callback),on('name1 name2 name3',callback), on({name1:callback1,name2:callback2})

这时候则可以抽象内部公用方法。通过递归的方式,on({name1:callback1,name2:callback2})类型的和on('name1 name2 name3',callback)类型,最终转化为最基本的on(name,callback)类型。核心代码如下:

this.eventsApi = function (iteratee, name, callback, context) {
    let event;
    if (name && typeof name === 'object') {
      Object.keys(name).forEach(key=> {
        event = this.eventsApi(key, name[key], context);
      })
    } else if (SEPARATE.test(name)) {
      var keys = name.split(SEPARATE);
      keys.forEach(key=> {
        event = iteratee.call(this,key, name[key], context);
      });
    } else {
      event = iteratee.call(this,name, callback, context);
    }
    return event;
};

View模块

  • 无状态,实例化的时候可以对应多个model实例,并以观察者的身份观察这些model的变化,通过这些model数据,加上指定的模版渲染dom,展示UI。

  • 销毁的时候注销掉所有model的观察,取消与相关model之间的关联。

  • 实例化的时候通过事件委托注册浏览器事件

实现
    • _ensureElement,确保View有一层dom包裹,如果this.el这个dom不存在,则通过id,className,tagName创建一个dom并赋值于this.el。

    • listenTo,将model与view实例关联起来,并收集关联model,存储于listenTo数组内,内部实现则是调用model的on函数

    • stopListening,view销毁前调用,通过listenTo数组找到关联model,并取消view与这些model之间的观察者关系。

    • $,将dom的查找定位在 this.$el下

    • delegateEvents,事件委托,以{'click #toggle-all': 'choose'}为例,为在this.el子节点的id等于toggle-all的dom注册click事件choose函数。核心代码如下:

    delegateEvents: function (events) {

       var $el = this.$el;
       Object.keys(events).forEach(item=> {
         var arr = item.split(' ');
         if (arr.length === 2) {
           var event = arr[0];
           var dom = arr[1];
           $el.on(event + '.delegateEvents' + this.$id, dom, this[events[item]].bind(this));
         }
       })
     },
    
    • undelegateEvents,注销掉通过delegateEvents注册的dom事件

    Model模块

    Model在backbone里被抽象为object类型的Model和array类型的Collection

    • 承载着应用的状态,可以随时和后端保持同步。

    • 内部实现了对数据变化的监听,一旦发生变化则通知观察者View发生变化。

    Model

    监听数据的变化,对model的修改,删除之后调用对应的trigger函数,通知订阅了model变化的view。

    • set函数,改变model数据,并触发change事件

    set: function (obj) {
        this._changing = true;
        this.changed = obj;
        this._previousAttributes = Object.assign({}, this.attributes);
        this.attributes = Object.assign({}, this.attributes, obj);
        const keys = [];
        Object.keys(obj).forEach(key=> {
          keys.push(key);
          this.trigger('change:' + key, this);
        }, this);
    
        if (keys.length > 0) {
          this.trigger('change', this);
        }
        this._changing = false;
     },
    
    • destroy函数触发destroy事件

    destroy: function () {
       this.stopListening();
       this.trigger('destroy', this);
    },
    
    Collection

    提供数组类型models的push,unshift,pop,shift,remove,reset等功能。push,unshift实际调用add函数,pop,shift实际调用remove函数。

    • add函数支持任意索引插入指定数组,触发add事件。核心的代码如下:

        export const splice = (array, insert, at)=> {
          at = Math.max(0, Math.min(array.length, at));
          let len = insert.length;
          let tail = [];
          for (let i = at; i < array.length; i++) {
            tail.push(array[i]);
          }
          for (let i = 0; i < tail.length; i++) {
            array[i + at + len] = tail[i];
          }
          for (let i = 0; i < len; i++) {
            array[i + at] = insert[i];
          }
          return array;
        };
    
    • remove函数支持删除指定model,触发update事件。

    • _addReference,调用add方法新增model时,通过观察者模式增加该model与collection之间的关联,model的变化通知collection。核心代码如下:

        _addReference: function (model) {
          model.on('all',this._onModelEvent,this);
         }
    • _removeReference,调用remove,reset移除model时,取消该model与collection关联。核心代码如下:

        _removeReference: function(model) {
           if (this === model.collection) delete model.collection;
           model.off('all', this._onModelEvent, this);
        }
    

    extend

    生产环境下需要在保留原生View,Model类的功能情况下做一些业务拓展,这时候需要用到类的继承。

    虽然es6支持extend继承,但这边我还是手写了一份。思路则是返回一个构造函数,该函数的原型为新的实例对象props,而props的原型对象则是父函数的原型(有点拗口,自己看代码理解)。
    核心代码如下:

    export const extend = function (props) {
      var parent = this;
      var child = function () {
        parent.apply(this, arguments);
      };
      child.prototype = Object.assign(Object.create(parent.prototype), props, { constructor: child });
      return child;
    };

    todomvc效果图

    图片描述

    源码

    web前端mvc实现

    小节

    整篇文章基本是围绕着如下2点

    • view-model,collection-model的观察者模式的实现展开,期间view,model的销毁则取消与之有关联对象的关系,如view销毁时,注销掉与之关联的model的回调函数。

    • 监听数据变化,并通知观察者作出响应,如model变化后触发trigger('change')

    好了,文章草草写到这了,多谢各位看官,以上也是纯个人观点,有问题欢迎各位web前端mvc设计指教。

    阅读 3.6k

    js源码分析
    一些js库的源码分析

    有空的时候给自己设定一个小目标。

    400 声望
    70 粉丝
    0 条评论

    有空的时候给自己设定一个小目标。

    400 声望
    70 粉丝
    文章目录
    宣传栏