闭包在组合查询中的使用
什么是闭包?
我记得前人给我总结闭包的时候说过: 口口相传的闭包拥有三个特性,有函数,有内部变量,有返回值, 其目的就是为了绕过浏览器的垃圾回收机制, 令闭包对象及其相关的引用值长期留在浏览器中. 但是现在对闭包有了一点点自己的理解.
首先看一下MDN的对于闭包的定义: 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 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中并不只有一种.
闭包在业务场景中的使用
在日常的业务开发过程中,我们使用闭包的场景更多的是处理请求的缓存值,如果命中的缓存就取缓存值,如果没有就重新发起请求。
但是鲜有对函数调用时,缓存函数形参的实现,这个我找了很多的资料,最多的还是对于请求结果的缓存,却忽略了对于多区块条件查询时,函数执行的参数的处理。
其示意图如下:
这个是我们在中后台应用中经常遇见的条件查询示意图, Table部分负责渲染数据,所以我们需要一个Request方法来获取数据,其他部分的查询表单就是Request方法的形参.
最为常见的搜索模式就是 Search + Pagenation 组合查询. 为了应对这种情景下我的组合查询,我们会提前定义一个SearchObj用于储存Search Change 和 Pagenation Change 后的值。所以它一定会有如下操作: 1. 在Pagenation 改变后 更新 SearchObj中的pagenation值; 2. 发起Request请求;
其代码实现如下:
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。
引用之前实现的一个简易版本:
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;
};
}
分析一下:
- 首先定义一个变量Store 用于缓存memoFn的params
- 其次执行memoFn, 其形参就是 store + params
- 考虑到重置操作, 只是简单传递一个 isFresh 来操作 store 的重置
上面的代码也有其问题:
- 由闭包带来的内存泄漏问题, React组件卸载后,其memoFn的还存在于浏览器内存当中
- 横向扩展问题, 没有办法继续在当前实现的基础上继续扩展其他的方法属性
改写一下上面的代码, 将一些属性定义在原型上, 使用Map或者是WeakMap数据结构, 其进阶版本的实现如下:
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就可以轻易的实现。
持续更新中