震惊!用 Suspense 解决请求依赖的复杂场景居然这么简单!

fd96fc3e08c7597c28ad54c90458e623.png

有一种复杂场景 React 新手经常处理不好。

那就是一个页面有多个模块,每个模块都有自己的数据需要请求。与此同时,可能部分模块的数据还要依赖父级的异步数据才能正常请求自己的数据。如下图所示,当我们直接访问该页面时,页面请求的数据就非常多。而且这些数据还有一定的先后依赖关系。

0641dc2fbf5b55c10abc97934dc76396.png

大概数据请求的顺序依次如下

1. 自动登录 -> 个人用户信息,权限信息
2. 左侧路由信息
3. 页面顶层数据
4. 页面五个模块各自的数据

这些接口数据依赖关系比较明确,前面的接口请求完成之后,后续的接口才能正确请求。

如果页面四个模块的接口数据相互之间没有关系,其实整个页面还会简单一些,但是很多时候复杂度往往来自于后端的不配合。前端与后端的沟通在一些团队经常出现问题。

有的后端不愿意配合前端页面结构修改接口,前端也沟通不下来,只能自己咬牙在混乱的接口情况下写页面,就导致了无论是组件的划分也好还是页面的复杂度也好都变得杂乱无章。从而增加了开发成本。

因此,只有在一些比较规范的团队里,页面五个模块的数据解耦做得比较好。模块之间干净简洁的依赖关系能有效降低开发难度。

因此许多前端比较依赖把所有接口都放在父级组件中去请求的方案,这样不管你的接口是否混乱,在前端总能处理。但是这样的结果就是页面组件的耦合变得更加严重

在 React 19 中,我们可以使用 Suspense 嵌套来解决这种请求之间前后依赖的方案。我们在项目中模拟了这种场景的实现。具体的演示图如下

5dc9c6ef0d365212ed30e87f9a7a4fb0.gif

1

重新考虑初始化

和之前的方案一样,我们先定义父组件的请求接口

const getMessage = async () => {
  const res = await fetch('https://api.chucknorris.io/jokes/random')
  return res.json()
}

然后在父组件中,将 getMessage() 执行之后返回的 promise 作为状态存储在 useState 中。这样,当我点击时,只需要重新执行依次 getMessage() 就可以更新整个组件

const [
  messagePromise, 
  setMessagePromise
] = useState(null)

但是此时我们发现,messagePromise 并没有初始值,因此初始化时,接口并不会请求。这种情况下,有两种交互我们需要探讨。一种是通过点击按钮来初始化接口。另外一种就是组件首次渲染就要初始化接口。

我们之前的案例中,使用了取巧的方式,在函数组件之外提前获取了数据,这会导致访问任何页面该数据都会加载,因此并非合适的手段

// 我们之前的案例这样做是一种取巧的方式
const api = getMessage()

