bigonion 发表于 2023-12-13 23:51:37

如何消除JS异步传染性?

本帖最后由 bigonion 于 2023-12-14 15:09 编辑

# [首页<<](https://bigonion.cn/blog)



# 如何消除JS异步传染性?




## 前言

我们在平时开发的过程中经常会遇到一个棘手的问题,就是大环境下是同步调用的方式,但是有一个外部库使用了链式,所以不得不对整个调用栈都进行链式重写,这就被称之为异步调用的传染性。今天来对消除async和await的传染性做一些探讨

---

## 正文

### 问题的抛出


首先来看一个例子

```js
'use strict';
    async function fetchData() {
      return await fetch('https://bigonion.cn')
            .then(response => response.text())
    }
    async function main() {
      let _data
      _data = await fetchData()
      console.log(_data.toString().substring(0, 200))
    }
    main()
```
在这个案例中,我们用fetch来请求网站拿到源码,由于fetch是异步调用,在所有调用了fetchData的函数必须要带上async或者以promise的方式被使用。

那么如果我想让fetchData不带上async和await并能够让`_data`顺利拿到请求的结果怎么办?

直接去掉async await吗?

```js
    function fetchData() {
      return fetch('https://bigonion.cn')
            .then(response => response.text())
    }
    function main() {
      let _data
      _data = fetchData()
      console.log(_data)
    }
    main()
```

显然这是不行的,因为这样`_data`拿到的就是一个promise类型,打印出来的结果也是一个promise对象

### 解决

#### 重新设计Fetch函数

这里问题出在fetch是异步上,所以我们需要重新对fetch进行设计,由于涉及到修改fetch,所以我们得在一个闭包里面运行,此闭包里面的fetch是会被修改的,也就是提供一个新的fetch函数。

当然这里实现的fetch比较简陋,不具备then属性,所以调用的时候把处理数据放在`changedFetchEnv`函数中
```js
    function fetchData() {
      return fetch('https://bigonion.cn')
    }
    function main() {
      let _data
      _data = fetchData()
      console.log("页面GET到的内容\n:", _data.toString().substring(0, 200))
    }
    function changedFetchEnv(func){
          func()
      }
    changedFetchEnv(main)
```

下面对fetch进行重写:
1. 设置缓存为一个数组,方便多次fetch请求,i是fetch调用的次数,函数中可能有多次使用fetch,`_fetch`是原`fetch`函数
2. 如果命中缓存,且状态是已经完成,则立即返回缓存的数据,如果是失败状态则返回错误信息。
3. 定义空`result`结构,具有`status、data、err`属性
4. 保存本次结果到`cache`缓存里
5. 利用原`fetch`函数`_fetch`来请求
6. 如果成功,则设定`result.status` 为 "fulfilled" 状态
7. 如果失败,则设定`result.status` 为 "rejected" 状态
8. 同时设定`result.data` 为 请求到的结果
9. 最后利用`throw`方法抛出本次请求的`_promise`对象,来立刻终止异步进程
10. 然后用try catch执行主函数,一旦检测到错误,且类型是promise,则表明没有命中缓存结果需要重新fetch一次,就再次运行主函数,并且清空`i`。

```js
    function fetchData() {
      return fetch('https://bigonion.cn')
    }
    function main() {
      let _data
      _data = fetchData()
      console.log("页面GET到的内容\n:", _data.toString().substring(0, 200))
    }
    function changedFetchEnv(func) {
      let cache = []
      let i = 0
      let _fetch = window.fetch
      window.fetch = (...args) => {
            if (cache) {
                if (cache.status === "fulfilled") {
                  return cache.data
                }
                else if (cache.status === "rejected") {
                  throw cache.err
                }
            }
            const result = {
                status: "pending",
                data: null,
                err: null
            }
            cache = result

            const _promise = _fetch(...args)
                .then(res => res.text())
                .then(
                  (res) => {
                        result.status = "fulfilled"
                        result.data = res
                  },
                  (err) => {
                        result.status = "rejected"
                        result.err = err
                  })
            throw _promise
      }
      
      try {
            func()
      } catch (err) {
            if (err instanceof Promise) {
                i = 0
                err.then(func, func);
            }
      }
    }
    changedFetchEnv(main)
```


最后我们发现,即使不用async和await关键字,通过异常抛出的方式,我们一样能把异步的函数变成同步的方式去写代码,并且能甩掉async和await关键字,防止了污染调用栈。

