React面试题纪实

416 阅读15分钟

react更新机制:

  • 是根据this.setState()的调用 进行更新视图(只要一直调用setSatte(),就会更新视图)

setState

  • setState 是类组件当中 修改状态的唯一方式
  • 状态是可以直接修改的,只是!!!视图并不会更新
  • setState到底是同步还是异步(面试)?
  • setState是异步的,但是在 原生JS事件定时器 当中它的表现是同步的
  • 什么是原生JS事件:得到原生节点,通过这个原生节点绑定的事件;
  • 工作当中,有时候我们会通过setTimeout使得setState表现为同步执行代码,方便我们想进行状态修改之后的操作场景:
 setTimeout(() => {
    this.setState({ ct: this.state.ct + 1 })
    console.log(this.state.ct);
    this.setState({ ct: this.state.ct + 1 })
    console.log(this.state.ct);
    this.setState({ ct: this.state.ct + 1 })
    console.log(this.state.ct);
    }, 0)
  • 或者是通过设置setState的第二个参数,第二个参数是在setState完成时进行的回调函数(优先推荐)
 this.setState({ ct: this.state.ct + 1 }, () => {
    console.log('this.state.ct', this.state.ct);
 })

合成事件

  • react jsx模板当中 on+事件名绑定的事件 都是 合成事件!
  • 主要就是通过 事件代理的方式 对于所有的事件进行统一的管理。
  • 1 利用事件代理 可以减少节点事件的内存占用,对于cpu和内存消耗都是最优的方案。
  • 2 事件统一管理之后,react可以单独的提供一个react整合了之后的Event事件对象。 抹平不同的浏览器的差异!!!

不同的React版本 事件触发器绑定的父节点不一样

  • React 16及以下版本 事件都是绑定在 html节点上
  • React 17 开始 事件绑定在 React应用的根节点上

高阶组件(HOC)

高阶组件(High-Order Component)接受 React 组件作为输入,输出一个新的 React 组件。

  • 高阶组件不是组件,是增强函数,可以输入一个元组件,输出一个新的增强组件
  • 高阶组件的主要作用是代码复用,操作状态和参数
  • 简单来说:高阶组件就是一个函数,传入一个React组件,返回一个新的组件

实现高阶组件的方法:

  • 属性代理(props proxy):高阶组件通过被包裹的 React 组件来操作 props。
  • 反向继承(inheritance inversion):高阶组件继承于被包裹的 React 组件。
export const hc = (Cp) => {
    //高阶组件是函数不能使用hook
    return (props) => {
        //返回一个组件 才能调用hook
        const location = useLocation()
        const params = useParams()
        //{...props} 让原来组件保持原有的属性
        return (
            <div>
                <Cp location={location} params={params} {...props}/>
            </div>
        )
    }
}

渲染属性(Render Props)

  • 通过Props 传入一个 函数/(函数组件) ,这个函数返回的是jsx模板内容。所以在组件内部可以通过执行这个函数来渲染一些指定的内容,并且可以把一些复用的逻辑参数传递给这个函数使用。
class Mouse extends React.Component {

          state = { x: 0, y: 0 };

          handleMouseMove = (event) => {
            this.setState({
              x: event.clientX,
              y: event.clientY,
            });
          };

          render() {
            return (
              <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
                <div> props a:  { this.props.a }</div>
                <div> props b:  { this.props.b }</div>
                <div> props c: { this.props.c() } </div>
                <div> props redner: { this.props.render(this.state) }</div>
              </div>
            );
          }
        }
<Mouse
    a="1" 
    b={2} 
    c={ ()=>123}
    render={ (args)=>{ return <div> {JSON.stringify(args) } </div>}}>
 </Mouse>

高阶组件和渲染属性的区别:在于高阶组件可以传入类组件/函数组件,而渲染属性只支持传入一个函数组件,其他的都基本类似。

