9.组件原理详解

在以往的日常前端开发中,使用最原始的html元素来拼凑整个页面,随着页面慢慢的复杂起来,如果还是用这种方式,就会出现以下问题:

  1. 页面使用了大量的html元素,导致页面卡顿;
  2. 维护页面困难,大量的html元素需要手动去更改;
  3. 大量的业务代码和渲染代码混杂,业务更改导致页面显示不正常,调试会浪费很多的时间。

在市面有很多框架使用了mvvm的架构,通过数据驱动页面的更改,确实能解决部分问题,然而却引入了一大堆的定义数据格式代码,往往控制一个状态需要双倍的代码,且并没有发挥html的本质意义。

之前关于Page对象的介绍,Page对象主要是用于处理业务,如果在渲染层没有简化Page实例对象更改渲染的话,最后的结果是和以往的页面是类似的。因此我想到的解决方案,就是引入一种基于dom思想理念的一种组件方式,将业务逻辑抽离出来,然后通过调用组件的属性和方法对组件进行操作,通过监听组件来触发组件的事件,Page与组件的交互和Page与dom的交互保持一致。

引入Compoent对象的意义在于,分担Page对象处理渲染层的压力。举个简单的例子,就拿原生的时间输入框,我们把它当作一个组件,在使用得时候,我们只需要引入html标签

<input type="date">

关于里面的细节放在组件定义上实现,在业务实现中不需要理解组件的实现原理,如果我们要改变组件的值或者改变它的行为

input.value = "2014-10-01";
input.focus();

如果组件进行了更改,我们可以监听事件来进行交互

input.addEventListener("change", fn, false);

与其它框架不同,这里引入的组件是一个符合标准的xml格式的标签,它的属性值只能是string或数值,不能包含数组或对象。因为保持标准的html结构才能让html可以跨框架,即使放在原生浏览器中,也能保持良好的表现形式,即使不依赖任何js。

这一篇主要介绍组件的原理,处理组件的生命周期,以及如何和Page对象的交互。
下一篇主要介绍组件的定义,怎么样创建一个标准的组件,包括一些技巧。

需求

类似的,我们需要组件和Page对象、service一样,可以按需引入。我们希望组件有以下的功能:

  1. 和其它对象一样,拥有初始化,销毁等方法,在不需要的时候减少内存的使用;
  2. 使用方式尽量要和原生dom要一致;
  3. 具有自定义组件功能,可以根据不同的场景填充不同的内容,类似slot;
  4. 配合Page在history上的操作,拥有保存和快速还原等功能。

实现思路

**首先,**引入了组件之后,我们可以把页面渲染结构看作下面的图。

组件图

每个页面都包含若干个组件,同时每个组件也有若干个组件。在每个页面中,应该是一棵组件树。

在上一篇中,我们抽出了HtmlProto,Page和Component都是和HTML有关的,将它们都继承于HtmlProto对象,因此他就实现了初始化,销毁等方法。在HtmlProto上加入一个数组,专门存放Component对象数组,同时加一个属性uid,代表唯一的值。

**其次,**我们在页面上插入html的时候,我们要做的处理应该在html放在fragment上时候,然后进行如下操作:

  1. 组件筛选,将它们用临时dom替换并标记,解析标签内的属性和内容,作为数据保存起来;
  2. 通过标签引入组件的js,然后实例化组件对象,在组件初始化的时候,可能其中内容包含其它组件,将会是一个递归的过程,直到初始化完毕;
  3. 将生成的fragment替换之前的临时dom。

这三个过程就是组件的初始化过程,无论是在页面上还是组件上,都是同样的。

**最后,**在业务处理后,如果页面是保存操作,那每个组件都要进行保存。如果页面是直接销毁,每个组件也就销毁了。另外的,如果该页面是从历史中取出来的,那么组件执行的是restore过程,restore中的组件执行和页面逻辑是一样的,快速还原组件状态。

最终代码如下

HtmlProto的initialize方法

initialize: function (dom, html, option, feeback) {
    option = option || {};
    this.parentDom = dom;
    this.template.innerHTML = html;
    var fragment = this.template.content;
    // 第一步在fragment筛选符合条件的元素
    var components = this._beforeInitComponent(fragment, html); 

    var that = this;
    // 初始化组件
    this._initComponent(components, function () {
        // 组件因为是替换操作,没有parentDom,所以保存nodes,后续进行相同的操作,
        // 在组件的操作中,要保持nodes的排序一致性
        that.beforeInitialize(fragment);
        that.getDomObj();
        // staticPage做插入文档操作
        if (option.method === "insert") dom.insertBefore(fragment, dom.firstChild); 
        // 组件做replace
        else if (option.method === "replace") dom.parentNode.replaceChild(fragment, dom); 
        else if (option.method === "before") dom.parentNode.insertBefore(fragment, dom);
        else { // Page做默认覆盖操作
            dom.innerHTML = "";
            dom.appendChild(fragment);
        }
        that._beforeInit(feeback);
    });
},

其中_beforeInitComponent使用了TreeWalker对象, 使用了过滤方法

