classnames源码解读(大部分)

80 阅读8分钟

零、目录结构

目录/文件名作用
benchmarksbenchmark测试用例及测试脚本文件
testsmocha测试脚本文件
index.js主入口
dedupe.jsdedupe版本入口
bind.jsbind版本入口

一、benchmarks目录

benchmarks/fixture.js

var fixtures = [
	{
		description: 'strings',
		args: ['one', 'two', 'three'],// 参数格式为数组传入的多个字符串
		expected: 'one two three'// 期望输出 空格拼接的所有字符串
	},
	{
		description: 'object',
		args: [{one: true, two: true, three: false}],// 参数格式为对象数组,属性值为布尔类型
		expected: 'one two'// 期望输出 空格拼接的属性值为true的属性字符串
	},
	{
		description: 'strings, object',
		args: ['one', 'two', {four: true, three: false}],// 参数格式为包含对象和字符串的数组
		expected: 'one two four'// 期望输出 所有字符串及对象中属性值为true的属性,空格分隔
	},
	{
		description: 'mix',
		args: ['one', {two: true, three: false}, {four: 'four', five: true}, 6, {}],// 参数格式为包含对象和字符串、数值的数组
		expected: 'one two four five 6'// 期望输出 所有字符串及对象中属性值为true的属性、数值,空格分隔
	},
	{
		description: 'arrays',
		args: [['one', 'two'], ['three'], ['four', ['five']], [{six: true}, {seven: false}]],// 参数格式为数组包含对象和字符串、数值;数组可多层嵌套
		expected: 'one two three four five six'// 期望输出 所有字符串及对象中属性值为true的属性,空格分隔
	}
];

module.exports = fixtures;

测试用例的输入输出值

benchmarks/package.json

{
  "name": "classnames-benchmarks",
  "version": "1.0.0",
  "private": true,
  "description": "",
  "main": "run.js",
  "scripts": {
    "test": "echo \"Tests should be run in the main classnames package.\" && exit 1"
  },
  "author": "Jed Watson",
  "license": "MIT",
  "devDependencies": {
    "benchmark": "2.1.4",
    "classnames": "*"
  }
}

Q:依赖了benchmark,不知道是啥查一下?

A:与单元测试类似,用于对代码片段进行基准测试的库。

benchmarks/run.js

var fixtures = require('./fixtures');// 测试用例
var local = require('../');// 根目录中的index.js中的classNames函数
var dedupe = require('../dedupe');// 根目录中的dedupe.js中的_classNames函数
var localPackage = require('../package.json');

function log (message) {
	console.log(message);// 打印日志
}

try {
	var npm = require('classnames');// benchmarks目录下的依赖classnames下index.js中classNames函数
	var npmDedupe = require('classnames/dedupe');// benchmarks目录下的依赖classnames下dedupe.js中_classNames函数
	var npmPackage = require('./node_modules/classnames/package.json');
} catch (e) {
	log('There was an error loading the benchmark classnames package.\n' +
		'Please make sure you have run `npm install` in ./benchmarks\n');
	process.exit(0);
}

if (localPackage.version !== npmPackage.version) {
	log('Your local version (' + localPackage.version + ') does not match the installed version (' + npmPackage.version + ')\n\n' +
		'Please run `npm update` in ./benchmarks to ensure you are benchmarking\n' +
		'the latest version of this package.\n');
	process.exit(0);
}

var runChecks = require('./runChecks');
var runSuite = require('./runSuite');

fixtures.forEach(function (f) {// f为当前遍历到的对象?
	runChecks(local, npm, dedupe, npmDedupe, f);// 执行测试方法
	runSuite(local, npm, dedupe, npmDedupe, f, log);
});

benchmarks/runChecks.js

var assert = require('assert');// node提供的断言测试函数

function sortClasses (str) {
	return str.split(' ').sort().join(' ');// 将字符串根据空格分割成数组后排序,然后再用空格间隔拼成字符串
}

