仿 elmGetter (opinionated version)
本帖最后由 LinLin00 于 2023-12-19 21:54 编辑# 仿 elmGetter (opinionated version)
看了 @cxxjackie 写的 (https://bbs.tampermonkey.net.cn/thread-2726-1-1.html),哥哥真是太强啦!
本着学习的心态,我也用 ts 模仿着写了一个 "opinionated" 的自用函数,并做了一些改动:
- 不考虑 jQuery、XPath 选择器,并移除了兼容方面的考量
- 去掉了 `create` 方法,因为这与 elmGetter 这个库的主题相违背,变成 util 库了
- 原有的 ```get``` 方法为返回 `Prosime<Element>`,本意是使用现代 await 语法简化开发,但会带来一些问题
首先这会导致 get 和 each 的 API 风格不统一
接着是是使用选择器数组同时获取多个元素时,分开的多次 await 可能造成性能问题,这个在哥哥的代码里使用 ```Promise.all``` 来解决
然后再看一个例子
```JavaScript
await elmGetter.get(无效的选择器, parentA)
await elmGetter.get(有效的选择器, parentB)
```
如果在开发过程中有一个无效的元素器且没有设置超时值, 将会无限阻塞线程,不会往下运行,这会增加开发者的调试时间。
想要程序按照你想的方式运行,应该使用 ```elmGetter.get(无效的选择器, parentA).then(e => { ... })```
因此我统一了 API 风格,第一个参数总是为选择器或选择器数组,第二个参数总是为回调函数,第三个参数是可选的 options。这可以让正确的事情变得容易
- 原有的 ```each``` 函数不支持选择器数组和 timeout 参数,且每调用一次 each 函数,都会对每一个 `mutation.target` 或者每个 `mutation.addedNodes` 中的元素应用一次 `querySelectorAll`。虽然这可以通过 [用逗号将选择器合并](https://bbs.tampermonkey.net.cn/forum.php?mod=redirect&goto=findpost&ptid=2726&pid=31395) 解决一部分,但是在如果不同选择器对应不同的 callback 函数时该问题仍然存在。
这对监听大量选择器时有可能产生性能上的问题,因此我做了一个优化,保证对单个 `mutation.target` 仅会调用一次 `querySelectorAll`
- 原有的函数不支持 `querySelectorAll`,但是 [“不好实现,因为列表何时加载完毕是不可知的”](https://bbs.tampermonkey.net.cn/forum.php?mod=redirect&goto=findpost&ptid=2726&pid=73580)
我仿照事件防抖 debounce 的思想,假定符合 selector 的元素连续出现,在某个指定的时间 delay 都没有再次出现之后,可以视为全部出现,以此来模拟 querySelectorAll。在我的代码中这个 allDelay 默认为 1000 ms
- 我的版本中函数返回一个手动移除本次函数调用添加的所有 selector 的函数,这个函数的返回值是是否在调用函数的时候移除了所有 selector
如果因为新出现的元素有符合 selector 的,执行 callback 之后自动 remove 了相应的 processor,则返回值为假
还有一些小改动,就不展开说了,**Talk is cheap, Show you the code!**
如有错误,还请哥哥们指出
```javascript
/**
* 该函数用于观察和响应 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
*/
```
```typescript
export type processNode = (node: Element) => void
export type SelectorCallbackTuple =
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(() => {
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 :
// 总是会先立即执行 querySelector(All) 并应用 callback
const notExistSelectors = selectors.filter(selector => {
const result = all
? parent.querySelectorAll(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 =
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 })
```
get设计为Promise是为了方便开发,但我发现大部分人还是写成了.then回调,后来确实考虑过改成与each统一的风格,但是API的变动已经难以向下兼容,因此作罢。
模拟querySelectorAll的思路是一个可选的方案,不过每个人的硬件配置、网络状况等有差别,而且定时器置于后台时有问题,我想尽量避免这类不确定性太大的方案。
create的加入只是个人喜好,因为我自己开发时会用到。后续我还想加入一个函数,用于监听特定元素的变动,比如文本变化,由于在回调里修改文本会造成死循环,我的想法是在回调执行期间暂停监听(可能需要重新包装MutationObserver),但又涉及异步回调的问题,目前已有一些解决的想法,但我不确定这个函数会不会跟create一样食之无味,所以迟迟没有加入。 cxxjackie 发表于 2023-12-19 22:52
get设计为Promise是为了方便开发,但我发现大部分人还是写成了.then回调,后来确实考虑过改成与each统一的 ...
1. 直接发 3.0.0🙂
2. 自己开发用到的小函数可以单独放在一个文件啊,不然这个库的命名不太好,做了太多的事情
3. 修改文本的话也许可以参考我代码里的 ```checked: WeakSet<Node>```,因为这保证了 MutationObserver 产生的节点只会触发一次
4. 要不直接再发个 util 工具包,各种常用的油猴脚本开发小函数都可以往里塞,再发个 npm 包,方便像我这样的使用 vscode 写脚本并打包的开发者🥹 本帖最后由 LinLin00 于 2023-12-19 23:31 编辑
其实 promise 版本很容易改成回调
```javascript
const getWithCallback = (selector, parent, f, timeout) => elmGetter.get(selector, parent, timeout).then(f)
```
我的回调也很容易改成 promise
```javascript
const dynamicQueryWithPromise = (selector, options) => new Promise(resolve => dynamicQuery(selector, resolve, options))
```
随便封装 😂
但是 promise 版本有个缺点就是无法手动取消监听了,除非同时返回 promise 和 dynamicQuery() 的结果,要么就是使用 AbortController 了,太麻烦了
回调和 promise 各有其适用场景,现在这个场景想想还是回调好 LinLin00 发表于 2023-12-19 23:26
其实 promise 版本很容易改成回调
```javascript
const getWithCallback = (selector, parent, f, time ...
就是因为很容易转换所以我目前没有改动API的想法,不然更新版本后原有脚本需要做太多修改,我还是希望尽量向下兼容的。 本帖最后由 greatYear 于 2024-1-30 13:48 编辑
赞。我加了个 callback 返回数组
/**
* callback 返回数组,非一个个元素处理,一次性返回所有符合 selector 的元素
*/
export const foreverQueryAll = (
selector: string | string[],
callback: (nodes: Element[]) => void,
options: Options = {}
) => {
const checked: Set<Element> = new Set()
let timmer: NodeJS.Timeout;
return dynamicQuery(selector, (node) => {
checked.add(node)
clearTimeout(timmer)
timmer = setTimeout(() => {
const nodes = Array.from(checked.values())
checked.clear()
callback(nodes)
}, 500)
}, { once: false, ...options });
}
本帖最后由 pawjazz 于 2024-11-27 08:20 编辑
谢谢 看看好不好使 本帖最后由 pawjazz 于 2024-11-27 08:21 编辑
谢谢谢谢谢谢
页:
[1]