Banana-FE / github-weekly

Learn something from Github ,Go !
MIT License
3 stars 0 forks source link

2020-Oct-Week3 #2

Open webfansplz opened 3 years ago

webfansplz commented 3 years ago

Share your knowledge and repository sources from Github . ♥️

2020/10/19 - 2020/10/23 ~

BlingSu commented 3 years ago

Repository (仓库地址):https://github.com/vuejs/vue-next/blob/master/packages/shared/src/patchFlags.ts Gain (收获) : 运算符 + 我也不知道有啥....

Vue3核心的ts、proxy、composition之类的,在vue3虚拟dom中,比如 静态标记update性能可以说是提升了1.3~2倍,ssr提升了2~3倍。

普通编译模板的静态标记

<div id="app">
  <h1>test</h1>
  <p>哈哈哈</p>
  <div>{{name}}</div>
</div>

在vue2中会解析成

function render() {
  with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, [_c('h1', [_v("test")]), _c('p', [_v("哈哈哈")]), _c('div', [_v(
      _s(name))])])
  }
}

前面两个标签是静态的, 后续的渲染就不会产生别的变化,vue2里面依旧使用_c新建vdom,在diff的时候对比,这样会有一些额外的性能损耗。

vue3中年是如何解析的

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", { id: "app" }, [
    _createVNode("h1", null, "test"),
    _createVNode("p", null, "哈哈哈"),
    _createVNode("div", null, _toDisplayString(_ctx.name), 1 /* TEXT */)
  ]))
}
// Check the console for the AST

这里最后一行也就是最后一个_createVNode的第四个参数是一个1,只有当这个参数有带上的时候才会去追踪,静态节点不需要去遍历操作之类的,这样性能就可以大大提升,这就是vue3性能比vue2好的原因之一

复杂点来看

<div id="app">
  <h1>test</h1>
  <p>哈哈哈</p>

  <div>{{name}}</div>
  <div :class="{red:isRed}">red</div>
  <button @click="handleClick">btn</button>
</div>

会解析成这样

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", { id: "app" }, [
    _createVNode("h1", null, "test"),
    _createVNode("p", null, "哈哈哈"),
    _createVNode("div", null, _toDisplayString(_ctx.name), 1 /* TEXT */),
    _createVNode("div", {
      class: {red:_ctx.isRed}
    }, "red", 2 /* CLASS */),
    _createVNode("button", { onClick: _ctx.handleClick }, "btn", 8 /* PROPS */, ["onClick"])
  ]))
}

根据_createVNode中的第四个参数的值其实可以很容易的才出来他明显就是一个标示,可以理解为一个flag,根据他是text,props等等之类的标示来判定他再diff的时候要对比哪些,不用再去做没有用的props遍历,贼牛逼!

export const enum PatchFlags {
  TEXT = 1,// 表示具有动态textContent的元素
  CLASS = 1 << 1,  // 表示有动态Class的元素
  STYLE = 1 << 2,  // 表示动态样式(静态如style="color: red",也会提升至动态)
  PROPS = 1 << 3,  // 表示具有非类/样式动态道具的元素。
  FULL_PROPS = 1 << 4,  // 表示带有动态键的道具的元素,与上面三种相斥
  HYDRATE_EVENTS = 1 << 5,  // 表示带有事件监听器的元素
  STABLE_FRAGMENT = 1 << 6,   // 表示其子顺序不变的片段(没懂)。 
  KEYED_FRAGMENT = 1 << 7, // 表示带有键控或部分键控子元素的片段。
  UNKEYED_FRAGMENT = 1 << 8, // 表示带有无key绑定的片段
  NEED_PATCH = 1 << 9,   // 表示只需要非属性补丁的元素,例如ref或hooks
  DYNAMIC_SLOTS = 1 << 10,  // 表示具有动态插槽的元素
}

以上是vue3的pathFlags,说白了就是通过枚举的方式来定义,嗯他是什么标示。就是通过运算符来flag,仅此而已。

那么问题来了,如果同时有props和text的绑定的,那么咋办呢?

<div id="app">
  <h1>test</h1>
  <p>哈哈哈</p>
  <div :id="userId">{{name}}</div>
</div>

