2023 react 面试题

368 阅读32分钟

react 和 vue 区别

react

以下参考地址:juejin.cn/post/719476…

juejin.cn/post/718238…

对react 框架的理解(React的特性有哪些/设计思想)
  1. 组件化
  2. 数据驱动视图:通过setState
  3. JSX语法: 用于声明组件结构,是一个 JavaScript 的语法扩展。
  4. 单向数据绑定
  5. 虚拟DOM
  6. 声明式编程
jsx
Jsx语法是必须的吗

不使用jsx语法也可以使用React。但,使用createElement方法会使代码更加的冗余,而jsx更加简洁。

JSX是什么,它和JS有什么区别

JSX是react的语法糖,它允许在html中写JS,它不能被浏览器直接识别,需要通过webpack、babel之类的编译工具转换为JS执行

JSX与JS的区别:

  1. JS可以被打包工具直接编译,不需要额外转换,jsx需要通过babel编译,它是React.createElement的语法糖,使用jsx等价于React.createElement
  2. jsx是js的语法扩展,允许在html中写JS;JS是原生写法,需要通过script标签引入
Babel 插件是如何实现JSX到JS的编译

Babel 读取代码并解析,生成 AST,再将 AST 传入插件层进行转换,在转换时就可以将 JSX 的结构转换为 React.createElement 的函数。

为什么提出JSX

React认为视图和逻辑内在耦合,比如,在 UI 中需要绑定处理事件、在某些时刻状态发生变化时需要通知到 UI,以及需要在 UI 中展示准备好的数据。

React并没有采用将视图与逻辑进行分离到不同文件这种人为地分离方式,而是通过将二者共同存放在称之为“组件”的松散耦合单元之中,来实现关注点分离。

为什么在文件中没有使用react,也要在文件顶部import React from “react”

只要使用了jsx,就需要引用react,因为jsx是react语法糖,通过babel编译后是React.createElement

为什么React自定义组件首字母要大写

jsx通过babel转义时,调用了React.createElement函数,它接收三个参数,分别是type元素类型,props元素属性,children子元素。

如下图所示,从jsx到真实DOM需要经历jsx->虚拟DOM->真实DOM。如果组件首字母为小写,它会被当成字符串进行传递,在创建虚拟DOM的时候,就会把它当成一个html标签,而html没有app这个标签,就会报错。组件首字母为大写,它会当成一个变量进行传递,React知道它是个自定义组件就不会报错了

<app>lyllovelemon</app>
// 转义后
React.createElement("app",null,"lyllovelemon")
​
<App>lyllovelemon</App>
// 转义后
React.createElement(App,null,lyllovelemon)
React组件为什么不能返回多个元素

这个问题也可以理解为React组件为什么只能有一个根元素,原因:

  1. React组件最后会编译为render函数,函数的返回值只能是1个,如果不用单独的根节点包裹,就会并列返回多个值,这在js中是不允许的

  2. react的虚拟DOM是一个树状结构,树的根节点只能是1个,如果有多个根节点,无法确认是在哪棵树上进行更新

    vue的根节点为什么只有一个也是同样的原因

React组件怎样可以返回多个组件
  • 使用HOC(高阶函数)

  • 使用React.Fragment,可以让你将元素列表加到一个分组中,而且不会创建额外的节点(类似vue的template)

    renderList(){
      this.state.list.map((item,key)=>{
        return (<React.Fragment>
          <tr key={item.id}>
            <td>{item.name}</td>
            <td>{item.age}</td>
            <td>{item.address}</td>
          </tr> 
        </React.Fragment>)
      })
    }
    
  • 使用数组返回

    renderList(){
      this.state.list.map((item,key)=>{
        return [
          <tr key={item.id}>
            <td>{item.name}</td>
            <td>{item.age}</td>
            <td>{item.address}</td>
          </tr>
        ]
      })
    }
    ​
    
React中元素和组件的区别

react组件有类组件、函数组件

react元素是通过jsx创建的

