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>
    </>
  )
}
複製代碼

結語

上面僅是做者本身的理解,若有錯誤請及時指出。

參考

相關文章
相關標籤/搜索