文章目录
- 1.1、React生命周期
- 1.2、JSX
- 1.3、类组件和函数组件
- 1.4、react组件设计模式
- 1.5、高阶组件
- 1.6、setState的同步异步
- 1.7、调用setState后会发生什么
- 1.8、组件通信
- 1.9、虚拟DOM、diff算法、key的作用
- 1.10、什么是 React
- 1.11、react渲染流程
- 1.12、React Router常用API
- 1.12、React Router有几种模式,以及实现原理?
- 1.13、createElement和cloneElement的区别
- 1.14、受控组件和不受控组件
- 1.15、context 多层级通信
- 1.16、组件状态保存(类似vue的keep-alive)
- 1.17、useEffect和useLayoutEffect
- 1.18、react-hooks使用限制
- 1.19、immutable
- 1.20、React性能优化方法
- 1.21、路由懒加载及实现原理
- 1.22、useEffect如何区分生命周期
- 1.23、常见的hook
- 1.24、不同版本做过什么优化
- 1.26、副作用
- 1.27、redux是什么
- 1.28、常用中间件
- 1.29、react-redux
- 1.30、reducer
1.1、React生命周期
React的生命周期
:React实例从被创建到被销毁的过程,React组件中包含一系列勾子函数(生命周期回调函数), 会在特定的时刻调用。
只有 class 组件才有生命周期,因为 class 组件会创建对应的实例,而函数组件不会 。
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(){}
}
挂载阶段
挂载阶段
:组件实例被创建和插入 DOM 树的过程
由ReactDOM.render()触发初次渲染
- constructor():类组件构造函数,在组件挂载之前调用;仅用于初始化内部state以及为事件处理函数绑定实例
- getDerivedStateFromProps() :会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用,此方法适用于state 的值在何时候都取决于 props【新增】
- render():初始化渲染或更新渲染调用,是 class 组件中唯一必须实现的方法;
- componentDidMount():会在组件挂载后(插入 DOM 树中)立即调用;
更新阶段
由组件内部this.setSate()
或父组件重新render
触发或强制更新forceUpdate()
- getDerivedStateFromProps(): 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用,此方法适用于state 的值在何时候都取决于 props【新增】
- shouldComponentUpdate() :根据该函数的返回值,来确定组件是否重新渲染
- render(): 初始化渲染或更新渲染调用,是 class 组件中唯一必须实现的方法;
- getSnapshotBeforeUpdate():在最近一次渲染输出(提交到 DOM 节点)之前调用;此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate();
- componentDidUpdate():会在更新后会被立即调用,首次渲染不会执行此方法;
卸载阶段
由ReactDOM.unmountComponentAtNode()
触发
componentWillUnmount()
:会在组件卸载及销毁之前直接调用;
重要的勾子
render
:初始化渲染或更新渲染调用。componentDidMount
:在组件挂载成功之后调用,该过程组件已经成功挂载到了真实 DOM 上。一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
componentWillUnmount
:在组件卸载成功之前调用,做一些收尾工作, 如:清理定时器、取消订阅消息
已经废弃的勾子
componentWillMount
componentWillReceiveProps
componentWillUpdate
使用会出现警告,在React 18.0需要加上UNSAFE_
前缀才能使用,以后可能会被彻底废弃,不建议使用。
1.2、JSX
JSX(JS XML)
是一种类似于XML的JS扩展语法,用来简化创建虚拟DOM, 在React中可以方便地用来描述UI ,JSX 是React.createElement()的语法糖
JSX会编译成React.createElement(),React.createElement将返回一个ReactElement的js对象,编译工作交由babel操作
import React from 'react';
//JSX不是字符串, 也不是HTML/XML标签,最终产生的就是一个JS对象
var ele = <h1>Hello, JSX!</h1>
// 等价于
var element = React.createElement("h1",null,"Hello, world!"
);
1.3、类组件和函数组件
从实际开发、性能优化、趋势分析三方面分析:
- 实际开发中,类组件是基于面向对象编程的,主打继承、生命周期等核心概念,而函数组件的内核是函数式编程,主打immutable/引用透明等
- 性能优化上,类组件主要依靠React.PureComponent进行浅比较,从而控制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
- 使用全局变量和事件,绑定在window
- 使用状态管理框架,如 redux
- 消息发布订阅机制,如pubsub-js
1.9、虚拟DOM、diff算法、key的作用
原生JS渲染页面:数据 => 真实DOM
React渲染页面:数据 => 虚拟DOM(内存中的数据)=> 真实DOM
原生的JS DOM操作非常消耗性能
虚拟DOM是一个用来描述真实DOM结构的js对象。虚拟DOM会通过ReactDOM.render进行渲染成真实DOM,呈现在页面上。
优点:减少了DOM操作,减少了回流与重绘,保证性能的下限,并极大的优化大量操作DOM时产生的性能损耗
缺点:首次渲染DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
diff算法是用于比较新旧虚拟节点树之间差异的一种算法,react采用3种类型进行对比,分别是树对比、组件对比、元素对比:
- 树对比: DOM 节点的跨级操作比较少,diff算法只会对相同层级的 DOM 节点进行比较,如果更新后的节点不存在,会将该节点以及其子节点完全删除,不会再继续比较。如果出现了 DOM 节点的跨级操作,会将该节点以及其子节点完全删除,然后再移动后的位置重新创建。
- 组件对比:如果是同一类型组件,则进行树对比,如果不是同一类型组件,则直接替换,不会再继续对比
- 元素对比:当节点处于同一层级的时候时,有三种操作:插入、 移动、删除, 对于同一层级的同组子节点,可以通过唯一的标识key进行区分,发现了key值相同的新旧节点,就会执行移动操作(然后依然按原策略深入节点内部的差异对比更新),提高渲染效率,而不会执行原策略的删除旧节点,创建新节点的操作
React16起,引入了Fiber架构,是为了使整个更新过程可以随时暂停和恢复,节点和树分别采用了FiberNode和FiberTree进行重构,FiberNode使用了双链表的结构,可以直接找到兄弟节点和子节点
1.10、什么是 React
React
用于动态构建用户界面的 JS UI库。
React特点:
- 使用
虚拟DOM和Diff算法
,尽量复用DOM节点,减少与真实DOM的交互 - 使用
JSX
,代码的可读性更好 组件化模式
,提高代码复用率、且让代码更好维护声明式编程
, 让编码人员无需直接操作DOM,提高开发效率
1.11、react渲染流程
react16以后的渲染流程是Fiber Reconciler
Fiber Reconciler特点
- 协作性多任务模式 - 线程会定时放弃自己当前运行权利,交给主线程运行
- 策略性优先级 - 调度任务通过标记tag的方式区分优先级执行
React 渲染流程:React 负责将 State 转换为虚拟 DOM 结构后,再转换成真实 DOM 结构,交给浏览器渲染。
渲染过程基本可以划分为调和和提交两个阶段
- 调和阶段主要是计算出diff树,自上而下逐个节点检查并构造新的树,特点是可终止
- 提交阶段是根据diff更新DOM树,回调生命周期函数等,同步执行,不可中断暂停
1.12、React Router常用API
react-router
的本质就是页面的URL
发生改变时,页面的显示结果可以根据URL
的变化而变化,可以实现无刷新的条件下切换显示不同的页面,可以通过前端路由实现单页面(SPA)应用。
react-router-dom的常用API:
-
router组件(BrowserRouter、HashRouter)
<BrowserRouter>
和HashRouter
就是router根组件,用于包裹整个应用 -
路由匹配组件(Routes 、 Route、switch)
<Switch>
适用于当匹配到第一个组件的时候,后面的组件就不应该继续匹配v6版本中
<Routes>
替换<Switch>
,<Routes>
和<Route>
要配合使用,且必须要用<Routes>
包裹<Route>
,当URL发生变化时,<Routes>
都会查看其所有子<Route>
元素以找到最佳匹配并呈现组件 -
Navigation组件(Link、NavLink、Navigate、Redirect)
<Link>
:修改URL,且不发送网络请求(路由链接)<NavLink>
:与<Link>
组件类似,且可实现导航的“高亮”效果。<Navigate>
:只要<Navigate>
组件被渲染,就会修改路径,切换视图。<redirect>
:用于路由的重定向 -
Outlet:当
<Route>
产生嵌套时,渲染其对应的后续子路由。
1.12、React Router有几种模式,以及实现原理?
React Router
对应的hash模式
和history模式
对应的组件为<HashRouter>
和<BrowserRouter>
,作为最顶层组件包裹其他组件。
hash模式
:通过监听浏览器 onhashchange 事件变化,查找对应路由应用。
history模式
:通过 html5 的history API 中新增的 pushState() 和 replaceState() 方法修改浏览器记录,改变页面路径。
区别
- 兼容:hash 可以兼容到 IE8,而 history 只能兼容到 IE10。
- 网络请求:使用 hash 模式,地址改变时通过 hashchange 事件,只会读取哈希符号后的内容,并不会发起任何网络请求。而 history 模式,每访问一个页面都要发起网络请求,每个请求都需要服务器进行路由匹配、数据库查询、生成HTML文档后再发送响应给浏览器,这个过程会消耗服务器的大量资源,给服务器的压力较大。
- 服务器配置:hash 不需要服务器任何配置。history 进行刷新页面时,无法找到url对应的页面,会出现 404 问题。因为域名后面的路由是由前端控制的,后端只能保留域名部分,所以就会造成页面丢失的问题,需要服务器端将所有请求重定向到初始的HTML文件,并让React应用程序接管路由处理
1.13、createElement和cloneElement的区别
- React.createElement和React.cloneElement都是用来创建react元素的,区别是传参不一样
- createElement传入的第一个参数是react元素,而cloneElement第一个参数是element
1.14、受控组件和不受控组件
受控组件和非受控组件是两种不同的表单输入组件的概念,区别在于受不受react组件的控制
- 受控组件是指通过react组件的 state 属性来维护维护值的表单组件,并通过 onChange 事件处理程序更新。受控组件提供了对表单数据的精确控制,比如实时现场验证
- 非受控组件是指通过 DOM 元素自身维护值的表单组件。通过使用 ref 属性,可以访问到非受控组件的当前值。非受控组件更加简单,适用于简单的表单场景,不需要高度控制输入的值。比如提交时验证
1.15、context 多层级通信
context的作用是为了避免在组件间层层传递变量,我们可以通过createContext(null)来创建一个新的context,新创建的context包含一个provider 和一个consumer
传递时,需要用Provider包裹父组件,通过value携带参数,在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变更后同步调用,主要处理页面闪烁灯问题,因为是同步,所以在较大计算量时会造成阻塞
- useLayoutEffect总是会比useEffect先执行,它在组件渲染完成后、尚未触发浏览器布局和绘制之前执行,可以用于进行 DOM 操作或获取 DOM 元素的尺寸和位置等计算
1.18、react-hooks使用限制
主要有两个限制:
-
只能在函数内部的最外层调用 Hook,不要在循环、条件判断或者子函数中调用
react用链表结构来严格保证hooks的顺序,在调用时按顺序加入链表中,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。
-
只能在 React 的函数组件中调用 Hook,不要在普通JS函数中调用
由于Hooks依赖React的渲染机制和函数组件的特性,所以只能在React的函数组件中调用。在普通的JavaScript函数中使用Hooks是没有意义的,因为它们无法享受到React提供的状态管理和渲染优化的好处
链表(Linked List)是一种常见的数据结构,用于存储和组织数据。它由一系列节点(Node)组成,每个节点包含了数据和指向下一个节点的引用(指针或链接)。链表的特点是节点之间通过指针相互连接,形成一个链式结构,并且节点可以按需动态分配内存,灵活地插入、删除和修改数据。
1.19、immutable
- immutable 是指不可改变的数据,原理是 用结构共享数据的方法保存数据,当数据被修改时,会返回一个对象,但是这个的对象会尽可能的利用之前的数据结构而不会对内存造成浪费
- 使用immutable可以给react应用带来性能的优化,主要体现在减少渲染次数。在react性能优化的时候,当传入子组件 props 或 state 不止一层,或者传入的是 Array 和 Object 类型时,React.PureComponent浅比较就失效了,这时也可以在
shouldComponentUpdate()
中使用使用deepCopy
和deepCompare
来避免不必要的render()
,但deepCopy
和deepCompare
一般都是非常耗性能的。这个时候我们就需要Immutable
,Immutable
通过is
方法则可以完成对比,而无需像一样通过深度比较的方式比较,提高性能。
1.20、React性能优化方法
-
类组件使用 React.PureComponent进行浅比较,从而控制 shouldComponentUpdate 的返回值避免子组件重新渲染。当传入 props 或 state 不止一层,或者传入的是 Array 和 Object 类型时,浅比较就失效了,当然我们也可以在
shouldComponentUpdate()
中使用使用deepCopy
和deepCompare
来避免不必要的render()
,但deepCopy
和deepCompare
一般都是非常耗性能的。这个时候我们就需要Immutable或Immer
(在 React 工作流中,如果只有父组件发生状态更新,即使父组件传给子组件的所有 Props 都没有修改,也会引起子组件的 Render 过程。如果子组件的 Props 和 State 都没有改变,子组件的不应该重新 Render )
-
函数组件使用
React.Memo
(浅比较)来避免子组件的重复渲染,当传给子组件的派生状态每次都是新的引用时,还需要搭配useCallback
缓存函数,更加推荐使用ahooks中的useMemoizedFn
,与useCallback效果相同,但它不传入依赖项,函数引用地址不变 -
函数组件使用
useMemo
缓存大量的计算 -
发布订阅模式跳过中间组件 Render 过程,比如祖孙组件通信,使用React.createContext 和React.useContext,这样就少了中间组件的渲染阶段
-
列表项使用 key 属性,提高渲染效率
-
避免使用内联css样式和匿名函数
-
在React中,组件是不允许返回多个节点的,使用React.Fragment或<></>避免添加额外的DOM
-
路由组件懒加载,React.Lazy和React.Suspense
- 通过React的
lazy
函数配合import()
函数动态加载路由组件,路由组件代码会被分开打包 - 通过
<Suspense>
指定在加载得到路由打包文件前显示一个自定义loading界面
- 通过React的
1.21、路由懒加载及实现原理
懒路由加载
:用的时候才加载,如果不用路由懒加载,页面在第一次进入的时候,就请求了所有组件的数据,如果组件过多,页面可能会出现卡顿现象,应该是用户按哪个链接再请求哪个组件
实现原理:
-
通过
React.lazy
配合Webpack import()函数
动态加载路由组件,路由组件代码会被分开打包React.lazy 方法返回的是一个 lazy 组件的对象,与 Promise 类似它具有 Pending、Resolved、Rejected 三个状态,分别代表组件的加载中、已加载、和加载失败三中状态。
-
通过
React.Suspense
渲染 React.lazy 异步加载的组件,主要通过捕获组件的状态去判断如何加载,当这个组件的状态为 Pending 时显示的是 Suspense 中 fallback 的内容,只有状态变为 resolve 后才显示组件。如果遇到网络问题或是组件内部错误,页面的动态资源可能会加载失败,为了优雅降级,可以使用 Error boundary(错误边界) 来解决这个问题。
1.22、useEffect如何区分生命周期
useEffect(异步执行副作用)
: 在函数组件中执行副作用操作 (用于模拟类组件中的生命周期钩子) , 在执行 DOM 更新之后调用 。
useEffect可以看成是 componentDidMount
,componentDidUpdate
和 componentWillUnmount
三者的结合。
- componentDidMount: 组件挂载完成 (开启监听, 发送ajax请求)
- componentDidUpdate:组件更新完成
- componentWillUnmount:组件即将卸载(收尾工作, 如: 清理定时器)
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.26、副作用
在React中,副作用(Side Effects)是指执行与组件渲染结果无关的操作。这些操作可以包括但不限于:
- 发送网络请求
- 订阅或取消订阅事件
- 操作浏览器缓存或本地存储
- 修改全局状态
- 改变DOM
1.27、redux是什么
redux是一个实现状态集中管理的容器,遵循三大基本原则
- 单一数据源
- state是只读的
- 使用纯函数来执行修改
store的数据,通过dispatch来派发action
redux工作流程
view 调用store的dispatch接收action传入store,reducer进行state操作,然后view通过store提供的getState获取最新的数据
1.28、常用中间件
- redux-thunk - 用于异步操作
- redux-promise - 处理异步操作
- redux-sage - 处理异步操作
- redux-logger - 用于日志记录
1.29、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)
1.30、reducer
- reducer是纯函数,它规定应用程序的状态怎样因响应action而变化,reducer通过接收先前的状态和action来工作, 然后返回一个新的状态。
- 它根据操作的类型确定需要执行哪种更新,然后返回新的值,如果不需要完成任务,就会返回原来的状态