React Suspense for Data(一)

前言

目前Suspense尚处于实验阶段,大部分文档尚未被翻译。我基于目前的官方文档,对Suspense做一些介绍。html

什么是 Suspense ?

⚠️⚠️⚠️本文是概念性的,主要介绍Suspense解决了那些问题,而不是正确的使用方法。目前Facebook只在生产中,使用了SuspenseRelay的集成方案。若是你不使用Relay,可能须要等待一段时间才能在应用程序中真正的使用Suspense。(本文示例中的代码是"伪"代码,真实的实现可能要复杂的多,示例代码不要复制粘贴到你的项目中。)react

Suspense是React16.6版本中新增的组件,容许咱们等待一些代码的加载,并在等待时声明加载状态。api

// 使用React.lazy以及Suspense进行代码分割的例子
const Foo = React.lazy(() => import('./Foo'))
function Component () {
  return (
    <Suspense fallback={<Spinner />}> <Component /> </Suspense>
  )
}
复制代码

Suspense for Data是一个新的特性。容许您使用Suspense等待任何其余内容。包括Ajax请求异步返回的数据。(本文着重于介绍异步获取数据的例子)。Suspense可让组件在渲染以前进行等待。Suspense是一种通讯机制,告知组件数据还没有准备就绪,React会等待它准备好后更新UI。promise

Suspense for Data 的简单示例

Suspense for Data Demo

const apiParent = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve({ name: 'parent' }), 1000)
  })
}
const apiChild = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve({ name: 'child' }), 2000)
  })
}
// pending 时,wrapPromise会抛出一个Promise
// resolve 时,wrapPromise会返回结果
const wrapPromise = (promise) => {
  let status = 'pending'
  let result = null
  let suspender = promise.then(res => {
    status = 'success'
    result = res
  }).catch(err => {
    status = 'error'
    result = err
  })
  return {
    read() {
      if (status === 'pending') {
        throw suspender
      } else if (status === 'error') {
        throw result
      } else if (status === 'success') {
        return result
      }
    }
  }
}
const http = () => {
  const parentPromise = apiParent()
  const childPromise = apiChild()
  return {
    parent: wrapPromise(parentPromise),
    child: wrapPromise(childPromise)
  }
}
const resource = http()

function Parent () {
  const result = resource.parent.read()
  return <div>Parent: { result.name }</div>
}

function Child () {
  const result = resource.child.read()
  return <div>Child: { result.name }</div>
}
复制代码
function App() {
  return (
    <div className="App">
      {/* 在Parent没有返回结果前,显示<h1>Loading Parent...</h1> */}
      <React.Suspense fallback={<h1>Loading Parent...</h1>}>
        <Parent/>
        {/* 在Child没有返回结果前,显示<h1>Loading Child...</h1> */}
        <React.Suspense fallback={<h1>Loading Child...</h1>}>
          <Child/>
        </React.Suspense>
      </React.Suspense>
    </div>
  )
}
复制代码

Suspense 与传统请求数据的方法

在实际开发一个应用时,应该根据需求混合使用不一样的方法。这里区别看待,只是为了更好的权衡它们的取舍。dom

咱们彻底能够在不说起其余数据获取方法的状况下,介绍 Suspense。可是这样咱们就难以知道,Suspense解决了那些问题,以及Suspense与如今的方案有那些不一样。异步

Approach 1: 渲染时请求数据(例如: useEffect,componentDidMount)

渲染时请求数据,是React应用中经常使用的获取数据的方法。由于它直到组件在屏幕上进行渲染后,才开始请求数据。会致使所谓的“瀑布”问题。ui

function Foo () {
  const [state, setState] = useState('')
  useEffect(() => {
    api().then(res => setState(res))
  }, [])
  if (!state) return <div>Loading Foo……</div>
  return (
    <div>Foo</div>
  )
}
function Bar () {
  const [state, setState] = useState('')
  useEffect(() => {
    api().then(res => setState(res))
  }, [])
  if (!state) return <div>Loading Bar……</div>
  return (
    <React.Fragment> <div>Bar</div> <Foo/> </React.Fragment> ) } function App() { return ( <div className="App"> <Bar/> </div> ) } 复制代码

考虑上面的代码。代码的执行顺序将会是this

  1. 开始获取Bar组件的数据
  2. 等待……
  3. 完成渲染Bar组件
  4. 开始获取Foo组件的数据
  5. 等待……
  6. 完成渲染Foo组件

若是获取Bar组件的数据,须要花费3秒。那么,咱们只能在3秒后,开始获取Foo组件的数据。这就是“瀑布问题”, 应该被并行处理的请求序列。spa

Approach 2: 请求数据完成后渲染

咱们能够使用Promise.all避免瀑布问题。翻译

const api = (ms = 1000, type) => {
  return new Promise(resolve => {
    setTimeout(() => resolve('result'), ms)
  })
}
const fakeHttp = () => {
  return Promise.all([api(1000, 'bar'), api(3000, 'foo')])
}
const promise = fakeHttp()
function Foo (props) { 
  if (!props.state) return <div>Loading Foo……</div>
  return (
    <div>Foo</div>
  )
}
function Bar () {
  const [state, setState] = useState('')
  useEffect(() => {
    promise.then(res => setState(true))
  }, [])
  if (!state) return <div>Loading Bar……</div>
  return (
    <React.Fragment>
      <div>Bar</div>
      <Foo state={state}/>
    </React.Fragment>
  )
}
复制代码

考虑上面的代码。代码的执行顺序将会是

  1. 开始获取Bar组件的数据
  2. 开始获取Foo组件的数据
  3. 等待……
  4. 完成渲染Bar组件
  5. 完成渲染Foo组件

