上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
12下一页
返回列表 发新帖

如何消除JS异步传染性?

[复制链接]
  • TA的每日心情
    开心
    2024-2-27 14:20
  • 签到天数: 88 天

    [LV.6]常住居民II

    22

    主题

    97

    回帖

    306

    积分

    荣誉开发者

    积分
    306

    油中2周年新人报道荣誉开发者生态建设者

    发表于 2023-12-13 23:51:37 | 显示全部楼层 | 阅读模式

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

    首页<<

    如何消除JS异步传染性?

    前言

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


    正文

    问题的抛出

    首先来看一个例子

    '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吗?

        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函数中

        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
        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[i]) {
                    if (cache[i].status === "fulfilled") {
                        return cache[i].data
                    }
                    else if (cache[i].status === "rejected") {
                        throw cache[i].err
                    }
                }
                const result = {
                    status: "pending",
                    data: null,
                    err: null
                }
                cache[i++] = 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

    邮箱:[email]bigonion@bigonion.cn[/email]

    NameSpace: 大聪花的家

    Origin: 大聪花的博客

    Powered by markdown 在线

    "2023.12.13"

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

  • TA的每日心情
    慵懒
    14 小时前
  • 签到天数: 811 天

    [LV.10]以坛为家III

    31

    主题

    552

    回帖

    1555

    积分

    荣誉开发者

    积分
    1555

    荣誉开发者新人进步奖油中2周年生态建设者新人报道挑战者 lv2油中3周年喜迎中秋

    发表于 2023-12-14 09:38:10 | 显示全部楼层

    image.png
    代码似乎有点问题,无法和其他代码同步了

    回复

    使用道具 举报

  • TA的每日心情
    开心
    6 小时前
  • 签到天数: 213 天

    [LV.7]常住居民III

    305

    主题

    4189

    回帖

    4056

    积分

    管理员

    积分
    4056

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

    发表于 2023-12-14 09:58:14 | 显示全部楼层
    ggnb,学习到了

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

    不过还有些细节问题,fetch然后业务代码,然后再fetch,那么这中间的业务代码是不是需要幂等
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    6 小时前
  • 签到天数: 213 天

    [LV.7]常住居民III

    305

    主题

    4189

    回帖

    4056

    积分

    管理员

    积分
    4056

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

    发表于 2023-12-14 09:58:51 | 显示全部楼层
    不过开发中倒是没注意过这个,我感觉直接加个await/async的成本比这样小吧
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2024-2-27 14:20
  • 签到天数: 88 天

    [LV.6]常住居民II

    22

    主题

    97

    回帖

    306

    积分

    荣誉开发者

    积分
    306

    油中2周年新人报道荣誉开发者生态建设者

    发表于 2023-12-14 13:12:18 | 显示全部楼层
    steven026 发表于 2023-12-14 09:38
    [md]![image.png](data/attachment/forum/202312/14/093742uqm7mb1npp9pqxjr.png)
    代码似乎有点问题,无法 ...

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

    使用道具 举报

  • TA的每日心情
    开心
    2024-2-27 14:20
  • 签到天数: 88 天

    [LV.6]常住居民II

    22

    主题

    97

    回帖

    306

    积分

    荣誉开发者

    积分
    306

    油中2周年新人报道荣誉开发者生态建设者

    发表于 2023-12-14 13:15:23 | 显示全部楼层
    王一之 发表于 2023-12-14 09:58
    ggnb,学习到了

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

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

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    881

    回帖

    1379

    积分

    荣誉开发者

    积分
    1379

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

    发表于 2023-12-14 22:20:36 | 显示全部楼层
    但是这个也不能叫消除异步,而是等异步出结果后重复执行一遍同步函数,我觉得副作用很大,比方说我在main里面fetch之前执行了很多同步操作,放到changedFetchEnv之后这些同步操作就会被执行2次,不仅有性能问题,还容易引发预期之外的错误:
    1. window.globalTest = 0;
    2. function main() {
    3.     window.globalTest++;
    4.     if (window.globalTest > 1) {
    5.         throw new Error('Unknown');
    6.     }
    7.     const _data = fetchData();
    8.     console.log(_data)
    9. }
    复制代码
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2024-2-27 14:20
  • 签到天数: 88 天

    [LV.6]常住居民II

    22

    主题

    97

    回帖

    306

    积分

    荣誉开发者

    积分
    306

    油中2周年新人报道荣誉开发者生态建设者

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

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

    使用道具 举报

  • TA的每日心情
    慵懒
    2022-3-8 11:41
  • 签到天数: 2 天

    [LV.1]初来乍到

    22

    主题

    881

    回帖

    1379

    积分

    荣誉开发者

    积分
    1379

    荣誉开发者卓越贡献油中2周年生态建设者油中3周年挑战者 lv2

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

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

    使用道具 举报

  • TA的每日心情
    开心
    2024-2-27 14:20
  • 签到天数: 88 天

    [LV.6]常住居民II

    22

    主题

    97

    回帖

    306

    积分

    荣誉开发者

    积分
    306

    油中2周年新人报道荣誉开发者生态建设者

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

    你说的对,那就需要把获取数据的这些步骤全都写在操作前面来确保执行一次
    回复

    使用道具 举报

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表