前言

前端结构一路走来,从最原始的DOM操作,到jQuery-like之类的DOM操作东西库,再到后来的AMD、 CMD模块化开发,以及后续涌现出的一系列MVC、MVVM结构等,本质都是为了让前端开发愈加职责清楚,快速迭代 。

假如咱们细心考虑,咱们就会发现不论咱们采用哪种结构,亦或许直接操作底层DOM来安排前端代码,咱们潜意识都会将前端安排架构从凌乱、松懈的结构渐渐向树形结构挨近进,构成一颗隐形树然后进行办理。

为什么都会向树形结构挨近呢?

那么咱们就有必要了解一下数据结构中如何定义的树以及有哪些优势。

— 摘自维基百科-树(数据结构)

它是由n(n>0)个有限节点组成一个具有层次联系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:

  • 每个节点都只有有限个子节点或无子节点;
  • 没有父节点的节点称为根节点;
  • 每一个非根节点有且只有一个父节点;
  • 除了根节点外,每个子节点能够分为多个不相交的子树;
  • 树里面没有环路(cycle)

如下所示:

从0到1纯手工打造前端结构

优势显而易见:

  1. 有向图,安排架构清晰
  1. 树是连通的,给定一个节点,就能够知道该节点下的子孙;以及知道该节点上的父辈;

根据树形结构的优势,咱们还会发现生活中也用到了很多,典型就是宗族、公司人员办理。

方针

凭借ES6现有才能,自己完成一套小型前端结构:

  1. 将松懈、无序模块(组件)转化成一颗树
  1. 节点之间能良好通讯
  1. 数据更新触发页面实时更新
  1. 组件毁掉实时释放占用内存

思路

为了到达上述方针,咱们逐一击破。

转化树

为了将模块亦或许组件,安排成树形结构,必定咱们会想到写一个基类,以其为纽带,将松懈、无序的代码结构转变成一颗有序树。

节点互通

转变成一颗有序树后,模块、组件之间还涉及到通讯问题:

  • 父节点告诉子孙节点

    • 在相关之际,咱们能够将子节点存储在父节点下统一办理,需求的时候,经过遍历拿到指定子节点
    • 假如父节点,想要跨层获取孙子以及子孙节点,咱们能够经过父节点->子节点->…->指定子孙节点,递归获取
  • 子孙节点告诉父辈节点

    • 在相关之际,咱们一起也能够将父节点注入子节点之中,这样后续子节点需求的时候,经过已相关的父节点引证,直接告诉
    • 假如子节点,想要跨越多层,告诉父辈节点,那么咱们能够经过子节点->父节点->…->指定父辈节点,递归获取
  • 堂兄弟之间互相通讯

    • 根据上述思路,父子节点已能完结通讯,那么堂兄弟间,能够凭借一起的父辈来进行通讯
    • 一起父辈,隔了几代,为了不使代码结构变得强耦合,咱们也能够经过发布/订阅形式来到达堂兄弟之间的通讯

数据更新

运用ES6的模版字符串,动态注入更新数据,采用局部更新页面,如下

const template = function ({name}) {
    return `<div class="slot1"></div><div class="slot2"></div>`;
};
init() {
 this.$el.find(('.slot1'))
}
const data = {
    name: 'nian'
};
this.$el.html(template(data));

组件毁掉

考虑组件下面挂载的子组件以及内存占用问题

完成

有了上述思路,下面咱们就来一一完成吧。

转化树

写一个基类,一切模块、组件承继于它。

export default class BasicView {
    constructor() {
        this._components = {};
    }
    render () {}
    registerComponent (name, component) {
        // 已注册,先毁掉
        if (this._components.hasOwnProperty(name)) {
            let comp = this._components[name];
            // 毁掉remove待写
            comp.remove();
        }
        // 父子相关
        this._components[name] = component;
        component._parentView = this;
        component._componentName = name;
        return component;
    }
    remove() {
        // todo
    }
}

如上所示,调用registerComponent办法,经过this._components和component._parentView相关了父子,并回来component,这样在父节点就能够获取到该子节点了。

但,假如有多层级,想访问子节点的子孙,那么咱们依然经过registerComponent回来的component来获取代码会变得不可控,而且会重复处理反常。

