当我们在浏览器中创建新选项卡时,将创建一个新的浏览上下文。 浏览上下文聚合了许多我们认为理所当然的东西:诸如会话和历史记录管理,导航和资源加载之类的东西。 我们还有一个事件循环,用于处理脚本和DOM之间的交互。
在本教程中,我们将首先对事件循环有基本的了解,该事件循环主要是根据反应堆模式设计的。 然后,我们看一些与浏览上下文关联的辅助对象。 最后,我们将列出最佳实践列表,以尽可能有效地利用事件循环。
反应堆模式
每个浏览上下文都带有关联的事件循环。 事件循环绑定到上下文当前文档的window 。 可以与承载相同来源document另一个window共享它。
事件循环以规范化的方式处理正在运行的事件,即,它们无法交错或并行执行。 例如,即使Worker允许我们并行运行其他一些JavaScript,我们也无法直接与DOM或核心JavaScript进行交互。 我们需要发送通过事件处理的消息。 这些事件由事件循环处理,因此将按顺序运行。
大多数事件循环都是以某种反应堆模式实现的。 原因之一是所有反应堆系统在设计上都是单线程的,但是当然可以存在于多线程环境中。 反应器的结构如下:
- 我们有资源(表示为句柄,即引用,例如JavaScript回调)。
- 一个多路分解器,以规范化运行事件。
- 调度程序,注册/注销处理程序并调度事件。
- 当然,我们还需要事件处理程序来使用句柄引用的资源。
多路分解器尽快将句柄发送到调度程序,即在所有先前事件都已处理之后。 在内部,多路分解器可能具有某种队列,该队列使用先进先出(FIFO)原理进行操作。 实际的实现可能使用某种优先级队列。
下图描绘了反应堆模式的不同组件之间的关系。
在浏览器中运行JavaScript的情况下,句柄是JavaScript函数,可能捕获了一些对象。 事件处理程序是DOM事件处理程序。 反应器允许代码同时运行,而不会出现任何潜在的跨线程问题。
有多个调度程序,例如,因为每个EventTarget都可以注册,注销和调度事件。 但是,每个事件循环只有一个分配的多路分解器。
浏览事件循环
既然我们对反应堆模式及其在事件循环中的实现有了模糊的了解,我们需要从交互的角度仔细研究事件循环,在我们的例子中就是JavaScript。
每次我们使用JavaScript进行函数调用时,运行时都会将一个框架添加到所谓的调用堆栈中。 堆栈具有后进先出(LIFO)结构。 调用堆栈跟踪调用路径。 框架由函数的参数及其局部状态(由当前正在运行的指令定义)以及所有局部变量及其相应值组成。
为了说明这个概念,让我们考虑以下代码。
function init() {
setTimeout(function () {}, 0);
}
init();
如果现在在每次更改后保存调用堆栈,则最终得到以下图片。 我们从一个空的调用栈开始。 调用init函数后,我们在堆栈上有了一个元素。 setTimeout调用为我们提供了另一个元素,即使函数本身只会使上下文切换到本机函数也是如此。 返回到init方法后,我们在堆栈上只剩下一个元素。 最后,堆栈再次为空。
在这一点上,区分JavaScript中的两种触发事件模式开始变得有意义:同步和异步。 可以通过以下示例代码说明差异。 我们使用一个异步事件( click )触发器来运行一个同步( focus )触发器。 需要注意的是click也可以以同步的方式,通过使用触发click()方法。
var button = document.querySelector('button');
var text = document.querySelector('input[type=text]');
button.addEventListener('click', function () {
console.log('foo');
text.focus();
console.log('bar');
}, false);
text.addEventListener('focus', function () {
console.log('baz');
}, false);
我们将看到foo , baz和bar将被记录。 因此,在调用focus()之后立即处理了focus事件。 还有一些事件(例如DOM突变事件)总是同步触发的。 我们将在本系列的第八篇文章中讨论DOM突变事件。
浏览器中事件循环的事件多路分解器实现必须区分运行的普通事件回调(称为Tasks )和首选的较小代码块(称为Microtasks) 。
当任务以常规方式入队时,微任务以高优先级入队。 它们将尽快执行,即始终在下一个任务之前执行。 大多数DOM交互作为任务进入事件循环,例如,调用setTimeout的回调函数。 另一方面,新的Promise类型的回调作为微任务被调用。
使微任务排队的API的另一个示例是MutationObserver接口。 目前,我们将MutationObserver的讨论推迟到本系列的第八篇文章中。
以下示例说明了普通任务和微任务之间的区别。
function init () {
console.log('foo');
setTimeout(later, 0);
now();
Promise.resolve().then(soon);
console.log('bar');
}
function later () {
console.log('baz');
}
function soon () {
console.log('norf');
}
function now () {
console.log('qux');
}
init();
我们将看到foo , qux , bar , norf和baz被记录。 取决于浏览器, norf和baz的顺序可能是错误的,但应与显示的顺序相同 。 在浏览器的控制台中运行先前的示例代码也会产生有趣的结果。
例如,下面显示了最新版本的Opera的结果。
执行Promise的回调后,将显示init函数的结果( undefined )。 因此,浏览器的调试器工具是使用常规任务而不是微任务集成的。
尽管浏览器可能会使用正常任务之间的时间来执行固有步骤(例如发出渲染调用),但微任务将在当前代码运行完成后立即运行。
会话和历史记录管理
浏览上下文包含更多服务。 这些服务大多数通常不可访问,但是其中一些可能会公开我们可以使用的API。 例如,某些基于硬件的服务在navigator对象中公开。
此外,我们可以通过history对象访问(本地)历史。 在这里,我们找到几种有用的方法。 可能最有用的一个是pushState() 。 它包含三个参数:
- 要推送的条目的状态。 这必须是一个字符串。 我们可以使用JSON数据结构。
- 标题供我们参考。 大多数浏览器都不使用此参数,因此我们可以完全省略此参数。
- 最后显示一个URL。 浏览器通常会将其显示在位置栏中。
让我们来看一个例子。
history.pushState(null, null, 'https://www.example.com/my-state');
调用pushState将立即更改显示的URL。 当用户按下无所不在的后退按钮时会发生什么? 一般来说,由浏览器决定,但是一个好的决定是弹出当前状态。
前进或后退操作也通过history对象公开。 例如,我们可以执行以下操作:
history.back();
history.forward();
调用back编程方式或通过用户界面可能导致回到前一页。 如果堆栈已经为空,则无法弹出当前状态。 这也可以通过简短的演示来体验,该演示包含两个按钮和一个列表。
触发逻辑的示例代码如下。
var list = document.querySelector('ul');
var buttons = document.querySelectorAll('button');
var back = buttons[0];
var forward = buttons[1];
back.addEventListener('click', function (ev) {
history.back();
}, false);
forward.addEventListener('click', function (ev) {
var c = list.childElementCount.toString();
var url = 'foo-' + c;
history.pushState(c, null, url);
var item = document.createElement('li');
item.textContent = url;
list.appendChild(item);
}, false);
window.addEventListener('popstate', function (ev) {
list.removeChild(list.children[ev.state * 1]);
}, false);
历史API是在单页应用程序(SPA)中实现路由的一种好方法。 仍然可以使用URL进行路由的另一种方法是通过处理URL的哈希并侦听hashchange事件。 同样,我们使用事件来异步触发回调。
最佳实践
事件循环的缺点是Web应用程序高度依赖于当前活动任务或微任务的处理时间。 因此,我们无法加入长时间运行的计算。 浏览器将在一段时间后停止计算。 规避此问题的一个好习惯是将计算分为几个部分,由事件循环依次处理。
显示事件循环的简单方案如下所示。 循环自由旋转,直到任务正在等待运行。 如果我们认为任务与JavaScript有关,则需要将控件传递给JavaScript引擎。 最后,代码可以注册更多的回调。 如果触发了事件,则将所有回调作为要运行的任务放入事件循环中。
在浏览器中,只要关联事件发生,回调都会排队。 没有回调的事件不会排队。
调用setTimeout()至少等待提供的时间,然后将指定的回调放入队列。 如果队列中没有其他任务,则回调将立即被调用,否则我们必须等待。 这就是为什么setTimeout的第二个参数定义最小时间而不是保证时间的原因。
长时间运行的计算应放置在Worker , Worker提供其自己的事件循环。 它还包含自己的内存管理。 因此,由于通过事件进行通信,因此Web工作人员将不会干扰我们的Web应用程序的事件循环。 整个方案是非阻塞的。
通常,我们应该始终避免使用阻塞代码。 直接使用回调或事件,大多数API已经以非阻塞方式公开。 不幸的是,存在遗留异常,例如alert或同步XHR。 除非我们确切知道我们在做什么,否则切勿使用它们。
那么我们应该使用任务还是微任务? Promise API使用微任务是有原因的。 如果可能的话,我们可能会为此目标。 尽快执行微任务,实际上是在执行当前代码之后立即执行。 因此可以防止不必要的渲染。 为什么在插入结果之前渲染一次,这将导致需要进行另一次渲染操作? 但是,如果有疑问,我们应该使用标准任务将控件传递回浏览器。
结论
全面了解JavaScript的工作方式以及它与DOM和其他资源的交互方式非常重要。 这从事件循环开始。 该概念不仅在浏览器中实现,而且还存在于Node.js的核心中。 掌握JavaScript意味着掌握事件循环。
在上一教程中,我们看到浏览器为我们提供了几种机制,这些机制聚集在浏览上下文中并在几个点公开。 即使我们无法控制某些内部机制,我们仍然应该知道它们的存在以及它们的工作方式。
翻译自: https://code.tutsplus.com/tutorials/html5-mastery-the-browsing-context--cms-24845

被折叠的 条评论
为什么被折叠?