很简单,把位运算组合即可

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", { id: "app" }, [
    _createVNode("h1", null, "test"),
    _createVNode("p", null, "哈哈哈"),
    _createVNode("div", {
      id: _ctx.userid,
      "\"": ""
    }, _toDisplayString(_ctx.name), 9 /* TEXT, PROPS */, ["id"])
  ]))
}

text是1,props是8,组合起来就是9?咋算呢????通过位运算来判定需要做text和props的判断,按位与不久好了,通俗说,不是0就需要比较!打个比方

类型      二进制
text      000000001
props     000001000
merge     000001001

上面就是按位与 1和8的二进制位就是1

测试一下(放到控制台)

let TEXT = 1,
    PROPS = 8,
    CLASS = 2,
    VAL = 9

!!(TEXT & VAL)  // true
!!(PROPS & VAL)  // true
!!(CLASS & VAL) // false

如果你不会运算符....那么 百度一下哈哈哈

综上所述,其实这种方式呢还可以运用在很多种地方,比如权限控制???把最高级设置成1000然后二级0100三级0010...这样的方式来做-。-这样不就是简单明了吗哈哈哈哈哈哈。

AzTea commented 3 years ago

Repository (仓库地址):https://github.com/ustbhuangyi/better-scroll/tree/dev/packages Gain (收获) : 插件化架构简单了解

BS插件化架构包管理:

  1. Core:https://github.com/ustbhuangyi/better-scroll/tree/dev/packages/core

Core 通常提供系统运行所需的最小功能集,不会因为业务功能扩展而不断修改,而插件模块是可以根据实际业务功能的需要不断地调整或扩展。BS的core实现了基础的列表滚动效果:

import { BScroll } from './BScroll'

export { BScrollInstance } from './Instance'
export { Options, CustomOptions } from './Options'
export { TranslaterPoint } from './translater'
export { MountedBScrollHTMLElement } from './BScroll'
export { Behavior, Boundary } from './scroller/Behavior'
export { createBScroll, CustomAPI } from './BScroll'

export default BScroll

使用

import BScroll from '@better-scroll/core'

const bs = new BScroll('.wrapper', {/* ... */})
  1. plugin:https://github.com/ustbhuangyi/better-scroll/tree/dev/packages

插件模块是独立的模块,包含特定的处理、额外的功能和自定义代码,来向核心系统增强或扩展额外的业务能力。「通常插件模块之间也是独立的,也有一些插件是依赖于若干其它插件的。重要的是,尽量减少插件之间的通信以避免依赖的问题。」 BS的插件模块包括了mouse-wheel、observe-dom、pull-down、pull-up、scroll-bar等等

  1. shared-utils:https://github.com/ustbhuangyi/better-scroll/tree/dev/packages/shared-utils

插件运行时需要的基础接口

浏览器就是一个典型的插件化架构,浏览器是内核,页面是插件,这样通过不同的URL地址加载不同的页面,来提供非常丰富的功能。而且,我们开发网页时候,浏览器会提供很多API和能力,这些接口通过 window来挂载, 比如,DOM、BOM、Event、Location等等(百度到的)

另外jQuery、vue-cli、bable、webpack等都是使用插件系统。

rxyshww commented 3 years ago

Repository (仓库地址): es5-shim js猴子补丁,旨在低版本浏览器同样可以使用一些好用的js原生方法。 Gain (收获) : 手动实现原生函数bind 虽然我们很少用低版本浏览器了,这些polyfill显得有些多余,但是里面的一些思想,很值得我们学习。

先说说bind是什么: 1)bind是用于绑定this指向的 2)使用方法:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

我们先来自己实现一下bind:

Function.prototype.bind = function (context) {
    var me = this;
    var argsArray = Array.prototype.slice.call(arguments);
    return function () {
        return me.apply(context, argsArray.slice(1))
    }
}

基本原理就是缓存原函数,并返回一个新的函数,然后新的函数运行时,通过apply来手动指定函数运行上下文。 arguments是一个类数组,通过Array.prototype.slice.call将其转为真正的数组。

颗粒化(curring)实现

bind接收多个参数,从第二个参数开始的参数,将会作为函数执行的预置参数:

function list() {
  return Array.prototype.slice.call(arguments);
}
// 创建一个函数,它拥有预设参数列表。
var leadingThirtysevenList = list.bind(null, 37);

