本帖最后由 LinLin00 于 2023-12-19 21:54 编辑
仿 elmGetter (opinionated version)
看了 @cxxjackie 写的 elmGetter 库,哥哥真是太强啦!
本着学习的心态,我也用 ts 模仿着写了一个 "opinionated" 的自用函数,并做了一些改动:
-
不考虑 jQuery、XPath 选择器,并移除了兼容方面的考量
-
去掉了 create
方法,因为这与 elmGetter 这个库的主题相违背,变成 util 库了
-
原有的 get
方法为返回 Prosime<Element>
,本意是使用现代 await 语法简化开发,但会带来一些问题
首先这会导致 get 和 each 的 API 风格不统一
接着是是使用选择器数组同时获取多个元素时,分开的多次 await 可能造成性能问题,这个在哥哥的代码里使用 Promise.all
来解决
然后再看一个例子
await elmGetter.get(无效的选择器, parentA)
await elmGetter.get(有效的选择器, parentB)
如果在开发过程中有一个无效的元素器且没有设置超时值, 将会无限阻塞线程,不会往下运行,这会增加开发者的调试时间。
想要程序按照你想的方式运行,应该使用 elmGetter.get(无效的选择器, parentA).then(e => { ... })
因此我统一了 API 风格,第一个参数总是为选择器或选择器数组,第二个参数总是为回调函数,第三个参数是可选的 options。这可以让正确的事情变得容易
-
原有的 each
函数不支持选择器数组和 timeout 参数,且每调用一次 each 函数,都会对每一个 mutation.target
或者每个 mutation.addedNodes
中的元素应用一次 querySelectorAll
。虽然这可以通过 用逗号将选择器合并 解决一部分,但是在如果不同选择器对应不同的 callback 函数时该问题仍然存在。
这对监听大量选择器时有可能产生性能上的问题,因此我做了一个优化,保证对单个 mutation.target
仅会调用一次 querySelectorAll
-
原有的函数不支持 querySelectorAll
,但是 “不好实现,因为列表何时加载完毕是不可知的”
我仿照事件防抖 debounce 的思想,假定符合 selector 的元素连续出现,在某个指定的时间 delay 都没有再次出现之后,可以视为全部出现,以此来模拟 querySelectorAll。在我的代码中这个 allDelay 默认为 1000 ms
-
我的版本中函数返回一个手动移除本次函数调用添加的所有 selector 的函数,这个函数的返回值是是否在调用函数的时候移除了所有 selector
如果因为新出现的元素有符合 selector 的,执行 callback 之后自动 remove 了相应的 processor,则返回值为假
还有一些小改动,就不展开说了,Talk is cheap, Show you the code!
如有错误,还请哥哥们指出
/**
* 该函数用于观察和响应 DOM 的动态变化。
* 它提供了一种方式,可以将自定义回调应用到匹配特定选择器的元素上,用于动态查询和处理 DOM 元素。
* @param selector 要匹配元素的 CSS 选择器或选择器数组。
* @param callback 处理匹配元素的回调函数。
* @param options 配置选项,包含一系列可选设置:
* - parent?: ParentNode - 父节点,默认为 document。
* - once?: boolean - 是否只执行一次,默认为 true。
* - timeout?: number - 超时时间(毫秒),默认为 -1,表示无超时。
* - onTimeout?: () => void - 超时时的回调函数。
* - all?: boolean - 是否模拟 querySelectorAll,默认为 true。
* - allDelay?: number - 模拟 querySelectorAll 时的 debounce 延时(毫秒),默认为 1000。
* @returns 一个函数,用于取消所有 selector,其返回值为是否在调用函数时移除了所有 selector
*/
export type processNode = (node: Element) => void
export type SelectorCallbackTuple = [string, processNode]
export const dynamicQuery = (() => {
function addObserver(target: Node, callback: (node: Node) => void) {
// observer 断开连接之后,正在运行的循环仍会运行,所以使用一个 canceled 变量在断开之后立即结束循环
let canceled = false
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
if (mutation.type === 'childList' || mutation.type === 'attributes') {
if (canceled) return
callback(mutation.target)
for (const node of mutation.addedNodes) {
if (canceled) return
callback(node)
}
}
}
})
observer.observe(target, {
subtree: true,
childList: true,
attributes: true,
})
return () => {
canceled = true
observer.disconnect()
}
}
// 确保每个 Node 仅有一个 observer,避免创建大量的观察者对象
const observedNodeMap = new WeakMap<Node, { processors: Set<SelectorCallbackTuple>, remove: () => void }>()
function addProcessor(target: Node, processor: SelectorCallbackTuple) {
let observedNode = observedNodeMap.get(target)
if (!observedNode) {
// 确保每个元素仅触发一次检查
let checked: WeakSet<Node> | null = new WeakSet()
// 这里 Set 仅保存二元组(数组)的引用,作用是为了能方便地删除 processor
let processors: Set<SelectorCallbackTuple> | null = new Set()
const checkAndApply = (e: Element) => {
if (checked && !checked.has(e)) {
checked.add(e)
processors?.forEach(([s, f]) => {
if (e.matches(s)) {
f(e)
}
})
}
}
const disconnect = addObserver(target, e => {
if (e instanceof Element) {
checkAndApply(e)
// 当一个 observer 绑定大量 selector 时,仅需执行一次 querySelectorAll
e.querySelectorAll('*').forEach(checkAndApply)
}
})
observedNode = {
processors,
remove: () => {
disconnect()
checked = null
processors = null
},
}
observedNodeMap.set(target, observedNode)
}
observedNode.processors.add(processor)
}
// 返回是否在本次 removeProcessor 中删除了 processor,不论如何总是会保证删除
function removeProcessor(target: Node, processor: SelectorCallbackTuple) {
const observedNode = observedNodeMap.get(target)
if (!observedNode) return false
const isDeleteInThisTime = observedNode.processors.delete(processor)
if (!observedNode.processors.size) {
observedNode.remove()
observedNodeMap.delete(target)
}
return isDeleteInThisTime
}
/**
* 该函数用于观察和响应 DOM 的动态变化。
* 它提供了一种方式,可以将自定义回调应用到匹配特定选择器的元素上,用于动态查询和处理 DOM 元素。
* @param selector 要匹配元素的 CSS 选择器或选择器数组。
* @param callback 处理匹配元素的回调函数。
* @param options 配置选项,包含一系列可选设置:
* - parent?: ParentNode - 父节点,默认为 document。
* - once?: boolean - 是否只执行一次,默认为 true。
* - timeout?: number - 超时时间(毫秒),默认为 -1,表示无超时。
* - onTimeout?: () => void - 超时时的回调函数。
* - all?: boolean - 是否模拟 querySelectorAll,默认为 true。
* - allDelay?: number - 模拟 querySelectorAll 时的 debounce 延时(毫秒),默认为 1000。
* @returns 一个函数,用于取消所有 selector,其返回值为是否在调用函数时移除了所有 selector
*/
return function (selector: string | string[], callback: processNode = console.log, options: {
parent?: ParentNode,
once?: boolean,
timeout?: number,
onTimeout?: () => void,
all?: boolean,
allDelay?: number,
} = {}) {
const {
parent = document,
once = true,
timeout = -1,
onTimeout = () => console.log('dynamicQuery Timeout!', arguments),
all = true,
allDelay = 1000,
} = options
const selectors = Array.isArray(selector) ? selector : [selector]
// 总是会先立即执行 querySelector(All) 并应用 callback
const notExistSelectors = selectors.filter(selector => {
const result = all
? parent.querySelectorAll(selector)
: ([parent.querySelector(selector)].filter(e => e !== null)) as Element[]
result.forEach(callback) // Side Effect!
// 筛选留下查询不到的 selector
return result.length === 0
})
if (once && notExistSelectors.length === 0) return () => false
// 如为 once,仅需监听现存页面查询不到的 selector,否则持续监听所有 selector
const listenSelectors = once ? notExistSelectors : selectors
const processors = listenSelectors.map(selector => {
// 对于每个 selector, 保证对符合要求的每个元素仅处理一次
const processed = new WeakSet()
let timer
const process = (e: Element) => {
if (!processed.has(e)) {
processed.add(e)
callback(e)
if (once) {
if (all) {
// 使用 timer 实现 debounce,在一定时间间隔内出现的符合 selector 的每个元素都会被处理
// 以此来模拟 querySelectorAll 的效果
clearTimeout(timer)
timer = setTimeout(remove, allDelay)
}
else {
// 如果 once 为 true 且 all 为假,则类似单个 querySelector,回调触发一次就立即 remove
remove()
}
}
}
}
const processor: SelectorCallbackTuple = [selector, process]
const remove = () => removeProcessor(parent, processor)
addProcessor(parent, processor)
return remove
})
const removeAllProcessor = () => processors.every(f => f())
// 如果设置了 timeout 超时参数,则不论其他参数如何都会移除本次函数调用添加的所有 selector
if (timeout >= 0) {
setTimeout(() => {
removeAllProcessor()
onTimeout()
}, timeout)
}
// 返回一个手动移除本次函数调用添加的所有 selector 的函数
// 这个函数的返回值是是否在调用函数的时候移除了所有 selector
// 如果因为新出现的元素有符合 selector 的,执行 callback 之后自动 remove 了相应的 processor,则返回值为假
return removeAllProcessor
}
})()
/**
* 该函数用于观察和响应 DOM 的动态变化。
* 它提供了一种方式,可以将自定义回调应用到匹配特定选择器的元素上,用于动态查询和处理 DOM 元素。
* @param selector 要匹配元素的 CSS 选择器或选择器数组。
* @param callback 处理匹配元素的回调函数。
* @param options 配置选项,包含一系列可选设置:
* - parent?: ParentNode - 父节点,默认为 document。
* - timeout?: number - 超时时间(毫秒),默认为 -1,表示无超时。
* - onTimeout?: () => void - 超时时的回调函数。
* - all?: boolean - 是否模拟 querySelectorAll,默认为 true。
* - allDelay?: number - 模拟 querySelectorAll 时的 debounce 延时(毫秒),默认为 1000。
* @returns 一个函数,用于取消所有 selector,其返回值为是否在调用函数时移除了所有 selector
*/
export const foreverQuery = (s, f, o) => dynamicQuery(s, f, { once: false, ...o })