简介
本篇主要讲的是我们 react 中的开发技巧和一些设计思路
ps:本篇提及的 react 的一些设计方案大部分都是基于react16版本之后的哈,所以一些方法并不能给16之前使用,别踩坑哈!
1、提高兼容性的兜底方案
背景
在我们开发设计中,总会有我们考虑遗漏的一些场景
例如
- 接口放回字段不符合预期导致我们设置的state异常进而导致组件异常;
- 页面某组件正处于加载中呈现给用户的组件从用户的呈现上看上去就很奇怪;
针对这些我们往往就需要一个兜底的方案,让项目在出现异常情况下,不给用户展现一个奇奇怪怪页面
解决思路
利用好 getDerivedStateFromError 和 componentDidError 生命周期,解决数据异常问题
getDerivedStateFromError
和componentDidError
这个两个生命周期是react16之后才有的,其出现就是为了在组件中出现异常时在渲染前能及时捕获,并针对异常做出相应的处理,具体可以看我 第一篇 中关于这个两个生命周期的介绍;
ps:react15 中有一个支持有限的错误边界方法 handleError
。react16后此方法不再起作用,你需要改用componentDidCatch
。
下面举例
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 更新 state 使下一次渲染可以显降级 UI return { hasError: true }; } render() { if (this.state.hasError) { // 你可以渲染任何自定义的降级 UI return <h1>Something went wrong.</h1>; } return this.props.children; } }
利用好<Suspense>组件,解决组件正在加载中问题
Suspense
组件在16.6正式发布,有了它,我们就可以在当我们组件处于加载中时通过fallback
先临时给用户呈现一个loading组件例如骨架屏;
fallback
属性接受任何在组件加载过程中你想展示的 React 元素。你可以将Suspense
组件置于懒加载组件之上的任何位置。你甚至可以用一个Suspense
组件包裹多个懒加载组件。
下面举例
import React, { Suspense } from 'react'; const OtherComponent = React.lazy(() => import('./OtherComponent')); const AnotherComponent = React.lazy(() => import('./AnotherComponent')); function MyComponent() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <section> <OtherComponent /> <AnotherComponent /> </section> </Suspense> </div> ); }
2、基于路由的代码分割
背景
对于单页面应用开发设计(SPA),路由的设计绝对是必不可缺的一环,那么如何设计好路由呢?
- 要做到按需(按照对应路由)加载对应组件,而不是一次性加载全部,也不是每次加载都全部重新加载
- 每个路由模块清晰分明,互不影响;
解决思路
利用React.lazy
和React Router
,来配置基于路由的代码分割;
React.lazy
函数能让你像渲染常规组件一样处理动态引入(的组件)。
下面举例
import React, { Suspense, lazy } from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; const Home = lazy(() => import('./routes/Home')); const About = lazy(() => import('./routes/About')); const App = () => ( <Router> <Suspense fallback={<div>Loading...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </Suspense> </Router> );
3、任务状态管理
背景
在一个典型的 react 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但是一个大型的项目怎么可能这么搞,这不就一层套一层,不得把人给套麻了,因此我们就需要一个任务状态管理库,这个可是一个大学问了,是项目的核心之一了,我们对此我们要考虑哪方面的问题呢?
- 什么时候使用任务状态管理库、什么时候直接通过props传给子组件就好?
- 用哪个任务状态管理库好一点?
解决思路
1、什么时候使用任务状态管理库、什么时候直接通过props传给子组件就好?
ps:下面为个人理解(欢迎留言讨论👏)
使用哪种方式要从下面这几个角度去考虑:
- 是否是多个组件共享的状态:如果只是很多个组件共享的话建议用任务状态管理;
- 是否嵌套多层:例如父组件需要传值给子组件的子组件的子组件用,那你总不能一层一层的传下去吧,此时就建议用上任务状态管理;
- 是否为全局共享的组件,如果是的话,同样可以建议用任务状态管理;
- 是否与服务器大量的交互,有时候这种复杂的与服务器进行大量的交互,想要剥离出来单独管理,也可以使用任务状态管理
总而言之,我们的目的就是为了让整个项目的变的清晰易懂,组件之间又不容易相互污染,同时也让组件减少重复渲染,具体项目具体分析吧
2、用项目中用哪个任务状态管理库好呢?
目前关于 react 的任务状态管理方式有很多:基于 react hook的Context、redux、mobx、dva、recoil等等
我们先来看看目前目前这些的使用热度:
通过对比npm包的下载量,我们可以发现redux
目前是最流行的,其次是mobx
总结
秉着火啥为学啥的原则😜,大家都在用它必然优秀,迭代维护必然更健全,个人推荐可以把redux和mobx给学一下,毕竟是mobx是连redux的作者也推荐过的,后面在的 react常用第三方库专栏中为也会写一下,react、mobx的相关学习文档;
当然作为 react 16后自带的context,我们也要知道如何使用,毕竟人家是react自带的;
4、ref 的合理使用
背景
首先我们要知道ref是什么,最为react组件的三大核心(state、props、refs)属性之一,refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
你可以理解为它用来代替document.getElementById等这些方法,React中除开特定的几个地方只能使用document.getElementById这种直接操作真实DOM的情况外,大多数时候是不应该直接操作真实DOM的。为啥呢?
React会管理一个虚拟DOM,然后会把虚拟DOM渲染成真实DOM,但是渲染过程中并不是每次都全部内容渲染,而是会根据一定的算法,最终尽可能少的生成新的真实DOM,从而提升效率。
官网说了,勿过度使用refs,为啥呢?
因为 React 推崇 数据不可变(immutable)的编程思想,即不会直接修改对象的属性,而是深拷贝一份新对象,再对新的对象进行修改。
在 React 中每次 setState 传递的都是一个新的对象(新的引用),在更新渲染时 React 获取的也是新的对象,而 refs 在组件的每一个生命周期都保持唯一引用,修改 ref.current的值使得其违背了 immutable 原则。
ref的使用和性能关系倒不大。
那么我们如何合理使用refs呢?
合理使用refs要考虑的点
下面几个场景适合使用refs:
- 管理焦点,文本选择或媒体播放,例如绑定
<audio />
组件做一些音频播放操作; - 触发强制动画,例如在某事件之后控制某组件滚动条滚动;
- 集成第三方dom库
在使用refs时要考虑的几个点:
- 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
- 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
- 你不能在函数组件上使用 ref 属性,因为他们没有实例。当然如果你要用的话可以使用useRef()
5、高阶组件设计与使用注意点
背景
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
那么在设计和使用高级组件要考虑哪几个方面呢?
如何设计与使用高级组件
设计HOC方面要考虑这个几个点:
- 可复用性、可读性;
- 不改变原始组件,即引用高阶组件中的原始组件不能受到影响;
- 务必复制静态方法,当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法,为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上,当然你也可以使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法;
使用HOC要注意下面的点:
- 不要在 render 方法中使用 HOC,React 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有子组件的状态丢失。
- Refs 不会被传递,虽然高阶组件的约定是将所有 props 传递给被包装组件,但这对于 refs 并不适用。那是因为 ref 实际上并不是一个 prop - 就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。
6、通过Profiler API进行性能分析
我们在做性能优化中,是想知道一个组件性能怎么样的时候可以用 Profiler 去分析测量一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益。
用法
Profiler 能添加在 React 树中的任何地方来测量树中这部分渲染所带来的开销。 它需要两个 prop :一个是 id(string),一个是当组件树中的组件“提交”更新的时候被React调用的回调函数 onRender(function)。
render( <App> <Profiler id="Panel" onRender={callback}> <Panel {...props}> <Profiler id="Content" onRender={callback}> <Content {...props} /> </Profiler> <Profiler id="PreviewPane" onRender={callback}> <PreviewPane {...props} /> </Profiler> </Panel> </Profiler> </App> );
Profiler 需要一个 onRender 函数作为参数。 React 会在 profile 包含的组件树中任何组件 “提交” 一个更新的时候调用这个函数。 它的参数描述了渲染了什么和花费了多久。
function onRenderCallback( id, // 发生提交的 Profiler 树的 “id” phase, // "mount" (如果组件树刚加载) 或者 "update" (如果它重渲染了)之一 actualDuration, // 本次更新 committed 花费的渲染时间 baseDuration, // 估计不使用 memoization 的情况下渲染整棵子树需要的时间 startTime, // 本次更新中 React 开始渲染的时间 commitTime, // 本次更新中 React committed 的时间 interactions // 属于本次更新的 interactions 的集合 ) { // 合计或记录渲染时间。。。 }
让我们来仔细研究一下各个 prop:
- id: string - 发生提交的 Profiler 树的 id。 如果有多个 profiler,它能用来分辨树的哪一部分发生了“提交”。
- phase: "mount" | "update" - 判断是组件树的第一次装载引起的重渲染,还是由 props、state 或是 hooks 改变引起的重渲染。
- actualDuration: number - 本次更新在渲染 Profiler 和它的子代上花费的时间。 这个数值表明使用 memoization 之后能表现得多好。(例如 React.memo,useMemo,shouldComponentUpdate)。 理想情况下,由于子代只会因特定的 prop 改变而重渲染,因此这个值应该在第一次装载之后显著下降。
- baseDuration: number - 在 Profiler 树中最近一次每一个组件 render的持续时间。 这个值估计了最差的渲染时间。(例如当它是第一次加载或者组件树没有使用 memoization)。
- startTime: number - 本次更新中 React 开始渲染的时间戳。
- commitTime: number - 本次更新中 React commit 阶段结束的时间戳。 在一次 commit 中这个值在所有的 profiler 之间是共享的,可以将它们按需分组。
- interactions: Set - 当更新被制定时,“interactions” 的集合会被追踪。(例如当 render 或者 setState 被调用时)。
注意:
Profiler 增加了额外的开支,所以它在生产构建中会被禁用。
当然好的react开发技巧和设计思路远不止这些,持续更新中。。。