React---面试题总结

446 阅读14分钟

React三大核心理念

React是一个网页UI框架,通过组件化的方式解决视图层开发复用的问题,本质是一个组件化框架 核心思路:

  • 声明式:声明式编程优势直观,便于组合
  • 组件化:降低系统间功能的耦合性,提高内部的聚合性
  • 单向数据流:数据从父组件到子组件,子组件不能直接修改父组件传递的props。若需要修改,必须通过调用父组件传递的回调函数实现

React使用声明式(JSX)

jsx是JS的语法扩展,结构类似XML,主要用于声明React元素。JSX通过Babel编译,借助@babel/plugin-transform-react-jsx,JSX会被编译为React.createElement(),返回“React Element”的JS对象。

Babel插件如何实现JSX到JS的编译?

通过 babel 的 react 预设包(@babel/preset-react), 我们就可以对 JSX 进行转换:  JSX 转为 React.createElement(...)

类组件与函数组件的区别

  • 编程方式:类组件是基于面向对象编程,函数是函数式编程
  • 类组件有生命周期,函数组件没有。
  • 类组件需要继承Class,函数组件不需要。类组件可以获取实例化的this,可以基于this做各种操作,函数组件不行。
  • 类组件内部可以定义并维护state,函数组件为无状态组件(可以通过hooks实现)
  • 性能优化:类组件依靠shouldComponentUpdate控制组件更新,函数依靠React.Memo控制组件更新

虚拟DOM

定义

虚拟 DOM: 本质上就是一个 JS 对象, 通过一个对象来描述了每个 DOM 节点的特征, 并且通过虚拟 DOM 就能够完整的绘制出对应真实的 DOM

// React 虚拟DOM节点
const vNode={
    key=null,
    type='div',//标签名或组件名
    props:{
        children:[//子元素们(这里表示div下有两个span)
            { type:'span', ... },
            { type:'span', ... }
        ],
        className:'red',//标签上的属性
        onclick:()=>{} // 事件
    },
    ref=null
}

原理

虚拟 DOM 的原理可以分为三个部分:创建虚拟 DOM、比较新旧虚拟 DOM、将差异更新到真实 DOM。

  • 创建虚拟 DOM:会将页面上的每一个元素都抽象成一个 JavaScript 对象,包含了这个元素的标签名、属性、样式和子节点等信息
  • 比较新旧虚拟 DOM:新旧虚拟 DOM 的比较时,会将新旧虚拟 DOM 树进行深度优先遍历,依次比较每一个节点,找出它们之间的差异。当找到一个节点有差异时,就将这个节点的差异记录下来,并继续比较它们的子节点。
    • 主要是diff算法的策略
      • 如果节点类型不同,直接替换整个节点;
      • 如果节点类型相同,但是节点属性不同,更新节点属性;
      • 如果节点类型和属性都相同,但是子节点有变化,递归进行比较子节点。
  • 将差异更新到真实 DOM:将差异更新到真实 DOM 时,会根据 Diff 算法生成的差异对象,依次对真实 DOM 进行操作。对于每一个需要更新的节点,都会先创建一个对应的新节点,然后根据差异对象的操作类型,进行相应的更新操作。

优缺点

  • 优点
    • 提高渲染性能:虚拟 DOM 可以减少 DOM 操作的次数,从而提高页面的渲染性能。
    • 跨平台:虚拟 DOM 不依赖于浏览器,可以运行在各种平台上,包括服务器端和客户端。
    • 提高开发效率:通过使用虚拟 DOM,开发者可以更加便捷地更新页面,并且不需要手动操作 DOM。
  • 缺点
    • 内存消耗:虚拟 DOM 需要额外的内存来存储虚拟 DOM 树和真实 DOM 树之间的关系,这可能会增加内存消耗。

state和props的区别

相同点

  • 两者都是JS对象
  • 两者都是用来保存信息
  • props和state都能触发渲染更新

不同点

  • props外部方式传递给组件,state组件内自己管理维护
  • props组件内不可修改,state组件内可修改
  • state是多变的,可以修改

super()和super(props)的区别

原因

React中不能直接修改state的值。必须通过调用setState来告知React数据发生变化。setState方法是从Component中继承的。

setState

