本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
1. 前言
今天来学习以下 classnames 的源码,这个库应该 react 的开发者都有用过,这两者联系可谓是非常紧密,配合使用想当方便
1.1 你能学到
- classnames 的用法
- classnames 的原理
- classnames 中的测试
2. 看代码之前
2.1 了解该库的用途
A simple JavaScript utility for conditionally joining classNames together.
一个简单的 JavaScript 实用程序,用于有条件地将类名连接在一起。
2.2 使用方式
classNames 作为核心方法,可以接收任意数量的参数,并根据一些判断依据返回最终以空格分隔的类名串。
如果key 的值是 false 那么就不会加入到最后的字符串中
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'
这样可以结合 ES6 中的模板字符串达到动态类型的效果
let buttonType = 'primary';
classNames({ [`btn-${buttonType}`]: true });
react + classNames
在 react 组件中,就可以将其设置为 state,通过绑定事件的回调函数对其进行更改,使其类名动态更改、样式动态更改的效果
class Button extends React.Component {
// ...
render () {
var btnClass = 'btn';
if (this.state.isPressed) btnClass += ' btn-pressed';
else if (this.state.isHovered) btnClass += ' btn-over';
return <button className={btnClass}>{this.props.label}</button>;
}
}
对象形式
var classNames = require('classnames');
//又或者这样
class Button extends React.Component {
// ...
render () {
var btnClass = classNames({
btn: true,
'btn-pressed': this.state.isPressed,
'btn-over': !this.state.isPressed && this.state.isHovered
});
return <button className={btnClass}>{this.props.label}</button>;
}
}
或者结合 props
var btnClass = classNames('btn', this.props.className, {
'btn-pressed': this.state.isPressed,
'btn-over': !this.state.isPressed && this.state.isHovered
});
3 看看源码
3.1 经典查看入口
从 packeage.json 中查看 main 字段就可以确认 index.js 是入口,该文件一共有 58 行。
3.2 直接开看
一开始先用了一个 自执行的函数 来包裹整个作用域 从而避免变量污染冲突,并且采用严格模式(好像几乎所有开源项目都是用严格模式的)
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/* global define */
(function () {
'use strict';
hasOwnProperty
hasOwnProperty
这个方法用来判断对象的属性是否属于自己本身——而不是往原型链上面找到的
var hasOwn = {}.hasOwnProperty;
随后就是主要方法 classNames 了(接下来为了方便我写也方便读者阅读,就全部都写到注释里面了)
function classNames() {
var classes = []; //一个专门存储最后类名合集的数组
//传入参数不限制数量,自然是用到参数对象 arguments 这个东西
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i]; //遍历 arguments 拿到每一项
if (!arg) continue; //如果该项的值为 undefined、null之类的就直接跳过
var argType = typeof arg;//获取该项的类型
//字符串或者数字之类的直接加入 classes 中就完事了
if (argType === 'string' || argType === 'number') {
classes.push(arg);
} else if (Array.isArray(arg)) {
if (arg.length) {
var inner = classNames.apply(null, arg); //针对数组中的每一项都需要进行判断是否能够加入 classes 中,所以利用 递归+apply 达到数组扁平化的效果
if (inner) { //递归调用返回的不是空字符串 '' 的话就加入 classes
classes.push(inner); //放入
}
}
} else if (argType === 'object') {//对象的情况下
//如果自带的 toString 方法 和 Object 的一样
if (arg.toString === Object.prototype.toString) { //'[object object]'的情况
for (var key in arg) {//用 for in 遍历对象中的可枚举属性
if (hasOwn.call(arg, key) && arg[key]) {//如果该属性是自身的 && value 为 true(或者说 可以转变为 true)
classes.push(key);//就将 key 放入 classes 中,注意是key
}
}
} else {//否则就用自身自定义的 toString 方法
classes.push(arg.toString());
}
}
}
return classes.join(' ');//用 join 方法将数组变为字符串,用' '隔开
}
//用于支持各种导出方式
if (typeof module !== 'undefined' && module.exports) {// CommonJS
classNames.default = classNames;
module.exports = classNames;
} else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {// AMD, 通过判断是否又 define 方法以及 define.amd 是否为 object
// register as 'classnames', consistent with npm package name
define('classnames', [], function () {
return classNames;
});
} else {
//浏览器环境
window.classNames = classNames;
}
}());
3.3 除了 index 的其他版本
bind 版本
前面的 index 只是单纯的拼接,而 bind 版本 还可以通过 bind 指定读取属性的对象,传入 classNames 的参数先作为 key 到绑定的对象中寻找 value,如果有,就放value 进去,如果没有才放入 key
使用起来是这样的:
var classNames = require('classnames/bind');
var styles = {
foo: 'abc',
bar: 'def',
baz: 'xyz'
};
var cx = classNames.bind(styles);
var className = cx('foo', ['bar'], { baz: true }); // => "abc def xyz"
主要代码差别在这里:
classes.push(this && this[arg] || arg);//绑定了this,并且key(arg)对应的value有值=> this[arg] || 没有这个东西=>arg 本身
dedupe 版本
dedupe 版本不是单纯的拼接,而是有去重操作。而具体去重操作是通过对象Object 键值对来实现的,所以也就有后来的能覆盖前面属性
var classNames = require('classnames/dedupe');
classNames('foo', 'foo', 'bar'); // => 'foo bar'
classNames('foo', { foo: false, bar: true }); // => 'bar'
用 StorageObject 来存储进行去重
function StorageObject() {}
StorageObject.prototype = Object.create(null);
//使用时就是这样
var classSet = new StorageObject();
使用 create(null)
可以让后面的判断省去一个 hasOwnProperty
。
主要区别就是 _parse方法,其中又调用了一些其他解析方法,其实也就只是不同的数据类型对应不同的操作
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 中 解析的入口就是对数组类型的操作—— 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);
最后根据对象键值对的value 是否为 true,来决定是否放入list——即结果数组,再用 join 进行处理返回最终的字符串
for (var k in classSet) {
if (classSet[k]) {
list.push(k)
}
}
return list.join(' ');
3.4 benchmark
基准测试
你可以会有这样的需求:想比较两个效果相同的方法谁的性能较优。但是JS代码在不同运行环境下运行的效率可能是不一样的。这就是为什么我们需要基准测试。
我们从依赖包中可以看见 benchmark
这个包,这是用于做性能基准测试的,这里是他的官方仓库。
这里就是 classNames 中进行测试的地方,具体怎么写测试样例建议看下面学习资源中的官方文档。
4. 学习资源
- benchmark
-
5. 总结 & 收获
arguments ,在很多方法中都很有用到,很实用
- 用 apply 第二个参数接收数组形式来实现数组扁平化的效果
- 利用对象键值对来去重
- 各个环境的导出处理,这个我还是第一次了解
- 总体来说,是没想到经常使用的一个库逻辑居然如此简单——读取参数内容,根据类型处理,放入数组最后再进行拼接
- 当然,尽管自己看源代码都能够知道这行代码是做什么的,但是如果说要自己实现一个.. 😴