portals

  • 提供了一种很好的将子节点渲染到父组件以外的 DOM 节点的方式。
  • 最典型的应用场景:当父组件具有 overflow: hidden 或者 z-index 的样式设置时,组件有可能被其他元素遮挡,这个时候你就可以考虑是否需要使用 Portal 使组件的挂载 脱离父组件
  • 一般而言,组件在装载的时候会就近装载在该组件最近的父元素下,而现在你可以使用 Portal 将组件渲染到任意一个 已存在 的 DOM 元素下,这个 DOM 元素并不一定必须是组件的父组件
ReactDOM.createPortal(child, container);
  • 模态框案例
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- react 的核心框架库 -->
    <script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/react/16.14.0/umd/react.development.js"
        type="application/javascript"></script>
    <!-- react-dom react的web的操作类库 -->
    <script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/react-dom/16.14.0/umd/react-dom.development.js"
        type="application/javascript"></script>
    <!-- babel 解决语法兼容性的类库  -->
    <script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/babel-standalone/7.0.0-alpha.20/babel.min.js"
        type="application/javascript"></script>
    <script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/axios/0.26.0/axios.min.js"
        type="application/javascript"></script>
</head>

<body>

    <div id="app"></div>
    <div id="t" ></div>


    <script type="text/babel">


        class Modal extends React.Component {

            constructor(props) {
                super(props);
            }

            render() {
                //  通过 createPortal方法我们可以 自主的决定我们的内容挂载在任意节点
                // 而不是仅限制在 我们React的应用跟节点当中
                return ReactDOM.createPortal(
                    <div className='modal'>我是模态框</div>,  // 渲染的内容
                    document.querySelector("#t")
                )
            }
        }

        const { useState } = React;
        const App = () => {

            const [showModal, setShowModal] = useState(true)

            return (
                <div>
                    <button onClick={() => { setShowModal(!showModal) }}>切换模态框的显示</button>
                    <div>
                        {
                            showModal && <Modal ></Modal>
                        }
                    </div>
                </div>
            )
        }


        ReactDOM.render(<App></App>, document.getElementById('app'))

    </script>

    <style>
        #app {
            overflow: hidden;
            height: 300px;
            transform: translate(0, 0);
        }

        .modal {
            width: 400px;
            height: 251px;
            border: 3px solid #ccc;
            box-shadow: 3px 3px 3px rgba(0, 0, 0, .5);
            position: fixed;
            left: 50%;
            top: 30%;
            transform: translateX(-50%);
            background: #fff;
        }
    </style>
</body>

</html>

错误边界

  • 错误边界:是一种 React 组件,这种组件可以捕获并打印发生在其 子组件树 任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

  • 注意:错误边界无法捕获以下场景中产生的错误:

  • 事件处理(了解更多

  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)

  • 服务端渲染

  • 它自身抛出来的错误(并非它的子组件)这个很重要

<script type="text/babel">
        
        const { useState } = React;

        class ErrorBoundary extends React.Component {
            constructor(props) {
                super(props);
                this.state = { hasError: false };
            }
            //static getDerivedStateFromProps类似,返回的结果都能合并state
            // getDerivedStateFromError 在子代组件发生错误的时候执行
            static getDerivedStateFromError(error) {
                // 更新 state 使下一次渲染能够显示降级后的 UI
                // 返回的对象会合并到当前的state(从而实现更新state)
                return { hasError: true };
            }

            componentDidCatch(err){
                //  一般我们会在这个地方把错误信息 发送给服务器进行记录以便后期排查原因
                console.log('子组件有错误的时候会触发',err)
            }
         

            render() {
                if (this.state.hasError) {
                    // 你可以自定义降级后的 UI 并渲染
                    return <h1>内部故障,请与管理员联系</h1>;
                }

                return this.props.children; 
            }
        }

        const Cp1 = ()=>{
            const [a,setA] = useState('abc')

            return (
                <div> 
                    <div> 我是Cp1组件</div>
                    <div> a : {a}</div>
                    <button onClick={()=> setA({a:1,b:2,c:3}) }>修改a的值</button>
                </div>
            )
        }
 

        const App = ()=>{

            return (
                <div>
                    <ErrorBoundary>
                     <Cp1></Cp1>
                    </ErrorBoundary>
                 </div>   
            )
        }
        ReactDOM.render( <App></App> ,  document.getElementById('app')  )

    </script>

