源码阅读:classnames

这篇具有很好参考价值的文章主要介绍了源码阅读:classnames。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

源码阅读:classnames

简介

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

可以通过 npm 包管理器从 npm 注册表上下载:

npm install classnames

classNames 函数接受任意数量的参数,可以是字符串或对象。参数 'foo' 是 { foo: true } 的缩写。如果与给定键关联的值是假的,则该键将不会包含在输出中。

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'

// 支持不同类型的参数同时传入
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'

classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'

// 数组将按照上述规则递归展平
const arr = ['b', { c: true, d: false }];
classNames('a', arr); // => 'a b c'
// 相当于
classNames('a', 'b', { c: true, d: false }); // => 'a b c'

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

在 React 中使用:
下面这段代码实现了一个具有交互功能的按钮组件。按钮的样式类名将根据按钮的状态动态改变,从而实现按钮按下和鼠标悬停的反馈效果。

import React, { useState } from 'react';

export default function Button (props) {
	const [isPressed, setIsPressed] = useState(false);
	const [isHovered, setIsHovered] = useState(false);

	let btnClass = 'btn';
	if (isPressed) btnClass += ' btn-pressed';
	else if (isHovered) btnClass += ' btn-over';

	return (
		<button
			className={btnClass}
			onMouseDown={() => setIsPressed(true)}
			onMouseUp={() => setIsPressed(false)}
			onMouseEnter={() => setIsHovered(true)}
			onMouseLeave={() => setIsHovered(false)}
		>
			{props.label}
		</button>
	);
}

而使用classnames库来动态生成按钮的类名:

import React, { useState } from 'react';
import classNames from 'classnames';

export default function Button (props) {
	const [isPressed, setIsPressed] = useState(false);
	const [isHovered, setIsHovered] = useState(false);

	const btnClass = classNames({
		btn: true,
		'btn-pressed': isPressed,
		'btn-over': !isPressed && isHovered,
	});

	return (
		<button
			className={btnClass}
			onMouseDown={() => setIsPressed(true)}
			onMouseUp={() => setIsPressed(false)}
			onMouseEnter={() => setIsHovered(true)}
			onMouseLeave={() => setIsHovered(false)}
		>
			{props.label}
		</button>
	);
}
  • 'btn: true':键为btn,表示按钮应该包含类名 btn
  • 'btn-pressed': isPressed:键为btn-pressed,表示当isPressedtrue时,按钮应该包含类名btn-pressed
  • 'btn-over': !isPressed && isHovered:键为btn-over,表示当isPressedfalseisHoveredtrue时,按钮应该包含类名btn-over

因为可以将对象、数组和字符串参数混合在一起,所以支持可选的 className prop属性也更简单,因为结果中只包含真实参数:

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

此外,作者还提供了另外两个版本:dedupe 版本和 bind 版本。

其中dedupe 版本可以正确地删除类的重复数据,并确保从结果集中排除后面参数中指定的虚假类。但是此版本速度较慢(大约 5 倍),因此它作为一个可选的版本。

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

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

而另一个bind 版本可以让你结合 css-modules,以便在组件中动态地添加或删除 CSS 类名,同时保证 css-modules 的作用域。

css-modules 是一种在项目中使用局部作用域的 CSS 的方法。它通过给每个类名添加一个唯一的哈希值,确保类名在整个应用程序中是唯一的,避免了全局作用域的类名冲突。

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

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

const cx = classNames.bind(styles);

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

下面是一个使用classnamesbind版本结合css-modules的示例:

import { useState } from 'react';
import classNames from 'classnames/bind';
import styles from './submit-button.css';

const cx = classNames.bind(styles);

export default function SubmitButton ({ store, form }) {
  const [submissionInProgress, setSubmissionInProgress] = useState(store.submissionInProgress);
  const [errorOccurred, setErrorOccurred] = useState(store.errorOccurred);
  const [valid, setValid] = useState(form.valid);

  const text = submissionInProgress ? 'Processing...' : 'Submit';
  const className = cx({
    base: true,
    inProgress: submissionInProgress,
    error: errorOccurred,
    disabled: valid,
  });

  return <button className={className}>{text}</button>;
}

