本文参加了由公众号@若川视野 发起的每周源码共读活动点击了解详情一起参与。

1. 前言

今天来学习以下 classnames 的源码,这个库应该 react 的开发者都有用过,这两者联系可谓是非常紧密,配合使用想当方便

1.1 你能学到

  1. classnames 的用法
  2. classnames 的原理
  3. classnames 中的测试

2. 看代码之前

先从 README 中了解该库的相关信息

2.1 了解该库的用途

A simple JavaScript utility for conditionally joining classNames together.

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

2.2 使用方式

classNames 作为核心方法,可以接收任意数量的参数,并根据一些判断依据返回最终以空格分隔的类名串。
如果key 的值是 false 那么就不会加入到最后的字符串中

  1. classNames('foo', 'bar'); // => 'foo bar'
  2. classNames('foo', { bar: true }); // => 'foo bar'
  3. classNames({ 'foo-bar': true }); // => 'foo-bar'
  4. classNames({ 'foo-bar': false }); // => ''
  5. classNames({ foo: true }, { bar: true }); // => 'foo bar'
  6. classNames({ foo: true, bar: true }); // => 'foo bar'
  7. // lots of arguments of various types
  8. classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
  9. // other falsy values are just ignored
  10. classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'

数组传入就按扁平化后再根据判断处理:

  1. var arr = ['b', { c: true, d: false }];
  2. classNames('a', arr); // => 'a b c'

这样可以结合 ES6 中的模板字符串达到动态类型的效果

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

react + classNames

在 react 组件中,就可以将其设置为 state,通过绑定事件的回调函数对其进行更改,使其类名动态更改、样式动态更改的效果

  1. class Button extends React.Component {
  2. // ...
  3. render () {
  4. var btnClass = 'btn';
  5. if (this.state.isPressed) btnClass += ' btn-pressed';
  6. else if (this.state.isHovered) btnClass += ' btn-over';
  7. return <button className={btnClass}>{this.props.label}</button>;
  8. }
  9. }

对象形式

  1. var classNames = require('classnames');
  2. //又或者这样
  3. class Button extends React.Component {
  4. // ...
  5. render () {
  6. var btnClass = classNames({
  7. btn: true,
  8. 'btn-pressed': this.state.isPressed,
  9. 'btn-over': !this.state.isPressed && this.state.isHovered
  10. });
  11. return <button className={btnClass}>{this.props.label}</button>;
  12. }
  13. }

或者结合 props

  1. var btnClass = classNames('btn', this.props.className, {
  2. 'btn-pressed': this.state.isPressed,
  3. 'btn-over': !this.state.isPressed && this.state.isHovered
  4. });

3 看看源码

3.1 经典查看入口

packeage.json 中查看 main 字段就可以确认 index.js 是入口,该文件一共有 58 行。

3.2 直接开看

一开始先用了一个 自执行的函数 来包裹整个作用域 从而避免变量污染冲突,并且采用严格模式(好像几乎所有开源项目都是用严格模式的)

  1. /*!
  2. Copyright (c) 2018 Jed Watson.
  3. Licensed under the MIT License (MIT), see
  4. http://jedwatson.github.io/classnames
  5. */
  6. /* global define */
  7. (function () {
  8. 'use strict';

hasOwnProperty

hasOwnProperty这个方法用来判断对象的属性是否属于自己本身——而不是往原型链上面找到的

  1. var hasOwn = {}.hasOwnProperty;

随后就是主要方法 classNames 了(接下来为了方便我写也方便读者阅读,就全部都写到注释里面了)

  1. function classNames() {
  2. var classes = []; //一个专门存储最后类名合集的数组
  3. //传入参数不限制数量,自然是用到参数对象 arguments 这个东西
  4. for (var i = 0; i < arguments.length; i++) {
  5. var arg = arguments[i]; //遍历 arguments 拿到每一项
  6. if (!arg) continue; //如果该项的值为 undefined、null之类的就直接跳过
  7. var argType = typeof arg;//获取该项的类型
  8. //字符串或者数字之类的直接加入 classes 中就完事了
  9. if (argType === 'string' || argType === 'number') {
  10. classes.push(arg);
  11. } else if (Array.isArray(arg)) {
  12. if (arg.length) {
  13. var inner = classNames.apply(null, arg); //针对数组中的每一项都需要进行判断是否能够加入 classes 中,所以利用 递归+apply 达到数组扁平化的效果
  14. if (inner) { //递归调用返回的不是空字符串 '' 的话就加入 classes
  15. classes.push(inner); //放入
  16. }
  17. }
  18. } else if (argType === 'object') {//对象的情况下
  19. //如果自带的 toString 方法 和 Object 的一样
  20. if (arg.toString === Object.prototype.toString) { //'[object object]'的情况
  21. for (var key in arg) {//用 for in 遍历对象中的可枚举属性
  22. if (hasOwn.call(arg, key) && arg[key]) {//如果该属性是自身的 && value 为 true(或者说 可以转变为 true)
  23. classes.push(key);//就将 key 放入 classes 中,注意是key
  24. }
  25. }
  26. } else {//否则就用自身自定义的 toString 方法
  27. classes.push(arg.toString());
  28. }
  29. }
  30. }
  31. return classes.join(' ');//用 join 方法将数组变为字符串,用' '隔开
  32. }
  33. //用于支持各种导出方式
  34. if (typeof module !== 'undefined' && module.exports) {// CommonJS
  35. classNames.default = classNames;
  36. module.exports = classNames;
  37. } else if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {// AMD, 通过判断是否又 define 方法以及 define.amd 是否为 object
  38. // register as 'classnames', consistent with npm package name
  39. define('classnames', [], function () {
  40. return classNames;
  41. });
  42. } else {
  43. //浏览器环境
  44. window.classNames = classNames;
  45. }
  46. }());