const element = <div className="element">我是元素</div> 
react 的生命周期
  • 挂载

    • 当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

      • constructor()

      • static getDerivedStateFromProps()

      • render()

      • componentDidMount()

        getDerivedStateFromProps

        该方法是新增的生命周期方法,是一个静态的方法,因此不能访问到组件的实例

        执行时机:组件创建和更新阶段,不论是props变化还是state变化,都会调用。

        在每次render方法前调用,第一个参数为即将更新的props,第二个参数为上一个状态的state,可以比较props 和 state来加一些限制条件,防止无用的state更新

        该方法需要返回一个新的对象作为新的state或者返回null表示state状态不需要更新

  • 更新

    当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

    • static getDerivedStateFromProps()
    • shouldComponentUpdate()
    • render()
    • getSnapshotBeforeUpdate()
    • componentDidUpdate()

    getSnapshotBeforeUpdate

    该周期函数在render后执行,执行之时DOM元素还没有被更新

    该方法返回的一个Snapshot值(不返回报错),作为componentDidUpdate第三个参数传入

    getSnapshotBeforeUpdate(prevProps, prevState) {
        console.log('#enter getSnapshotBeforeUpdate');
        return 'foo';
    }
    ​
    componentDidUpdate(prevProps, prevState, snapshot) {
        console.log('#enter componentDidUpdate snapshot = ', snapshot);
    }
    ​
    

    此方法的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些UI视觉上的状态

    shouldComponentUpdate

    参数两个,nextProps:下一个props值,nextState下一个state值。

    返回false的话,那么react是不会去进行diff流程,也不会去对比props,更不会去更新用户界面上的组件了

  • 卸载

    当组件从 DOM 中移除时会调用如下方法:

    • componentWillUnmount()
  • 错误处理

    当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

    • static getDerivedStateFromError():更改状态,从而显示降级组件
    • componentDidCatch():打印错误信息
React父子组件的生命周期调用顺序
React 事件执行顺序

parent constructor

parent getDerivedStateFromProps

parent render

son constructor

son getDerivedStateFromProps

son render

// 注意

son didMount

parent didMount

son willUnmount

parent willUnmount

对于react 虚拟DOM的理解
  • js对象,保存在内存中
  • 是对真实DOM结构的映射

虚拟 DOM 的工作流程:

挂载阶段:React 将结合 JSX 的描述,构建出虚拟 DOM 树,然后通过 ReactDOM.render 实现虚拟 DOM 到真实 DOM 的映射(触发渲染流水线);

更新阶段:页面的变化先作用于虚拟 DOM,虚拟 DOM 将在 JS 层借助算法先对比出具体有哪些真实 DOM 需要被改变,然后再将这些改变作用于真实 DOM。

虚拟 DOM 解决的关键问题有以下三个:

  • 减少 DOM 操作:虚拟 DOM 可以将多次 DOM 操作合并为一次操作
  • 研发体验/研发效率的问题:虚拟 DOM 的出现,为数据驱动视图这一思想提供了高度可用的载体,使得前端开发能够基于函数式 UI 的编程方式实现高效的声明式编程。
  • 跨平台的问题:虚拟 DOM 是对真实渲染内容的一层抽象。同一套虚拟 DOM,可以对接不同平台的渲染逻辑,从而实现“一次编码,多端运行”
VDOM 和 DOM 的区别
  • 真实DOM存在重排和重绘,虚拟DOM不存在;
  • 虚拟 DOM 的总损耗是“虚拟 DOM 增删改+真实 DOM 差异增删改+排版与重绘(可能比直接操作真实DOM要少)”,真实 DOM 的总损耗是“真实 DOM 完全增删改+排版与重绘”

传统的原生 api 或 jQuery 去操作 DOM 时,浏览器会从构建 DOM 树开始从头到尾执行一遍流程。

当你在一次操作时,需要更新 10 个 DOM 节点,浏览器没这么智能,收到第一个更新 DOM 请求后,并不知道后续还有 9 次更新操作,因此会马上执行流程,最终执行 10 次流程。

而通过 VNode,同样更新 10 个 DOM 节点,虚拟 DOM 不会立即操作 DOM,而是将这 10 次更新的 diff 内容保存到本地的一个 js 对象中,最终将这个 js 对象一次性 attach 到 DOM 树上,避免大量的无谓计算。

VDOM 和 DOM 优缺点

真实 DOM 的优势:

  • 易用

真实 DOM 的缺点:

  • 效率低,解析速度慢,内存占用量过高
  • 性能差:频繁操作真实 DOM,易于导致重绘与回流

虚拟 DOM 的优势:

  • 简单方便:如果使用手动操作真实 DOM 来完成页面,繁琐又容易出错,在大规模应用下维护起来也很困难
  • 性能方面:使用 Virtual DOM,能够有效避免真实 DOM 数频繁更新,减少多次引起重绘与回流,提高性能
  • 跨平台:React 借助虚拟 DOM,带来了跨平台的能力,一套代码多端运行

