中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

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

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

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

發布時間:2023-03-27 14:28:19 來源:億速云 閱讀:214 作者:iii 欄目:開發技術

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

    Suspense 提出的背景

    假設我們現在有如下一個應用:

    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} />
        </>
      )
    }

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

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

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

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

    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} />
        </>
      )
    }

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

    Suspense 的使用

    Suspense 用于數據獲取

    還是上面的例子,我們使用 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 的時候就已經開始請求數據了,即 "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 組件的作用是,當發現其包裹的組件拋出異常且異常為 Promise 對象時,會渲染 fallback 中的內容,即 <p>Loading user...</p>。等到 Promise 對象 resolve 的時候會再次觸發重新渲染,顯示其包裹的內容,又因為獲取文章列表的時間比用戶短,所以這里會同時顯示用戶信息及其文章列表(具體過程后續會再進行分析)。這樣,通過 Suspense 組件,我們就解決了前面的兩個問題。

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

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

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

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

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

    Suspense 除了可以用于上面的數據獲取這種場景外,還可以用來實現 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>
      )
    }

    這也是我們實現遠程組件的基本原理。

    原理

    介紹了這么多關于 Suspense 的內容后,你一定很好奇它到底是如何實現的吧,我們先不研究 React 源碼,先嘗試自己實現一個 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 方法的回調中改變 state 觸發重新渲染。

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

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

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

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

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

    其中,異常處理函數 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 源碼解讀之首次渲染流程中已經介紹過了,此處就不再贅述了。

    render 階段后,會形成如下所示的 Fiber 結構:

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

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

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

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

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

    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 了以后觸發 react 的重新渲染
            wakeable.then(retry, retry)
          }
        })
      }
    }

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

    向AI問一下細節

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

    AI

    寻乌县| 江都市| 喀什市| 瓦房店市| 盐城市| 临城县| 新源县| 广饶县| 长治市| 陆良县| 枣强县| 鄂托克旗| 崇州市| 汉川市| 南木林县| 乌兰浩特市| 稷山县| 二连浩特市| 建始县| 镇平县| 清镇市| 平乐县| 开江县| 铜梁县| 德州市| 余姚市| 香河县| 叶城县| 大英县| 瓮安县| 滁州市| 两当县| 井研县| 潜江市| 铁岭市| 科技| 惠水县| 苏州市| 清原| 平遥县| 桐乡市|