我正在编写一个脚本来删除我的所有YouTube评论。这段代码的每一行都可以删除一条注释,但是当我将它放入循环中时,我会得到Uncaught TypeError: Cannot read property 'click' of undefined
,当我单独运行每一行时,它不会出现。我想如果我能找到一种在代码行之间睡觉的方法,我可以删除错误。
var myList = document.getElementsByClassName("dropdown-trigger style-scope ytd-menu-renderer");
for(i = 0; i < myList.length; i++) {
myList[i].click();
document.getElementsByClassName("style-scope ytd-menu-navigation-item-renderer")[4].click(); //error here
document.getElementById("confirm-button").click();
}
我尝试使用setTimeout,如下所示:
var myList = document.getElementsByClassName("dropdown-trigger style-scope ytd-menu-renderer");
for(i=0; i<myList.length; i++) {
myList[i].click();
setTimeout(function(){document.getElementsByClassName("style-scope ytd-menu-navigation-item-renderer")[4].click();}, 1000);
setTimeout(function(){document.getElementById("confirm-button").click();}, 5000);
}
它返回了279个,但没有错误,也没有删除任何评论。发生了什么事?
答案 0 :(得分:1)
罪魁祸首是,自您执行.click()
函数以来,页面没有时间更新DOM。你需要给它那个时间,可能是requestAnimationFrame
或不太精确setTimeout
。在下面的解决方案中(完全未经测试,在现场制作),进展是在暂停后执行每个操作(打开下拉列表,单击删除,确认)以给DOM更新时间。考虑:
function deleteAllComments ( ) {
'use strict';
var commentsToDelete, i;
function openNextCommentOptionsDropdown ( ) {
if ( ! (++i < commentsToDelete.length ) ) {
console.log('No more comments to remove.');
return;
}
commentsToDelete[i].click();
setTimeout(activateCommentDeleteButton, 50);
}
function activateCommentDeleteButton ( ) {
var el = document.getElementsByClassName("style-scope ytd-menu-navigation-item-renderer")[4];
if ( ! el ) {
console.warn('No "Delete" button found, at comment #', i, commentsToDelete[i]);
console.log('Stopping delete operation');
return;
}
el.click();
setTimeout(activateCommentDeleteConfirmationButton, 50);
}
function activateCommentDeleteConfirmationButton ( ) {
var el = document.getElementById("confirm-button");
if ( ! el ) {
console.warn('Unable to confirm comment delete action, at comment #', i, commentsToDelete[i]);
console.log('Stopping delete operation');
return;
}
el.click();
setTimeout(openNextCommentOptionsDropdown, 50); // continue larger "loop"
}
commentsToDelete = document.getElementsByClassName("dropdown-trigger style-scope ytd-menu-renderer");
openNextCommentOptionsDropdown(); // start the "loop"
}
这里有两个很大的细节:
为响应代码已启动的操作(setTimeout(..., 50)
提供DOM更新时间,其中50毫秒应该足够时间;根据需要减少,或者只使用requestAnimationFrame
。
防御性编码:在对退回的项目盲目执行函数之前,首先检查项目是否实际存在(if ( ! el ) { ... }
)
答案 1 :(得分:1)
以下所有选项都有点“hackish” - 但是在完成单击事件处理后,没有一致且简单的方法来获取通知。 您可以将自己的单击事件侦听器添加到目标元素,但是,可以通过stopPropagation将其“静音”... 即使这样,也没有(简单的)方法知道DOM何时因为点击事件而“重新绘制” - 你可以看一下使用 MutationObserver,我想
我假设(从不这样做),document.getElementsByClassName("style-scope ytd-menu-navigation-item-renderer")[4]
定位的元素会因点击myList[i].click();
而动态添加
这就是你遇到问题的原因。在myList[i].click();
之后,DOM将不会“更新”“一段时间” - 因此,document.getElementsByClassName("style-scope ytd-menu-navigation-item-renderer")
可能很容易失败。
一个潜在的问题是,如果删除评论会更改
document.getElementsByClassName("dropdown-trigger style-scope ytd-menu-renderer");
定位的元素数量 - 因为HTMLCollection
是“实时”,删除DOM元素会改变HTMLCollection
- 只有最后两个代码片段将不受这种可能性的影响
所以,这里有四种不同的方法
选项1 - ES5,只是简单的'回调
var DELAY = 0; // try 0, then try increasing values
var myList = document.getElementsByClassName("dropdown-trigger style-scope ytd-menu-renderer");
function confirmClick(callback3) {
document.getElementById("confirm-button").click();
setTimeout(callback3, DELAY);
}
function itemClick(callback2, callback3) {
document.getElementsByClassName("style-scope ytd-menu-navigation-item-renderer")[4].click();
setTimeout(callback2, DELAY, callback3);
}
function listClick(element, callback1, callback2, callback3) {
element.click();
setTimeout(callback1, DELAY, callback2, callback3);
}
function doOne(i) {
listClick(myList[i], itemClick, confirmClick, function() {
++i;
if (i < myList.length) {
doOne(i);
}
});
}
doOne(0);
当你试图将多个异步(某种)进程“链接”在一起时,
Promise
(似乎)使代码变得不那么麻烦
选项2 - 承诺,但嵌套setTimeout
的 -
使用Array#reduce
将点击链接在一起,以便严格按照一个接一个地进行处理
这是丑陋的,真的很难看,让金字塔建筑留给死去的埃及人, 包括说明我想要实现的目标
var DELAY = 0; // try 0, then try increasing values
var myList = document.getElementsByClassName("dropdown-trigger style-scope ytd-menu-renderer");
var p = Promise.resolve();
for(i = 0; i < myList.length; i++) {
myList[i].click();
p = p.then(() => new Promise(resolve) => {
setTimeout(() => {
document.getElementsByClassName("style-scope ytd-menu-navigation-item-renderer")[4].click();
setTimeout(() => {
document.getElementById("confirm-button").click();
setTimeout(resolve, DELAY);
}, DELAY);
}, DELAY);
});
}
选项3 - 使用辅助函数的Promise - 基本上是上面的代码,但添加了一个帮助函数来防止金字塔
const DELAY = 0; // try 0, then try increasing values
const clickThenDelay = element => new Promise(resolve => {
element.click();
setTimeout(resolve, DELAY);
});
var myList = document.getElementsByClassName("dropdown-trigger style-scope ytd-menu-renderer");
Array.from(myList).reduce((p, item) => {
return p
.then(() => clickThenDelay(item))
.then(() => clickThenDelay(document.getElementsByClassName("style-scope ytd-menu-navigation-item-renderer")[4]))
.then(() => clickThenDelay(document.getElementById("confirm-button")))
}, Promise.resolve());
选项4 - 类似于选项3,但是这一个在触发点击之前添加了一个点击事件处理程序,在点击被“处理”之后被删除。 据推测,最后添加的处理程序最后被调用。但是,不确定是否可以保证。 此外,如果先前的处理程序调用{{1}}
,这将不起作用event.stop[Immediate]Propagation()