Chrome Extentsion 开发,content.js MutationObserver 能观察到第一层,但无法观察第二层,为什么加了SetTimeout 就稳定运行?

在一个 chrome extension 项目中,在 content.js 使用 mutationobserver 来监听 dom 发生的改变,但实际上时而能执行时而不行

{
  "manifest_version": 3,
  "name": "TEST TAG",
  "version": "1.0.0",
  "description": "A Chrome extension built with Vite and Vue",
  "icons": {
    "16": "icons/icon16.png",
    "19": "icons/icon19.png",
    "38": "icons/icon38.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "permissions": [
    "storage",
    "activeTab",
    "scripting"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "action": {
    "default_popup": "index.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "19": "icons/icon19.png",
      "38": "icons/icon38.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_idle",
      "all_frames": true,
      "type": "module"
    }
  ]
} 

以下是能够稳定成功执行的代码

setTimeout(() => {
    observerHistoryItems()
}, 5000)


function observerHistoryItems() {
    // 可以更换成更为具体的 element
    const targetBody = confirmLastOne()

    console.log("info", "targetBody", targetBody)
    // console.log("info", "lastDiv", lastDiv)

    const config = {
        childList: true,
        subtree: true,
        attributes: true,
        characterData: true
    }

    const callback = (mutations) => {
        console.log("this is one observe callback")

        mutations.forEach((mutation) => {
            // setTimeout(() => {
                //TODO
                if(true || mutation.target.localName === "ol" ){
                    console.log("info", "Mutation detected:", mutation);
                }
            // }, 500)
            // observer.disconnect()
        });
    }

    const observer = new MutationObserver(callback)
    observer.observe(targetBody, config)
    console.log("info", "Started observing with config:", config)
}



function confirmLastOne(){
    let nodeList = document.querySelectorAll('div[class*="group/sidebar"]')
    console.log("info","nodeList",nodeList, Object.prototype.toString.call(nodeList))
    console.log("info","nodeList[0]",nodeList[0], Object.prototype.toString.call(nodeList[0]))

    let targetBody = nodeList[0]
    // CHATGPT 的 history 在最后一个 div 中

    console.log("confirmLastOne", "lastElementChild", targetBody.lastElementChild)
    // return targetBody.lastElementChild

    console.log("info", "is Element?", targetBody instanceof Element)
    console.log("info", "is Node?", targetBody instanceof Node)
    return targetBody
}

重点就在代码刚开始的 setTimeout 上,当设置的时间过短,则无法运行,只会检测第一层 div,但是第一层后面的 dom 变化,我无法触发 callback,我有注意到 observe 是把对应的 config 设置为 true,我也明确了后面的 dom 变化是又 js 请求获得的数据渲染的

当我把 setTimeout 设置时间长一点则运行正常,后面所有的 mutation 都能被 callback。

我猜测 setTimeout 是把主线程让了出来,等待 UI 渲染,能够获取真正的 Node,才能被 observe。但奇怪的是,我把 setTimeout 设置很短,甚至为0,也能获取 dom,类型为 Node,但就是无法被 observe。我想知道其中的原因并优化(PS:我已经用过 ChatGPT, DeepSeek, Copilot。但是 AI 关注的是我 observe 的两个参数,我都验证了没问题)

阅读 405
1 个回答

我认为问题的核心在于DOM的"真正准备状态"与表面可见性之间的差异。即使你的confirmLastOne()函数能获取到DOM元素并确认它是Node和Element,但该元素可能处于一种"半就绪"状态。

在现代网页中,DOM操作通常涉及多个阶段:

  1. 基本DOM结构渲染
  2. JavaScript框架(如React)接管并初始化组件
  3. 事件监听器和数据绑定设置
  4. 网络请求获取数据并更新视图

你的观察器在这些阶段之间的某个点被初始化了。当setTimeout时间较短时,你可能捕获到了物理上存在但"功能上未就绪"的DOM元素。

建议尝试以下方法:

  1. 使用递归检测目标元素的完整性

    function whenElementFullyReady(selector, callback, checkInterval = 500) {
      const element = document.querySelector(selector);
      
      if (element && element.childNodes.length > 5) {  // 某个表明"完全就绪"的指标
     callback(element);
      } else {
     setTimeout(() => whenElementFullyReady(selector, callback, checkInterval), checkInterval);
      }
    }
    
    whenElementFullyReady('div[class*="group/sidebar"]', (element) => {
      // 确保元素已完全就绪后初始化观察器
      observerHistoryItems();
    });
  2. 监听特定事件后再初始化

    // 可以尝试监听页面网络活动完成后再初始化
    let networkIdle = false;
    let originalXHR = window.XMLHttpRequest.prototype.send;
    let requestCount = 0;
    
    window.XMLHttpRequest.prototype.send = function() {
      requestCount++;
      originalXHR.apply(this, arguments);
      
      this.addEventListener('loadend', () => {
     requestCount--;
     if (requestCount === 0 && !networkIdle) {
       networkIdle = true;
       setTimeout(observerHistoryItems, 500); // 网络请求都完成后,给DOM再一点时间
     }
      });
    };

这个问题本质上是异步操作与DOM渲染时序的复杂性。短延迟时不工作而长延迟时工作的原因,确实是因为长延迟给了页面足够的时间完成所有渲染阶段。

更优雅的解决方案是使用基于实际DOM状态的检测,而不是固定的时间延迟。你还可以考虑添加调试信息,观察DOM元素在不同时间点的内部结构变化,找出真正表示"完全就绪"的特征。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题