2022 react 面试题

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特性。

  1. useState:类似于类组件的state,给函数组件添加内部状态。
  2. useEffect:相当于把类组件的componentDidMount、componentDidUpdate和componentWillUnmount,放在一个API里实现了。

    返回一个函数: componentWillUnmount或依赖项改变时会执行。

    useEffect不传第二个参数:

        componentDidMount时会执行函数。

        无论哪个状态或属性改变时,都会执行函数和返回值函数。

    useEffect传入第二个参数:

        第二个参数为空数组:componentDidMount时会执行函数。

        第二个参数为非空数组:componentDidMount时会执行函数,依赖项改变时会执行函数和返回值函数。

  3. useContext与createContext、useReducer配合使用,可以实现redux的功能。
  4. useRef既可以获取dom、获取类组件实例,也可以当成变量使用,还可以配合useImperativeHandle来获取子组件暴露出来的内容。
  5. useCallback用于缓存一个函数,useMemo用于缓存一个值。

    useCallback(fn, deps) 相当于useMemo(() => fn, deps)

  6. useLayoutEffect:实现逻辑与useEffect一样。区别是useLayoutEffect是利用微任务,把回调函数放到dom渲染前执行;useEffect是利用宏任务,把回调函数放到dom渲染完成后执行。可以用useLayoutEffect来获取DOM布局并同步触发重渲染。一般情况下,尽可能使用标准的useEffect,以避免阻塞视觉的更新。
  7. useDeferredValue:react18新增,返回一个非紧急渲染的值。
  8. useTransition:react18新增,返回[isPending, startTransition],isPending表示是否正在渲染。被startTransition包裹的setState,触发的渲染被标记为不紧急渲染;在startTransition里切换Suspense视图,不会渲染fallback。
  9. 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>
            &ensp;
            <button onClick={() => navigate('/')}>go home</button>
            &ensp;
            <button onClick={() => navigate('/alinum/2')}>go 阿里数/2</button>
            &ensp;
            <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.不修改程序的任何数据或引起副作用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值