【若川视野 x 源码共读】经常用的 classNames,你知道是怎么实现的么

964 阅读4分钟

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

1. 前言

今天来学习以下 classnames 的源码,这个库应该 react 的开发者都有用过,这两者联系可谓是非常紧密,配合使用相当方便

1.1 你能学到

  1. classnames 的用法
  2. classnames 的原理
  3. classnames 中的测试

2. 看代码之前

先从 README 中了解该库的相关信息

2.1 了解该库的用途

A simple JavaScript utility for conditionally joining classNames together.

一个简单的 JavaScript 实用程序,用于有条件地将类名连接在一起。

2.2 使用方式

classNames 作为核心方法,可以接收任意数量的参数,并根据一些判断依据返回最终以空格分隔的类名串。
如果key 的值是 false 那么就不会加入到最后的字符串中

classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'

// lots of arguments of various types
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'

// other falsy values are just ignored
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'

数组传入就按扁平化后再根据判断处理:

var arr = ['b', { c: true, d: false }];
classNames('a', arr); // => 'a b c'

这样可以结合 ES6 中的模板字符串达到动态类型的效果

let buttonType = 'primary';
classNames({ [`btn-${buttonType}`]: true });

react + classNames

在 react 组件中,就可以将其设置为 state,通过绑定事件的回调函数对其进行更改,使其类名动态更改、样式动态更改的效果

class Button extends React.Component {
  // ...
  render () {
    var btnClass = 'btn';
    if (this.state.isPressed) btnClass += ' btn-pressed';
    else if (this.state.isHovered) btnClass += ' btn-over';
    return <button className={btnClass}>{this.props.label}</button>;
  }
}

对象形式

var classNames = require('classnames');
//又或者这样
class Button extends React.Component {
  // ...
  render () {
    var btnClass = classNames({
      btn: true,
      'btn-pressed': this.state.isPressed,
      'btn-over': !this.state.isPressed && this.state.isHovered
    });
    return <button className={btnClass}>{this.props.label}</button>;
  }
}

或者结合 props

var btnClass = classNames('btn', this.props.className, {
  'btn-pressed': this.state.isPressed,
  'btn-over': !this.state.isPressed && this.state.isHovered
});

3 看看源码

3.1 经典查看入口

packeage.json 中查看 main 字段就可以确认 index.js 是入口,该文件一共有 58 行。

3.2 直接开看

一开始先用了一个 自执行的函数 来包裹整个作用域 从而避免变量污染冲突,并且采用严格模式(好像几乎所有开源项目都是用严格模式的)

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

(function () {
	'use strict';

hasOwnProperty

hasOwnProperty这个方法用来判断对象的属性是否属于自己本身——而不是往原型链上面找到的

	var hasOwn = {}.hasOwnProperty;

随后就是主要方法 classNames 了(接下来为了方便我写也方便读者阅读,就全部都写到注释里面了)

	
	function classNames() {
		var classes = []; //一个专门存储最后类名合集的数组
  //传入参数不限制数量,自然是用到参数对象 arguments 这个东西
		for (var i = 0; i < arguments.length; i++) {
			var arg = arguments[i]; //遍历 arguments 拿到每一项
			if (!arg) continue; //如果该项的值为 undefined、null之类的就直接跳过

			var argType = typeof arg;//获取该项的类型

      //字符串或者数字之类的直接加入 classes 中就完事了
			if (argType === 'string' || argType === 'number') {
				classes.push(arg);
			} else if (Array.isArray(arg)) {
				if (arg.length) {
					var inner = classNames.apply(null, arg); //针对数组中的每一项都需要进行判断是否能够加入 classes 中,所以利用 递归+apply 达到数组扁平化的效果
					if (inner) { //递归调用返回的不是空字符串 '' 的话就加入 classes
						classes.push(inner);  //放入
					}
				}
			} else if (argType === 'object') {//对象的情况下
        //如果自带的 toString 方法 和 Object 的一样
				if (arg.toString === Object.prototype.toString) { //'[object object]'的情况
					for (var key in arg) {//用 for in 遍历对象中的可枚举属性
						if (hasOwn.call(arg, key) && arg[key]) {//如果该属性是自身的 && value 为 true(或者说 可以转变为 true)
							classes.push(key);//就将 key 放入 classes 中,注意是key
						}
					}
				} else {//否则就用自身自定义的 toString 方法
					classes.push(arg.toString());  
				}
			}
		}

		return classes.join(' ');//用 join 方法将数组变为字符串,用' '隔开
	}
//用于支持各种导出方式
	if (typeof module !== 'undefined' && module.exports) {// CommonJS
		classNames.default = classNames;
		module.exports = classNames;
	} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {// AMD, 通过判断是否又 define 方法以及 define.amd 是否为 object
		// register as 'classnames', consistent with npm package name
		define('classnames', [], function () {
			return classNames;
		});
	} else {
    //浏览器环境
		window.classNames = classNames;
	}
}());