虚拟 DOM 的缺点:

  • 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化,首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,速度比正常稍慢
函数组件和类组件输出差别(闭包陷阱)

以下函数组件代码,先alert再add,页面显示的值和alert的值分别是什么

import {useState} from "react";
​
const FunctionComponentClosure = () => {
    const [value, setValue] = useState(1);
    const log = () => {
        setTimeout(() => {
            alert(value)
        }, 3000)
    }
    return (
        <div>
            <p>{value}</p>
            <button onClick={log}>alert</button>
            <button onClick={() => setValue(value + 1)}>add</button>
        </div>
    )
}
​
export default  FunctionComponentClosure

alert :1

页面显示:2

原因:log方法内的value和点击动作触发时的value相同,后续value的变化不会对log内部的value产生任何的影响。这种现象被称为 闭包陷阱,即函数式组件每次render都产生一个新的log函数,这个log函数会产生一个当前阶段value值的闭包。

除了闭包陷阱之外,函数组件和类组件还存在如下区别:

  • 写法不同:函数组件代码更加简洁
  • 函数组件不需要处理this但是类组件需要
  • 类组件有生命周期和state函数组件不存在(但是函数组件中可以通过hooks达到类似的效果)
如何解决闭包陷阱
const Test = () => {
    const [value, setValue] = useState(1);
    const countRef = useRef(value)
​
    const log = function () {
        setTimeout(() => {
            alert(countRef.current)
        }, 3000)
    }
    useEffect(() => {
        countRef.current = value
    }, [value])
​
    return (
        <div>
            <p>{value}</p>
            <button onClick={log}>alert</button>
            <button onClick={() => setValue(value + 1)}>add</button>
        </div>
    )
}
​

useRef每次render都会返回同一个引用类型对象,设置和读取都在这个对象上处理的话,就可以得到最新的value值了。

在类组件中情况是否会相同呢?

class Test extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            value: 1
        }
    }
    log = () => {
        setTimeout(() => {
            alert(this.state.value)
        }, 3000)
    }
    render() {
        return (
            <div>
                <p>{this.state.value}</p>
                <button onClick={this.log}>alert</button>
                <button onClick={() => this.setState({
                    value: this.state.value + 1
                })}>add</button>
            </div>
        )
    }
}
export default Test

alert和页面显示的值相同。

受控组件和非受控组件

受控组件:简单理解为双向绑定,数据和视图的变化是同步的,受控组件一般需要初始状态(value或者checked) 和一个 状态更新事件函数

非受控组件:不受控制的组件,在其内部存储自身的状态,可以通过ref查询DOM的当前值。初始状态为defaultValue

推荐使用受控组件,在受控组件中数据由React组件处理。

操作DOM的情况下一般需要使用非受控组件,数据由DOM本身处理,控制能力较弱,但是代码量更少。

React如何实现状态自动保存(vue中的keep-alive)
为什么需要状态保存

在React中通常使用路由去管理不同的页面,在切换页面时,路由将会卸载掉未匹配的页面组件,所以比如从列表进入详情页面,等到退回列表页面时会回到列表页的顶部。

什么情况下需要状态保存
  • 列表进入详情
  • 已填写但是未提交的表单
  • 管理系统中可切换和关闭的标签

总而言之就是在交互过程中离开需要对状态进行保存的场景

React为什么不支持

状态保存在vue中可以使用keep-alive进行实现,但是react认为这个功能容易造成内存泄漏,所以暂时不支持。

如何实现
  1. 手动保存状态:适用于数据较少的情况

在componentWillUnmount的时候将状态通过redux进行保存,然后在componentDidMount周期进行数据恢复。

  1. 通过路由实现:

基本思想是,将KeepAlive中的组件也就是children取出来,渲染到一个不会被卸载的组件keeper中,在使用Dom操作将keeper内的真实内容移入对应的keepalive

useEffect和useLayoutEffect有什么区别。 -----?

相同点:

  • 处理副作用:函数组件内不允许操作副作用。比如:改变DOM、设置订阅、操作定时器等
  • 底层都是调用mountEffectlmpl方法,基本上可以替换使用

不同点:

  • useEffect在像素变化之后异步调用,改变屏幕内容可能会造成页面的闪烁
  • useLayoutEffect在像素变化之前同步调用,可能会造成页面延迟显示,但是不会闪烁:主要用于处理DOM操作、调整样式、避免页面闪烁等。因为是同步执行,所以要避免做大量计算,从而避免造成阻塞。
  • useLayoutEffect先于useEffect执行
