分析classnames插件源码

133 阅读4分钟

说明

想必读者或多或少都使用过,或者知道有这么一个帮助开发者取类名的插件:classnames

当然,Vue开发者可能很少使用,毕竟Vue本身就动态自定义class类名,而React中会麻烦点,因此这个插件会用的比较多,今天我们就来看看classnames内部是如何实现的。

一、资源准备

# 克隆项目
https://github.com/JedWatson/classnames

# 进入
cd classnames

image.png

index.js是入口文件,但是,其实这里一共有三个模块:index.js、bind.js、dedupe.js;

  • index.js:普通的处理class类名;
  • bind.js:在index.js基础上支持指定this;
  • dedupe.js:在index.js基础上支持去重;

实际三种引入:都支持commonjs和esmodule。

import classNames from 'classnames';
var classNames = require('classnames');

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

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

二、源码分析

我们先分析index.js的,bind.js和index.js几乎一样,而dedupe就是把index.js的多个判断抽离成单独函数。

考虑到代码量不多,我这边直接把全部代码复制来,在代码中注释说明。

1、index

(function () {
    'use strict';
    
    // 单独抽离获取属性是否在对象自身上的方法
    var hasOwn = {}.hasOwnProperty;

    // classNames方法
    function classNames() {
        // 存储最后的类名
        var classes = [];
        // 遍历参数
        for (var i = 0; i < arguments.length; i++) {
            var arg = arguments[i];
            // 所有转换为布尔值为false的一律跳过不添加:如undefined/null/0/''/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,注意这里的inner为最后数组根据split为空格拆分的字符串
                    var inner = classNames.apply(null, arg);
                    if (inner) {
                        classes.push(inner);
                    }
                }
            } else if (argType === 'object') {
                // 对象的话,剔除重写了toString的数据
                if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes('[native code]')) {
                    classes.push(arg.toString());
                    continue;
                }
                // 其余对象遍历,如果是自身属性,且值转为布尔值为true,则添加键名
                for (var key in arg) {
                    if (hasOwn.call(arg, key) && arg[key]) {
                        classes.push(key);
                    }
                }
            }
        }
        // 数组拆分成字符串
        return classes.join(' ');
    }
    
    // commonjs模块时挂载
    if (typeof module !== 'undefined' && module.exports) {
        classNames.default = classNames;
        module.exports = classNames;
    } else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
        // amd模块时挂在
        // register as 'classnames', consistent with npm package name
        define('classnames', [], function () {
            return classNames;
        });
    } else {
        // 浏览器时挂载 
        window.classNames = classNames;
    }
}());

2、bind

bind的话就是在index.js基础上,对于每一个push的操作,进行判断是否在this上为true而已,此处不再赘述。

(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(this && this[arg] || arg);
			} else if (Array.isArray(arg)) {
				classes.push(classNames.apply(this, arg));
			} else 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(this && this[key] || key);
					}
				}
			}
		}

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

3、dedupe

考虑到需要去重,此处通过Object.create(null),创建了一个比较干净的对象作为存储容器进行去重。

(function () {
	'use strict';

	var classNames = (function () {
		// don't inherit from Object so we can skip hasOwnProperty check later
		// http://stackoverflow.com/questions/15518328/creating-js-object-with-object-createnull#answer-21079232
                // 创建原型对象为null的构造函数,创建的实例用来作为去重对象存储容器
		function StorageObject() {}
		StorageObject.prototype = Object.create(null);
                
                // 处理数组
		function _parseArray (resultSet, array) {
			var length = array.length;

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

		var hasOwn = {}.hasOwnProperty;
                // 处理数字
		function _parseNumber (resultSet, num) {
			resultSet[num] = true;
		}
                
                // 处理对象
		function _parseObject (resultSet, object) {
			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+/;
		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);
			}
		}
                // classnames方法
		function _classNames () {
			// don't leak arguments
			// https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-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);

			var list = [];

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

			return list.join(' ');
		}

		return _classNames;
	})();

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

三、总结

通过分析,其实classname的实现并不复杂,只要熟练掌握各种数据类型,和对应的内置方法,我们完全可以自己实现。