利用 Generator 和 Fetch 对 json 数据流 stream 进行边下载边解析

js在es6 之后,提供了 Generator 函数,可以自由控制函数的执行过程,可以在函数内部暂停执行,也可以在外部恢复执行。
这种函数最大的特点就是:对于状态机控制可以用非常简单明了的语句,来表达复杂的逻辑。
但是数年中少有实际用到 Generator 函数的实践。本文就是一个实用的实践,下面笔者讲介绍一下如何利用 Generator 暂停json的解析过程,来实现边下载边解析的功能。能让前端页面不必等到json全部拿到后再解析渲染,如渲染一个大量数据的场景/图表等地方,提升用户体验。
当然笔者对 Generator 的理解也不是特别深,运用不是特别熟练,如果有不对的地方,欢迎指正。这里我也只是抛砖引玉。

另外再简单介绍一些的背景知识:
为啥要用Fetch?xhr不行吗? 我们知道http是基于tcp的,在发送网络包的时候并不是一次性把内容都发送出去,而是先切割成小块,然后一块一块的发送出去。
我们如果能直接拿到每一个数据包的内容,我们就能实现边下载边解析的功能了。
以前的xhr并没有给开发者提供这个功能。而Fetch就提供了这个功能,Fetch 的 Response 对象提供了一个body属性,这个属性是一个可读的流,我们可以通过这个流来获取到每一个数据包的内容。

接下来就是如何实现这个功能了。

先说一下最终所要的实现:

fetchStreamJson({
  // 请求地址
  url: './bigJson1.json',
  // 解析配置
  JSONParseOption: {
    // 要求完整解析对应路径下的数据,才能上报(可选)
    completeItemPath: ['data', '[]'],
    // json解析的回调
    jsonCallback: (error, isDone, value) => {
      console.log('jsonCallback', error, isDone, value)
    }
  },
  // fetch请求配置,同浏览器 fetch api
  fetchOptions: {
    method: 'GET',
  },
})

核心逻辑其实和以前普通的解析json的逻辑相同,都是逐字读取字符,然后根据字符的不同,做不同的处理。网上也有很多json解析器的教程,这里不再详细介绍。笔者也是借用了一个较为成熟的json解析器 json-bigint,然后在其基础上做了一些修改,来实现边下载边解析的功能。

下面主要来说一下区别点:

1. 如何拿到解析了一部分的数据

原解析器是通过递归的方式来解析json的,如解析到一个 object 类型,此时如果内部的值也是 object 类型,那么就会递归调用解析函数,直到解析到最底层的值。这样实现在一次性完整数据的时候是没问题的。每次解析到 object 类型的都会执行函数,函数的执行栈增加。当解析完当前的 object ,函数也会退出当前的栈,并 return 解析后的对象。当所有递归都执行完,都弹出栈,整个json也解析完成,return 最终结果。
但是我们想json解析到一半的时候,就直接返回当前这一半的数据,就不能这样了。
例如:我们想要解析 { "a": 1, "b": 2 }
递归还是需要的,但是不能依赖函数的返回了,因为执行到一半的时候,我们想拿到一半 { "a": 1 },此时函数还没执行完,还没 return,我们就需要通过其他的方式来拿到这一半的数据。
这里是维护了一个当前解析json的变量resJson。保存当前的解析过程的json,每解析一小步,就修改一次这个对象。
同时也有对应的一个set函数,设置当前resJson该如何修改,这个函数是会变的,如:当执行到解析 array 的时候,后面每一个值都是 array 的值,那么就需要把这个值 push 到当前 array 中,我们重置这个 set 函数,修改为 push 新的值到当前的 array 中,这样后面执行完一小步的时候,执行这个 set 函数,即可正确给这个数组加一项。 object 类型的也是同理。
后续我们每完成一小步,都执行set来设置 resJson 而不是return出去。

2. 如何实现暂停和恢复

这里我们需要在对应的位置卡住程序。哪个位置呢,其实就是当前网络包结束的位置。在进行逐字解析的时候,其实也会判断一下每个字符是否合规。如第一个字符是 ‘f’,那么我们预测后面只能是 ‘a’,(因为只能是 false ),如果不是,就会报错。当解析到 fal 的时候当前网络包介绍了,这里我们就可以判断是不是所有网络包都结束了,如果都结束了,就直接执行后面的语句,就会报错了。如果网络包没有都结束,在这个位置yield卡住程序。当下一个网络包收到的时候,我们调用 next() ,让 Genarator 执行后面的语句。

3. 一个小优化,如何让json解析完特定路径下的内容后,再执行回调

通常情况下,面对非常大的这种json,它里面的内容并不是随机的格式,而是一个大数组,里面有一个个的对象。每个对象都会对应渲染一个组件,或者其他形式。
我们想要来让解析器在解析到特定路径下的内容后,再执行回调。比如后端返回如下格式:

{
  "data": [
    {
      "name": "a",
      "age": 1
    },
    {
      "name": "b",
      "age": 2
    },
    {
      "name": "c",
      "age": 3
    },
    ...
  ]
}

我们想要得到回调内容是, 每个对象都有完整的name和age

{
  "data": [
    {
      "name": "a",
      "age": 1
    },
  ]
}

而不是,下面回调中只有name,age在下一次回调中才能完成

{
  "data": [
    {
      "name": "a"
    },
  ]
}

实现这种效果,我们需要维护一个当前正在解析的路径的一个栈,当解析对象的对应的 key 的时候,我们将这个key放入栈中。
如果遇到了数组,因为数组的key是数字,但每个key对我们来说都是平等的,所以我们暂定用一个特定的字符串[]来表示,当解析到数组的时候,我们将[]放入栈中。
当解析完数组的单个值后,我们判断是不是路径和要求的相同,进而判断是否执行回调。这里我们也可以用Symbol来代替[],避免和json中的key冲突。

其他的功能实现,这里不再详述了,有兴趣的同学可以看一下源码的实现。
目前已经发布到了npm上。

demo效果:

这里我将网速设置为3Mb/s,完整下载此json,需要1s左右。

普通请求:
在这里插入图片描述
steam请求:

在这里插入图片描述

可以看到普通请求,页面会等到json完全加载完成后,才开始渲染数据。有较长的白屏时间。
steam请求下,页面会边下边解析。数据逐步加载的,几乎没有白屏时间。

其他问题:

  1. 会不会出现网络包传输速度比js解析更快的情况出现,也就是会不会js还没解析完这个网络包,下一个网络包就到了,然后又调用 next() ?
    理论上是不会的,js解析过程是同步代码,只有执行完当前的代码,才会执行下一个异步队列中的代码(也就是下一次网络包的执行回调),所以不会出现这种情况。
    另外经过测试,js的解析速度是GB/s级别的,而网络包的传输速度是MB/s级别的,远远高于以太网的传输速度。当然后续也可以用 wasm 来实现,进一步提升解析速度。
其他类似解决方案:

可以利用 EventSource 方式,让服务端把数据分割,再分批推送给前端。

代码地址

https://github.com/maotong06/stream-json-parse

参考资料

  1. https://github.com/sidorares/json-bigint
  2. https://es6.ruanyifeng.com/#docs/Generator
  3. https://developer.mozilla.org/zh-CN/docs/Web/API/Streams_API/Using_readable_streams
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值