var list2 = leadingThirtysevenList(); 
// [37]
var list3 = leadingThirtysevenList(1, 2, 3);
// [37, 1, 2, 3]

上面的代码就不太满足需求了,修改成为以下代码:

Function.prototype.bind = Function.prototype.bind || function (context) {
    var me = this;
    var args = Array.prototype.slice.call(arguments, 1);
    return function () {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        return me.apply(contenxt, finalArgs);
    }
}

构造函数场景下的兼容

bind返回的函数如果作为构造函数,搭配new关键字出现的话,我们的绑定this就需要“被忽略”

Function.prototype.bind = Function.prototype.bind || function (context) {
    var me = this;
    var args = Array.prototype.slice.call(arguments, 1);
    var F = function () {};
    F.prototype = this.prototype;
    var bound = function () {
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.contact(innerArgs);
        return me.apply(this instanceof F ? this : context || this, finalArgs);
    }
    bound.prototype = new F;
    return bound;
}

源码是怎样的

var Empty = function Empty() {};
// ...
bind: function bind(that) {
    var target = this;
    if (!isCallable(target)) {
        throw new TypeError('Function.prototype.bind called on incompatible ' + target);
    }
    var args = array_slice.call(arguments, 1);
    var bound;
    var binder = function () {
        if (this instanceof bound) {
            var result = target.apply(
                this,
                array_concat.call(args, array_slice.call(arguments))
            );
            if ($Object(result) === result) {
                return result;
            }
            return this;
        } else {
            return target.apply(
                that,
                array_concat.call(args, array_slice.call(arguments))
            );
        }
    };
    var boundLength = max(0, target.length - args.length);
    var boundArgs = [];
    for (var i = 0; i < boundLength; i++) {
        array_push.call(boundArgs, '$' + i);
    }
    bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }')(binder);

    if (target.prototype) {
        Empty.prototype = target.prototype;
        bound.prototype = new Empty();
        Empty.prototype = null;
    }
    return bound;
}

其实我们的实现已经基本和它差不多了,我们来看看区别:

    bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }')(binder);

bound使用了系统自己的构造函数Function来声明,第一个参数是binder,函数体内又binder.apply(this, arguments) 我们知道这种动态创建函数的方式,类似eval。最好不要使用它,因为用它定义函数比用传统方式要慢得多。 那么ES5-shim为什么使用它,是抽风了吗。

你可能不知道,每个函数都有length属性。对,就像数组和字符串那样。函数的length属性,用于表示函数的形参个数。更重要的是函数的length属性值是不可重写的。

function test (){}
test.length  // 输出0
test.hasOwnProperty('length')  // 输出true
Object.getOwnPropertyDescriptor('test', 'length') 
// 输出:
// configurable: false, 
// enumerable: false,
// value: 4, 
// writable: false 

现在我们明白了: ES5-shim是为了最大限度的进行兼容,包括对返回函数length属性的还原。如果按照我们之前实现的那种方式,length值始终为零。 所以:既然不能修改length的属性值,那么在初始化时赋值总可以吧! 于是我们可通过eval和new Function的方式动态定义函数来。 在源码里有一段很有趣的注释

            // XXX Build a dynamic function with desired amount of arguments is the only
            // way to set the length property of a function.
            // In environments where Content Security Policies enabled (Chrome extensions,
            // for ex.) all use of eval or Function costructor throws an exception.
            // However in all of these environments Function.prototype.bind exists
            // and so this code will never be executed.

他解释了为什么要使用动态函数,就如同我们上边所讲的那样,是为了保证length属性的合理值。但是在一些浏览器中出于安全考虑,使用eval或者Function构造器都会被抛出异常。但是,巧合也就是这些浏览器基本上都实现了bind函数,这些异常又不会被触发。

So, What a coincidence!

scorpioLh commented 3 years ago

Repository (仓库地址):https://github.com/ElemeFE/element/blob/dev/packages/progress/src/progress.vue Gain (收获) : SVG的基础知识和使用方式。。。