Component 对比PureComponent(纯组件)@性能优化

  • PureComponet 很多地方跟Component 类似,他们的主要区别是 PureComponent会对props和state内部去实现在shouldCompnentd的状态的第一层的比较(浅层比较,浅比较),只有发生了变化才允许视图更新。 而Component只要是调用了setState 无论状态是否发生变化都会执行视图的更新(因为SCU默认是返回true)。

  • 所以 PureComponent 某些情况下会更加有利于节省性能,这也是React项目的主要优化手段之一。一般我们会在一些纯展示型的页面当中使用PureComponent。

  • 在PureComponent组件当中,如果是对引用数据类型进行操作,但是未引起内存地址发生变化,是无法监听到的,但是可以借助拓展运算符,因为其会返回一个新的对象/数组即一个新的内存地址

this.setState({
    // userInfo:this.state.userInfo, // userInfo内存地址没发生实际的变化
    userInfo:{...this.state.userInfo}, // 等于一个新的对象
}) 

React.Fragment幽灵标签

  • 返回多个元素或组件时候,Fragments 可以让你聚合一个子元素列表,并且不在 DOM 中增加额外节点。
  • 类似Vue当中的<template/>

React Hook

@ useState

  • 函数组件当中可以使用useState来创建一个状态和修改状态的方法,传入的值就是状态的初始值

@ useEffect

  • 作用一:模拟了生命周期

  • useEffect 在挂载的时候都会执行一次 模拟了 类组件的 ComponentDidMountVue mounted

  • useEffect 如果没有设置第二个参数的时候 模拟了 类组件的 ComponentDidUpdateVue updated

  • useEffect的函数当中返回一个函数 这个函数叫 “清除函数” 清除函数会在组件卸载的时候执行 模拟了 类组件的 ComponentWillUnmount 和 Vue beforeDestroy

  • 作用二:实现了对于状态的变化的监听

  • useEffect( 执行函数,[ 状态1, 状态2,...]) 可以在第二个参数当中设置要监听的状态, 当指定的状态发生变化了就会触发 执行函数。

  • react useEffect(()=>{ },[状态]) <=> vue watch(状态,执行函数) <=> vue wacthEffect()

@ useContext

  • 在函数组件当中通过传入一个上下文对象 来获取上下文对象供应的数据

@ useMemo 性能优化的一点

  • 使用useMemo 可以缓存一个函数返回的结果,并且可以执行依赖项,只有依赖项指定的状态发生变化了才会去重新更新缓存的值。( vue 当中的 计算属性)

@ useCallback 性能优化的一点

  • useCallback缓存一个函数,跟useEffect和useMemo一样可以设置依赖项。某种意义上 useCallback是useMemo缓存一个函数的语法糖。 所以这也是React项目当中性能优化的手段之一。

@ useReducer

  • useReducer 可以设置一个 迭代函数 以及 设置一个初始值来创建状态 和一个分发函数, 通过触发分发函数来让迭代函数再次执行 ,迭代函数会得到当中的状态以及分发函数传入的参数,最终迭代函数返回的值会更新状态。
  • 这是除了 useState以外 一个独立的管理状态的方式。

@ useRef

  • useRef 是用在函数组件当中创建ref对象的方法。 创建的ref用在html节点或者是函数组件上可以快速的方便通过ref对象的current属性来得到对应的html节点对象或者是组件实例。

ref 创建引用对象

  • 类组件创建ref对象
constructor(props){
  super(props);
  // this.myh1 是通过 React.createRef来创建的一个引用对象(ref对象)
  this.myh1 = React.createRef(null);  
  this.mycp2 = React.createRef(null);
 }
  • 通过ref对象的current属性,能找到html节点或组件实例
//绑定ref对象
{/* 一个html节点 绑定了ref属性等于一个 ref对象的话 通过这个ref对象的current属性可以访问这个节点对象 */}
<h1 id='kfc' ref={this.myh1}>我是cp1组件</h1>

