我是靠谱客的博主 机灵人生,最近开发中收集的这篇文章主要介绍三千字详解 classnames,精读源码,解读重点功能的实现前言听说你叫 classNameclassnames 出现的契机classnames 的用法classnames 的原理,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

前言

本文主要讲解 classnames 相关的知识点。

对 classnames 用法做了详细介绍。

对 classnames 源码,按照功能模块进行解读。尤其对于源码中关键代码从实现层面做了解读。

在总结过程中,对 CSS-in-JS 写法有了不同的想法,结合大佬的文章,将想法记录在了文末。

文章速读

阅读本篇文章,你将有如下图中的收获:

听说你叫 className

讲 classnames 之前,科普一点关于它「兄弟」 className 的知识点。

万物皆有源之 JSX

众所周知,在 Recat 中配合使用 JSX 语法。而 JSX 在语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase(小驼峰命名)来定义属性的名称。

因此,在 React 中使用 className 为元素指定 CSS 的 class。

className 的两种用法

我们在 React 中是这样为元素添加样式的:

<div className="mt20">Hello World</div> 

或者根据某个变量的值进行动态绑定,如下就根据 selected 的值判断是否为元素添加 active 样式:

<div className={`${selected ? 'active' : ''}`}>{selectedText}
</div> 

必有但是

动态绑定的方式确实让代码变得更加灵活,但是上面这种 ES6 模板字符串的写法,当有多个判断的时,会显得臃肿且不好维护。

那,有没有更好的实现方案呢?

classnames 出现的契机

每当遇到一个新知识点,我总是不由的想,它为什么会诞生?它帮忙解决了什么问题?

开发者需要什么?

从前面的文字中提取关键讯息,不难发现,开发者需要更简洁绑定多个 className 的方式。

classnames 是什么?

而 classnames 源码的第一句介绍,正好符合开发者的需要

A simple JavaScript utility for conditionally joining classNames together.

直译过来就是

一个简单的、有条件的绑定多个 className 的 JavaScript 实用函数.

classnames 的用法

用前先安装

classnames 是一个第三方 JavaScript 库,使用前需要先安装。

npm、bower、yarn三件套

# via npm
npm install classnames

# via Bower
bower install classnames

# or Yarn (note that it will automatically save the package to your `dependencies` in `package.json`)
yarn add classnames 

语法

classnames 函数支持多个传参,参数的类型也支持数值、字符串、对象、数组等多种。

classnames(class1,class2,...classN) 

用法很多样

基础用法

// 多个字符串 两种写法均可
classNames('mt10', 'font20') // => 'mt10 font20'
classNames('mt10 font20') // => 'mt10 font20'

//「字符串+对象」组合
classNames('switch', { selected: false }) // => 'switch'
classNames('switch', { selected: true }) // => 'switch selected'

// 对象 三种写法均可
classNames({ switch: true }, { selected: true }); // => 'switch selected'
classNames({ switch: true, selected: true }); // => 'switch selected'
classNames({ 'switch selected': true }); // => 'switch selected'

// 数组
classNames('font20', ['mt10', { switch: true, selected: false }]); // => 'font20 mt10 switch' 

ES6 语法

支持动态类名

let type = 'disabled';
classNames('radio', { [`radio-${type}`]: true }); // => 'radio radio-disabled'
classNames('radio', `radio-${type}`); // => 'radio radio-disabled' 

React 中使用

前面提到过 className 的知识点,React 中添加 class 使用 className。

import classNames from 'classnames';

const checkboxCls = classNames('checkbox-group', { 'checkbox-group-horizontal': true });
<div className={checkboxCls}></div> 

小结

1.classnames 支持多种写法。
2.classnames 函数的入参是无序的,但是编写习惯是字符串类型的放前面,其他放在后面。
3.classnames 函数会忽略入参中的错误值。
4.classnames 函数支持动态类名。

classnames 的原理

源码目录

功能模块

目录结构

classnames
 ┣ ????benchmarks
 ┃ ┣ ????fixtures.js 
 ┃ ┣ ????run.js 
 ┃ ┣ ????runChecks.js 
 ┃ ┣ ????runInBrowser.js 
 ┃ ┣ ????runSuite.js 
 ┣ ????tests
 ┃ ┣ ????bind.js 
 ┃ ┣ ????dedupe.js 
 ┃ ┣ ????index.js 
 ┣ ????bind.js
 ┣ ????dedupe.js
 ┗ ????index.js 

解读时刻

底层代码

基础功能

index.js 中的代码并不是很多,做了以下几步操作。

1.定义数组 classes,一个包含所有的最终有效的 class 的数组。

var classes = []; 

2.for 循环函数自带 arguments 实例,得到 classNames 函数的所有实参。当某个实参的值不存的时候,跳过这个实参,进入下一个迭代。

for (var i = 0; i < arguments.length; i++) {
	var arg = arguments[i];
	if (!arg) continue;
} 

3.获取每个参数的数据类型,后面会根据这个类型做不同的处理。

var argType = typeof arg; 

4.如果参数的数据类型是字符串或者数值,直接放到 classes 数组中。

if (argType === 'string' || argType === 'number') {classes.push(arg);
} 

5.如果参数的数据类型是数组且数组不为空时,进行递归+apply处理。将数组参数扁平化,得到扁平化之后的值存在,则放到 classes 数组中。

if (Array.isArray(arg)) {if (arg.length) {var inner = classNames.apply(null, arg);if (inner) {classes.push(inner);}}} 

6.如果参数的数据类型是对象时,处理相对复杂一些。

