快来跟我一起学 React(Day8)
Posted vv_小虫
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了快来跟我一起学 React(Day8)相关的知识,希望对你有一定的参考价值。
简介
我们继续上一节的内容,开始分析 React 官网:https://reactjs.org/docs/accessibility.html 的 “高级指引” 部分,这一部分会涉及到性能优化、Portals、Render Props 等概念的分析,跟上节奏,我们一起出发吧!
知识点
- 性能优化
- Portals
- Render Props
- 类型检查
准备
我们直接用上一节中的 react-demo-day5
项目来作为我们的 Demo
项目,还没有创建的小伙伴可以直接执行以下命令 clone
一份代码:
git clone -b dev https://gitee.com/vv_bug/react-demo-day5.git
接着进入到项目根目录 react-demo-day5
,并执行以下命令来安装依赖与启动项目:
npm install --registry https://registry.npm.taobao.org && npm start
等项目打包编译成功,浏览器会自动打开项目入口,看到上面截图的效果的时候,我们的准备工作就完成了。
性能优化
UI 更新需要昂贵的 DOM 操作,而 React 内部使用几种巧妙的技术以便最小化 DOM 操作次数。对于大部分应用而言,使用 React 时无需专门优化就已拥有高性能的用户界面。尽管如此,你仍然有办法来加速你的 React 应用。
shouldComponentUpdate 的作用
我们先贴一张官方的组件生命周期图:
可以看到,我们可以根据组件的 shouldComponentUpdate
方法去控制是否继续往下走 render
方法,以及后面的 节点 diff
对比,最后更新 DOM
节点等操作。
可以看到,我们在平时项目中,要尽量避免不必要的 render
操作,我们可以利用组件的 shouldComponentUpdate
方法去控制。
我们在 src/advanced-guides
目录下创建一个 optimizing
目录:
mkdir ./src/advanced-guides/optimizing
然后在 src/advanced-guides/optimizing
目录下创建一个 index.tsx
文件:
import React, useState from "react";
import HobbiesCom from "./hobbies.com";
function Optimizing()
// 输入框 ref 引用
let inputRef = React.createRef<htmlInputElement>();
// 额外的 state 数据
let [count, setCount] = useState<number>(0);
// hobbies 数据
let [hobbies] = useState<Array<string>>(["打游戏", "做美食"]);
/**
* 添加 hobby
*/
function handleAddHobby()
if (inputRef.current)
setCount(++count);
return (
<div>
<input placeholder="请输入你的爱好" ref= inputRef />
<button onClick= handleAddHobby >添加</button>
/* hobby 列表组件 */
<HobbiesCom hobbies= hobbies />
count
</div>
);
export default Optimizing;
可以看到,我们简单创建了一个输入框,然后通过 “添加” 按钮修改了 count
的值,而 count
是我们额外定义的一个 State
数据。
接着我们在 src/advanced-guides/optimizing
目录下创建一个 hobbies.com.tsx
组件:
import React from "react";
export type Prop =
hobbies: Array<string>,
;
class HobbiesCom extends React.Component<Prop>
state =
choosed: -1
handleClick(index: number)
this.setState(
choosed: index
);
render()
return (
<ul>
this.props.hobbies.map((hobby, index) => (
<li
style=this.state.choosed === index ? backgroundColor: "red" : undefined
key=`hobby-$hobby-index`
onClick=this.handleClick.bind(this, index)
>
hobby
</li>
))
</ul>
);
export default HobbiesCom;
可以看到,我们在 HobbiesCom
组件中渲染了 hobbies
的数据。
我们重新运行项目看结果:
npm start
可以看到,即使我们没有修改 HobbiesCom
组件中的 habbies
属性值,但是 HobbiesCom
组件仍然走了 render
函数,我们希望的是当 habbies
数组的数据没有变化的时候, HobbiesCom
组件不需要重新进行渲染,那我们该怎么做呢?
我们修改一下 src/advanced-guides/optimizing/hobbies.com.tsx
组件:
import React from "react";
export type Prop =
hobbies: Array<string>,
;
class HobbiesCom extends React.Component<Prop>
state =
choosed: -1
shouldComponentUpdate(nextProps: Prop, nextState: any)
// 浅对比新老属性的 hoobies 变量跟新老 state 的 choosed 属性
return nextProps.hobbies !== this.props.hobbies || this.state.choosed !== nextState.choosed;
...
export default HobbiesCom;
可以看到,只有当 shouldComponentUpdate
方法返回是 true
的时候,组件才会走 render
方法,我们对属性跟 State
做了一个浅对比,所以避免了不必要 render
方法的执行。
其实在 React
中,提供了一个 PureComponent
组件,它的作用就是在 shouldComponentUpdate
方法中对属性跟 State
进行了一个浅对比。
我们可以用 PureComponent
组件改造一下 src/advanced-guides/optimizing/hobbies.com.tsx
组件:
import React from "react";
export type Prop =
hobbies: Array<string>,
;
class HobbiesCom extends React.PureComponent<Prop>
state =
choosed: -1
handleClick(index: number)
this.setState(
choosed: index
);
render()
return (
<ul>
this.props.hobbies.map((hobby, index) => (
<li
style= this.state.choosed === index ? backgroundColor: "red" : undefined
key= `hobby-$ hobby -index`
onClick= this.handleClick.bind(this, index)
>
hobby
</li>
))
</ul>
);
export default HobbiesCom;
可以看到,我们只需要把继承的 React.Component
改成 React.PureComponent
即可,效果跟上面的一样,我就不演示了哈,小伙伴自己跑跑。
React.memo
我们上面说的都是 “类组件” 中的性能优化,在 “类组件” 中我们可以通过自定义 shouldComponentUpdate
方法或者 React.PureComponent
来减少不必要的 render
操作,但是在 “函数式组件” 中我们该怎么做呢?
在 React17+
,官方已经给给我们提供了一个高阶组件 React.memo
。
我们直接在 src/advanced-guides/optimizing
目录下创建一个 hobbies.func.tsx
函数式组件:
import React, useState from "react";
export type Prop =
hobbies: Array<string>,
;
function HobbiesFunc(props: Prop)
let [choosed, setChoosed] = useState(-1);
function handleClick(index: number)
setChoosed(index);
return (
<ul>
props.hobbies.map((hobby, index) => (
<li
style= choosed === index ? backgroundColor: "red" : undefined
key= `hobby-$ hobby -index`
onClick= () =>
handleClick(index);
>
hobby
</li>
))
</ul>
);
function areEqual(prevProps: Prop, nextProps: Prop)
/*
如果把 nextProps 传入 render 方法的返回结果与
prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
return prevProps.hobbies === nextProps.hobbies;
export default React.memo<Prop>(HobbiesFunc, areEqual);
可以看到,我们通过 React.memo
高阶组件中的第二个参数 areEqual
方法中做了判断,当新老 Props
中的 habbies
数据相等的时候,不执行组件的 render
操作。
React.memo
仅检查 props 变更。如果函数组件被 React.memo
包裹,且其实现中拥有 useState
,useReducer
或 useContext
的 Hook,当 context 发生变化时,它仍会重新渲染。
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现,所以我们 Demo
中其实不需要定义第二个参数,默认就可以达到我们想要的效果了。
效果就不演示啦,跟前面的类组件一致,小伙伴自己跑跑看效果哦。
我们分别通过 Demo
演示了 “类组件” 跟 “函数式组件” 中的性能优化,小伙伴一定要自己打断点跑跑看效果哦。
Portals
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
ReactDOM.createPortal(child, container)
第一个参数(child
)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container
)是一个 DOM 元素。
通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:
render()
// React 挂载了一个新的 div,并且把子元素渲染其中
return (
<div> this.props.children
</div> );
然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:
render()
// React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
// `domNode` 是一个可以在任何位置的有效 DOM 节点。
return ReactDOM.createPortal(
this.props.children,
domNode );
一个 portal 的典型用例是当父组件有 overflow: hidden
或 z-index
样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框。
我们还是通过 Demo
来演示一下。
首先在 src/advanced-guides
目录下创建一个 portals
目录:
mkdir ./src/advanced-guides
接着我们在 src/advanced-guides/portals
目录下创建一个组件 index.tsx
:
import React, useState from "react";
import Model from "./model";
function Portals()
let [isShowModel, setShowModel] = useState(false);
function handleShow()
if (!isShowModel)
setTimeout(() =>
setShowModel(false);
, 1000);
setShowModel((show) =>
return !show;
);
return (
<React.Fragment>
isShowModel && (
<Model>
<div className="model">hello model</div>
</Model>
)
<div onClick=handleShow>点我 show model</div>
</React.Fragment>
);
export default Portals;
可以看到,我们用了一个 isShowModel
状态去控制 Model
组件的展示。
然后我们在 src/advanced-guides/portals
目录下创建一个 model.tsx
组件:
import ReactDOM from "react-dom";
function Model(props: any)
return ReactDOM.createPortal(props.children, document.body);
export default Model;
很简单,我们直接用 ReactDOM.createPortal
定义并返回了一个 React
元素,并且把 Model
组件的子元素都绑定到了 document.body
节点上了。
接着我们去 src/main.scss
样式文件中添加一个 model
样式:
.root
font-size: 16px;
.model
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 200px;
height: 200px;
margin: auto;
line-height: 200px;
text-align: center;
background-color: rgba(0,0,0,0.7);
我们重新运行项目看结果:
可以看到,当我们点击 “show model” 按钮的时候,页面中显示了我们 Model
组件的子元素,并且这些子元素最后都被挂载到了 document.body
节点上了。
下面我们换一种实现方式,我们来实现一个 Toast
组件,让它能像 Model
组件一样,挂载到 body
节点上,然后指定时间后自动消失。
我们在 src/advanced-guides/portals
目录底下创建一个 toast.tsx
组件:
import ReactDom from "react-dom";
import React from "react";
class Toast
static divEle: HTMLDivElement | null;
static show(msg: string, timing: number = 1000)
Toast.hide();
Toast.divEle = document.createElement("div");
ReactDom.render(
(<div className="model">msg</div>),
Toast.divEle
);
document.body.appendChild(Toast.divEle);
setTimeout(() =>
Toast.hide();
, timing);
static hide()
Toast?.divEle && document.body.removeChild(Toast.divEle);
Toast.divEle = null;
export default Toast;
可以看到,我们用 ReactDom.render
创建了一个 React
根元素,把 Toast
的内容挂载到了一个 div
元素中,接着又把这个 div
元素挂在到了 document.body
节点上了。
我们修改一下 src/advanced-guides/portals/index.tsx
组件,用一下我们的 Toast
组件:
import React, useState from "react";
import Model from "./model";
import Toast from "./toast";
function Portals()
let [isShowModel, setShowModel] = useState(false);
function handleShow()
if (!isShowModel)
setTimeout(() =>
setShowModel(false);
, 1000);
setShowModel((show) =>
return !show;
);
function apiShowModel()
Toast.show("你好呀", 2000);
return (
<React.Fragment>
isShowModel && (
<Model>
<div className="model">hello model</div>
</Model>
)
<div onClick=handleShow>点我 show model</div>
<div onClick=apiShowModel>Api Show</div>
</React.Fragment>
);
export default Portals;
可以看到,我们在 apiShowModel
方法中调用了 Toast.show("你好呀", 2000)
方法创建了个一个 Toast
。
我们重新运行项目看结果:
可以看到,跟前面 Model
组件的效果一致。
下面我们用 ReactDom.createPortal
改造一下 Toast
组件:
import ReactDom from "react-dom";
import React from "react";
class Toast
static divEle: React.RefObject<HTMLDivElement> | null;
static show(msg: string, timing: number = 1000)
Toast.hide();
Toast.divEle = React.createRef<HTMLDivElement>();
ReactDom.render(
ReactDom.createPortal((<div ref=Toast.divEle className="model">msg</div>), document.body),
document.createElement("div")
);
setTimeout(() =>
Toast.hide();
, timing);
static hide()
Toast?.divEle?.current && document.body.removeChild(Toast.divEle.current);
Toast.divEle = null;
export default Toast;
可以看到,我们把之前的 document.body.appendChild
改成了 ReactDom.createPortal
,效果跟前面的一致,我就不演示啦,很明显了吧,小伙伴有没有弄懂 ReactDom.createPortal
的原理呢?
Render Props
术语 “render prop” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术,在 vue
跟 Angular
中叫 “插槽”。
具有 render prop 的组件接受一个返回 React 元素的函数,并在组件内部通过调用此函数来实现自己的渲染逻辑。
说起来抽象,我们还是通过 Demo
来分析一下。
比如我们现在有一个组件支持 “头部”、“内容”、“尾部” 的展示,其中 “头部”、“尾部” 支持自定义方式。
我们首先在 src/advanced-guides
目录下创建一个 render-props
目录:
mkdir ./src/advanced-guides/render-props
然后我们在 src/advanced-guides/render-props
目录下创建一个 index.tsx
组件:
import PropTypes from "prop-types";
type Prop =
renderHeader: () => React.ElementType,
renderFooter: () => React.ElementType,
;
function RenderProp(props: Prop | undefined)
return (
<div style=backgroundColor: "darkcyan">
/* 渲染头部 */
props?.renderHeader()
/* 渲染内容 */
<div>我是 RenderProps 组件内容模块</div>
/* 渲染尾部 */
props?.renderFooter()
</div>
);
RenderProp.propTypes =
renderHeader: PropTypes.func,
renderFooter: PropTypes.func,
;
RenderProp.defaultProps =
renderHeader: () => (<div>我是默认头部内容</div>),
renderFooter: () => (<div>我是默认尾部内容</div>),
;
export default RenderProp;
可以看到,我们创建了一个 RenderProp
组件,RenderProp
组件中渲染了三个部分 “头部”、“内容”、“尾部”,并且 “头部”、“尾部” 支持自定义。
我们修改一下 src/advanced-guides/index.tsx
组件,引入 RenderProp
组件:
/**
* 核心概念列表
*/
import CodeSplit from "./code-split";
import Context from "./context";
import ErrorBoundaries from "./error-boundaries";
import ErrorCom from "./error";
import ForwardRef from "./forward-ref";
import Optimizing from "./optimizing";
import Portals from "./portals";
import RenderProps from "./render-props";
function AdvancedGuides()
return (
<ErrorBoundaries>
<div>
/* 代码分割 */
<CodeSplit/>
/* Context */
<Context/>
/* 报错的组件 */
<ErrorCom/>
/* Refs 转发 */
<ForwardRef/>
/* 性能优化 */
<Optimizing/>
/* Portals */
<Portals/>
/* Render Props */
<RenderProps/>
</div>
</ErrorBoundaries>
);
;
export default AdvancedGuides;
然后我们重新运行项目看结果:
可以看到,RenderProp
组件渲染了默认的内容。
接下来我们修改一下 src/advanced-guides/index.tsx
组件,自定义 RenderProps
的 “头部” 跟 “尾部” 内容:
/**
* 核心概念列表
*/
import CodeSplit from "./code-split";
import Context from "./context";
import ErrorBoundaries from "./error-boundaries";
import ErrorCom from "./error";
import ForwardRef from "./forward-ref";
import Optimizing from "./optimizing";
import Portals from "./portals";
import RenderProps from "./render-props";
function AdvancedGuides()
return (
<ErrorBoundaries>
<div>
/* 代码分割 */
<CodeSplit/>
/* Context */
<Context/>
/* 报错的组件 */
<ErrorCom/>
/* Refs 转发 */
<ForwardRef/>
/* 性能优化 */
<Optimizing/>
/* Portals */
<Portals/>
/* Render Props */
<RenderProps
renderHeader=() => (<div>我是自定义头部内容</div>)
renderFooter=() => (<div>我是自定义尾部内容</div>)
/>
</div>
</ErrorBoundaries>
);
;
export default AdvancedGuides;
然后重新运行项目看结果:
可以看到,我们成功的自定义了 RenderProp
组件的头部跟尾部内容。
有小伙伴要疑问了 “这样做的目的是什么呢?”我举个例子吧:
比如你的领导需要你开发一个 RenderProp
组件,告诉你需要支持 “头部”、“内容”、“尾部” 的渲染。ok,你能力很强,很快就完成了领导的需求,你开心的去休假去了,然后你领导把这个组件直接给到了另外一个开发手中,领导心血来潮了,说 “头部内容样式需要改改,赶紧把那个休假的人叫回来!!”,哈哈,这个时候你是不是就很无语了呢?那如果你的组件支持自定义功能,你就可以很牛逼的告诉领导:“ 不想用默认样式的话,支持自定义头部跟尾部的,爱怎么玩就怎么玩”。
使用 PropTypes 进行类型检查
随着你的应用程序不断增长,你可以通过类型检查捕获大量错误。对于某些应用程序来说,你可以使用 Flow 或 TypeScript 等 javascript 扩展来对整个应用程序做类型检查。但即使你不使用这些扩展,React 也内置了一些类型检查的功能。要在组件的 props 上进行类型检查,你只需配置特定的 propTypes
属性,比如我们上面的 RenderProp
组件:
import PropTypes from "prop-types";
type Prop =
renderHeader: () => React.ElementType,
renderFooter: () => React.ElementType,
;
function RenderProp(props: Prop | undefined)
return (
...
);
RenderProp.propTypes =
renderHeader: PropTypes.func,
renderFooter: PropTypes.func,
;
RenderProp.defaultProps =
renderHeader: () => (<div>我是默认头部内容</div>),
renderFooter: () => (<div>我是默认尾部内容</div>),
;
export default RenderProp;
我们使用了 ts
静态类型校验跟 prop-types
的动态校验,如果在使用组件的时候不按规定传递属性类型的话,开发模式中直接就会报错了。
我们修改一下 src/advanced-guides/index.tsx
文件:
/* Render Props */
<RenderProps
renderHeader=1
renderFooter=() => (<div>我是自定义尾部内容</div>)
/>
可以看到,首先是 IDE
报错了,说我们需要的是 function
类型,但是你传递的是 number
类型,Webpack
编译也直接提示报错了。
接着是页面中的提示 :
PropTypes
以下提供了使用不同验证器的例子:
import PropTypes from 'prop-types';
MyComponent.propTypes =
// 你可以将属性声明为 JS 原生类型,默认情况下
// 这些属性都是可选的。
optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalSymbol: PropTypes.symbol,
// 任何可被渲染的元素(包括数字、字符串、元素或数组)
// (或 Fragment) 也包含这些类型。
optionalNode: PropTypes.node,
// 一个 React 元素。
optionalElement: PropTypes.element,
// 一个 React 元素类型(即,MyComponent)。
optionalElementType: PropTypes.elementType,
// 你也可以声明 prop 为类的实例,这里使用
// JS 的 instanceof 操作符。
optionalMessage: PropTypes.instanceOf(Message),
// 你可以让你的 prop 只能是特定的值,指定它为
// 枚举类型。
optionalEnum: PropTypes.oneOf(['News', 'Photos']),
// 一个对象可以是几种类型中的任意一个类型
optionalUnion: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Message)
]),
// 可以指定一个数组由某一类型的元素组成
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),
// 可以指定一个对象由某一类型的值组成
optionalObjectOf: PropTypes.objectOf(PropTypes.number),
// 可以指定一个对象由特定的类型值组成
optionalObjectWithShape: PropTypes.shape(
color: PropTypes.string,
fontSize: PropTypes.number
),
// An object with warnings on extra properties
optionalObjectWithStrictShape: PropTypes.exact(
name: PropTypes.string,
quantity: PropTypes.number
),
// 你可以在任何 PropTypes 属性后面加上 `isRequired` ,确保
// 这个 prop 没有被提供时,会打印警告信息。
requiredFunc: PropTypes.func.isRequired,
// 任意类型的必需数据
requiredAny: PropTypes.any.isRequired,
// 你可以指定一个自定义验证器。它在验证失败时应返回一个 Error 对象。
// 请不要使用 `console.warn` 或抛出异常,因为这在 `oneOfType` 中不会起作用。
customProp: function(props, propName, componentName)
if (!/matchme/.test(props[propName]))
return new Error(
'Invalid prop `' + propName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
,
// 你也可以提供一个自定义的 `arrayOf` 或 `objectOf` 验证器。
// 它应该在验证失败时返回一个 Error 对象。
// 验证器将验证数组或对象中的每个值。验证器的前两个参数
// 第一个是数组或对象本身
// 第二个是他们当前的键。
customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName)
if (!/matchme/.test(propValue[key]))
return new Error(
'Invalid prop `' + propFullName + '` supplied to' +
' `' + componentName + '`. Validation failed.'
);
)
;
限制单个元素
你可以通过 PropTypes.element
来确保传递给组件的 children 中只包含一个元素。
import PropTypes from 'prop-types';
class MyComponent extends React.Component
render()
// 这必须只有一个元素,否则控制台会打印警告。
const children = this.props.children;
return (
<div>
children
</div>
);
MyComponent.propTypes =
children: PropTypes.element.isRequired
;
默认 Prop 值
您可以通过配置特定的 defaultProps
属性来定义 props
的默认值:
class Greeting extends React.Component
render()
return (
<h1>Hello, this.props.name</h1>
);
// 指定 props 的默认值:
Greeting.defaultProps =
name: 'Stranger'
;
// 渲染出 "Hello, Stranger":
ReactDOM.render(
<Greeting />,
document.getElementById('example')
);
如果你正在使用像 transform-class-properties 的 Babel 转换工具,你也可以在 React 组件类中声明 defaultProps
作为静态属性。此语法提案还没有最终确定,需要进行编译后才能在浏览器中运行。要了解更多信息,请查阅 class fields proposal。
class Greeting extends React.Component
static defaultProps =
name: 'stranger'
render()
return (
<div>Hello, this.props.name</div>
)
defaultProps
用于确保 this.props.name
在父组件没有指定其值时,有一个默认值。propTypes
类型检查发生在 defaultProps
赋值后,所以类型检查也适用于 defaultProps
。
函数组件
如果你在常规开发中使用函数组件,那你可能需要做一些适当的改动,以保证 PropsTypes 应用正常。
假设你有如下组件:
export default function HelloWorldComponent( name )
return (
<div>Hello, name</div>
)
如果要添加 PropTypes,你可能需要在导出之前以单独声明的一个函数的形式,声明该组件,具体代码如下:
function HelloWorldComponent( name )
return (
<div>Hello, name</div>
)
export default HelloWorldComponent
接着,可以直接在 HelloWorldComponent
上添加 PropTypes:
import PropTypes from 'prop-types'
function HelloWorldComponent( name )
return (
<div>Hello, name</div>
)
HelloWorldComponent.propTypes =
name: PropTypes.string
export default HelloWorldComponent
上面的这些内容我们在前面的 Demo
中都有演示过,我们就不再演示了,小伙伴自己多敲敲哦!
总结
ok,React
的高级指引部分我们就算是分析完毕了,认认真真看到这里的小伙伴想必搞定面试跟简单的 React
项目应该是问题不大了,后面章节我们将会会介绍 React
中的 Hook
、“全家桶”、“源码分析” 等等。
这节到这就结束啦,下节见~
欢迎志同道合的小伙伴一起交流,一起学习。
Demo 项目全部代码:https://gitee.com/vv_bug/react-demo-day5/tree/dev/
觉得写得不错的可以点点关注,帮忙转发跟点赞。
以上是关于快来跟我一起学 React(Day8)的主要内容,如果未能解决你的问题,请参考以下文章