var tree = document.createTreeWalker(dom, NodeFilter.SHOW_ELEMENT, componentfilter.filter, false);
var component = tree.firstChild();
while (component) {
    var obj = {
        component: component, // 元素
        id: component.id, // id
        name: component.tagName.toLowerCase(), // tagName
        slot: component.innerHTML, // 插槽
        dataset: component.dataset, // 自定义数据
        property: {}, // 属性
        originHTML: originHTML // 原始html
    };
    var attributes = component.attributes;
    for (var i = 0; i < attributes.length; i++) {
        var attribute = attributes[i];
        obj.property[attribute.name] = attribute.value; // 将属性放在property上
    }
    components.push(obj);
    component = tree.nextNode();
}

其中的componentfilter

var componentfilter = (function () {
    var preNode = null, pattern = /^(\w+-)+\w+$/;
    return {
        destroy: function () {
            preNode = null;
        },
        filter: function (node) {
            if (node.constructor.name === "HTMLElement" && pattern.test(node.tagName)) {
                if (preNode && preNode.contains(node)) return NodeFilter.FILTER_SKIP;
                preNode = node;
                return NodeFilter.FILTER_ACCEPT;
            }
            return NodeFilter.FILTER_SKIP;
        }
    }
})();

这里_initComponent方法主要是把上面的components生成Component对象,最终的是_addComponent方法

_initComponent: function (components, next) {
    if (components.length === 0) return next();
    var len = components.length;
    var feeback = function () {
        if (--len === 0) {
            next();
        }
    }
    for (var i = 0; i < components.length; i++) {
        this._addComponent(components[i], feeback);
    }
},
_addComponent: function (component, feeback) {
    var that = this;
    var app = this._getApp();
    // 加载组件定义js。
    app.getComponentByName(component.name, function (com, config) {
        if (com) {
            var newComponent = new com(), id = component.id;
            newComponent.baseUrl = getBaseUrl(config.js);
            if (id) newComponent.id = id;
            newComponent.parent = that;
            extend(newComponent.data, component.dataset); // 声明在html上的data赋值
            extend(newComponent.dataset, component.dataset); // 保持html的一致
            extend(newComponent.property, component.property); // 把属性附加到组件中
            newComponent.slot = component.slot;
            newComponent.render(function (html) {
                // 这是将定义的slot和html上的标记结合起来
                var nextHtml = newComponent.initSlot(html, newComponent.slot);
                // 如果组件中包含其它组件,这是一个递归的过程
                newComponent.initialize(component.div, nextHtml, {
                    method: "replace"
                }, function () {
                    that.components.push(newComponent);
                    if (typeof feeback === "function") feeback();
                });
            })
        } else {
            if (typeof feeback === "function") feeback();
        }
    })
},

下面是initSlot方法,这个方法在Component的原型对象中声明
这里的难点在于相同的组件可以多层嵌套。和模板渲染引擎的情况有点像,无法用正则匹配。

对于html中使用

  <a-btn>
        <div slot="content">content</div>
        <div slot="name">name</div>
  </a-btn>

组件定义

  <button class="a-btn-style">
    <slot name="name">a</slot>
    <slot name="content">b</slot>
  </button>

生成的组件是会变成这样

  <button class="a-btn-style">
        <div>name</div>
        <div>content</div>
  </button>

如果组件定义中slot没有被html中定义,将会默认以slot的元素的形式渲染,如果是包含多个元素,比如将content的slot变成多个元素,可以用以下

  <template slot="content"><div>1</div><div>2</div></template>

在Component实例对象中,都有一个slots数组,他们是保存插槽的信息,插槽的进一步使用,需要组件中实现。具体initSlot的方法实现,参见源代码

组件和原生dom一样,都可以通过定义事件,监听事件来进行交互,因为组件也是通过html元素方式嵌入页面的,它也支持dom事件,通过自定义事件可以让事件进行冒泡。以下是触发自定义事件的方法

dispatchCustomEvent: function (name, data) {
    if (this.nodes.length > 0) {
        var event = document.createEvent("CustomEvent");
        event.initCustomEvent(name, true, true, data);
        this.nodes[0].dispatchEvent(event);
    }
    return this;
},

在最后一步,组件的save和restore,是一个解析树的过程,统一先将元素组components先save或restore,如下代码

save: function () {
    HtmlProto.prototype.save.call(this);
    if (typeof this.beforeSave === "function") {
        this.beforeSave();
    }
    for (var i = 0; i < this.components.length; i++) {
        this.components[i].save();
    }
    var div = document.createElement("div");
    div.id = this.uid;
    div.className = "loadinghidden";
    if (this.nodes.length > 0 && this.nodes[0].parentNode) this.nodes[0].parentNode.insertBefore(div, this.nodes[0]);
},
restore: function () {
    var fragment = this.template.content;
    var div = document.getElementById(this.uid);
    for (var i = 0; i < this.nodes.length; i++) {
        fragment.appendChild(this.nodes[i]);
    }
    if (div && div.parentNode) {
        div.parentNode.replaceChild(fragment, div);
    }

    for (var i = 0; i < this.components.length; i++) {
        this.components[i].restore();
    }

    if (typeof this.afterRestore === "function") {
        this.afterRestore();
    }
    this._init();
},

与Page的restore和save执行流程类似。

案例地址

结语

这里主要介绍了Component的原理,如何将Component对象内嵌到页面中,将Component的使用方式尽量和原生的元素保持一致.

推广

底层框架开源地址:https://gitee.com/string-for-100w/string
演示网站: https://www.renxuan.tech/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值