故,咱们需求在基类BasicView,提供getComponent办法来到达获取子组件才能,如下

export default class BasicView {
    constructor() {}
    render () {}
    registerComponent (name, component) {/* 坚持不变 */}
    getComponent (name) {
        if (this._components.hasOwnProperty(name)) {
            let comp = this._components[name];
            return comp;
        } else {
            throw new Error(`${name}组件不存在`);
        }
    }
    remove() {/* todo */}
}

经过承继BasicView基类,这样就将松懈无序的模块以及组件,转化成了一棵笼统树。

节点互通

  • 父节点告诉子节点,咱们能够运用上述getComponet办法来做到,如下

    • class ChildComponent extends BasicView {
          method() {
              // todo
          }
      }
      class ParentComponent extends BasicView {
          init () {
              this.comp = this.registerComponent('childComponent', new ChildComponent());
          }
          operateChild () {
              // this.comp.method();
              this.getComponent('childComponent').method();
          }
      }
      
  • 子节点告诉父辈节点,咱们能够凭借在registerComponent时,相关在子节点上的_parentView

    • 最简略的办法,咱们能够在BasicView下写一个getParent办法得到父节点并操作

      • export default class BasicView {
            constructor() {
                this._components = {};
            }
            render () {}
            registerComponent (name, component) {/* 坚持不变 */}
            getComponent() {/* 坚持不变 */}
            getParent() {
                return this._parentView || {};
            }
            remove() {/* todo */}
        }
        // demo
        childComponent.getParent().xxx();
        
      • 但,假设这么做,父子之间耦合度太深,子组件还需知道父组件或许约好父组件有哪些公共办法,扩展性以及独立性极差,缺陷多多。
      • 所以,咱们需求解耦,子节点只需抛出告诉事件,具体执行细节在父辈组件执行,这样就极大削减了耦合度,俗称控制回转,如下
      • class ChildComponent extends BasicView {
            notifyParent (data) {
                this.trigger('eventName', data);
            }
        }
        class ParentComponent extends BasicView {
            constructor () {
                this.appEvents = {
                    'eventName childComponent': 'notifyMethod'
                };
            }
            init () {
                this.comp = this.registerComponent('childComponent', new ChildComponent());
            }
            notifyMethod (data) {
                // 实践执行细节
            }
        }
        
      • 要到达这一作用,咱们需求在基类BasicView中,完成这一细节trigger办法,如下
      • export default class BasicView {
            constructor() {
                this.appEvents = {};
                this._components = {};
            }
            render () {}
            registerComponent (name, component) {/* 坚持不变 */}
            getComponent() {/* 坚持不变 */}
            trigger(eventName, ...data) {
                let parent = this._parentView;
                if (parent) {
                    let componentName = this._componentName;
                    // emitComponentEvent完成细节todo
                    parent.emitComponentEvent(eventName, componentName, ...data);
                    // 往上持续传播
                    parent.trigger(eventName, ...data);
                }
            }
            remove() {/* todo */}
        }
        
        1. emitComponentEvent办法完成,需求拿到父节点appEvents,逐一匹配,成功后执行相关办法,不然不做任何操作,完成细节如下
      • export default class BasicView {
            constructor() {
                this.appEvents = {};
                this._components = {};
            }
            render () {}
            registerComponent (name, component) {/* 坚持不变 */}
            getComponent() {/* 坚持不变 */}
            emitComponentEvent (event, componentName, ...data) {
                let delegateEventSplitter = /^(S+)s*(S+)$/;
                Object.keys(this.appEvents).forEach( (key) => {
                    let funcName = this.appEvents[key];
                    let match = key.match(delegateEventSplitter);
                    let eventName = match[1],
                        selector = match[2];
                    if (selector === componentName && event === eventName) {
                        this[funcName] && this[funcName](...data);
                    }
                });
            }
            trigger(eventName, ...data) {
                let parent = this._parentView;
                if (parent) {
                    let componentName = this._componentName;
                    // emitComponentEvent完成细节todo
                    parent.emitComponentEvent(eventName, componentName, ...data);
                    // 往上持续传播
                    parent.trigger(eventName, ...data);
                }
            }
            remove() {/* todo */}
        }
        
  • 堂兄弟节点之间互相通讯

    • 凭借一起的父辈节点,以其为纽带进行转发,如下:
    • class ChildComp1 extends BasicView {
          notifyComp2 (data) {
              this.trigger('eventName', data);
          }
      }
      class ChildComp2 extends BasicView {
          method() {}
      }
      class ParentView extends BasicView {
          constructor () {
              this.appEvents = {
                  'eventName child1': 'comp1NotifyComp2'
              };
          }
          init () {
              this.comp1 = this.registerComponent('child1', new ChildComp1());
              this.comp2 = this.registerComponent('child2', new ChildComp2());
          }
          comp1NotifyComp2 (data) {
              this.comp2.method(data);
          }
      }
      
    • 运用发布/订阅者形式,削减层级太深过度耦合