<template>
  <div
    class="el-progress"
    :class="[
      'el-progress--' + type,
      status ? 'is-' + status : '',
      {
        'el-progress--without-text': !showText,
        'el-progress--text-inside': textInside,
      }
    ]"
    role="progressbar"
    :aria-valuenow="percentage"
    aria-valuemin="0"
    aria-valuemax="100"
  >
    <!-- 直线型进度条 -->
    <div v-if="type === 'line'" class="el-progress-bar">
      <div class="el-progress-bar__outer" :style="{height: strokeWidth + 'px'}">
        <div class="el-progress-bar__inner" :style="barStyle">
          <div v-if="showText && textInside" class="el-progress-bar__innerText">{{ content }}</div>
        </div>
      </div>
    </div>
    <!-- 环形进度条 -->
    <div v-else class="el-progress-circle" :style="{height: width + 'px', width: width + 'px'}">
      <svg viewBox="0 0 100 100">
        <path
          class="el-progress-circle__track"
          :d="trackPath"
          stroke="#e5e9f2"
          :stroke-width="relativeStrokeWidth"
          fill="none"
          :style="trailPathStyle"
        />
        <path
          class="el-progress-circle__path"
          :d="trackPath"
          :stroke="stroke"
          fill="none"
          :stroke-linecap="strokeLinecap"
          :stroke-width="percentage ? relativeStrokeWidth : 0"
          :style="circlePathStyle"
        />
      </svg>
    </div>
    <div
      v-if="showText && !textInside"
      class="el-progress__text"
      :style="{fontSize: progressTextSize + 'px'}"
    >
      <template v-if="!status">{{ content }}</template>
      <i v-else :class="iconClass" />
    </div>
  </div>
