我编写了 14 个函数来转储 lodash 并减少捆绑包大小......

2025-05-25

我编写了 14 个函数来转储 lodash 并减少捆绑包大小......

Lodash 和 underscore 永远改变了我编写 Javascript 的方式,但今天对于最常见的功能可能有更好的选择。

我最近检查了我们的主要应用程序,希望减少捆绑包的大小,并很快发现尽管我们尽了最大努力进行特定的功能导入,但我们仍然导入了大部分 lodash。

我们转向了 lodash-es,这有点帮助,但我仍然在关注一些占用大约 30% 软件包空间的实用函数。

问题是,作为一个节点模块,库已经做出了许多关于填充旧功能的选择,因此根据你的目标浏览器,你可能会有很多你不需要的代码。

我确定了 lodash 中使用的 14 个核心函数,并用现代 JavaScript 重写了它们,以便打包过程能够根据目标环境决定需要提供哪些 polyfill。导入文件大小显著减少。

经过 tree-shaking 之后,在我的函数之前:
构建分析

我的代码:4.1kb(未压缩/未最小化,但在旧版浏览器上需要 polyfill)

核心功能

以下是我对该功能列表所做的事情:

匹配的功能

  • 筛选
  • forEach(数组和对象)
  • groupBy
  • 键值
  • 映射(数组和对象)
  • 合并
  • 忽略
  • 排序方式
  • 独特
  • uniqBy

实施“足够”

  • 挑选
  • 获取(不支持数组语法)
  • 设置(不支持数组语法)
  • 防抖动(使用 maxWait、flush、cancel)

功能

以下是这些功能、它们的作用以及我如何实现它们:

pick(函数(项目)=>值|属性名称)

我们将从它开始,pick因为它对于其他一切非常有用。pick将返回一个函数来从对象中提取属性 - 我的实现将把字符串转换为这个,但保留其他值。

你可以pick像这样使用自己:

const array = [{ name: "mike", a: 1 }, { name: "bob", a: 2 }]

