《classnames源码》阅读笔记


theme: arknights

《classnames源码》阅读笔记

GitHub地址

源码目录总览

源码目录总览

参考官方文档中的内容,我们可以知道classnames 有一个主要版本(index)和两个替代版本
(分别是dedupebind)。在看目录的时候也可以发现 classnames 具有多个对外暴露的入口。

  • index.jsclassnames的主要使用的版本
  • dedupe.jsclassnames中一个可选的,用于删除重复数据的版本
  • bind.jsclassnames中另一个可选的,用于css modules形式的版本
  • *.d.ts是定义类型的文件
  • bower.json是包管理工具bower的配置文件(后面会提到bowernpm的区别)
  • tests/目录下是classnames三个版本的测试文件以及一个适用于测试代码的类型文件
  • benchmarks/目录用于存放benchmarks相关的文件(benchmarks是一个检测代码性能的工具)

用法概述

安装
    # via npm
    npm install classnames
    
    # via yarn 
    yarn add classnames
一般用法
    // 引入包
    import classnames from 'classnames';
    
    // 接收多个字符串作为参数
    classnames('classA', 'classB') // => classA, classB
    
    // 接收字符串 和 对象作为参数(对象的值可以为 true || false)
    classnames('classA', { classB: true }) // => classA, classB
    
    // 接收字符串 和 对象作为参数(对象的值也可以是一个返回 true || false 的表达式)
    classnames('classA', { classB: 1 < 2 }) // => classA
    
    // 接收一个对象作为参数,对象的键名是对应的类名,对象的值的规则与上面一致
    classnames({ classA: true, classB: false }) // => classA
    
    // 接收多个对象作为参数,对象的键与值的规则与上面一致
    classnames({ classA: true }, { classB: true }) // => classA, classB
    
    // 接收一个数组作为参数
    classnames(['classA', { classB: false }, { classC: 1 }]) // => classA, classC
    
    // 接收数组和其它类型参数的组合
    classnames(['classA', { classB: false }], 'classC') // => classA, classC
    
    // 需要注意的特殊例子!
    // 当存在针对同一个类的不同值时,只要其中一个值是真值,就会应用这个类(与这个值的计算顺序无关)
    classnames('classA', true ? 'classB' : '', { classB: false }) => classA, classB
    
    classnames('classA', { classB: false }, true ? 'classB' : '') => classA, classB
    // 动态计算类名
    
    type UserType = "guest" | "host";
    
    const user: UserType = 'guest';
    
    classnames(`${user}_type_color`); // => guest_type_color
dedupe用法

dedupe主要用于解决上面提到的特殊例子中存在的问题

有时候我们会在代码中应用多个样式,同时我们希望是否应用某个样式取决于这个样式最终的计算结果。还是用上面提到的特殊例子:

  • 不使用dedupe

    classnames('classA', true ? 'classB' : '', { classB: false }) => classA, classB
    
    classnames('classA', { classB: false }, true ? 'classB' : '') => classA, classB
    
    classnames('classA', { classB: true }, false ? 'classB' : '') => classA, classB
    
  • 使用dedupe

    classnames('classA', true ? 'classB' : '', { classB: false }) => classA
    
    classnames('classA', { classB: false }, true ? 'classB' : '') => classA, classB
    
    classnames('classA', { classB: true }, false ? 'classB' : '') => classA, classB
    
bind用法

bind适用于通过css modules引入样式并需要动态计算的场景。文档中建议在支持ES6的情况下使用模板字符串的形式来替代bind方法。

直接来看例子

    .foo {
        color: red;
    }
    
    .bar {
        font-size: 30px;
    }
    
    .zoo {
        border: 1px solid red;
    }
    import styles from './index.css';
    
    const cs = classnames.bind(styles)
    
    // foo, zoo
    <div className={cs('foo', { bar: false }, [{ zoo: true }])}></div>

看看源码

通用代码

