手撕 classnames 源码

classnames (opens new window) npm 包

将不同数据类型,比如字符串、数字、object、array 转换成标准 CSS 的 class 字符串类型

可用于 react 或原生 js 中 class 拼接

当然也可用于 Vue

只是没必要,因为 Vue class 功能与之类似,而且相当强大

一共 3 个版本:

  • 默认版:使用 index.js,缺点不支持 css module、去重
  • dedupe version 去重版本:使用 dedupe.js 缺点比默认版本处理速度慢 5x
  • bind version css module 版本:使用 bind.js 在默认版本基础上可动态改变 this 指向,缺点不支持去重

源码讲解只涉及默认版,因为比较常用

# 结合 React 示例

注意 2 点:

  1. 0、false、undefined、null、'' falsely 值不会出现在结果中
  2. 默认版本,重复的值,不会自动去重,若去重可使用 dedupe 版本
import classNames from "classnames";
const Btn = () => {
  // 其中 classNames 函数中传入的参数,放入数组中 [...],也是可以的
  const cls = useMemo(
    () =>
      classNames(
        // class 字符串类型
        "btn",
        // class 数组类型
        ["btn-primary"],
        // class 对象类型
        {
          "text-warning": true,
        },
        "btn",
        // 以下 class 无效
        0,
        false,
        undefined,
        null,
        ""
      ),
    []
  );

  console.log(cls); // btn btn-primary text-warning btn -> 同名的 class 不去重

  return <button className={cls}>button</button>;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 源码初探

Github 源码 (opens new window)

所有的代码处于严格模式下,包裹在匿名自执行函数中

在应用中引入 classnames 模块时

模块会自执行,将模块以 cjs、amd、global 方式导出

比较有意思的是 {}.hasOwnProperty 引用一个判断对象属性归属的快捷方式

(function() {
  "use strict";

  var hasOwn = {}.hasOwnProperty;

  // ... 关键省略部分

  // CMD 规范实现,用于 node、webpack 等
  if (typeof module !== "undefined" && module.exports) {
    classNames.default = classNames;
    module.exports = classNames;
  } else if (
    typeof define === "function" &&
    typeof define.amd === "object" &&
    define.amd
  ) {
    // AMD 规范实现,用于 seajs
    define("classnames", [], function() {
      return classNames;
    });
  } else {
    // 挂载于 global,这里是 window 对象上,用于浏览器
    window.classNames = classNames;
  }
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 源码深入

遍历传入 classnames 函数的参数数组 arguments

处理不同数据类型的 class

将处理的结果收集到 classes 数组中

最后通过 join(' ') 方法将 classes 转换为标准的 class 并返回

function classNames() {
  var classes = [];
  for (var i = 0; i < arguments.length; i++) {
    var arg = arguments[i];
    // 若参数是 falsely 值,跳过此次循环,不收集
    if (!arg) continue;

    var argType = typeof arg;
    // 字符串或数字类型的 class 直接 push 到数组中
    if (argType === "string" || argType === "number") {
      classes.push(arg);
    } else if (Array.isArray(arg)) {
      if (arg.length) {
        // class 是数组且长度 > 0,递归调用,this 在这里没意义,指向 null
        var inner = classNames.apply(null, arg);
        if (inner) {
          classes.push(inner);
        }
      }
    } else if (argType === "object") {
      // class 对象重写的 toString 方法
      // 多出现于三方类库中
      // Issue: https://github.com/JedWatson/classnames/issues/278
      if (
        arg.toString !== Object.prototype.toString &&
        !arg.toString.toString().includes("[native code]")
      ) {
        classes.push(arg.toString());
        continue;
      }
      // class 是对象,且 key 是在 class 本身且值不为 falsely,push 到数组中
      for (var key in arg) {
        if (hasOwn.call(arg, key) && arg[key]) {
          classes.push(key);
        }
      }
    }
  }

  // 循环结束,将结果转换成标准 class 字符串返回
  return classes.join(" ");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 对比 Vue class 绑定

Vue class 绑定 (opens new window)

classnames 用法有诸多相似

Vue class 绑定

// 绑定字符串,如果确定 class,可以不使用 class 绑定语法的
<div :class="`active`"></div>

// 绑定对象, isActive 为真时,class 为 active
<div :class="{active: isActive}"></div>

// 绑定数组,结果同上
<div :class="[isActive ? 'active' : '']"></div>

// 数组中嵌套对象,结果同上
<div :class="[{active: isActive}]"></div>
1
2
3
4
5
6
7
8
9
10
11

classnames 支持字符串、对象、数组数据类型,然而 Vue 内置的 class 绑定中天然支持

所以为什么 Vue 中可以不用 classnames

# 小结

结合 react 说明了 classnames 库的使用方式

通过源码阐明了实现原理

简单地说:将字符串、数字、对象、数组方式的数据结构转换为 class 支持字符串拼接方式

最后对比了 Vue 中 class 绑定,表明 Vue 中天然对 class 支持较为强大

所以不建议使用 classnames

扫一扫,微信中打开

微信二维码