3.3 除了 index 的其他版本

bind 版本

前面的 index 只是单纯的拼接,而 bind 版本 还可以通过 bind 指定读取属性的对象,传入 classNames 的参数先作为 key 到绑定的对象中寻找 value,如果有,就放value 进去,如果没有才放入 key
使用起来是这样的:

  1. var classNames = require('classnames/bind');
  2. var styles = {
  3. foo: 'abc',
  4. bar: 'def',
  5. baz: 'xyz'
  6. };
  7. var cx = classNames.bind(styles);
  8. var className = cx('foo', ['bar'], { baz: true }); // => "abc def xyz"

主要代码差别在这里

  1. classes.push(this && this[arg] || arg);//绑定了this,并且key(arg)对应的value有值=> this[arg] || 没有这个东西=>arg 本身

dedupe 版本

dedupe 版本不是单纯的拼接,而是有去重操作。而具体去重操作是通过对象Object 键值对来实现的,所以也就有后来的能覆盖前面属性

  1. var classNames = require('classnames/dedupe');
  2. classNames('foo', 'foo', 'bar'); // => 'foo bar'
  3. classNames('foo', { foo: false, bar: true }); // => 'bar'

用 StorageObject 来存储进行去重

  1. function StorageObject() {}
  2. StorageObject.prototype = Object.create(null);
  3. //使用时就是这样
  4. var classSet = new StorageObject();

使用 create(null) 可以让后面的判断省去一个 hasOwnProperty
主要区别就是 _parse方法,其中又调用了一些其他解析方法,其实也就只是不同的数据类型对应不同的操作

  1. function _parse (resultSet, arg) {
  2. if (!arg) return;
  3. var argType = typeof arg;
  4. // 'foo bar'
  5. if (argType === 'string') {
  6. _parseString(resultSet, arg);
  7. // ['foo', 'bar', ...]
  8. } else if (Array.isArray(arg)) {
  9. _parseArray(resultSet, arg);
  10. // { 'foo': true, ... }
  11. } else if (argType === 'object') {
  12. _parseObject(resultSet, arg);
  13. // '130'
  14. } else if (argType === 'number') {
  15. _parseNumber(resultSet, arg);
  16. }
  17. }

主函数 classNames 中 解析的入口就是对数组类型的操作—— arguments 就是一种类数组

  1. var len = arguments.length;
  2. var args = Array(len);
  3. for (var i = 0; i < len; i++) {
  4. args[i] = arguments[i];
  5. }
  6. var classSet = new StorageObject();
  7. _parseArray(classSet, args);

最后根据对象键值对的value 是否为 true,来决定是否放入list——即结果数组,再用 join 进行处理返回最终的字符串

  1. for (var k in classSet) {
  2. if (classSet[k]) {
  3. list.push(k)
  4. }
  5. }
  6. return list.join(' ');

3.4 benchmark

基准测试

你可以会有这样的需求:想比较两个效果相同的方法谁的性能较优。但是JS代码在不同运行环境下运行的效率可能是不一样的。这就是为什么我们需要基准测试。


我们从依赖包中可以看见 benchmark 这个包,这是用于做性能基准测试的,这里是他的官方仓库
这里就是 classNames 中进行测试的地方,具体怎么写测试样例建议看下面学习资源中的官方文档。

4. 学习资源

  • benchmark
  • benchmark-github

    5. 总结 & 收获

  • arguments ,在很多方法中都很有用到,很实用

  • 用 apply 第二个参数接收数组形式来实现数组扁平化的效果
  • 利用对象键值对来去重
  • 各个环境的导出处理,这个我还是第一次了解
  • 总体来说,是没想到经常使用的一个库逻辑居然如此简单——读取参数内容,根据类型处理,放入数组最后再进行拼接
  • 当然,尽管自己看源代码都能够知道这行代码是做什么的,但是如果说要自己实现一个.. 😴