{/* 一个类组件 绑定了ref属性等于一个 ref对象的话 通过这个ref对象的current属性可以访问这个组件实例 */}
<Cp2 ref={ this.mycp2 }></Cp2>

//获取ref对象的current属性,拿到html节点/组件实例
fnByRef = () =>{ 
 console.log('ref获取节点', this.myh1.current )
}

fnByRefCp = () =>{ 
 console.log('ref获取组件实例', this.mycp2.current )
}

上下文对象 context

  • context是react提供的一种跨组件通讯的方式,在某个父组件开始可以向它的所有子代组件进行数据的传递。

  • 这也是react当中组件通讯的主要方式之一。

  • 创建一个上下文对象

const Myctx = React.createContext(默认值)
  • 如何供应
  • ps:实际项目当中我们供应的值一般都是供应状态和修改状态修改的方法
<上下文对象.Provider value={这里就是要供应的内容}>

    children子组件

</上下文对象.Provider>
  • 子代组件当中如何获取供应的数据
  • @类组件获取方式
//类组件 可以通过给类设置一个静态属性 contextType = 上下文对象
//组件内
static contextType = 上下文对象
//组件外
类组件.contextType = 上下文对象;

// 类组件内部就可以通过 this.context来访问 供应的数据
  • @函数组件获取方式
// 函数组件通过 useContext来得到供应的值
const ctx = useContext(上下文对象)

// 类组件内部就可以通过 ctx 来访问供应的数据
  • @createContext()传入的默认值使用问题

  • 默认值的生效与否:取决于<上下文对象.Provider>标签是否存在,不存在则使用默认值;与注入的value存在与否无关;也就是有<上下文对象.Provider>标签,没有value,代表没有注入值,不会使用默认值

  • @两种组件通用方法

//使用上下文对象的Consumer组件来获取
<上下文对象.Consumer>
    { 
        value=>(
            // value 就是 上下文供应的值
            子组件的内容
    	)
    }
</上下文对象.Consumer>

react生命周期大全

image.png

  • 更新时候状态以及生命周期参数对比
【数据更新阶段】 关于状态以及生命周期的传参(props,state):
   render之前          this.state还是初始状态 生命周期的传参的是更新之后的props/state
   render函数执行时    this.state已经更新完毕 
   render之后          生命周期的传参的是更新之前的props/state
  • 类组件
constructor(props) {
    super(props)
    console.log('父组件传入的参数 如果组件首尾有内容传入的属性多加一个children',props);
    //创建实例阶段之前 beforeCreate
    
    this.state = {
         ct: 1,
         status: '',
         chatList: [
         { id: 1, from: "xiaom", text: '我是聊天内容' }
           ]
       }
    //实例创建完毕 created
}


static getDerivedStateFromProps(props, state) {
      //触发时期:组件挂载阶段先执行一次,在render之前; 状态更新时 也会执行一次,在shouldComponentUpdate之前
      //在render之前调用的生命周期的入参是 更新的props值 更新的状态(非一挂载时,一挂载时,是最原始的props,state)
      //作用:返回的对象会合并到我们的state,props
          }

//触发时期:render处于更新阶段时 在render之前被触发
//在render之前调用的生命周期的入参是 更新的props值 更新的状态
//作用:组件视图能否更新 SCU  阻止的是视图的更新 并不能阻止数据的更新
//而react的响应机制就是 只要调用this.setState() 我就触发更新
//在此生命周期 我们可以进行判断 根据原先的this.props/this.state 与更新之后的nextProps, nextState进行比较 发生变化再进行视图更新操作
//SCU 被认为是react性能优化的一个手段
 shouldComponentUpdate(nextProps, nextState) 
 //SCU最终可以根据返回的布尔值来决定是否(false不更新视图 true更新视图) 更新视图,并不能阻止数据的更新
  
  if(state.ct>5) return true
  return false
}

render() {
   //触发时期:组件挂载之前/更新之前 beforeMount beforeUpdate
   return (
          <div>
              我是组件1
           </div>
           )
}  

//触发时期:组件挂载完成之后 mounted
componentDidMount() {
                console.log('componentDidMount()时 组件挂载完成之后 mounted');
            }

