type
status
date
slug
summary
tags
category
icon
password
高级指引
1.代码分割
对应用进行代码分割能够帮助你“懒加载”当前用户所需要的内容,能够显著地提高你的应用性能。在你的应用中引入代码分割的最佳方式是通过动态
import()
语法。React.lazy
React.lazy
函数能让你像渲染常规组件一样处理动态引入(的组件)。React.lazy
接受一个函数,这个函数需要动态调用 import()
。它必须返回一个 Promise
,该 Promise 需要 resolve 一个 defalut
export 的 React 组件。然后应在
Suspense
组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。基于路由的代码分割
在你的应用中使用
React.lazy
和 React Router (opens new window)这类的第三方库,来配置基于路由的代码分割。2.全局数据 Context
组件间的数据是从上而下传递的,对于那些需要共享的数据如地区偏好等属性,会比较繁琐。
Context 就是为了共享那些对于一个组件树而言是“全局”的数据。
使用 context, 我们可以避免通过中间元素传递 props:
类似于 vue 中的 provide
React.createContext
创建一个 Context 对象。只有当组件所处的树中没有匹配到 Provider 时,其
defaultValue
参数才会生效。Context.Provider
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。
Provider 接收一个
value
属性,传递给消费组件。当 Provider 的
value
值发生变化时,它内部的所有消费组件都会重新渲染。通过新旧值检测来确定变化,使用了与
Object.is
(opens new window)相同的算法。当传递对象给 value 时,检测变化的方式会导致一些问题:父组件重新渲染时,Provider 重新渲染,value 属性总是被赋值为新的对象,导致下面的所有消费组件全部重新渲染,因此可以将 value 状态提升到父节点的 state 里,将 state 进行传递。
Class.contextType
此属性可以让你使用
this.context
来获取最近 Context 上的值。你可以在任何生命周期中访问到它,包括 render 函数中。Context.Consumer
一个 React 组件可以订阅 context 的变更,此组件可以让你在函数式组件 (opens new window)中可以订阅 context。
这需要函数作为子元素(function as a child) (opens new window)这种做法。这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的
value
值等同于往上组件树离这个 context 最近的 Provider 提供的 value
值。如果没有对应的 Provider,value
参数等同于传递给 createContext()
的 defaultValue
。Context.displayName
context 对象接受一个名为
displayName
的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。嵌套组件中更新 Context
从一个在组件树中嵌套很深的组件中更新 context 是很有必要的。在这种场景下,可以通过 context 传递一个函数,使得 consumers 组件更新 context。
theme-context.js
theme-toggler-button.js
app.js
消费多个 Context
为了确保 context 快速进行重渲染,React 需要使每一个 consumers 组件的 context 在组件树中成为一个单独的节点。
如果两个或者更多的 context 值经常被一起使用,那你可能要考虑一下另外创建你自己的渲染组件,以提供这些值。
3.错误边界
部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。
错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
错误边界无法捕获以下场景中产生的错误:
- 事件处理(了解更多 (opens new window))
- 异步代码(例如
setTimeout
或requestAnimationFrame
回调函数)
- 服务端渲染
- 它自身抛出来的错误(并非它的子组件)
如果一个 class 组件中定义了
static getDerivedStateFromError()
(opens new window)或 componentDidCatch()
(opens new window)这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用
static getDerivedStateFromError()
渲染备用 UI ,使用 componentDidCatch()
打印错误信息。然后你可以将它作为一个常规组件去使用:
4.Refs 转发
Ref 转发(forwardRef)使得某些组件可以接收
ref
,并将其向下传递(换句话说,“转发”它)给子组件。笔记
类似于 vue 中的 ref 。
即在父组件中引用子节点的 DOM 节点。通常不建议这样做,因为它会打破组件的封装,但它偶尔可用于触发焦点或测量子 DOM 节点。
在下面的示例中,
FancyButton
使用 React.forwardRef
来获取传递给它的 ref
,然后转发到它渲染的 DOM button
:这样,使用
FancyButton
的组件可以获取底层 DOM 节点 button
的 ref ,并在必要时访问,就像其直接使用 DOM button
一样。以下是对上述示例发生情况的逐步解释:
- 我们通过调用
React.createRef
创建了一个 React ref (opens new window)并将其赋值给ref
变量。
- 我们通过指定
ref
为 JSX 属性,将其向下传递给<FancyButton ref={ref}>
。
- React 传递
ref
给forwardRef
内函数(props, ref) => ...
,作为其第二个参数。
- 我们向下转发该
ref
参数到<button ref={ref}>
,将其指定为 JSX 属性。
- 当 ref 挂载完成,
ref.current
将指向<button>
DOM 节点。
注意第二个参数ref
只在使用React.forwardRef
定义组件时存在。常规函数和 class 组件不接收ref
参数,且 props 中也不存在ref
。Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。
5.Fragments
Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。
用法
父组件
子组件
<Columns />
需要返回多个 <td>
元素以使渲染的 HTML 有效。如果在 <Columns />
的 render()
中使用了父 div,则生成的 HTML 将无效。输出
带 key 的 Fragments
使用显式
<React.Fragment>
语法声明的片段可能具有 key。一个使用场景是将一个集合映射到一个 Fragments 数组 - 举个例子,创建一个描述列表:新语法
使用
<> </>
,除了它不支持 key 或属性。6.高阶组件(HOC)
高阶组件是参数为组件,返回值为新组件的函数。是一种基于 react 特性形成的一种设计模式(react 中实现装饰器模式)。常用于第三方库中,进行共享逻辑。
笔记
类似于 mixins 方案,但是 mixins 会产生更多麻烦。
组件是 React 中代码复用的基本单元。但你会发现某些模式并不适合传统组件。
例如,假设有一个
CommentList
组件,它订阅外部数据源,用以渲染评论列表:稍后,编写了一个用于订阅单个博客帖子的组件,该帖子遵循类似的模式:
它们实现了类似的功能:
- 在挂载时,向
DataSource
添加一个更改侦听器。
- 在侦听器内部,当数据源发生变化时,调用
setState
。
- 在卸载时,删除侦听器。
抽象出共享逻辑
第一个参数是被包装组件。第二个参数通过
DataSource
和当前的 props 返回我们需要的数据。当渲染
CommentListWithSubscription
和 BlogPostWithSubscription
时, CommentList
和 BlogPost
将传递一个 data
prop,其中包含从 DataSource
检索到的最新数据:再看一下另一个案例
实例:页面复用
假设我们有两个页面
pageA
和 pageB
分别渲染两个分类的电影列表,普通写法可能是这样:页面少的时候可能没什么问题,但是假如随着业务的进展,需要上线的越来越多类型的电影,就会写很多的重复代码,所以我们需要重构一下:
约定
1.将不相关的 props 传递给被包裹的组件
HOC 应该透传与自身无关的 props。大多数 HOC 都应该包含一个类似于下面的 render 方法:
也就是说,我们我们在使用这个被高阶组件包裹过的组件后,可能会收到一些无关的属性,我们不应该将这些无关的属性传给被包裹组件,而应该透传,这样可以保证HOC的复用性和灵活性,否则,一些无关的属性可能影响他包裹的组件。
什么是实传和透传?
实传就是你在组件内部进行了注册,可以在这个组件内部使用,同时你可以将嵌套 (opens new window)组件已经注册的属性传递下去。
透传是父组件传递一些在子组件没有注册的属性和没有使用的方法,实际上这些message是用于给子组件的嵌套组件使用的。
2.最大化可组合性
HOC 通常可以接收多个参数。如上面提到的页面复用案例。
建议编写组合式工具函数
许多第三方库都提供了
compose
工具函数,包括 lodash (比如 lodash.flowRight
(opens new window)), Redux (opens new window)和 Ramda (opens new window)。3.包装显示名称以便调试
HOC 创建的容器组件会与任何其他组件一样,会显示在 React Developer Tools (opens new window)中。为了方便调试,请选择一个显示名称,以表明它是 HOC 的产物。
最常见的方式是用 HOC 包住被包装组件的显示名称。比如高阶组件名为
withSubscription
,并且被包装组件的显示名称为 CommentList
,显示名称应该为 WithSubscription(CommentList)
:7.Portals
可以将子节点渲染到存在于父组件以外的 DOM 节点。常用于能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框。
React Portal之所以叫Portal,因为做的就是和“传送门”一样的事情:render到一个组件里面去,实际改变的是网页上另一处的DOM结构。
用法
通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:
然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:
如实现一个对话框(Dialog),最直观的做法,就是直接在 JSX 中把 Dialog 画出来,像下面代码的样子。
但上面代码存在一个局限性,那就是 Dialog 最终只能渲染在上面的 HTML 中。而当 Dialog 被包在其他组件中,要用 CSS 的 position 属性控制 Dialog位置,就要求从 Dialog 往上一直到 body 没有其他 postion 是 relative 的元素干扰,这……有点难为作为通用组件的 Dialog,毕竟,谁管得住所有组件不用 position 呢。此外,Dialog 的样式,因为包在其他元素中,各种样式纠缠,CSS 样式太容易搞成一坨浆糊了。
使用 Portal 就可以让Dialog这样的组件在表示层和其他组件没有任何差异,但是渲染的东西却像经过传送门一样出现在另一个地方。
通过 Portal 进行事件冒泡
一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先。
在 React v16之前的 React Portal 实现方法,有一个小小的缺陷,就是Portal是单向的,内容通过Portal传到另一个出口,在那个出口DOM上发生的事件是不会冒泡传送回进入那一端的。
也就是说,这样的代码。
在Dialog画出的内容上点击,onDialogClick是不会被触发的。
当然,这只是一个小小的缺陷,大部分场景下事件不传过来也没什么大问题。
在React v16中,通过 Portal 渲染出去的 DOM,事件是会冒泡从传送门的入口端冒出来的,上面的 onDialogClick 也就会被调用到了。
8.Diff算法
状态变更
当 state 或 props 更新时,react 会产生一个新的 DOM 树,并基于这两棵树的差别来控制渲染。
元素类型比对
1.元素类型改变
如当
<a>
变成 <img>
,React 会直接销毁原有的树,其对应的 DOM 节点也都全部销毁,并将新树的 DOM 插入到对应的 DOM 节点中。所有跟原有的树所关联的 state 也会被销毁。触发了一个完整的重建流程,所有的生命周期也都完整的执行了一遍。2.元素类型未改变
属性变更
当元素类型未改变时,React 会保留 DOM 节点,仅对比有改变的属性。
如
style
变更时:通过比对这两个元素,React 知道只需要修改 DOM 元素上的
color
样式,无需修改 fontWeight
。在处理完当前节点之后,React 继续对子节点进行递归。
组件更新
组件更新时,组件实例保持不变,这样 state 在跨越不同的渲染时保持一致。React 将更新该组件实例的 props 以跟最新的元素保持一致,并且调用该实例的
componentWillReceiveProps()
和 componentWillUpdate()
方法。节点进行递归
React 支持
key
属性。当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。以下例子在新增 key
之后使得之前的低效转换变得高效:如果没有 key, React 会销毁所有元素并重新创建。
现在 React 知道只有带着
'2014'
key 的元素是新元素,带着 '2015'
以及 '2016'
key 的元素仅仅移动了。9.Refs & DOM
refs 提供了一种 props 以外的方式修改子组件。
何时使用 Refs
下面是几个适合使用 refs 的情况:
- 管理焦点,文本选择或媒体播放。
- 触发强制动画。
- 集成第三方 DOM 库。
避免使用 refs 来做任何可以通过声明式实现来完成的事情。
举个例子,避免在
Dialog
组件里暴露 open()
和 close()
方法,最好传递 isOpen
属性。创建Refs
Refs 是使用
React.createRef()
创建的,并通过 ref
属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。访问 Refs
当 ref 被传递给
render
中的元素时,对该节点的引用可以在 ref 的 current
属性中被访问。ref 的值根据节点的类型而有所不同:
- 当
ref
属性用于 HTML 元素时,构造函数中使用React.createRef()
创建的ref
接收底层 DOM 元素作为其current
属性。
- 当
ref
属性用于自定义 class 组件时,ref
对象接收组件的挂载实例作为其current
属性。
- 你不能在函数组件上使用
ref
属性,因为他们没有实例,但你可以在其内部使用,只要其指向的是一个 DOM 元素或 Class 组件。
回调Refs
另一种设置 refs 的方式,称为“回调 refs”。它能更精细地控制何时 refs 被设置和解除。
不同于传递
createRef()
创建的 ref
属性,通过传递一个函数,这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。组件间传递回调形式的refs
在上面的例子中,
Parent
把它的 refs 回调函数当作 inputRef
props 传递给了 CustomTextInput
,而且 CustomTextInput
把相同的函数作为特殊的 ref
属性传递给了 <input>
。结果是,在 Parent
中的 this.inputElement
会被设置为与 CustomTextInput
中的 input
元素相对应的 DOM 节点。10.Render Props
使用一个函数式的 prop 来共享代码,即组件间共享状态。
具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑,更具体地说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。
现在有一个鼠标移动组件 Mouse ,和一个跟随鼠标移动的组件 Cat,使用 Render Props 就能让 Cat 组件轻松的在 Mouse 组件外获取鼠标状态。
记住,
children
prop 并不真正需要添加到 JSX 元素的 “attributes” 列表中。相反,你可以直接放置到元素的内部!- 作者:慕雨
- 链接:https://www.axin.work/article/0e74f77d-15e5-4a2d-becc-508f2183d3b4
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。