3.3 除了 index 的其他版本

bind 版本

前面的 index 只是单纯的拼接,而 bind 版本 还可以通过 bind 指定读取属性的对象,传入 classNames 的参数先作为 key 到绑定的对象中寻找 value,如果有,就放value 进去,如果没有才放入 key
使用起来是这样的:

var classNames = require('classnames/bind');

var styles = {
  foo: 'abc',
  bar: 'def',
  baz: 'xyz'
};

var cx = classNames.bind(styles);

var className = cx('foo', ['bar'], { baz: true }); // => "abc def xyz"

主要代码差别在这里

classes.push(this && this[arg] || arg);//绑定了this,并且key(arg)对应的value有值=> this[arg] || 没有这个东西=>arg 本身

dedupe 版本

dedupe 版本不是单纯的拼接,而是有去重操作。而具体去重操作是通过对象Object 键值对来实现的,所以也就有后来的能覆盖前面属性

var classNames = require('classnames/dedupe');

classNames('foo', 'foo', 'bar'); // => 'foo bar'
classNames('foo', { foo: false, bar: true }); // => 'bar'

用 StorageObject 来存储进行去重

function StorageObject() {}
StorageObject.prototype = Object.create(null);

//使用时就是这样
var classSet = new StorageObject();

使用 create(null) 可以让后面的判断省去一个 hasOwnProperty
主要区别就是 _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);
  }
}

主函数 classNames 中 解析的入口就是对数组类型的操作—— arguments 就是一种类数组

var len = arguments.length;
var args = Array(len);
for (var i = 0; i < len; i++) {
  args[i] = arguments[i];
}

var classSet = new StorageObject();
_parseArray(classSet, args);

最后根据对象键值对的value 是否为 true,来决定是否放入list——即结果数组,再用 join 进行处理返回最终的字符串

for (var k in classSet) {
  if (classSet[k]) {
    list.push(k)
  }
}

return list.join(' ');

3.4 benchmark

基准测试

你可以会有这样的需求:想比较两个效果相同的方法谁的性能较优。但是JS代码在不同运行环境下运行的效率可能是不一样的。这就是为什么我们需要基准测试。


我们从依赖包中可以看见 benchmark 这个包,这是用于做性能基准测试的,这里是他的官方仓库
这里就是 classNames 中进行测试的地方,具体怎么写测试样例建议看下面学习资源中的官方文档。

4. 学习资源

5. 总结 & 收获

  • arguments ,在很多方法中都很有用到,很实用
  • apply 第二个参数接收数组形式来实现数组扁平化的效果
  • 利用对象键值对来去重
  • 各个环境的导出处理,这个我还是第一次了解
  • 总体来说,是没想到经常使用的一个库逻辑居然如此简单——读取参数内容,根据类型处理,放入数组最后再进行拼接
  • 当然,尽管自己看源代码都能够知道这行代码是做什么的,但是如果说要自己实现一个.. 😴