这是针对您的问题的量身定制的答案。为什么你不能绕过异步,它仍然会是 90% 长的解释,但请耐心等待 - 它通常会对你有所帮助。我保证最后会有一些相关的东西chrome.storage
。
在我们开始之前,我将重申规范链接:
那么,让我们来讨论一下 JS 异步性。
第 1 部分:它是什么?
要涵盖的第一个概念是运行时环境。在某种程度上,JavaScript 被嵌入到另一个控制其执行流程的程序中——在本例中是 Chrome。发生的所有事件(计时器、点击等)都来自运行时环境。JavaScript 代码为事件注册处理程序,这些处理程序由运行时记住并在适当时调用。
其次,理解 JavaScript 是单线程的很重要。运行时环境维护一个单独的事件循环;如果在某个事件发生时还有其他一些代码在执行,则该事件会被放入队列以在当前代码终止时进行处理。
看看这段代码:
var clicks = 0;
someCode();
element.addEventListener("click", function(e) {
console.log("Oh hey, I'm clicked!");
clicks += 1;
});
someMoreCode();
那么,这里发生了什么?当这段代码执行时,当执行到达 时.addEventListener
,会发生以下情况:运行时环境被通知当事件发生(element
被点击)时,它应该调用处理程序函数。
重要的是要了解(尽管在这种特殊情况下很明显)该函数此时未运行。它只会稍后运行,当该事件发生时。一旦运行时确认“我将运行(或“回调”,因此名称为“回调”)发生这种情况时,执行就会继续。如果someMoreCode()
尝试访问clicks
,它将是0
,而不是1
。
这就是所谓的异步性,因为这是在当前执行流程之外发生的事情。
第 2 部分:为什么需要它,或者为什么同步 API 正在消失?
现在,一个重要的考虑因素。假设这someMoreCode()
实际上是一段运行时间很长的代码。如果在它仍在运行时发生点击事件会发生什么?
JavaScript 没有中断的概念。运行时会看到有代码在执行,并将事件处理程序调用放入队列中。处理程序在someMoreCode()
完全完成之前不会执行。
虽然单击事件处理程序在不能保证单击发生的意义上是极端的,但这解释了为什么您不能等待异步操作的结果。这是一个不起作用的例子:
element.addEventListener("click", function(e) {
console.log("Oh hey, I'm clicked!");
clicks += 1;
});
while(1) {
if(clicks > 0) {
console.log("Oh, hey, we clicked indeed!");
break;
}
}
你可以点击你的心的内容,但会增加的代码clicks
耐心地等待(非终止)循环终止。oop。
请注意,这段代码不仅冻结了这段代码:在我们等待时不再处理每个事件,因为只有一个事件队列/线程。JavaScript 中只有一种方法可以让其他处理程序完成它们的工作:终止当前代码,并让运行时知道在发生我们想要的事情时调用什么。
这就是为什么将异步处理应用于另一类调用的原因:
- 需要运行时而不是 JS 来做某事(例如磁盘/网络访问)
- 保证终止(无论成功或失败)
让我们来看一个经典的例子:AJAX 调用。假设我们要从 URL 加载文件。
- 假设在我们当前的连接上,运行时可以在 100 毫秒内以 JS 中可以使用的形式请求、下载和处理文件。
- 在另一个连接上,这有点糟糕,需要 500 毫秒。
- 有时连接真的很糟糕,所以运行时会等待 1000 毫秒并超时放弃。
如果我们要等到这完成,我们就会有一个可变的、不可预测的和相对较长的延迟。由于 JS 等待的工作方式,所有其他处理程序(例如 UI)不会在此延迟期间完成其工作,从而导致页面冻结。
听起来很熟悉?是的,这正是同步 XMLHttpRequest 的工作方式。它不是while(1)
JS 代码中的循环,而是本质上发生在运行时代码中——因为 JavaScript 在等待时不能让其他代码执行。
是的,这允许使用熟悉的代码形式:
var file = get("http://example.com/cat_video.mp4");
但是一切都冻结了,代价是可怕的、可怕的。成本如此可怕,事实上,现代浏览器认为这已被弃用。这是关于 MDN 主题的讨论。
现在让我们看看localStorage
。它符合“终止对运行时的调用”的描述,但它是同步的。为什么?
简单地说:历史原因(这是一个非常古老的规范)。
虽然它肯定比网络请求更可预测,localStorage
但仍然需要以下链:
JS code <-> Runtime <-> Storage DB <-> Cache <-> File storage on disk
这是一个复杂的事件链,需要暂停整个 JS 引擎。这导致了被认为是不可接受的性能。
现在,Chrome API 从头开始就是为性能而设计的。您仍然可以在较旧的API 中看到一些同步调用,例如chrome.extension
,并且有些调用是在 JS 中处理的(因此作为同步有意义)但chrome.storage
(相对)是新的。
因此,如果在运行时执行某些操作时会出现延迟,则它采用范式“我确认您的调用并将返回结果,现在同时做一些有用的事情”。与 XMLHttpRequest 不同,这些调用没有同步版本。
引用文档:
它 [ chrome.storage
] 与批量读取和写入操作异步,因此比阻塞和串行 localStorage
API更快。
第 3 部分:如何接受异步性?
处理异步性的经典方法是回调链。
假设您有以下同步代码:
var result = doSomething();
doSomethingElse(result);
假设现在doSomething
是异步的。然后就变成了:
doSomething(function(result) {
doSomethingElse(result);
});
但如果它更复杂呢?说是:
function doABunchOfThings() {
var intermediate = doSomething();
return doSomethingElse(intermediate);
}
if (doABunchOfThings() == 42) {
andNowForSomethingCompletelyDifferent()
}
嗯.. 在这种情况下,您需要在回调中移动所有这些。return
必须改为调用。
function doABunchOfThings(callback) {
doSomething(function(intermediate) {
callback(doSomethingElse(intermediate));
});
}
doABunchOfThings(function(result) {
if (result == 42) {
andNowForSomethingCompletelyDifferent();
}
});
这里有一系列回调:立即doABunchOfThings
调用doSomething
,终止,但稍后调用doSomethingElse
,其结果if
通过另一个回调提供。
显然,这种分层会变得混乱。好吧,没有人说 JavaScript 是一门好语言.. 欢迎来到回调地狱。
有一些工具可以使其更易于管理,例如Promises和async
/await
。我不会在这里讨论它们(空间不足),但它们不会改变基本的“此代码只会稍后运行”部分。
TL;DR 部分:我绝对必须有存储同步,呸!
有时有正当理由需要同步存储。例如,webRequest
API 阻塞调用不能等待。或者回调地狱会让你付出惨痛的代价。
你可以做的是有一个同步缓存异步的chrome.storage
。它会带来一些成本,但这并非不可能。
考虑:
var storageCache = {};
chrome.storage.sync.get(null, function(data) {
storageCache = data;
// Now you have a synchronous snapshot!
});
// Not HERE, though, not until "inner" code runs
如果您可以将所有初始化代码放在一个函数中init()
,那么您将拥有:
var storageCache = {};
chrome.storage.sync.get(null, function(data) {
storageCache = data;
init(); // All your code is contained here, or executes later that this
});
到代码init()
执行时,然后当任何分配给处理程序的事件init()
发生时,storageCache
将被填充。您已将异步性降低到 ONE 回调。
当然,这只是在执行时存储看起来的快照get()
。如果你想保持与存储的一致性,你需要设置更新到storageCache
via chrome.storage.onChanged
events。由于 JS 的单事件循环特性,这意味着缓存只会在您的代码不运行时更新,但在许多情况下这是可以接受的。
同样,如果要将更改传播到storageCache
实际存储,仅设置storageCache['key']
是不够的。您需要编写一个set(key, value)
shim,既写入storageCache
又调度 (asynchronous) chrome.storage.sync.set
。
实现这些留作练习。