当我们调用Puppeteer的waitForSelector API时会发生什么?

4
这个问题基于Puppeteer和无头Chrome交互(基于Chrome开发者工具协议)。
Puppeteer向Chrome devtools发送JSON格式的消息来控制Chrome操作,例如访问页面、在文本字段中输入或单击按钮等。
当我们执行以下代码行时(这有助于等待#username可见):
await page.waitForSelector('#username', { visible: true });

Puppeteer向Chrome发送以下5个消息。

{"sessionId":"EB950D87CE0E2EED6D432F080811B87D","method":"Runtime.callFunctionOn","params":{"functionDeclaration":"async function waitForPredicatePageFunction(predicateBody, polling, timeout, ...args) {\n  const predicate = new Function('...args', predicateBody);\n  let timedOut = false;\n  if (timeout)\n    setTimeout(() => timedOut = true, timeout);\n  if (polling === 'raf')\n    return await pollRaf();\n  if (polling === 'mutation')\n    return await pollMutation();\n  if (typeof polling === 'number')\n    return await pollInterval(polling);\n\n  /**\n   * @return {!Promise<*>}\n   */\n  function pollMutation() {\n    const success = predicate.apply(null, args);\n    if (success)\n      return Promise.resolve(success);\n\n    let fulfill;\n    const result = new Promise(x => fulfill = x);\n    const observer = new MutationObserver(mutations => {\n      if (timedOut) {\n        observer.disconnect();\n        fulfill();\n      }\n      const success = predicate.apply(null, args);\n      if (success) {\n        observer.disconnect();\n        fulfill(success);\n      }\n    });\n    observer.observe(document, {\n      childList: true,\n      subtree: true,\n      attributes: true\n    });\n    return result;\n  }\n\n  /**\n   * @return {!Promise<*>}\n   */\n  function pollRaf() {\n    let fulfill;\n    const result = new Promise(x => fulfill = x);\n    onRaf();\n    return result;\n\n    function onRaf() {\n      if (timedOut) {\n        fulfill();\n        return;\n      }\n      const success = predicate.apply(null, args);\n      if (success)\n        fulfill(success);\n      else\n        requestAnimationFrame(onRaf);\n    }\n  }\n\n  /**\n   * @param {number} pollInterval\n   * @return {!Promise<*>}\n   */\n  function pollInterval(pollInterval) {\n    let fulfill;\n    const result = new Promise(x => fulfill = x);\n    onTimeout();\n    return result;\n\n    function onTimeout() {\n      if (timedOut) {\n        fulfill();\n        return;\n      }\n      const success = predicate.apply(null, args);\n      if (success)\n        fulfill(success);\n      else\n        setTimeout(onTimeout, pollInterval);\n    }\n  }\n}\n//# sourceURL=__puppeteer_evaluation_script__\n","executionContextId":4,"arguments":[{"value":"return (function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {\n      const node = isXPath\n        ? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue\n        : document.querySelector(selectorOrXPath);\n      if (!node)\n        return waitForHidden;\n      if (!waitForVisible && !waitForHidden)\n        return node;\n      const element = /** @type {Element} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);\n\n      const style = window.getComputedStyle(element);\n      const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();\n      const success = (waitForVisible === isVisible || waitForHidden === !isVisible);\n      return success ? node : null;\n\n      /**\n       * @return {boolean}\n       */\n      function hasVisibleBoundingBox() {\n        const rect = element.getBoundingClientRect();\n        return !!(rect.top || rect.bottom || rect.width || rect.height);\n      }\n    })(...args)"},{"value":"raf"},{"value":30000},{"value":"#username"},{"value":false},{"value":true},{"value":false}],"returnByValue":false,"awaitPromise":true,"userGesture":true},"id":28}

---------------

{"sessionId":"EB950D87CE0E2EED6D432F080811B87D","method":"Runtime.callFunctionOn","params":{"functionDeclaration":"s => !s\n//# sourceURL=__puppeteer_evaluation_script__\n","executionContextId":4,"arguments":[{"objectId":"{\"injectedScriptId\":4,\"id\":3}"}],"returnByValue":true,"awaitPromise":true,"userGesture":true},"id":29}

-------------

{"sessionId":"EB950D87CE0E2EED6D432F080811B87D","method":"DOM.describeNode","params":{"objectId":"{\"injectedScriptId\":4,\"id\":3}"},"id":30}

-------------

{"sessionId":"EB950D87CE0E2EED6D432F080811B87D","method":"DOM.resolveNode","params":{"backendNodeId":11,"executionContextId":3},"id":31}

-------------

{"sessionId":"EB950D87CE0E2EED6D432F080811B87D","method":"Runtime.releaseObject","params":{"objectId":"{\"injectedScriptId\":4,\"id\":3}"},"id":32}

我希望理解这里发生了什么。第一个消息看起来像是JavaScript函数。这个JavaScript函数是否在无头Chrome中执行?

基本上,我需要清楚地了解在执行waitForSelector时发生了什么。

编辑 如果从第一个JSON消息中提取JavaScript函数,则如下所示。

async function waitForPredicatePageFunction(predicateBody, polling, timeout, ...args) {\
  const predicate = new Function('...args', predicateBody);\
  let timedOut = false;\
  if (timeout)\
    setTimeout(() => timedOut = true, timeout);\
  if (polling === 'raf')\
    return await pollRaf();\
  if (polling === 'mutation')\
    return await pollMutation();\
  if (typeof polling === 'number')\
    return await pollInterval(polling);\
\
  /**\
   * @return {!Promise<*>}\
   */\
  function pollMutation() {\
    const success = predicate.apply(null, args);\
    if (success)\
      return Promise.resolve(success);\
\
    let fulfill;\
    const result = new Promise(x => fulfill = x);\
    const observer = new MutationObserver(mutations => {\
      if (timedOut) {\
        observer.disconnect();\
        fulfill();\
      }\
      const success = predicate.apply(null, args);\
      if (success) {\
        observer.disconnect();\
        fulfill(success);\
      }\
    });\
    observer.observe(document, {\
      childList: true,\
      subtree: true,\
      attributes: true\
    });\
    return result;\
  }\
\
  /**\
   * @return {!Promise<*>}\
   */\
  function pollRaf() {\
    let fulfill;\
    const result = new Promise(x => fulfill = x);\
    onRaf();\
    return result;\
\
    function onRaf() {\
      if (timedOut) {\
        fulfill();\
        return;\
      }\
      const success = predicate.apply(null, args);\
      if (success)\
        fulfill(success);\
      else\
        requestAnimationFrame(onRaf);\
    }\
  }\
\
  /**\
   * @param {number} pollInterval\
   * @return {!Promise<*>}\
   */\
  function pollInterval(pollInterval) {\
    let fulfill;\
    const result = new Promise(x => fulfill = x);\
    onTimeout();\
    return result;\
\
    function onTimeout() {\
      if (timedOut) {\
        fulfill();\
        return;\
      }\
      const success = predicate.apply(null, args);\
      if (success)\
        fulfill(success);\
      else\
        setTimeout(onTimeout, pollInterval);\
    }\
  }\
}\
//# sourceURL=__puppeteer_evaluation_script__\

在参数列表中,我看到以下参数

return (function predicate(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {\
      const node = isXPath\
        ? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue\
        : document.querySelector(selectorOrXPath);\
      if (!node)\
        return waitForHidden;\
      if (!waitForVisible && !waitForHidden)\
        return node;\
      const element = /** @type {Element} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);\
\
      const style = window.getComputedStyle(element);\
      const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();\
      const success = (waitForVisible === isVisible || waitForHidden === !isVisible);\
      return success ? node : null;\
\
      /**\
       * @return {boolean}\
       */\
      function hasVisibleBoundingBox() {\
        const rect = element.getBoundingClientRect();\
        return !!(rect.top || rect.bottom || rect.width || rect.height);\
      }\
    })(...args)

然后,我看到其他的参数:

raf

3000

username

false

true

false

这些都是信息。我无法将它们联系在一起来理解正在发生的事情。您能否详细解释一下这里发生了什么。

1个回答

4
在 Puppeteer 中,waitFor 使用轮询来解决问题。 waitForSelector 使用 raf 选项进行轮询:

raf: 在 requestAnimationFrame 回调中不断执行 pageFunction。这是最紧密的轮询模式,适合观察样式更改。

所以,基本上,waitForSelector 将发送一个函数,在每个 requestAnimationFrame 上运行。当选择器可见时(或隐藏,具体取决于您的选项),该函数将解析一个 Promise 或者超时。
当该函数被序列化并发送到 Chromium 时,会发生以下情况:
  • 将执行 waitForPredicatePageFunction
  • 由于 polling 方法将是 raf,因此它将等待 pollRaf
  • pollRaf 将执行作为参数传递的函数,此处为 selector 检查。
  • 如果为 false,则返回一个 Promise。
  • 它将调用自身调用 requestAnimationFrame
  • 它将循环直到谓词返回 true 或超时。

我已经编辑了我的问题,并提供了更多的细节。请求能否解释一下实际发生了什么。我知道JavaScript,只是基础水平。 - Austin
@Austin,我不明白你想知道什么。 - hardkoded
我们有一个名为waitForPredicatePageFunction的JavaScript函数。其中的参数predicateBody看起来像另一个JavaScript函数。哪个会被执行,或者两个都会被执行。我看到其他函数被调用,比如requestAnimationFrame(onRaf); fulfill(); 这些在哪里定义?也许是因为我的JS知识不够全面,所以我没有完全理解它。如果您可以编辑您的答案,说明正在按顺序执行的内容,那将是非常有帮助的。 - Austin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接