前端面试基础题:React JSX、高级组件和路由...
✨React介绍
如果从 MVC 的角度来看, React 仅仅是视图层(V),也就是只负责视图的渲染,而并非提供了完整的 M 和 C 的功能。
特点:声明式、基于组件、学习一次随处使用。
脚手架的使用:
- 创建项目:
npx/npm init/yarn create-react-app app-name
- 在脚手架中使用:导入
react
和react-dom
两个包
import React from 'react'
import ReactDOM from 'react-dom'
// 调用 React.createElement(创建React元素名称, 该React元素的属性, 该React元素的子节点) 方法创建react元素
// 调用 ReactDOM.render(要渲染的React元素, 用于指定渲染到页面中位置的DOM对象) 方法渲染react元素到页面中
const title = React.createElement('h1', null, 'Hello React');
ReactDOM.render(title, document.getElementById('root'));
React的设计思想:
- 组件化:每个组件都符合开放-封闭原则,封闭是针对渲染工作流来说的,指的是组件内部的状态都由自身维护,只处理内部的渲染逻辑。开放是针对组件通信来说的,指的是不同组件可以通过props(单项数据流)进行数据交互。
- 数据驱动视图:如果要渲染界面,不应该直接操作DOM,而是通过修改数据(state或prop),数据驱动视图更新。
- 虚拟DOM:虚拟DOM是对真实DOM的映射,React通过新旧虚拟DOM对比,得到需要更新的部分,实现数据的增量更新。
✨JSX
JSX 是 JavaScript XML 的简写,表示在 JavaScript 代码中写 XML(HTML) 格式的代码。
JSX仅仅是createElement()
方法的语法糖(简化语法),JSX语法被@babel/preset-react插件编译为createElement()
方法。
JSX和JS的区别:
- JS可以被打包工具直接编译,不需要额外转换,jsx需要通过babel编译,它是
React.createElement
的语法糖,使用jsx等价于React.createElement
。 - jsx是js的语法扩展,允许在html中写JS;JS是原生写法,需要通过script标签引入。
基本使用步骤:
// 使用JSX语法,创建react元素
const title = (<h1>Hello</h1>);
// 使用ReactDOM.render()方法渲染react元素到页面中
ReactDOM.render(title, root);
- React元素的属性名使用驼峰命名法
- 特殊属性名:class → className(给HTML元素添加类名),for → htmlFor(label标签),tabindex → tabIndex
- 没有子节点的React元素可以用
/>
结束 - 推荐:使用小括号包裹JSX,从而避免JS中的自动插入分号陷阱
- JSX中使用JS表达式
const name = 'Jack';
const dv = (<div>你好,我叫{name}</div>);
语法中是单大括号,不是双大括号;单大括号中可以使用任意的JavaScript表达式,不能出现语句(if / for等);JS中的对象是一个例外,一般只会出现在style属性中。
2. JSX的条件渲染
根据条件渲染特定的 JSX 结构,可以使用if/else或三元运算符或逻辑与运算符来实现 。
const loadData = () => {
if (isLoading) {
return (<div>数据加载中,请稍后...</div>);
}
return (<div>数据加载完成,此处显示加载后的数据</div>);
}
const dv = (<div>{loadData()}</div>)
3. JSX的列表渲染
如果要渲染一组数据,应该使用数组的map()
方法 ,渲染列表时应该添加key
属性,key
属性的值要保证唯一。
const songs = {
{ id: 1, name: "痴心绝对" },
{ id: 2, name: "像我这样的人" },
{ id: 3, name: "南山南" }
}
const list = (
<ul>
{ songs.map(item => <li key={item.id}>{item.name}</li>) }
</ul>
)
4. JSX的样式处理
行内样式——style
<h1 style={ { color: 'red', backgrondColor: 'skyblue' } }>
JSX的样式处理
</h1>
类名——className
<h1 className="title">
JSX的样式处理
</h1>
✨创建React组件
类组件和函数组件的区别:类组件是有状态的,即具有内部私有数据(state),函数组件是无状态的。函数组件没有生命周期方法,只有类组件有。
- 使用函数创建组件
- 使用 JS 的函数(或箭头函数)创建的组件
- 函数名称必须以大写字母开头
- 函数组件必须有返回值,表示该组件的结构
- 如果返回值为 null,表示不渲染任何内容
- 渲染函数组件: 用函数名作为组件标签名
- 组件标签可以是单标签也可以是双标签
// 创建
function Hello () {
return <div>这是我的第一个函数组件!</div>
]
// 渲染
ReactDOM.render(<Hello/>, root);
2. 使用类创建组件
- 使用ES6的class创建的组件
- 类名称也必须以大写字母开头
- 类组件应该继承
React.Component
父类,从而可以使用父类中提供的方法或属性 - 类组件必须提供
render()
方法 render()
方法必须有返回值,表示该组件的结构
class Hello extends React.Component {
render() {
return <div>Hello, Class Component!</div>
}
}
ReactDOM.render(<Hello/>, root)
3. 将组件抽离为独立JS文件
// Hello.js
import React from 'react'
class Hello extends React.Component {
render () {
return <div>Hello Class Component!</div>;
}
}
export default Hello; // 导出Hello组件
// index.js
import Hello from './Hello.js'
ReactDOM.render(<Hello/>, root);
有状态组件和无状态组件
- 函数组件又叫做无状态组件,类组件又叫做有状态组件
- 函数组件没有自己的状态, 只负责数据展示(静)
- 类组件有自己的状态, 负责更新 UI,让页面“动” 起来
- 状态(state)即数据,是组件内部的私有数据,只能在组件内部使用
- state 的值是对象,表示一个组件中可以有多个数据
- 通过
this.state
获取状态,使用this.setState({要修改的数据})
修改状态 setState
是一个异步方法,但是在setTimeout
/setInterval
等定时器里逃脱了React对它的掌控,变成了同步方法- setState方法第二个参数是一个可选的回调函数,可以获取最新的state值。回调函数会在组件更新完成之后执行,等价于在
componentDidUpdate
生命周期内执行
为什么不能直接修改state
,而是必须调用setState
?
state
值的改变,目的是页面的更新,希望React使用最新的state来渲染页面,但是直接赋值的方式并不能让React监听到state的变化。
class Hello extends React.Component {
constructor () {
super () {
// 初始化state
this.state = {
count: 0
}
}
}
// 简化语法
// state = {
// count: 0
// }
render() {
return (
<div>有状态组件: {this.state.count}</div>
)
}
}
// 修改状态
// 正确
this.setState({
count: this.state.count + 1
})
// 错误
this.state.count += 1
✨ React事件处理
事件绑定
- React事件绑定语法与DOM事件语法相似
- 语法:
on+事件名称={事件处理程序}
,比如:onClick={() => {}}
- 注意:React 事件采用驼峰命名法,比如:
onMouseEnter
、onFocus
function App() {
function handleClick () {
console.log('单击事件触发')
}
return <button onClick={this.handleClick}>点我</button>
}
class App extends React.Component {
handleClick () {
console.log('单击事件触发')
}
render () {
return <button onClick={this.handleClick}>点我</button>
}
}
事件对象
- 可以通过事件处理程序的参数获取到事件对象
- React中的事件对象叫做:合成事件(对象)
- 合成事件:兼容所有浏览器,无需担心跨浏览器兼容性问题
function handleClick(e) {
e.preventDefault()
console.log('事件对象', e)
}
<a onClick={handleClick}>点我,不会跳转页面</a>
事件绑定的this指向
事件处理程序中this
的值为undefined
。
onIncrement () {
this.setState({ // TypeError: Cannot read property 'setState' of undefined
count: this.state.count + 1
})
}
希望:this
指向组件实例(render
方法中的this
指向组件实例)
- 箭头函数
利用箭头函数自身不绑定this
的特点,render()
方法中的this
为组件实例,可以获取到setState()
。
class Hello extends React.Component {
onIncrement () {
this.setState({ count: this.state.count + 1 })
}
render () {
return (<button onClick={ () => this.onIncrement() }></button>)
}
}
2. Function.prototype.bind()
利用ES5中的bind方法,将事件处理程序中的this与组件实例绑定到一起。
class Hello extends React.Component {
constructor () {
super()
this.onIncrement = this.onIncrement.bind(this)
}
// ...省略onIncrement
render () {
return (<button onclick={this.onIncrement}></button>)
}
}
3. class的实例方法
利用箭头函数形式的class实例方法 ,注意:该语法是实验性语法,但是,由于babel的存在可以直接使用。
class Hello extends React.Component{
onIncrement = () => {
this.setState({...})
}
render () {
return (<button onclick={this.onIncrement}></button>)
}
}
✨表单处理
- 受控组件
其值受到React控制的表单元素。
- 在
state
中添加一个状态,作为表单元素的value
值(控制表单元素值的来源) - 给表单元素绑定
change
事件,将 表单元素的值设置为state
的值(控制表单元素值的变化)
state = { txt: '' }
<input type="text" value={this.state.txt} onChange={ e => this.setState({ txt: e.target.value }) } />
2. 非受控组件
借助于ref
,使用原生DOM方式来获取表单元素值。
- 调用
React.createRef()
方法创建一个对象 - 将创建好的ref对象添加到文本框中
- 通过ref对象获取到文本框的值
// 1.
constructor () {
super()
this.txtRef = React.createRef()
}
// 2.
<input type="text" ref={this.txtRef} />
// 3.
console.log(this.txtRef.current.value)
✨组件通讯
- 父组件传递数据给子组件
- 父组件提供要传递的state数据
- 给子组件标签添加属性,值为
state
中的数据 - 子组件中通过
props
接收父组件中传递的数据
// 父组件
class Parent extends React.Component {
state = { lastName: '王' }
render () {
return (
<div>传递给子组件:<Child name={this.state.lastName} /></div>
)
}
}
// 子组件
function Child (props) {
return (<div>子组件接收到的数据:{props.name}</div>)
}
2. 子组件传递数据给父组件
- 父组件提供一个回调函数(用于接收数据)
- 该函数作为属性的值,传递给子组件
// 父组件
class Parent extends React.Component {
getChildMsg = (msg) => {
console.log('接收到子组件的数据:', msg);
}
render () {
return (
<div>
子组件:<Child getMsg={this.getChildMsg} />
</div>
)
}
}
// 子组件
class Child extends React.Component {
state = { childMsg: 'React' }
handleClick = () => {
this.props.getMsg(this.state.ChildMsg)
}
render () {
rerturn (<button onclick={ this.handleClick }>点我</button>);
}
}
3. 兄弟组件
- 将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态
- 公共父组件职责: 1. 提供共享状态 2. 提供操作共享状态的方法
- 要通讯的子组件只需通过props接收状态或操作状态的方法
// 父组件
class Counter extends React.Component {
// 提供共享状态
state = { count: 0 }
// 提供修改状态的方法
onIncrement = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<Child1 count={ this.state.count } />
<Child2 onIncrement={ this.onIncrement } />
</div>
)
}
}
// 子组件
const Child1 = props => {
return <h1>计数器:{ props.count }</h1>
};
const Child2 = props => {
return <button onClick={ () => props.onIncrement() }>+1</button>
}
✨Context
用于跨组件传递数据。
- 调用
React.createContext()
创建Provider
(提供数据) 和Consumer
(消费数据)两个组件。 - 使用
Provider
组件作为父节点。 - 设置
value
属性,表示要传递的数据。 - 调用
Consumer
组件接收数据。
const { Provider, Consumer } = React.createContext()
export { Provider, Consumer }
<Provider value="pink">
<div className="App">
<Child1 />
</div>
</Provider>
<Consumer>
{data => <span>data参数表示接收到的数据 -- {data}</span>}
</Consumer>
✨Props
children属性
- children属性:表示组件标签的子节点。当组件标签有子节点时, props就会有该属性
- children属性与普通的props一样,值可以是任意值(文本、 React元素、组件,甚至是函数)
function Hello (props) {
return (
<div>
组件的子节点:{props.children}
</div>
)
}
<Hello>我是子节点</Hello>
props校验
props校验:允许在创建组件的时候,就指定props的类型、格式等
- 安装包
prop-types
- 导入
prop-types
包 - 使用
组件名.propTypes = {}
来给组件的props添加校验规则 - 校验规则通过
ProTypes
对象来指定
import PropTypes from 'prop-types'
function App(props) {
return (
<h1>Hi, {props.colors}</h1>
)
}
App.propTypes = {
// 约定colors属性为array类型
// 如果类型不对, 则报出明确错误, 便于分析错误原因
colors: PropTypes.array
}
约束规则:
- 常见类型:array、bool、func、number、object、string
- React元素类型:element
- 必填项:isRequired
- 特定结构的对象:shape({ })
// 常见类型
optionalFunc: PropTypes.func,
// 必选
requiredFunc: PropTypes.func.isRequired,
// 特定结构的对象
optionalObjectWithShape: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
})
默认值:
给props设置默认值,在未传入props时生效
function App(props) {
return (
<div>
此处展示props的默认值: {props.pageSize}
</div>
)
}
// 设置默认值
App.defaultProps = {
pageSize: 10
}
// 不传入pageSize属性
<App />
✨组件的生命周期
只有类组件才有生命周期。
挂载阶段
- constructor
- 传入参数为props,包含由父作用域传递过来的自定义属性和children(组件内部嵌套的视图结构),第一行的代码必须是
super(props)
,调用父类的构造器函数。 - 组件自有的
state
只能在这里定义。 - 作用:继承父类、定义状态state、改变this指向。
- 传入参数为props,包含由父作用域传递过来的自定义属性和children(组件内部嵌套的视图结构),第一行的代码必须是
- render
- 这个生命周期函数是必须要有的。这个生命周期函数横跨两个阶段,在挂载阶段和更新阶段都会执行。
- 在组件初始化、调用
this.setState()
、this.forceUpdate()
、props
发生变化时执行。 - 在更新阶段,如果当前组件有
shouldComponentUpdate()
并返回false
时,render()
将不执行。在挂载阶段,render()
一定会执行,shouldComponentUpdate
不影响render()
。 - 在
render
内部,return
不能使用this.setState()
。
- componentDidMount
- 相当于Vue中的
Mounted
,表示挂载阶段完成,各种业务逻辑都可在这里完成(DOM、ref、调接口、定时器)。 - 这里可以使用多次
this.setState()
,默认是异步的。
- 相当于Vue中的
更新阶段
- render
- componentDidUpdate
- 相当于Vue中的
Updated
,表示更新阶段已完成。 - 三种触发更新的方法:props变化、
this.setState()
、this.forceUpdate()
。 - 在这里可以使用
this.setState()
,但是必须给终止条件,否则造成死循环。
- 相当于Vue中的
卸载阶段
- componentWillUnmount
- 相当于Vue中的
beforeDestory
,表示当前组件即将被销毁。 - 作用:清除定时器、长链接、缓存等一些耗费性能和占用内存。
- 相当于Vue中的

