【若川视野 x 源码共读】第26期 | classnames

171 阅读2分钟

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

classNames, 非常常用,在react中想要动态添加className时,就会使用classNames这个库。如果业务中需要动态添加类名,你或许想到使用三元表达式等方式,但是这样不够直观。如果三元表达式的一部分还是三元表达式,这样就很容易出错。比如下面的例子:

state = {
    class1:true,
    class2:true
};
// 不使用classNames
render() {
    const {class1,class2} = this.state;
    let divClass='class-test';
    divClass = class1 ? 'class-test-class1' : ' ';
    divClass = divClass + class2 ? 'class-test-class2' : ' ';
    return (
        <div className={divClass}>test</div>
    );
}
// 使用classNames
render() {
  const {class1,class2} = this.state;
  let divClass=classNames({
        class-test-class1:class1,
        class-test-class2:class2
  })
  return (
      <div className={divClass}>test</div>
  );
}

1.学习内容

classNames仓库地址:

https://github.com/JedWatson/classnames.git

2.用例

根据用例了解用法:

var classNames = require('classnames');
classNames('foo', 'bar'); // => 'foo bar'

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'

3.猜测源码实现思路

根据用例猜测源码实现思路:

最终给dom设置的class样式都是如下形式的:

<div class="class1 class2 class3"></div>

所以classNames的最终返回结果就是以空格分隔的字符串。classNames是一个函数,接受逗号分隔的参数,而参数可能是字符串,数字,对象,数组;针对对象要筛选出那些值为真值的键,针对数组的每一个元素都要进行检查。所以classNames是一个递归的函数。

4.源码

/*!
  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)) {
        // 如果是数组则针对数组中的每一个元素应用classNames函数
				if (arg.length) {
					var inner = classNames.apply(null, arg);
          // 数组中每一个元素获得结果则放入结果集合中
					if (inner) {
						classes.push(inner);
					}
				}
			} else if (argType === 'object') {
        // 如果是对象则检查对象的toString方法和对象类型原型上的toString对比
				if (arg.toString === Object.prototype.toString) {
          // 如果是对象原型上的toString则依次针对每一个为真值的自有属性,放入结果集合中
					for (var key in arg) {
						if (hasOwn.call(arg, key) && arg[key]) {
							classes.push(key);
						}
					}
				} else {
          // 否则调用此对象的自定义toString方法
					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;
	}
}());

整体流程:主要处理逻辑+导出

主要处理逻辑:

1.遍历参数类表arguments,依次检查每一个参数

2.如果参数为假值,略过,检测下一个

3.如果参数为真值,检查参数类型:

(1)数字或者字符串加入到结果集合中

(2)数组则针对数组的每一个元素递归处理获得结果,结果为真值则加入到结果集合中

(3)对象则检查是否重写了toString方法。如果重写了则结果集合中加入调用toString方法后的值;

如果没有重写则依次检查对象的每一个自有属性,为真值则加入到结果集合中

4.结果集合(数组)以空格分隔组成字符串

5.总结收获

总结:classNames为一函数,接收不定数目的参数,参数可以是字符串,可以是对象,可以是数组。针对数字或者字符串将放入结果集中;针对对象只保留值为真的属性;针对数组,遍历每一个数组元素,递归调用classNames函数。

收获:了解classNames的使用方法和实现思路

6.参考文章

[1]www.jianshu.com/p/2dadb6079…