源码解读

由于代码比较短,这里便直接放源码,并在其中加上了注释,读者可自行阅读:

index

/*!
	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];
			// 如果参数为空或为false,则跳过
			if (!arg) continue;

			// 获取参数的类型
			var argType = typeof arg;

			// 如果参数是字符串或数字,则直接添加到类名数组中
			if (argType === 'string' || argType === 'number') {
				classes.push(arg);
			} else if (Array.isArray(arg)) {
				if (arg.length) {
					// 如果参数是数组,则递归调用classnames函数,并将数组作为参数传入
					var inner = classNames.apply(null, arg);
					if (inner) {
						// 如果递归调用的结果不为空,则将结果添加到类名数组中
						classes.push(inner);
					}
				}
			} else if (argType === 'object') {
				// 判断 object 是否是一个自定义对象
				// 因为原生的 JavaScript 对象(例如 Array、Object 等)的 toString 方法包含 [native code]
				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);
					}
				}
			}
		}

		// 将类名数组通过空格连接成字符串,并返回
		return classes.join(' ');
	}

	// 判断是否在CommonJS环境下,如果是,则将classNames赋值给module.exports
	if (typeof module !== 'undefined' && module.exports) {
		classNames.default = classNames;
		module.exports = classNames;
	} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
		// 如果在AMD环境下,则将classnames函数注册为模块,并将其命名为'classnames'
		define('classnames', [], function () {
			return classNames;
		});
	} else {
		// 在浏览器环境下,将classnames函数挂载到全局的window对象上
		window.classNames = classNames;
	}
}());

dedupe

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

(function () {
	'use strict';

	var classNames = (function () {
		// 创建一个不继承自Object的空对象,以便后面可以跳过hasOwnProperty的检查
		function StorageObject() {}
		StorageObject.prototype = Object.create(null);
		
		// 解析数组,将数组中的每个元素解析为classNames
		function _parseArray (resultSet, array) {
			var length = array.length;

			for (var i = 0; i < length; ++i) {
				_parse(resultSet, array[i]);
			}
		}

		var hasOwn = {}.hasOwnProperty;
		
		// 解析数字,将数字作为classNames的属性
		function _parseNumber (resultSet, num) {
			resultSet[num] = true;
		}

		// 解析对象,将对象的属性作为classNames的属性
		function _parseObject (resultSet, object) {
			// 判断 object 是否是一个自定义对象
			// 因为原生的 JavaScript 对象(例如 Array、Object 等)的 toString 方法包含 [native code]
			if (object.toString !== Object.prototype.toString && !object.toString.toString().includes('[native code]')) {
				resultSet[object.toString()] = true;
				return;
			}

			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];
				}
			}
		}

		var SPACE = /\s+/;
		// 解析字符串,将字符串按照空格分割为数组,并将数组中的每个元素作为classNames的属性
		function _parseString (resultSet, str) {
			var array = str.split(SPACE);
			var length = array.length;

			for (var i = 0; i < length; ++i) {
				resultSet[array[i]] = true;
			}
		}

		// 解析参数,根据参数的类型调用相应的解析函数
		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);
			}
		}

		// 主函数
		function _classNames () {
			// 避免arguments泄漏
			var len = arguments.length;
			var args = Array(len);
			for (var i = 0; i < len; i++) {
				args[i] = arguments[i];
			}

			// 创建一个存储classNames的对象
			var classSet = new StorageObject();
			// 解析参数并将结果存储在classSet对象中
			_parseArray(classSet, args);

			var list = [];

			// 将classSet中值为true的属性加入到list数组中
			for (var k in classSet) {
				if (classSet[k]) {
					list.push(k)
				}
			}

			return list.join(' ');
		}

		return _classNames;
	})();

	// 判断是否在CommonJS环境下,如果是,则将classNames赋值给module.exports
	if (typeof module !== 'undefined' && module.exports) {
		classNames.default = classNames;
		module.exports = classNames;
	} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
		// 如果在AMD环境下,则将classnames函数注册为模块,并将其命名为'classnames'
		define('classnames', [], function () {
			return classNames;
		});
	} else {
		// 在浏览器环境下,将classnames函数挂载到全局的window对象上
		window.classNames = classNames;
	}
}());

bind

/*!
	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];
			// 如果参数为空或为false,则跳过
			if (!arg) continue;

			var argType = typeof arg;

			// 如果参数是字符串或数字,则直接添加到类名数组中
			if (argType === 'string' || argType === 'number') {
				classes.push(this && this[arg] || arg);
			} else if (Array.isArray(arg)) {
				// 如果参数是数组,则递归调用classnames函数,并将数组作为参数传入
				classes.push(classNames.apply(this, arg));
			} else if (argType === 'object') {
				// 判断 object 是否是一个自定义对象
				// 因为原生的 JavaScript 对象(例如 Array、Object 等)的 toString 方法包含 [native code]
				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(this && this[key] || key);
					}
				}
			}
		}

		return classes.join(' ');
	}

	// 判断是否在CommonJS环境下,如果是,则将classNames赋值给module.exports
	if (typeof module !== 'undefined' && module.exports) {
		classNames.default = classNames;
		module.exports = classNames;
	} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
		// 如果在AMD环境下,则将classnames函数注册为模块,并将其命名为'classnames'
		define('classnames', [], function () {
			return classNames;
		});
	} else {
		// 在浏览器环境下,将classnames函数挂载到全局的window对象上
		window.classNames = classNames;
	}
}());

index.js 相比,这个版本增加了对this上下文的处理。

类型声明

// 以下类型声明主要用于定义一个名为 "classNames" 的命名空间和相关的类型。
// 在这个声明中,"classNames" 命名空间中定义了一些类型和接口
declare namespace classNames {
	// "Value" 是一个联合类型,表示可以接受的值的类型,包括字符串、数字、布尔值、未定义和空值
  type Value = string | number | boolean | undefined | null;
	// "Mapping" 是一个类型别名,表示一个键值对的集合,其中键是字符串,值可以是任何类型
  type Mapping = Record<string, unknown>;
	// "ArgumentArray" 是一个接口,继承自数组类型 "Array<Argument>",表示一个参数数组,其中每个元素都是 "Argument" 类型
  interface ArgumentArray extends Array<Argument> {}
	// "ReadonlyArgumentArray" 是一个接口,继承自只读数组类型 "ReadonlyArray<Argument>",表示一个只读的参数数组
  interface ReadonlyArgumentArray extends ReadonlyArray<Argument> {}
	// "Argument" 是一个联合类型,表示可以作为参数的类型,可以是 "Value"、"Mapping"、"ArgumentArray" 或 "ReadonlyArgumentArray"
  type Argument = Value | Mapping | ArgumentArray | ReadonlyArgumentArray;
}

// 定义了一个名为 "ClassNames" 的接口,它是一个函数类型,可以接受 "classNames.ArgumentArray" 类型的参数,并返回一个字符串
interface ClassNames {
	(...args: classNames.ArgumentArray): string;

	default: ClassNames;
}

declare const classNames: ClassNames;

// 通过 "export as namespace" 来将 "classNames" 声明为全局命名空间
export as namespace classNames;
// 使用 "export =" 来导出 "classNames",使其可以在其他模块中使用
export = classNames;

学习与收获

  1. 使用严格模式

在源码开头使用严格模式的主要原因是为了确保代码的质量和可靠性。严格模式可以帮助开发者避免一些常见的错误和不规范的语法,同时也提供了更严格的错误检查和更清晰的错误提示。使用严格模式可以减少一些隐患,提高代码的可维护性和可读性。

此外,严格模式还可以禁止一些潜在的危险行为,例如禁止使用未声明的变量、禁止对只读属性赋值、禁止删除变量等。这可以提高代码的安全性,减少一些潜在的漏洞和安全风险。

因此,为了确保代码的质量、可靠性和安全性,许多开发者选择在源码开头使用严格模式。这样可以强制要求代码符合更严格的规范,减少错误和潜在的问题,并提高代码的可维护性和可读性。

  1. 创建一个不继承自 Object 的空对象

Object.create(null) 是一个创建一个新对象的方法,该对象没有原型链,也就是没有继承任何属性和方法。这意味着该对象没有内置的属性和方法,只能通过直接赋值来添加属性和方法。使用 Object.create(null) 创建的对象被称为“纯净对象”或“字典对象”,它适用于需要一个纯粹的键值对集合而不需要继承的场景。在这种对象中,键和值可以是任何类型的数据,而不仅限于字符串。

在代码中使用了 StorageObject.prototype = Object.create(null); 创建了一个不继承自 Object 的空对象 StorageObject。这样可以跳过 hasOwnProperty 的检查,提高代码的性能。

  1. 解析不同类型的参数,包括字符串、数组、对象和数字

  2. 设计模式

单例模式是一种创建型设计模式,用于确保某个类只有一个实例,并提供一个全局访问点来访问该实例。

  • 单例模式:通过立即执行函数包裹代码,在执行函数内部创建了一个classNames对象,并将其赋值给全局变量window.classNames。这样就保证了只有一个classNames对象存在,其他地方无法再创建新的classNames对象。

工厂模式是一种创建型设计模式,它提供了一种创建对象的接口,但具体创建的对象类型可以在运行时确定。工厂模式可以分为简单工厂模式、工厂方法模式和抽象工厂模式。

  1. 简单工厂模式:也称为静态工厂模式,它直接使用一个静态方法来创建对象。
  2. 工厂方法模式:也称为虚拟工厂模式,它定义了一个工厂接口,并由不同的具体工厂实现来创建不同的对象。
  • 工厂模式:通过工厂函数_classNames()创建classNames对象,该对象可以根据不同的参数类型调用不同的解析函数来解析参数,并将结果存储在classSet对象中。
  1. 判断运行环境并导出 classNames

根据不同的运行环境,判断是否在 CommonJS 环境下、AMD 环境下或浏览器环境下。如果在 CommonJS 环境下,将 classNames 赋值给 module.exports;如果在 AMD 环境下,将 classNames 注册为模块,并命名为 'classnames';如果在浏览器环境下,将 classNames 挂载到全局的 window 对象上。文章来源地址https://www.toymoban.com/news/detail-621949.html

  1. TypeScript 类型声明
  • 命名空间声明:使用declare namespace可以定义一个命名空间,将相关的类型和接口组织在一起,防止命名冲突并提供模块化的结构。
  • 类型别名和联合类型:使用 type 关键字可以定义类型别名,方便重复使用复杂的类型。联合类型可以用于表示一个值可以是多个不同类型之一。
  • 接口和继承:使用 interface 关键字可以定义接口,表示一种对象的结构。接口可以继承自其他接口,通过继承可以复用已有的接口定义。
  • 函数类型:可以使用接口来定义函数类型,指定函数的参数类型和返回值类型。
  • 类型导出和模块导入:使用 export 关键字可以将类型或值导出,使其可以在其他模块中使用。使用 import 关键字可以在其他模块中导入已导出的类型或值。

到了这里,关于源码阅读:classnames的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【前端|Javascript第1篇】一文搞懂Javascript的基本语法

    欢迎来到JavaScript的奇妙世界!作为前端开发的基石,JavaScript为网页增色不少,赋予了静态页面活力与交互性。如果你是一名前端小白,对编程一无所知,或者只是听说过JavaScript却从未涉足过,那么你来对了地方!本篇博客将带领你逐步进入JavaScript的大门,一步一步地探索这

    2024年02月14日
    浏览(30)
  • Web前端-JavaScript

    目录 1 概述 2 HTML嵌入JS代码三种方式 2.1 第一种方式 2.2 第二种方式 2.3 第三种方式 3 变量 4 数据类型 4.1 概述 4.2 Number数据类型  4.3 几个值得注意的函数 4.4 Boolean数据类型  4.5 String数据类型 4.6 关于Object类型 4.7 NaN、undefined、null的区别 4 函数 5 事件 5.1 JS中有哪些常用的事件

    2024年02月09日
    浏览(44)
  • JavaScript前端接收流式数据

    在Java开发中,前端接收流式数据通常涉及到使用WebSocket 或Server-Sent Events(SSE)这样的技术。这两种技术都允许服务器推送实时数据到客户端,以便在浏览器中进行处理和更新。 1.  WebSocket: WebSocket是一种在单个 TCP 连接上进行全双工通信的协议。在Java 中,你可以使用Java

    2024年04月27日
    浏览(28)
  • 前端开发——Javascript知识(介绍)

    目录 有关JavaScript的知识  JavaScript的优点   JavaScript的领域 JavaScript的组成 JavaScript的特点 第一个JavaScript程序 在 HTML 文档中嵌入 JavaScript 代码 在脚本文件中编写 JavaScript 代码 JavaScript内容  Html内容  JavaScript 代码执行顺序 JavaScript中的几个重要概念 标识符 保留字 区分

    2024年02月01日
    浏览(37)
  • 前端面试问题-JavaScript

    1 闭包 闭包就是能够读取其他函数内部变量的函数 闭包是指有权访问另⼀个函数作⽤域中变量的函数,创建闭包的最常⻅的⽅式就是在⼀个函数内创建另⼀个函数,通过另⼀个函数访问这个函数的局部变量,利⽤闭包可以突破作⽤链域 闭包的特性: 函数内再嵌套函数 内部函

    2024年02月15日
    浏览(36)
  • 前端进化笔记-JavaScript(三)

    人类在白色的底色上描绘图画,地球在黑色的底色上创造生命。 JavaScript的变量可以说是独树一帜。只需要一个(或两个等)(const,let)就可以创建变量,创建时不考虑变量的类型,这是其他语言少有的强大功能。当然强大的功能总是伴随着问题。 原始值:Undefined,

    2024年02月08日
    浏览(72)
  • 前端JavaScript

    全称JavaScript但是与Java一毛钱关系都没有 之所以这么叫是为了蹭Java的热度。JavaScript是一门前端工程师的编程语言 但是它本身有很多逻辑错误(不是很严谨)。 IT行业鄙视链: 后端 前端、运维、测试、产品、老板 前端想一统天下:node.js JS简介 1.ECMAScript和JavaScript的关系 1996年11月

    2024年02月06日
    浏览(32)
  • 前端基础之JavaScript

    JavaScript书写 JavaScript标签内写代码 引入其他js文件 语言规范 注释: 结束符: 在JavaScript中,结束符以;分好表示 变量声明: 1. 在JavaScript中,变量的命名以:数字,字母,_,$组成,不能以数字开头。 2.声明变量时,使用var 变量名来声明 注意事项:变量名的命名规范使用驼峰

    2024年02月06日
    浏览(31)
  • 前端面试题---->JavaScript

    原因: 当使用const声明一个对象或数组时,实际上是保证了对象或数组的引用不会被修改,但对象或数组本身的属性或元素是可以被修改的。这是因为const只能保证指向的内存地址不变,但并不保证内存地址指向的内容不变,而基本类型的变量在内存中存储的是值本身,而不

    2024年03月27日
    浏览(44)
  • 【前端】JavaScript简介

    人不走空                                                                              目录         🌈个人主页:人不走空       💖系列专栏:算法专题 ⏰诗词歌赋:斯是陋室,惟吾德馨 JavaScript的基础 JavaScript进阶 现代Web开发与JavaScript JavaScript的未来 作

    2024年02月21日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包