minhuaF / blog

I will write my front-end story.
9 stars 1 forks source link

【React】Suspense #17

Open minhuaF opened 3 years ago

minhuaF commented 3 years ago

Suspense 是什么

“Suspense就是让组件‘等待’某个异步操作,直到该异步操作结束即可渲染。”

在代码的实际应用有两种情况:

  1. 异步加载资源;
  2. 异步请求数据;

Suspense正式发布是于版本v16.6.0

到目前v17.0.2,Suspense的异步请求数据也还没有正式发布,还在试验阶段。

所以,目前只能用Suspense来进行资源异步加载;(没有Suspense之前都是怎么做的组件异步记载呢?)

为什么要用Suspense?

  1. 解决开发请求数据,或者异步加载资源时的状态控制;能简化代码;
  2. 在用hooks开发时,如果嵌套的组件各自都有接口请求,有可能会造成"瀑布问题",用Suspense能避免此问题发生;

解释“瀑布问题”

直接上代码,会比较清晰;

const sleep = (time) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        message: 'success',
        time: time
      })
    }, time)
  })
}

const Father = () => {
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    console.log('Father 开始请求...')
    sleep(5000).then(() => {
      console.log('Father 结束请求...')
      setLoading(false)
    })
  }, []);

  if(loading) {
    return (<div>Father loading ...</div>)
  };

  return (
    <div>
      {console.log('Father 开始渲染...')}
      This is Father!
      <Child/>
    </div>
  )
}

const Child = () => {
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    console.log('Child 开始请求...')
    sleep(2000).then(() => {
      console.log('Child 结束请求...')
      setLoading(false)
    })
  }, []);

  if(loading) {
    return (<div>Child loading ...</div>)
  };

  return (
    <div>
      {console.log('Child 开始渲染...')}
      This is Child
    </div>
  )
}

ReactDOM.render( <Father />,  document.getElementById('root'));

上面代码在控制台的输出如下: image

从上面的代码和执行过程来看,Child组件的请求是在Father组件请求完成之后才发出,这并不是期望的效果,期待的效果是能同时发出请求,减少用户等待的时间。

Suspense 用法示例

使用Suspense异步请求数据

参考官网示例重写上面的实现


const warpPromise = (promise) => {
  let status = 'pending';
  let result;
  let suspender = promise.then(
    r => {
      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
      }
    }
  }
}

const fetchProfileData = () => {
  let fatherPromse = fatherHander();
  let childPromse = childHander();
  return {
    father: warpPromise(fatherPromse),
    child: warpPromise(childPromse),
  }
}

const fatherHander = () => {
  console.log('Father 开始请求...')
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('Father 结束请求...')
      resolve({
        msg: 'This is Father'
      })
    }, 5000)
  })
}

const childHander = () => {
  console.log('Child 开始请求...')
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('Child 结束请求...')
      resolve({
        msg: 'This is Child'
      })
    }, 2000)
  })
}

const resource = fetchProfileData();

const FatherPage = () => {
  return (
    <Suspense fallback={<div>Father loading ...</div>}>
      <Father />
    </Suspense>
  )
}

const Father = () => {
  const data = resource.father.read();
  return (
    <div>
      {console.log('Father 开始渲染...')}
      {data.msg}
      <Suspense fallback={<div>Child loading ...</div>}>
        <Child />
      </Suspense>
    </div>
  )
}

const Child = () => {
  const data = resource.child.read();
  return (
    <div>
      {console.log('Child 开始渲染...')}
      {data.msg}
    </div>
  )
}

ReactDOM.createRoot(document.getElementById('root')).render(<Father />)

上面代码执行的结果(Father是5s,Child是2s) image

如果改下Father和Child接口请求的延迟:Father 是1s,Child是2s,结果如下

image

细品下上面的区别(不会截gif图...)

结论就是:

  1. Father和Child的请求是同时发出;
  2. 如果Father的请求比Child的慢,那么等Father请求回来之后统一渲染,期间不会渲染Child组件的fallback;
  3. 如果Father的请求比Child的快,那么Fahter请求结束之后会先渲染Father组件的内容,同时会看到Child组件的fallback的内容; (TODO: 待补充.....看下能不能从源码看出个蛛丝马迹.)

使用Suspense进行代码拆分

使用Suspense进行代码拆分

在React v16.6.0 版本中,新增了React.lazy函数,能让你像渲染常规组件一样处理动态引用的组件,配置webpack的code Splitting,实现只有当组件被加载时,对应的资源才会被导入,从而达到懒加载的效果。

React.lazy 不能单独使用,需要配合Suspense组件一起使用,不然react会报错。

LazyLoadComponent.js

import React from "react";

const LazyLoadComponent = () => {
  return (
    <div>This is the LazyLoadComponent</div>
  )
}

export default LazyLoadComponent;

SuspenseLazyLoadDemo.js

import React, { Suspense, useState, lazy } from 'react';

const LazyLoadComponent = lazy(() => import(/* webpackChunkName: "LazyLoadComponent" */ './LazyLoadComponent'));

/**
 * 点击按钮时改变状态,加载对应的组件
 */
export default function SuspenseLazyLoadDemo() {
  const [showChildren, setshowChildren] = useState(false);
  const showChildrenHandler = () => {
    if (!showChildren) {
      setshowChildren(true)
    }
  }
  return (
    <div>
      <div onClick={showChildrenHandler}>Click me can load child...</div>
      {
        showChildren &&
        <Suspense fallback={<div>loading....</div>}>
          <LazyLoadComponent />
        </Suspense>
      }
    </div>
  )
}

运行上方代码,从控制台中可以看到LazyLoadComponent组件被单独出来一个chunk。

达到了资源异步加载的效果 image

拆分的代码会异步加载,可以有效地减少首屏包的体积。在实际操作中,发现fallback中添加对应组件的背景或者骨架图或者布局占位,能达到较好的用户体验,而且状态也不需要开发手动去控制。 拆分的代码会异步加载,可以有效地减少首屏包的体积。在实际操作中,发现fallback中添加对应组件的背景或者骨架图或者布局占位,能达到较好的用户体验,而且状态也不需要开发手动去控制。

Suspense核心源码分析

异步请求资源

(TODO: 待补充......)

异步请求数据

(TODO: 待补充......)

参考资料

精读《Suspense 改变开发方式》 React Suspense for Data(分析数据“瀑布”问题) 用于数据获取的 Suspense