React 18.2.0 已经发布很久了,这几天抽空重新完整的过了一遍 React的官网,并结合当时学习React 16.8 的总结,将笔记和案例记录下来,方便以后查阅和迭代。
总的来说react的语法不算太多,打算按照 React 官网 文档的教程顺序,从 安装 -> 核心概念 -> 高级指引 -> hook 开始。
本文档会结合一些案例(基于react18),方便大家阅读和实践,以备 开箱即用。
无障碍
代码分割
Context
React中的上下文特点:
-
当某个组件创建了上下文后,上下文中的数据,会被所有
后代组件共享 -
如果某个组件依赖了上下文,会导致该组件不再纯粹(外部数据仅来源于属性props)
-
一般情况下,用于第三方组件(通用组件)
上下文的数据变化:
上下文中的数据不可以直接变化,最终都是通过状态改变;
在上下文中加入一个处理函数,可以用于后代组件更改上下文的数据;
React.createContext(默认值)
创建上下文:
上下文是一个独立于组件的对象,该对象通过React.createContext(默认值)创建。
返回的是一个包含两个属性的对象。
- Provider属性
上下文生产者:是一个组件,该组件会创建一个上下文。
上下文赋值:该组件有一个value属性,通过该属性,可以为其数据赋值。
- 在
类组件中,直接使用this.context获取上下文数据
要求:必须拥有静态属性 contextType , 应赋值为创建的上下文对象。
<script type="text/babel">
'use strict';
// 1. 创建上下文
const ctx = React.createContext();
class A extends React.Component {
state = {
name: 'ACtx',
desc: 'A组件创建的上下文'
};
render() {
// 2. ctx.Provider组件创建上下文
// value 上下文赋值
return (
<ctx.Provider value={this.state}>
<div>
<h1>Title: A</h1> {this.props.children}
</div>
</ctx.Provider>
);
}
}
// 3. 在`类组件`中,直接使用`this.context`获取上下文数据
// 要求:必须拥有静态属性 `contextType` , 应赋值为创建的上下文对象。
class BChild extends React.Component {
static contextType = ctx;
render() {
return (
<div>
<h2>Title: B</h2>
<p>来自A的上下文数据:{this.context.name}</p>
</div>
);
}
}
function App() {
return (
<div className="App">
<A>
<BChild></BChild>
</A>
</div>
);
}
const root = ReactDOM.createRoot(document.querySelector('#app'));
root.render(
<div>
<App />
</div>
);
</script>
react-note/1.类组件使用上下文中的数据.html at master · PantherVkin/react-note (github.com)
- Consumer属性
消费者组件,使用上下文中的数据。
- 在函数组件中,需要使用
Consumer来获取上下文数据
Consumer是一个组件;
它的子节点,是一个函数(它的props.children需要传递一个函数),上下文通过参数传递给函数,返回结果会渲染到页面;
// 3. 使用`Consumer`来获取上下文数据
function BChild() {
return (
<div>
<ctx.Consumer>
{(value) => {
return (
<div>
<h2>Title: B</h2>
<p>来自A的上下文数据:{value.name}</p>
</div>
);
}}
</ctx.Consumer>
</div>
);
}
function App() {
return (
<div className="App">
<A>
<BChild></BChild>
</A>
</div>
);
}
react-note/2.函数组件中使用 consumer.html at master · PantherVkin/react-note (github.com)
- class中使用
Consumer
// 3. 使用`Consumer`来获取上下文数据
class BChild extends React.Component {
render() {
return (
<div>
<ctx.Consumer>
{(value) => {
console.log('v', value);
return (
<div>
<h2>Title: B</h2> <p>来自A的上下文数据:{value.name}</p>
</div>
);
}}
</ctx.Consumer>
</div>
);
}
}
react-note/3.类组件中使用 consumer.html at master · PantherVkin/react-note (github.com)
注意细节
- 强制刷新问题?
如果,上下文提供者(Context.Provider)中的value属性发生变化(Object.is比较),会导致该上下文提供的所有后代元素全部重新渲染(强制更新),无论该子元素是否有优化(无论shouldComponentUpdate函数返回什么结果)。
<script type="text/babel">
'use strict';
const ctx = React.createContext();
class A extends React.Component {
state = {
name: 'ACtx',
desc: 'A组件创建的上下文'
};
changeContext = () => {
this.setState({
name: 'ACtx',
desc: 'A组件创建的上下文'
});
};
render() {
// ctx.Provider组件创建上下文
// value 上下文赋值
return (
<ctx.Provider value={this.state}>
<div>
<h1>Title: A</h1> <button onClick={this.changeContext}>改变上下文</button>
{this.props.children}
</div>
</ctx.Provider>
);
}
}
class BChild extends React.Component {
static contextType = ctx;
shouldComponentUpdate(nextProps, nextState) {
console.log('运行了优化');
return false;
}
render() {
console.log('childB render');
return (
<h1>
name: {this.context.name},desc: {this.context.desc}
</h1>
);
}
}
function App() {
return (
<div className="App">
<A>
<BChild></BChild>
</A>
</div>
);
}
const root = ReactDOM.createRoot(document.querySelector('#app'));
root.render(
<div>
<App />
</div>
);
</script>
react-note/4.注意.html at master · PantherVkin/react-note (github.com)
- 解决思路
为 state 嵌套一层。
react-note/5.注意-解决.html at master · PantherVkin/react-note (github.com)
错误边界
默认情况下,若一个组件在渲染期间(render)发生错误,会导致整个组件树全部被卸载。
部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。
错误边界是一种 React 组件,该组件会捕获到渲染期间(render)子组件(渲染期间、生命周期方法以及构造函数)发生的错误,并有能力阻止错误继续传播。
- 案例:不使用错误边界时
react-note/1.不使用错误边界时.html at master · PantherVkin/react-note (github.com)
错误边界
如果一个 class 组件中定义了 static getDerivedStateFromError() 或 componentDidCatch() 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。
当抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。
- 编写生命周期函数
getDerivedStateFromError
-
静态函数
-
运行时间点:渲染子组件的过程中,发生错误之后,在更新页面之前
-
只有子组件发生错误,才会运行该函数
-
该函数返回一个对象,React会将该对象的属性
覆盖掉当前组件的state -
参数:错误对象
-
通常,该函数用于改变状态
- 编写生命周期函数
componentDidCatch
-
实例方法
-
运行时间点:渲染子组件的过程中,发生错误,更新页面之后,由于其运行时间点比较靠后,因此不太会在该函数中改变状态
-
通常,该函数用于记录错误消息
<script type="text/babel">
'use strict';
class Child extends React.PureComponent {
state = {
count: 0
};
handleClick = () => {
this.setState({
count: this.state.count + 1
});
};
render() {
if (this.state.count === 5) {
throw new Error('count Error');
}
return (
<div>
<button onClick={this.handleClick}>点击+1</button> <h2>{this.state.count}</h2>
</div>
);
}
}
class ErrorBound extends React.PureComponent {
state = {
hasError: false
};
static getDerivedStateFromError(error) {
console.log('发生错误了', error);
return {
hasError: true
};
}
componentDidCatch(error, info) {
console.log('记录错误信息');
}
render() {
if (this.state.hasError) {
return <h1>出现错误了!</h1>;
}
return this.props.children;
}
}
function App() {
return (
<div className="App">
<ErrorBound>
<Child></Child>
</ErrorBound>
<h2>另一个组件</h2>
</div>
);
}
const root = ReactDOM.createRoot(document.querySelector('#app'));
root.render(
<div>
<App />
</div>
);
</script>
react-note/1.使用错误边界时.html at master · PantherVkin/react-note (github.com)
注意
- 错误边界无法捕获以下场景中产生的错误:
-
事件处理(了解更多)
-
异步代码(例如
setTimeout或requestAnimationFrame回调函数) -
服务端渲染
-
它自身抛出来的错误(并非它的子组件)
-
仅处理渲染子组件期间的同步错误
-
错误边界的工作方式类似于 JavaScript 的
catch {},不同的地方在于错误边界只针对 React 组件。只有 class 组件才可以成为错误边界组件。大多数情况下, 你只需要声明一次错误边界组件, 并在整个应用中使用它。
属性默认值和类型检查
属性默认值
通过一个静态属性defaultProps告知react属性默认值。
- 函数组件 的属性默认值
在调用函数组件之前已经完成了混合。
import React from 'react'
// 函数组件的默认属性,
export function CompDefaultProps(props) {
console.log(props)
return <div>
<h1>{props.name}</h1>
<h2>{props.number}</h2>
</div>
}
CompDefaultProps.defaultProps = {
name: 'Drew',
number: 24
}
- 类组件 的属性默认值。
在constructor 调用之前已经完成了混合。
- 写法1
export class CompDefaultPropsCls extends React.Component {
render() {
return <div>
<h1>{this.props.name}</h1>
<h2>{this.props.number}</h2>
</div>
}
}
CompDefaultPropsCls.defaultProps = {
name: "DrewCls",
number: 24
}
- 写法2
export class CompDefaultPropsCls extends React.Component {
static defaultProps = {
name: "DrewCls",
number: 24
}
render() {
return <div>
<h1>{this.props.name}</h1>
<h2>{this.props.number}</h2>
</div>
}
}
PropTypes 进行类型检查
-
PropTypes
对组件使用静态属性propTypes告知react如何检查属性。
facebook/prop-types: Runtime type checking for React props and similar objects (github.com)
PropTypes.any://任意类型
PropTypes.array://数组类型
PropTypes.bool://布尔类型
PropTypes.func://函数类型
PropTypes.number://数字类型
PropTypes.object://对象类型
PropTypes.string://字符串类型
PropTypes.symbol://符号类型
PropTypes.node://任何可以被渲染的内容,字符串、数字、React元素
PropTypes.element://react元素
PropTypes.elementType://react元素类型
PropTypes.instanceOf(构造函数)://必须是指定构造函数的实例
PropTypes.oneOf([xxx, xxx])://枚举
PropTypes.oneOfType([xxx, xxx]); //属性类型必须是数组中的其中一个
PropTypes.arrayOf(PropTypes.XXX)://必须是某一类型组成的数组
PropTypes.objectOf(PropTypes.XXX)://对象由某一类型的值组成
PropTypes.shape(对象): //属性必须是对象,并且满足指定的对象要求
PropTypes.exact({...})://对象必须精确匹配传递的数据
//自定义属性检查,如果有错误,返回错误对象即可
属性: function(props, propName, componentName) {
//...
}
- 案例
- 类组件使用PropTypes
react-note/1.prop-type 类型检查.html at master · PantherVkin/react-note (github.com)
- 函数组件使用PropTypes
react-note/2.prop-type 类型检查-函数组件.html at master · PantherVkin/react-note (github.com)
HOC 高阶组件
HOF
Higher-Order Function, 高阶函数,以函数作为参数,并返回一个函数。
HOC
Higher-Order Component, 高阶组件,以组件作为参数,并返回一个组件。
通常,可以利用HOC实现横切关注点。一般用来做功能增强的。
- 案例:withLog
react-note/1.withLog.html at master · PantherVkin/react-note (github.com)
注意
- 不要在render/函数组件 中使用高阶组件,一开始就定义好
问题:
会导致每次都得到一个新的AComp、BComp,不断的在销毁组件。会浪费效率,会导致之前类组件的状态丢失。
-
不要在高阶组件内部更改传入的组件
-
命名习惯上with开头
Refs
reference: 引用。
ref 使用字符串
场景: 希望直接使用dom元素中的某个方法,或者希望直接只用自定义组件中的某个方法。
- ref作用于
内置的html组件,得到的将是真实的dom对象
案例:点击按钮,input聚焦,ref使用字符串的方式。
class Test extends React.Component {
handleClick = () => {
console.log(this.refs.inputRef, this.refs.inputRef.value);
this.refs.inputRef.focus();
};
render() {
return (
<div>
<input type="text" ref="inputRef" />
<button onClick={this.handleClick}>点击聚焦</button>
</div>
);
}
}
react-note/1.ref使用字符串-内置的html组件.html at master · PantherVkin/react-note (github.com)
- ref 作用于类组件,得到的将是类的实例
class A extends React.Component {
method() {
console.log('调用了组件A的方法');
}
render() {
return <h1>组件A</h1>;
}
}
class Comp extends React.Component {
handleClick = () => {
console.log(this);
console.log(this.refs.txt.value);
this.refs.compA.method();
};
render() {
return (
<div>
<input ref="txt" type="text" />
<A ref="compA" />
<button onClick={this.handleClick}>聚焦</button>
</div>
);
}
}
react-note/2.ref使用字符串-类组件.html at master · PantherVkin/react-note (github.com)
- 注意:
-
ref 不再推荐使用字符串赋值,字符串赋值的方法将来可能会被移除
-
ref 不能作用于函数组件
-
ref 推荐使用对象或者是函数
ref 使用对象
通过React.createRef 函数创建。
// ref 使用对象,通过React.createRef 函数创建
class Test2 extends React.Component {
constructor(props) {
super(props);
this.test2 = React.createRef();
}
handleClick = () => {
console.log(this.test2);
this.test2.current.focus();
};
render() {
return (
<div>
<input type="text" ref={this.test2} />
<button onClick={this.handleClick}>Test2点击聚焦</button>
</div>
);
}
}
react-note/3.ref使用对象.html at master · PantherVkin/react-note (github.com)
ref 使用函数
- 参数el
ref作用于内置的html组件,得到的将是真实的dom对象。
ref 作用于类组件,得到的将是类的实例。
// ref 使用函数
class Test3 extends React.Component {
handleClick = () => {
console.log(this.test3);
this.test3.focus();
};
render() {
return (
<div>
<input
type="text"
ref={(el) => {
this.test3 = el;
}}
/>
<button onClick={this.handleClick}>Test3点击聚焦</button>
</div>
);
}
}
react-note/4.ref使用函数.html at master · PantherVkin/react-note (github.com)
- ref 使用函数时 的调用时间
-
componentDidMount的时候会调用该函数,在componentDidMount 中可以使用ref。 -
如果ref的值发生了变动(旧的函数被新的函数替代),分别调用旧的函数以及新的函数,时间点出现在
componentDidUpdate之前
旧的函数被调用时,传递null;
新的函数被调用时,传递对象。
// ref 使用函数
// ref的值发生了变动(旧的函数被新的函数替代),分别调用旧的函数以及新的函数,时间点出现在componentDidUpdate 之前
// 旧的函数被调用时,传递null
// 新的函数被调用时,传递对象
class Test4 extends React.Component {
handleClick = () => {
this.test4.focus();
this.setState({});
};
render() {
return (
<div>
<input
type="text"
ref={(el) => {
console.log('函数被调用了', el);
this.test4 = el;
}}
/>
<button onClick={this.handleClick}>Test4点击聚焦</button>
</div>
);
}
}
function App() {
return (
<div className="App">
<Test4></Test4>
</div>
);
}
react-note/5.ref 使用函数时 的调用时间.html at master · PantherVkin/react-note (github.com)
- 如果ref 所在的组件被卸载,会调用函数
谨慎使用ref
-
能够使用属性和状态进行控制,就不要使用ref。
-
使用场景:
-
调用真实的dom对象中的方法
-
某个时候需要调用类组件的方法
Ref 转发
需求?
ref 映射到自定义组件的内部。
函数组件
React.forwardRef方法:
- 参数:
传递的是函数组件,不能是类组件。
并且,函数组件需要有第二个参数来得到ref。
- 返回值:
返回一个新的组件。
// 2. newA 把传递的ref ,通过函数组件的第二个参数交给A处理。
function A(props, ref) {
return (
<div>
<h1 ref={ref}>Title: A</h1>
</div>
);
}
// 1. React.forwardRef 创建NewA
// const NewA = React.forwardRef(function A(props, ref) {
// return (
// <div>
// <h1 ref={ref}>Title: A</h1>
// </div>
// );
// });
const NewA = React.forwardRef(A);
class App extends React.Component {
ARef = React.createRef();
componentDidMount() {
console.log('ref', this.ARef);
}
render() {
return (
<div className="App">
{/* 2. newA 把传递的ref ,通过函数组件的第二个参数交给A处理。 */}
<NewA ref={this.ARef}></NewA>
</div>
);
}
}
react-note/1.React.forwardRef.html at master · PantherVkin/react-note (github.com)
类组件
- 通过props 把ref 传递过来
// 类组件1: 通过props 把ref 传递过来。
class A extends React.Component {
render() {
return (
<div>
<h1 ref={this.props.refA}>Title: A</h1>
</div>
);
}
}
class App extends React.Component {
ARef = React.createRef();
componentDidMount() {
console.log('ref', this.ARef);
}
render() {
return (
<div className="App">
<A refA={this.ARef}></A>
</div>
);
}
}
react-note/2.类组件实现ref转发.html at master · PantherVkin/react-note (github.com)
- 使用forwardRef
class A extends React.Component {
render() {
return (
<div>
<h1 ref={this.props.refA}>Title: A</h1>
</div>
);
}
}
// 返回类组件,将ref 传递给refA 属性
const NewA = React.forwardRef((prop, ref) => <A {...prop} refA={ref}></A>);
class App extends React.Component {
ARef = React.createRef();
componentDidMount() {
console.log('ref', this.ARef);
}
render() {
return (
<div className="App">
<NewA ref={this.ARef}></NewA>
</div>
);
}
}
react-note/3.类组件使用forwardRef.html at master · PantherVkin/react-note (github.com)
常用场景
高阶组件中转发。
/**
* 高阶组件
* @param {*} comp 组件
*/
function withLog(Comp) {
class LogWrapper extends React.Component {
componentDidMount() {
console.log(`日志:组件${Comp.name}被创建了!${Date.now()}`);
}
componentWillUnmount() {
console.log(`日志:组件${Comp.name}被销毁了!${Date.now()}`);
}
render() {
//正常的属性
//forwardRef代表要转发的ref {current:null}
const {forwardRef, ...rest} = this.props;
return (
<div>
<Comp ref={forwardRef} {...rest} />{' '}
</div>
);
}
}
return React.forwardRef((props, ref) => {
return <LogWrapper {...props} forwardRef={ref} />;
});
}
class A extends React.Component {
render() {
return <h1>A:{this.props.a}</h1>;
}
}
const AComp = withLog(A);
class App extends React.Component {
myRef = React.createRef();
componentDidMount() {
console.log(this.myRef);
}
render() {
return (
<div>
<AComp ref={this.myRef} isLogin a={1} />{' '}
</div>
);
}
}
react-note/4.高阶组件中转发.html at master · PantherVkin/react-note (github.com)
PureComponent
PureComponent
- 纯组件,用于
避免不可必要的渲染(运行render函数),从而提高效率。
-
优化:如果一个组件的属性和状态,都没有发生变化,重新渲染该组件是没有必要的。
-
PureComponent是一个组件,如果某个组件继承自该组件,则该组件的
sholdComponentUpdate会进行优化,对属性和状态进行浅比较,如果相等则不会重新渲染。
- PureComponent 进行是浅比较
为了效率,应尽量使用PureComponent;
要求不改动之前的状态,永远是创建新的状态覆盖之前的状态(Immutable ,不可变对象);
有一个第三方JS库,Immutable.js,他专门用于制作不可变对象。
import React, {PureComponent} from 'react'
class Test extends PureComponent{
render() {
return <h1>Test</h1>
}
}
React.memo
- 函数组件使用
React.memo函数制作纯组件
React.memo 仅检查 props 变更。
如果函数组件被 React.memo 包裹,且其实现中拥有 useState,useReducer 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染。
const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
});
- 默认情况下其只会
对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
export default React.memo(MyComponent, areEqual);
render props
有时候,某些组件的各种功能及其处理逻辑几乎完全相同,只是显示的界面不一样,建议下面的方式认选其一来解决重复代码的问题(横切关注点)。
- render props
-
某个组件,需要某个属性
-
该属性是一个函数,函数的返回值用于渲染
-
函数的参数会传递为需要的数据
-
注意:纯组件的属性(尽量避免每次传递的render props的地址不一致)
-
通常该属性的名字叫做render
react-note/1.render props.html at master · PantherVkin/react-note (github.com)
- HOC
react-note/2.hoc.html at master · PantherVkin/react-note (github.com)
Portals
- 插槽
将一个React元素渲染到指定的DOM容器中。
ReactDOM.createPortal(React元素, 真实的DOM容器),该函数返回一个React元素;
function Child() {
return ReactDom.createPortal(<Test></Test>, document.querySelector('.modal'))
}
react-note/1.html at master · PantherVkin/react-note (github.com)
- 注意事件冒泡
React中的事件是包装过的;
它的事件冒泡是根据虚拟DOM树来冒泡的,与真实的DOM树无关。
React中的事件
使用场景
需要和原生dom 事件混用的时候,需要注意。
这里的事件:React内置的DOM组件中的事件。
- react给document注册事件
-
几乎所有的元素的事件处理,均在document的事件中处理
-
一些不冒泡的事件,是直接在元素上监听
-
一些document上面没有的事件,直接在元素上监听
-
在document的事件处理,React会根据虚拟DOM树的完成事件函数的调用
-
React的事件参数,并非真实的DOM事件参数,是React合成的一个对象,该对象类似于真实DOM的事件参数
-
stopPropagation,阻止事件在虚拟DOM树中冒泡 -
nativeEvent,可以得到真实的DOM事件对象 -
为了提高执行效率,React使用事件对象池来处理事件对象
注意事项
-
如果给真实的DOM注册事件,阻止了事件冒泡,则会导致react的相应事件无法触发
-
如果给真实的DOM注册事件,事件会先于React事件运行
-
通过React的事件中阻止事件冒泡,无法阻止真实的DOM事件冒泡
-
可以通过
nativeEvent.stopImmediatePropagation(),阻止document上剩余事件的执行 -
在事件处理程序中,不要异步的使用事件对象,如果一定要使用,需要调用
persist函数
工具
严格模式
StrictMode(React.StrictMode),本质是一个组件,该组件不进行UI渲染(React.Fragment <> </>),它的作用是,在渲染内部组件时,发现不合适的代码。
-
识别不安全的生命周期
-
关于使用过时字符串 ref API 的警告
-
关于使用废弃的 findDOMNode 方法的警告
-
检测意外的副作用
React要求,副作用代码仅出现在以下生命周期函数中。
-
ComponentDidMount -
ComponentDidUpdate -
ComponentWillUnMount
副作用:一个函数中,做了一些会影响函数外部数据的事情,例如:
-
异步处理
-
改变参数值
-
setState
-
本地存储
-
改变函数外部的变量
相反的,如果一个函数没有副作用,则可以认为该函数是一个纯函数。
在严格模式下,虽然不能监控到具体的副作用代码,但它会将不能具有副作用的函数调用两遍,以便发现问题。(这种情况,仅在开发模式下有效)。
- 检测过时的 context API
Profiler
性能分析工具。
分析某一次或多次提交(更新),涉及到的组件的渲染时间。
-
火焰图:得到某一次提交,每个组件总的渲染时间以及自身的渲染时间。
-
排序图:得到某一次提交,每个组件自身渲染时间的排序。
-
组件图:某一个组件,在多次提交中,自身渲染花费的时间。