</template>
<script>
export default {
  name: 'ElProgress',
  props: {
    /** 进度条类型 */
    type: {
      type: String,
      default: 'line',
      validator: val => ['line', 'circle', 'dashboard'].indexOf(val) > -1
    },

    /** 百分比 */
    percentage: {
      type: Number,
      default: 0,
      required: true,
      validator: val => val >= 0 && val <= 100
    },

    /** 进度条当前状态 */
    status: {
      type: String,
      validator: val => ['success', 'exception', 'warning'].indexOf(val) > -1
    },

    /** 进度条的宽度 */
    strokeWidth: {
      type: Number,
      default: 6
    },

    /** circle/dashboard 类型路径两端的形状 */
    strokeLinecap: {
      type: String,
      default: 'round'
    },

    /** 进度条显示文字内置在进度条内(只在 type=line 时可用) */
    textInside: {
      type: Boolean,
      default: false
    },

    /** 环形进度条画布宽度(只在 type 为 circle 或 dashboard 时可用) */
    width: {
      type: Number,
      default: 126
    },

    /** 是否显示进度条文字内容 */
    showText: {
      type: Boolean,
      default: true
    },

    /** 进度条背景色(会覆盖 status 状态颜色) */
    color: {
      type: [String, Array, Function],
      default: ''
    },

    format: Function
  },
  computed: {
    /** 
     * 直线进度条的样式
     * 根据百分比给出已完成进度部分的宽度和颜色
     */
    barStyle() {
      const style = {}
      style.width = this.percentage + '%'
      style.backgroundColor = this.getCurrentColor(this.percentage)
      return style
    },

    /** 圆环线的宽 */
    relativeStrokeWidth() {
      return (this.strokeWidth / this.width * 100).toFixed(1)
    },

    /** 半径 */
    radius() {
      if (this.type === 'circle' || this.type === 'dashboard') {
        return parseInt(50 - parseFloat(this.relativeStrokeWidth) / 2, 10)
      } else {
        return 0
      }
    },

    /** 
     * 绘制环形进度条图形
     * M = moveto(M X,Y) :将画笔移动到指定的坐标位置
     * L = lineto(L X,Y) :画直线到指定的坐标位置
     * H = horizontal lineto(H X):画水平线到指定的X坐标位置
     * V = vertical lineto(V Y):画垂直线到指定的Y坐标位置
     * C = curveto(C X1,Y1,X2,Y2,ENDX,ENDY):三次贝赛曲线
     * S = smooth curveto(S X2,Y2,ENDX,ENDY)
     * Q = quadratic Belzier curve(Q X,Y,ENDX,ENDY):二次贝赛曲线
     * T = smooth quadratic Belzier curveto(T ENDX,ENDY):映射
     * A = elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):弧线
     * Z = closepath():关闭路径
     * 
     * m 同M,但使用的是相对坐标,参数x, y
     * 
     * a 的命令参数:a rx ry x-axis-rotation large-arc-flag sweep-flag dx dy
     * 弧形命令A的前两个参数分别是x轴半径和y轴半径,弧形命令A的第三个参数表示弧形的旋转情况,
     * large-arc-flag(角度大小) 和sweep-flag(弧线方向),large-arc-flag决定弧线是大于还是小于180度,0表示小角度弧,1表示大角度弧。
     * sweep-flag表示弧线的方向,0表示从起点到终点沿逆时针画弧,1表示从起点到终点沿顺时针画弧。
     */
    trackPath() {
      const radius = this.radius
      const isDashboard = this.type === 'dashboard'
      return `
          M 50 50
          m 0 ${isDashboard ? '' : '-'}${radius}
          a ${radius} ${radius} 0 1 1 0 ${isDashboard ? '-' : ''}${radius * 2}
          a ${radius} ${radius} 0 1 1 0 ${isDashboard ? '' : '-'}${radius * 2}
          `
    },

    /** 圆的周长 */
    perimeter() {
      return 2 * Math.PI * this.radius
    },

    /** 仪表板比圆环少0.25的长度 */
    rate() {
      return this.type === 'dashboard' ? 0.75 : 1
    },

    /** 
     * 偏移量计算
     * 由于仪表盘和环形的0.25缺口,所以偏移量不同
     */
    strokeDashoffset() {
      const offset = -1 * this.perimeter * (1 - this.rate) / 2
      return `${offset}px`
    },

    /**
     * 绘制图形
     * strokeDasharray: 属性控制用于描边路径的破折号和间隙的模式。
     * strokeDashoffset: 属性指定进入短划线模式的距离以开始短划线。
     */
    trailPathStyle() {
      return {
        strokeDasharray: `${(this.perimeter * this.rate)}px, ${this.perimeter}px`,
        strokeDashoffset: this.strokeDashoffset
      }
    },

    circlePathStyle() {
      return {
        strokeDasharray: `${this.perimeter * this.rate * (this.percentage / 100)}px, ${this.perimeter}px`,
        strokeDashoffset: this.strokeDashoffset,
        transition: 'stroke-dasharray 0.6s ease 0s, stroke 0.6s ease'
      }
    },

    /** 
     * 进度条颜色
     * 根据用户传递的颜色来提供进度条颜色
     * 或者根据当前状态来提供颜色
     */
    stroke() {
      let ret
      if (this.color) {
        ret = this.getCurrentColor(this.percentage)
      } else {
        switch (this.status) {
          case 'success':
            ret = '#13ce66'
            break
          case 'exception':
            ret = '#ff4949'
            break
          case 'warning':
            ret = '#e6a23c'
            break
          default:
            ret = '#20a0ff'
        }
      }
      return ret
    },

    /** 根据状态给圆环进度条中心位置更换icon */
    iconClass() {
      if (this.status === 'warning') {
        return 'el-icon-warning'
      }
      if (this.type === 'line') {
        return this.status === 'success' ? 'el-icon-circle-check' : 'el-icon-circle-close'
      } else {
        return this.status === 'success' ? 'el-icon-check' : 'el-icon-close'
      }
    },

    /** 根据进度条类型调整文字说明的宽度 */
    progressTextSize() {
      return this.type === 'line'
        ? 12 + this.strokeWidth * 0.4
        : this.width * 0.111111 + 2
    },

    /** 进度条文字 */
    content() {
      if (typeof this.format === 'function') {
        return this.format(this.percentage) || ''
      } else {
        return `${this.percentage}%`
      }
    }
  },
  methods: {
    /** 根据百分比获取颜色 */
    getCurrentColor(percentage) {
      if (typeof this.color === 'function') {
        return this.color(percentage)
      } else if (typeof this.color === 'string') {
        return this.color
      } else {
        return this.getLevelColor(percentage)
      }
    },

    /** 给颜色数组排序 */
    getLevelColor(percentage) {
      const colorArray = this.getColorArray().sort((a, b) => a.percentage - b.percentage)
      for (let i = 0; i < colorArray.length; i++) {
        if (colorArray[i].percentage > percentage) {
          return colorArray[i].color
        }
      }
      return colorArray[colorArray.length - 1].color
    },

    /** 
     * 获取颜色数组
     * 让进度条可以根据不同的百分比显示不同的颜色
     */
    getColorArray() {
      const color = this.color
      const span = 100 / color.length
      return color.map((seriesColor, index) => {
        if (typeof seriesColor === 'string') {
          return {
            color: seriesColor,
            progress: (index + 1) * span
          }
        }
        return seriesColor
      })
    }
  }
}
</script>
diandiantong commented 3 years ago

