原生JavaScript封装选项卡插件

0.095字数 951阅读 3231

一、初步实现选项卡

html

<div class="box" id="tabFir">
    <ul>
        <li class="selected">页卡一</li>
        <li>页卡二</li>
        <li>页卡三</li>
    </ul>
    <div class="selected">内容一</div>
    <div>内容二</div>
    <div>内容三</div>
</div>

css

* {margin: 0; padding: 0; font-family: "\5FAE\8F6F\96C5\9ED1", Helvetica, sans-serif; font-size: 14px;-webkit-user-select: none;}
ul,li {list-style: none;}
.box {width: 500px; margin: 20px auto;}
.box ul {position: relative; top: 1px; }
.box ul:after{content: ""; display: block; clear: both;}
.box ul li {float: left; margin-right: 15px; width: 100px; height: 30px; border: 1px solid green; line-height: 30px; text-align: center; cursor: pointer;}
.box ul li.selected {background: lightblue; border-bottom-color: lightblue}
.box div{height: 150px; line-height: 150px; background: lightblue; border: 1px solid green; text-align: center; display: none; }
.box div.selected{display: block;}

基础JavaScript

var tabFir = document.getElementById('tabFir'),
    oLis = tabFir.getElementsByTagName('li'),
    oDivs = tabFir.getElementsByTagName('div');

function changeTab(n) {
    for (var i = 0; i < oLis.length; i++) {
        oLis[i].className = null;
        oDivs[i].className = null;
    }
    oLis[n].className = 'selected';
    oDivs[n].className = 'selected';
}

错误绑定点击事件的方式

for (var i = 0; i < oLis.length; i++) {
    oLis[i].onclick = function () {
        changeTab(i);
    }
}

以上方式绑定点击事件是达不到效果的,因为JavaScript中所有的事件绑定都是异步编程的,开始我们只是给元素的点击行为绑定了一个方法,但是需要手动点击才会执行这个方法,再次期间,不会干等着点击,会继续执行下一次循环,当点击的时候,循环早已结束。

在给元素绑定事件的时候,绑定的这个方法还只是定义部分,此时方法中存储的都是字符串,此时我们看到的i只是一个字符。

当点击的时候,执行对应的绑定方法,形成一个私有的作用域A,在A中会使用到变量i,而i不是自己私有的,是上级作用域window下的i,此时window下的i已经变为oLis.lenght。

解决方式一:使用自定义属性

要操作(获取/修改)当前元素的某个值,但是还不想受执行顺序和环境的影响,我们最简单的方式就是把其放在自己的自定义属性上。

for (var i = 0; i < oLis.length; i++) {
    oLis[i].index = i;
    oLis[i].onclick = function () {
        changeTab(this.index);
    }
}

解决方式二:使用闭包

for (var i = 0; i < oLis.length; i++) {
    ~ function (index) {
        oLis[i].onclick = function () {
            changeTab(index);
        }
    }(i);
}

或者

for (var i = 0; i < oLis.length; i++) {
    oLis[i].onclick = (function (index) {
        return function () {
            changeTab(index);
        }
    })(i);
}

解决方式三:使用ES6中的let声明循环变量i

for (let i = 0; i < oLis.length; i++) {
    oLis[i].onclick = function () {
        changeTab(i);
    }
}

使用let,声明的变量仅在块级作用域内有效。

上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

二、深入剖析选项卡

以上的选项卡只能算是一个demo,是不能真正用于项目中的,因为获取切换的div用的是getElementsByTagName:


demo的方式.png

该方法是获取某个容器中的所有的div,如果在某个选项卡中也有div元素呢:

选项卡中还有div.png

此时用就失效了,所以我们可以用到DOM操作和样式操作库的封装中封装的DOM库,并且可以处理的更优雅:

var tabFir = document.getElementById('tabFir'),
    tabFirst = utils.firstChild(tabFir);
var oLis = utils.children(tabFirst);

for (var i = 0; i < oLis.length; i++) {
    oLis[i].onclick = function () {
        // --> 首先把兄弟元素的selected样式都移除掉
        var curSiblings = utils.siblings(this);
        for (var i = 0; i < curSiblings.length; i++) {
            utils.removeClass(curSiblings[i], 'selected');
        }
        // --> 再让当前点击这个元素有选中的样式
        utils.addClass(this, 'selected');
        
        // --> 再让当前的这个li父亲元素的所有的弟弟元素中(三个div)和当前点击的这个li索引相同的有选中的样式,其余的移除选中样式
        var index = utils.index(this);
        var divList = utils.nextAll(this.parentNode);
        for (i = 0; i < divList.length; i++) {
            i === index ? utils.addClass(divList[i], 'selected') : utils.removeClass(divList[i], 'selected');
        }
    }
}

