按哪个顺序接收storage个事件?

例如,TAB_1执行代码

localStorage.set('key', 1);
localStorage.set('key', 2);

Tab_2执行代码

localStorage.set('key', 3);
localStorage.set('key', 4);

它们同时执行代码.

问题:

  • %2可以存储在%1之前吗?
  • 3或4可以存储在1和2之间吗?
  • tab_3是否可以接收storage个与值存储顺序不同的事件?(例如,如果我们存储3,4,1,2,我们可以接收1,2,3,4,甚至以随机顺序如4,1,2,3)

找到答案:

  • 目前,所有的问题都是否定的,但标准并不明确这种行为,所以这种行为可能会改变.你可以通过在控制台中的一些页面上运行这个代码来判断这个行为是否仍然相同,比如https://www.google.com(浏览器一开始可能会拒绝打开太多的标签,并可能会询问它是否可以,所以你可能需要允许打开测试页面的许多标签,比如第https://www.google.com页)
// We want to catch possible races, hence
// `testKey` is as short as possible to make it faster to process
// and increase operations per second and chance of race
const testKey = "0";
// Removing `testKey`, so that previous tests didn't interfere with this one,
// even if we didn't finish running them
localStorage.removeItem(testKey);

const runCount = 10_000;
// We want to catch possible races.
// To increase chance of race, we want as many parallel threads as there are CPU-s,
// because when there are more than 1 thread per CPU core,
// CPU will spend time not on racing code logic, but CPU context switch
const cpuCount = navigator.hardwareConcurrency;
const expectedTotalRunCount = cpuCount * runCount;
let totalRunCount = 0;
const storageEventListInOrderOfReceive = [];
window.addEventListener("storage", (storageEvent) => {
  storageEventListInOrderOfReceive.push(storageEvent);

  // Checking if we processed all events, caused by our test,
  // and if we did, running assertions
  if (
    storageEvent.key === testKey &&
    ++totalRunCount === expectedTotalRunCount
  ) {
    const changeRecordList = storageEventListInOrderOfReceive.map(
      (storageEvent) => {
        let oldTabNumber;
        let oldRunNumber;
        if (storageEvent.oldValue !== null) {
          const oldTabNumberAndRunNumber = storageEvent.oldValue.split(",");
          oldTabNumber = Number.parseInt(oldTabNumberAndRunNumber[0]);
          oldRunNumber = Number.parseInt(oldTabNumberAndRunNumber[1]);
        } else {
          oldTabNumber = null;
          oldRunNumber = null;
        }

        const newTabNumberAndRunNumber = storageEvent.newValue.split(",");
        const newTabNumber = Number.parseInt(newTabNumberAndRunNumber[0]);
        const newRunNumber = Number.parseInt(newTabNumberAndRunNumber[1]);

        return {
          oldTabNumber,
          newTabNumber,
          oldRunNumber,
          newRunNumber,
        };
      },
    );
    console.log(changeRecordList);

    let areEventsSequential = true;
    for (let i = 0; i < changeRecordList.length - 1; i++) {
      const currentChangeRecord = changeRecordList[i];
      const nextChangeRecord = changeRecordList[i + 1];

      let shouldLog = false;
      if (currentChangeRecord.newTabNumber !== nextChangeRecord.oldTabNumber) {
        areEventsSequential = false;
        shouldLog = true;
        console.error(
          `currentChangeRecord.newTabNumber !== nextChangeRecord.oldTabNumber`,
        );
      }
      if (currentChangeRecord.newRunNumber !== nextChangeRecord.oldRunNumber) {
        areEventsSequential = false;
        shouldLog = true;
        console.error(
          `currentChangeRecord.newRunNumber !== nextChangeRecord.oldRunNumber`,
        );
      }

      if (currentChangeRecord.newRunNumber !== runCount) {
        if (
          currentChangeRecord.newTabNumber !== nextChangeRecord.newTabNumber
        ) {
          areEventsSequential = false;
          shouldLog = true;
          console.error(
            `currentChangeRecord.newTabNumber !== nextChangeRecord.newTabNumber`,
          );
        }
        if (
          currentChangeRecord.newRunNumber !==
          nextChangeRecord.newRunNumber - 1
        ) {
          areEventsSequential = false;
          shouldLog = true;
          console.error(
            `currentChangeRecord.newRunNumber !== nextChangeRecord.newRunNumber - 1`,
          );
        }
      } else {
        if (
          currentChangeRecord.newTabNumber === nextChangeRecord.newTabNumber
        ) {
          areEventsSequential = false;
          shouldLog = true;
          console.error(
            `currentChangeRecord.newTabNumber === nextChangeRecord.newTabNumber`,
          );
        }
        if (nextChangeRecord.newRunNumber !== 1) {
          areEventsSequential = false;
          shouldLog = true;
          console.error(`nextChangeRecord.newRunNumber !== 1`);
        }
      }

      if (shouldLog) {
        console.error(
          "currentIndex:",
          i,
          "currentChangeRecord:",
          currentChangeRecord,
          "nextChangeRecord:",
          nextChangeRecord,
        );
      }
    }

    if (areEventsSequential) {
      console.log("Are events sequential:", areEventsSequential);
    } else {
      console.error("Are events sequential:", areEventsSequential);
    }

    // Cleaning up `testKey`
    localStorage.removeItem(testKey);
  }
});