对react hook的理解,解决了什么问题

官方给出的动机是解决长时间使用和维护react过程中常遇到的问题,例如:

  • 难以重用和共享组件中的与状态相关的逻辑
  • 逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 local state 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面
  • 类组件中的this增加学习成本,类组件在基于现有工具的优化上存在些许问题
  • 由于业务变动,函数组件不得不改为类组件等等

在以前,函数组件也被称为无状态的组件,只负责渲染的一些工作

在有了hooks之后,函数组件也可以是有状态的组件,内部也可以维护自身的状态以及做一些逻辑方面的处理。

hooks的出现,使函数组件的功能得到了扩充,拥有了类组件相似的功能,在我们日常使用中,使用hooks能够解决大多数问题,并且还拥有代码复用机制,因此优先考虑hooks。

React常用的hooks
useState

定义状态,解决了函数组件没有状态的问题。

接受一个初始值(初始值可以是一个具体数据类型,也可以是一个函数,该函数只执行一次返回值作为初始值)作为参数,返回一个数组,第一项是变量,第二项是设置变量的函数。

  • 对象不可局部更新:state是一个对象时,不能局部更新对象属性,useState不会合并,会把整个对象覆盖。要用展开运算符自己进行属性值的覆盖。

  • 地址要变更:对于引用类型,数据地址不变的时候,认为数据没有变化,不会更新视图。

  • useState异步回调问题:如何获取到更新后的state,使用useEffect,当state变化时触发

  • 操作合并:传入对象会被合并,传入函数,使用preState参数不会被合并

    setState({
                ...state,
                name: state.name + '!'
            })
            
    setState((pre) => ({ ...state, name: pre.name + '!' }))
    

对比类组件的state

  1. 在正常的react的事件流里(如onClick等):

    • setState和useState是异步执行的(不会立即更新state的结果)
    • 多次执行setState和useState,只会调用一次重新渲染render
    • 传入对象会被合并,函数则不会被合并。可以通过setState传入一个函数来更新state,这样不会被合并
  2. 在setTimeout,Promise.then等异步事件中:

    • setState和useState是同步执行的(立即更新state的结果)
    • 多次执行setState和useState,每一次的执行setState和useState,都会调用一次render

setState执行机制(类组件)

setState第一个参数可以是一个对象,或者是一个函数,而第二个参数是一个回调函数,用于可以实时的获取到更新之后的数据。

同步异步

  • 在组件生命周期或React合成事件中,setState是异步。要想获取更新后的值,可以通过setState的第二个参数传入一个函数(函数组件通过useEffect)。
  • 在setTimeout或者原生dom事件中,setState是同步。

批量更新

  • 合成事件或者生命周期中setState传入对象会被合并。要想避免合并可以将第一个参数写成函数
  • 而在setTimeout或者原生dom事件中,由于是同步的操作,所以并不会进行覆盖现象。
useEffect

异步操作:

useEffect返回的是clean-up函数,因此没有办法返回一个promise实现异步

  • 立即执行函数:

    useEffect(() => {
        (async function anyNameFunction() {
          await loadContent();
        })();
      }, []);
    
  • 在useEffect外部或者内部实现async/await函数,然后在内部调用

useContext

共享状态钩子。不同组件之间共享状态,避免props层层传递

  • React.createContext
  • Context.Provider <--> useContext(Context) /<Context.Consumer>
useReducer

Action钩子,复杂版的useState

redux的原理是用户在页面中发起action,从而通过reducer方法来改变state,从而实现页面和状态的通信。而Reducer的形式是(state, action) => newstate。类似,我们的useReducer()是这样的:

const [state, dispatch] = useReducer(reducer, initialState)
useEffect的触发时机

触发机制跟第二个参数有关:

  • 第二个参数不传时:每次渲染完成后触发
  • 第二个参数是一个空数组时:初始化渲染完成后触发,相当于didMounted
  • 第二个参数是非空数组时:数组中数据有一项更新时触发

数组中的内容一般是props或者state,是普通变量时不会触发执行

hooks使用规则
  • Hooks只在函数组件顶层调用,不要在循环、条件判断或者嵌套函数中调用钩子。在类组件中无法使用。
  • 对于自定义Hooks,使用use开头命名。
useMemo、memo、useCallback