//在render之后调用的生命周期的入参是 原先的props值 原先的状态
//触发时期:render()之后 虚拟节点更新到视图之前
//getSnapshotBeforeUpdate的目的就是实际更新之前来记录当中没更新到真实节点的时候网页当中一些元素的根节点位置
//这个生命周期Return的值 就会给到componentDidUpdate()当中的第三个参数
getSnapshotBeforeUpdate(prevProps, prevState) {
     return xxx
}


/在render之后调用的生命周期的入参是 (原先的props值 原先的状态 snapshot)
//snapshot 就是getSnapshotBeforeUpate的返回值
//触发时期:组件更新之后 updated
componentDidUpdate(prevProps, prevState, snapShot) {
    console.log('组件更新之后 updated')     
}

//卸载之前*** 清除定时器 清除事件监听器
//触发时期:组件卸载之前 beforeUnmount
 componentWillUnmount() {
      console.log('componentWillUnmount()时 组件卸载之前 beforeUnmount');
            }

受控组件

  • 在react 表单元素的双向绑定 不叫作双向绑定 而是称为受控组件
  • 原理同Vue的v-model一样:
- v-model本质上是语法糖,v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件
-   text 和 textarea 元素使用 value 属性和 input 事件
-   checkbox 和 radio 使用 checked 属性和 change 事件
-   select 字段将 value 作为 prop 并将 change 作为事件
<input 
    type='text' 
    value={this.state.value} 
    onChange={(e) => this.setState({ value: e.target.value })} 
/>

组件通信 类组件

  • props
  • 面试雷区:当组件当中存在内容时,通过this.props可以得到传入的属性+children属性
  • 注入props
class Cp1 extends React.Component {
  constructor(props) {
   super(props)
   this.state = {
       val: ''
   }
   }
   fn1 = (res) => {
       console.log('我是cp1里面的fn1', res)
       this.setState({val:res})
   }
   render() {
      return (
          /*<Cp2 a='1' b={2}> </Cp2>//这里可以得到三个属性 包含子内容children */
        <div>
          <div>我是组件1</div >
          <hr />
          <h1>组件2传递出来的参数:{this.state.val}</h1>
              <Cp2 a='1' b={2} c={this.fn1} > </Cp2>
        </div>
     )
  }
}
  • 获取props
constructor(props) {
   super(props);
   //组件内部 可以通过this.props来获取自定义组件传递的自定义属性以及子内容(组件头尾之间的内容)
  console.log('props', this.props);
  this.state = {
      value: ""
   }
}
  • emits
  • 父组件props传递一个函数给子组件,子组件内部触发该函数时,传递给父组件并且传入参数

