上个月在某个群里看到了这么一张图:
当时立即联想到之前看的一篇 关于为什么 React 不用 Typescript 来写的知乎回答,于是顺手在群里提了一句 Facebook 的代码有自己的流水线,这些代码会经过编译步骤,最终会转换成异步代码。
毕竟,谁让前端不能发送同步请求呢。
但是中午听到 React Server Components 的发布,于是去看了 Dan 和 Lauren 的 talk,才反应过来这是 React Server Components 的 demo
里的代码。但是也并没有在意,想着如果不是 Facebook 内部流水线处理的话,可能只是为了 demo 方便展示才这样写,因为后面还出现了同步的 db.query
(根据以往经验,NodeJS 里数据库操作一般也都是异步操作)。
过了一会儿,有个同事又发了 Sebastian Markbåge 的一条 gist:
// Infrastructure.js
let cache = new Map();
let pending = new Map();
function fetchTextSync(url) {
if (cache.has(url)) {
return cache.get(url);
}
if (pending.has(url)) {
throw pending.get(url);
}
let promise = fetch(url).then(
response => response.text()
).then(
text => {
pending.delete(url);
cache.set(url, text);
}
);
pending.set(url, promise);
throw promise;
}
async function runPureTask(task) {
for (;;) {
try {
return task();
} catch (x) {
if (x instanceof Promise) {
await x;
} else {
throw x;
}
}
}
}
// Program.js
function getUserName(id) {
var user = JSON.parse(fetchTextSync('/users/' + id));
return user.name;
}
function getGreeting(name) {
if (name === 'Seb') {
return 'Hey';
}
return fetchTextSync('/greeting');
}
function getMessage() {
let name = getUserName(123);
return getGreeting(name) + ', ' + name + '!';
}
runPureTask(getMessage).then(message => console.log(message));
这条 gist
就很有意思了,主要的逻辑其实很简单:
- 第一次执行
task
,向pending
Map
里设置一个promise
,并throw
出去 catch
接收到之后判断是Promise
的实例,转入await x
await
完成后pending Map
删除promise
,并向cache Map
里设置返回值for(;;)
循环继续执行,此时fetchTextSync
检测到cache
里存在值,直接返回值
这样用一个无条件的 for
循环配合 throw
+ await
,竟然成功地将异步操作转换成了看似同步的操作。尤其是这一行
JSON.parse(fetchTextSync('/users/' + id))
,完全超乎想象。不得不说,大佬能想到的东西真的是与众不同,有兴趣的可以去看下具体的讨论。
一般到这儿就结束了,但是意外情况还是有的。
某个网站的控制台
之后某一天的开发过程中(控制台开着),突然想打开某个网站想看一个东西,就直接在地址栏输入了 xxx.com
,然后回车。按照常规操作,查完也就关了,但是鼠标移动到 ❎ 之前瞟了一眼控制台,看到了一堆 warnings。当然这种事情司空见惯,老手根本不用想都知道,基本不外乎:
- 你的代码组织 or 性能有问题
- 某个功能未来不再支持
- 你进行了危险操作
这里插播一个典型案例 ⬇️
正当我准备点击 ❎ 的时候,一条从来没看到过的 warning 吸引了我的注意:
这是一条 Deprecation warning,代表后面提到的功能未来可能不再支持。后面紧跟的两个词是 Synchronous XMLHttpRequest,左边的意思是 同步,右边是 XMLHttpRequest。两个词分开都很容易理解,毕竟对 JavaScript 开发者(以我为例)来说,这两个词的每一个词中蕴含的概念都是深深印在脑海里的,却从来不会觉得这两个词会有什么交集。但是这条 warning 里竟然放到了一起,这也让我小小的脑袋产生了大大的困惑:前端可以发送同步请求么?
重新理解 gist
这个时候再来想想 Sebastian Markbåge 的那条 gist,虽然调用的时候是
JSON.parse(fetchTextSync('/users/' + id));
看起来非常同步的一段代码。但是仔细想想,其实它是通过 for
+ throw
+ await
,在循环中来回判断promise
的状态,然后利用 try-catch
兜住 throw
的错误,反复执行代码,所以 gist 中 getMessage
实际上会调用三次(具体调用逻辑可以自己尝试),每一次都将代码的执行进度向前推进。
同步请求
既然浏览器已经将关键词提供给我们了,那我们就直接搜索。果不其然,第一条记录就是 MDN 的文章:Synchronous and asynchronous requests,这篇文章第一句话就讲到:
所以浏览器是支持同步请求的!Confirmed!
但是支持情况怎么样呢?MDN 也给了解答。在文章的 Synchronous Request 部分,开头就说到从 Gecko 30.0 (Firefox 30.0 / Thunderbird 30.0 / SeaMonkey 2.27)、Blink 39.0、和 Edge 13 开始,同步请求已经被废弃了,而这其中竟然少了 chrome 的身影,所以 chrome 到现在为止都是支持同步请求的,但是既然已经标上了 Deprecated 的标签,恐怕未来也难逃被废弃的命运。
至于如何调用同步请求,则很简单:
xhr.open('GET', '/', false)
这其中的第三个参数,就是 async
参数,默认为 true
,置为 false
就可以用来发送同步请求。
至于同步请求有什么影响,我觉得不用废话,毕竟同步的概念大家应该已经太熟悉了。
XMLHttpRequest 标准
再回到 chrome 的控制台,看这个 warning,它提供了一条链接,但是这个链接并不指向 chrome 的网站或者 MDN,而是指向了 XMLHttpRequest
标准的页面。跳过去看一下会发现,其实跟 MDN 讲得大差不差,但是标准毕竟是标准,还为我们指明了几个重点:
- 只有主线程的 Synchronous Request 被废弃了,worker 中并没有
- 如果
async
为false
,建议浏览器抛出类型为InvalidAccessError
的DOMException
参考链接
- Synchronous and asynchronous requests
- XMLHttpRequest Standard
- 某不提名网站(warning 产生地)