介绍
和Atomics.waitAsync对应的方法是Atomics.wait,它们都是在等待SharedArrayBuffer所代表的共享内存的某个位置的值变化。
不同的是,Atomics.wait是让线程睡眠来等待,从而致使线程阻塞,这种阻塞在UI线程是不可接受的,它会导致整个页面卡死无法响应,因此如果你在UI主线程调用Atomics.wait,浏览器会抛出异常。
而Atomics.waitAsync可以让线程异步的等待,这样线程不会阻塞,因此它可以用在UI主线程中,也可以用在其它线程中。按照JavaScript的习惯,它应该会返回Promise,不过这里情况有点特殊,Promise是被包在一个对象中返回的,而且有时候不会返回Promise。
请往下看。
我们先举个例子来看一下如何使用它:
UI主线程:
// 创建Web Worker
const worker = new Worker('worker.js');
// 创建SharedArrayBuffer
const length = 10;
const sharedBuffer = new SharedArrayBuffer(length * Int32Array.BYTES_PER_ELEMENT);
// 用Int32Array初始化sharedBuffer
const sharedArray = new Int32Array(sharedBuffer);
for (let i = 0; i < length; i++) {
sharedArray[i] = 0;
}
// 将sharedBuffer传给Web Worker,采用structuredClone深度拷贝
worker.postMessage(sharedBuffer);
// 注意,它返回的是一个对象,async是true时,value才是Promise
const {async, value} = Atomics.waitAsync(sharedArray, 0, 0);
(async function() {
if(async) {
console.log('async', async, 'value', await value);
} else {
// 初始值不匹配或者timeout时间是0
console.log('没有等待的原因', value);
}
})();
worker.js文件:
self.addEventListener('message', m => {
// m.data就是sharedBuffer的拷贝
// 基于sharedBuffer创建Int32Array
const sharedArray = new Int32Array(m.data);
// 模拟从服务端获取数据
setTimeout(function() {
console.log('开始传递数据', Date.now());
// 最关键的一步,将数据放入共享内存
Atomics.store(sharedArray, 0, 100);
Atomics.notify(sharedArray, 0);
}, 5000);
});
从例子中可以看到,UI主线程中调用了Atomics.waitAsync,来等待Worker线程的Atomics.notify,整个UI线程不会被阻塞。
我们来看一下它的具体用法。
具体用法与含义
Atomics.waitAsync的意思是当指定位置的值与期望值一致时,就继续等待。
语法
Atomics.waitAsync(typedArray, index, value)
Atomics.waitAsync(typedArray, index, value, timeout)
参数
参数:TypedArray
必须是一个基于SharedArrayBuffer的Int32Array或者BigInt64Array类型。
参数:index
TypedArray中的索引
参数:value
index索引处的期望值,当实际值和期望值一致时,Atomics.waitAsync将继续等待
参数:timeout(可选)
毫秒值,等待超过该时间就不再继续等待了。
以下情况会被认为没有超时时间:
- 非数字值。
- 不传该参数。
负值会被转成0,0会被认为不能等待。
返回值
返回值是一个Object对象。它有两个属性:
async属性:
true或者false,布尔值,用于告知当前函数是异步还是同步,异步就是true,同步是false。异步时,value属性是一个Promise,同步时value属性是一个字符串。
value属性
当async是true时,也就是说函数是异步的,这时value属性就是Promise,Promise被resolve时,其值可能是"ok"或者"timed-out"。"ok"代表正常的等待并且值已经改变。"time-out"代表等待超过了timeout参数设定的时间。该Promise没有被reject的情况。
当async是false时,也就是说函数是同步的,这时value就是一个字符串,其值有可能是"not-equal"或者"time-out","not-equal"说明value期望值参数和实际值不符,"time-out"说明timeout参数是0。
返回值综述
返回值只有这三种情况:
{ async: false, value: "not-equal" }
{ async: false, value: "timed-out" }
{ async: true, value: promise }
这个API有时会是同步的,有时会是异步的。
同步的情况: 1. 期望值和实际值不同,这时会立即返回,没有等待的必要。 2. timeout参数是0,立即超时返回。
异步的情况: 除了同步的情况外,其他情况下都是异步的。
抛异常的情况
TypedArray参数如果不是基于SharedArrayBuffer的Int32Array或者BigInt64Array类型,则会抛TypeError类型异常。
index参数超过了TypedArray的范围时抛RangeError类型异常。
各种情况说明
Atomics.waitAsync的使用限制较少。
我们还延续上面的例子,将主线程的代码修改如下,将worker.js的代码都删掉:
const worker = new Worker('worker.js');
const length = 10;
const sharedBuffer = new SharedArrayBuffer(length * Int32Array.BYTES_PER_ELEMENT);
const sharedArray = new Int32Array(sharedBuffer);
for (let i = 0; i < length; i++) {
sharedArray[i] = 0;
}
worker.postMessage(sharedBuffer);
// 注意看下面的代码
const {async: async1, value: value1} = Atomics.waitAsync(sharedArray, 0, 0);
const {async: async2, value: value2} = Atomics.waitAsync(sharedArray, 0, 0);
if(async2) {
value2.then((returnedValue) => {
console.log('async2', async2, 'returnedValue2', returnedValue);
});
} else {
console.log('没有等待的原因', value2);
}
if(async1) {
value1.then((returnedValue) => {
console.log('async1', async1, 'returnedValue1', returnedValue);
});
} else {
console.log('没有等待的原因', value1);
}
Atomics.notify(sharedArray, 0);
// 控制台打印
// async1 true returnedValue1 ok
// async2 true returnedValue2 ok
可以看到,我们在同一个内存地址wait了两次,这是可以的,而且还可以在不同的Worker中wait。Promise被resolve的先后顺序按照先入先出的原则,谁先wait谁先被notify。
另外可以在同一个线程对同一个SharedArrayBuffer的同一个位置进行wait和notify,也就是说线程可以notify自己。
Stage阶段和浏览器实现
目前除了Firefox,其他主流浏览器均已实现该特性。
但是最积极的Chrome是于2020年实现的该特性,考虑到国内的浏览器更新速度,目前还不建议大量使用该特性,除非你的客户端版本可控。
Polyfill
目前还没有可以完全模拟的Polyfill。
标准提议仓库中提供了一个Polyfill,通过另开一个Worker来实现异步,Worker比较重量级的,因此该 Polyfill 性能不是很好。
要查看该 Polyfill 可以点击此处
结束语
Atomics.waitAsync更加完善了Web Worker,CPU密集型的应用在 Web 前端的可能性变得越来越大。