1、React 中 keys 的作用是什么?
在 Diff
算法中,React
会通过key来判断fiber节点能否复用,key和type都相同就能复用。不写key,默认都是null,如果type还相同,就会复用,这样会按照下标顺序依次复用老节点。虽然这样diff算法更快,但只适用于简单无状态的组件,对于有状态的组件,元素复用可能会出现数据错位(如顺序变了)的问题。对于大部分场景,组件都有自己的数据,虽然带上key会给diff算法增加一些开销,但可以保证组件能被正确的复用,而不出现数据错位的问题。
另外同一组元素,key值要保证唯一且稳定,如果key不唯一,复用时可能会错位,如果key不稳定,就无法复用,就会删除重建,影响性能。
2、React 中 refs 的作用是什么
1.ref可以获取dom元素、组件实例
用在原生标签上,ref获取的是真实dom;
用在类组件上,ref获取的是类实例;
函数组件上,需要用React.forwardRef包装一下,才能添加ref,获取的是原组件里绑定的ref
2.函数组件中, useRef也可以当成变量使用,还可以配合useImperativeHandle来获取子组件暴露出来的内容。
3.创建refs:React.createRef() useRef()
ref是一个函数又有什么好处?
方便react销毁组件、重新渲染的时候去清空refs的东西,防止内存泄露
3、setState第二个参数的作用
因为setState是一个异步的过程, 执行完setState之后不能立刻获取更改的state里面的值, 所以就提供了第二个参数--回调函数,就是用来监听state里面数据的更改。
4、createElement 与 cloneElement 的区别是什么
createElement
是创建 React虚拟dom
,而 cloneElement
则是复制某个React虚拟dom
并传入新的 Props
5、react是什么?
1.React是一个网页UI框架,通过组件化的方式解决视图层开发复用的问题,本质是一个组件化框架。
2.它的核心设计思路有三点:声明式、组件化、通用性
声明式的优势在于直观、可以轻松描述应用。
组件化的优势在于视图的拆分与模块复用,可以更容易做到高内聚低耦合。
通用性在于一次学习,随处编写。比如React Native,React 360等,这里主要靠虚拟DOM 来保证实现。这使得 React 的适用范围变得足够广,无论是 Web、Native、VR,甚至Shell应用都可以进行开发。这也是React 的优势。
3.优点:
1.开发团队和社区强大;
2.一次学习,随处编写;
3.API比较简洁;
4.单向数据流:单向数据流使得数据的变化、流向都变很清晰、容易控制。因此程序会更加直观,易于理解,有利于维护。
4.缺点:
1.作为一个视图层的框架,React 的劣势也十分明显。没有官方系统解决方案,需要引入很多第三方模块,选型成本高。
2.父组件重新渲染时,即使子组件的props和state没有改变也会重新被渲染。
6.react代码优化方案?
1.React.memo 和 PureComponent,都可以防止组件不必要地重新渲染。React.memo只浅比较props,如果没有改变,就不会重新渲染;PureComponent会对props和state都进行浅比较,如果都没有改变,就将不会重新渲染。
React.memo():用在函数组件,const App1 = React.memo(App);
PureComponent:用在类组件,class App extends PureComponent
2.useCallback 和 useMemo,都可以防止不必要地重新渲染,在函数组件内部使用
3.sholudComponentUpdate,可以防止不必要地重新渲染,在类组件内部使用
4.遍历列表时必须为 item 添加 key,并保证同一组key的唯一与稳定
5.render
函数中减少内联函数的写法,因为每次更新时均会重新创建新的内联函数,即使内容没有发生任何变化。类组件中将函数保存在组件的成员对象中,函数组件可以使用useCallback缓存,这样只会创建一次。
6.尽可能使用标准的useEffect,少使用useLayoutEffect,以避免阻塞视图的更新
7.使用useDeferredValue和useTransition对非紧急渲染的内容做优化
8.配合使用 Suspense
、lazy、import进行懒加载
import React, { lazy, Suspense } from "react";
export default class CallingLazyComponents extends React.Component {
render() {
let ComponentToLazyLoad = null;
if (this.props.name == "Mayank") {
ComponentToLazyLoad = lazy(() => import("./mayankComponent"));
} else if (this.props.name == "Anshul") {
ComponentToLazyLoad = lazy(() => import("./anshulComponent"));
}
return (
<div>
<h1>This is the Base User: {this.state.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<ComponentToLazyLoad />
</Suspense>
</div>
);
}
}
7、react组件生命周期
1.挂载阶段(组件实例被创建并插入DOM中时),钩子函数执行的顺序:
constructor:实例挂载之前
static getDerivedStateFromProps(props, state):在render方法之前调用,并且在初始挂载及后续更新时都会被调用。它返回一个对象来更新state,如果返回null则不更新任何内容。
render:渲染组件
componentDidMount:实例已挂载完成, 一般在这个函数中进行Ajax请求。如果将AJAX
请求放在其他生命周期函数中,不能保证请求仅在组件挂载完毕后才会调用。如果数据请求在组件挂载之前就完成,并且调用了setState
函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount
函数中进行 AJAX
请求能有效避免这个问题。
2.更新阶段,钩子函数执行的顺序:
static getDerivedStateFromProps(props, state)
sholudComponentUpdate:默认返回值为true, 返回true时组件会重新渲染, 返回false时组件不重新渲染。一般在这个函数中做组件性能优化,主要用来手动阻止组件重新渲染。在初始化时或者调用forceUpdate时会忽略此方法。
render
getSnapshotBeforeUpdate:在最近一次渲染(提交给dom)之前调用,它使得组件能在更改之前从DOM中捕获一些信息(如,滚动位置)。此生命周期方法的任何返回值,都将作为参数传递给componentDidUpdate。此方法并不常见,但它可能出现在UI的处理中,如需要以特殊方式处理滚动位置等。返回snapshot的值(或null)。
componentDidUpdate:在组件完成更新后立即调用。在初始化时不会被调用。
3.销毁阶段:
componentWillUnmount:在组件从DOM中移除之前立刻被调用。一般用来销毁不用的变量或者是解除无用定时器以及解绑无用事件, 防止内存泄漏。
4.错误处理:
componentDidCatch:后代组件抛出错误后被调用。在“提交”阶段被调用,因此允许执行副作用。它接收两个参数:error - 抛出的错误,info - 有关组件引发错误的栈信息。
static getDerivedStateFromError: 后代组件抛出错误后被调用。它将抛出的错误作为参数,并返回一个值以更新state。渲染阶段调用,因此不允许出现副作用。
8、为什么虚拟dom会提高性能
操作 dom 是比较昂贵的,频繁的dom操作容易引起页面的回流和重绘,但是通过抽象 VNode 进行中间处理,通过diff算法,可以有效减少不必要的dom操作,从而提高性能。
提高性能具体实现:
- 用
JavaScript
对象结构表示 DOM 树的结构;然后用这个树构建一个真正的DOM
树,插到文档当中 - 当状态变更的时候,重新构造一棵新的
JavaScript
对象树。然后用新的树和旧的树进行比较,记录两棵树差异 - 把2所记录的差异应用到步骤1所构建的真正的
DOM
树上,视图就更新
虚拟DOM一定会提高性能吗?
- 不一定。它的优势是在于diff算法和批量处理策略,将所有的DOM操作搜集起来,一次性去改变真实的DOM,但在首次渲染上,虚拟DOM会多了一层计算,消耗一些性能,所以有可能会比html渲染的要慢
- 虚拟DOM实际上是给我们找了一条最短,最近的路径,并不是说比DOM操作的更快,而是路径最简单
9、diff算法
1.react17+中DOM-DIFF就是,根据老的fiber树和最新的虚拟dom对比生成新的fiber树的过程。diff算法的目的是找到能复用的节点并复用,不能复用的才新建或修改,从而最小量的操作dom,达到高效更新视图的目的。
2.为了降低算法复杂度,diff算法会预设三个限制:
1)只对比同层级的节点,因为DOM节点跨层级的移动操作特别少,可以忽略不计。
2)不同类型的元素会产生不同的结构,会销毁老元素,创建新元素。
3)同一层级的一组元素,可以通过唯一的key进行区分。
3.一对一比较:
key和type有一个不同就销毁重建,都相同才复用,然后比较属性,属性有变化就更新,然后对比孩子。
4.多个对比:
1.先进行第一轮遍历,依次一一对比。 key和type都相同才复用,然后比较属性,属性有变化就更新,然后对比孩子。如果key和type都不相同,或key相同type不同,老节点标记为删除,新节点标记为新增,保存在patch数组里。如果type相同key不同,则说明顺序改变了,就停止遍历,并声明三个变量:
1)map对象,保存老节点
2)lastPlaceIndex变量。两个作用:判断老节点是否移动;寻找新老节点插入的位置。
3)patch数组,保存需要移动、添加或删除的节点。
2.遍历剩下的新节点,如果这个节点的key在map对象里找不到,就标记为新增,保存在patch数组里。
3.如果当前节点的key在map对象里能找到且类型相同,则复用这个老节点,并把它从map对象里删除。如果复用老节点的下标大于lastPlaceIndex,则不移动老节点,且把lastPlaceIndex的值设为复用老节点的下标;如果复用老节点的下标不大于lastPlaceIndex,则标记老节点为移动,并保存在patch数组里。
4.如果当前节点的key在map对象里能找到但类型不同,老节点标记为删除,新节点标记为新增,保存在patch数组里,并把老节点从map对象里删除。
5.遍历完成后,如果map对象里还有未访问的元素,就是多余的元素,都标记为删除,保存在patch数组里。
6.根据patch数组,移动、插入和删除相应的节点。
10、说说你用react有什么坑点?
1. JSX里用短路运算做表达式判断时候,需要将数字类型强转为boolean类型。如果不强转类型,数字0会在页面里面输出0
2.
componentDidUpdate里使用setState会死循环
3.函数组件中给ref绑定回调函数、或类组件中 ref
回调函数是内联函数,在更新过程中它会被执行两次,第一次传入参数 null
,然后第二次会传入参数 DOM 元素。因为以上两种情况在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。
11、我现在有一个button,要用react在上面绑定点击事件,要怎么做?
class Demo {
render() {
return <button onClick={(e) => {
alert('我点击了按钮')
}}>
按钮
</button>
}
}
你觉得你这样设置点击事件会有什么问题吗?
由于
onClick
使用的是匿名函数,所有每次重渲染的时候,会把该onClick
当做一个新的prop
来处理,会将内部缓存的onClick
事件进行重新赋值,所以相对直接使用函数来说,可能有一点的性能下降
修改为如下:
class Demo {
onClick = (e) => {
alert('我点击了按钮')
}
render() {
return <button onClick={this.onClick}>
按钮
</button>
}
12、调用setState时,React render是如何工作的?
1.每一个类组件内部有一个状态更新器,当调用setState时,状态更新器会往待更新队列里添加一个状态,然后判断是否是批量更新,如果不是,直接更新组件,如果是,会等到所有setState执行完后,由batchUpdate统一更新组件。
2.更新组件前,会先更新状态,然后根据shouldComponentUpdate来判断视图是否需要更新,如果是,就调用render函数获取新的虚拟dom。
3.然后让新的虚拟dom和旧的fiber结构做diff比较,生成新的fiber结构。最后根据新fiber结构的副作用链表,更新或创建真实dom
4.在生命周期函数或合成事件中会批量更新,因为这两种函数被react包装过了,在外层函数先设置isBatchUpdate为true,等内层包装的函数执行完毕,才开始批量更新。
13、useState状态更新时,React render是如何工作的?
1.每一个useState内部,都有一个状态更新器,当设置状态时,状态更新器会往待更新队列里添加一个状态,然后判断是否是批量更新,如果不是,直接更新组件,如果是,会等到所有的状态设置执行完后,由batchUpdate统一更新组件。
2.更新组件前,会先更新状态,存储到hook链表对应的位置,然后在异步回调里调用render函数获取新的虚拟dom,在异步回调里是为了保证多次设置状态,render函数只执行一次。
3.获取到新的虚拟dom,就让新的虚拟dom和旧的fiber结构做diff比较,生成新的fiber结构。最后根据新fiber结构的副作用链表,创建或更新真实dom
14、类组件和函数组件的相同点和不同点
相同点:它们都可以接收属性并且返回虚拟dom
不同点:
1.编程思想不同: 类组件需要创建实例,是基于面向对象的方式编程,而函数式组件不需要创建实例,接收输入,返回输出,是基于函数式编程的思路来编写的。
2.内存占用:类组件需要创建并保存实例,会占用一定内存,函数组件不需要创建实例,可以节约内存占用。
3.捕获特性:函数组件具有值捕获特性,因为函数组件是从闭包里拿的的状态,类组件是从组件实例上拿的状态;渲染和获取数据都在同一个闭包里完成,函数组件真正将数据和渲染绑定在一起了。
4.可测试性:函数式组件更方便编写单元测试
5.状态和生命周期: 类组件有自己的实例,组件实例有自己的状态和生命周期;函数组件没有自己的状态和生命周期,想要使用这些功能,要借助于hooks
6.逻辑复用: 类组件通过高阶函数进行逻辑复用,函数组件通过自定义Hooks实现逻辑复用。
7.跳过更新: 类组件通过shouldComponentUpdate和PureComponent来跳过更新,函数式组件使用React.memo、useMemo、useCallback来跳过更新。
8.发展前景: 未来函数式组件将会成为主流,因为它可以更好的屏蔽this问题、方便复用逻辑、更适合时间分片和并发渲染。
15、react hook:
Hook是React16.8的新增特性,它用来优化函数组件,使函数组件能使用state及其他 的React特性。
- useState:类似于类组件的state,给函数组件添加内部状态。
- useEffect:相当于把类组件的componentDidMount、componentDidUpdate和componentWillUnmount,放在一个API里实现了。
返回一个函数: componentWillUnmount或依赖项改变时会执行。
useEffect不传第二个参数:
componentDidMount时会执行函数。
无论哪个状态或属性改变时,都会执行函数和返回值函数。
useEffect传入第二个参数:
第二个参数为空数组:componentDidMount时会执行函数。
第二个参数为非空数组:componentDidMount时会执行函数,依赖项改变时会执行函数和返回值函数。
- useContext与createContext、useReducer配合使用,可以实现redux的功能。
- useRef既可以获取dom、获取类组件实例,也可以当成变量使用,还可以配合useImperativeHandle来获取子组件暴露出来的内容。
- useCallback用于缓存一个函数,useMemo用于缓存一个值。
useCallback(fn, deps) 相当于useMemo(() => fn, deps)
- useLayoutEffect:实现逻辑与useEffect一样。区别是useLayoutEffect是利用微任务,把回调函数放到dom渲染前执行;useEffect是利用宏任务,把回调函数放到dom渲染完成后执行。可以用useLayoutEffect来获取DOM布局并同步触发重渲染。一般情况下,尽可能使用标准的useEffect,以避免阻塞视觉的更新。
- useDeferredValue:react18新增,返回一个非紧急渲染的值。
- useTransition:react18新增,返回[isPending, startTransition],isPending表示是否正在渲染。被startTransition包裹的setState,触发的渲染被标记为不紧急渲染;在startTransition里切换Suspense视图,不会渲染fallback。
- useId:react18新增,生成唯一稳定的ID,避免不匹配。如:用于表单元素的label
const id = useId();
<label htmlFor={id}>Do you like React?</label>
<input id={id} type="checkbox" name="react"/>
10.useSyncExternalStore:由于并发渲染在React 18里的大规模使用。React 18 提供了useSyncExternalStore,用来保证外部store的一致性。
16、使用Hooks要遵守哪些原则
1.hooks只能在顶层调用,不要在循环、条件或嵌套函数中调用。
2.可以在函数组件中使用hook,也可以在自定义hook中使用其他hook。不能在类组件中使用hook,也不要在普通JavaScript函数中使用hook。
3.自定义hook的函数名要以use开头,便于和普通函数区别,普通函数想在哪用就在哪用,hook函数要遵守以上的使用规则。
17、Hooks的好处
1.hooks方便了组件间状态逻辑的复用。可以自定义一个hooks处理并返回一些状态和方法,然后所有用到相关状态逻辑的组件都能使用这个hooks。而类组件间复用状态逻辑,大多是通过render props或者高阶组件这两种方式,缺点是需要重新组织组件的结构,可能会很麻烦,使代码难以理解。
2.hooks可以将类组件中相互关联的部分拆分成粒度更小的函数,而不需要强制按照生命周期划分,这样更容易管理组件内部的状态逻辑。如类组件经常在同一个componentDidUpdate中处理很多逻辑,各种逻辑揉在一起,稍有不慎,容易出现bug,hooks可以拆分成多个useEffect,这样互不影响,容易管理。
3.hooks彻底告别了JavaScript中困扰已久的this问题。
18.React是如何把对Hooks Api的调用和组件联系起来的?
1.hooks 的实现原理就是,在创建函数组件的fiber节点时,在该fiber节点的memorizedState属性上存放一个hook链表,然后按照函数中每个 hooks api 的调用顺序,从hook链表的对应节点上存取数据并完成各自的逻辑。
2.fiber架构是React在16引入的,之前是jsx -> render function -> vdom 然后直接递归渲染vdom,现在则是多了一步vdom转fiber的reconcile,在reconcile的过程中创建真实dom、做diff并打上增删改的effectTag,添加到副作用链表上,然后一次性commit。这个reconcile是可被打断的,空闲时再触发fiber的schedule调度。
19.为啥只能在顶层调用hooks,不要在循环、条件或嵌套函数中调用
因为hook数据是按调用顺序保存和获取的,在循环、条件或嵌套函数中调用,就会打乱顺序,所以react不允许这样做。
20.React Portal 有哪些使用场景
Portal可以将子节点渲染到父组件以外的DOM节点上。类似于vue的teleport组件
21、什么是高阶函数? 什么是高阶组件? 什么是render props?
高阶函数:是一个函数,接收一个函数作为参数的函数,就是高阶函数。
高阶组件:是一个函数,接收一个组件作为参数,返回一个新的组件。它用来包装组件,把多个组件都用到的状态逻辑放在同一个地方处理。
render props:通过react组件的属性,给它传递过去另一个组件,然后在组件内部渲染传递过去的组件,这就是render props。它用来向组件内部插入内容,类似于vue的插槽。
常见使用render props的库:React Router
常见高阶组件:React.memo()、connect()、withRouter()
// 高阶组件的其中一个用法 - 反向继承: 返回的组件去继承之前的组件
function IIHoc(Comp) {
return class extends Comp {
render() {
return super.render();
}
};
}
22、什么是Fiber?
1.概念:Fiber是一种数据结构,使用链表将每个虚拟节点联系在一起。使原来一整个大的虚拟dom树,变成了具有指针的树形链表。
2.fiber之前遇到的问题:
1.随着应用越来越大,更新渲染的过程会变得吃力,大量的组件渲染会导致主进程长时间被占用,导致一些动画或高频操作出现卡顿的情况。
2.卡顿的关键点是因为“同步阻塞”。在react15的调度算法中,使用“同步递归”的方式进行遍历渲染组件树,而这个过程最大的问题就是,它是一整个大的渲染任务,无法暂停和恢复,会阻断ui渲染,影响页面性能。
3.vue没有这种问题,因为vue是响应式数据,虚拟dom的更新很精细化;而且父组件更新时,如果子组件没有变化,也不会更新;而且vue还在模板编译时,对虚拟节点进行了patchFlag标记、hoistStatic静态提升、cacheHandlers事件缓存、预解析字符串等一系列优化方案,无需fiber,它的更新性能也很优越;而react不一样,react只要父组件更新,子组件无论有没有改变,默认都会更新。所以从16版本开始,react引入了fiber
3.解决方案:解决同步阻塞的方法,通常有两种: 异步 与 任务分割。React Fiber 便是为了实现任务分割而诞生的。
4.具体实现:
1.任务分割:通过指针映射,每个任务单元都记录着它的上一个单元和下一个单元,从而使任务暂停后,可以知道从哪个地方恢复。
2.分散执行:在浏览器每个渲染帧,react都会检查本帧是否有空闲时间,如果有,就利用这段时间执行任务单元。每执行完一个单元,会再次查看否还有空闲时间,如果有,就继续执行;如果没有,就让出控制权,等下次浏览器闲暇时恢复调度,不影响浏览器高优先级任务的执行。
3.用到了关键是两个新API: requestIdleCallback 与 requestAnimationFrame
低优先级的任务交给 requestIdleCallback,这是个浏览器提供的,查询每一帧剩余空闲时间的回调函数。
高优先级的任务交给 requestAnimationFrame,它的回调函数会在浏览器重绘前执行,执行时机是浏览器每一帧的起点。
优先级策略: 文本框输入 > 本次调度结束需完成的任务 > 动画过渡 > 交互反馈 > 数据更新 > 不会显示但以防将来会显示的任务
5.fiber的执行有两个阶段:
1.把 vdom 处理成 fiber 结构,构建副作用链表:(reconcile阶段,可中断)
从根节点开始,通过深度优先遍历处理虚拟dom,生成Fiber结构。fiber对比虚拟dom,主要添加了return、child、sibling三个指针,以及一些其它属性(如类组件fiber会添加stateNode属性、指向类实例,函数组件fiber会添加memorizedState属性,保存组件的hooks)。
通过以上三个指针,可以轻松的找到每个节点的下一个节点或上一个节点,方便调度任务的暂停和恢复。处理完一个节点,就把下一个工作单元指向它的第一个孩子,然后结束本单元执行;如果没有子节点,说明当前节点已经完成了处理,就创建真实dom、添加属性和方法,但先不挂载,并把它添加到副作用链表中;然后把下一个工作单元指向它的下一个兄弟,并结束本单元执行;如果没有下一个兄弟,说明父节点已经完成处理,就把下一个工作单元指向它的叔叔,结束本单元执行;如果没有叔叔节点,就逐级往上找,直到最后为空,说明reconcile阶段已完成,进入commit阶段。
2.渲染真实dom:(commit阶段,不可中断)
构建完副作用链表,就按副作用链表的顺序,把真实dom渲染到页面上。
副作用链表的顺序是fiber节点完成处理的顺序,真实dom插入页面时就按这个顺序。
6.vdom 与 fiber 的关系:
vdom 和 fiber 没有本质的区别,fiber 可以认为是经过加工处理的 vdom,用于任务分割。
23、React Fiber 是如何实现更新过程可控?
更新过程的可控主要体现在下面几个方面:
-
任务分割
-
分散执行:任务挂起、恢复、终止
-
任务具备优先级
任务分割
在 react Fiber 机制中,它采用化整为零 的思想,将调和阶段(Reconiler)递归遍历 VDOM 这个大任务分成若干个小任务,每个任务只负责一个节点的处理
挂起
当第一个小任务完成后,先判断这一阵是否还有空闲时间,没有就挂起下一个任务的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务
恢复
在浏览器渲染完一帧后,判断当前帧是否有剩余时间,如果有就恢复之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段。
终止
其实并不是每次更新都会走到提交阶段。当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作。
任务具备优先级
React Fiber 除了通过挂起、恢复和终止来控制更新外,还给每个任务分配了优先级。具体点就是在创建或者更新 FiberNode 的时候,通过算法给每个任务分配一个到期时间(expirationTime)。在每个任务执行的时候除了判断剩余时间,如果当前处理节点已经过期,那么无论现在是否有空闲时间都必须执行该任务,过期时间的大小还代表着任务的优先级。
24、react受控组件和非受控组件
受控组件就是基于状态或者属性的更新来驱动视图渲染的组件。
在react中,input、textarea等组件默认是非受控组件(输入框内部的值是用户控制,和React无关)。但是也可以转化成受控组件,通过value绑定状态、onChange绑定方法,触发onChange时改变这个状态,这样就成为了受控组件。
// 这个input就是受控组件,受react状态控制
<input type="text" value={this.state.a} onChange={val => this.setState({a: val})} />
// 这个input不是受控组件,因为不受react状态控制
<input type="text" ref={ref=>this.result=ref} />
25、 为什么 React 元素有一个 $$typeof 属性
是为了防止 XSS跨站脚本注入。因为 Symbol 无法被序列化,所以 React 可以通过有没有 $$typeof 属性来断出当前的 element 对象是从数据库来的还是自己生成的。如果没有 $$typeof 这个属性,react 会拒绝处理该元素。
26.什么是jsx?
JSX是js的语法扩展,JSX可以很好地描述UI的呈现形式。
JSX其实是React.createElement的语法糖。
好处:
1.实现声明式的编写用户界面,代码结构清晰、简洁,可读性强。
2.结构、样式和事件等能够实现高内聚低耦合,方便重用和组合。
3.不需要引入新的概念和语法,只写JavaScript即可。比如Angular就引入了控制器、作用域、服务等概念,增加学习成本。
JSX 中的组件名以大写字母开头的原因:因为 React 要知道当前渲染的是组件还是 HTML 元素
27、Error boundaries(边界组件)
1.Error boundaries是自定义的React组件,它会在其子代组件树中的任何位置捕获JS错误,然后记录这些错误,用来展示降级的UI而不至于崩溃。
2.如果class组件定义了生命周期方法 componentDidCatch或static getDerivedStateFromError中的任何一个(或两者),它就成为了Error boundaries。可让捕获子代组件未处理的JS错误并展示降级的UI。注意:仅使用Error boundaries组件来展示异常情况,防止崩溃;不要将它们用于流程控制。(react18暂时(2022/06/05)还没有对应的getSnapshotBeforeUpdate,componentDidCatch和getDerivedStateFromError生命周期的Hook等价写法,但会尽早实现。)
28、useState与useReducer的关系
useState是useReducer的语法糖,useState底层调用的还是useReducer,只不过设置useState状态时,传入的参数如果是对象,直接视为state,如果是函数,使用函数的返回值作为state;而设置useReducer状态时,传入的参数是action,需要调用reducer函数来获取state。
29、react合成事件/react中如何处理事件
合成事件:React为了处理不同浏览器之间的事件差异,同时实现跨端使用事件机制,就实现了一个中间层——SyntheticEvent,统一处理和分发。
React会在根节点上绑定所有事件,当事件触发时,先通过event.target找到对应的节点,然后从节点的store里找到对应的函数并执行。
与原生事件的区别:React并不是将事件直接绑定在dom上,而是采用事件委托的机制绑定到root节点上。
30.React中的StrictMode(严格模式)是什么?
React的StrictMode是一个辅助组件,可以帮助编写更好的react组件。使用<StrictMode />包括组件,可以帮助做以下检查:
1.验证内部组件是否遵循某些推荐做法,如果没有,会在控制台给出警告。
2.验证是否使用的已经废弃的方法,如果有,会在控制台给出警告。
3.通过识别潜在的风险预防一些副作用。
31.为什么类方法需要绑定到类实例?
为了在事件回调中使用this,这个绑定是必不可少的,这并不是React要求的。是因为在JS中,类的成员方法默认不会绑定到this。
32.什么是 React Context?
1.父子组件传值时,通过属性自上而下进行传递,但当层级比较多时,这种用法就极其繁琐。Context提供了一种组件间共享数据的方式,而不必通过组件树逐层传递属性,而且还可以在互不关联的组件间传值。
2.调用React.createContext返回一个Context 对象。Context 对象上有两个react element 对象:Provider(通过其value属性接收值,并提供出去) 和 Consumer(消费Provider提供的数据)。
3.Context配合useReducer可以实现redux的功能:
// 1.创建一个context,即AppContext:
import { createContext } from 'react';
export const AppContext = createContext({});
/*
2.用useReducer创建store和dispatch函数,并把它们传递给AppContext.Provider组件的value属性,
然后AppContext.Provider的所有子级组件都可以共用这一个value
*/
const reducer = (state, action) => {
if (action.type) {
state[action.type] = action.value;
}
return {...state};
}
function App() {
const [store, dispatch] = useReducer(reducer, { test: 'test' });
return (
<AppContext.Provider value={{ store, dispatch }}>
<BrowserRouter>
<MyRoutes />
</BrowserRouter>
</AppContext.Provider>
);
}
// 3.通过useContext使用AppContext里的值:
function Index() {
const { store, dispatch } = useContext(AppContext);
return (
<div onClick={() => dispatch({type: ‘test’, value: ‘test111’})}>
{store.test}
</div>
);
}
Context的实现原理:
1.在 fiber 调和阶段,会将 Provider 的 value 属性,赋值在context 的 _currentValue 属性上
2.fiber调和阶段处理到Consumer时,会从context 的 _currentValue 属性上获取最新的value,而Consumer的孩子是一个函数,就调用这个函数并把最新的value做为参数传递过去,最终完成渲染。
3.同样 fiber调和阶段处理的节点类型为类组件时,如果类存在静态属性contextType,且contextType的值是个context对象,就把context对象的_currentValue赋值给类实例的context属性上,在类中就能通过this.context获取该context对象的value。
4.useContext接收一个context对象作为参数,在fiber节点调和阶段,把context对象的_currentValue保存到hook链表对应的节点中,并返回保存的值。
使用 Context 之前的考虑 (react 官网)
Context 主要应用场景在于很多不同层级的组件需要访问同样的数据。请谨慎使用,因为这会使得组件的复用性变差。
33.组件中props校验
1.导入prop-types包,这个包是脚手架创建时自带的,无须额外安装
2.通过“组件.propTypes”给组件的props添加校验规则
import PropTypes from 'prop-types';
class Test extends React.component {
render () {
return <h1>Hi, {props.colors}</h1>
}
}
Test.propTypes = {
colors: PropTypes.string,
name: PropTypes.string.isRequired, // 可链式调用
}
/*
props的常见效验规则
数据类型:bool array func number object string
React元素的类型:element
是否为必填项:isRequired
特定结构的对象:shape({})
*/
34、React18新特性
1.Automatic batching (setState自动批处理)
1)React 18之前,如果在异步回调函数中(如Promise.then、setTimeout、setInterval)或原生事件里执行setState,无法做到合并处理,每次setState调用都会触发一次重新渲染。
2) React 18中,使用新的API -- ReactDOM.createRoot(root).render(jsx),就能实现自动批量更新。此时不是通过 isBatchUpdate 来判断是否批量更新,而是通过更新优先级进行判断。每次更新都会进行优先级判定,相同优先级的任务会被合并。如果不希望setState批量,只需要使用flushSync包裹。
ReactDOM.flushSync(() => {
setCount(c => c + 1); // 立即更新
setFlag(f => !f);
});
2.Concurrent APIs (并发渲染APIs)
Concurrent的最大特性是,渲染是可以中断的,稍后再继续渲染,也可以遗弃掉。
React18支持并发特性的三个API: startTransition、useTransition、useDeferredValue
import { useState, startTransition, useTransition, useDeferredValue } from 'react';
const [isPending, startTransition1] = useTransition();
/*
isPending表示是否正在渲染。
被startTransition包裹的setState,触发的渲染被标记为不紧急渲染;
在startTransition里切换Suspense视图,不会渲染fallback。
React优先进行紧急渲染,最后执行非紧急渲染。
*/
// 正常情况下是紧急更新
setInputValue(input);
// 在startTransition回调函数内的更新为 非紧急更新
startTransition(() => {
setSearchQuery(input);
});
const [filters, mergeFilter] = useState(defaultFilters);
// useDeferredValue:返回一个非紧急渲染的值
const deferedFilters = useDeferredValue(filters);
<Filters filters={filters} />
<List filters={deferedFilters}>
3.让SSR支持Suspense
Suspense等待目标ui加载,如果某个组件加载慢,就可以将fallback的内容传输到客户端(例如loading态),保证可以和用户尽早的交互。
<Suspense fallback={<Loading />}>
<Header />
<Suspense fallback={<ListPlaceholder />}>
<ListLayout />
</Suspense>
</Suspense>
4.useSyncExternalStore
由于并发渲染在React 18的大规模使用。React 18 提供了useSyncExternalStore,用来保证外部store的一致性。
const store = {
state: { count: 0 },
setState: (fn) => {
store.state = fn(store.state);
},
getSnapshot: () => {
const snap = Object.freeze(store.state);
return snap;
}
}
// use external store
const Component = () => {
const snap = useSyncExternalStore(store.setState, store.getSnapshot);
return <div>{snap.count}</div>
}
35、react-router用法
// 1.创建路由配置组件
/*==routes.jsx start==*/
import { Routes, Route, useRoutes } from 'react-router-dom';
import Index from '../components/index';
import Swiper from '../components/swiper/index';
import Alinum from '../components/alinum';
import Hooks from '../components/hooks';
// export default (props) => ( // 用常规的jsx创建路由配置组件
// <Routes>
// <Route path="/" element={<Index />}>
// <Route path="/alinum" element={<Alinum />} />
// <Route path="/alinum/:id" element={<Alinum />} />
// <Route index element={<Swiper />} />
// <Route path="/swiper" element={<Swiper />} />
// <Route path="/hooks" element={<Hooks />} />
// </Route>
// <Route path="*" element={<Index />} />
// </Routes>
// );
export default (props) => useRoutes([ // 通过useRoutes创建路由配置组件
{
path: "/",
element: <Index />,
children: [
{ path: "alinum", element: <Alinum /> },
{ path: "/alinum/:id", element: <Alinum /> },
{ index: true, element: <Swiper /> },
{ path: "swiper", element: <Swiper /> },
{ path: "hooks", element: <Hooks /> },
]
},
{ path: '*', element: <Index /> },
]);
/*==routes.jsx end==*/
// 2.挂载路由配置组件
/*==App.jsx start==*/
import { HashRouter, BrowserRouter } from 'react-router-dom';
import MyRoutes from './public/routes';
export default function () {
return (
<div className="outer_container">
<BrowserRouter>
<MyRoutes testProps="test" />
</BrowserRouter>
</div>
);
}
/*==App.jsx end==*/
// 3.设置页面导航和路由展示区域
/*==index.jsx start==*/
import { Link, Outlet } from 'react-router-dom';
export default function () {
return (
<div>
<Link to="/alinum">阿里数</Link>
<Link to="/alinum/1">阿里数/1</Link>
<Link to="/alinum?a=1&b=2">阿里数?a=1&b=2</Link>
<Link to="/swiper">轮播图</Link>
<Link to="/hooks">hooks</Link>
<div>
{/* 路由展示区,相当于 this.props.children */}
<Outlet/>
</div>
</div>
);
}
/*==index.jsx end==*/
// 4.在js里使用路由
/*==alinum.jsx start==*/
import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
export default function () {
const navigate = useNavigate(); // 相当于this.props.history.push
const location = useLocation(); // 相当于this.props.location
const [searchParams, setSearchParams] = useSearchParams();
const params = useParams();
console.log('params:', params); // {"id": "1"}
console.log('searchParams.get("a"):', searchParams.get('a'));
console.log('searchParams.get("b"):', searchParams.get('b'));
return (
<div>
<button onClick={() => setSearchParams({a: 2, b: 1})}>searchParams</button>
 
<button onClick={() => navigate('/')}>go home</button>
 
<button onClick={() => navigate('/alinum/2')}>go 阿里数/2</button>
 
<button onClick={() => navigate('/alinum?a=3&b=4')}>go ?a=3&b=4</button>
</div>
);
}
/*==alinum.jsx end==*/
36、react路由底层原理是什么?hash路由和history路由有什么区别?
1.react路由是根据url的不同来渲染不同的组件,在无刷新的情况下进行组件切换。通常有两种模式的路由,hash路由和history路由。
2.hash路由利用location.hash做路由控制,通过a标签或location.hash可以改变路由的hash,用onhashchange事件来监听hash的变化。
3.history路由利用window.history做路由控制,通过history.pushState(stateObj, title, url)和history.replaceState(stateObj, title, url)来改变history的状态,用onpopstate事件来监听history的变化。onpopstate可以监听到浏览器的前进、后退、history.forward、history.back、history.go,因为这些都会修改浏览器历史堆栈的指针;但监听不到history.pushState 和 history.replaceState,需要自己重写history.pushState、history.replaceState。
hash虽然出现在URL中,但不会被包括在HTTP请求中,对后端完全没有影响。
hash就是前端锚点,不会向服务器发送请求,不能做seo优化。
hash的url中带了一个 #, 而history没有。
hash不需要依赖后端,history需要后端做一些路由配置。
// 重写 history.pushState 和 history.replaceState
;(function (history) {
const oldPushState = history.pushState;
const oldReplaceState = history.replaceState;
history.pushState = rewriteFn(oldPushState, 'pushState');
history.replaceState = rewriteFn(oldReplaceState, 'replaceState');
function rewriteFn(fn, eventName) {
return function (state, title, pathname) {
const result = fn.call(history, state, title, pathname); // 执行旧的
if (typeof window.onpopstate === 'function') {
const customEvent = new CustomEvent(eventName, {detail: {pathname, state}});
// 调用window.onpopstate
window.onpopstate(customEvent);
}
return result;
}
}
}(history));
window.onpopstate = function setContent(eve) {
// ...
}
37、react路由懒加载+路由权限
用Suspense组件包裹React.lazy引入的组件
import React, { Component, Suspense } from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter, Route, Routes, Link, Outlet} from 'react-router-dom';
import App from './App';
const Card = React.lazy(() => import("./src/Card"));
const Messgae = React.lazy(() => import("./src/Messgae"));
const Dog = React.lazy(() => import("./src/Dog"));
const lazy = (Component) => (
<Suspense fallback={<div>loading....</div>}>
<Component />
</Suspense>
);
const allRoutes = [
{
route: 'Card',
component: Card
},
{
route: 'Message',
component: Messgae
},
{
route: 'Dog',
component: Dog
},
];
// 用户能获取到的路由
const getRoutes = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(['Card', 'Message'])
}, 1000);
});
}
const RouteComs = (props) => {
const { list } = props;
return (
<Routes>
<Route path="/" element={<App list={list}/>}>
{
list.map((code) => {
const currentRoute = allRoutes.find(e => e.route === code);
if (currentRoute) {
const path = currentRoute.route;
const element = lazy(currentRoute.component);
return <Route path={path} element={element} key={path} />;
}
return '';
})
}
</Route>
</Routes>
);
}
getRoutes().then((list) => {
ReactDOM.render(
<App>
<BrowserRouter>
<RouteComs list={list}/>
</BrowserRouter>
</App>,
document.getElementById('root')
);
});
38、Redux介绍
1.Redux是一个状态管理库,它是一个单独的库, 与react没有关系。
2.它也是利用了单向数据流的思想,配合react非常好用。
3.在项目较大、逻辑较复杂的时候,单向数据流使得数据的变化、流向都很清晰、容易控制。因而使程序更加直观、易于理解,有利于维护。
Redux的三大原则:
1.单一数据源:一个应用只有一个store,整个应用的所有State都存储在唯一的Store中。
2.State是只读的:对于Redux来说,任何时候都不能直接修改state,唯一改变state的方法就是通过dispatch派发action来间接修改。而这一看似繁琐的状态修改方式,实际上反映了Rudux状态管理的核心思想,并且因此使大型应用中的状态管理变得更加清晰规范、容易控制。
3.状态的改变通过纯函数来完成:Redux通过纯函数来执行状态的修改,Action表明了状态修改的意图,而真正执行状态修改的是Reducer。Reducer必须是一个纯函数,当Reducer接收到Action时,并不是直接修改State的值,而是通过返回一个新的状态对象来修改状态。
redux的三个基本概念:action, reducer, store
actions是一个普通对象, 用来描述事件发生的类型, 它作为store.dispatch方法的参数,传给reducer。
reducer是一个纯函数,它接收action和旧的state, 并且返回新的state。
store就是把action和reducer联系到一起的对象, 一个应用只有一个store。
store的职责:
储存应用的state;
提供dispatch(action)方法创建或更新state;
提供getState()方法获取state;
通过subscribe(listener)注册监听器。
redux缺点:
1、action没有命名空间,所有reducer共用一套actions
2、流程太繁琐,需要写各种 action,reducer。
3、要想完成异步数据,得配合其他库。
redux的基本实现:
1、首先实现一个createStore函数,接收两个参数,第一个是reducer函数,第二个是初始状态。
2、在createStore函数里声明两个变量,一个state保存store的状态,另一个listeners数组保存所有的订阅者。
3.在createStore函数里声明三个函数:一个getState函数,用于返回上面声明的state状态;一个subscribe函数,接收一个订阅函数作为参数,并把它保存到listeners数组里;最后一个dispatch函数,在dispatch函数调用用户传入的reducer函数、传入老状态和action,并把返回的新状态赋给state,然后遍历listeners数组,调用里面所有的函数,就能把新状态同步给所有订阅者。最后createStore函数的返回值是一个对象,包含getState、subscribe、dispatch三个函数。
39.React-redux
1.由于redux是独立于react之外的状态管理工具,当我们在react中使用它时,需要手动订阅store的状态,然后对react组件进行更新。而react-reudx则实现了自动订阅功能,我们只需对store进行处理,react组件就会自动更新。
2.redux也是单向数据流,跟React很搭,所以react一般都用redux做状态管理。通过react-redux库的connect方法来连接react组件和redux,通过mapStateToProps把store的state映射到react组件的props,通过mapDispatchToProps把store的dispatch操作映射到组件的属性。
react-redux的实现原理:
1.react-redux就是把redux仓库保存在一个Context对象里实现的,它提供了几个重要的东西:Provider组件,connect高阶组件,useSelector和useDispatch
2.Provider组件就是把Context.Provider元素包装渲染一下,并把redux仓库传递到Context.Provider元素的value属性上。
3.Provider组件的子代组件通过connect高阶组件链接redux,高阶组件上渲染的是原组件,并把属性、state、dispatch都传过去。高阶组件初始化时通过context获取redux仓库并订阅,当收到变更通知时调用setState更新组件。
4.useSelector先通过useContext获取到redux仓库,然后调用用户传入的回调函数并传入redux仓库,最后将返回值返回。useSelector会在真实dom挂载前订阅redux仓库,当收到变更通知时调用useReducer更新组件。
5.useDispatch的实现很简单,通过useContext获取到redux仓库,然后将redux仓库的dispatch方法返回。
中间件:中间件就是一个函数,它对store.dispatch进行了一些改造。如store.dispatch方法原来只能把一个对象做为action,通过redux-thunk中间件改造后,可以把函数也作为action,并可以在这个action函数里做异步操作。现成的支持异步处理的中间件:redux-thunk、redux-promise。
40、什么是纯函数?
满足以下三个条件函数就是纯函数:
1.无论何时何地、不管调用多少次,相同参数始终返回相同的值。
2.不依赖任何外部数据。自包含(不使用全局变量等)
3.不修改程序的任何数据或引起副作用