首先,对象类型的参数做了更精准的一次判断,使用 Object.prototype.toString 进行原生对象类型判断,为了进一步确保对象类型判断的准确性,还增加了对象 toString 方法的 toString() 的输出值的判断,原生函数的 toString() 会返回 “function fetch() { [native code] }”。

之后,对于不同的条件结果,进行了不同的处理:

  • 当两个判断都不成立的时候,表示该参数不是原生对象,将参数调用 toString 的结果放到 classes 数组中。
  • 否则,循环对象参数,将每个自有属性放到 classes 数组中。
if (argType === 'object') {if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {classes.push(arg.toString());continue;}for (var key in arg) {if (hasOwn.call(arg, key) && arg[key]) {classes.push(key);}}
} 

7.提供了三种导出 classNames 的方式。

  • module.exports 导出 classNames
  • 直接返回 classNames
  • 将classNames 挂载到 window 上
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 namedefine('classnames', [], function () {return classNames;});
} else {window.classNames = classNames;
} 

补充

为什么非继承自 Object.prototype 的对象最终使用其 toString() 返回的值。然后我就想到自定义实例对象。

function MarginTop(val) {this.val = val;
}

const mt10 = new MarginTop('mt10');

MarginTop.prototype.toString = function mtToString() {return `${this.val}`;
};

console.log(mt10.toString === Object.prototype.toString); // false
console.log(mt10.toString()); // 'mt10' 

bind 功能

为什么有这个版本的功能呢?

这里跟大家分享一个我个人阅读源码的小技巧。

当看到的源码里没有功能的详细注释的时候,可以到 README.md 里面找一找,有没有对该功能的解释。

比如这里 bind 版本的功能,README.md 给的解释是:

当你使用[css-modules]方式,或者抽象类和实际输出到DOM的真实类做映射的方式,可能需要使用 bind 变量。

且结合使用案例,了解到 bind 功能提供了抽象类名映射的功能,所以它的实现代码与基础功能的代码区别在于确定最终的参数的值。

会先判断是否参数是否存在对应的枚举值,如果存在则将枚举值放到 classes 数组中,否则将该参数放到 classes 数组中。

// 基础功能代码
classes.push(arg);
// bind 功能代码
classes.push(this && this[arg] || arg); 

dedupe 功能

dedupe 的功能主要是帮助消除类的重复数据,并确保结果集中已经排除了后面参数中指定的错误类。

相较 bind 功能,dedupe 做的处理要更多。

1.使用 Object.create() 方法创建一个新对象。可以帮助后面跳过hasOwnProperty检查。

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

// 去重对象
var classSet = new StorageObject(); 

2.利用对象的 key 值不会重复的特性,进行去重。很机智的实现方式。将所有的对象值设置为 true。

function _parseString(resultSet, str) {var array = str.split(SPACE);var length = array.length;for (var i = 0; i < length; ++i) {resultSet[array[i]] = true;}
} 

3.定义数组 list,一个包含所有的最终有效的 class 的数组。

var list = []; 

4.将所有 value 值为 true 的 key 值放到 list 数组中。

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

5.classNames 导出的处理跟基础功能一致。

测试用例

我看川哥提示「多关注测试用例」。然后我发现源码中测试用例有三个文件 bind.jsdedupe.jsindex.js,分别对应了三种版本的测试用例,为什么会提供三个版本的测试用例呢?容我研究研究。

基础版本

想要看懂测试用例,还需要了解一些单元测试的知识点。以部分代码为例,来看看 classnames 提供的测试用例里面的都写了点什么。

源码片段

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');
	});

	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');
	});......
}); 

名词解释

名词介绍参数
describe定义一组测试的函数,创建一个测试集。
it定义一个具体的测试用例的函数。两个入参。第一个参数是测试描述,字符串类型。第二个是测试代码,函数类型。
assert断言函数。
assert.equal类似相等运算符的方法。会将第一个参数和第二个参数进行比较。一般三个参数。第一个参数是实际值。第二个参是预测值。第三个参数存放错误信息。

一组测试用例

看完名词解释,豁然开朗,这个页面定义了一组具体的测试用例。我不一一列出来了,根据 it 函数里的测试描述和测试代码不难理解测试内容。

简单分享几个我理解的它要做的测试:

// 过滤错误值的入参并将正确的值连接起来
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('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');
}); 

其他版本

classnames 也提供了bind 版本和dedupe 版本的测试用例。

基准测试

这个概念我第一次听到。于是我百度了一下相关知识点。

首先,百科的介绍是

基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。

其次,为什么需要基准测试?因为基准测试可以帮忙测试代码的性能。

最后,我观察了一下 classnames 中的代码。基于 fixtures.js 文件中提供的不同数据类型的组合和功能文件不同的引入方式,进行了测试。

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

// npm下载的文件
suite.add('npm#' + fixture.description, function () {npm.apply(null, fixture.args);
});

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

// other handling
suite.on('complete', function () {log('n> Fastest is' + (' ' + this.filter('fastest').map(result => result.name).join(' | ')).replace(/s+/, ' ') + 'n');
}); 

最后

为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

最后

以上就是机灵人生为你收集整理的三千字详解 classnames,精读源码,解读重点功能的实现前言听说你叫 classNameclassnames 出现的契机classnames 的用法classnames 的原理的全部内容,希望文章能够帮你解决三千字详解 classnames,精读源码,解读重点功能的实现前言听说你叫 classNameclassnames 出现的契机classnames 的用法classnames 的原理所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(45)

评论列表共有 0 条评论

立即
投稿
返回
顶部