他们三个的应用场景都是缓存结果,当依赖值没有改变时避免不必要的计算或者渲染。

  • useCallback 是针对函数进行“记忆”的,当它依赖项没有发生改变时,那么该函数的引用并不会随着组件的刷新而被重新赋值。

    当我们觉得一个函数不需要随着组件的更新而更新引用地址的时候,我们就可以使用 useCallback 去修饰它。

  • React.memo 是对组件进行 “记忆”,当它接收的 props 没有发生改变的时候,那么它将返回上次渲染的结果,不会重新执行函数返回新的渲染结果。

  • React.useMemo是针对 值计算 的一种“记忆“,当依赖项没有发生改变时,那么无需再去计算,直接使用之前的值,对于组件而言,这带来的一个好处就是,可以减少一些计算,避免一些多余的渲染。当我们遇到一些数据需要在组件内部进行计算的时候,可以考虑一下 React.useMemo

useMemo与useEffect的区别

传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行不应该在渲染期间内执行的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。

useEffect在渲染后执行,可以访问渲染后的值。

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。和useEffect类似,但是如果每次渲染时都计算,那就没必要使用useMemo了。

state和props有什么区别

相同点:

  • 两者都是 JavaScript 对象
  • 两者都是用于保存状态
  • props 和 state 都能触发渲染更新

区别:

  • props 是外部传递给组件的,而 state 是在组件内被组件自己管理的,一般在 constructor 中初始化
  • props 在组件内部是不可修改的,但 state 在组件内部可以进行修改 state 是多变的、可以修改
super和super(props)的区别

super 关键字实现调用父类,super 代替的是父类的构建函数,使用 super(name) 相当于调用sup.prototype.constructor.call(this,name)(父类构造函数)

super() 就是将父类中的 this 对象继承给子类的,子类是没有自己的 this 对象的,它只能继承父类的 this 对象,然后对其进行加工。 没有 super() 子类就得不到 this 对象。

综上所述:

  • 在 React 中,类组件基于 ES6,所以在 constructor 中必须使用 super
  • 在调用 super 过程,无论是否传入 props,React 内部都会将 porps 赋值给组件实例 porps 属性中
  • 如果只调用了 super(),那么 this.props 在 super() 和构造函数结束之间仍是 undefined
react事件绑定方式有哪些

绑定方式

  1. render方法中使用bind
  • <div onClick={this.handleClick.bind(this)}>test</div>
  • 这种方式在组件每次render渲染的时候,都会重新进行bind的操作,影响性能
  1. render方法中使用箭头函数
  • <div onClick={e => this.handleClick(e)}>test</div>
  • 每一次render的时候都会生成新的方法,影响性能
  1. constructor中bind:this.handleClick = this.handleClick.bind(this);
  2. 定义阶段使用箭头函数绑定 onClick={this.handleclick} const this.handleclick=()=>{}

区别

  • 编写方面:方式一、方式二、方式四写法简单,方式三的编写过于冗杂
  • 性能方面:方式一和方式二在每次组件render的时候都会生成新的方法实例,性能问题欠缺。若该函数作为属性值传给子组件的时候,都会导致额外的渲染。而方式三、方式四只会生成一个方法实例

综合上述,方式四是最优的事件绑定方式。

react 中组件之间如何通信
React中key的作用

对比不同类型的元素:当元素类型变化时,会销毁重建

对比同一类型的元素:当元素类型不变时,比对及更新有改变的属性并且“在处理完当前节点之后,React 继续对子节点进行递归。”

对子节点进行递归:React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。若key一致,则进行更新,若key不一致,就销毁重建

react函数组件和类组件的区别

针对两种React组件,其区别主要分成以下几大方向:

  • 编写形式:类组件的编写形式更加的冗余
  • 状态管理:在hooks之前函数组件没有状态,在hooks提出之后,函数组件也可以维护自身的状态
  • 生命周期:函数组件没有生命周期,这是因为生命周期钩子都来自于继承的React.Component,但是可以通过useEffect实现类似生命周期的效果
  • 调用方式:函数组件通过执行函数调用,类组件通过实例化然后调用实例的render方法
  • 获取渲染的值:函数组件存在闭包陷阱,类组件不存在(Props在 React中是不可变的所以它永远不会改变,但是 this 总是可变的,以便您可以在 render 和生命周期函数中读取新版本)
react高阶组件以及应用场景

js高阶函数(Higher-order function),至少满足下列一个条件的函数

  • 接受一个或多个函数作为输入
  • 输出一个函数

通用的逻辑放在高阶组件中,对组件实现一致的处理,从而实现代码的复用。所以,高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用。

