Skip to content

挑战看200 个npm模块源码:第1个 目录:

一、介绍

二、手写padLeft

三、源码分析

1、判断基偶数

2、num >>= 1

四、性能比较

字数:大约1500字

最近看到阿里前端技术专家狼叔在 17 年的这篇《迷茫时学习 Node.js 最好的方法》提到:今天小弟过来找我,说迷茫,我告诉他一个密法:一天看 10 个 npm 模块,坚持一年就是 3000+,按正常工作需要,超过 200 个都很厉害了。

所以我也来挑战啦!

一、介绍

今天看的是https://github.com/jonschlinkert/pad-left.git。代码写得是很好的,短短的几十行代码,从中也学到不少好东西。

先了解一下这个npm包

js
var pad = require('pad-left');
pad(  '4', 4, '0') // 0004
pad( '35', 4, '0') // 0035
pad('459', 4, '0') // 0459

看到这里你肯定想说:“艹,这么简单?这代码还需要分析?坑人的吧!”

不要着急,慢慢看。我们先来想一下,如果我们不看他的源码,我们自己会怎么实现这个函数呢?首先,我们会去获取需要填充在左侧的pad的length是多少,然后去循环,将规定的填充字符一个一个的填充在左侧。按照这个思路,我们实现的代码是这样的:

二、手写padLeft

js
const padLeftFn = (str, len, ch) => {
  //将str转成string
  str = str + '';
  // 获取左侧需要填充部分长度
  len = len - str.length;
  //如果len为负数则直接返回str
  if(len <= 0) return
  // 将ch参数转为字符,否则如果是0,则会出现0+0
  ch = ch + '';
  //如果我们没有给出ch参数,ch默认为一个空格
  if(!ch){
    ch = ' ';
  }
  while(len--){
    str = ch + str;
  }
  return str;
}
padLeftFn(0,4,0) // 0000

这样,我们的代码就写出来了,但是为什么我会说我自己写的这个代码和人家写的就差距很大呢,我们看分析一下性能问题就知道了。

我写的这个代码这里的循环时间复杂度是O(n),一次简单的while循环。

三、源码分析

那我们就要看看人家写的源码。

js
function padLeft(str, num, ch) {
  str = str.toString();

  if (typeof num === "undefined") {
    return str;
  }

  if (ch === 0) {
    ch = "0";
  } else if (ch) {
    ch = ch.toString();
  } else {
    ch = " ";
  }

  return repeat(ch, num - str.length) + str;
}
js
function repeat(str, num) {
  if (typeof str !== "string") {
    throw new TypeError("expected a string");
  }

  // cover common, quick use cases
  if (num === 1) return str;
  if (num === 2) return str + str;

  var max = str.length * num;
  if (cache !== str || typeof cache === "undefined") {
    cache = str;
    res = "";
  } else if (res.length >= max) {
    return res.substr(0, max);
  }

  while (max > res.length && num > 1) {
    if (num & 1) {
      res += str;
    }

    num >>= 1;
    str += str;
  }

  res += str;
  res = res.substr(0, max);
  return res;
}

主要是看repeat中的循环部分。

首先,leftPad的作者并没有从一开始就循环添加pad。而是将num === 1 和 num === 2的直接相加返回了。这里考虑到用户在使用时取1、2可能的num比较多,那直接相加返回了,时间复杂度这里就是O(1)。

当数据比较多的时候,源码的实现思路是这样的,每次循环,num除以2,num变成了原来的一半,那么循环次数相应减少到原来的一半,那么str相应增加到原来的两倍的长度str=str+str,这样每次循环都将num减少到原来的一半。这样就将原本O(n)的复杂度减少到了O(log(n))。我们来分析源码。

js
while (max > res.length && num > 1) {
  if (num & 1) {
    res += str;
  }

  num >>= 1;
  str += str;
}

res += str;
res = res.substr(0, max);
return res;

1、判断基偶数

首先if (num & 1)是什么意思?,其实就是判断是否是奇数,技术厉害一点这些他们都知道了。我们每一步都给大家讲清楚。看看它是怎么判断基数偶数的。

假如num这个时候为7,7转为二进制后是00000111,这个和00000001按位与。

js
0   0   0   0   0   1   1   1
0   0   0   0   0   0   0   1

按位&(相同则为1,不相同,则为0)
0   0   0   0   0   0   0   1

得到的二进制再转为10进制就是1。

再来个偶数8

js
0   0   0   0   1   0   0   0
0   0   0   0   0   0   0   1

按位&(相同则为1,不相同,则为0)
0   0   0   0   0   0   0   0

这里判断num如果是奇数,就先给pad一个str的字符,为什么这里要判断num是否是奇数呢?我们先往下看。

2、num >>= 1

num >>= 1;这又是什么意思,它的意思是,num的二进制向右移动一位,并且把移动一位之后的值重新赋值给num,也就是num = num二进制向右移动一位。

要知道右移一位数并赋值是用来做什么的,最简单的方法就是把移位后的得到的数字转为10进制看一下,以len=10为例。

js
               2^3     2^1
0   0   0   0   1   0   1   0   
// 10
------->移动一位
                   2^2     2^0
0   0   0   0   0   1   0   1   0
// 5

可以看出右移一位之后,其实就是相对原来的数除以了2。10向右移一位之后得到的是5。但是需要注意,与一般js里的/符号不同的是,移位的这个方法,会直接保留整数部分,截取掉小数部分。回过来看上一句,当len为奇数的时候,后面除以2就会把小数截取掉,所以先添加一个pad。num除以2之后,str就要变成原来的两倍长度的字符串。直到num最后小于2的时候跳出循环。最后返回pad+str。

四、性能比较

看看性能比较:

这个性能区别就很大了。100多毫秒:0.几毫秒。