console.log(array.map(pick('name')) //=> ["mike", "bob"]

Enter fullscreen mode Exit fullscreen mode

执行
import {get} from './get'
export function pick(fn) {
  return typeof fn === "string" ? (v) => get(v,fn) : fn
}
Enter fullscreen mode Exit fullscreen mode

过滤器(数组,函数(项目)=>布尔值 | 字符串)

我们filter经常使用名称属性,因此过滤器基本上只是选择和现有的过滤函数:

const array = [{ name: "mike", a: 1 }, { name: "bob", a: 2 }, { a: 4 }]

console.log(filter(array, 'name')) //=> [{ name: "mike", a: 1 }, { name: "bob", a: 2 }]

Enter fullscreen mode Exit fullscreen mode

执行
import { pick } from "./pick"

export function filter(target, fn) {
  return target.filter(pick(fn))
}
Enter fullscreen mode Exit fullscreen mode

forEach(数组|对象,函数(值,键))

在 lodash 中,forEach 可以使用对象或数组,因此我们需要一个可以实现这一点的实现。回调函数会获取参数valuekey。它的工作原理如下:

const data = { a: 1, b: 2, d: "hello" }
forEach(data, (value, key)=>console.log(`${key}=${value}`) 
      //=> a=1
      //=> b=2
      //=> d=hello
Enter fullscreen mode Exit fullscreen mode

执行
import { pick } from "./pick"

export function applyArrayFn(target, fnName, fn) {
  fn = pick(fn)
  if (Array.isArray(target)) return target[fnName](fn)
  if (target && typeof target === "object")
    return Object.entries(target)[fnName](([key, value], index) =>
      fn(value, key, target, index)
    )
  throw new Error(`Cannot iterate ${typeof target}`)
}

export function forEach(target, fn) {
  return applyArrayFn(target, "forEach", fn)
}
Enter fullscreen mode Exit fullscreen mode

获取(对象、属性路径、默认值)

get允许您从对象读取属性,如果未找到任何中介或最终值,它将返回默认值

const data = { a: { b: {d: 1 } } }
get(data, "a.b.d") //=> 1
get(data, "a.c.d", "hmmm") //=> hmmm
Enter fullscreen mode Exit fullscreen mode

执行
export function get(object, path, defaultValue) {
  const parts = path.split(".")
  for (let part of parts) {
    if(!object) return defaultValue
    object = object[part]
  }
  return object ?? defaultValue
}
Enter fullscreen mode Exit fullscreen mode

groupBy(数组,函数(项目)=>键|属性名称)

创建一个以函数结果(或选定的属性名称)为键的对象,其中每个值都是具有相同键的项目的数组。

const array = [{ name: "mike", type: "user" }, { name: "bob", type: "user" }, { name: "beth", type: "admin"} ]

console.log(groupBy(array, 'type'))
    /*=>
       {
          admin: [{name: "beth", type: "admin" }],
          user: [{name: "mike", type: "user" }, {name: "bob", type: "user"}]
       }
    */

Enter fullscreen mode Exit fullscreen mode

执行
import { pick } from "./pick"

export function groupBy(target, fn) {
  fn = pick(fn)
  return target
    .map((value) => ({ value, key: fn(value) }))
    .reduce((c, a) => {
      c[a.key] = c[a.key] || []
      c[a.key].push(a.value)
      return c
    }, {})
}
Enter fullscreen mode Exit fullscreen mode

keyBy(数组,函数(项)=>键|属性名称)

类似,groupBy但结果是与键匹配的最后一个项目 - 通常这会给出一些键是唯一的(如 id)来创建查找

const array = [{ id: "a7", name: "mike", type: "user" }, { id: "z1", name: "bob", type: "user" }, { id: "a3", name: "beth", type: "admin"} ]

console.log(keyBy(array, 'id'))
    /*=>
       {
          "a3": {name: "beth", type: "admin", id: "a3" },
          "a7": {name: "mike", type: "user", id: "a7" },
          "z1": {name: "bob", type: "user", id: "z1"}
       }
    */

Enter fullscreen mode Exit fullscreen mode

执行
import { pick } from "./pick"

export function keyBy(target, fn) {
  fn = pick(fn)
  return target
    .map((value) => ({ value, key: fn(value) }))
    .reduce((c, a) => {
      c[a.key] = a.value
      return c
    }, {})
}

Enter fullscreen mode Exit fullscreen mode

映射(数组|对象,函数(值,键)=>值|属性名称)

映射对象和数组(如forEach

const records = {
          "a3": {name: "beth", type: "admin" },
          "a7": {name: "mike", type: "user" },
          "z1": {name: "bob", type: "user"}
       }
console.log(map(records, 'name')) /=> ["beth", "mike", "bob"]
Enter fullscreen mode Exit fullscreen mode

执行
import { pick } from "./pick"

export function applyArrayFn(target, fnName, fn) {
  fn = pick(fn)
  if (Array.isArray(target)) return target[fnName](fn)
  if (target && typeof target === "object")
    return Object.entries(target)[fnName](([key, value], index) =>
      fn(value, key, target, index)
    )
  throw new Error(`Cannot iterate ${typeof target}`)
}

export function forEach(target, fn) {
  return applyArrayFn(target, "map", fn)
}
Enter fullscreen mode Exit fullscreen mode

合并(目标,...源)

工作原理类似Object.assign,但会深入底层结构来更新更深层的对象,而不是替换它们。

const record = { id: "2", name: "Beth", value: 3, ar: ["test", { a: 3, d: { e: 4 } }] }
console.log(merge(record, { ar: [{ b: 1 }, { c: 3, d: { f: 5 } }]))

   /*=>
    {
      id: "2",
      name: "Beth",
      value: 3,
      ar: [{ b: 1 }, { c: 3, d: { f: 5, e: 4 } }]
    }
   */
Enter fullscreen mode Exit fullscreen mode

执行
export function merge(target, ...sources) {
  for (let source of sources) {
    mergeValue(target, source)
  }

  return target

  function innerMerge(target, source) {
    for (let [key, value] of Object.entries(source)) {
      target[key] = mergeValue(target[key], value)
    }
  }

  function mergeValue(targetValue, value) {
    if (Array.isArray(value)) {
      if (!Array.isArray(targetValue)) {
        return [...value]
      } else {
        for (let i = 0, l = value.length; i < l; i++) {
          targetValue[i] = mergeValue(targetValue[i], value[i])
        }
        return targetValue
      }
    } else if (typeof value === "object") {
      if (targetValue && typeof targetValue === "object") {
        innerMerge(targetValue, value)
        return targetValue
      } else {
        return value ? { ...value } : value
      }
    } else {
      return value ?? targetValue ?? undefined
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

省略(对象,arrayOfProps)

返回已删除列出的道具的对象

const record = { a: 1, b: 2, c: 3}
console.log(omit(record, ['b', 'c'])) //=> {a: 1}
Enter fullscreen mode Exit fullscreen mode

执行
export function omit(target, props) {
  return Object.fromEntries(
    Object.entries(target).filter(([key]) => !props.includes(key))
  )
}
Enter fullscreen mode Exit fullscreen mode

设置(对象,属性路径,值)

设置对象上的值,{}如有必要则创建空对象。

const record = { a: 1, d: { e: 1 } }
set(record, "a.d.e", 2) //=> { a: 1, d: { e: 2 } }
set(record, "a.b.c", 4) //=> { a: 1, b: { c: 4 }, d: { e: 2 } }
Enter fullscreen mode Exit fullscreen mode

执行
export function set(object, path, value) {
  const parts = path.split(".")
  for (let i = 0, l = parts.length - 1; i < l; i++) {
    const part = parts[i]
    object = object[part] = object[part] || {}
  }
  object[parts[parts.length - 1]] = value
}
Enter fullscreen mode Exit fullscreen mode

sortBy(数组,函数(项目)=>值|属性名称)

按子元素对数组进行排序。

const array = [{ id: "a7", name: "mike", type: "user" }, { id: "z1", name: "bob", type: "user" }, { id: "a3", name: "beth", type: "admin"} ]
console.log(sortBy(array, 'name'))
     /*=>
      [
        { id: "a3", name: "beth", type: "admin"} 
        { id: "z1", name: "bob", type: "user" }, 
        { id: "a7", name: "mike", type: "user" }, 
      ]
     */
Enter fullscreen mode Exit fullscreen mode

执行
import { pick } from "./pick"

export function sortBy(array, fn) {
  fn = pick(fn)
  return array.sort((a, b) => {
    const va = fn(a)
    const vb = fn(b)
    if (va < vb) return -1
    if (va > vb) return 1
    return 0
  })
}

Enter fullscreen mode Exit fullscreen mode

uniq(数组)

从现有数组创建一个唯一数组

const array = ['a', 'b', 'c', 'b', 'b', 'a']
console.log(uniq(array)) //=> ['a', 'b', 'c']
Enter fullscreen mode Exit fullscreen mode

执行
export function uniq(target) {
  return Array.from(new Set(target))
}
Enter fullscreen mode Exit fullscreen mode

uniqBy(数组,函数(项目)=>值|属性名称)

使用数组中对象的属性创建一个唯一数组。

const array = [{a: 1, b: 2}, {a: 4, b: 2}, {a: 5, b: 3}]
console.log(uniqBy(array, 'b')) //=> [{a: 1, b: 2}, {a: 5, b: 3}]
Enter fullscreen mode Exit fullscreen mode

执行
import { pick } from "./pick"

export function uniqBy(target, fn) {
  fn = pick(fn)
  const dedupe = new Set()
  return target.filter((v) => {
    const k = fn(v)
    if (dedupe.has(k)) return false
    dedupe.add(k)
    return true
  })
}
Enter fullscreen mode Exit fullscreen mode

部分实施debounce

lodashdebounce非常强大——对我来说太强大了,也太庞大了。我只需要一个可以去抖动的函数,一个最大等待时间,以及刷新或取消任何待处理调用的功能。(所以缺少的是尾随和前沿等等,以及其他我不使用的选项)。

const debounced = debounce(()=>save(), 1000, {maxWait: 10000})
...
debounced() // Call the debounced function after 1s (max 10s)
debounced.flush() // call any pending 
debounced.cancel() // cancel any pending calls
Enter fullscreen mode Exit fullscreen mode

执行
export function debounce(fn, wait = 0, { maxWait = Infinity } = {}) {
  let timer = 0
  let startTime = 0
  let running = false
  let pendingParams
  let result = function (...params) {
    pendingParams = params
    if (running && Date.now() - startTime > maxWait) {
      execute()
    } else {
      if (!running) {
        startTime = Date.now()
      }
      running = true
    }

    clearTimeout(timer)
    timer = setTimeout(execute, Math.min(maxWait - startTime, wait))

    function execute() {
      running = false
      fn(...params)
    }
  }
  result.flush = function () {
    if (running) {
      running = false
      clearTimeout(timer)
      fn(...pendingParams)
    }
  }
  result.cancel = function () {
    running = false
    clearTimeout(timer)
  }
  return result
}

Enter fullscreen mode Exit fullscreen mode

结论

如果只使用这些函数,可以省去对 lodash 的依赖。在我们的应用中,我们确实使用了其他 lodash 函数,但它们都使用了惰性导入(例如template)——因此,我们的应用加载速度显著提升。

您可以随意在自己的项目中使用任何代码。

文章来源:https://dev.to/miketalbot/14-functions-so-you-can-dump-lodash-and-reduce-your-bundle-size-3gg9
PREV
60fps JS 同时对数百万条记录进行排序、映射和减少(使用空闲时间协程) js-coroutines 更新协程 在此处获取库:它是如何工作的?
NEXT
为什么我从 React 转到 Svelte,以及其他人也会效仿?React 多年来一直是我的首选。之后我了解了 Svelte。Svelte 3.x。让我们重新构建 React 示例。我还喜欢 Svelte 的哪些方面?“等等,还有……”。然后,还有 Sapper 来部署 Svelte 应用。结论