迭代 puppeteer 异步方法for
循环对比Array.map()/Array.forEach()
由于所有 puppeteer 方法都是异步的,因此我们如何迭代它们并不重要。我对最常推荐和使用的选项进行了比较和评级。
为此,我创建了一个包含大量 React 按钮的 React.Js 示例页面here https://thedavidbarton.github.io/react-examples/#/lot-of-react-buttons(我只是称之为很多反应按钮). Here (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
可能需要一些时间。
一、Array.map
一般来说,如果我们只想执行异步方法,例如在迭代中,但我们不想返回一个新数组:使用它是一个不好的做法Array.map
。 Map 方法的执行将在所有 iteratee 执行完毕之前完成,因为 Array 迭代方法同步执行 iteratee,但 puppeteer 方法、iteratee 是:异步的。
代码示例
const elHandleArray = await page.$$('button')
elHandleArray.map(async el => {
await el.click()
})
await page.screenshot({ path: 'clicks_map.png' })
await browser.close()
特产
- 返回另一个数组
- .map 方法内并行执行
- fast
132 个按钮场景结果:❌
持续时间:891 毫秒
通过在 headful 模式下观察浏览器,它看起来好像可以工作,但是如果我们检查何时page.screenshot
发生了:我们可以看到点击仍在进行中。这是由于以下事实Array.map
默认情况下不能等待。幸运的是,脚本有足够的时间来解析所有元素上的所有点击,直到浏览器没有关闭。
1320 个按钮场景结果:❌
持续时间:6868 毫秒
如果我们增加同一选择器的元素数量,我们将遇到以下错误:UnhandledPromiseRejectionWarning: Error: Node is either not visible or not an HTMLElement
,因为我们已经达到了await page.screenshot()
and await browser.close()
:浏览器已关闭时,异步点击仍在进行中。
二. Array.forEach
所有迭代器都将被执行,但 forEach 将在所有迭代器完成执行之前返回,这在许多情况下对于异步函数来说并不是理想的行为。就傀儡师而言,这是一个非常相似的情况Array.map
, 除了Array.forEach
不返回新数组。
代码示例
const elHandleArray = await page.$$('button')
elHandleArray.forEach(async el => {
await el.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()
and await browser.close()
:浏览器已关闭时,异步点击仍在进行中。
三.页面.$$eval + forEach
性能最好的解决方案是稍加修改的版本bside's answer https://stackoverflow.com/a/54712599/12412595. The 页面.$$评估 () runs 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 模式下观察浏览器,我们看到效果是立竿见影的,而且只有在单击每个元素、每个承诺都得到解决后才会截取屏幕截图。
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()
特产
- 乍一看,异步行为按预期工作
- 循环内按顺序执行
- slow
132 个按钮场景结果:✅
持续时间:2957 毫秒
通过在 headful 模式下观察浏览器,我们可以看到页面点击是严格按顺序发生的,而且只有在点击每个元素后才会截取屏幕截图。
1320 个按钮场景结果:✅
持续时间:25 396 毫秒
工作原理与 132 个按钮的情况类似(但需要更多时间)。
Summary
- 避免使用
Array.map
如果您只想执行异步事件并且不使用返回的数组,请改用 forEach 或 for-of。 ❌
-
Array.forEach
是一个选项,但您需要包装它,以便下一个异步方法仅在 forEach 内解决所有 Promise 后才启动。 ❌
- Combine
Array.forEach
with $$eval
如果异步事件的顺序在迭代内并不重要,则可以获得最佳性能。 ✅
- Use a
for
/for...of
如果速度并不重要并且异步事件的顺序在迭代中确实很重要,则循环。 ✅
来源/推荐材料
- 塞巴斯蒂安·肖邦:JavaScript:使用 forEach() 进行异步/等待(codeburst.io) https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404
- 安东尼奥·瓦尔:使用 async/await 时使数组迭代变得容易(中等的) https://medium.com/@antonioval/making-array-iteration-easy-when-using-async-await-6315c3225838
- 将 async/await 与 forEach 循环结合使用(堆栈溢出) https://stackoverflow.com/questions/37576685/using-async-await-with-a-foreach-loop
- 使用包含异步等待的数组 foreach 进行等待(堆栈溢出) https://stackoverflow.com/questions/51738684/await-with-array-foreach-containing-async-await