前言
这不是一篇React入门教程,所以不会对React中的基础内容做详细讲解,我始终认为看官网是学习React知识点最佳方式。本文只是对React中面试可能会问到的问题进行总结及讲解,希望大家对写得不好的地方多多提出意见,下面我们开始吧~
版本问题📒
现在出去面试都会问到hooks,你知道hooks是什么时候出来的吗,React还有什么新东西出来呢?
- React16.8之前还没有hooks,hooks为函数组件赋予了状态,16.8之前的函数组件是无状态组件,那个时候写状态组件只能用类组件,通过this.setState的方式去引起render的重新渲染。
- React16还出了新的协调引擎--React Fiber,它的主要目的是使虚拟DOM可以进行增量式渲染。
经典问题❓
- MVC和MVVM的区别
-
二者都是设计模式,都是为了处理视图层、数据层以及数据视图之间的通信而诞生的
-
MVC
-
MVC = M(数据层)+ V (视图层)+ C(控制层)
- Model和View中是观察者模式,当View中发生事件处理时,会通过Controller改变Model再改变View
- 性能问题方面,在MVC中我们会大量操作DOM,频繁更新DOM,会阻塞浏览器渲染,影响用户体验
-
-
MVVM
-
MVVM = M(数据层)+ V (视图层)+ VM(控制层)
- 和MVC的区别就在于,MVVM使用ViewModel替代了controller,Model和View没有直接的联系
- ViewModel从Model中获取数据应用到View中,View中发生改变时,也会触发ViewModel
- react准确来说是MVVM中VM部分的框架
-
- react怎么实现双向绑定
- 通过state和onChange
- vue是通过Object.defineProperty实现双向绑定
- 什么是单向数据流
- 子组件只能通过props去接收父组件传递过来的数据
- react的虚拟DOM是什么,diff算法做了什么,Fiber又做了什么优化
- 是一个模拟DOM节点的js对象,通过ReactDOM.render渲染到真实DOM树中
- diff算法通过深度优先算法,对虚拟DOM树进行遍历,通过打补丁的方式,将变化更新到真实DOM中
- 由于之前的diff算法一旦开始比对,是不能中断的,如果内容比较多的对比,可能会造成阻塞,所以Fiber采用了片段式更新,对任务划分优先级,通过Scheduler对任务执行时机进行调度,尽量减少对比过程中对浏览器的阻塞。
- 此外,fiber树是一个特殊的链表结构,内部使用双缓冲模式对fiber树结构进行更新
- 类组件和函数组件有什么区别
- 思想不同:类组件是面向对象的思想,函数组件是函数式编程
- 类组件太重了,内部逻辑难以拆分和复用
- 函数组件会捕获render内部的状态,函数组件会每次都重新创建一遍,可以实现状态的同步更新
- 函数和react的理念更贴合,声明式编程
重难点
一、事件处理中传箭头函数和普通函数的区别
import React, { Component } from 'react'
export default class APP extends Component {
a = 1
handleClick () {
console.log(this.a) // 报错:Cannot read properties of undefined (reading 'a')
}
render() {
return (
<div>
<input/>
<button onClick={() => {
console.log(this.a) // 1
}}>add1</button>
<button onClick={this.handleClick}>add2</button>
</div>
)
}
}
问题分析🔍
-
箭头函数的this指向是周围环境决定的,传箭头函数的话,可以在箭头函数中获取类组件中的this
-
普通函数的this指向是由调用的对象决定的,而这里的handleClick是由react的事件系统调用的,Es6中规定类中的函数,会默认开启局部的严格模式,也就是说this不指向window,而是undefind
解决方案🙋
普通函数通过bind去绑定this,将类组件的this手动绑定到函数中
补充知识点🍬
- bind、call、apply改变this指向的区别?
- 手写bind
const obj1 = {
name: 'obj1',
getName() {
console.log(this.name)
}
}
const obj2 = {
name: 'obj2',
getName() {
console.log(this.name)
}
}
obj1.getName() // obj1
obj1.getName.bind(obj2) // this指向obj2,但是不会自动执行函数,所以不会输出内容
obj1.getName.call(obj2) // 输出obj2,this指向obj2,会自动执行函数,接受参数是一个一个地传
obj1.getName.apply(obj2) // 输出obj2,this指向obj2,会自动执行函数,接受的参数必须是严格的数组
二、React中的事件绑定和原生事件绑定的区别🌟
问题分析🔍
React并没有将事件绑定到具体的元素身上,而是在document身上,通过合成事件实现
好处:占用内存小,不用担心去移除事件,完全支持原生事件机制暴露的内容
三、React列表中为什么需要设置Key值,并且key值为什么不能设置成index
问题分析🔍
- 为什么需要key?
答:当页面中元素发生变化时,虚拟DOM会重新计算,根据key会比较方便地找出修改的地方,然后将修改的地方通过打补丁的方式同步给真实的DOM
- 为什么key不能设置为index?
答:如果列表会发生重排、增删的情况下,将key设置成index会造成同一个元素修改前后key值不一样 (只作显示的列表可用index作为key)
/** 结论🍰:
如果把index当作key的话,333这个元素在修改前后的key值发生了变化,
虚拟DOM得出的结论是删除了值为333的li元素,实际上是删除了值为222的li标签
*/
// 修改前
<ul>
<li key="111_0">111</li>
<li key="222_1">222</li>
<li key="333_2">333</li>
</ul>
// 修改后
<ul>
<li key="0">111</li>
<li key="1">333</li>
</ul>
四、React中条件渲染是创建/移除元素还是显示/隐藏元素
答:创建/移除,和vue的v-if以及v-show同理
五、React如何在页面中显示富文本
直白来讲就是在页面中直接解析代码片段,使用之前需确认该代码片段足够安全;
React DOM在渲染所有输出内容之前,默认会进行转义,所有内容都被转换成了字符串,可以有效地防止XSS(cross-site-scripting, 跨站脚本)攻击
// 使用dangerouslySetInnerHTML在页面中显示富文本内容
<span dangerouslySetInnerHTML={{
__html: '<b>123456</b>' // 会直接显示加粗的123456,将字符串中的标签解析出来
}}></span>
六、setState是同步还是异步?
问题分析🔍
setState在同步事件是异步的,因为每次state的改变都会引起重新渲染,为了提高性能,react会对组件中的setState操作进行合并,在事件循环机制中,宏任务执行完了之后才会去执行setState操作。官方解释
setState在异步事件中是同步更新状态。
这个是由react中批量更新的事务机制决定的,react会使用isBatchingUpdates变量去记录是否需要执行state队列中的内容,类似于锁的概念,当isBatchingUpdates的值变为false的时候,会开始执行state队列中的内容。在异步事件中,刚开始isBatchingUpdates是true,后面异步事件还未执行,isBatchingUpdates就变成了false,所以达到了同步的效果。
state = {
count: 0
}
add = () => {
this.setState({ count: this.state.count + 1 })
}
// 同步调用中,setState是异步的,所以最后this.state.count依然1
handleClick1 = () => {
this.add()
this.add()
this.add()
}
// 异步调用中,setState是同步的,最后this.state.count是3
handleClick2 = () => {
setTimeout(() => {
this.add()
this.add()
this.add()
}, 0)
}
解决方案🙋
setState(updater, [callback])
// 第一个参数传对象时,异步更新
setState({ count: 0 })
// 第一个参数传函数时,可以获取到最新的state和props值 => 也是把setState的异步改成同步的方法
setState((state, props) => {
return { count: state.count + props.step }
})
// 第二个参数是可选的,可以拿到合并更新完的最新结果
setState({ count: 0 },() => {
xxxx
})
七、state和props的区别和相同点
相同点
- 都是js对象,都能引起render渲染
- 都可以设置默认值
不同点
- props是从父组件传过来的属性,子组件不可以对它进行修改(props是只读的)
- 只有父组件主动更新props才能引起子组件内部的render渲染
- state是组件的内部管理状态,外部无法获取
- 只有通过setState更改state值才会引起render渲染
扩展🏄♀️
props的类属性类型验证(出于性能方面考虑,propTypes只在开发模式下进行检查)
// 使用组件 <App title="测试" show={false}/> //写组件 import PropTypes from 'prop-types' export default Class App extends React.component{ // 写法1:在类的里面定义 static propTypes = { title: PropTypes.string, // 限制title为字符串 show: PropTypes.bool // 限制show为布尔值 } // 设置默认值 static defaultProps = { show: true } render() { const { title, show } = this.props return {show && <div>{title}</div>} } } // 写法2:在类的外面写 App.propTypes = { title: PropTypes.string, // 限制title为字符串 show: PropTypes.bool // 限制show为布尔值 }PS:函数式组件只能在组件外面写defaultProps和propTypes
八、通信
受控组件和非受控组件
广义的说法:通过React中的props属性完全控制的组件被称为受控组件,否则为非受控组件
在表单元素中,使React中的state作为唯一数据源,被React用state控制取值的表单输入元素就叫作受控组件
父子通信
父传子-->使用props传递数据
子传父--> 1. 传递方法;2. 通过ref获取子组件
非父子通信
- 状态提升(子1/子2 --> 父,父 -->子,中间人的模式)
- 发布订阅者模式
- context状态树传参(生产者消费者模式)
const { Provider, Customer } = React.createContext()
// 生产者
class Page extends React.component{
contructor(){
this.state = {
defaultValue: 'light'
}
}
render{
return(
<Provider value={
color: this.state.defaultValue,
setColor: value => this.setState({ defaultValue: value })
}>
<Item1/>
<Item2/>
</Provider>
)
}
}
// 消费者1--改变值的组件
class Item1 extends React.component{
return(
<Customer>
{value => <div onClick={() => value.setColor('dark')}>我来改变值</div>}
</Provider>
)
}
// 消费者2--接受值的组件
class Item2 extends React.component{
return(
<Customer>
{value => <div>{value.color}</div>} // color为dark
</Provider>
)
}
扩展问题🏄♀️
➡观察者模式:监控一个对象的变化,一旦发生变化,就触发某种操作
🌰:老师观察学生,学生的状态原本是学习,一旦发现变为睡觉,就让学生去罚站
例子分析:
👀观察者:老师
⛰️被观察者:学生
📈模式:老师发现学生的状态变了,就做出某种响应
👩🏫观察者
- 需要一个身份
- 需要回调函数
👨🎓被观察者
- 属性:自己的状态
- 队列:记录谁在观察自己
- 方法:设置自己的状态,当我发送改变的时候,可以改变自己的状态
- 方法:添加观察者
- 方法:删除观察者
// 观察者构造函数
class Observer {
constructor(name, fn = () => {}){
this.name = name
this.fn = fn
}
}
// 被观察者构造函数
class Subject {
constructor (state) {
this.state = state
this.observers = [] // 存放观察者
}
setState(val) {
this.setState = val
// 执行观察者中的回调函数,并将被观察者的数据传给观察者
this.obervers.forEach(item => {
item.fn(val)
})
}
// 增加观察者
addObserver(obs) {
// 避免重复添加观察者
if (this.observers.findIndex(obs) < 0) {
this.observers.push(obs)
}
}
// 删除观察者
deleteObserver(obs) {
this.observers = this.observers.filter(item => item !== obs)
}
}
// 创建观察者
const teacher = new Observer('老师', state => console.log('因为' + state + '被批评'))
const headermaster = new Observer('校长', state => console.log('因为' + state + '批评老师'))
// 创建被观察者
const xiaoming = new Subject('学习')
// 先注册观察者,再更改状态
xiaoming.addObserver(teacher)
xiaoming.addObserver(headermaster)
xiaoming.setState('睡觉')
// 输出结果
因为睡觉被批评
因为睡觉批评老师
➡发布订阅模式:监听某个对象状态的变化,一旦发生变化,通过第三方告知监听者最新状态
比观察者多一个调度中心的概念
现实中的例子🌰
👨去书店买书📖,发现没有,给书店店员💁留了个电话,让店员书来了打电话给他
👨:观察者
📖 :被观察者
💁 :调度中心
// 观察者构造函数
class Observer{
constructor(){
this.message = {}
}
// 向消息队列中增加事件
on(type, fn) {
if (!this.message[type]) {
this.message[type] = []
}
this.message[type].push(fn)
}
// 从消息队列中删除事件
off(type, fn) {
if (!fn) {
delete this.message[type]
return
}
if (this.message[type]) {
this.message[type] = this.message[type].filter(f => f !== fn)
}
}
// 触发消息队列
tigger(type) {
if (this.message[type]) {
this.message[type].map(item => item())
}
}
}
// 创建调度中心
const clerk = new Observer()
const callBack1 = name => {
console.log(name + '到货了!')
}
const callBack2 = name => {
console.log(name + '只剩5本了!')
}
clerk.on('《活着》', callBack1)
clerk.on('《文城》', callBack1)
clerk.on('《文城》', callBack2)
clerk.off('《文城》', callBack1)
clerk.trigger('《活着》')
clerk.trigger('《文城》')
// 输出结果
《活着》到货了!
《文城》只剩5本了!
🌈区别分析
| 模式 | 观察者 | 发布订阅模式 |
|---|---|---|
| 优点 | 角色明确 | 1. 松耦合,发布者和订阅者无关联,靠调度中心联系 2. 灵活性较高,通常应用在异步编程中 |
| 缺点 | 紧耦合 | 当事件类型变多时,会增加维护成本 |
| 使用场景 | 双向绑定 | react非父子组件通信 |
九、ref
React.createRef()
React.forwardRef(props, ref)
转发ref,可以直接获取子组件的DOM元素
扩展问题🏄♀️
**问1:**通过React.createRef()创建ref获取子组件的方式为什么不推荐使用?
答:ref过于暴露,直接通过ref获取子组件会对子组件有破坏性
**问2:**不能对函数组件使用ref,因为函数组件没有实例
答:在函数组件外部包裹forwardRef,将函数组件转换成能接受ref的组件,并且在父组件中使用useRef去创建ref
十、插槽
和vue中的slot类似,原理是使用this.props.children去渲染组件里放的内容
作用
- 可以用于组件复用
- 减少父子组件通信
// 基本用法
class Com extends React.component{
render() {
return (
<div>{this.props.children}</div>
)
}
}
class CustomerCom extends React.component{
render() {
return (
<Com>我是组件</Com>
)
}
}
// 最终页面显示:我是组件
十一、生命周期
16.2之后将diff算法更新到Fiber
16.8之后出现了react hooks,函数组件开始有生命周期
问:为什么componentWillmount、componentWillUpdate、componentWillReceiveProps会被废弃?
**答:**因为将diff算法更新到Fiber之后,低优先级的任务可能会被打断,也就是说可能会多次执行,而componentWillmount就是低优先级的任务,所以一般不建议使用componentWillmount,如果要初始化state值,推荐在constructor中写
👴老生命周期
componentWillmount
初始化,render之前最后一次修改state状态的机会
render
只能访问state和props,不允许修改状态和DOM
componentDidMount
DOM渲染完成后触发,可以修改DOM,一般用于发送异步请求
componentWillUpdate
state即将更新的时候调用,和componentWillmount一样是低优先级
componentDidUpdate
state更新完成的时候调用
shouldComponentUpdate
可以控制state更改后是否render,是React中可以性能优化的生命周期
shouldComponentUpdate(nextProps, nextState) {
// 默认值是true,可以通过返回false阻止更新
if(Josn.strify(this.state) === Josn.strify(nextState)) {
return false
}
}
延伸:不能直接修改this.state中的值,因为直接修改了的话,在shouldComponentUpdate中会判断为未修改,会阻止render更新
🍪直接修改this.state,可以改掉state中属性的值,但是不会引发render
componentWillReceiveProps:父组件的重新渲染会调用这个回调函数,无论props变了没有
这个生命周期可以拿到最新的props属性值,是在子组件中使用的,就算父组件传的props没有更新,也会使child组件生命周期更新
componentWillUnMount
组件销毁的时候会调用这个生命周期,可以用来清除事件监听
👶新生命周期
getDerivedStateFromProps:derived是衍生的意思
初始化和state更新以及props更新都能触发该生命周期,可看作是componentWillMount和componentWillReceiveProps的结合
适用于第一次更新和后续都会更新的逻辑
// 配合componentDidUpdate使用代替
class Test extends React.component{
contructor() {
super()
this.state = {
name: ''
}
}
// 因为是静态方法,所以this是undefined
static getDerivedStatefromProps(nextProps, nextState) {
/**
props和state的改变都会触发这个回调函数的执行,但是react内部会将state变化合并处理(每个事件循环机制之后会合并处理),所以对 性能的影响不大
*/
return {
name: nextState.name
}
}
// 可以拿到最新更新完的状态值
componentDidUpdate(preProps, preState) {
// 避免重复执行操作
if (this.state.name === preState.name) {
return
}
console.log(this.state.name)
}
}
getSnapshotBeforeUpdate:在更新之前获取快照
代替componentWillUpdate,在render之后,componentDidUpdate之前执行,DOM渲染之前执行
class Test extends React.component{
contructor(){
super()
this.state = {
name: 'huhaha'
}
}
componentDidUpdate(preProps, preState, value){
// value就是getSnapshotBeforeUpdate返回的值
console.log(value) // 100
}
getSnapshotBeforeUpdate() {
return 100
}
}
十二、React的性能优化🌟
手动优化
通过手动控制shouldComponentUpdate中返回true和false,去决定是否需要触发render
使用纯组件
PureComponent通过对state和props进行浅对比,实现了shouldComponentUpdate
注意⚠️:
- 如果你的props和state是比较复杂的数据结构,不建议使用PureComponent,可能需要通过forceUpdate去强制组件更新
- PureComponent中的shouldComponentUpdate会跳过所有子组件的prop的更新,所以子组件也必须是纯组件
函数组件的优化
使用React.memo
十三、React中的hooks
为什么需要用hooks?
- 高阶组件为了复用,导致层级太深
- 类组件的生命周期复杂
- 写成function组件的无状态组件,后续想添加状态,重构起来比较麻烦
useState(状态管理)
⚠️:state的更改会引起整个函数组件的更新,而类组件是只会引起render中的内容更新
import { useState } from 'react'
const App = () => {
const [num, setNum] = useState(1)
return <div>{num}</div>
}
useEffect(解决函数的副作用)
1⃣️第二个参数中传空数组或者常量,表示只会在DOM挂载完成之后,执行一次,相当于类组件中的componentDidMount
// 页面渲染完成后发送请求
useEffect(() => {
// 发送异步请求
...
}, [])
2⃣️第二个参数传依赖的变量,useEffect会监听该变量的变化,变量每次变化useEffect都会执行
// num每次都会加1
const [num, setNum] = useState(1)
useEffect(() => {
setNum(num + 1)
}, [num])
3⃣️模拟销毁组件前的周期,即componentWillUnmount
useEffect(() => {
// 函数体
...
return () => {
...
}
}, [])
问:useEffect和useLayoutEffect有什么区别?
**答:**简单来说,调用时机不同。
useLayoutEffect和useEffect功能类似,区别在于前者是在DOM更新后调用的,后者是在DOM渲染完成后调用的;
一般情况下,官方推荐优先使用useEffect,useLayoutEffect比较适合用于避免页面抖动,即DOM会频繁更新的情况,如果在useEffect中操作DOM过多的话,会引起页面频繁重绘、重排,所以操作DOM建议放在useLayoutEffect中
useCallback(记忆函数)
问:记忆指的是什么?
答:之前说state每次更改都会引起函数组件的更新,按道理state的值每次都会变成初始值,但是结果是每次都能拿到前一次操作的结果值,说明react内部对state进行了缓存,也就是记忆功能。
// 对于函数而言,函数组件每次更新,都会被重新定义
const handleClick = useCallback(() => {
console.log(name)
}, [name])
// 第二个参数的三种情况
- 不传参数:每次都会被重新创建
- 传空数组:不会被重新创建,每次都是拿第一次创建的那个函数
- 传依赖:当依赖发生变化时,才会被重新创建
useMemo(记忆组件)
useMemo完全可以替换useCallback,区别在于useCallback不执行函数,只是返回函数,
而useMemo会执行函数并将函数执行的结果返回,等同于vue中计算属性
useCallback(fn, inputs) 等价于 useMemo(() => fn, inputs)
const [num, setNum] = useState(1)
const list = useMemo(() => {
return num
}, [num])
// 如果第一个参数中的函数没有返回值,useMemo会返回undefind
useRef(保存引用值)
常规作用
和React.createRef作用相同,通过ref获取DOM元素或者获取子组件
保存引用值
**问:**如何在react hooks中保存一个变量?
**答:**由于每次state更新都会引起组件的更新,而有些变量不需要引起视图变化,所以我们希望用普通变量存储,而普通变量没有记忆功能,在组件更新的时候会被重新定义,所以需要使用ref保存普通变量
import { useRef } from 'react'
const Test = props => {
const nameRef = useRef('测试')
return <div>{nameRef.current}</div>
}
// 下次组件更新的时候,nameRef.current的值还是‘测试’
useContext(跨级通信)
需要结合类组件中的createContext使用,也是用来实现跨级通信的
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
// 向下传递数据
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
// 使用数据
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
useReducer
当组件状态比较多的时候,逻辑复杂且有很多子组件的情况下,useReducer比useState更适用
替代useState
// 替代useState实现计时器功能
const reducer = (state, action) => {
switch(action.type) {
case 'increment':
return { count: state.count++ }
case: 'decrement':
return { count: state.count-- }
case: 'set':
return { count: action.payload }
case: 'reset':
return init(action.payload)
default:
return state
}
}
const initState = { count: 1 }
const init = (state) => { count: state }
const App = () => {
const [state, dispatch] = useReducer(reducer, initState, init)
return (
<div>
<button onClick={() => dispatch({ type: 'increment' })}>加</button>
{state.count}
<button onClick={() => dispatch({ type: 'decrement' })}>减</button>
<button onClick={() => dispatch({ type: 'set', payload: { count: 10 } })}>设置为10</button>
<button onClick={() => dispatch({ type: 'reset', payload: initState })}>重置</button>
</div>
)
}
结合useContext使用
当子组件层级比较深的时候,我们可以通过context将reducer的state和dispatch传给子组件,这样子组件之间也可以进行通信
import React, { useContext, useReducer } from 'react'
const reducer = (state, action) => {
switch(action.type) {
case 'increment':
return { count: state.count++ }
case: 'decrement':
return { count: state.count-- }
default:
return state
}
}
const initState = { count: 1 }
const GlobalContext = React.createContext()
const App = () => {
const [state, dispatch] = useReducer(reducer, initState)
return <GlobalContext value={{ state, dispatch }}>
<Child1/>
<Child2/>
</GlobalContext>
}
// 改变state值
const Child1 = () => {
const { dispatch } = useContext(GlobalContext)
return <button onClick={() => dispatch({ type: 'increment' })}>+</button>
}
// 显示state值
const Child2 = () => {
const { state } = useContext(GlobalContext)
return <div>{state.count}</div>
}
自定义hooks🌟
当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。
使用场景:输入框搜索内容的时候防抖
// 普通的防抖函数
const debounce = (fun, timeout) => {
let timeId = null
return function() {
clearTimeout(timeId)
timeId = setTimeout(() => {
fun.call(this)
}, timeout)
}
}
// 自定义hook实现的防抖函数
const useDebounce = (value, delay) => {
const [debounceValue, setDebounceValue] = useState(value)
useEffect(() => {
const timeout = setTimeout(() => setDebounceValue(value), delay)
return () => clearTimeout(timeout)
}, [value, delay])
}