setState发生了什么

  • 调用 setState 函数之后,React 会将传入的参数对象与组件当前状态合并,然后触发调和过程,经过调和过程,React 会以相对高效的方式,根据新的状态构建 React 元素树并重新渲染整个 UI 界面
  • React 得到元素树之后, React 会自动计算出新旧树节点的差异,根据差异对界面进行最小化重新渲染,React 的差异算法能相对精确得知发生改变的节点,保证按需更新

setstate的说明,第一个参数

  • 可以多次调用setState(),但是只会调用第一次重新渲染
    • 如果后面的一次this.setState()想基于第一个this.setState()的结果做操作,如何实现?

      • 使用setState((state,props)=>{})语法
      class App extends React.Component{
         state = {
           count:1
         }
         handleClick = () =>{
           this.setState((state,props)=>{
             return{ count:state.count + 1} // 返回的count的值为2
           })
           this.setState((state,props)=>{
           console.log("state的值:”,state) // 此时count的值为2
             return{ count:state.count + 1} // 返回的count的值为3
           })
         }
      }
      

setState()的第二个参数

  • 场景:在状态更新(页面完成重新渲染)后立即执行某个操作
  • 语法:setState(updater,[callback])
this.setState(
  (state.props)=>{} // 参数1
  ()=>{console.log('这个回调函数会在状态更新后立即执行')}  //参数2
)

setState是异步还是同步?

  • setState只在合成事件和构造函数中是异步的。
    • 异步中如果对同一个值进行多次setState,setState批量更新策略会对其进行覆盖,取最后一次的执行。
    • 如果是同时setState多个不同的值,在更新时会对其进行合并批量更新
  • 在原生事件和setTimeout中是同步的。

合成事件

React是基于浏览器的事件机制自身实现的一套事件机制,包括事件注册、事件合成、事件冒泡、事件派发等。在React中这套事件机制被称为合成事件。

原生事件、合成事件的执行顺序

  • React 所有事件都挂载在 document 对象上
  • 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件
  • 所以会先执行原生事件,然后处理 React 事件
  • 最后真正执行 document 上挂载的事件
*** 事件名称命名方式不同
// 原生事件绑定方式
<button onclick="handleClick()">按钮命名</button>   
// React 合成事件绑定方式
const button = <button onClick={handleClick}>按钮命名</button>
***  事件处理函数书写不同
// 原生事件 事件处理函数写法
<button onclick="handleClick()">按钮命名</button>     
// React 合成事件 事件处理函数写法  
const button = <button onClick={handleClick}>按钮命名</button>

React构建组件的方式有哪些

函数组件

function HelloComponent(props){
  return <div>{props.name}</div>
}

React.createClass

function HelloComponent(props){
  return React.createElement('div',null,'hello',props.name)
}

继承React.Component

class Timer extends React.Component{
}

Hook(钩子)

useState(用于在函数组件中添加状态管理)

const [number,setNumber] = useState(initialState)

useState通过传入一个初始值,返回一个数组,数组的第一项是设置的当前组件的值,第二项是修改这个值的方法。

import React,{useState} from 'react'
function Test(){
// 在后续的重新渲染中,useState返回的第一个值始终是更新后最新的state
 const [count,setCount] = useState(0) // 初始数据是基本数据类型
 const [tc,setTc] = useState({name:'tom',age:12})  // 定义的初始数据是引用类型,使用函数式更新结合扩展运算符
 const increment = ()=>{
   setCount(count + 1)  // 直接调用setState传入更新的数据
   // setState(()=>count + 1) // 对于复杂的更新逻辑使用函数式更新
 }
 const handleClick= ()=>{
   setTc(()=>{
   return {
     ...tc,
     name:'alan'
   }})
 }
 return(
  <div>  
      <p>{count}</p>
      <button onClick={increment}></button>
       <button onClick={handleClick}></button>
  </div>
 )
}
export default Test

useContext(跨级组件之间通信)

useContext 用于在函数组件中访问上下文(Context)的值。它接收一个上下文对象(通过 React.createContext 创建),并返回当前上下文的值。它使得在组件树中深层级的组件中能够方便地使用上下文数据。

useEffect(用于处理副作用和模拟类组件的生命周期方法)

使用场景

  • 数据获取、手动更改 DOM、设置订阅等

不同依赖数组的区别

import React, { useState, useEffect } from 'react';
function App(){
  const [rightCount, setRightCount] = useState(0);
// 第一种:没有添加依赖数组,它的触发时机有:
    -   组件挂载、卸载的时候
    -   页面每一次`re-render`的时候,即`leftCount``rightCount`更新的时候,也是左按钮和右按钮点击的时候

  useEffect(()=>{console.log('112121')})
  useEffect(()=>{console.log('112121')},[rightCount])
  useEffect(()=>{console.log('112121')},[])
}
// 

清除函数的作用

useEffect(()=>{
   cosnt timer = setInterval(()=>{
     console.log('2323232')
   },1000)
   return ()=>{
     cleatInterval(timer)
   }
},[])

useMemo(缓存、记忆计算结果)

useMemo仅在任何依赖项的值发生变化时才会重新计算

useCallback

只有当其中一个依赖项的值发生变化时,回调才会发生变化。

diff算法

diff算法定义

diff算法可以帮助我们计算出Virtual Dom中真正变化的部分,针对该部分进行实际的DOM操作,而非渲染整个页面,保证每次操作后页面的高效渲染。

diff策略

tree diff :Web UI中DOM节点跨层级的移动操作比较少,忽略不计 component diff:相同类的两个组件将会生成相似得树形结构,不同类的两个组件将会生成不同的树形结构 element diff:对于同一层级的一组子节点,可以通过唯一ID进行区分

tree diff(同层级比较)

  • React通过使用updateDepth对虚拟DOM树进行层次遍历
  • 两棵树对同一层级节点进行比较,只要该节点不存在,那么该节点与其所有子节点会被删除,不比较
  • 只需要遍历一次,便完成整个DOM树的比较

component diff(类型组件)

  • 同一类型的组件,按照原策略比较virtual DOM tree
  • 同类型组件,组件A转化为组件B,如果virtual DOM无变化,通过shouldComponentUpdate()方法 判断是否
  • 不同类型的组件,diff算法会把要改变的组件判断为dirty component,替换整个组件的所有节点

element diff

节点属于同一层级时,diff提供3种节点操作,插入,移动,删除

  • 插入:新的组件类型不在就集合中,全新的节点,需要对新节点进行插入操作
  • 移动:旧集合中有新组件类型,且element是可更新的类型,就需要移动
  • 删除:旧组件类型,在新集合里也有,但对应的element不同则不能直接复用和更新,需要删除/

HOC(Higher-Order Components,高阶组件)

定义

高阶组件是一个参数为组件,并且返回值为新组件的函数

*** 定义类组件返回
// 定义一个高阶组件 
// 1.高阶组件会接收一个组件作为参数 
function hoc(Cpn) { 
    class NewCpn extends PureComponent { 
       render(){
         return <Cpn/>
       }
    } 
    // 2.并且返回一个新的组件 
    return NewCpn 
}
*** 定义函数组件返回
// 定义一个高阶组件 
// 1.高阶组件会接收一个组件作为参数 
function hoc(Cpn) { 
    function NewCpn() { } 
    // 2.并且返回一个新的组件 
    return NewCpn 
}

实现高阶组件的方式

  • 属性代理
  • 反向继承

运用场景

  • props增强
  • 渲染判定鉴权
  • 日志打点、表单校验

组件之间传参方法

父子之间通信(props)

格式:

父组件---传入数据
function EmailInput(){
  return (
    <label>Email:<input value={props.email}></label>
  )
}
<子组件 自定义属性1={值1} 自定义属性={值2}。。。/>
const element = <EmailInput email="1213@163.com"/>
子组件 ---类组件---接收数据
// 接收数据: class 组件需要通过 this.props 来获取
class 子组件 extends Component {
   console.log('从父组件中传入的自定义属性被收集在对象':this.props)
   render(){return (<div>子组件的内容</div>)}
}

子传父

  • 父组件提供函数,传递给子组件
  • 子组件接收函数并且调用
//父组件提供函数,传递给子组件
class Parent extends React.Component {
  state:{
    num:11
  }
  fn = (num)=>{
    console.log('接收到子组件数据',num)
  }
  render(){
    return (
      <div>
        <Child fn = {this.fn}/>
      </div>
    )
  }
}
// 子组件接收函数并且调用
class Child extends React.Component {
  handleClick = ()=>{
    // 调用父组件传入的props,并传入参数
    this.props.fn(11)
  }
  return (
    <button onClick={this.handleClick}>按钮</button>
  )
}

兄弟之间通信

思路:状态提升,将共享状态提升到最近的公共组件中,由公共父组件管理这个状态

// Parent.js

任意组件之间通信 context

React-router的原理

React Router 路由的基础实现原理分为两种,

  • 如果是切换 Hash 的方式,那么依靠浏览器 Hash 变化即可;
  • 如果是切换网址中的 Path,就要用到 HTML5 History API 中的 pushState、replaceState 等。在使用这个方式时,还需要在服务端完成 historyApiFallback 配置。

在 React Router 内部主要依靠 history 库完成,这是由 React Router 自己封装的库,为了实现跨平台运行的特性,内部提供两套基础 history,一套是直接使用浏览器的 History API,用于支持 react-router-dom;另一套是基于内存实现的版本,这是自己做的一个数组,用于支持 react-router-native。

React Router 的工作方式可以分为设计模式与关键模块两个部分。从设计模式的角度出发,在架构上通过 Monorepo 进行库的管理。Monorepo 具有团队间透明、迭代便利的优点。其次在整体的数据通信上使用了 Context API 完成上下文传递。 在关键模块上,主要分为三类组件:第一类是 Context 容器,比如 Router 与 MemoryRouter;第二类是消费者组件,用以匹配路由,主要有 Route、Redirect、Switch 等;第三类是与平台关联的功能组件,比如 Link、NavLink、DeepLinking 等。

redux

定义

Redux 是一个独立的 JS 状态管理库。提供可预测化的状态管理。

Redux 核心概念及工作流程:

Store:存储状态的容器,JS 对象

View:视图,HTML 页面

Actions:对象,描述对状态进行怎样的操作

Reducer:函数,操作状态并返回新的状态

Redux 核心 API

1:创建 Store 状态容器

const store = Redux.createStore(reducer)

2:创建用于处理状态的 reducer

function reducer(state = onotoalState,action){}

3:获取状态

store.getState()

4:订阅状态

store.subscribe(function(){})

5:触发 action

store.dispatch({type:'description...'})

React状态管理有哪些?

Flux

Mobx

Mobx实现观察者模式(发布-订阅模式) Mobx 提供了类似observable和的装饰器computed来定义可观察的状态和反应函数。用action修饰的动作用于修改状态,确保跟踪所有更改。

Mobx三个概念

  • State(状态):能被Mobx追踪到的响应式状态数据,状态发生变化时,相关的依赖就会自动更新
  • Conputed(计算属性):只有在其依赖的状态发生变化时,计算属性才会重新计算
  • Action:用来改变状态

如何访问Mobx状态下的变量?

使用装饰器 observable 将变量定义为可观察来访问状态中的变量。

import { observable, computed } from 'mobx';

class MyStore {
  @observable myVariable = 'Hello Mobx';

  @computed get capitalizedVariable() {
    return this.myVariable.toUpperCase();
  }
}

const store = new MyStore();
console.log(store.capitalizedVariable); // Output: HELLO MOBX

store.myVariable = 'Hi Mobx';
console.log(store.capitalizedVariable); // Output: HI MOBX

Redux 工作流程

1:组件通过 dispatch 方法触发 Action

2:Store 接收 Action 并将 Action 分发给 Reducer

3:Reducer 根据 Action 类型对状态进行更改并将更改后的状态返回给 Store

4:组件订阅 Store 中的状态,Store 中的状态更新会同步到组件

Redux和Mobx有什么区别?

  • Redux 是一种更简单、更有主见的状态管理库,它遵循严格的单向数据流,并提倡不变性。它需要更多的模板代码和显式更新,但与 React 的集成度很高。

  • Mobx 提供的 API 更灵活、更直观,模板代码更少。它允许你直接修改状态,并自动跟踪变化以获得更好的性能。在 Redux 和 Mobx 之间做出选择取决于您的具体需求和偏好。

class组件中,事件中的this为啥是undefined

标准函数中,this引用的是把函数当成方法调用的上下文对象。 箭头函数中,this引用的是定义箭头函数的上下文。 bind()方法会创建一个新的函数实例,this值会被绑定到传给bind()的对象。apply()和call()都会以指定的this调用函数.

class Test extends React.Component{
  constructor(props) {
   super(props)
   this.state = {count:0}
  }
  handleClick = ()=>{
   console.log('箭头函数this',this)// 有内容,不为undefined
  }
  handle(){
   console.log('标准函数 this',this)  // undefined
  }
  render(){
   return (
    <div><button onClick={this.handleClick}>箭头函数</button>
    <button onClick={this.handle}>标准函数</button>
    </div>
   )
  }
}