基础知识点
虚拟dom与性能提升
1.普通的数据渲染流程
- state数据
- jsx模板
- 数据 + 模板 结合,生成真实的dom,来显示
- state发生改版
- 数据 + 模板 结合,生成真实的dom,替换原始的dom
缺陷:
- 第一次生成了一个完整的dom片段
- 第二次生成了一个完整的dom片段
- 第二次的dom替换第一次的dom,非常消耗性能
2.改良版数据渲染流程
- state数据
- jsx模板
- 数据 + 模板 结合,生成真实的dom,来显示
- state发生改版
- 数据 + 模板 结合,生成真实的dom,但并不直接替换原始的dom
- 新的dom(只是DocumentFragment,并没有发生挂载)和原始的dom做比对,找差异
- 找出具体发生变化的标签元素
- 只用新的dom中的发生变化的标签元素,替换掉旧的dom中的标签元素
缺陷:
- 增加了dom比对的操作,性能提升并不明显
3.使用虚拟dom提升性能
- state数据
- jsx模板
- 数据 + 模板 生成虚拟dom(虚拟dom就是一个js对象,用它来描述真实的dom)
['div', {id: 'abc'}, ['span', {}, 'hello world']]
- 用虚拟dom的结构生成真实的dom,来显示
<div id='abc'><span>hello world</span></div>
- state发生改变
- 数据 + 模板 生成新的虚拟dom
['div', {id: 'abc'}, ['span', {}, 'bye bye']]
- 比较原始虚拟dom和新的虚拟dom的区别,找到变化的是span中的内容
- 直接操作dom,改变span中的内容
特点:
- 生成虚拟dom会有新能损耗,但由于虚拟dom实际是js对象,而创建一个js对象实际损耗是极低的
- 虚拟dom之间的比对性能极大的优于直接进行真实dom之间的比对,使得最终的渲染效率得到了提升
- 得益于虚拟dom的使用,可以使用react native开发原生应用,其与react的区别在于是将虚拟dom转换为真实dom还是原生组件
- 比对虚拟dom变化时的diff算法为同层节点比较,如果同层节点不一致则不会再比较下一层级的节点
列表中的key
- key的作用也是用在虚拟算法的diff算法中的
- key帮助React识别哪些元素改变了,比如被添加或删除,进而提升渲染效率,如果不指定key,将会抛出警告,并且默认会以元素在数组中的下标作为key
- 数组元素中使用的key在其兄弟节点之间应该是独一无二的。然而,它们不需要是全局唯一的
- key会传递信息给React,但不会传递给你的组件,因此在组件中无法获取key,如果组件中需要使用key属性的值,需要使用其他属性名显式传递这个值
组件挂载
- 使用ReactDOM.render()方法将组件挂载到dom节点上
React的核心JSX
- JSX:javaScript语法扩展
- 语法核心:在{}中可使用JavaScript表达式
- 在JSX中,false, null, undefined, true是合法的子元素,但它们并不会被渲染
JSX的属性
- 特殊属性:className表示class,htmlFor表示for
- 其他属性:与html属性保持一致
JSX的本质
- 被编译为React.createElement方法,因此在render方法中也可以直接使用返回React.createElement方法调用的结果的方式,例如
- 通过以下两种方式的对比可以知道JSX通过识别<>内首字母的大小写来确定编译的最终方式
直接使用html标签
- 使用html标签:方法的第一个参数编译为标签名字符串
使用自定义组件
- 首字母大写:方法的第一个参数编译为自定义组件的构造方法
组件的函数写法
- 当一个组件没有生命周期的回调和state状态管理的时候可以使用function的形式,编写更加简单,对比如下
组件的props(属性)和state(状态)
props属性
- props属性是只读的,不能被改变
- 如果在使用组件时,只写了props属性却没有赋值,则会默认赋值为true
- 属性展开,以下两个组件是等价的
function App1() {
return <Greeting firstName="Ben" lastName="Hector" />;
}
function App2() {
const props = {firstName: 'Ben', lastName: 'Hector'};
return <Greeting {...props} />;
}
state状态
- state是组件内部的数据,可以动态改变
- 在类组件中,constructor是唯一可以直接通过赋值的方式设置state的地方,在constructor之外需要使用setState来修改state中的值
- React 使用 Object.is 比较算法 来比较 state
异步更新
- this.props和this.state可能会异步更新,因此以下代码可能会无法更新counter
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
- 为保证在调用setState后立即读取this.state或者保证逻辑在应用更新后触发,可使用以下三种方式处理
- componentDidUpdate
- setState(updater, callback)并在updater函数中处理的方式,updater函数中接收的state和props都保证为最新
- setState(updater或者state对象, callback)中callback函数中处理的方式,通常更建议使用componentDidUpdate
// 示例
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
render方法的执行
render方法的执行时机
-
当组件的state或者props发生改变时,render函数就会重新执行,页面也会被重新渲染
-
当父组件的render函数被执行时,它的子组件的render都将被执行
-
每次组件更新时render方法都会被调用,但只要在相同的DOM节点中渲染<Clock />,就仅有一个Clock组件的class实例被创建使用
-
触发组件更新的方式
- 当组件的state或者props发生变化时,此时可使用shouldComponentUpdate函数进行控制
- 调用forceUpdate()强制更新,此时更新不受制于shouldComponentUpdate函数,但子组件会按照正常的生命周期方法来渲染,包括会正常调用shouldComponentUpdate()方法
- 订阅了Context后,当Provider的value值发生变化时,它内部的所有消费组件都会重新渲染,且Provider及其内部consumer组件都不受制于shouldComponentUpdate函数
组件的生命周期
Initialization(初始化)
- 初始化组件的数据,包括state和props,初始化的过程主要在constructor中完成
Mounting(挂载)
- componentWillMount:在组件即将被挂载到页面的时刻自动执行
- componentDidMount:在组件被挂载到页面之后自动执行
Updation(更新)
- shouldComponentUpdate:在组件被更新之前自动被执行
- componentWillUpdate:在shouldComponentUpdate之后(shouldComponentUpdate需要返回true),组件被更新之前被执行
- componentDidUpdate:在组件被更新之后自动被执行
Unmounting(卸载)
- componentWillUnmount:当组件即将被从页面中卸载的时候被执行
受控组件和非受控组件
- 受控组件: React的state成为“唯一数据源”,渲染表单的React组件还控制着用户输入过程中表单发生的操作,被React以这种方式控制取值的表单输入元素就叫做“受控组件”
- 非受控组件:将真实数据储存在DOM节点中,可以使用ref获取DOM节点的方式获取组件的值
- 受控组件和非受控组件主要针对表单提交而言,两者有以下区别
- 受控组件:使用state来管理状态,并为每种事件的改变编写一种处理程序
- 非受控组件:使用ref获取DOM节点的方式,直接获取值
- 在受控组件上给value属性指定值后会阻止用户更改输入,如果指定了value,但输入仍可编辑,则可能是将value设置为undefined或null
- 非受控组件可以使用defaultValue/defaultChecked来为元素指定默认值
- 在React中,<input type="file" /> 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制
- 使用示例:
CommentBox.js
和CommentBoxNew.js
- 参考内容
父子组件中数据的传递
向子组件中传递数据
- 通过props传递
子组件向父组件传递数据
- 使用父组件传递函数的方式,并在子组件中调用来影响父组件的数据,注意this的绑定问题,this需要绑定到父组件上
React开发思想
- 状态提升(lifting state up)
- 自上而下的数据流(top-down data flow)
Context
- 使用示例:
App.js
和ThemedBar.js
Context是什么
- Props属性是自上而下单项传递的
- Context提供了在组件中共享数据的方法,而不用在每个组件中层层传递,例如:主题,认证的用户等
Context使用
- 设计目的是共享那些对于组件来说是全局数据的数据
- 不要仅仅为了避免在几个层级下的组件传递props而使用context
- 每个Context对象都会返回一个Provider React组件,它允许消费组件订阅context的变化
- 当Provider的value值发生变化时,它内部的所有消费组件都会重新渲染。Provider及其内部consumer组件都不受制于shouldComponentUpdate函数,因此当consumer组件在其祖先组件退出更新的情况下也能更新
订阅Context的两种方式
- 使用contextType订阅
关键代码
- Provider,然后使用它的值
- const value = this.context;
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树
// 为当前的 theme 创建一个 context(“light”为默认值)
const ThemeContext = React.createContext('light');
// 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。
// 此属性可以使用 this.context 来消费最近 Context 上的那个值。可以在任何生命周期中访问到它,包括 render 函数中
class MyClass extends React.Component {
// 指定 contextType 读取当前的 ThemeContext
// React 会往上找到最近的 ThemeContext Provider,然后使用它的值
static contextType = ThemeContext;
// 上面写法与以下写法等价
// MyClass.contextType = ThemeContext;
render() {
// 使用this.context直接获取context中的值
const value = this.context;
/* 基于这个值进行渲染工作 */
}
}
- 使用Consumer组件包裹订阅
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
React官方文档笔记
创建基于TypeScript的项目
- 执行命令:
npx create-react-app my-app --template typescript
添加TypeScript到现有项目中
- 执行命令:
npm install --save-dev typescript
- 添加TypeScript配置文件(tsconfig.json):
npx tsc --init
- 配置源码和编译后代码的输出位置
{
"compilerOptions": {
// ...
"rootDir": "src",
"outDir": "build"
// ...
},
}
严格模式
- 使用<React.StrictMode></React.StrictMode>包裹组件可以使组件运行在严格模式下
- 跟Fragment一样,StrictMode不会渲染任何可见的UI
- 严格模式检查仅在开发模式下运行;它们不会影响生产构建
PropTypes和defaultProps
- PropTypes 仅在开发模式下对props进行类型校验
- PropTypes和defaultProps主要用于类组件中
import PropTypes from 'prop-types';
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}
// 对组件的prop做校验的写法
Greeting.propTypes = {
// 在以下语句中,string表示name的类型为string,isRequired表示属性必传
name: PropTypes.string.isRequired,
// 其中element代表了限制的类型,此处为限制子元素
children: PropTypes.element.isRequired,
};
// defaultProps 用于确保 this.props.name 在父组件没有指定其值时,有一个默认值。
// propTypes 类型检查发生在 defaultProps 赋值后,所以类型检查也适用于 defaultProps
Greeting.defaultProps = {
name: 'Stranger'
};
- 为函数组件添加PropTypes
import PropTypes from 'prop-types'
function HelloWorldComponent({ name }) {
return (
<div>Hello, {name}</div>
)
}
HelloWorldComponent.propTypes = {
name: PropTypes.string
}
export default HelloWorldComponent
组件内方法调用顺序
- 当组件被传给ReactDOM.render()方法时,React会调用组件的构造函数
- 调用组件的render方法,然后React会更新DOM来匹配组件渲染的输出
- 组件的输出被插入到DOM后,React就会调用ComponentDidMount()生命周期方法
- 当组件状态有更新时,将重新调用render方法来渲染页面
- 当组件从DOM中移除时,React会调用componentWillUnmount()生命周期方法
forceUpdate
- 默认情况下,当组件的state或props发生变化时,组件将重新渲染。如果需要以自定义的方式让组件重新渲染,则可以调用forceUpdate()强制让组件重新渲染
- 调用forceUpdate()将致使组件调用render()方法,此操作会跳过该组件的shouldComponentUpdate()
事件处理
- React事件的命名采用小驼峰式(camelCase),而不是纯小写
- 使用JSX语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
- 在React中不能通过返回false的方式阻止默认行为,必须显式的使用preventDefault,例如,传统的HTML中阻止链接默认打开一个新页面,可以这样写
<a href="#" onclick="console.log('The link was clicked.'); return false">
Click me
</a>
在 React 中,可能是这样的
function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}
- 通常情况下,在组件的事件回调中,如果没有在方法后面添加()使其立即执行,例如 onClick=,则应该为这个方法绑定 this,
这是因为class的方法默认不会绑定this。如果忘记绑定this.handleClick并把它传入了onClick,当调用这个函数的时候this的值为undefined - 向事件处理程序传递参数
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
// 在这两种情况下,React的事件对象e会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过bind的方式,
// 事件对象以及更多的参数将会被隐式的进行传递
条件渲染
使用if判断进行条件渲染
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}
使用与运算符 &&
{unreadMessages.length > 0 && <h2> You have {unreadMessages.length} unread messages.</h2>}
// 在 JavaScript 中,true && expression 总是会返回 expression, 而 false && expression 总是会返回 false
// 因此,如果条件是 true,&& 右侧的元素就会被渲染,如果是 false,React 会忽略并跳过它
使用三目运算符
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}
阻止组件渲染
- 在render方法中直接返回null可以阻止组件渲染,但其并不会影响组件的生命周期
组合和继承
- 有的组件的展示内容需要在具体使用的时候决定,无法提前知晓,比如模态框的展示内容等,此时可以使用children prop来处理
- 在以下示例中,<FancyBorder> JSX标签之间的所有内容都会作为一个children prop传递给FancyBorder组件。以下示例中,FancyBorder会将
渲染在一个<div>中
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
- 除此之外,也有类似vue插槽的功能
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}
错误边界
错误边界
- 只有class组件才可以成为错误边界组件
- 错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误
- 自React 16起,任何未被错误边界捕获的错误将会导致整个React组件树被卸载
错误边界使用
- static getDerivedStateFromError():用于在内部渲染备用UI
- componentDidCatch():打印错误信息
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
Fragments
- Fragments用于包裹一些其他的标签或者组件,但起本身不会被渲染到DOM节点中,其作用与Vue中的<template>标签相同
- 可使用
<> </>
短语法来代替React.Fragment
,但这样写后不能支持key属性 - 用法示例
class Columns extends React.Component {
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
}
}
// 渲染结果
<table>
<tr>
<td>Hello</td>
<td>World</td>
</tr>
</table>
Portals
- Portals提供类似Vue中瞬间移动的功能,目的是将一个组件挂载到父组件之外的DOM节点,可用于做页面的模态框,提示框等,实际上antd组件库中的Modal组件即是使用Portals实现的
- 补充内容:模态框:模态框描述了 UI 的一部分,如果一个元素阻挡了用户与应用的其它部分的互动,这个元素就是模态的
以下示例是使用Portals手动实现模态框的功能
---------- Portals弹框部分 ----------
interface PropsType {
visible: boolean,
onHide: () => void
}
export const PortalDialog: React.FC<PropsWithChildren<PropsType>> = (props) => {
const {visible, onHide, children} = props
return (
<>
{visible ? createPortal(<div>
<div className={styles["mask"]}/>
<div className={styles["portal"]}>
{children}
<Button onClick={onHide}>关闭弹框</Button>
</div>
// 注意:以下的dialog-root与App组件对应的root元素处于同一层级
</div>, (document.getElementById("dialog-root") as Element)) : null}
</>
)
}
---------- 调用弹框部分 ----------
export const DialogPage: React.FC = () => {
const [isPortalVisible, setIsPortalVisible] = useState(false)
const showPortal = () => {
setIsPortalVisible(true)
}
const hidePortal = () => {
setIsPortalVisible(false)
}
return (
<>
<Button onClick={showPortal}>打开弹框</Button>
<PortalDialog visible={isPortalVisible} onHide={hidePortal}>
// 弹框中要展示的内容
<div>我是弹框中的内容</div>
</PortalDialog>
</>
)
}
---------- 弹框样式声明部分 ----------
// 遮罩层
.mask {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: #ccc;
opacity: .5;
}
// 弹框
.portal {
position: absolute;
padding: 2rem;
width: 30rem;
height: 20rem;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
border-radius: .5rem;
border: 1px solid #ddd;
box-shadow: 0 0 20px 2px #ddd;
z-index: 100;
}
Refs
- Refs提供了一种访问DOM节点或访问在render方法中创建的React元素的方式
创建或设置Refs的方式
使用createRef创建并设置Refs
该方法仅能在class组件中使用
// 访问dom节点
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
使用回调方式设置Refs
- 使用回调方式设置Refs在React 16.3之前比较常用,使用这种方式时需要传递一个回调函数
- React将在组件挂载时,会调用ref回调函数并传入DOM元素,当卸载时调用它并传入null,在componentDidMount或componentDidUpdate触发前,React会保证refs一定是最新的
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = null;
this.focusTextInput = () => {
// 使用原生 DOM API 使 text 输入框获得焦点
if (this.textInput) this.textInput.focus();
};
}
componentDidMount() {
// 组件挂载后,让文本框自动获得焦点
this.focusTextInput();
}
render() {
// 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React 实例上(比如 this.textInput)
return (
<div>
<input type="text" ref={dom => this.textInput = dom}/>
<input type="button" value="Focus the text input" onClick={this.focusTextInput}/>
</div>
);
}
}
访问Refs
- 访问方式:
const node = this.myRef.current;
- ref 的值根据节点的类型而有所不同
- 当ref属性用于HTML元素时,构造函数中使用React.createRef()创建的ref对象接收底层DOM元素作为其current属性
- 当ref属性用于自定义class组件时,ref对象接收组件的挂载实例作为其current属性,此时可以通过实例直接调用子组件的方法
- 不能在函数组件上使用ref属性,因为他们没有实例
- React会在组件挂载时给current属性传入DOM元素,并在组件卸载时传入null值。ref会在componentDidMount或componentDidUpdate生命周期钩子触发前更新
Refs与函数式组件
以上创建和使用Refs的方式均只能使用在class组件中,注意:无法在函数组件上使用 ref 属性,因为他们没有实例
function CustomTextInput(props) {
// 这里必须声明 textInput,这样 ref 才可以引用它
const textInput = useRef(null);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput} />
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}
在组件之间传递Refs
Refs可以在组件之间进行传递,不管是回调形式创建的Refs还是通过 React.createRef()创建的Refs
Refs转发
- 在Hook出现之前,Refs转发是常用的在函数式组件中使用 ref 的方式
- Refs转发是指允许某些组件接收 ref,并将其向下传递(即转发)给子组件
- Refs转发通常会使用forwardRef来进行获取,forwardRef的参数为一个渲染函数,渲染函数接收props和ref参数并返回一个React节点,示例如下
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 可以直接获取 DOM button 的 ref,并在必要时访问
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
通过Props传递
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}
class Parent extends React.Component {
render() {
return (
<CustomTextInput
inputRef={el => this.inputElement = el}
/>
);
}
}
在以上示例中:Parent 把它的 refs 回调函数当作 inputRef props 传递给了 CustomTextInput,而且 CustomTextInput 把相同的函数作为特殊的 ref 属性传递给了 <input>。结果是,在 Parent 中的 this.inputElement 会被设置为与 CustomTextInput 中的 input 元素相对应的 DOM 节点
Hook
Hook是什么
- Hook是一些可以在函数组件里“钩入”React state及生命周期等特性的函数,也就是说使用Hook可以在不编写class的情况下使用state以及其他的React特性
- Hook的本质是JavaScript函数
Hook的使用规则
- 只能在React函数的最顶层以及任何return之前调用Hook,不要在循环,条件判断或嵌套函数中调用
补充说明:Hook只能放在顶层是因为在使用多个Hook时,react为了识别Hook与state的对应关系,是按照hook的调用顺序来对应的,如果放在条件语句中,
则可能导致有的Hook不再被执行,从而破坏了顺序导致对应关系也被破坏,这样将产生bug,因此只能在React函数的最顶层使用,如果需要增加条件判断,
则只能在Hook内部进行,为保证这条规则不被破坏,也推荐使用lint插件进行规则检查
- 只能在React的函数组件中调用Hook,Hook在普通的JavaScript函数和class组件内部不起作用
- 自定义Hook中也可以调用Hook
- 可使用eslint插件强制执行规则检查,需要额外安装的
eslint-plugin-react-hooks
依赖,安装命令为:npm install eslint-plugin-react-hooks --save-dev
,对应的ESLint配置对下
// 你的 ESLint 配置
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
"react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
}
}
useState
- 使用useState后,由于可以在函数中管理数据的状态,从而不用再转换为class并在构造函数中声明this.state,这使得函数组件具有了class的功能
- 一般而言,在函数退出后变量就会”消失”,而state中的变量会被React保留,React会在重复渲染时记住它当前的值,并且提供最新的值给函数组件
- useState可以直接使用数字、字符串或者对象来声明一个状态值,如果是多个状态值,则多次调用useState即可
- useState的返回值为:当前state(state始终为更新后最新的值)以及更新state的函数,分别表示了class中的this.state.xx和this.setState
- 如果新的state需要通过使用先前的state计算得出,那么可以将函数传递给setState,该函数将接收先前的state,并返回一个更新后的值,参考内容
- useState的initialState参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始state需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的state,此函数只在初始渲染时被调用
- 将函数传给useState时会立即执行并将结果作为state,在set方法调用时,如果传入一个函数,函数也会立即执行;所以,当要用useState保存函数时,不能直接传入要保存的函数,需要将要保存的函数作为传给useState或者set方法的函数的返回值
- React会确保setState函数的标识是稳定的,并且不会在组件重新渲染时发生变化,因此在使用useEffect或者useCallback时可以从依赖中省去setState
- useState返回的set函数是异步处理的
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
</>
);
}
- 读取State(以count变量为例)
- 在class中使用this.state.count
- 在函数中,可以直接使用count
- 更新State(以count为例)
- 在class中,通过this.setState()来更新
- 在函数中,直接调用setCount方法,并传入新的值即可
useReducer
- useReducer是useState的替代方案,在state逻辑复杂且包含多个子值或者下一个state依赖之前的state的场景下比useState更适用
- 基本用法:
const [state, dispatch] = useReducer(reducer, initialArg, init);
- 如果 useReducer 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行(React 使用 Object.is() 比较算法 来比较 state)
- 两种初始化useReducer state的方式
- 方式一:将state作为第二个参数传入useReducer(最简单)
const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
);
- 方式二:惰性创建初始state,需要将init函数作为useReducer的第三个参数传入,这样初始state将被设置为
init(initialArg)
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
- useReducer跟redux的用法非常类型,但也存在以下两点主要的差异
- useReducer 无法获取全局 store,必须要搭配 useContext 才能做到类似轻量化的 redux
- useReducer 没有 middleware ,不能像 Redux 一样能用 thunk 或 saga 来异步处理数据
useEffect
- useEffect是副作用函数,React会在每次渲染后调用副作用函数,包括第一次渲染的时候,React保证了每次运行effect的同时,DOM都已经更新完毕
- useEffect是通过创建闭包的方式执行的,此时内部只能看到创建闭包时候的数据值,因此如果需要根据数据的变化来改变内部执行的逻辑,则需要加入依赖
- useEffect可以看做是componentDidMount、componentDidUpdate、componentWillUnmount这三个生命周期函数的组合
- 由于通常会有让组件在加载和更新时执行相同的操作,即在每次渲染后执行相同操作,但是在class组件中没有提供这样的方法,这时只能在componentDidMount和componentDidUpdate增加一样的逻辑来实现
- effect的执行时机:与componentDidMount、componentDidUpdate不同的是,传给useEffect的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用,但会保证在任何新的渲染前执行。在开始新的更新前,React总会先清除上一轮渲染的effect
- 需要清除和不需要清除是两种常见的副作用操作,需要清除时,可使用回调函数来指定如何“清除”副作用,回调函数会在组件销毁时执行
- 每次重新渲染后,都会生成新的effect,替换掉之前的effect,正是基于这个原因,effect会提供变量的最新的值,而不用担心其会过期
- Hook允许按照代码的用途来分割代码,所以跟useState一样,useEffect可以在组件中多次使用,而不是像声明周期函数那样,React将按照effect声明的顺序依次调用组件中的每一个effect
- useEffect使用技巧
- 如果useEffect依赖的数据为一个state且其改变频繁,则可以使用useState传入一个函数的方式去掉这个依赖
- 不需要清除的effect
- 与componentDidMount和componentDidUpdate不同过的是,使用useEffect调度的effect不会阻塞浏览器更新屏幕,这会让应用看起来响应更快
- 需要清除的effect
- 如果需要声明一个需要清除的effect,则可以在effect中返回一个函数,React会在执行清除操作时调用它,而React会在每次重新渲染和卸载组件的时候执行清除操作
- 通过跳过对effect函数的调用来进行性能优化
- 在class组件中,可以在componentDidUpdate中添加比较的逻辑进行性能优化,例如
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
- 在useEffect中,可以通过传递数组作为第二个可选参数的方式进行优化,对于有清除操作的effect同样适用
- 特别注意,如果第二个参数传入一个空数组,意味着该hook只在组件挂载时运行一次,如果不传入第二个参数,则useEffect会在每次页面渲染的时候执行
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
useContext
- 基本使用:
const value = useContext(MyContext);
- 接收一个context对象(React.createContext 的返回值)并返回该context的当前值,当前的context值由上层组件中举例最近的<MyContext.Provider>的value prop决定
- 其原理是读取context的值以及订阅context的变化,具体使用时需要在上层组件树中使用<MyContext.Provider>来为下层组件提供context
- 当组件上层最近的<MyContext.Provider>更新时,该Hook会触发重新渲染,并使用最新传递给MyContext provider的context value值
- useContext的参数必须是context对象本身
- 使用步骤
- 创建context
const defaultContextValue = {
username: '猪小明'
}
export const appContext = React.createContext(defaultContextValue)
- 传递context
ReactDOM.render(
<appContext.Provider value={defaultContextValue}>
<App/>
</appContext.Provider>
)
- 接收context
/**
* 使用Consumer接收
*/
const Robot = (props) => {
return (
<appContext.Consumer>
{(value) => {
<div>
<p>作者:{value.username}</p>
</div>
}}
</appContext.Consumer>
)
}
/**
* 使用userContext接收
*/
const Robot = (props) => {
const value = useContext(appContext)
return (
<div>
<p>作者:{value.username}</p>
</div>
)
}
useRef
- useRef返回一个可变的ref对象,其 .current 属性被初始化为传入的参数(initialValue),返回的ref对象在组件的整个生命周期内保持不变,本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”
- useRef() 比class组件中的 ref 属性更有用,它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式
- useRef创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象
- 当 ref 对象内容发生变化时,useRef 并不会发出通知,变更.current属性也不会引发组件重新渲染
// 示例一:useRef的常规使用
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
// 示例二:使用useRef惰性创建一次对象的示例
function Image(props) {
const ref = useRef(null);
// IntersectionObserver 只会被·创建一次
function getObserver() {
if (ref.current === null) {
ref.current = new IntersectionObserver(onIntersect);
}
return ref.current;
}
// 当你需要时,调用 getObserver()
// ...
}
useImperativeHandle
useImperativeHandle 可以在使用 ref 时自定义暴露给父组件的实例值,通常其应当与 forwardRef 一起使用,但在大多数情况下,应当避免使用 ref 这样的命令式代码,语法:useImperativeHandle(ref, createHandle, [deps])
,示例如下
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
在以上示例中,渲染 <FancyInput ref= /> 的父组件可以调用inputRef.current.focus()
useCallback
- 返回一个memoized回调函数
- 把内联回调函数以依赖项数组作为参数传入useCallback,它将返回该回调函数的memoized版本,该回调函数仅在某个依赖项改变时才会更新
useCallback(fn, deps)
相当于useMemo(() => fn, deps)
const memoizedCallback = useCallback(
(函数参数) => {
函数体;
},
[a, b],
);
useMemo
- 返回一个 memoized 值
- 把创建函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算memoized值,这种优化有助于避免在每次渲染时都进行高开销的计算
- 如果没有依赖项传入,useMemo在每次渲染时都会计算新的值
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
React是如何把对Hook的调用和组件联系起来的
- 每个组件内部都有一个"记忆单元格"列表,它们只不过是用来存储一些数据的JavaScript对象。当用useState()调用一个Hook的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个useState()调用会得到各自独立的本地state的原因
Redux
Redux工作流程
- 以下以通知朋友修改其动态错别字的方式来类比整个模块
- React Component:订阅了朋友圈的人
- Action Creators:打电话或者发短信的行为
- Store:朋友圈
- Reducers:修改朋友圈的哪条动态以及修改方式(修改哪一个错别字,同时包含了朋友圈的初始化数据(原数据))
Redux使用步骤
- 创建store(createStore),并同时给store传入一个修改朋友圈动态的方式(reducer)
- 使用dispatch告诉store需要修改的朋友圈动态
- 在reducer中设置修改朋友圈的方式(修改哪一个错别字,同时包含了朋友圈的初始化数据(原数据))
- store订阅store中的朋友圈动态消息事件
Redux设计和使用的基本原则
- store是唯一的
- 只有store能改变自己的内容(reducer中虽然操作了store中的数据,但具体的改变还是store本身在执行)
- reducer必须是纯函数(纯函数:给定固定的输入就一定会有固定的输出,而且不会有任何的副作用)
原生Redux的使用
安装命令:npm install redux -S
---------- 第一步:创建reducer ----------
import i18n from "i18next";
import {ADD_LANGUAGE, CHANGE_LANGUAGE, LanguageActionTypes} from "./languageActions";
export interface LanguageState {
language: "en" | "zh";
languageList: { name: string, code: string }[]
}
const defaultState: LanguageState = {
language: "zh",
languageList: [
{
name: "中文", code: "zh",
}, {
name: "English", code: "en"
}
]
}
export const languageReducer = (state = defaultState, action: LanguageActionTypes) => {
console.log(action)
switch (action.type) {
case CHANGE_LANGUAGE:
i18n.changeLanguage(action.payload) // 此用法将产生副作用,不符合reducer的标准
return {
...state,
language: action.payload
}
case ADD_LANGUAGE:
return {
...state,
languageList: [...state.languageList, action.payload]
}
}
return state
}
---------- 第二步:创建store ----------
import {compose, createStore} from "redux";
import {languageReducer} from "./language/languageReducer";
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export const store = createStore(languageReducer, composeEnhancers())
---------- 第三步:获取store中的state并订阅store中的数据状态变更事件 ----------
class HeaderComponent extends React.Component<RouteComponentProps & WithTranslation, State> {
constructor(props: RouteComponentProps & WithTranslation) {
super(props);
const storeState = store.getState();
this.state = {
language: storeState.language,
languageList: storeState.languageList
}
// 订阅store
store.subscribe(this.handleStoreChange)
}
handleStoreChange = () => {
const { language, languageList } = store.getState()
this.setState({
language, languageList
})
}
changeLanguage = () => {
const {language, languageList} = this.state
const otherLanguage = languageList.filter(e => e.code === language)[0]
store.dispatch(getLanguageChangeAction(otherLanguage.code))
}
render() {
return (
<button onClick={this.changeLanguage}>切换语言</button>
);
}
}
Redux第三方库
- React-Redux
- Redux-toolkit插件,使用方式可参考这里
- Redux中间件,主要指Redux-thunk和Redux-sage,用于处理异步请求的redux,真实项目中的Redux架构往往需要借助于中间件才能完成,常用中间件如下
- Redux中间件往往用来对Redux本身进行扩展,只作用于Action和store的部分
- Redux中间件是对redux的dispatch的升级
Redux第三方库之React-Redux插件
- React-Redux是一个第三方模块,用于在react中方便的使用redux
- 安装命令:
npm install react-redux -S
- React-Redux使用步骤
- 使用与原生redux相同的方式创建reducer和store
- 使用React-Redux的Provider组件连接store进行数据传递
- 使用store,使用方法包括使用connect生成高阶组件的方式和使用React-Redux hook两种方式,由于hook的方式较为简单,此处主要介绍使用connect生成高阶组件的步骤
- 使用connect将自定义组件和store进行连接
- 指定连接的方式(mapStateToProps指定获取store中数据的映射规则,mapDispatchToProps指定改变store中的数据的映射规则),作为connect的参数
- 通过上一步,可以直接像使用props中的属性的方式一样使用映射规则中指定的属性或者方法
---------- 第一步:创建reducer和store,创建方式与原生的redux方式相同,此处直接省略 ----------
---------- 第二步:使用Provider组件连接store进行数据传递 ----------
const ReactReduxApp = (
// 将Provider和store进行连接
<Provider store={store}>
<ReactReduxTodoList />
</Provider>
);
ReactDOM.render(
ReactReduxApp,
document.getElementById('root')
)
---------- 第三步:使用store ----------
/**
* 使用方式一:使用connect创建高阶组件并且进行state和dispatch映射的方式获取数据
*/
// 指定连接的规则
const mapStateToProps = (state) => {
return {
inputValue: state.inputValue
}
}
// store.dispatch to props
const mapDispatchToProps = (dispatch) => {
return {
handleInputChange (e) {
const action = getInputChangeAction(e.target.value)
dispatch(action)
}
}
}
// 声明Props的类型
type PropsType = ReturnType<typeof mapState>
& ReturnType<typeof mapDispatch>
const ReactReduxTodoList: React.FC<PropsType> = (props) => {
const {inputValue, handleInputChange} = props
// 使用inputValue和handleInputChange属性
}
// 将自定义组件和store进行连接
export default connect(mapStateToProps, mapDispatchToProps)(ReactReduxTodoList)
/**
* 使用方式二:在函数组件中使用hooks的方式获取数据
*/
export const Header: React.FC = () => {
// 连接store,获取store中存储的language对象的值
const language = useSelector(state => state.language.language)
const languageList = useSelector(state => state.language.languageList)
// 获取dispatch作为action转发的函数
const dispatch = useDispatch()
// const dispatch = useDispatch<Dispatch<LanguageActionTypes>>()
const menuClickHandler = (e: any) => {
if (e.key === 'new') {
dispatch(addLanguageActionCreator("新语言", "new_lang"))
return
}
dispatch(changeLanguageActionCreator(e.key))
}
return (
<div onClick={menuClickHandler} className={style['app-header']}>
</div>
)
}
Redux中间件之Redux-thunk
- Redux-thunk用于实现在异步函数中使用redux,并不是代码必须,但有助于代码的关注点分离
- Redux-thunk扩展了dispatch的支持范围,从只支持对象扩展到可以支持函数
- 使用Redux-thunk时,要求传入的action为一个函数(redux本身要求传入的action必须为一个对象)
- 安装命令:
npm install redux-thunk -S
- Redux-thunk使用步骤
- 引入中间件,注意如果需要同时引入redux-devtools时,可参照redux-devtools在github文档中的写法
- 增加处理异步请求的action,在action内部调用异步方法,同时异步方法完成后使用dispatch(传入的参数)转发真实需要处理的action
// 引入中间件
const composeEnhancers =
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
}) : compose;
const enhancer = composeEnhancers(
applyMiddleware(thunk),
);
export const store = createStore(reducer, enhancer);
//----------------------------------------
// 处理异步请求的action
export const getListAction = () => {
return (dispatch) => {
axios.get('http://localhost.charlesProxy.com:3000/api/list.json')
.then(res => {
const action = initListAction(res.data)
dispatch(action)
})
}
}
Redux中间件之Redux-saga
- Redux-saga可以将异步文件拆分到sagas这样的文件来单独管理
- 安装命令:
npm install redux-saga -S
- Redux-saga使用步骤,示例项目
- 引入中间件,注意如果需要同时引入redux-devtools时,可参照redux-devtools在github文档中的写法
- 编写generator函数,并将其导出作为参数传入中间件的run方法中
// 引入中间件,注意createStore和run方法的调用顺序
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// noinspection JSUnresolvedVariable
const composeEnhancers =
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
}) : compose;
const enhancer = composeEnhancers(
applyMiddleware(sagaMiddleware),
);
export const store = createStore(reducer, enhancer);
// 在run方法中传入saga
sagaMiddleware.run(mySaga)
//----------------------------------------
// 捕获action并在其中加入异步获取数据的逻辑,注意
function* getInitList() {
try {
const res = yield axios.get('http://localhost.charlesProxy.com:3000/api/list.json')
const action = initListAction(res.data)
yield put(action)
} catch (e) {
console.log('list.json 网络请求失败');
}
}
// saga必须导出为一个generator函数
// generator 函数
export function* mySaga() {
// 监听redux的action,当接收到type为GET_INIT_LIST的action时,调用getInitList函数
yield takeEvery(GET_INIT_LIST, getInitList)
}
按需异步加载组件
在访问页面时,默认会把所有源码打包的js文件一次性加载,但是实际上有很多页面的内容并不会用到,因此可以使用异步加载的方式
React16.8以后可以使用React.Lazy和React.Suspense组合的方式来使用异步加载,但写法比较繁琐,更推荐使用第三方的react-loadable库来实现异步加载,示例项目
使用react-loadable异步加载的实现步骤
- 如果组件中需要接收外部传入的参数,由于此时外部没有直接引入组件,此时需要使用withRouter将组件包裹
class Detail extends React.Component {
componentDidMount() {
// 正常获取参数
const id = this.props.match.params.id
// 使用参数...
}
render() {
const {title, content} = this.props
return (
<DetailWrapper>
<Header>{title}</Header>
<Content dangerouslySetInnerHTML = {{__html: content}} />
</DetailWrapper>
)
}
}
const mapState = (state) => ({})
const mapDispatch = (dispatch) => ({})
export const MyDetail = connect(mapState, mapDispatch)(withRouter(Detail))
- 编写loadable.js文件,在其中设置异步加载的组件,有以下两种方式
/**
* 方式一:当需要异步加载的组件的导出方式为默认导出时
*/
export const LoadableDetail = Loadable({
// 此处由于loadale.js文件放在组件相同的文件夹下,且组件存在的路径为:index.js文件中,因此可直接写./
loader: () => import('./'),
loading: () => <div>数据加载中</div>
})
/**
* 方式二:当需要异步加载的组件的导出方式为命名导出时
*/
export const LoadableDetail = Loadable({
// 指定组件的路径
loader: () => import('./index'),
loading: () => <div>数据加载中</div>,
render: (loaded, props) => {
// 指定需要加载的组件名称
const MyDetail = loaded.MyDetail
// 返回组件
return <MyDetail/>
}
})
- 引用异步加载组件
function App() {
return (
<div className="App">
<Provider store={store}>
<BrowserRouter>
<Route path='/detail/:id' exact component={LoadableDetail}/>
</BrowserRouter>
</Provider>
</div>
)
}
添加CSS样式文件
- 直接引入css文件会对对应组件整个组件树的样式产生影响,可使用CSS-in-JS的方式解决此问题,同时也有styled-components和emotion等好用的第三方库可以更加方便的使用样式
原生的CSS-in-JS
- 常规的css引用方式:
import './index.css'
,使用此种方式极易造成css的全局样式污染以及命名冲突等问题 - 使用原生的CSS-in-JS其原理是将css文件作为对象引入,通过访问对象来独立加载样式,示例:
import styles from './index.module.css'
- 使用CSS-in-JS的方式往往还需要额外的插件来支持代码的智能提示:
npm install typescript-plugin-css-modules --save-dev
使用步骤
- 编写普通的css样式文件
/**
* App.module.css
*/
.App {
text-align: center;
}
.App-header {
display: flex;
flex-direction: column;
align-items: center;
justify-items: center;
color: white;
}
- 引入并使用样式
import styles from './App.module.css'
function App() {
return (
// 对应的样式为:App.module.css文件中的.App类
<div className={styles.App}>
</div>
)
}
第三方组件库的使用
styled-components
- 安装styled-components:
npm install styled-components -S
- 使用步骤,示例项目
- 在App.js中引入
style.js
- 在
style.js
中编写全局样式,注意从4版本开始,全局样式需要使用createGlobalStyle来创建- 在App.js根标签中增加
<GlobalStyle/>
即可影响全局的样式
emotion
- 安装emotion:
npm install @emotion/react -S
npm install @emotion/styled -S
- 使用emotion写行内样式,示例项目
- 在页面组件的顶部写上如下代码,告知组件中使用了 emotion 行内样式
/* @jsxImportSource @emotion/react */
- 行内样式编写格式:
css={ /* 行内样式代码 */ }
图标使用
通过styled-components使用图标
- 安装styled-components:
npm install styled-components -S
- 下载图标,注意下载时需要在项目中设置包含的文件,选择:woff,ttf,eot,svg,base64
- 将图标文件(包括:.woff,.svg,.ttf,.eot,.css)拷入项目中,修改iconfont.css文件名为iconfont.js
- 修改.woff,.svg,.ttf,.eot,.css对应的url的引用路径,改为相对路径,将class声明删除
- 引入styled-components,使用createGlobalStyle创建全局样式并导出
- 在项目中使用
<span class="iconfont">3</span>
的方式使用图标,具体使用方式可参照这里和这里
以组件的形式使用svg
- 将图标作为组件引入
import { ReactComponent as SoftwareLogo } from "logo.svg";
- 使用图标组件
const PageHeader = () => {
return (
<Header between={true}>
<ButtonNoPadding type={"link"} onClick={resetRoute}>
<SoftwareLogo width={"5rem"} color={"rgb(38, 132, 255)"} />
</ButtonNoPadding>
</Header>
);
};
immutable
- 示例项目
- 使用immutable的fromJS方法可以将对象转换为不可变对象
- 此时可以直接使用对象的set方法,此方法会结合之前immutable对象的值,和设置的值,返回一个新的对象
- 使用示例:
src/common/header/store/reducer.js,src/common/header/index.js
- 使用redux-immutable下的combineReducers方法可以使组合了多个reducer的store最终也变成immutable的对象,示例:
src/store/reducer.js
- 使用redux-immutable的fromJS包裹的对象的复合属性也会被转换为immutable的对象,此时如果需要使用外层对象来set新值给内层的immutable对象,此时的新值也需要为immutable对象,示例:
src/common/header/store/actionCreators.js
路由与SPA
路由
- 作用:类比路由器,路由器的作用是通过读取路由表,根据tcp/ip中的地址将数据包按照最佳路线传输到指定地点,在这个过程中,数据传输的路线的计算过程就是路由(routing)
传统的网站的路由方式
- 当浏览器的url发生变化时,浏览器页面相应的发生改变,此种方式路由系统难以管理且会暴露资源的路径
现代路由方式
- js,css,html会被打包为一个大文件,一次性传输给浏览器
- js通过劫持浏览器路由,生成虚拟路由来动态渲染页面dom元素,路由与实际的文件没有对应关系
react-router
- react-router 和 react-router-dom 的关系类似于 react 和 react-dom、react-native
- react-router 用于进行路由的计算和路由状态的管理,react-router-dom 则用于消费 react-router 的计算结果,用于 dom 的呈现
- react-router-dom
- 安装react-router-dom会自动安装react-router这个核心框架
- <Link />组件可以渲染出<a/>标签
- <BrowserRouter />组件可以利用H5 API实现路由切换
- <HashRouter />组件则利用原生JS中的window.location.hash来实现路由切换
- 使用react-router实现路由的要求
- 路由导航与原生浏览器操作行为一致:
<BrowerRouter/>
- 路由的路径解析原理与原生浏览器一致,可以自动识别url路径:
<Route/>
- 路径的切换以页面为单位,不发生页面堆叠(只匹配一个Route):
<Switch/> + Route中的exact属性
- 使用react-router实现页面跳转
/**
* 第一种方式:使用withRouter高阶组件实现页面跳转,并在组件中获取参数实现跳转
*/
interface PropsType extends RouteComponentProps {
id: number | string;
}
const ProductImageComponent: React.FC<PropsType> = ({ id, history }) => {
return (
<div onClick={() => history.push(`detail/${id}`)}>
内容
</div>
)
}
export const ProductImage = withRouter(ProductImageComponent)
/**
* 第二种方式:使用useRouter获取history实现跳转,本质上跟第一种方式差不多,只是获取history的方式不一样
*/
export const Header: React.FC = () => {
const history = useHistory()
return (
<Button.Group className={style['button-group']}>
<Button onClick={() => history.push('/register')}>注册</Button>
<Button onClick={() => history.push('/signIn')}>登录</Button>
</Button.Group>
)
}
/**
* 第三种方式:使用Link超链接的方式跳转
*/
export const Header: React.FC = () => {
return (
<Layout.Header className={style['main-header']}>
<Link to='/'>
<img src={logo} alt="" className={style['App-logo']}/>
<Typography.Title level={3} className={style['title']}>环球旅游网</Typography.Title>
</Link>
</Layout.Header>
)
}
获取路由参数
/**
* 第一种方式:直接通过对象的链式调用获取
*/
import {RouteComponentProps} from 'react-router-dom'
interface MatchParams {
touristId: string;
}
export const DetailPage: React.FC<RouteComponentProps<MatchParams>> = (props) => {
return (
<div>路线id:{props.match.params.touristId}</div>
)
}
/**
* 第二种方式:使用hook的方式获取
*/
import {RouteComponentProps, useParams} from 'react-router-dom'
interface MatchParams {
touristId: string;
}
export const DetailPage: React.FC<RouteComponentProps<MatchParams>> = (props) => {
const {touristId} = useParams<MatchParams>();
return (
<div>路线id:{touristId}</div>
)
}