用户操作需要 JS 事件处理程序的参与
用户操作可以触发 Javascript 事件(点击、焦点事件、按键事件等),这些事件参与并可能影响用户操作,因此很明显,在处理用户操作时,单个 JS 线程无法执行,因为如果是这样,那么 JS 线程就无法参与用户操作,因为它已经在做其他事情了。因此,在 JS 线程可参与该过程之前,浏览器不会处理默认的用户操作。
渲染
渲染比较复杂。典型的 DOM 修改顺序是这样的:1) DOM 被 JS 修改,布局标记为脏,2) JS 线程完成执行,因此浏览器现在知道 JS 已完成修改 DOM,3) 浏览器进行布局以重新布局更改的 DOM,4 )浏览器根据需要绘制屏幕。
步骤 2) 在这里很重要。如果浏览器在每次 JS DOM 修改后都进行新的布局和屏幕绘制,并且 JS 实际上要进行大量 DOM 修改,那么整个过程的效率可能会非常低。另外,还会存在线程同步问题,因为如果您在浏览器尝试重新布局和重绘的同时让 JS 修改 DOM,则必须同步该活动(例如,阻止某人,以便操作可以在不发生任何操作的情况下完成)底层数据被另一个线程更改)。
仅供参考,有一些解决方法可用于强制重新布局或强制从 JS 代码内重新绘制(不完全是您所要求的,但在某些情况下很有用)。
多线程访问 DOM 非常复杂
DOM 本质上是一个大的共享数据结构。浏览器在解析页面时构造它。然后加载脚本和各种JS事件就有机会修改它。
如果突然有多个 JS 线程可以访问 DOM,并且同时运行,那么您将遇到一个非常复杂的问题。您将如何同步访问?您甚至无法编写最基本的 DOM 操作,其中涉及在页面中查找 DOM 对象然后修改它,因为这不是原子操作。在您找到 DOM 对象和进行修改之间,DOM 可能会发生变化。相反,您可能必须获取 DOM 中至少一个子树的锁,以防止在您操作或搜索它时它被其他线程更改。然后,在进行修改后,您必须释放锁并从代码中释放有关 DOM 状态的任何知识(因为一旦释放锁,其他线程可能会更改它)。而且,如果你没有正确地做事,你可能会陷入僵局或各种令人讨厌的错误。实际上,您必须将 DOM 视为并发的多用户数据存储。这将是一个更加复杂的编程模型。
避免复杂性
“单线程 JS”设计决策中有一个统一的主题。保持事情简单。不需要了解多线程环境和线程同步工具以及多线程调试即可编写可靠的浏览器 JavaScript。
浏览器 Javascript 是一个成功平台的原因之一是,所有级别的开发人员都可以轻松访问它,并且相对容易学习和编写可靠的代码。虽然随着时间的推移,浏览器 JS 可能会获得更高级的功能(就像我们使用 WebWorkers 一样),但您可以绝对确定,这些功能将以简单的事情保持简单的方式完成,而更高级的事情可以由更高级的开发人员完成,但不需要打破现在让事情变得简单的任何事情。
仅供参考,我已经用 Node.js 编写了一个多用户 Web 服务器应用程序,并且由于 Nodejs Javascript 的单线程特性,服务器设计的复杂程度大大降低,我一直对此感到惊讶。是的,有一些东西写起来比较麻烦(学习编写大量异步代码的承诺),但是哇,你的 JS 代码永远不会被另一个请求中断的简化假设大大简化了设计、测试并减少了很难发现和修复并发设计和编码中总是充满的错误。
讨论
当然,第一个问题可以通过允许用户操作事件处理程序在自己的线程中运行以便它们随时发生来解决。但是,你立刻就拥有了多线程 Javascript,现在需要一个全新的 JS 基础设施来进行线程同步和全新的 bug 类别。 Javascript 浏览器的设计者始终决定不打开这个盒子。
如果需要,渲染问题可以得到改善,但浏览器代码会变得非常复杂。你必须发明某种方法来猜测正在运行的 JS 代码何时似乎不再更改 DOM(也许经过了一些毫秒而没有更多更改),因为你必须避免立即进行重新布局和屏幕绘制每次 DOM 更改。如果浏览器这样做,一些 JS 操作将比现在慢 100 倍(100 倍是一个疯狂的猜测,但重点是它们会慢很多)。而且,您必须在布局、绘制和 JS DOM 修改之间实现线程同步,这是可行的,但很复杂,需要大量工作,并且为浏览器实现错误提供了肥沃的土壤。而且,您必须决定当您在重新布局或重绘过程中并且 JS 线程进行 DOM 修改时要做什么(没有一个答案很好)。