溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

React之Suspense提出的背景及使用方法是什么

發(fā)布時間:2023-03-27 14:28:19 來源:億速云 閱讀:176 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容主要講解“React之Suspense提出的背景及使用方法是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“React之Suspense提出的背景及使用方法是什么”吧!

    Suspense 提出的背景

    假設(shè)我們現(xiàn)在有如下一個應(yīng)用:

    const Articles = () => {
      const [articles, setArticles] = useState(null)
      useEffect(() => {
        getArticles().then((a) => setArticles(a))
      }, [])
      if (articles === null) {
        return <p>Loading articles...</p>
      }
      return (
        <ul>
          {articles.map((article) => (
            <li key={article.id}>
              <h5>{article.title}</h5>
              <p>{article.abstract}</p>
            </li>
          ))}
        </ul>
      )
    }
    export default function Profile() {
      const [user, setUser] = useState(null)
      useEffect(() => {
        getUser().then((u) => setUser(u))
      }, [])
      if (user === null) {
        return <p>Loading user...</p>
      }
      return (
        <>
          <h4>{user.name}</h4>
          <Articles articles={articles} />
        </>
      )
    }

    該應(yīng)用是一個用戶的個人主頁,包含用戶的基本信息(例子中只有名字)以及用戶的文章列表,并且規(guī)定了必須等待用戶獲取成功后才能渲染其基本信息以及文章列表。 該應(yīng)用看似簡單,但卻存在著以下幾個問題:

    • "Waterfalls",意思是文章列表必須要等到用戶請求成功以后才能開始渲染,從而對于文章列表的請求也會被用戶阻塞,但其實對于文章的請求是可以同用戶并行的。

    • "fetch-on-render",無論是 Profile 還是 Articles 組件,都是需要等到渲染一次后才能發(fā)出請求。

    對于第一個問題,我們可以通過修改代碼來優(yōu)化:

    const Articles = ({articles}) => {
      if (articles === null) {
        return <p>Loading articles...</p>
      }
      return (
        <ul>
          {articles.map((article) => (
            <li key={article.id}>
              <h5>{article.title}</h5>
              <p>{article.abstract}</p>
            </li>
          ))}
        </ul>
      )
    }
    export default function Profile() {
      const [user, setUser] = useState(null)
      const [articles, setArticles] = useState(null)
      useEffect(() => {
        getUser().then((u) => setUser(u))
        getArticles().then((a) => setArticles(a))
      }, [])
      if (user === null) {
        return <p>Loading user...</p>
      }
      return (
        <>
          <h4>{user.name}</h4>
          <Articles articles={articles} />
        </>
      )
    }

    現(xiàn)在獲取用戶和獲取文章列表的邏輯已經(jīng)可以并行了,但是這樣又導致 Articles 組件同其數(shù)據(jù)獲取相關(guān)的邏輯分離,隨著應(yīng)用變得復雜后,這種方式可能會難以維護。同時第二個問題 "fetch-on-render" 還是沒有解決。而 Suspense 的出現(xiàn)可以很好的解決這些問題,接下來就來看看是如何解決的。

    Suspense 的使用

    Suspense 用于數(shù)據(jù)獲取

    還是上面的例子,我們使用 Suspense 來改造一下:

    // Profile.js
    import React, {Suspense} from 'react'
    import User from './User'
    import Articles from './Articles'
    export default function Profile() {
      return (
        <Suspense fallback={<p>Loading user...</p>}>
          <User />
          <Suspense fallback={<p>Loading articles...</p>}>
            <Articles />
          </Suspense>
        </Suspense>
      )
    }
    // Articles.js
    import React from 'react'
    import {getArticlesResource} from './resource'
    const articlesResource = getArticlesResource()
    const Articles = () => {
      debugger
      const articles = articlesResource.read()
      return (
        <ul>
          {articles.map((article) => (
            <li key={article.id}>
              <h5>{article.title}</h5>
              <p>{article.abstract}</p>
            </li>
          ))}
        </ul>
      )
    }
    // User.js
    import React from 'react'
    import {getUserResource} from './resource'
    const userResource = getUserResource()
    const User = () => {
      const user = userResource.read()
      return <h4>{user.name}</h4>
    }
    // resource.js
    export function wrapPromise(promise) {
      let status = 'pending'
      let result
      let suspender = promise.then(
        (r) => {
          debugger
          status = 'success'
          result = r
        },
        (e) => {
          status = 'error'
          result = e
        }
      )
      return {
        read() {
          if (status === 'pending') {
            throw suspender
          } else if (status === 'error') {
            throw result
          } else if (status === 'success') {
            return result
          }
        },
      }
    }
    export function getArticles() {
      return new Promise((resolve, reject) => {
        const list = [...new Array(10)].map((_, index) => ({
          id: index,
          title: `Title${index + 1}`,
          abstract: `Abstract${index + 1}`,
        }))
        setTimeout(() => {
          resolve(list)
        }, 2000)
      })
    }
    export function getUser() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve({
            name: 'Ayou',
            age: 18,
            vocation: 'Program Ape',
          })
        }, 3000)
      })
    }
    export const getUserResource = () => {
      return wrapPromise(getUser())
    }
    export const getArticlesResource = () => {
      return wrapPromise(getArticles())
    }

    首先,在 Profile.js 中開始引入 UserArticles 的時候就已經(jīng)開始請求數(shù)據(jù)了,即 "Render-as-You-Fetch"(渲染的時候請求),且兩者是并行的。當渲染到 User 組件的時候,由于此時接口請求還未返回,const user = userResource.read() 會拋出異常:

    ...
      read() {
        if (status === 'pending') {
          throw suspender
        } else if (status === 'error') {
          throw result
        } else if (status === 'success') {
          return result
        }
      },
    ...

    Suspense 組件的作用是,當發(fā)現(xiàn)其包裹的組件拋出異常且異常為 Promise 對象時,會渲染 fallback 中的內(nèi)容,即 <p>Loading user...</p>。等到 Promise 對象 resolve 的時候會再次觸發(fā)重新渲染,顯示其包裹的內(nèi)容,又因為獲取文章列表的時間比用戶短,所以這里會同時顯示用戶信息及其文章列表(具體過程后續(xù)會再進行分析)。這樣,通過 Suspense 組件,我們就解決了前面的兩個問題。

    同時,使用 Suspense 還會有另外一個好處,假設(shè)我們現(xiàn)在改變我們的需求,允許用戶信息和文章列表獨立渲染,則使用 Suspense 重構(gòu)起來會比較簡單:

    React之Suspense提出的背景及使用方法是什么

    而如果使用原來的方式,則需要修改的地方比較多:

    React之Suspense提出的背景及使用方法是什么

    可見,使用 Suspense 會帶來很多好處。當然,上文為了方便說明,寫得非常簡單,實際開發(fā)時會結(jié)合 Relay 這樣的庫來使用,由于這一款目前還處于試驗階段,所以暫時先不做過多的討論。

    Suspense 除了可以用于上面的數(shù)據(jù)獲取這種場景外,還可以用來實現(xiàn) Lazy Component

    Lazy Component

    import React, {Suspense} from 'react'
    const MyComp = React.lazy(() => import('./MyComp'))
    export default App() {
      return (
        <Suspense fallback={<p>Loading Component...</p>}>
          <MyComp />
        </Suspense>
      )
    }

    我們知道 import('./MyComp') 返回的是一個 Promise 對象,其 resolve 的是一個模塊,既然如此那這樣也是可以的:

    import React, {Suspense} from 'react'
    const MyComp = React.lazy(
      () =>
        new Promise((resolve) =>
          setTimeout(
            () =>
              resolve({
                default: function MyComp() {
                  return <div>My Comp</div>
                },
              }),
            1000
          )
        )
    )
    export default function App() {
      return (
        <Suspense fallback={<p>Loading Component...</p>}>
          <MyComp />
        </Suspense>
      )
    }

    甚至,我們可以通過請求來獲取 Lazy Component 的代碼:

    import React, {Suspense} from 'react'
    const MyComp = React.lazy(
      () =>
        new Promise(async (resolve) => {
          const code = await fetch('http://xxxx')
          const module = {exports: {}}
          Function('export, module', code)(module.exports, module)
          resolve({default: module.exports})
        })
    )
    export default function App() {
      return (
        <Suspense fallback={<p>Loading Component...</p>}>
          <MyComp />
        </Suspense>
      )
    }

    這也是我們實現(xiàn)遠程組件的基本原理。

    原理

    介紹了這么多關(guān)于 Suspense 的內(nèi)容后,你一定很好奇它到底是如何實現(xiàn)的吧,我們先不研究 React 源碼,先嘗試自己實現(xiàn)一個 Suspense

    import React, {Component} from 'react'
    export default class Suspense extends Component {
      state = {
        isLoading: false,
      }
      componentDidCatch(error, info) {
        if (this._mounted) {
          if (typeof error.then === 'function') {
            this.setState({isLoading: true})
            error.then(() => {
              if (this._mounted) {
                this.setState({isLoading: false})
              }
            })
          }
        }
      }
      componentDidMount() {
        this._mounted = true
      }
      componentWillUnmount() {
        this._mounted = false
      }
      render() {
        const {children, fallback} = this.props
        const {isLoading} = this.state
        return isLoading ? fallback : children
      }
    }

    其核心原理就是利用了 “Error Boundary” 來捕獲子組件中的拋出的異常,且如果拋出的異常為 Promise 對象,則在傳入其 then 方法的回調(diào)中改變 state 觸發(fā)重新渲染。

    接下來,我們還是用上面的例子來分析一下整個過程:

    export default function Profile() {
      return (
        <Suspense fallback={<p>Loading user...</p>}>
          <User />
          <Suspense fallback={<p>Loading articles...</p>}>
            <Articles />
          </Suspense>
        </Suspense>
      )
    }

    我們知道 React 在渲染時會構(gòu)建 Fiber Tree,當處理到 User 組件時,React 代碼中會捕獲到異常:

    do {
      try {
        workLoopConcurrent()
        break
      } catch (thrownValue) {
        handleError(root, thrownValue)
      }
    } while (true)

    React之Suspense提出的背景及使用方法是什么

    其中,異常處理函數(shù) handleError 主要做兩件事:

    throwException(
      root,
      erroredWork.return,
      erroredWork,
      thrownValue,
      workInProgressRootRenderLanes
    )
    completeUnitOfWork(erroredWork)

    其中,throwException 主要是往上找到最近的 Suspense 類型的 Fiber,并更新其 updateQueue

    const wakeables: Set&lt;Wakeable&gt; = (workInProgress.updateQueue: any)
    if (wakeables === null) {
      const updateQueue = (new Set(): any)
      updateQueue.add(wakeable) // wakeable 是 handleError(root, thrownValue) 中的 thrownValue,是一個 Promise 對象
      workInProgress.updateQueue = updateQueue
    } else {
      wakeables.add(wakeable)
    }

    React之Suspense提出的背景及使用方法是什么

    completeUnitOfWork(erroredWork) 在React 源碼解讀之首次渲染流程中已經(jīng)介紹過了,此處就不再贅述了。

    render 階段后,會形成如下所示的 Fiber 結(jié)構(gòu):

    React之Suspense提出的背景及使用方法是什么

    之后會進入 commit 階段,將 Fiber 對應(yīng)的 DOM 插入到容器之中:

    React之Suspense提出的背景及使用方法是什么

    注意到 Loading articles... 雖然也被插入了,但確是不可見的。

    前面提到過 SuspenseupdateQueue 中保存了 Promise 請求對象,我們需要在其 resolve 以后觸發(fā)應(yīng)用的重新渲染,這一步驟仍然是在 commit 階段實現(xiàn)的:

    function commitWork(current: Fiber | null, finishedWork: Fiber): void {
      ...
      case SuspenseComponent: {
        commitSuspenseComponent(finishedWork);
        attachSuspenseRetryListeners(finishedWork);
        return;
      }
      ...
    }
    function attachSuspenseRetryListeners(finishedWork: Fiber) {
      // If this boundary just timed out, then it will have a set of wakeables.
      // For each wakeable, attach a listener so that when it resolves, React
      // attempts to re-render the boundary in the primary (pre-timeout) state.
      const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any)
      if (wakeables !== null) {
        finishedWork.updateQueue = null
        let retryCache = finishedWork.stateNode
        if (retryCache === null) {
          retryCache = finishedWork.stateNode = new PossiblyWeakSet()
        }
        wakeables.forEach((wakeable) => {
          // Memoize using the boundary fiber to prevent redundant listeners.
          let retry = resolveRetryWakeable.bind(null, finishedWork, wakeable)
          if (!retryCache.has(wakeable)) {
            if (enableSchedulerTracing) {
              if (wakeable.__reactDoNotTraceInteractions !== true) {
                retry = Schedule_tracing_wrap(retry)
              }
            }
            retryCache.add(wakeable)
            // promise resolve 了以后觸發(fā) react 的重新渲染
            wakeable.then(retry, retry)
          }
        })
      }
    }

    到此,相信大家對“React之Suspense提出的背景及使用方法是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進入相關(guān)頻道進行查詢,關(guān)注我們,繼續(xù)學習!

    向AI問一下細節(jié)

    免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI