0 tl;dr
如果程序的需求有切换页面时保存状态到localStorage的话,请确保自己的程序进程安全。浏览器似乎没有对localStorage加锁。
1 问题原因
这两天学react的时候用到了localStorage。恰逢又有有同学讲到了进程和线程。
一个问题从脑海中蹦了出来:多进程的浏览器如何管理这一块看起来像是“共享内存”的存储空间?
1.1 前置知识:什么是localStorage
简而言之:
- window的属性
- 能够长期存储在本地硬盘(关闭浏览器后还存在)
- 同一个域名下的页面都能够共享同一份localStorage
1.2 发现的问题:“共享内存”
现代浏览器每个标签页就是一个子进程。而同一个域名的页面都能访问到硬盘上的同一个对象,那么经典问题就来了:数据一致性如何保证?
2 分析
万事不决问谷歌。先看看有没有人和我想得差不多。
确实有人发现了这个问题:同步读写一致性无法保证。
回答的哥们儿去WHATWG的文档看了一圈发现语焉不详,标准并没有规范浏览器的具体实现。
而浏览器为了效率一般会保存一份缓存,这就会引起不一致。
3 验证
理论有了,那么着手验证一把。
3.1 实验设计
一个简单的思路是:页面获得焦点时,读取数据并展示;页面失去焦点时,重复多次写入数据以制造不一致的机会。
那么准备两个页面,这两个页面失焦时分别对同一个key写入不同的数据,激活时读取该key的值,验证读取和写入的情况是否一致。
3.2 代码
先express起个静态服务器:
// app.js
const express = require('express')
const app = express()
app.use(express.static('./'))
app.listen(3000, () => {
console.log('Server Listening at http://127.0.0.1:3000')
})
然后在同目录下,建两个html文档index.html
和page2.html
:
其中,index.html
从0-9999更新10000次数据库,写入一个对象,其值递增。
<!-- index.html -->
<body>
<div></div>
<script>
const div = document.querySelector('div')
document.addEventListener('visibilitychange', () => {
if(document.hidden){
for(let i = 0;i< 10000;i+=1){
localStorage.setItem('abkey', JSON.stringify({abk: i}))
}
}else{
const v = localStorage.getItem('abkey')
if(v) div.innerHTML = `本次数据为:${JSON.parse(v).abk},原始数据为:${v}`
else div.innerHTML = '暂未存储数据'
}
})
</script>
</body>
page2.html
从0000-29999更新10000次数据库,写入一个对象,其值递增。
<!-- page2.html -->
<body>
<div></div>
<script>
const div = document.querySelector('div')
document.addEventListener('visibilitychange', () => {
if(document.hidden){
for(let i = 20000;i< 30000;i+=1){
localStorage.setItem('abkey', JSON.stringify({abk: i}))
}
}else{
const v = localStorage.getItem('abkey')
if(v) div.innerHTML = `本次数据为:${JSON.parse(v).abk},原始数据为:${v}`
else div.innerHTML = '暂未存储数据'
}
})
</script>
</body>
3.3 实验结果
可见,localStorage并不存在一个写入锁去保护数据的一致性。(毕竟不是数据库)
4 总结
不应当意念式编程——程序员应当保证自己程序的正确性,而不是假设自己依赖的基础设施的行为和自己的想象相同。