目前Suspense尚處於實驗階段,大部分文檔尚未被翻譯。我基於目前的官方文檔,對Suspense做一些介紹。html
⚠️⚠️⚠️本文是概念性的,主要介紹
Suspense
解決了那些問題,而不是正確的使用方法。目前Facebook只在生產中,使用了Suspense
與Relay
的集成方案。若是你不使用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
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>
)
}
複製代碼
在實際開發一個應用時,應該根據需求混合使用不一樣的方法。這裏區別看待,只是爲了更好的權衡它們的取捨。dom
咱們徹底能夠在不說起其餘數據獲取方法的狀況下,介紹 Suspense
。可是這樣咱們就難以知道,Suspense
解決了那些問題,以及Suspense
與如今的方案有那些不一樣。異步
渲染時請求數據,是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
若是獲取Bar組件的數據,須要花費3秒。那麼,咱們只能在3秒後,開始獲取Foo組件的數據。這就是「瀑布問題」, 應該被並行處理的請求序列。spa
咱們能夠使用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>
)
}
複製代碼
考慮上面的代碼。代碼的執行順序將會是
咱們解決了瀑布問題。可是卻映入了另外一個問題,咱們必須等待全部數據返回後纔開始渲染。
雖然咱們能夠把請求從Promise.all
拆開,分別發起兩個Promise,可是隨着組件樹愈加的複雜,這顯然不是一個好主意,維護起來將會至關的困難。
在以前的方法中。咱們的步驟都是
使用Suspense
後,咱們能夠無需等待響應返回就開始渲染。
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>
)
}
複製代碼
resource.foo.read()
沒有返回數據,組件被掛起。React跳過它,嘗試渲染樹中的其餘組件。resource.bar.read()
沒有返回數據,組件被掛起,React跳過它。Suspense fallback
。Suspense fallback
將會消失。當咱們調用read()方法時,要麼獲取數據,要麼將組件掛起
使用Suspense
能夠幫助咱們消除if (statr) return loading
這樣的的模版代碼。咱們還能夠根據須要,增刪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}/>
</>
)
}
複製代碼
在上面的代碼,接口返回的結果可能存在「競態」的問題。
由於每一次接口響應返回時間是不肯定的,因此可能存在前一次的返回的結果,覆蓋後一次的狀況。而使用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>
</>
)
}
複製代碼
在Suspense
版本的例子中,咱們不須要等待響應結束後設置組件的狀態,這樣很容易出錯,由於咱們須要考慮設置對應狀態的時機。咱們直接傳給子組件資源對象resource,只要resource.read
沒有返回數據,組件將一直處於掛起的狀態,當props.resource
更新,從新請求,組件依然處於掛起的狀態,只到resource.read
返回數據,組件纔會被從新渲染,咱們就不須要考慮競態的問題。
當異步請求發生了錯誤,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>
</>
)
}
複製代碼
上面僅是做者本身的理解,若有錯誤請及時指出。