高阶组件遵循的规则

  • 不要改变原始组件,而应该使用组合

  • HOC 应该透传与自身无关的 props

  • 包装显示名字以便于调试

  • 不要在 render() 方法中使用高阶组件:这将导致子树每次渲染都会进行卸载,和重新挂载的操作!

  • Refs 不会被传递:ref 实际上并不是一个 prop(就像 key 一样),它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。

    高阶组件可以传递所有的props,但是不能传递ref,ref可以使用React.forwardRef

react render原理,在什么时候触发

render存在两种形式:

  • 类组件中的render方法
  • 函数组件的函数本身

触发时机:

  • 类组件setState
  • 函数组件通过useState hook修改状态

一旦执行了setState就会执行render方法(无论值是否发生变化),useState 会判断当前值有无发生改变确定是否执行render方法,一旦父组件发生渲染,子组件也会渲染

如何提高组件的渲染效率

类组件通过调用setState方法, 就会导致render,父组件一旦发生render渲染,子组件一定也会执行render渲染

  • shouldComponentUpdate

    • 通过shouldComponentUpdate生命周期函数来比对 state和 props,确定是否要重新渲染
    • 默认情况下返回true表示重新渲染,如果不希望组件重新渲染,返回 false 即可
  • React.memo

    • React.memo 只能用于函数组件
    • 如果需要深层次比较,这时候可以给memo第二个参数传递比较函数
为什么需要前端路由

早期:一个页面对应一个路由,路由跳转导致页面刷新,用户体验差

ajax的出现使得不刷新页面也可以更新页面内容,出现了SPA(单页应用)。SPA不能记住用户操作,只有一个页面对URL做映射,SEO不友好

前端路由帮助我们在仅有一个页面时记住用户进行了哪些操作

前端路由解决了什么问题
  1. 当用户刷新页面,浏览器会根据当前URL对资源进行重定向(发起请求)
  2. 单页面对服务端来说就是一套资源,怎么做到不同的URL映射不同的视图内容
  3. 拦截用户的刷新操作,避免不必要的资源请求;感知URL的变化
React Router 有几种模式,实现原理是什么

react Router 有四个库:

  • react router:核心库,封装了Router,Route,Switch等核心组件,实现了从路由的改变到组件的更新的核心功能,
  • react router dom:dom环境下的router。在react-router的核心基础上,添加了用于跳转的Link组件,和histoy模式下的BrowserRouter和hash模式下的HashRouter组件等。所谓BrowserRouter和HashRouter,也只不过用了history库中createBrowserHistory和createHashHistory方法
  • react router native:RN环境下的router
  • react router config

React Router对应的hash模式和history模式对应的组件为:

  • HashRouter
  • BrowserRouter

BrowserRouter 与 HashRouter 对⽐

BrowserRouter使用的HTML5history api实现路由跳转 ​ HashRouter使用URL的hash属性控制路由跳转

  • HashRouter 最简单,每次路由变化不需要服务端接入,根据浏览器的hash来区分 path 就可以;BrowserRouter需要服务端解析 URL 返回页面,因此使用BrowserRouter需要在后端配置地址映射。
  • BrowserRouter 触发路由变化的本质是使⽤ HTML5 history API( pushState、replaceState 和 popstate 事件)
  • HashRouter 不⽀持 location.key 和 location.state,动态路由需要通过?传递参数。改变URL以#分割的路径字符串
  • Hash history 只需要服务端配置一个地址就可以上线,但线上的 web 应⽤很少使用这种方式。
为什么 React 的 Diff 算法不采用 Vue 的双端对比算法?

React 不能通过双端对比进行 Diff 算法优化是因为目前 Fiber 上没有设置反向链表

React、Vue3、Vue2 的 Diff 算法对比

不同点

对静态节点的处理不一样。

由于 Vue 是通过 template 模版进行编译的,所以在编译的时候可以很好对静态节点进行分析然后进行打补丁标记,然后在 Diff 的时候,Vue2 是判断如果是静态节点则跳过过循环对比,而 Vue3 则是把整个静态节点进行提升处理,Diff 的时候是不过进入循环的,所以 Vue3 比 Vue2 的 Diff 性能更高效。而 React 因为是通过 JSX 进行编译的,是无法进行静态节点分析的,所以 React 在对静态节点处理这一块是要逊色的。

