Skip to content
On this page

classnames

classnames 用于有条件地将 classNames 连接在一起.

Usage

js
classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'

// 接收许多不同类型的参数
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'

// 其他 false 值被忽略
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'

// 数组递归展开
var arr = ['b', { c: true, d: false }];
classNames('a', arr); // => 'a b c'

// 动态类名
let buttonType = 'primary';
classNames({ [`btn-${buttonType}`]: true }); // => btn-primary

// 删除重复数据 dedupe
import classNames from 'classnames/dedupe';
classNames('foo', 'foo', 'bar'); // => 'foo bar'
classNames('foo', { foo: false, bar: true }); // => 'bar'

// css-modules bind
/* components/submit-button.js */
import { Component } from 'react';
import classNames from 'classnames/bind';
import styles from './submit-button.css';
let cx = classNames.bind(styles);
export default class SubmitButton extends Component {
  render () {
    let text = this.props.store.submissionInProgress ? 'Processing...' : 'Submit';
    let className = cx({
      base: true,
      inProgress: this.props.store.submissionInProgress,
      error: this.props.store.errorOccurred,
      disabled: this.props.form.valid,
    });
    return <button className={className}>{text}</button>;
  }
};

源码分析

classNames 函数

js
function classNames() {
  var classes = [];

  // 循环传入的所有参数
  for (var i = 0; i < arguments.length; i++) {
    // 获取参数
    var arg = arguments[i];
    // 假值直接跳过
    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) {
          // 将返回的结果推入 ['foo', 'bar zoo']
          classes.push(inner);
        }
      }
    } else if (argType === 'object') {
      // 对象类型
      if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {
        // 对象自身定义的 toString 方法,调用后推入结果数组
        classes.push(arg.toString());
        continue;
      }

      for (var key in arg) {
        if (hasOwn.call(arg, key) && arg[key]) {
          // 是对象自身属性并且值存在,推入结果数组
          classes.push(key);
        }
      }
    }
  }

  // 使用空格连接结果数组
  return classes.join(' ');
}

classNames 重复数据消除版本

js
// dedupe
var classNames = (function () {
  // 不继承自Object,可以跳过hasOwnProperty检查
  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) {
    resultSet[num] = true;
  }

  function _parseObject (resultSet, object) {
    if (object.toString !== Object.prototype.toString && !object.toString.toString().includes('[native code]')) {
      // 对象自身定义的 toString 方法
      resultSet[object.toString()] = true;
      return;
    }

    for (var k in object) {
      if (hasOwn.call(object, k)) {
        // 是对象自身属性,根据值设置真假值
        resultSet[k] = !!object[k];
      }
    }
  }

  var SPACE = /\s+/;
  function _parseString (resultSet, str) {
    // 以空格、换行、tab缩进等所有的空白,拆分参数
    var array = str.split(SPACE);
    var length = array.length;

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

  function _parse (resultSet, arg) {
    if (!arg) return;
    // 参数类型
    var argType = typeof arg;

    if (argType === 'string') {
      // 字符串类型
      // 'foo bar' => { foo: true, bar: true }
      _parseString(resultSet, arg);

    } else if (Array.isArray(arg)) {
      // 数组类型
      // ['foo', 'bar'] => { foo: true, bar: true }
      _parseArray(resultSet, arg);

    } else if (argType === 'object') {
      // 对象类型
      // { 'foo': true, bar: '1' } => { foo: true, bar: true }
      _parseObject(resultSet, arg);

    } else if (argType === 'number') {
      // 数字类型
      // '130' => { '130': true }
      _parseNumber(resultSet, arg);
    }
  }

  function _classNames () {
    // 参数长度
    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) {
      if (classSet[k]) {
        // 将真值,放入结果数组
        list.push(k)
      }
    }

    // 使用空格连接数组结果 ['foo', 'bar', '123'] => 'foo bar 123'
    return list.join(' ');
  }

  return _classNames;
})();

classNames css-modules 版本

js
// bind
var hasOwn = {}.hasOwnProperty;

function classNames () {
  var classes = [];

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

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

    if (argType === 'string' || argType === 'number') {
      // 字符串类型与数字类型
      // 从当前 this 上获取 key 对应的映射值或者默认值
      classes.push(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 && !arg.toString.toString().includes('[native code]')) {
        // 对象自身存在自定义 toString
        classes.push(arg.toString());
        continue;
      }

      for (var key in arg) {
        if (hasOwn.call(arg, key) && arg[key]) {
          // 是对象自身的属性并且存在
          // 从当前 this 上获取 key 对应的映射值或者默认值
          classes.push(this && this[key] || key);
        }
      }
    }
  }

  // 使用空格连接
  return classes.join(' ');
}

var styles = {
  foo: 'abc',
  bar: 'def',
  baz: 'xyz'
};

var cx = classNames.bind(styles);

var className = cx('foo', ['bar'], { baz: true }); // => "abc def xyz"