function runChecks (local, npm, dedupe, npmDedupe, fixture) {
	// sort assertions because dedupe returns results in a different order
	// assert.equal第一个入参为执行函数 第二个入参为期待结果
  // 不满足期望则抛出AssertionError异常,整个程序将会停止执行
	assert.equal(sortClasses(local.apply(null, fixture.args)), sortClasses(fixture.expected));
	assert.equal(sortClasses(dedupe.apply(null, fixture.args)), sortClasses(fixture.expected));
	assert.equal(sortClasses(npm.apply(null, fixture.args)), sortClasses(fixture.expected));
	assert.equal(sortClasses(npmDedupe.apply(null, fixture.args)), sortClasses(fixture.expected));
}

module.exports = runChecks;

深入浅出Node.js

在断言规范中,定义了以下几种检测方法:

  • ok():判断结果是否为真
  • equal():判断实际值与期望值是否相等
  • notEqual():判断实际值与期望值是否不相等
  • deepEqual():判断实际值与期望值是否深度相等(对象或数组的元素时是否相等)
  • notDeepEqual():判断实际值与期望值是否不深度相等
  • strictEqual():判断实际值与期望值是否严格相等(相当于===)
  • notStrictEqual():判断实际值与期望值是否不严格相等(相当于!==)
  • throws():判断代码块是否抛出异常

除此之外,Node的assert模块还扩充了如下两个断言方法

  • doesNotThrow():判断代码块是否没有抛出异常
  • ifError():判断实际值是否为一个假值(null、undefined、o、’ ’、false),如果实际值为真值,将会抛出异常

benchmarks/runSuite.js

var benchmark = require('benchmark');

function runSuite (local, npm, dedupe, npmDedupe, fixture, log) {
	var suite = new benchmark.Suite();// 基准测试套件

	// 为基准测试套件添加测试用例
	suite.add('local#' + fixture.description, function () {
		local.apply(null, fixture.args);
	});

	suite.add('  npm#' + fixture.description, function () {
		npm.apply(null, fixture.args);
	});

	suite.add('local/dedupe#' + fixture.description, function () {
		dedupe.apply(null, fixture.args);
	});

	suite.add('  npm/dedupe#' + fixture.description, function () {
		npmDedupe.apply(null, fixture.args);
	});

	// after each cycle
	suite.on('cycle', function (event) {
		log('* ' + String(event.target));
	});

	// other handling
  // 在指定事件上注册监听器,并返回Suite实例
	suite.on('complete', function () {
		log('\n> Fastest is' + (' ' + this.filter('fastest').map(result => result.name).join(' | ')).replace(/\s+/, ' ') + '\n');
	});

	suite.on('error', function (event) {
		log(event.target.error.message);
		throw event.target.error;
	});

	// 运行基准测试,并返回Suite实例
	suite.run();
}

module.exports = runSuite;

深入浅出Node.js

在断言规范中,定义了以下几种检测方法:

  • ok():判断结果是否为真
  • equal():判断实际值与期望值是否相等
  • notEqual():判断实际值与期望值是否不相等
  • deepEqual():判断实际值与期望值是否深度相等(对象或数组的元素时是否相等)
  • notDeepEqual():判断实际值与期望值是否不深度相等
  • strictEqual():判断实际值与期望值是否严格相等(相当于===)
  • notStrictEqual():判断实际值与期望值是否不严格相等(相当于!==)
  • throws():判断代码块是否抛出异常

除此之外,Node的assert模块还扩充了如下两个断言方法

  • doesNotThrow():判断代码块是否没有抛出异常
  • ifError():判断实际值是否为一个假值(null、undefined、o、’ ’、false),如果实际值为真值,将会抛出异常

注意:

  1. pnpm run benchmarks时会报错(需要到./benchmarks目录下安装依赖)

    There was an error loading the benchmark classnames package. Please make sure you have run npm install in ./benchmarks

二、根目录

index.js(直接利用数组存储有效属性)