for (let cpuIndex = 0; cpuIndex < cpuCount; cpuIndex++) {
  const tab = window.open(location.href);
  const scriptElement = document.createElement("script");
  scriptElement.textContent = `
const startParallelExecutionBroadcastChannel = new BroadcastChannel('${testKey}');
startParallelExecutionBroadcastChannel.onmessage = () => {
  // To be sure that nothing is synchronized by some properties of \`BroadcastChannel\`,
  // we run this logic as last item in event loop
  setTimeout(() => {
    for(let runNumber = 1; runNumber < ${runCount + 1}; runNumber++) {
      localStorage.setItem('${testKey}', \`${cpuIndex + 1},$\{runNumber}\`)
    }
    // Closing current tab to not do it manually
    close();
  }, 0);
};
`;
  tab.document.head.appendChild(scriptElement);
}

const startParallelExecutionBroadcastChannel = new BroadcastChannel(testKey);
startParallelExecutionBroadcastChannel.postMessage(undefined);

console.log("Broadcasted `startParallelExecutionBroadcastChannel`");

推荐答案

JavaScript是单线程的(在没有Web Worker的情况下,他们不通过共享内存进行通信),浏览器选项卡中的处理顺序定义得很好.

至于跨选项卡订购,SPEC cautions:

本地存储获取方法提供对共享状态的访问.本规范没有定义在多进程用户代理中与其他代理集群的交互,建议作者假设没有锁定机制.例如,站点可以try 读取键的值,递增其值,然后将其写回,使用新值作为会话的唯一标识符;如果站点同时在两个不同的浏览器窗口中执行此操作两次,则可能最终为两个会话使用相同的"唯一"标识符,这可能会带来灾难性的影响.

不幸的是,这里没有定义"锁定机构"的确切含义.该示例说明,localStorage键的值可能随时更改,即使在执行一个JavaScript线程时也是如此.也就是说,如果一个线程先读后写,则另一个线程可能已在此期间写入.它没有明确描述是否保证新值以相同的顺序显示在所有窗口中.

HTML Spec Issue 403要求更好地指定localStorage的并发行为,但目前还没有定义解决方案...

Implementation in Current Browsers

Earlier versions of the WebStorage spec需要localStorage个实现来使用Mutex,当前版本的Chrome和Firefox仍然这样做,如以下测试所示:

在第一个浏览器窗口中:

function test() {
    console.log("polling for changes");
    let x = localStorage.test;
    for (let i = 0; i < 100000000; i++) {
        let y = localStorage.test;
        if (x != y) {
            console.log("seeing new value", y);
            x = y;
        }
        if (i % 10000000 == 0) {
            let n = parseInt(x) + 1;
            console.log("setting new value", n);
            localStorage.test = n;
        }
    }
}
window.onstorage = e => console.log("seeing storage event", e);
localStorage.test = 0;
test();

在执行该操作时,在第二个浏览器窗口中:

window.onstorage = e => console.log("seeing storage event", e);
localStorage.test = parseInt(localStorage.test) + 20;

Chrome和Firefox格式的输出:

polling for changes
setting new value 1
seeing new value 1
setting new value 2
seeing new value 2
setting new value 3
seeing new value 3
setting new value 4
seeing new value 4
setting new value 5
seeing new value 5
setting new value 6
seeing new value 6
setting new value 7
seeing new value 7
setting new value 8
seeing new value 8
setting new value 9
seeing new value 9
setting new value 10
seeing new value 10
undefined
seeing storage event StorageEvent {isTrusted: true, key: 'test', oldValue: '10', newValue: '30', …}

也就是说,在这些浏览器的当前版本中,访问localStorage的代码的执行被延迟,直到获得互斥体,导致JavaScript线程按顺序执行(而不是并行执行,因为规范语言鼓励我们假设).

Javascript相关问答推荐

我无法在NightWatch.js测试中获取完整的Chrome浏览器控制台日志(log)

我在这个黑暗模式按钮上做错了什么?

从PWA中的内部存储读取文件

react—router v6:路由没有路径

Prisma具有至少一个值的多对多关系

使用Promise.All并发解决时,每个promise 的线性时间增加?

当用户点击保存按钮时,如何实现任务的更改?

Puppeteer上每页的useProxy返回的不是函数/构造函数

变量在导入到Vite中的另一个js文件时成为常量.

将基元传递给THEN处理程序

基于产品ID更新条带产品图像的JavaScript命中错误

P5.js中的分形树

Node.js错误: node 不可单击或不是元素

在JavaScript中,有没有一种方法可以迭代字符串的词法标记?

如何让SVG图标在被点击和访问后改变 colored颜色 ,并在被访问后取消点击时恢复到原来的 colored颜色 ?

在高位图中显示每个y轴系列的多个值

Promise.race()返回已解析的promise ,而不是第一个被拒绝的promise

在传单的图像覆盖中重新着色特定 colored颜色 的所有像素

在点击链接后重定向至url之前暂停

单击时同时 Select 和展开可访问的行