qs 一个字符串序列化的库 收获: 如何通过预置方法初始化数据来提高传入需要处理数据的容错率、代码功能拆分组合理解

入口 lib/index.js stringify: 方法接受传入两个参数,第一个为需要格式的数据,第二个为输出的格式(第二参数接受自定义函数所以需要预先判定是否为 function )

if (typeof options.filter === 'function') {
    filter = options.filter;
    obj = filter('', obj);
} else if (isArray(options.filter)) {
    filter = options.filter;
    // 如果第二参数为数组,则根据传入的格式来进行 key 获取
    objKeys = filter;
}

如果传入的数据格式是对象,则获取他的key 放入 objKeys

if (!objKeys) {
    objKeys = Object.keys(obj);
}

如果是函数的话会挂载方法过去(normalizeStringifyOptions 内做了方法预处理)

// 使用参数预置初始化值
var defaults = {
    addQueryPrefix: false,
    allowDots: false,
    charset: 'utf-8',
    charsetSentinel: false,
    delimiter: '&',
    encode: true,
    encoder: utils.encode,
    encodeValuesOnly: false,
    format: defaultFormat,
    formatter: formats.formatters[defaultFormat],
    // deprecated
    indices: false,
    serializeDate: function serializeDate(date) {
        return toISO.call(date);
    },
    skipNulls: false,
    strictNullHandling: false
};

var filter = defaults.filter;
if (typeof opts.filter === 'function' || isArray(opts.filter)) {
    filter = opts.filter;
}
// 验证值是否存在 (undefined 值会被删掉就是因为这个, undefined 不被 boolean 识别)
skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls,

识别处理格式并赋值备用

var arrayFormat;
if (opts && opts.arrayFormat in arrayPrefixGenerators) {
    arrayFormat = opts.arrayFormat;
} else if (opts && 'indices' in opts) {
    arrayFormat = opts.indices ? 'indices' : 'repeat';
} else {
    arrayFormat = 'indices';
}

var generateArrayPrefix = arrayPrefixGenerators[arrayFormat];

根据获取到的 key 来进行具体数据获取拼接的数组

for (var i = 0; i < objKeys.length; ++i) {
    var key = objKeys[i];
    if (options.skipNulls && obj[key] === null) {
        continue;
    }
    pushToArray(keys, stringify(
        obj[key],
        key,
        generateArrayPrefix,
        options.strictNullHandling,
        options.skipNulls,
        options.encode ? options.encoder : null,
        options.filter,
        options.sort,
        options.allowDots,
        options.serializeDate,
        options.formatter,
        options.encodeValuesOnly,
        options.charset
    ));
}

// 把数据拼接成数组

var pushToArray = function (arr, valueOrArray) {
    push.apply(arr, isArray(valueOrArray) ? valueOrArray : [valueOrArray]);
};

组装数据

var joined = keys.join(options.delimiter);
var prefix = options.addQueryPrefix === true ? '?' : '';

if (options.charsetSentinel) {
    if (options.charset === 'iso-8859-1') {
        // encodeURIComponent('&#10003;'), the "numeric entity" representation of a checkmark
        prefix += 'utf8=%26%2310003%3B&';
    } else {
        // encodeURIComponent('✓')
        prefix += 'utf8=%E2%9C%93&';
    }
}

return joined.length > 0 ? prefix + joined : '';
webfansplz commented 3 years ago

仓库:

ms -Tiny milisecond conversion utility

源码实现:

/**
 * Helpers.
 */

var s = 1000;
var m = s * 60;
var h = m * 60;
var d = h * 24;
var w = d * 7;
var y = d * 365.25; // why ? 见以下解析第1点

/**
 * Parse or format the given `val`.
 *
 * Options:
 *
 *  - `long` verbose formatting [false]
 *
 * @param {String|Number} val
 * @param {Object} [options]
 * @throws {Error} throw an error if val is not a non-empty string or a number
 * @return {String|Number}
 * @api public
 */

