【若川视野x源码共读】classnames

120 阅读2分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

基本信息

目标

  • 学会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