Skip to content

闭包在组合查询中的使用

什么是闭包?

我记得前人给我总结闭包的时候说过: 口口相传的闭包拥有三个特性,有函数,有内部变量,有返回值, 其目的就是为了绕过浏览器的垃圾回收机制, 令闭包对象及其相关的引用值长期留在浏览器中. 但是现在对闭包有了一点点自己的理解.

首先看一下MDN的对于闭包的定义: 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

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

其常规写法如下:

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

以上代码中, ff 就是 f1函数执行 f2 时形成的一个函数实例的引用, f2维持了一个对它的词法环境的引用 a, 所以在调用ff时, a 依然存在.

闭包的写法种类有很多, 可以参考闭包的多种写法, 由此可见实现一个闭包的方式, 在js中并不只有一种.

闭包在业务场景中的使用

在日常的业务开发过程中,我们使用闭包的场景更多的是处理请求的缓存值,如果命中的缓存就取缓存值,如果没有就重新发起请求。

但是鲜有对函数调用时,缓存函数形参的实现,这个我找了很多的资料,最多的还是对于请求结果的缓存,却忽略了对于多区块条件查询时,函数执行的参数的处理。

其示意图如下:

image-1

这个是我们在中后台应用中经常遇见的条件查询示意图, Table部分负责渲染数据,所以我们需要一个Request方法来获取数据,其他部分的查询表单就是Request方法的形参.

最为常见的搜索模式就是 Search + Pagenation 组合查询. 为了应对这种情景下我的组合查询,我们会提前定义一个SearchObj用于储存Search Change 和 Pagenation Change 后的值。所以它一定会有如下操作: 1. 在Pagenation 改变后 更新 SearchObj中的pagenation值; 2. 发起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} />

    <Pagenation onChange={handleSearchChange} />
  </>)
}

这个是比较常规的处理联合查询的业务, 其实现过程还算是比较简单, 但是一旦遇到了Search部分拆分了太多的UI, 或者是多个Search组合的情景下, 每一个Search变动之后都需要重复经历上面两步,赋值和请求.

现在我们是不是可以考虑一下, 将以上步骤简化一下, 就利用闭包的特性, 始终调用memo函数, 将函数执行的形参缓存下来, 从而达到免去重复操作的步骤.

实现memo函数

首先我们要实现一个memo函数, 其次结合我们的技术栈React, 我们还要实现一个Memo Hooks。

引用之前实现的一个简易版本:

javascript
function memo(fn) {
  let store = {};
  // eslint-disable-next-line func-names
  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或者是WeakMap数据结构, 其进阶版本的实现如下:

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()
  
  // 查询闭包内部缓存的 Store
  call.get = call.prototype.get

  // 原 Map 方法, 查看Store中是否有对应的 Key
  call.has = key => cache.has(key)

  // 原Map方法, 清空当前Key下全部缓存的值
  call.clear = () => cache.clear()

  // 原Map方法, 清空对应的Key的Map
  call.delete = key => cache.delete(key)

  // 根据Key更新 对应的值
  call.update = update

  // 原Map方法, 查询size
  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 memof1 = memoQuery(logger, { key: 'logger' })

上面的代码似乎是好了一点点, 它的api全部是Map对象的api, 基于Map的扩展是不是比自己半斤八两的封装好的太多了.

这里将一个注销接口暴露出来, 专门用于组件销毁后其词法环境的引用值无法被浏览器回收的问题, 从而避免内存泄漏问题.

我可以将返回的方法做成一个AOP或者是一个Promise, 这样用户可以自定义一些行为, 可以处理参数的类型问题或者是接口返回的数据接口问题,AOP就可以轻易的实现。


持续更新中

相关文章:
源码地址: https://github.com/stack-wuh/utils.git

MIT License.