✨render-props和高阶组件(HOC)
如果两个组件中的部分功能相似或相同,该如何处理?
处理方式: 复用相似的功能(联想函数封装)
复用什么? 1. state 2. 操作state的方法 (组件状态逻辑 )
两种方式: 1. render props模式 2. 高阶组件(HOC)
注意:这两种方式不是新的API,而是利用React自身特点的编码技巧,演化而成的固定模式(写法)
render-props模式
- 创建Mouse组件,在组件中提供复用的状态逻辑代码(状态、操作状态的方法)
- 将要复用的状态作为
props.render(state)
方法的参数,暴露到组件外部 - 使用
props.render()
的返回值作为要渲染的内容
class Mouse extends React.Component {
state = {
x: 0,
y: 0,
}
// 鼠标移动事件处理程序
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 监听鼠标移动
componentDidMount() {
window.addEventListener('mouseover', this.handleMouseMove);
}
render() {
return this.props.render(this.state);
}
}
class App extends React.Component {
render() {
return (
<div>
<h1>render props模式</h1>
<Mouse render={(mouse) => {
return <p>鼠标位置:{mouse.x} {mouse.y}</p>
}} />
<Mouse render={(mouse) => {
return <img src={img} alt="猫" style={{
position: 'absolute',
top: mouse.y,
left: mouse.x
}} />
}} />
</div>
)
}
}
ReactDOM.render(<App/>, document.getElementById('root'))
高阶组件
高阶组件(HOC,Higher-Order Component) 是一个函数,接收要包装的组件,返回增强后的组件高阶组件。
高级组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过prop将复用的状态传递给被包装组件WrappedComponent。
- 创建一个函数,名称约定以with开头
- 指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
- 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
- 在该组件中,渲染参数组件,同时将状态通过props传递给参数组件
// 创建高阶组件
function withMouse(WrappedComponent) {
// 该组件提供复用的状态逻辑
class Mouse extends React.Component {
state = {
x: 0,
y: 0
}
// 鼠标移动事件处理程序
handleMouseMove = (e) => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 监听鼠标移动
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}
// 解除事件监听
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
render() {
return <WrappedComponent {...this.state}></WrappedComponent>
}
}
return Mouse;
}
const Position = props => (
<p>
鼠标当前位置:(x: {props.x}, y: {props.y})
</p>
)
// 获取增强后的组件
const MousePosition = withMouse(Position)
存在问题:
- 得到的两个组件名称相同
原因:默认情况下,React使用组件名称作为displayName
解决方法:为高阶组件设置displayName便于调试时区分不用的组件 - props丢失
原因:高阶组件没有往下传递props
解决方法:渲染WrappedComponent时,将state和this.props一起传递给组件
// 问题一
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
// 问题二
<WrappedComponent {...this.state} {...this.props} />
✨setState()
setState() 是异步更新数据的。注意:使用该语法时,后面的 setState() 不要依赖于前面的 setState()。可以多次调用 setState() ,只会触发一次重新渲染。
推荐使用:setState((state, props) => {}[, callback])
- 参数state:表示最新的state
- 参数props:表示最新的props
- callback:在状态更新(页面完成重新渲染)后立即执行某个操作
setState的两个作用:1. 修改state,2. 更新组件(UI)
过程:父组件重新渲染时,也会重新渲染子组件,但只会渲染当前组件子树。
this.state = { count: 1 }
this.setState({
count: this.state.count + 1
})
console.log(this.state.count) // 1
this.setState((state, props) => {
return {
count: state.count + 1
}
})
this.setState((state, props) => {
console.log('第二次调用:', state) // 2
return {
count: state.count + 1
}
})
console.log(this.state.count) // 1
✨组件性能优化
- 减轻state
只存储跟组件渲染相关的数据(比如: count / 列表数据 / loading 等),不用做渲染的数据不要放在 state 中,比如定时器 id等,对于这种需要在多个方法中用到的数据,应该放在 this 中。 - 避免不必要的重新渲染
组件更新机制:父组件更新会引起子组件也被更新,子组件没有任何变化时也会重新渲染。
解决方式:使用钩子函数shouldComponentUpdate(nextProps, nextState)
作用:通过返回值决定该组件是否重新渲染,返回 true 表示重新渲染, false 表示不重新渲染
触发时机:更新阶段的钩子函数,组件重新渲染前执行 (shouldComponentUpdate → render)
class Hello extends Component {
shouldComponentUpdate() {
// 根据条件,决定是否重新渲染组件
return false
}
}
- 纯组件
PureComponent
与React.Component
功能相似,区别是PureComponent
内部自动实现了shouldComponentUpdate
钩子,不需要手动比较。
原理:纯组件内部通过分别对比前后两次props和state的值,来决定是否重新渲染组件
class Hello extends React.PureComponent {
render () {
return (
<div>纯组件</div>
)
}
}
纯组件内部是浅层对比,对于引用类型来说,只比较对象的引用地址是否相同。state 或 props 中属性值为引用类型时,应该创建新数据,不要直接修改原数据!
✨虚拟DOM和Diff算法
React部分更新的实现原理。
- 初次渲染时, React 会根据初始state(Model),创建一个虚拟 DOM 对象(树) 。
- 根据虚拟 DOM 生成真正的 DOM,渲染到页面中。
- 当数据变化后(setState()),重新根据新的数据,创建新的虚拟DOM对象(树)。
- 与上一次得到的虚拟 DOM 对象,使用 Diff 算法 对比(找不同),得到需要更新的内容。
- 最终, React 只将变化的内容更新(patch)到 DOM 中,重新渲染到页面。
✨路由
React路由时URL路径和组件的对应关系。
- 安装路由组件库:
react-router-dom
- 导入核心组件:
Router
、Route
、Link
- 使用Router组件包裹整个应用
- 使用Link组件作为导航菜单(路由入口)
- 使用Route组件配置路由规则和要展示的组件(路由出口)
import {HashRouter as Router, Routes, Route, Link}
<Router>
<Link to="/about">about页面</Link>
<Link to="/home">home页面</Link>
<Routes>
<Route path="/about" element={<About/>}/>
<Route path="/home" element={<Home/>}/>
</Routes>
</Router>
路由嵌套:
<Routes>
<Route path="invoices" element={<Invoices />}>
<Route path=":invoiceId" element={<Invoice />} />
<Route path="sent" element={<SentInvoices />} />
</Route>
</Routes>
分别通过如下三种路径匹配:
"/invoices"
"/invoices/123"
"/invoices/sent"
匹配模式:
- 默认情况下, React 路由是模糊匹配模式
- 模糊匹配规则:只要 pathname 以 path 开头就会匹配成功
path 代表Route组件的path属性
pathname 代表Link组件的to属性(也就是 location.pathname) - 精确匹配:只有当 path 和 pathname 完全匹配时才会展示该路由 (给默认路由添加 exact 属性)
<Route exact path="/" element=... />