数据更新

前面咱们已处理将松懈的结构转化成一颗笼统树,且完成了节点之间的通讯。

那么,怎样将这棵树反应到页面中呢?

首先,咱们考虑单个节点怎样将数据与模版映射。

运用ES6的字符串模版即可,而且咱们考虑到目前浏览器的功能都不错,当数据更新时,咱们完全能够批量将模版更新到页面中。

注:这里采用原生DOM形式,后续可根据状况,替换成虚拟DOM

如下

class ComponentView extends BasicView {
    constructor() {
        this.$el = $('<div class="componentView" />');
    }
    template(data) {
        const { name } = data;
        return `我是${name}`;
    }
    async render() {
        const data = await axios.fetch('xxxx');
        this.$el.html(this.template(data));
    }
}

单个节点处理,那么咱们就能够运用el,凭借基类registerComponent办法将零星的el,凭借基类registerComponent办法将零星的el也组装成为一棵树,如下

export default class BasicView {
    constructor() {
        this.appEvents = {};
        this._components = {};
    }
    render () {}
    registerComponent (name, component, container, dontRender) {
        if (this._components.hasOwnProperty(name)) {
            let comp = this._components[name];
            comp.remove();
        }
        this._components[name] = component;
        component._parentView = this;
        component._componentName = name;
        // 父节点$el挂载子节点$el
        if (container) {
            if (typeof container === 'string') {
                this.$el.find(container).append(component.$el);
            } else {
                $(container).append(component.$el);
            }
            if (dontRender !== true) {
                component.render();
            }
        }
        return component;
    }
    getComponent() {/* 坚持不变 */}
    emitComponentEvent (event, componentName, ...data) {/* 坚持不变 */}
    trigger(eventName, ...data) {/* 坚持不变 */}
    remove() {/* todo */}
}

demo如下

class ChildComp1 extends BasicView {
    constructor() {
        this.$el = $('<div class="childComp1" />');
    }
    notifyComp2 (data) {
        this.trigger('eventName', data);
    }
    remove () {
        // todo
        super.remove()
    }
}
class ChildComp2 extends BasicView {
    constructor() {
        this.$el = $('<div class="childComp2" />');
    }
    method() {}
}
class ParentView extends BasicView {
    constructor () {
        this.$el = $('<div class="parentView" />');
        this.appEvents = {
            'eventName child1': 'comp1NotifyComp2'
        };
    }
    template() {
        return `<div class='childContainer'></div>`;
    }
    init () {
        this.comp1 = this.registerComponent('child1', new ChildComp1(), '.childContainer');
        this.comp2 = this.registerComponent('child2', new ChildComp2(), '.childContainer');
    }
    comp1NotifyComp2 (data) {
        this.comp2.method(data);
    }
}

组件毁掉

当咱们需求移除页面中指定组件时,咱们需求考虑该组件下的一切子孙节点,以及相关内存泄漏等因素,所以在基类BasicView下,补充remove办法,如下

export default class BasicView {
    constructor() {
        this.appEvents = {};
        this._components = {};
    }
    render () {}
    registerComponent (name, component) {/* 坚持不变 */}
    getComponent() {/* 坚持不变 */}
    emitComponentEvent (event, componentName, ...data) {/* 坚持不变 */}
    trigger(eventName, ...data) {/* 坚持不变 */}
    remove() {
        this._components && Object.keys(this._components).forEach( (key) => {
            this._components[key].remove();
        });
        this.$el.remove();
    }
}