function Message() {
  ...

但是如果我们直接把 getMessage() 放在组件内部执行,也存在不小的问题。因为当组件因为其他的状态发生变化需要重新执行时,此时 getMessage() 也会冗余的多次执行。

// 此时会冗余多次执行
const [
  messagePromise, 
  setMessagePromise
] = useState(getMessage())

理想的情况是 getMessage() 只在组件首次渲染时执行依次,后续状态的改变就不在执行。而不需要多次执行。

我们先来考虑通过点击事件初始化接口的交互。此时我们可以先设置 messagePrmoise 的初始值为 null.

const [
  messagePromise, 
  setMessagePromise
] = useState(null)

不过这样做有一个小问题就是如果我将 messagePromise 值为 null 时传递给了子组件。那么子组件就会报错,因此我们需要特殊处理。一种方式就是在子组件内部判断

const MessageOutput = ({messagePromise}) => {
  if (!messagePromise) return
  const messageContent = use(messagePromise)

或者

// 这种写法是在需要默认显示状态时的方案
const MessageOutput = ({messagePromise}) => {
  const messageContent = messagePromise ? use(messagePromise) : {value: '默认值'}

另外一种思路就是设置一个状态,子组件基于该状态的值来是否显示。然后在点击时将其设置为 true

const [show, setShow] = useState(false)

function __clickHandler() {
  setMessagePromise(getMessage())
  setShow(true)
}
{show && <MessageContainer messagePromise={messagePromise} />}

另外一种交互思路就是初始化时就需要马上请求数据。此时我们为了确保 getMessage() 只执行一次,可以新增一个非 state 状态来记录组件的初始化情况。默认值为 false,初始化之后设置为 true

const i = useRef(false)
let __api = i.current ? null : getMessage()
const [
  messagePromise, 
  setMessagePromise
] = useState(null)

然后在 useEffect 中,将其设置为 true,表示组件已经初始化过了。

useEffect(() => {
  i.current = true
}, [])

这是利用 useState 的内部机制,初始化值只会赋值一次来做到的。从而我们可以放心更改后续 __api 的值为 null.

从这个细节的角度来说,函数组件多次执行的确会给开发带来一些困扰,Vue3/Solid 只执行一次的机制会更舒适一些,不过处理得当也能避免这个问题。

2

Suspense 嵌套

接下来,我们需要考虑的就是 Suspense 嵌套执行的问题就行了。这个执行起来非常简单。我们只需要将有异步请求的模块用 Suspense 包裹起来当成一个子组件。然后该子组件可以当成一个常规的子组件作为 Suspense 组件的子组件。

例如,我们声明一个子组件如下所示

const getApi = async () => {
  const res = await fetch('https://api.chucknorris.io/jokes/random')
  return res.json()
}

export default function Index(props) {
  const api = getApi()

  return (
    <div>
      <div id='tips'>多个 Suspense 嵌套,子组件第一部分</div>
      <div className="content">
        <div className='_05_dou1_message'>父级消息: {props.value}</div>
        <Suspense fallback={<div>Loading...</div>}>
          <Item api={api} />
        </Suspense>
      </div>
    </div>
  )
}

const Item = ({api}) => {
  const joke = api ? use(api) : {value: 'nothing'}

  return (
    <div className='_03_a_value_update'>子级消息:{joke.value}</div>
  )
}

然后我可以将这个子组件放在 Suspense 内就可以了

import DouPlus1 from './Dou1'
import DouPlus2 from './Dou2'
const MessageOutput = ({messagePromise}) => {
  const messageContent = use(messagePromise)
  return (
    <div>
      <p>{messageContent.value}</p>
      <DouPlus1 value={messageContent.value} />
      <DouPlus2 value={messageContent.value} />
    </div>
  )
}

在另外一个子组件中,我们还设计了内部状态,用于实现切换按钮,来增加页面交互的复杂度。并且每次切换都会请求接口。

4ed8b279dfd5d3522c6dff029e63a600.gif

如果切换时,上一个接口没有请求完成,React 会自己处理好数据的先后问题。不需要我们额外考虑竞态条件的情况。完整代码如下

var tabs = ['首页', '视频', '探索']

export default function Index() {
  var r = useRef(false)
  var api = r.current ? null : getApi()
  const [promise, setPromise] = useState(api)
  const [current, setCurrent] = useState(0)

  useEffect(() => {
    r.current = true
  }, [])

  return (
    <div>
      <div id='tips'>多个 Suspense 嵌套,子组件第二部分</div>
      <div className="content">
        {tabs.map((item, index) => (
          <button 
            id='btn_05_item' 
            className={current == index ? 'active' : ''}
            onClick={() => {
              setCurrent(index)
              setPromise(getApi())
            }}
            key={item}
          >{item}</button>
        ))}
        
        <Suspense fallback={<div className='_05_a_value_item'>Loading...</div>}>
          <Item api={promise} />
        </Suspense>
      </div>
    </div>
  )
}

const Item = ({api}) => {
  const joke = use(api)

  return (
    <div className='_05_a_value_item'>{joke.value}</div>
  )
}

3

总结

当我们要在复杂交互的情况下使用嵌套 Suspense 来解决问题,如果我们组件划分得当、与数据依赖关系处理得当,那么代码就会相当简单。不过这对于开发者来说,会有另外一个层面的要求。那就是如何合理的处理好组件归属问题。

许多前端页面开发难度往往都是由于组件划分不合理,属性归属问题处理不够到位导致的。因此 Suspense 在这个层面有了一个刚需,开发者必须要具备合理划分组件的能力,否则即使使用了 Suspense,也依然可能导致页面一团混乱。

- END -

如果您关注前端+AI 相关领域可以扫码加群交流

773823de143711f890eb4195f2f32902.jpeg

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

e4ebbd2e6e8e3d9fad4a147c1c66d3a5.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ava实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),可运行高分资源 Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。下面详细介绍C语言的基本概念和语法。 1. 变量和数据类型 在C语言中,变量用于存储数据,数据类型用于定义变量的类型和范围。C语言支持多种数据类型,包括基本数据类型(如int、float、char等)和复合数据类型(如结构体、联合等)。 2. 运算符 C语言中常用的运算符包括算术运算符(如+、、、/等)、关系运算符(如==、!=、、=、<、<=等)、逻辑运算符(如&&、||、!等)。此外,还有位运算符(如&、|、^等)和指针运算符(如、等)。 3. 控制结构 C语言中常用的控制结构包括if语句、循环语句(如for、while等)和switch语句。通过这些控制结构,可以实现程序的分支、循环和多路选择等功能。 4. 函数 函数是C语言中用于封装代码的单元,可以实现代码的复用和模块化。C语言中定义函数使用关键字“void”或返回值类型(如int、float等),并通过“{”和“}”括起来的代码块来实现函数的功能。 5. 指针 指针是C语言中用于存储变量地址的变量。通过指针,可以实现对内存的间接访问和修改。C语言中定义指针使用星号()符号,指向数组、字符串和结构体等数据结构时,还需要注意数组名和字符串常量的特殊性质。 6. 数组和字符串 数组是C语言中用于存储同类型数据的结构,可以通过索引访问和修改数组中的元素。字符串是C语言中用于存储文本数据的特殊类型,通常以字符串常量的形式出现,用双引号("...")括起来,末尾自动添加'\0'字符。 7. 结构体和联合 结构体和联合是C语言中用于存储不同类型数据的复合数据类型。结构体由多个成员组成,每个成员可以是不同的数据类型;联合由多个变量组成,它们共用同一块内存空间。通过结构体和联合,可以实现数据的封装和抽象。 8. 文件操作 C语言中通过文件操作函数(如fopen、fclose、fread、fwrite等)实现对文件的读写操作。文件操作函数通常返回文件指针,用于表示打开的文件。通过文件指针,可以进行文件的定位、读写等操作。 总之,C语言是一种功能强大、灵活高效的编程语言,广泛应用于各种领域。掌握C语言的基本语法和数据结构,可以为编程学习和实践打下坚实的基础。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值