【源码学习】classnames

157 阅读1分钟

注:本文参加了由 公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

源码

github 地址:classnames

index 版本(默认版本)

/*!
  Copyright (c) 2018 Jed Watson.
  Licensed under the MIT License (MIT), see
  http://jedwatson.github.io/classnames
*/
/* global define */

// 使用自执行函数来包裹整个作用域,从而避免变量污染和冲突
(function () {
    'use strict'; // 严格模式

    var hasOwn = {}.hasOwnProperty; // hasOwnProperty() 方法用来判断对象的属性是否属于自身,而不是原型链上的

    // 未设置参数是为了不限制参数的个数,可通过 arguments 获取所有的参数
    function classNames() {
        var classes = []; // 存储最终类名合集的数组

        for (var i = 0; i < arguments.length; i++) { // 遍历 arguments
            var arg = arguments[i];
            if (!arg) continue; // 若参数为 falsy 值,则直接跳过

            var argType = typeof arg; // 获取参数类型

            // string 或 number 类型的参数直接加入 classes 中
            if (argType === 'string' || argType === 'number') {
                classes.push(arg);
            } else if (Array.isArray(arg)) {
                if (arg.length) {
                    var inner = classNames.apply(null, arg); // 若参数为非空数组,则递归调用 classNames
                    if (inner) { // 若递归调用的返回值为非空字符串,则将返回值加入 classes 中
                        classes.push(inner);
                    }
                }
            } else if (argType === 'object') {
                if (arg.toString === Object.prototype.toString) { // 等价于 Object.prototype.toString.call(arg) === '[object Object]'
                    for (var key in arg) {
                        if (hasOwn.call(arg, key) && arg[key]) { // 若 key 为该对象的可枚举属性且对应的 value 为 Truthy,则将 key 加入 classes 中
                            classes.push(key);
                        }
                    }
                } else { // 若参数为其他类型,则将该参数的 toString 返回值加入 classes 中
                    classes.push(arg.toString());
                }
            }
        }

        return classes.join(' '); // 将 classes 转换为字符串返回
    }

    // 不同的运行环境使用不同的导出方法
    if (typeof module !== 'undefined' && module.exports) { // CommonJS
        classNames.default = classNames;
        module.exports = classNames;
    } else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { // AMD
        // register as 'classnames', consistent with npm package name
        define('classnames', [], function () {
            return classNames;
        });
    } else { // 浏览器环境
        window.classNames = classNames;
    }
}());

dedupe 版本(去重版本)

dedupe 版本主要是通过对象(后者覆盖前者的性质)来实现的。

    // don't inherit from Object so we can skip hasOwnProperty check later
    // http://stackoverflow.com/questions/15518328/creating-js-object-with-object-createnull#answer-21079232
    function StorageObject() {}
    StorageObject.prototype = Object.create(null); // 创建空对象,Object.create(null) 方式创建的空对象不会继承原型链上的属性和方法

function _parseArray (resultSet, array) {
    var length = array.length;

    for (var i = 0; i < length; ++i) {
        _parse(resultSet, array[i]);
    }
}

...

function _parse (resultSet, arg) {
    if (!arg) return;
    var argType = typeof arg;

    // 'foo bar'
    if (argType === 'string') {
        _parseString(resultSet, arg); // 处理字符串类型参数

    // ['foo', 'bar', ...]
    } else if (Array.isArray(arg)) {
        _parseArray(resultSet, arg); // 处理数组类型参数

    // { 'foo': true, ... }
    } else if (argType === 'object') {
        _parseObject(resultSet, arg); // 处理对象类型参数

    // '130'
    } else if (argType === 'number') {
        _parseNumber(resultSet, arg); // 处理数字类型参数
    }
}           

// 主函数
function _classNames () {
    // don't leak arguments
    // https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments
    var len = arguments.length;
    var args = Array(len);
    for (var i = 0; i < len; i++) { // 将类数组 arguments 转换为数组
        args[i] = arguments[i];
    }

    var classSet = new StorageObject(); // 声明存放类名的对象
    _parseArray(classSet, args); // 开始解析

    var list = []; // 最终类名合集的数组

    for (var k in classSet) {
        if (classSet[k]) { // 将对象中 value 为 true 的对应 key 放入 list 中
            list.push(k)
        }
    }

    return list.join(' '); // 将 list 数组转换为字符串返回
}

return _classNames;

bind 版本

bind 版本是通过 bind() 指定 this 绑定的对象,传入 classnames 的参数将作为 key 到绑定的对象中进行匹配,若存在对应的 value,则将 value 加入到 classes 数组中;若不存在,则加入 key 本身。

与 index 版本的主要代码差别在:

if (argType === 'string' || argType === 'number') {
    classes.push(this && this[arg] || arg); // 首先加入 this 对象的 this[arg] 值,若不存在,则加入 arg
} else if (Array.isArray(arg)) {
    classes.push(classNames.apply(this, arg));
} else if (argType === 'object') {
    if (arg.toString === Object.prototype.toString) { // 等价于 Object.prototype.toString.call(arg) === '[object Object]'
        for (var key in arg) {
            if (hasOwn.call(arg, key) && arg[key]) {
                classes.push(this && this[key] || key);
            }
        }
    } else {
        classes.push(arg.toString());
    }
}

依赖库

  • mocha(功能测试框架)

    Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases. Hosted on GitHub.

  • benchmark(基准测试库)

    A benchmarking library that supports high-resolution timers & returns statistically significant results. 官网 通过 benchmark 库的测试,比较 index 版本和 dedupe 版本执行结果的快慢(即性能对比)。

总结

  • Object.create(null) 方式创建的空对象不会继承原型链上的属性和方法
  • 利用对象的键值对(后者覆盖前者特性)进行去重
  • 了解到 npm 包如何处理不同环境(CommonJS、AMD、浏览器)的导出
  • 了解了 mocha 功能测试框架、benchmark 基准测试库