在JavaScript中检测变量何时更改

4
我有一个Chrome应用程序,我希望在分辨率更改时正确调整大小(尺寸与屏幕宽度成比例)。我编写了一个函数来使用正确的尺寸重新绘制应用程序,现在我想仅在需要时执行它。
分辨率更改会导致screen.width更改,但是(可能并不奇怪,因为它们涉及不同的事物)“resize”事件未被触发,并且据我所知没有任何事件被触发。
我知道Proxy对象(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy),因此我编写了一些代码来检测变量何时被设置并执行我的回调,这似乎有效,但在分辨率更改的情况下无效。
因此,我在网上搜索并尝试了这个https://gist.github.com/eligrey/384583(本质上是几个stackoverflow问题提供的答案,实际上是我最初提供的答案,尽管缺少新的Proxy对象提供的抽象)。
这似乎也可以工作(如果我手动设置了screen.width并在执行了screen.watch(“width”,()=> console.log(“hello”))之后进行screen.width = 100;,则会执行我的回调)。但在分辨率更改的情况下不起作用(实际上,最重要的是,分配此观察程序似乎会阻止分配screen.width)。
我有三个问题:
1)当我分配一个观察器/代理时,出了什么问题。
2)我如何找出浏览器在这个细节层面上正在做什么(是什么发送触发器来更改screen.width?我猜这是操作系统,看起来像什么)
3)是否有更好的方法来实现我最初想要的内容(Chrome应用程序调整大小)。
主要我对问题1)感兴趣,对3)不再关心。
要复制我的问题,
  • 在Firefox或Chrome中打开一个新标签页
  • 进入开发者控制台
  • 检查screen.width,更改分辨率,并观察到screen.width的变化
  • https://gist.github.com/eligrey/384583复制并粘贴代码
  • 执行screen.watch("width", (id, oldval, newval) => {console.log("hello"); return newval;});
  • 执行screen.width = 100; 并观察到hello被记录并且screen.width被设置为100
  • 更改分辨率并观察到screen.width未被设置

编辑 - 正如Bertrand的答案揭示的那样,调整大小事件实际上可能会在分辨率更改时触发,但这似乎是作为响应于分辨率更改而强制边界窗口变小的结果,如果窗口很小,则screen.width可以更改而不触发调整大小事件。


最简单的解决方案是使用定时器每10秒检查一次screen.width - georg
将事件监听器添加到窗口调整大小事件中不起作用?通常在Chrome中它可以正常工作。 - Mark Baijens
@georg 我考虑过这个方案,但不满意(我需要响应非常快)。我不太关心 Chrome 应用程序的原因是我已经有了备用计划,可以确保我所需的功能。 - Countingstuff
@Mark Baijens没错,我很满意window.addEventListener("resize", f)并且以前也用过它,但似乎分辨率更改不同于调整大小,我刚在Chrome上再次尝试了一下,但它没有起作用。如果相关的话,我正在使用Windows 10。 - Countingstuff
在Chrome浏览器中有Resize Observer - pmkro
@pmkro 谢谢,我之前不知道这个类,但是它似乎不能响应分辨率的变化。我刚刚使用了 const resizer = new ResizeObserver(() => console.log("hello")); resizer.observe(document.body);。现在调整窗口大小会输出 hello,但改变分辨率则不会。 - Countingstuff
2个回答

5

当我分配一个监视器/代理来搞砸事情时,发生了什么。

问题在于width属性实际上是Screen.prototype上的getter,而不是window.screen上的普通值:

console.log(window.screen.width);
console.log(window.screen.hasOwnProperty('width'));

// The property exists on the prototype instead, and is a getter:
const descriptor = Object.getOwnPropertyDescriptor(Screen.prototype, 'width');
console.log(descriptor);

如果width属性是一个普通属性,由浏览器通过eval设置一个简单的值组成。
window.screen.width = 1500

如果需要拦截对.width属性的分配操作,那么代理或Object.watch polyfill就适用了。这就是为什么在将自己的setter分配给window.screen.width后,你会发现:

执行screen.width = 100;并观察hello是否被记录,以及screen.width是否被设置为100。

当你调用你之前分配给windowscreen属性的setter时,它会显示hello。相反,因为内置属性不是通过浏览器赋值的普通值,所以当屏幕改变时,你的hello就不会被记录。这种情况与此示例片段中正在进行的情况有点相似:

