localStorage 是同步的,其设计初衷是为了简化 API 并适应早期的 Web 应用场景。尽管底层硬盘 IO 本质上是异步的,但浏览器通过阻塞 JavaScript 线程实现了同步行为。对于需要存储大量数据或避免阻塞主线程的场景,建议使用异步的 IndexedDB。
一、为什么会有这样的问题?
localStorage 是 Web Storage API 的一部分,它提供了一种存储键值对的机制。它的数据是持久存储在用户的硬盘上的,而不是内存中。这意味着即使用户关闭浏览器或电脑,数据也不会丢失,除非主动清除浏览器缓存或使用代码删除。
当你通过 JavaScript 访问 localStorage 时,浏览器会从硬盘中读取数据或向硬盘写入数据。虽然读写操作期间,数据可能会被暂时存放在内存中以提高处理速度,但其主要特性是持久性,并且不依赖于会话。
二、硬盘是 IO 设备,IO 读取不都是异步的吗?
是的,硬盘确实是 IO 设备,大部分与硬盘相关的操作系统级 IO 操作是异步进行的,以避免阻塞进程。但在 Web 浏览器环境中,localStorage 的 API 被设计为同步的,即使底层的硬盘读写操作具有 IO 特性。
JavaScript 代码在访问 localStorage 时,浏览器提供的 API 通常会在 js 执行线程上下文中直接调用。这意味着尽管硬盘需要等待数据读取或写入完成,localStorage 的读写操作是同步的,会阻塞 JavaScript 线程直到操作完成。
三、完整操作流程
localStorage 实现同步存储的方式是阻塞 JavaScript 的执行,直到数据的读取或写入操作完成。这种同步操作的实现可以简单概述如下:
-
js线程调用 :当 JavaScript 代码执行一个
localStorage的操作,比如localStorage.getItem('key')或localStorage.setItem('key', 'value'),这个调用发生在 js 的单个线程上。 -
浏览器引擎处理:浏览器的 js 引擎接收到调用请求后,会向浏览器的存储器子系统发出同步 IO 请求。此时 js 引擎等待 IO 操作的完成。
-
文件系统的同步 IO:浏览器存储器子系统对硬盘执行实际的存储或检索操作。尽管操作系统层面可能对文件访问进行缓存或优化,但从浏览器的角度看,它会进行一个同步的文件系统操作,直到这个操作返回结果。
-
操作完成返回:一旦 IO 操作完成,数据要么被写入硬盘,要么被从硬盘读取出来,浏览器存储器子系统会将结果返回给 js 引擎。
-
JavaScript 线程继续执行:js 引擎在接收到操作完成的信号后,才会继续执行下一条 js 代码。
四、为什么 localStorage 被设计为同步的?
-
历史原因:localStorage 是在早期 Web 标准中引入的,当时的 Web 应用相对简单,对异步操作的需求并不强烈。
-
API 简洁性:同步 API 更易于理解和使用,开发者无需处理回调或 Promise,代码更直观。
-
数据量小:localStorage 设计用于存储少量数据(通常为 5MB 左右),同步操作在数据量较小时对性能影响不大。
-
兼容性考虑:保持同步行为有助于兼容旧代码和旧浏览器。
-
浏览器政策:浏览器厂商可能出于提供一致用户体验或方便管理用户数据的角度,选择保持其同步特性。
五、那 IndexedDB 会造成滥用吗?
虽然 IndexedDB 提供了更大的存储空间和更丰富的功能,但潜在地也可能被滥用。不过,相比 localStorage,它增加了一些特性来降低被滥用的风险:
-
异步操作:IndexedDB 是异步 API,即使处理更大的数据也不会阻塞主线程,避免对页面响应性的直接影响。
-
用户提示和权限:某些浏览器在网站尝试存储大量数据时,可能会弹出提示要求用户授权,使用户有机会拒绝超出合理范围的存储请求。
-
存储配额和限制:尽管 IndexedDB 提供的存储容量比 localStorage 大得多,但它也不是无限的。浏览器会设定一定的存储配额,超出时拒绝更多的存储请求。
-
更清晰的存储管理:IndexedDB 的数据库形式允许有组织的存储和更容易的数据管理,用户或开发者可以更容易地查看和清理占用的数据。
-
逐渐增加的存储:某些浏览器在数据库大小增长到一定阈值时,可能会提示用户是否允许继续存储,而不是一开始就分配很大的空间。
六、一个简单测试例子
平时编写代码时,我们并没有以异步的方式使用 localStorage。以下是一个简单的测试示例:
xml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
const testLocalStorage = () => {
console.log("==========> 设置 localStorage 之前");
localStorage.setItem('testLocalStorage', '我是同步的');
console.log("==========> 获取 localStorage 之前");
console.log('==========> 获取 localStorage', localStorage.getItem('testLocalStorage'));
console.log("==========> 获取 localStorage 之后");
}
testLocalStorage();
</script>
</body>
</html>
运行上述代码,你会发现日志输出是顺序执行的,验证了 localStorage 的同步特性。