三、简单封装

假如一个页面中有多处用到选项卡:

页面中有多处用到选项卡.png

这时候,如果是一个个的选项卡去处理就太麻烦了,也太冗余了,正确的做法是将选项卡的代码封装,让其变成通用的组件,可用于所有的符合结构的选项卡。

实现一个选项卡封装:我们可以分析出,只要多个选项卡的主体结构一样,那么每一个实现的思想都是一模一样的,唯一不一样的就是最外层的盒子不一样。

tab.js

~function () {
    /**
     * 封装一个选项卡的插件,只要大结构保持统一,以后实现选项卡的功能,只需要调取这个方法执行即可实现
     * @param container 当前要实现选项卡的这个容器
     * @param defaultIndex 默认选中项的索引
     */
    function tabChange(container, defaultIndex) {
        var tabFirst = utils.firstChild(container),
            oLis = utils.children(tabFirst),
            divList = utils.children(container, 'div');

        // --> 让defaultIndex对应的页卡有选中的样式
        defaultIndex = defaultIndex || 0;
        utils.addClass(oLis[defaultIndex], 'selected');
        utils.addClass(divList[defaultIndex], 'selected');

        // --> 实现具体的切换功能
        for (var i = 0; i < oLis.length; i++) {
            oLis[i].onclick = function () {
                var curSiblings = utils.siblings(this);
                for (var i = 0; i < curSiblings.length; i++) {
                    utils.removeClass(curSiblings[i], 'selected');
                }
                utils.addClass(this, 'selected');

                var index = utils.index(this);
                for (i = 0; i < divList.length; i++) {
                    i === index ? utils.addClass(divList[i], 'selected') : utils.removeClass(divList[i], 'selected');
                }
            }
        }
    }
    window.icemanTab = tabChange;
}();

使用:

var box1 = new icemanTab(boxList[0] , 0);
var box2 = new icemanTab(boxList[1] , 1);
var box3 = new icemanTab(boxList[2] , 2);

封装后的方法还加入默认选中。

四、利用事件委托再次封装

在上面的封装中,tab栏的点击是在每个li上加点击事件,我们这里使用事件委托的方式,可以让效率提高2~3倍。

// --> 使用事件委托来优化我们的点击操作
tabFirst.onclick = function (e) {
    e = e || window.event;
    e.target = e.target || e.srcElement;
    // --> 说明我当前点击的是li标签
    if (e.target.tagName.toLowerCase() === 'li') {
        detailFn.call(e.target, oLis, divList);
    }
}
function detailFn(oLis, divList) {
    // this --> 当前点击的这个li
    var index = utils.index(this);
    utils.addClass(this, 'selected');

    for (var i = 0; i < divList.length; i++) {
        i === index ? utils.addClass(divList[i], 'selected') : (utils.removeClass(divList[i], 'selected'), utils.removeClass(oLis[i], 'selected'));
    }
}

五、使用面向对象的方式最后封装

~function () {
    function tabChange(container, defaultIndex) {
        return this.init(container, defaultIndex);
    }
    tabChange.prototype = {
        constructor: tabChange,
        // 按照索引来设置默认选中的页卡
        defaultIndexEven: function () {
            utils.addClass(this.oLis[this.defaultIndex], 'selected');
            utils.addClass(this.divList[this.defaultIndex], 'selected');
        },
        // 事件委托实现绑定切换
        liveClick: function () {
            var _this = this;
            this.tabFirst.onclick = function (e) {
                e = e || window.event;
                e.target = e.target || e.srcElement;
                // --> 说明我当前点击的是li标签
                if (e.target.tagName.toLowerCase() === 'li') {
                    _this.detailFn(e.target);
                }
            }
        },
        detailFn: function (curEle) {
            // this --> 当前点击的这个li
            var index = utils.index(curEle);
            utils.addClass(curEle, 'selected');
            for (var i = 0; i < this.divList.length; i++) {
                i === index ? utils.addClass(this.divList[i], 'selected') : (utils.removeClass(this.divList[i], 'selected'), utils.removeClass(this.oLis[i], 'selected'));
            }
        },
        // 初始化,也是当前插件的唯一入口
        init: function (container, defaultIndex) {
            this.container = container || null;
            this.defaultIndex = defaultIndex || 0;
            this.tabFirst = utils.firstChild(this.container);
            this.oLis = utils.children(this.tabFirst);
            this.divList = utils.children(this.container, 'div');

            this.defaultIndexEven();
            this.liveClick();
            return this;
        },
    };
    
    window.icemanTab = tabChange;
}();

个人公众号(icemanFE):分享更多的前端技术和生活感悟

个人公众号.png

推荐阅读更多精彩内容