index.js,deeupe.jsbind.js 中都使用到了这个通用代码,用于针对不同的环境去导出classNames方法。

        // 适用于 nodejs 的环境,使用 commonjs 规范引入模块
      if (typeof module !== 'undefined' && module.exports) {
        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 {
        // 以上情况均不适用,就把 classNames 函数挂载到 window 上
        window.classNames = classNames;
      }
index.js
(function () {
      'use strict';
        
      // 将空对象的 hasOwnProerty 保存到hasOwn变量
      // 防止入参对象修改了同名方法导致判断结果错误
      var hasOwn = {}.hasOwnProperty;

      function classNames() {
        // 定义类名数组
        var classes = [];

        // 遍历入参
        for (var i = 0; i < arguments.length; i++) {
          // 拿到当前第 i 个入参
          var arg = arguments[i];

          // 入参的值类型为 falsy 则跳过这个值
          if (!arg) continue;

          // 判断当前入参的类型
          var argType = typeof arg;

          // 如果当前入参类型为 字符串 || 数字, 就直接推入类名数组
          if (argType === 'string' || argType === 'number') {
            classes.push(arg);

            // 如果当前入参类型为 数组
          } else if (Array.isArray(arg)) {

            // 首先判断数组是否为空

            // 不为空
            if (arg.length) {
              // 将入参的数组作为新的参数 递归调用 classNames 函数
              var inner = classNames.apply(null, arg);

              // 如果有返回值, 就将本次递归调用的结果推到 类名数组中
              if (inner) {
                classes.push(inner);
              }

              // 没有结果则忽略

              // 如果当前入参的类型是 object
            }
          } else if (argType === 'object') {
            // 如果当前入参对象的 toString 不等于 对象原型链上的 toString  && 入参对象的 toString 方法中不包含 "[native code]" 字符
            if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {
              // 调用入参对象自定义的 toString 方法后,再将得到的字符串推入 类名数组

              // TODO: 感觉这里需要额外增加一个判断,
              // 对入参的 toString 方法的返回值做判断
              // 如果返回值是一个 (字符串 || 数字) && 当前入参调用 toString 之后的值是真值,就直接推入 类名数组
              // 否则应当递归调用 classNames 函数(与数组的处理方法一致)
              if(typeof arg.toString() === 'string' || typeof arg.toString() === 'number' && arg.toString() ) {
                // && arg.toString()
                classes.push(arg.toString());
                continue;
              } else {
                classNames.apply(null, arg)
              }
            }

            // 没有修改过入参对象的 toString 方法

            // 遍历入参对象的 key
            for (var key in arg) {
              // 如果当前的key 是入参对象本身的属性 并且这个属性的值是真值
              if (hasOwn.call(arg, key) && arg[key]) {
                // 将这个属性名推入 类名数组
                classes.push(key);
              }
            }
          }
        }

        // 将 当前类名数组 用空格拼接并返回
        return classes.join(' ');
      }

      // 通用代码...
    }());

dedupe.js

简单来说,dedupe版本为了解决类名重复的问题,构造了一个对象来保存所有的类名;由于对象的key无法重复,所以对象中后定义的类名的值会覆盖之前定义的相同类名的值。在对象构造完成后,再去遍历取得对象中所有值为truekey,并将这些key返回,最终添加到DOM上。

 (function () {
        'use strict';

        var classNames = (function () {
          // 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);

          // 用于解析数组入参的方法
          function _parseArray(resultSet, array) {
            var length = array.length;

            for (var i = 0; i < length; ++i) {
              // 对数组中的每一个参数都进行解析
              _parse(resultSet, array[i]);
            }
          }

          var hasOwn = {}.hasOwnProperty;

          // 用于解析数字的方法
          function _parseNumber(resultSet, num) {
            // 这里没有对数字的合法性做校验(0, -0, Infinity)
            // 但是没有关系,在最后一步遍历 list 数组的时候只取真值
            resultSet[num] = true;
          }

          // 用于解析对象的方法
          function _parseObject(resultSet, object) {
            // 与 index.js 中类似的判断,检查入参对象是否具有自定义的 toString 方法
            // 有的话就调用 其自定义的 toString 方法,并将结果添加到 存储对象 中
            if (object.toString !== Object.prototype.toString && !object.toString.toString().includes('[native code]')) {
              resultSet[object.toString()] = true;
              return;
            }

            // 没有自定义的 toString 方法

            // 遍历入参对象
            for (var k in object) {

              // 检查当前的 key 是否是入参对象自身的 key
              if (hasOwn.call(object, k)) {
                // set value to false instead of deleting it to avoid changing object structure
                // https://www.smashingmagazine.com/2012/11/writing-fast-memory-efficient-javascript/#de-referencing-misconceptions

                // 将 当前key 对应的值 转为布尔值之后 存储在 存储对象 中

                // 这里的逻辑与 index.js 不同,在 index.js 中,是直接将当前的 key 放到最终的 classes 类名数组中
                // 但是 在 dedupe.js 中,因为去重的需要,所以会先将 key 放在对象中,用于更新它的值
                resultSet[k] = !!object[k];
              }
            }
          }

          // 定义一个去重的正则
          var SPACE = /\s+/;

          // 用于解析字符串的方法
          function _parseString(resultSet, str) {
            // 使用 空格 将字符串分成数组
            var array = str.split(SPACE);
            var length = array.length;

            for (var i = 0; i < length; ++i) {
              // 遍历数组,将每个字符串都作为 存储对象 上的一个 key, 值默认为 true
              resultSet[array[i]] = true;
            }
          }

          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);
            }
          }

          // 最终返回的供外部调用的 classNames 函数
          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++) {
              args[i] = arguments[i];
            }

            // 创建一个 存储对象
            var classSet = new StorageObject();

            // 对入参做解析,并将解析后的值都放到新创建的 存储对象 上
            _parseArray(classSet, args);

            // 存储最终类名的数组
            var list = [];

            // 遍历 存储对象
            for (var k in classSet) {

              // 如果 存储对象 当前 key 的值是真值,就推到 类名数组中
              if (classSet[k]) {
                list.push(k)
              }
            }

            // 最后返回一个类名字符串
            return list.join(' ');
          }

          return _classNames;
        })();

        // 通用代码...
      }());
  

对比

关于类的处理包除了 classnames 以外,还有一个最近在项目里用到的是 clsx;在我看来,两者的差别就在于 clsx 直接省去了数组转字符串这个步骤,直接定义了一个字符串,然后把符合条件的(真值)的类名加到后面:

clsx处理代码

总结

这次去看classnames的源码,确实了解了它的工作原理,没有想象的这么复杂;针对通用代码,还去学习了一下从IIFECJS,再从AMDCMDES Module的模块化过程,下次再把这个笔记也输出一下!

冲!

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容