module.exports = function (val, options) {
  options = options || {};
  var type = typeof val;
  // 字符串类型且不等于'',走parse方法
  if (type === "string" && val.length > 0) {
    return parse(val);
  }
  // number类型 且 为一个有限数值
  else if (type === "number" && isFinite(val)) {
    // long选项? fmtLong: fmtShort
    return options.long ? fmtLong(val) : fmtShort(val);
  }
  // 类型错误报错
  throw new Error(
    "val is not a non-empty string or a valid number. val=" +
      JSON.stringify(val)
  );
};

/**
 * Parse the given `str` and return milliseconds.
 *
 * @param {String} str
 * @return {Number}
 * @api private
 */

// 字符串解析成数字
function parse(str) {
  str = String(str);
  // why? why 100? 见以下解析2,3点
  if (str.length > 100) {
    return;
  }
  // 基操正则..
  var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
    str
  );
  if (!match) {
    return;
  }
  var n = parseFloat(match[1]);
  var type = (match[2] || "ms").toLowerCase();
  switch (type) {
    case "years":
    case "year":
    case "yrs":
    case "yr":
    case "y":
      return n * y;
    case "weeks":
    case "week":
    case "w":
      return n * w;
    case "days":
    case "day":
    case "d":
      return n * d;
    case "hours":
    case "hour":
    case "hrs":
    case "hr":
    case "h":
      return n * h;
    case "minutes":
    case "minute":
    case "mins":
    case "min":
    case "m":
      return n * m;
    case "seconds":
    case "second":
    case "secs":
    case "sec":
    case "s":
      return n * s;
    case "milliseconds":
    case "millisecond":
    case "msecs":
    case "msec":
    case "ms":
      return n;
    default:
      return undefined;
  }
}

/**
 * Short format for `ms`.
 *
 * @param {Number} ms
 * @return {String}
 * @api private
 */
// 直接相除四舍五入取整
function fmtShort(ms) {
  var msAbs = Math.abs(ms);
  if (msAbs >= d) {
    return Math.round(ms / d) + "d";
  }
  if (msAbs >= h) {
    return Math.round(ms / h) + "h";
  }
  if (msAbs >= m) {
    return Math.round(ms / m) + "m";
  }
  if (msAbs >= s) {
    return Math.round(ms / s) + "s";
  }
  return ms + "ms";
}

/**
 * Long format for `ms`.
 *
 * @param {Number} ms
 * @return {String}
 * @api private
 */
// long选项其实就是单位描述更加详细完整
function fmtLong(ms) {
  var msAbs = Math.abs(ms);
  if (msAbs >= d) {
    return plural(ms, msAbs, d, "day");
  }
  if (msAbs >= h) {
    return plural(ms, msAbs, h, "hour");
  }
  if (msAbs >= m) {
    return plural(ms, msAbs, m, "minute");
  }
  if (msAbs >= s) {
    return plural(ms, msAbs, s, "second");
  }
  return ms + " ms";
}

/**
 * Pluralization helper.
 */

// 复数判断
function plural(ms, msAbs, n, name) {
  // 复数判断,因为Math.round是四舍五入取整,所以此处判断使用n*1.5
  var isPlural = msAbs >= n * 1.5;
  // ms(89999, { long: true }) 1 minute
  // ms(90000, { long: true }) 2 minutes
  return Math.round(ms / n) + " " + name + (isPlural ? "s" : "");
}

解析:

  1. Why are there 365.25 days in a year?

  2. Limit str to 100 to avoid ReDoS of 0.3s

  3. By limiting the input length It prevents the regular expression that does the parsing from consuming too much cpu time blocking the event loop

收获:

其实 ms 的源码非常精简,也非常易读,但是我却从它的 History Commits 榨出了一些知识点:

第一版的 ms 代码其实是更简单的,当然了也存在一些问题。后面随着迭代和一些大佬(我在这里也看到了 tj 大佬,tj 大佬真是无处不在啊...)的 pr 贡献,发生了以下变化:

看源码,真的也能从它的 History Commits 学到很多~

100 多行,2kb 多的库,却有 3.2k 的 star,足已见得它的实用与易用。