本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
基本信息
- 简介:A simple JavaScript utility for conditionally joining classNames together.
- github:github.com/JedWatson/c…
- npm:www.npmjs.com/package/cla…
目标
- 学会classnames的用法
- 学会classnames的原理
- 关注测试用例
用法
下面简单说下用法:
安装
#via npm
npm install classnames
#via yarn
yarn add classnames
引入
// esm 下
import classNames from 'classnames';
// Node.js, Browserify
const classNames = require('classnames');
使用
const classNames = require('classnames');
// 常规用法
classNames('foo', 'bar'); // => 'foo bar'
classNames(['foo', 'bar']); // => 'foo bar'
classNames({'foo': true, 'bar': false}); // => 'foo' ,有条件判断输出类名
classNames(['foo', ['bar', ['cdd']]]); // => 'foo bar cdd',数组扁平化处理
// falsy 值不会输出
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'
// 后者覆盖前者
classNames('foo', { foo: false, bar: true }); // => 'bar'
// 不重复
classNames('foo', 'foo', 'bar'); // => 'foo bar'
原理
这里我们看下classNames里面做了什么,主要有以下几点:
参数
classNames没设置参数(无参),主要通过遍历arguments来获取
function classNames() {
//...
for(var i = 0; i < arguments.length; i++) {
//...
}
//...
}
处理过程
处理过程主要针对数字、字符串、数组、对象进行处理。有以下几个版本:
index.js版本
这个版本主要通过数组来暂存参数值
字符串和数字
// index.js
// 直接将该值push到数组中
if (argType === 'string' || argType === 'number') {
classes.push(arg);
}
数组
主要通过apply方法处理数组元素
// index.js
if (Array.isArray(arg)) {
if (arg.length) {
// 借用apply函数实现数组的扁平化,再次执行classNames函数,对数组元素处理。以此递归
var inner = classNames.apply(null, arg);
if (inner) {
classes.push(inner);
}
}
}
对象
这里对对象处理主要有以下几个方面
- 对象原型是否为Object.prototype
- 通过for in遍历对象属性,实现对属性的处理
- 属性是否为自身值:通过 hasOwnProperty 来判断
var hasOwn = {}.hasOwnProperty; //
if (argType === 'object') {
if (arg.toString === Object.prototype.toString) {
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
} else {
classes.push(arg.toString());
}
}
dedupe版本
这个版本主要通过StorageObject对象(空对象)来存储,通过对象属性实现去重
storageObject
function StorageObject() {}
StorageObject.prototype = Object.create(null);
_parse
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);
}
}
字符串
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; // 由于是对象存储,故要置为true
}
}
数字
function _parseNumber (resultSet, num) {
resultSet[num] = true; // 对象属性置为true
}
数组
function _parseArray (resultSet, array) {
var length = array.length;
for (var i = 0; i < length; ++i) {
_parse(resultSet, array[i]);
}
}
对象
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 {
resultSet[object.toString()] = true; // 对象属性置为true
}
}
返回结果
两个版本结果均为返回字符串
- index.js版本:通过Array.prototype.join拼接字符串返回
- dedupe版本:通过对象 -> 数组 -> Array.prototype.join拼接字符串返回
导出
通过对全局环境判断,进行不同的挂载
if (typeof module !== 'undefined' && module.exports) {
// node环境
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;
}
测试用例
classNames包里的测试用例包括以下方面:
功能测试
通过Mocha库,测试classNames包中的功能能够正常运行
mocha
安装
npm install --global mocha
使用
这里借用部分实例展示如何使用:
var assert = require('assert');
var classNames = require('../');
// describe:测试套件,一组相关测试,里面包括一些测试用例。参数1为名称,参数2为实际执行函数
// it:测试用例,参数一为名称,参数2为实际执行函数
describe('classNames', function () {
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');
});
});
参考
基准测试
通过benchmark库测试,比较本地、npm包下普通版本、dedupe版本执行结果快慢(性能对比)
benchmark
安装
npm install benchmark
使用
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.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.run();
}
参考
总结
通过对classNames包的分析,可以从中学到一些知识:
- 参数值处理
- 数组扁平化
- 去重
- npm包如何处理各个环境下的导出
- 提高自身的测试知识,了解到功能测试框架Mocha以及基准测试框架benchmark