对于这种异常抛出的方法,我个人认为还是有非常大的可能性的,的的确确把看起来完全不可能消除的副作用给湮灭了,而且还有很大的可复用性。听说这个方法在React中已经被广泛使用了,具体情况可以看下面视频链接,还有实现的思维导图讲解,说的还是比较详细的。

也希望大家能给出一些建议和意见,对于这种方法,你怎么看?

----

## 参考资料

https://www.bilibili.com/video/BV1Qc411S7wU/?spm_id_from=333.337.search-card.all.click&vd_source=347109678632e4593a175ba64105c5ff









---



## 关于

作者:bigonion   
邮箱:bigonion@bigonion.cn   
NameSpace: [大聪花的家](https://bigonion.cn)   
Origin: [大聪花的博客](https://bigonion.cn/blog)   
Powered by (https://md.bigonion.cn)

"2023.12.13"
声明:未经本人同意,禁止转载、搬运、抄袭!






steven026 发表于 2023-12-14 09:38:10

!(data/attachment/forum/202312/14/093742uqm7mb1npp9pqxjr.png)
代码似乎有点问题,无法和其他代码同步了

王一之 发表于 2023-12-14 09:58:14

ggnb,学习到了

主要就是要promise再去执行func,然后再调用的时候有cache,从cache里取结果返回?

不过还有些细节问题,fetch然后业务代码,然后再fetch,那么这中间的业务代码是不是需要幂等

王一之 发表于 2023-12-14 09:58:51

不过开发中倒是没注意过这个,我感觉直接加个await/async的成本比这样小吧

bigonion 发表于 2023-12-14 13:12:18

steven026 发表于 2023-12-14 09:38
!(data/attachment/forum/202312/14/093742uqm7mb1npp9pqxjr.png)
代码似乎有点问题,无法 ...

这里应该是没有问题的,gg尝试这样写就能明显发现是顺序执行,结果是在后台请求完成后重写执行main的同步再输出,你也可以在main函数里面加一个 console.log("domain"),就能发现是按照预期执行的了
    console.log("12")
    changedFetchEnv(main)
    console.log("23")

bigonion 发表于 2023-12-14 13:15:23

王一之 发表于 2023-12-14 09:58
ggnb,学习到了

主要就是要promise再去执行func,然后再调用的时候有cache,从cache里取结果返回?


业务代码肯定是需要等的,无论是正常异步还是这样异常抛出的方式,只不过这里能消除async的副作用罢了,当函数第一次执行到请求时会立刻终止,然后再次执行拿着缓存结果顺序执行

cxxjackie 发表于 2023-12-14 22:20:36

但是这个也不能叫消除异步,而是等异步出结果后重复执行一遍同步函数,我觉得副作用很大,比方说我在main里面fetch之前执行了很多同步操作,放到changedFetchEnv之后这些同步操作就会被执行2次,不仅有性能问题,还容易引发预期之外的错误:
window.globalTest = 0;
function main() {
    window.globalTest++;
    if (window.globalTest > 1) {
      throw new Error('Unknown');
    }
    const _data = fetchData();
    console.log(_data)
}

bigonion 发表于 2023-12-15 22:20:22

cxxjackie 发表于 2023-12-14 22:20
但是这个也不能叫消除异步,而是等异步出结果后重复执行一遍同步函数,我觉得副作用很大,比方说我在main里 ...

既然你知道这是重复执行的函数的话,我的理解是可以通过加入对 if( _data) 的判断来避免重复执行,只有当 _data有数据才执行,不就可以避免反复执行了嘛😁

cxxjackie 发表于 2023-12-15 23:21:33

bigonion 发表于 2023-12-15 22:20
既然你知道这是重复执行的函数的话,我的理解是可以通过加入对 if( _data) 的判断来避免重复执行,只有当 ...

fetch之后的代码不会被重复执行(抛出错误强制中止了),只有之前的会,所以判断_data应该是没有意义的。

bigonion 发表于 2023-12-19 10:40:17

cxxjackie 发表于 2023-12-15 23:21
fetch之后的代码不会被重复执行(抛出错误强制中止了),只有之前的会,所以判断_data应该是没有意义的。 ...

你说的对,那就需要把获取数据的这些步骤全都写在操作前面来确保执行一次
页: [1]
查看完整版本: 如何消除JS异步传染性?