前言
记录一些个人认为比较常见的React相关的常见面试题以及个人理解答案,如有错误请见谅以及指出,谢谢
1.1、React生命周期
class Test extends React.Component{
constructor(props){ // 构造函数
super(props)
this.state = {}
}
// 初始化,更新时会调用
static getDerivedStateFromProps(props,state){
// 必须返回一个对象,会和state合并
return {}
}
// 初始化渲染时使用
componentDidMount(){}
// 组件更新时调用 返回false 不更新
shoudComponentUpdate(prevProps,nextState){ return true }
// 组件更新时调用,返回的值会设置在componentDidUpdate的第三个参数
getSnapshotBeforeUpdate(prevprops,nextState) { return ''}
// 组件更新后调用
componentDidUpdate(preProps,preState,valueFromSnaspshot){}
// 组件卸载时调用
componentWillUnmount() {}
// 组件抛出错误
static getDerivedStateFromError(){}
}
1.2、JSX
JSX:JSX 本质是javascript的语法拓展,它和模板语言相似,但是具备javascript的能力,直白的说,JSX 是React.createElement()的语法糖
- JSX会编译成React.createElement(),React.createElement将返回一个ReactElement的js对象,编译工作交由babel操作
<div className="testDiv"><h1 className="testH1">content</h1></div>
// 上面jsx代码会转译成下面内容 React.createElement(标签名,标签参数,标签内容)
React.createElement('div',{ className: 'testDiv'},
React.createElement('h1', { className: 'testH1' }, 'content')
)
1.3、类组件和函数组件
-
在开发的时候,类组件是基于面向对象编程的,主打继承、生命周期等核心概念,而函数组件的内核是函数式编程,主打immutable/引用透明等
-
性能优化上,类组件主要依靠shouldComponentUpdate来阻断渲染来提升性能,而函数组件依靠React.memo缓存渲染结果来提高性能
-
从趋势上,React官方更推崇 组合优于继承 的设计概念,所以未来函数组件成为主推方案的概率会大些
1.4、react组件设计模式
无状态组件(展示组件)
只作展示、独立运行、不额外增加功能的组件,特点是复用性强,可分为代理组件,样式组件,布局组件
有状态组件(灵巧组件)
包含业务逻辑和数据状态的组件称为有状态组件,或者灵巧组件,特点是功能丰富、复杂度高、复用性低,有状态组件分为 容器组件和高阶组件
- 容器组件 - 几乎没有复用性,主要用于拉取数据和组合组件
- 高阶组件 - 实际是一个函数概念,一个函数可以接收另一个函数作为参数,然后返回一个函数,称为高阶函数
1.5、高阶组件
- 实际是一个函数概念,一个函数可以接收另一个函数作为参数,然后返回一个函数,称为高阶函数
// 高阶组件 判断登录状态
const checkLogin = (WrappedCompont) => {
return props => {
const isLogin = true; // 这里是登录判断条件,根据实际处理,未登录则显示登录组件
return isLogin ? <WrappedCompont {...props} /> : <LoginPage />
}
}
// 调用高阶组件,函数写法
class Demo extends React.Component{}
const DemoPage = checkLogin(Demo)
// 装饰器写法
@checkLogin
class Demo extends React.Component{}
1.6、setState的同步异步
- setState是先存入state队列还是直接更新,根据isBatchingUpdates值判断,true则存入队列(异步),否则直接更新(同步)
- 在react可控制的事件里,是执行异步,如react的生命周期事件和合成事件(如onClick)
- 在react无法控制的事件,则执行同步,如原生事件、addEventListener、setTimeout等
1.7、调用setState后会发生什么
- 调用setState后,react会将传入的参数对象和组件当前的状态合并,然后触发调和过程返回新的状态,react会根据新的状态构建新的DOM树,然后重新渲染
1.8、组件通信
单层级通信
- 父组件给子组件通信,传递props
- 子组件向父组件通信,使用回调函数
- 兄弟组件通信,通过父组件当中间件传递
跨多层组件通信
- React的context api
- 使用全局变量和事件,绑定在window
- 使用状态管理框架,如 redux
- 自定义事件订阅模式组件(不推荐,不好管理)
1.9、虚拟DOM的工作原理,Virtual DOM 也称VDOM,也就是虚拟DOM
虚拟DOM的工作原理是通过js对象模拟DOM的节点
以react为例,在render函数中写的jsx会在babel插件下,编译为React.createElement执行的JSX中的属性参数,
React.createElement执行后会返回一个描述节点的对象,描述自己的tag类型,props属性以及children 子级等,
这些对象通过树形结构组成一个虚拟DOM树,在状态发生改变时,对比更新前后虚拟DOM树的差异,这个过程称为diff,生成的结果称为patch,计算之后,会渲染patch完成对真实DOM的操作
虚拟DOM优点
- 改善大规模DOM操作的性能
- 规避XSS风险
- 以较低成本实现跨平台开发
虚拟DOM缺点
- 内存占用较高(因为需要模拟整个网页的真实DOM)
- 高性能应用场景存在难以优化的情况
1.10、diff算法
diff算法是指生成更新补丁的方式,主要运用于虚拟DOM树变化后,更新真实DOM
react的diff算法,触发更新的时间主要是在state变化和hooks调用之后。此时触发虚拟DOM树变更遍历,采用了深度优先的遍历算法,采用3种类型节点的比对,分别是树、组件和元素
- 树比对 - 由于网页视图中较少出现跨层级节点移动,所以两个虚拟DOM树只对同一层级的节点进行比较
- 组件比对 - 如果组件是同一类型,则进行树对比,如果不是则直接放入补丁中
- 元素比对 - 主要发生在同层级中,通过节点标记(key)操作生成补丁,节点操作对应真实DOM的操作
React16起,引入了Fiber架构,是为了使整个更新过程可以随时暂停和恢复,节点和树分别采用了FiberNode和FiberTree进行重构,FiberNode使用了双链表的结构,可以直接找到兄弟节点和子节点
1.11、react渲染流程
- react16以后的渲染流程是Fiber Reconciler
Fiber Reconciler特点
- 协作性多任务模式 - 线程会定时放弃自己当前运行权利,交给主线程运行
- 策略性优先级 - 调度任务通过标记tag的方式区分优先级执行
渲染过程基本可以划分为 Render 和 Commit 两个阶段
- Render 阶段主要是计算出diff树,自下而上逐个节点检查并构造新的树,特点是可终止
- Commit 阶段是根据diff更新DOM树,回调生命周期等,同步执行,不可中断暂停
1.12、key的作用
- key是react用于追踪哪些列表的元素被更改的辅助标识
在开发过程中,需要保证某个元素的key在同级元素中是唯一的
在react diff算法中react会借助元素的key值来判断该元素是新创建的还是被移动而来的元素,从而减少不必要的元素重渲染
1.13、createElement和cloneElement的区别
- React.createElement和React.cloneElement都是用来创建react元素的,区别是传参不一样
- createElement传入的第一个参数是react元素,而cloneElement第一个参数是element
1.14、受控组件和不受控组件
- 每当表单状态发生变化时,都会被写入组件的state中,这种组件成为受控组件
- 受控组件:没有维持自己的状态,状态由父组件控制,仅通过父组件的props获取当前值,然后通过回调函数通知更改
- 不受控组件:保持着自己的状态,refs用于获取当前值
1.15、context 多层级通信
context的作用是为了避免在组件间层层传递变量,我们可以通过createContext(null)来创建一个新的context,新创建的context包含一个provider 和一个consumer
传递时,需要用Provider包裹父组件,在Provider包裹下的层层组件中,通过consumer包裹子组件来读取穿度的变量
1.16、组件状态保存(类似vue的keep-alive)
- vue的keep-alive是把虚拟DOM 保存在内存中,React认为容易造成内存泄漏,所以官方不提供状态保存方案
1、手动保存状态
配合componentWillUnmount生命周期,通过redux之类的状态管理框架对数据进行保存,通过componentDidMount进行数据恢复,但数据量大的时候比较麻烦
2、通过路由实现保存 react-router
这个方法实现比较麻烦,原理是因为react状态丢失是由于路由切换卸载了组件引起的,所以从根本上改变路由对组件的渲染行为,有以下方式
- 重写组件 - 可参考 react-live-route,实现成本比较高
- 重写路由库 - 可参考 react-keeper, 实现成本和风险更高
3、模拟真实功能
github 有类似的实现插件 react-activation,实现原理是:由于react 会卸载掉处于固有组件层级内的组件,所以我们需要将children子属性抽取处理,渲染到一个不会被卸载的组件内,再使用DOM操作将其真实内容移到对应的内容,实现此功能
1.17、useEffect和useLayoutEffect
相同点
- useEffect和useLayoutEffect都是用于处理副作用,也就是改变DOM、设置订阅、操作定时器等
- 使用方法一样,底层也一样,都是调用mountEffectlmpl方法,基本可以直接替换
不同点
- useEffect在React渲染过程中是被异步调用的,用于绝大部分场景,而useLayoutEffect会在所有DOM变更后同步调用,主要处理DOM操作、调整样式、避免页面闪烁灯问题,因为是同步,所以在较大计算量时会造成阻塞
- useLayoutEffect总是会比useEffect先执行
1.18、react-hooks使用限制
主要有两个限制
- 不要在循环、条件、嵌套函数中调用hook
- 在react的函数组件中调用hook
因为hooks的设计是基于数组实现,在调用时是按顺序加入数组中,如果使用循环、条件等有可能会导致数组取值错位,导致执行到错误的hook
1.19、immutable
-
immutable 是指不可改变的数据,创建后不会更改,任何修改都会返回一个新的immutable对象
-
使用immutable可以给react应用带来性能的优化,主要体现在减少渲染次数
-
在react性能优化的时候,为了避免重复渲染,通常是在shouleComponentUpdate中做对比,判断返回是否需要返回true更新
-
immutable通过is方法可以完成对比,无需通过深度比较的方式比较
1.20、React性能优化方法
- 1、减少渲染的节点、降低渲染的计算量(复杂度)
- 2、避免重复渲染,类组件通过shouldComponentUpdate来比对state和props(浅比较),确认是否需要重新渲染,函数组件则是用React.memo
- shouldComponentUpdate:React.PureComponent类内置了对shouldComponentUpdate的实现,如果要深对比,可以加入Immutable.js
- memo和useMemo:两个都是函数组件的优化思路
- React.memo是函数版的shouldComponentUpdate,但只针对props,不针对state的变化
- useMemo则是更加精细的memo,可以控制是否需要重复执行某一段逻辑
1.21、react懒加载
- react16.6 新增了React.lazy函数,配合webpack的codesplitting可以实现懒加载,只有组件加载时才会导入对应资源
// 不用lazy
import otherComponent from './otherComponent'
// 使用lazy
const otherComponent = React.lazy(()=>import('./otherComponent'))
1.22、useEffect如何区分生命周期
- useEffect可以看成是 componentDidMount、componentDidUpdate和componentWillUnmount三者的结合
useEffect(()=>{
console.log('mounted')
return ()=>{ console.log('willunmount') }
},[source])
// source不传则是默认componentDidMount,传参后就是对应参数的componentDidUpdate
1.23、常见的hook
- useState - 状态钩子,定义组件的state
- useEffect - 生命周期钩子
- useContext - 获取context对象
- useCallback - 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染
- useMemo - 缓存传入的props,避免重复渲染
- useRef - 获取组件的真实节点
1.24、不同版本做过什么优化
React15: 架构可以分为两层
- reconciler(协调器) - 负责找出变化的组件
- render(渲染器) - 负责把变化的组件渲染到页面上
在React15及以前,reconciler(协调器)采用递归的方式创建虚拟DOM,递归过程是不可中断的,如果层级很深,递归时间超过了16ms,用户交互就会卡顿
因此,React将递归改成异步可中断的Filber
- React16: 架构可分为三层
- scheduler(调度器) - 负责调度任务的优先级,优先级高的任务先进入reconciler
- reconciler(协调器) - 负责找出变化的组件
- render(渲染器) - 负责把变化的组件渲染到页面上
1.25、如何理解react
-
React是一个网页UI框架,通过组件化的方式解决视图层开发复用的问题,本质上是一个组件化框架
-
它的设计核心思路有三点: 声明式,组件化和通用性
-
声明式的优势在于直观和组合
-
组件化的优势在于视图的拆分与模块复用,可以更容易做到高内聚低耦合
-
通用性在于一次学习,随处编写,比如react native等,这里主要靠虚拟DOM来保证实现
-
React的劣势在于它没有提供完整的解决方案,需要开发者自行整合,比如react-router等
1.26、副作用
-
纯函数
-
确定的输入,产生确定的输出,与执行次数、时间无关
-
不产生副作用
-
常见副作用
-
系统IO相关API
-
Date.now()、Math.random()等不确定性方法
-
在函数体内修改函数外变量的值
-
在函数体内修改函数参数的值
-
调用会产生副作用的函数
-
http请求
2.1、redux是什么
redux是一个实现状态集中管理的容器,遵循三大基本原则
- 单一数据源
- state是只读的
- 使用纯函数来执行修改
store的数据,通过dispatch来派发action
redux工作流程
view 调用store的dispatch接收action传入store,reducer进行state操作,然后view通过store提供的getState获取最新的数据
2.2、常用中间件
- redux-thunk - 用于异步操作
- redux-promise - 处理异步操作
- redux-sage - 处理异步操作
- redux-logger - 用于日志记录
2.3、react-redux
-
react-redux是官方推荐的库,具有高效且灵活的特性
-
react-redux分成两个核心provider 和 connection
// Provider
<Provider store = {store}> <App /> </Provider>
// connection
import { connect } from 'react-redux'
// 把redux的数据映射到react的props中去
const mapStateToProps = state => {
return {foo: state.foo}
}
// 将redux中的dispatch映射到组件内部的props中去
const mapDispatchToProps = dispatch =>{
return {
toclick: () => { dispatch({type:'toclick'}) }
}
}
connect(mapStateToProps, mapDispatchToProps)(MyComponent)
2.4、reducer
- reducer是纯函数,它规定应用程序的状态怎样因响应action而变化,reducer通过接收先前的状态和action来工作, 然后返回一个新的状态。
- 它根据操作的类型确定需要执行哪种更新,然后返回新的值,如果不需要完成任务,就会返回原来的状态