1. 源码简介

官方简介:A simple JavaScript utility for conditionally joining classNames together.
我理解为是一个动态将class name连接起来的库。(注意:它不是react官方的那个className属性)

该classNames函数接受任意数量的参数,可以是字符串或对象,基础用法如下:

  1. classNames('foo', 'bar'); // => 'foo bar'
  2. classNames('foo', { bar: true }); // => 'foo bar'
  3. classNames({ 'foo-bar': true }); // => 'foo-bar'
  4. classNames({ 'foo-bar': false }); // => ''
  5. classNames({ foo: true }, { bar: true }); // => 'foo bar'
  6. classNames({ foo: true, bar: true }); // => 'foo bar'

2. 学习目标

  • 学会 classnames 的用法
  • 学会 classnames 的原理
  • 关注测试用例

因为最近在学习写测试用例,所以主要关注这方面,所以测试用例的篇幅会长一点:https://github.com/JedWatson/classnames/tree/master/tests

3. 测试用例

测试用例主要有三个文件,index, dedupe, bind,下面这个篇幅可以跳过,就是把测试用例看一遍。看完了感受是,设计测试用例也不容易啊,用户行为防不胜防。也由此可见,写好一个库经常需要校验用户传参的类型。如果看文档比较迷惑,看源码比较困难,也可以选择简单看看测试用例,可以知道是如何使用的。

