本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
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 typesclassNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'// other falsy values are just ignoredclassNames(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), seehttp://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) { //递归调用返回的不是空字符串 '' 的话就加入 classesclasses.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) {// CommonJSclassNames.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 namedefine('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 第二个参数接收数组形式来实现数组扁平化的效果
- 利用对象键值对来去重
- 各个环境的导出处理,这个我还是第一次了解
- 总体来说,是没想到经常使用的一个库逻辑居然如此简单——读取参数内容,根据类型处理,放入数组最后再进行拼接
- 当然,尽管自己看源代码都能够知道这行代码是做什么的,但是如果说要自己实现一个.. 😴