Vue2 和 Vue3 的比对和更新是同步进行的,这个跟 React15 是相同的,就是在比对的过程中,如果发现了那些节点需要移动或者更新或删除,是立即执行的,也就是 React 中常讲的不可中断的更新,如果比对量过大的话,就会造成卡顿,所以 React16 起就更改为了比对和更新是异步进行的,所以 React16 以后的 Diff 是可以中断,Diff 和任务调度都是在内存中进行的,所以即便中断了,用户也不会知道。

对Fiber架构的理解,解决了什么问题

在 React15 以前 React 的组件更新创建虚拟 DOM 和 Diff 的过程是不可中断,如果需要更新组件树层级非常深的话,在 Diff 的过程会非常占用浏览器的线程,而我们都知道浏览器执行JavaScript 的线程和渲染真实 DOM 的线程是互斥的,也就是同一时间内,浏览器要么在执行 JavaScript 的代码运算,要么在渲染页面,如果 JavaScript 的代码运行时间过长则会造成页面卡顿

基于以上原因 React 团队在 React16 之后就改写了整个架构,将原来数组结构的虚拟DOM,改成叫 Fiber 的一种数据结构,基于这种 Fiber 的数据结构可以实现由原来不可中断的更新过程变成异步的可中断的更新

如何解决

屏幕刷新率(FPS)

  • 浏览器的正常绘制频率是60次/秒,小于这个值时,用户会感觉到卡顿
  • 绘制一次的称为一帧,平均每帧16.6ms
  • 每个帧的开头包括样式计算、布局和绘制
  • js的执行是单线程,js引擎和页面渲染引擎都占用主线程,GUI渲染和Javascript执行两者是互斥的
  • 如果某个js任务执行时间过长,浏览器会推迟渲染,每帧的绘制时间超过16.6ms,造成页面卡顿

Fiber把渲染更新过程拆分成多个子任务,每次只做一小部分,做完看是否还有剩余时间,如果有继续下一个任务,会执行 requestIdleCallback;如果没有,挂起当前任务,将时间控制权交给浏览器(浏览器可以进行渲染),等浏览器不忙的时候再继续执行

可以中断与恢复,恢复后也可以复用之前的中间状态,并给不同的任务赋予不同的优先级,其中每个任务更新单元为 React Element 对应的 Fiber节点

实现的上述方式的是requestIdleCallback方法: window.requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应

Fiber 架构可以分为三层:

  • Scheduler 调度器 —— 调度任务的优先级,高优任务优先进入 Reconciler。requestIdleCallback在调度器中用到。
  • Reconciler 协调器 —— 负责找出变化的组件
  • Renderer 渲染器 —— 负责将变化的组件渲染到页面上

相比 React15,React16 多了Scheduler(调度器) ,调度器的作用是调度更新的优先级。

在新的架构模式下,工作流如下:

  • 每个更新任务都会被赋予一个优先级。
  • 当更新任务抵达调度器时,高优先级的更新任务(记为 A)会更快地被调度进 Reconciler 层;
  • 此时若有新的更新任务(记为 B)抵达调度器,调度器会检查它的优先级,若发现 B 的优先级高于当前任务 A,那么当前处于 Reconciler 层的 A 任务就会被中断,调度器会将 B 任务推入 Reconciler 层。
  • 当 B 任务完成渲染后,新一轮的调度开始,之前被中断的 A 任务将会被重新推入 Reconciler 层,继续它的渲染之旅,即“可恢复”。

Fiber 架构的核心即是”可中断”、”可恢复”、”优先级”

fiber对生命周期的影响

从 Firber 机制 render 阶段的角度看 react 即将废除的三个生命周期的共同特点是都处于 render 阶段:

componentWillMount

componentWillUpdate

componentWillReceiveProps

setstate 是同步还是异步的

setState并不是单纯的异步或同步,这其实与调用时的环境相关

  • React内部机制能检测到的地方, setState就是异步的;

  • React检测不到的地方,例如 原生事件addEventListener,setInterval,setTimeoutsetState就是同步更新的

    或者说

  • 在合成事件 和 生命周期钩子(除componentDidUpdate) 中,setState是"异步"的;

  • 在 原生事件 和setTimeout 中,setState是同步的,可以马上获取更新后的值;

只是因为react性能机制体现为异步(异步现象主要原因是,在 React 的生命周期以及绑定的事件流中,所有的 setState 操作会先缓存到一个队列中,在整个事件结束后或者 mount 流程结束后,才会取出之前缓存的 setState 队列进行一次计算,触发 state 更新)

批量更新

多个顺序的setState不是同步地一个一个执行滴,会一个一个加入队列,然后最后一起执行。在 合成事件生命周期钩子 中,setState更新队列时,存储的是 合并状态(Object.assign)。因此前面设置的 key 值会被后面所覆盖,最终只会执行一次更新。