模板语法

        //1. jsx是一个表达式
        const ct = <h1>666</h1>

        //2. jsx里面可以 使用一对花括号引入一个表达式
        const ct1 = <h1>{15 * 1}</h1>

        //3. jsx 属性的绑定可以直接按照 { } 引入表达式来理解
        // const ct2 = <h1 class={['a', 'b'].join(" ")}>1223</h1>
        //0. 类的绑定 应该使用className 来绑定 可以使用但是会报错
        const ct2 = <h1 className={['a', 'b'].join(" ")}>1223</h1>

        //4. jsx style属性的绑定等于一个对象 {{}} 外层表示引入表达式 内层表示style样式
        const ct3 = <h1 style={{ fontSize: '25px', color: 'red' }}>style</h1>

        //5. jsx只能有一个根节点
        const ct4 = <div> <div>11</div><div>22</div> </div>

        //6 jsx 子内容不能是一个对象
        /* 报错内容:Objects are not valid as a React child (found: object with keys {name}). If you meant to render a collection of children, use an array instead. */
        const ct5 = <div>{{ name: "ss" }}</div> //xxx
        const ct6 = <div>{[0, 1]}</div> //01

        //7 如果表达式的值是 布尔值 undefined null 不渲染任何内容
        const showIt = true
        const ct8 = <h1>{showIt}</h1>

        //8 条件渲染
        //A B切换
        const show = true
        const ct7 = <div>{show ? <h1>111</h1> : <h6>111</h6>}</div>
        //是否渲染
        const show2 = true
        const ct9 = <div><h1>{show2 && <h1>2552555</h1>}</h1><span>00000</span></div>
        //是否渲染
        // const ct90 = <div>{show ? <h1>111</h1> : null}</div>
        //&&得到的一定是左右两边的其中一个值
        //左边的布尔值为true时,直接返回右边的值
        //左边的布尔值为false时,直接返回左边的值

        //9 列表渲染 如果花括号引入的是一个数组 数组的内容会依次出现
        const list = [1, 2, 23]
        const list1 = [
            <div>标题1</div>,
            <div>标题2</div>,
            <div>标题3</div>,
        ]
        const newList = [
            { id: 1, title: "我是标题1" },
            { id: 2, title: "我是标题2" },
            { id: 3, title: "我是标题3" },
        ]
        // const ct10 = <h1>{list}</h1>
        // const ct10 = <h1>{list1}</h1>
        const ct10 = <h1>{newList.map((item, index) => <div key={item.id}>{index} - {item.title}</div>)}</h1>

        //10 多行显示 建议使用圆括号包裹 ()
        //虽然新的babel工具当中允许我们不用 () 也可以多行显示
        //使用() 能让代码可读性更高
        const ct11 = (
            <div>
                <div>标题1</div>
                <div>标题2</div>
                <div>标题3</div>
            </div>
        )

        //11 react 使用 on事件名 (驼峰写法 onClick) 来绑定事件 等于的值就只能是一个函数
        //onClick就等于事件名
        const fn1 = (e) => {
            //react触发函数的第一个参数就是 event对象
            console.log('fn1', e);
        }
        const ct12 = <div onClick={fn1}>默认参数</div>
        //自定义函数
        const fn2 = (id, e) => {
            //react触发函数的第一个参数就是 event对象
            console.log('fn2', id, e);
        }
        //react触发函数的第一个参数就是 event对象
        const ct13 = <div onClick={(e) => { fn2(5, e) }}>自由传参数</div>

组件的开发

  • 类组件
  • react类组件当中,普通函数的this指向undefined,箭头函数this指向组件实例,对于普通函数,我们需要在需要在constructor通过bind的操作,改变this指向,返回一个绑定了新this的函数
constructor(props) {
  super(props);
  this.state = {
       ct: 10,
  }
 //对于 function形式声明 需要在constructor通过bind 的操作***
  this.fn1 = this.fn1.bind(this) 
  //返回一个新的函数 并指定了函数内部的this的指向
}
class Cp1 extends React.Component {
            constructor(props) {
                super(props);
                // 类组的状态声明的位置
                this.state = {
                    ct: 10,
                    list: [],
                    curIndex: 1
                }
                //***对于 function形式声明 需要在constructor通过bind 的操作***
                this.fn1 = this.fn1.bind(this) //返回一个新的函数 并指定了函数内部的this的指向
            }
            //新label 可以不书写 constructor 直接在类的内部声明state
            // state = {
            //     ct: 10,
            //     list: [],
            //     curIndex: 1
            // }
            //组件内部访问方式不问

            // fn1=function(){} 在react当中 function声明的函数 this默认指向undefined 不指向组件实例自身
            //为了拿到组件实例 需要在constructor通过bind 的操作 返回一个改变了this的新函数
            fn1() {
                console.log('fn1', this);
            }
            fn2 = () => {
                console.log('fn2', this);
            }
            //箭头函数 this指向组件实例自身

            render() {
                //render返回的内容就是 组件要渲染的内容
                return (
                    <div>
                        我是组件1
                    </div >

                )
            }
        }
  • 函数组件
const Cp2 = () => {
   //函数返回的内容就是 组件要渲染的内容
   return (
      <div>我是组件2</div>
      )
}

自定义hook

// 自定义hook 通常约定以 use开头
// 自定义hook就是 一个函数 返回一些状态和修改状态的方法
// 一般我们使用自定义hook的目的就是为了复用逻辑