Source code reading: classnames

Source code reading: classnames

Introduction

classnamesA simple JavaScript utility for conditionally concatenating class names together.

It can be downloaded from the registry via npmthe package manager npm:

npm install classnames

classNamesFunctions accept any number of arguments, which can be strings or objects. Parameter 'foo'is short for { foo: true }. If the value associated with a given key is false, the key will not be included in the output.

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

Used in React:
The following code implements a button component with interactive functions. The style class name of the button will dynamically change according to the state of the button, thereby achieving the feedback effect of button press and mouse hover.

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

And use classnamesthe library to dynamically generate the class name of the button:

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':The key is btn, indicating that the button should contain the class name btn.
  • 'btn-pressed': isPressed: key is btn-pressed, indicating that when isPressedistrue , the button should contain the class name btn-pressed.
  • 'btn-over': !isPressed && isHovered: The key is btn-over, indicating that when isPressedis falseand isHoveredistrue , the button should contain the class name btn-over.

Because you can mix object, array, and string parameters, supporting optional className propproperties is also simpler, since only the real parameters are included in the result:

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

In addition, the author provides two other versions: dedupeversion and bindversion.

The where dedupeversion correctly deduplicates classes and ensures that spurious classes specified in the following parameters are excluded from the result set. However this version is slower (approximately 5x), so it is an optional version.

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

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

Another bindversion lets you combine css-modulesto dynamically add or remove CSS class names from components while maintaining css-modulesscope.

css-modulesIs a way to use locally scoped CSS in your project. It ensures that class names are unique across the entire application by adding a unique hash value to each class name, avoiding global scope class name conflicts.

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'

Here is an example of using a classnamescombination of bindversions 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>;
}

Source code interpretation

Since the code is relatively short, the source code is posted directly here with comments added for readers to read by themselves:

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

Compared with index.js, this version adds thiscontext processing.

type declaration

// 以下类型声明主要用于定义一个名为 "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;

Learn and gain

  1. Use strict mode

The main reason to use strict mode at the beginning of your source code is to ensure the quality and reliability of your code. Strict mode can help developers avoid some common errors and irregular syntax, while also providing stricter error checking and clearer error prompts. Using strict mode can reduce some hidden dangers and improve the maintainability and readability of the code.

In addition, strict mode can also prohibit some potentially dangerous behaviors, such as prohibiting the use of undeclared variables, prohibiting assignment of read-only attributes, prohibiting deletion of variables, etc. This can improve the security of your code and reduce some potential vulnerabilities and security risks.

Therefore, in order to ensure the quality, reliability and security of the code, many developers choose to use strict mode at the beginning of the source code. This forces code to conform to stricter specifications, reduces errors and potential problems, and improves code maintainability and readability.

  1. Create an empty object that does not inherit from Object

Object.create(null)It is a method that creates a new object. The object does not have a prototype chain, that is, it does not inherit any properties or methods. This means that the object has no built-in properties and methods and can only be added through direct assignment. Objects created using Object.create(null)are called "pure objects" or "dictionary objects" and are suitable for scenarios that require a pure collection of key-value pairs without inheritance. In this kind of object, the keys and values ​​can be any type of data, not just strings.

Using in the code creates StorageObject.prototype = Object.create(null);an Objectempty object that does not inherit from StorageObject. This can skip hasOwnPropertythe check and improve the performance of the code.

  1. Parse different types of arguments, including strings, arrays, objects, and numbers

  2. Design Patterns

The singleton pattern is a creational design pattern that ensures that there is only one instance of a class and provides a global access point to access that instance.

  • Singleton mode: wrap the code by executing the function immediately, create an classNamesobject inside the executing function, and assign it to the global variable window.classNames. This ensures that only one classNamesobject exists and no new classNamesobjects can be created elsewhere.

Factory pattern is a creational design pattern that provides an interface for creating objects, but the specific object type created can be determined at runtime. Factory pattern can be divided into simple factory pattern, factory method pattern and abstract factory pattern.

  1. Simple factory pattern: Also called static factory pattern, it directly uses a static method to create objects.
  2. Factory method pattern: Also known as the virtual factory pattern, it defines a factory interface and creates different objects by different specific factory implementations.
  • _classNames()Factory pattern: Create an object through a factory function classNames, which can call different parsing functions to parse parameters according to different parameter types, and store the results in classSetthe object.
  1. Determine the running environment and exportclassNames

According to different operating environments, determine whether it is in CommonJSthe environment, AMDenvironment or browser environment. If in CommonJSthe environment, classNamesassign to module.exports; if in AMDthe environment, classNamesregister as a module and name it 'classnames'; if in the browser environment, classNamesmount to the global windowobject.

  1. TypeScript type declaration
  • Namespace declaration: Use declare namespaceto define a namespace, organize related types and interfaces together, prevent naming conflicts and provide a modular structure.
  • Type aliases and union types: Use typethe keyword to define type aliases to facilitate the reuse of complex types. Union types can be used to represent that a value can be one of several different types.
  • Interface and inheritance: Use interfacethe keyword to define an interface, which represents the structure of an object. Interfaces can inherit from other interfaces, and existing interface definitions can be reused through inheritance.
  • Function type: You can use interfaces to define function types and specify the parameter types and return value types of the function.
  • Type export and module import: Use exportthe keyword to export a type or value so that it can be used in other modules. importExported types or values ​​can be imported in other modules using the keyword.

Guess you like

Origin blog.csdn.net/p1967914901/article/details/132023841