Skip to content

闭包在组合查询中的使用:从缓存请求到缓存参数

摘要: 闭包不仅可以缓存请求结果,还可以缓存函数执行的形参,从而简化多条件组合查询的重复赋值逻辑。


什么是闭包

MDN 的定义:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起,这样的组合就是闭包。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

从定义可以看出,闭包形成后会创建一个包含该函数执行时引用的局部变量的词法环境组合。

常规写法:

javascript
function f1 () {
  let a = 0
  function f2 () {
    a++
    console.log(a)
  }
  return f2
}

var ff = f1()
ff() // a = 1
ff() // a = 2
ff() // a = 3

fff1 执行 f2 时形成的一个函数实例引用,f2 维持了对词法环境引用 a,所以调用 ffa 依然存在。

闭包的写法有很多种,可以参考闭包的多种写法


闭包在业务中的使用

日常开发中,闭包最常见的场景是缓存请求结果:命中缓存取缓存值,未命中重新发起请求。

但鲜有对函数调用时缓存形参的实现。在中后台应用中经常遇到的条件查询:

image-1

Table 负责渲染数据,需要一个 Request 方法获取数据,其他查询表单就是 Request 方法的形参。

最常见的搜索模式是 Search + Pagination 组合查询。通常会定义一个 SearchObj 存储 Search Change 和 Pagination Change 后的值,每次变化都要执行两步:更新 SearchObj → 发起 Request。

javascript
import { useState, useEffect } from 'react'

const Demo = () => {
  const [searchArgv, setSearchArgv] = useState({ pageNum: 1, pageSize: 7 })

  const handleRequest = (params) => {
    return fetch(api, params)
  }

  const handleSearchChange = (params) => {
    setSearchArgv(params)
  }

  useEffect(() => handleRequest(searchArgv), [searchArgv])

  return (
    <>
      <Search onChange={handleSearchChange} />
      <Table onChange={handleSearchChange} />
      <Pagination onChange={handleSearchChange} />
    </>
  )
}

这种常规处理在 Search 部分拆分太多 UI 或多个 Search 组合时,每一次变动都需要重复赋值和请求。

思路: 利用闭包特性,始终调用 memo 函数,将函数执行的形参缓存下来,免去重复操作。


实现 memo 函数

简易版本:

javascript
function memo (fn) {
  let store = {}
  return function (params, isFresh = false) {
    if (isFresh) {
      store = {}
    } else {
      store = Object.assign(store, params)
    }
    if (fn) {
      fn.call(this, store)
    }
    return store
  }
}

分析:

  1. 定义变量 store 缓存 memoFn 的 params
  2. 执行 memoFn,形参为 store + params
  3. 考虑重置操作,通过 isFresh 控制 store 重置

问题:

  1. 闭包带来的内存泄漏——React 组件卸载后 memoFn 仍存在于内存中
  2. 横向扩展问题——无法继续扩展其他方法属性

进阶版本

将属性定义在原型上,使用 Map 数据结构:

javascript
const KEY_FIELD = '@@'

function memoQuery (fn, { key = KEY_FIELD, initialValues = {} }) {
  const cache = new Map()

  if (initialValues) {
    cache.set(Symbol(key), initialValues)
  }

  const update = (key, val) => {
    const origin = cache.has(key) ? cache.get(key) : {}
    cache.set(key, { ...origin, ...val })
  }

  function call (params = {}) {
    update(key, params)
    fn.call(this, cache.get(key))
    this.get = key => cache.get(key)
  }

  call.prototype = new call()
  call.get = call.prototype.get
  call.has = key => cache.has(key)
  call.clear = () => cache.clear()
  call.delete = key => cache.delete(key)
  call.update = update
  call.size = cache.size
  call.unmounted = () => {
    call = new Object(null)
    cache.clear()
  }

  return call
}

const logger = (params) => console.log(`current is params: ${JSON.stringify(params)}`)
const memoFn = memoQuery(logger, { key: 'logger' })

改进:

  • API 全部是 Map 对象的 API,扩展性更好
  • 暴露 unmounted 接口,组件销毁后清理词法环境引用,避免内存泄漏
  • 可以扩展为 AOP 或 Promise,让用户自定义处理参数类型或接口返回数据结构

相关链接

MIT License.