咱们解决了瀑布问题。可是却映入了另外一个问题,咱们必须等待全部数据返回后才开始渲染。

虽然咱们能够把请求从Promise.all拆开,分别发起两个Promise,可是随着组件树愈加的复杂,这显然不是一个好主意,维护起来将会至关的困难。

Approach 3: 按需渲染(例如:集成了Suspense的Relay)

在以前的方法中。咱们的步骤都是

  1. 开始异步获取数据
  2. 异步请求完成
  3. 开始渲染

使用Suspense后,咱们能够无需等待响应返回就开始渲染。

  1. 开始异步获取数据
  2. 开始渲染
  3. 异步请求完成
const resource = http()

function Foo () {
  const result = resource.foo.read()
  return <div>Foo: { result.name }</div>
}

function Bar () {
  const result = resource.bar.read()
  return <div>Bar: { result.name }</div>
}

function Page () {
  return (
    <React.Suspense fallback={<h1>Loading Foo...</h1>}>
      <Foo/>
      <React.Suspense fallback={<h1>Loading Bar...</h1>}>
        <Bar/>
      </React.Suspense>
    </React.Suspense>
  )
}

function App() {
  return (
    <div className="App">
      <Page/>
    </div>
  )
}
复制代码
  1. 渲染以前,http开始请求数据,它会返回一个特殊的资源,而不是Promise(这一般由实现了Suspense的请求库进行封装)。
  2. React尝试渲染Page组件,返回Foo,和Bar做为子组件。
  3. React尝试渲染Foo,resource.foo.read()没有返回数据,组件被挂起。React跳过它,尝试渲染树中的其余组件。
  4. React尝试渲染Bar,resource.bar.read()没有返回数据,组件被挂起,React跳过它。
  5. 暂时没有东西能够渲染了,React会渲染组件树最上方的Suspense fallback
  6. 随着数据的流入,React会尝试从新渲染,最终获取全部的数据后,页面上的Suspense fallback将会消失。

当咱们调用read()方法时,要么获取数据,要么将组件挂起

使用Suspense能够帮助咱们消除if (statr) return loading这样的的模版代码。咱们还能够根据须要,增删Suspense组件控制加载状态的粒度(好比,两个列表的状况下。我只想要一个加载态,能够在两个列表的外面,统一添加一层Suspense边界。若是需想要两个加载态,能够给各个列表各添加一个Suspense边界)而无需对组件代码进行侵入式的修改。

Suspense 与竞态问题

const api = (id) => {
  const ms = getRandomTime()
  return new Promise(resolve => {
    setTimeout(() => resolve(id), ms)
  })
}

function Bar (props) {
  const { id, clickNumber } = props
  if (!id) return <h1>Loading……</h1>
  return (
    <div>state: { id } clickNumber: { clickNumber }</div>
  )
}

let id = 0
let clickNumber = 0
function Page () {
  const [selfId, setSelfId] = useState(id)
  return (
    <>
      <button onClick={() => {
        id += 1
        clickNumber += 1
        api(id).then((id) => setSelfId(id))
      }}>+</button>
      <Bar id={selfId} clickNumber={clickNumber}/>
    </>
  )
}
复制代码

Race Conditions Bug

在上面的代码,接口返回的结果可能存在“竞态”的问题。

由于每一次接口响应返回时间是不肯定的,因此可能存在前一次的返回的结果,覆盖后一次的状况。而使用Suspense能够很好的解决竞态的问题。下面咱们使用Suspense重写示例。

const api = (id) => {
  const ms = getRandomTime()
  return new Promise(resolve => {
    setTimeout(() => resolve(id), ms)
  })
}

const http = (id) => {
  return wrapPromise(api(id))
}

function Bar (props) {
  const { resource, clickNumber } = props
  const id = resource.read()
  return (
    <div>state: { id } clickNumber: { clickNumber }</div>
  )
}

let id = 0
let clickNumber = 0
const initResource = http(id)
function Page () {
  const [resource, setResource] = useState(initResource)
  return (
    <>
      <button onClick={() => {
        id += 1
        clickNumber += 1
        setResource(http(id))
      }}>+</button>
      <React.Suspense fallback={<h1>Loading……</h1>}>
        <Bar resource={resource} clickNumber={clickNumber}/>
      </React.Suspense>
    </>
  )
}
复制代码

Race Conditions Suspense OK

Suspense版本的例子中,咱们不须要等待响应结束后设置组件的状态,这样很容易出错,由于咱们须要考虑设置对应状态的时机。咱们直接传给子组件资源对象resource,只要resource.read没有返回数据,组件将一直处于挂起的状态,当props.resource更新,从新请求,组件依然处于挂起的状态,只到resource.read返回数据,组件才会被从新渲染,咱们就不须要考虑竞态的问题。

Suspense 处理错误

当异步请求发生了错误,Suspense能够借助“错误边界”捕获异步请求抛出的错误。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false }
  }
  static getDerivedStateFromError() {
    return { hasError: true }
  }
  render () {
    if (this.state.hasError) {
      return <h1>:( error</h1>
    }
    return this.props.children; 
  }
}

function Page () {
  const [resource, setResource] = useState(initResource)
  return (
    <>
      <button onClick={() => {
        id += 1
        clickNumber += 1
        setResource(http(id))
      }}>+</button>
      {/* 使用错误边界捕获异步错误 */}
      <ErrorBoundary>
        <React.Suspense fallback={<h1>Loading……</h1>}>
          <Bar resource={resource} clickNumber={clickNumber}/>
        </React.Suspense>
      </ErrorBoundary>
    </>
  )
}
复制代码

结语

上面仅是做者本身的理解,若有错误请及时指出。

参考

相关文章
相关标签/搜索