React18以后,使用了createRoot api后,所有setState都是异步批量执行的

调用setstate 发生了什么

自己总结: 1、创建更新队列 2、使用Fiber 的调度算法,生成新的Fiber树 3、执行doWork方法 4、在执行doWork方法时,react 会执行一遍队列中的方法,以获取新的节点,然后与旧的节点进行对比。为老节点打上更新、插入、替换等Tag标记。 5、在doWork完成后,获取新节点,然后再重复上面的过程。 6、当所有节点都doWork完成后,触发commitRoot方法,react进入commit阶段。 7、在commit阶段中,react会根据当前打的Tag标记,一次性更新整个dom. 【

  • 在 setState 的时候,React 会为当前节点创建一个 updateQueue 的更新列队。
  • 然后会触发 reconciliation(协调) 过程,在这个过程中,会使用名为 Fiber 的调度算法,开始生成新的 Fiber 树, Fiber 算法的最大特点是可以做到异步可中断的执行。
  • 然后 React Scheduler(渲染器) 会根据优先级高低,先执行优先级高的节点,具体是执行 doWork 方法。
  • 在 doWork 方法中,React 会执行一遍 updateQueue 中的方法,以获得新的节点。然后对比新旧节点,为老节点打上 更新、插入、替换 等 Tag。
  • 当前节点 doWork 完成后,会执行 performUnitOfWork(执行单元) 方法获得新节点,然后再重复上面的过程。
  • 当所有节点都 doWork 完成后,会触发 commitRoot 方法,React 进入 commit 阶段。
  • 在 commit 阶段中,React 会根据前面为各个节点打的 Tag,一次性更新整个 dom 元素。 】
为什么直接修改this.state无效

setState本质是通过一个队列机制实现state更新的。 执行setState时,会将需要更新的state合并后放入状态队列,而不会立刻更新state,队列机制可以批量更新state

如果不通过setState而直接修改this.state,那么这个state不会放入状态队列中,下次调用setState时对状态队列进行合并时,会忽略之前直接被修改的state,这样我们就无法合并了,而且实际也没有把你想要的state更新上去

React18后

v18 之前只在事件处理函数中实现了批处理,在 v18 中所有更新都将自动批处理,包括 promise链setTimeout等异步代码以及原生事件处理函数

为什么虚拟dom 会提高性能?

虚拟dom 相当于在 JS 和真实 dom 中间加了一个缓存,利用 diff 算法避免了没有必要的 dom 操作,从而提高性能。

为什么出现hook

Class

  1. 在组件之间复用状态逻辑很难
  2. 复杂组件变得难以理解
  3. 难以理解的class
useEffect和useLayoutEffect有什么区别
Redux工作原理

Redux工作原理

使用单例模式实现

Store 一个全局状态管理对象

Reducer 一个纯函数,根据旧state和props更新新state

Action 改变状态的唯一方式是dispatch action

Redux是一个状态管理库,使用场景:

  • 跨层级组件数据共享与通信
  • 一些需要持久化的全局数据,比如用户登录信息

connect 介绍下

**export** **default** **connect**(mapStateToProps, mapDispatchToProps)(**User**);

用于连接 store

mapStateToProps:此函数将state映射到 props 上,因此只要state发生变化,新 state 会重新映射到 props。 这是订阅store的方式。

mapDispatchToProps:此函数用于将 action creators 绑定到你的props 。以便我们可以在第12行中使用This . props.actions.sendemail()来派发一个动作。

作者:Fundebug 链接:juejin.cn/post/684490… 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Redux和Vuex的异同点,以及用到的相同的思想

相同点

  • state共享数据
  • 流程一致:定义全局state,触发修改方法,修改state
  • 全局注入store

不同点:

  • redux使用的是不可变数据,而Vuex是可变的。
  • redux每次都是用新的state替换旧的state,vuex是直接修改。
  • redux在检测数据变化时是通过diff算法比较差异的;vuex是通过getter/setter来比较的
  • vuex定义了state,getter,mutation,action;redux定义了state,reducer,action
  • vuex中state统一存放,方便理解;react中state依赖reducer初始值
  • vuex的mapGetters可以快捷得到state,redux中是mapStateToProps
  • vuex同步使用mutation,异步使用action;redux同步异步都使用reducer

相同思想

  • 单一数据源
  • 变化可预测
  • MVVM思想