/*!
  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;

	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) {// 非空数组
					var inner = classNames.apply(null, arg);// 递归调用
					if (inner) {// 有符合规则的值返回(空格分隔的字符串),追加到结果数组中
						classes.push(inner);
					}
				}
			} else if (argType === 'object') {// 参数为对象
				if (arg.toString === Object.prototype.toString) {// 判断非构造函数创造的数值、布尔、字符串
					for (var key in arg) {// 遍历对象属性
						if (hasOwn.call(arg, key) && arg[key]) {// 检查对象的自有属性(不含属性下的属性),并判断属性值是否为true
							classes.push(key);// 追加属性
						}
					}
				} else {
					classes.push(arg.toString());// 直接调用toString转字符串
				}
			}
		}

		return classes.join(' ');// 数组用空格拼接为字符串
	}

	if (typeof module !== 'undefined' && module.exports) {// 判断存在module.exports方法 CommonJS规范
		classNames.default = classNames;// 没懂这个.default是为啥
		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上,可全局调用
		window.classNames = classNames;
	}
}());

dedupe.js(利用对象存储所有属性,过滤有效属性再转为数组)

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

(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;
			// 遍历数组并透传set和数组的每一个元素给_parse方法(内部根据每个元素的类型执行对应的操作)
			for (var i = 0; i < length; ++i) {
				_parse(resultSet, array[i]);
			}
		}

		var hasOwn = {}.hasOwnProperty;// 判断属性是否为自身属性的方法

		// 直接将resultSet中属性为数值的属性值设为true
		function _parseNumber (resultSet, num) {
			resultSet[num] = true;
		}
	
		// 接受resultSet和对象
		function _parseObject (resultSet, object) {
			if (object.toString === Object.prototype.toString) {// 对象
				for (var k in object) {
					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>
						resultSet[k] = !!object[k];// 将对象的每个属性值写入
					}
				}
			} else {// 构造函数生成的数字、字符串、boolean
				resultSet[object.toString()] = true;
			}
		}

		var SPACE = /\s+/;
		function _parseString (resultSet, str) {// 解析字符串
			var array = str.split(SPACE);
			var length = array.length;

			for (var i = 0; i < length; ++i) {// 遍历字符串根据空格分割后生成的数组
				resultSet[array[i]] = true;// 将数组元素放入resultSet中作为属性,值为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);
			}
		}

		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();// 获得一个prototype为Object的{},很干净仅会存在参数生成的属性
			_parseArray(classSet, args);// 解析数组,将各参数作为属性写入classSet

			var list = [];

			for (var k in classSet) {// 遍历classSet的属性
				if (classSet[k]) {// 过滤属性值为true的属性写入数组
					list.push(k)
				}
			}

			return list.join(' ');// 返回过滤后数组用空格拼接的字符串
		}

		return _classNames;
	})();

	if (typeof module !== 'undefined' && module.exports) {
		classNames.default = classNames;
		module.exports = classNames;
	} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
		// register as 'classnames', consistent with npm package name
		define('classnames', [], function () {
			return classNames;
		});
	} else {
		window.classNames = classNames;
	}
}());

注意:**dedupe——**有一个替代版本的' classNames '可用,重复属性后面的属性值会覆盖前面的。

这个版本比较慢(大约5倍),所以它是作为一个选项提供的。

bind.js(直接利用数组存储有效属性,但优先读取调用者的指定属性

/*!
  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;

	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(this && this[arg] || arg);// 优先使用函数调用者绑定的this中的指定属性值,不存在再使用遍历到的属性值
			} else if (Array.isArray(arg)) {
				classes.push(classNames.apply(this, arg));// 数组会递归调佣线下传递this指向
			} else if (argType === 'object') {
				if (arg.toString === Object.prototype.toString) {
					for (var key in arg) {
						if (hasOwn.call(arg, key) && arg[key]) {
							classes.push(this && this[key] || key);
						}
					}
				} else {
					classes.push(arg.toString());
				}
			}
		}

		return classes.join(' ');
	}

	if (typeof module !== 'undefined' && module.exports) {
		classNames.default = classNames;
		module.exports = classNames;
	} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
		// register as 'classnames', consistent with npm package name
		define('classnames', [], function () {
			return classNames;
		});
	} else {
		window.classNames = classNames;
	}
}());

注意:可选的bind版本,如果你正在使用css-modules,或者类似的方法来动态生成类“名称”和实际输出到DOM的“className”值,你可能想要使用“bind”变量。

注意,在ES2015环境中,使用“动态类名”方法可能会更好

三、test目录

index.js

/* global describe, it */

var assert = require('assert');
var classNames = require('../');

