在for 循环与Array.map()/Array.forEach() 中迭代 puppeteer 异步方法
由于所有 puppeteer 方法都是异步的,因此我们如何迭代它们并不重要。我对最常用和最常用的选项进行了比较和评分。
为此,我创建了一个 React.Js 示例页面,其中包含许多 React 按钮 here(我称之为 Lot Of React Buttons)。这里 (1) 我们可以设置在页面上呈现多少个按钮; (2)我们可以通过点击激活黑色按钮变为绿色。我认为它与 OP 的用例相同,它也是浏览器自动化的一般情况(如果我们在页面上做某事,我们希望会发生一些事情)。
假设我们的用例是:
Scenario outline: click all the buttons with the same selector
Given I have <no.> black buttons on the page
When I click on all of them
Then I should have <no.> green buttons on the page
有一种保守且相当极端的情况。点击 no. = 132 按钮并不是一项庞大的 CPU 任务,no. = 1320 可能需要一些时间。
我。数组.map
一般来说,如果我们只想在迭代中执行像elementHandle.click 这样的异步方法,但又不想返回一个新数组:使用Array.map 是一种不好的做法。 Map方法的执行要在所有的迭代器完全执行之前完成,因为数组迭代方法是同步执行迭代器的,而puppeteer方法,迭代器是:异步的。
代码示例
const elHandleArray = await page.$$('button')
elHandleArray.map(async el => {
await el.click()
})
await page.screenshot({ path: 'clicks_map.png' })
await browser.close()
专业
132 个按钮场景结果:❌
持续时间:891 毫秒
通过在 headful 模式下观察浏览器,它看起来可以正常工作,但如果我们检查 page.screenshot 何时发生:我们可以看到点击仍在进行中。这是因为默认情况下无法等待Array.map。脚本有足够的时间来解决所有元素上的所有点击,直到浏览器没有关闭,这只是运气。
1320 按钮场景结果:❌
持续时间:6868 毫秒
如果我们增加同一选择器的元素数量,我们将遇到以下错误:
UnhandledPromiseRejectionWarning: Error: Node is either not visible or not an HTMLElement,因为我们已经到达await page.screenshot() 和await browser.close():异步点击仍在进行中,而浏览器已经关闭。
二。 Array.forEach
所有的迭代都将被执行,但 forEach 将在所有迭代完成之前返回,这在许多异步函数的情况下不是可取的行为。就 puppeteer 而言,它与 Array.map 非常相似,除了:Array.forEach 不返回新数组。
代码示例
const elHandleArray = await page.$$('button')
elHandleArray.forEach(async el => {
await element.click()
})
await page.screenshot({ path: 'clicks_foreach.png' })
await browser.close()
专业
132 个按钮场景结果:❌
持续时间:1058 毫秒
通过在 headful 模式下观察浏览器,它看起来可以正常工作,但如果我们检查 page.screenshot 何时发生:我们可以看到点击仍在进行中。
1320 按钮场景结果:❌
持续时间:5111 毫秒
如果我们使用相同的选择器增加元素的数量,我们将遇到以下错误:
UnhandledPromiseRejectionWarning: Error: Node is either not visible or not an HTMLElement,因为我们已经到达 await page.screenshot() 和 await browser.close():异步点击仍在进行中,而浏览器已经关闭。
三。 page.$$eval + forEach
性能最佳的解决方案是bside 的answer 的略微修改版本。 page.$$eval (page.$$eval(selector, pageFunction[, ...args])) 在页面内运行Array.from(document.querySelectorAll(selector)),并将其作为第一个参数传递给pageFunction。它用作 forEach 的包装器,因此可以完美地等待。
代码示例
await page.$$eval('button', elHandles => elHandles.forEach(el => el.click()))
await page.screenshot({ path: 'clicks_eval_foreach.png' })
await browser.close()
专业
- 在 .forEach 方法中使用异步 puppeteer 方法没有副作用
- .forEach 方法内的并行执行
- 极快
132 个按钮场景结果:✅
持续时间:711 毫秒
通过在 headful 模式下观察浏览器,我们可以看到效果是立竿见影的,而且只有在每个元素被点击、每个 promise 都被解决后才会截取屏幕截图。
1320 个按钮场景结果:✅
持续时间:3445 毫秒
就像 132 个按钮的情况一样,非常快。
四。 for...of 循环
最简单的选项,不是那么快并且按顺序执行。在循环未完成之前,脚本不会转到page.screenshot。
代码示例
const elHandleArray = await page.$$('button')
for (const el of elHandleArray) {
await el.click()
}
await page.screenshot({ path: 'clicks_for_of.png' })
await browser.close()
专业
- 第一眼看到异步行为按预期工作
- 在循环内按顺序执行
- 慢
132 个按钮场景结果:✅
持续时间:2957 毫秒
通过在 headful 模式下观察浏览器,我们可以看到页面点击是按严格的顺序发生的,而且屏幕截图是在每个元素都被点击后才截取的。
1320 个按钮场景结果:✅
持续时间:25 396 毫秒
就像 132 个按钮的情况一样工作(但需要更多时间)。
总结
- 如果您只想执行异步事件并且不使用返回的数组,请避免使用
Array.map,请改用 forEach 或 for-of。 ❌
-
Array.forEach 是一个选项,但您需要将其包装起来,以便下一个异步方法仅在所有承诺都在 forEach 中解决后才开始。 ❌
- 如果异步事件的顺序在迭代中无关紧要,则将
Array.forEach 与 $$eval 组合使用以获得最佳性能。 ✅
- 如果速度不重要并且异步事件的顺序在迭代中很重要,请使用
for/for...of 循环。 ✅
来源/推荐材料