const obj = (() => {
  let privateVal = 'propOriginal';
  // privateVal changes after 500ms:
  setTimeout(() => {
    console.log('Changing privateVal to propChanged');
    privateVal = 'propChanged';
  }, 500);
  
  // Return an object which has a getter, which returns privateProp:
  return {
    get privateProp() {
      return privateVal;
    }
  };
})();

// At this point, if one only has a reference to `obj`,
// there is no real way to watch for changes to privateVal:
console.log(obj.privateProp);

// Assigning a custom setter to obj.privateProp
// will only result in observing attempted assignments to obj.privateProp
// but will not be able to observe the change to privateVal:

Object.defineProperty(obj, 'privateProp', { set(newVal) {
  console.log('Observed change: new value is ' + newVal);
}});

setTimeout(() => {
  obj.privateProp = 1000;
}, 2500);

正如你所看到的,外部作用域中添加到 obj 的 setter 函数无法捕获对 privateVal 的更改。虽然与 window.screen 发生的情况不完全相同,但它们是类似的;您无法访问导致调用内置 getter 返回的值发生更改的底层代码结构。


screen 上的内置 getter 函数由本地代码组成,这意味着它不是普通的 JavaScript 函数。由本地代码组成的函数始终是浏览器(或代码运行的任何环境)提供的函数;它们通常无法通过自己编写的任何 JavaScript 函数来模拟。例如,只有 window.history 对象可以执行历史操作,如 history.back() ;如果覆盖了 window.history,并且您没有保存对其或任何其他 history 对象的引用,则无法编写自己的函数来执行 history.back(),因为 .back() 调用需要一个可见的 JavaScript 和浏览器引擎之间的接口,其中包含特权本机代码。

类似地,当调用内置的 window.screen getter 时,返回的值对于纯 JavaScript 来说是不直接可观测的。除了轮询之外,唯一其他观察更改的方法是监听 resize 事件,如其他答案中所述。 (类似于由 window.screen 返回的底层值,此 resize 事件由浏览器内部管理,否则不可观测。)

如果 resize 事件未作为响应分辨率更改而触发 - 例如,如果浏览器窗口已足够小,以至于不需要更改以获得较小的分辨率 - 则分辨率更改不会导致触发任何事件,这意味着除了轮询之外,不幸的是没有其他方法来监听它。(您可以尝试自己,看起来没有其他事件会在分辨率更改时触发)

可能有人可以编写(或调整)一个浏览器引擎,该引擎从操作系统中监听分辨率更改并在找到时发送 Javascript 事件,但这样做需要远远超出 JavaScript 的知识范围 - 随意浏览 Chromium 的源代码 获取详细信息,但它非常复杂,对于没有使用语言的人来说可能是不透明的。


非常好。非常感谢,这非常全面。谢谢您(间接)回答我的第二个问题,我希望得到更简单的东西,因为正如您所建议的那样,对于我这个没有做过任何c++的人来说,它相当晦涩,但我可能太天真了。 - Countingstuff

2
根据 Mozilla,屏幕大小调整事件只会在 window 对象上触发。因此,你应该添加监听器到 window 而不是 screen
window.addEventListener('resize', yourfunc)

在Windows 10下测试,使用Chrome非常顺畅。值得一提的是,浏览器计算实际屏幕大小时会同时考虑屏幕分辨率和缩放比例。
例如,如果你的分辨率是1920x1080,缩放比例为100%:
- 切换到3840x2160,缩放比例为200%会输出一个1920x1080的浏览器窗口,并且不会触发resize事件。 - 切换到150%的缩放比例会输出一个1280x720的浏览器窗口,并且会触发resize事件。
然而,这里有一个陷阱。只有当屏幕分辨率的更新实际上触发了浏览器窗口的调整大小时,才能检测到屏幕分辨率的更新。这并不是很明显,因为浏览器窗口的初始状态可能是窗口化/全屏模式,而屏幕大小的变化可能是变小或变大。

谢谢您的回复,这揭示了我没有意识到的一些微妙之处。所以我一直在做window.addEventListener('resize', yourfunc);就像你建议的那样,但它并没有起作用。我现在注意到它确实在某些情况下起作用,只是不是我考虑的那些情况。如果窗口足够大,使得改变分辨率会强制屏幕边界变小(例如全屏),则会触发调整大小事件(并且window.innerWidth会更改),但如果您的窗口较小,则不会触发调整大小事件,window.innerWidth不会更改,screen.width会更改。 - Countingstuff
确实。只有在浏览器窗口大小实际更新时,才会触发调整大小事件。我将其添加到答案中。 - Bertrand

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