// mocha测试框架
// 关键字describe:规定一组测试,可以嵌套
describe('classNames', function () {
	// 关键字it:规定单个测试
	it('keeps object keys with truthy values', function () {
		assert.equal(classNames({
			a: true,
			b: false,
			c: 0,
			d: null,
			e: undefined,
			f: 1
		}), 'a f');
	});

	it('joins arrays of class names and ignore falsy values', function () {
		assert.equal(classNames('a', 0, null, undefined, true, 1, 'b'), 'a 1 b');
	});

	it('supports heterogenous arguments', function () {
		assert.equal(classNames({a: true}, 'b', 0), 'a b');
	});

	it('should be trimmed', function () {
		assert.equal(classNames('', 'b', {}, ''), 'b');
	});

	it('returns an empty string for an empty configuration', function () {
		assert.equal(classNames({}), '');
	});

	it('supports an array of class names', function () {
		assert.equal(classNames(['a', 'b']), 'a b');
	});

	it('joins array arguments with string arguments', function () {
		assert.equal(classNames(['a', 'b'], 'c'), 'a b c');
		assert.equal(classNames('c', ['a', 'b']), 'c a b');
	});

	it('handles multiple array arguments', function () {
		assert.equal(classNames(['a', 'b'], ['c', 'd']), 'a b c d');
	});

	it('handles arrays that include falsy and true values', function () {
		assert.equal(classNames(['a', 0, null, undefined, false, true, 'b']), 'a b');
	});

	it('handles arrays that include arrays', function () {
		assert.equal(classNames(['a', ['b', 'c']]), 'a b c');
	});

	it('handles arrays that include objects', function () {
		assert.equal(classNames(['a', {b: true, c: false}]), 'a b');
	});

	it('handles deep array recursion', function () {
		assert.equal(classNames(['a', ['b', ['c', {d: true}]]]), 'a b c d');
	});

	it('handles arrays that are empty', function () {
		assert.equal(classNames('a', []), 'a');
	});

	it('handles nested arrays that have empty nested arrays', function () {
		assert.equal(classNames('a', [[]]), 'a');
	});

	it('handles all types of truthy and falsy property values as expected', function () {
		assert.equal(classNames({
			// falsy:
			null: null,
			emptyString: "",
			noNumber: NaN,
			zero: 0,
			negativeZero: -0,
			false: false,
			undefined: undefined,

			// truthy (literally anything else):
			nonEmptyString: "foobar",
			whitespace: ' ',
			function: Object.prototype.toString,
			emptyObject: {},
			nonEmptyObject: {a: 1, b: 2},
			emptyList: [],
			nonEmptyList: [1, 2, 3],
			greaterZero: 1
		}), 'nonEmptyString whitespace function emptyObject nonEmptyObject emptyList nonEmptyList greaterZero');
	});

	it('handles toString() method defined on object', function () {// toString被重写,数组推入toString执行的返回值
		assert.equal(classNames({
			toString: function () { return 'classFromMethod'; }
		}), 'classFromMethod');
	});

	it('handles toString() method defined inherited in object', function () {
		var Class1 = function() {};
		var Class2 = function() {};
		Class1.prototype.toString = function() { return 'classFromMethod'; }
		Class2.prototype = Object.create(Class1.prototype);

		assert.equal(classNames(new Class2()), 'classFromMethod');
	});
});

dedupe.js、bind.js原理同上

四、使用方法

  1. 支持数组、字符串、数值、对象作为入参,支持一个或多个
  2. dedupe版本:会自动去重,且多个同属性类名以最后一个属性值真假来判定是否存在。速度慢一些
  3. bind版本:适合cssModules这种动态生成类名的场景(优先绑定的对象存在的属性值)

五、一些想法

  • 万事开头难,大概是难在觉得自己不行,觉得源码很高深。但代码都是人写的,尤其是我们会读的开源代码库一般都是已经经过使用的代码,肯定比较规范合理的,就算会遇到很多自己难以理解的也可以通过这个机会去查资料解决。总之,试试就试试的心态很重要,别怕。
  • 开阔眼界的新方法get,过程中会遇到很多不理解或没接触过的东西(视个人情况而定),但如果你去主动了解一些或者看看别人写的文章就会感觉好理解一些,顺便开拓知识面很nice。
  • 感谢若川吧,活动挺好会让人比起自己一个人做这件事更容易动起来。(类似你在健身房,周围人都在健身,你可能会比自己在家更容易控制)