index.js

  • keeps object keys with truthy values:传入的是object,只返回”真值”的key

    1. assert.equal(classNames({
    2. a: true,
    3. b: false,
    4. c: 0,
    5. d: null,
    6. e: undefined,
    7. f: 1
    8. }), 'a f');
  • joins arrays of class names and ignore falsy values:传入一组数据,会忽略假值

    1. assert.equal(classNames('a', 0, null, undefined, true, 1, 'b'), 'a 1 b');
  • supports heterogenous arguments:支持不同类型的参数一起传参

    1. assert.equal(classNames({a: true}, 'b', 0), 'a b');
  • should be trimmed:去除空值

    1. assert.equal(classNames('', 'b', {}, ''), 'b');
  • returns an empty string for an empty configuration:如果是个空配置,返回空字符串

    1. assert.equal(classNames({}), '');
  • supports an array of class names:支持数组传参

    1. assert.equal(classNames(['a', 'b']), 'a b');
  • joins array arguments with string arguments:支持数组+字符串传参

    1. assert.equal(classNames(['a', 'b'], 'c'), 'a b c');
    2. assert.equal(classNames('c', ['a', 'b']), 'c a b');
  • handles multiple array arguments:支持多个数组传参

  • handles arrays that include falsy and true values:过滤数组内的真值和假值
  • handles arrays that include arrays:支持多维数组传参
  • handles arrays that include objects:支持数组对象传参
  • handles deep array recursion:支持多维数组+数组对象传参
  • handles arrays that are empty:过滤空数组
  • handles nested arrays that have empty nested arrays:处理嵌套空数组
  • handles all types of truthy and falsy property values as expected:按照预期处理所有类型的真值和假值
  • handles toString() method defined on object:处理定义在属性上的toString方法

    1. assert.equal(classNames({
    2. toString: function () { return 'classFromMethod'; }
    3. }), 'classFromMethod');
  • handles toString() method defined inherited in object:处理对象中继承的toString方法。 ```javascript var Class1 = function() {}; var Class2 = function() {}; Class1.prototype.toString = function() { return ‘classFromMethod’; } Class2.prototype = Object.create(Class1.prototype);

assert.equal(classNames(new Class2()), ‘classFromMethod’);

  1. <a name="XEoI5"></a>
  2. ### dedupe.js
  3. 这个脚本的测试用例是去重的,简单看一下
  4. - should dedupe dedupe:应该进行重复数据处理
  5. ```javascript
  6. assert.equal(dedupe('foo', 'bar', 'foo', 'bar', { foo: true }), 'foo bar');
  • should make sure subsequent objects can remove/add classes:同一个类名,可以增加/删除
    1. assert.equal(dedupe('foo', { foo: false }, { foo: true, bar: true }), 'foo bar');
    下面的就不展开了,和index.js套路差不多,下一个bind.js。

    bind.js

    这个脚本的测试用例是给className绑定的值做映射的,还挺有意思,可惜我没有用过这个库,不知道classNameBound的适用场景在哪。
    这是前提,意思就是classNames.bind(cssModulesMock),绑定了cssModulesMock ```javascript var cssModulesMock = { a: “#a”, b: “#b”, c: “#c”, d: “#d”, e: “#e”, f: “#f” };

var classNamesBound = classNames.bind(cssModulesMock);

  1. - keeps object keys with truthy values,也是返回真值,和className不同的是,它返回的不是a f,而是#a #f,这可能是因为上面使用了bind,做了一层映射,一会可以看看源码怎么弄的。
  2. ```javascript
  3. assert.equal(classNamesBound({
  4. a: true,
  5. b: false,
  6. c: 0,
  7. d: null,
  8. e: undefined,
  9. f: 1
  10. }), '#a #f');
  • keeps class names undefined in bound hash:有映射的映射,没有的就保留它的类名。

    1. assert.equal(classNamesBound({
    2. a: true,
    3. b: false,
    4. c: 0,
    5. d: null,
    6. e: undefined,
    7. f: 1,
    8. x: true,
    9. y: null,
    10. z: 1
    11. }), '#a #f x z');

    其他的就略过了,也是和index.js大同小异,就是多加了一层映射。

    4. 源码

    很短,50行代码。在看源码之前有一个大致流程,获取参数,对参数的各种类型解析,输出结果。
    看了源码之后,脑子:我会了,手:你不会。
    首先一个for循环遍历入参,如果 !arg 当前项为 false 假值,直接跳出当前循环,进入下一个循环;
    如果arg是string | number,直接push;
    如果arg是array,这里用到了递归 classNames.apply(null, arg),如果递归回来的结果不是空的,就push;达到一个拍平的效果。
    如果arg是object,先判断一下object上的toString有没有被修改,如果没有,迭代遍历object,判断只有当前属性是自身上的+对应的值的结果为真,就push(key);如果被修改了,直接push这个属性的toString()返回的结果。
    最后把结果数组classes转换成字符串 classes.join(‘ ‘)。

    index.js

    1. (function () {
    2. 'use strict';
    3. var hasOwn = {}.hasOwnProperty;
    4. function classNames() {
    5. var classes = [];
    6. for (var i = 0; i < arguments.length; i++) {
    7. var arg = arguments[i];
    8. if (!arg) continue;
    9. var argType = typeof arg;
    10. if (argType === 'string' || argType === 'number') {
    11. classes.push(arg);
    12. } else if (Array.isArray(arg)) {
    13. if (arg.length) {
    14. var inner = classNames.apply(null, arg);
    15. if (inner) {
    16. classes.push(inner);
    17. }
    18. }
    19. } else if (argType === 'object') {
    20. if (arg.toString === Object.prototype.toString) {
    21. for (var key in arg) {
    22. if (hasOwn.call(arg, key) && arg[key]) {
    23. classes.push(key);
    24. }
    25. }
    26. } else {
    27. classes.push(arg.toString());
    28. }
    29. }
    30. }
    31. return classes.join(' ');
    32. }
    33. // COMMON JS
    34. if (typeof module !== 'undefined' && module.exports) {
    35. classNames.default = classNames;
    36. module.exports = classNames;
    37. } else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
    38. // register as 'classnames', consistent with npm package name
    39. // AMD
    40. define('classnames', [], function () {
    41. return classNames;
    42. });
    43. } else {
    44. // 浏览器
    45. window.classNames = classNames;
    46. }
    47. }());

    dedupe.js

    看了一下大致是利用object key唯一性去做去重的。但源码内部实现,变量指向写得有点乱,就没细看了。

    bind.js

    逻辑和上面的基本一直,只是push的时候是:classes.push(this && this[key] || key); 结合上面的测试用例一起看,var classNamesBound = classNames.bind(cssModulesMock);
    意思就是,绑定this,且this[key]存在,则取this[key],否则取key。可以结合【keeps class names undefined in bound hash】这个用例一起理解。

    5. 感受

    写好一个库真难。