前端面试题 三

64 阅读8分钟

一,常用的hook有哪些:

useEffect, useState, useCallback, useMemo, useRef, useContext, useReducer

二,useCallback和useMemo的区别:

useCallback和useMemo都可以做性能优化,做缓存,区别是:

  1. useCallback允许在多次渲染中缓存函数,而useMemo在每次重新渲染时缓存的则是计算的结果
  2. useCallback常和memo一起使用,用来减少不必要的渲染

三,useRef如何用,或在什么条件下会使用

useRef能帮助引用一个不需要渲染的值: const ref = useRef(initialValue)

initialValue:ref对象的current属性的初始值。可以是任意类型的值,这个参数在首次渲染后被忽略。

  • 使用用ref引用一个值
  • 通过ref操作DOM
  • 避免重复创建ref的内容

在组件顶层调用useRef以声明一个ref。

useRef返回一个只有一个属性的对象:

  • current: 初始值为传递的initialValue。之后可以将其设置为其他值。如果将ref对象作为一个JSX节点的ref属性传递给React, React将为它设置current属性。
  • 在后续的渲染中,useRef将返回同一个对象。

Refs是一种脱围机制,包括管理焦点、滚动位置或调用React未暴露的浏览器API.

四、React 18 setState自动批处理

批处理是指为了获得更好的性能,在数据层,将多个状态更新批量处理,合并成一次更新(在视图层,将多个渲染合并成一次渲染)

  1. React 18之前,我们只在React事件处理函数中进行批处理更新。默认情况下,在promise, setTimeout, 原生事件处理函数中, 或任何其它事件内的更新都不会进行批处理:

  2. 在React 18中,上面的例子只会有一次render,因为所有的更新都将自动批处理。这样无疑是很好的提高了应用的整体性能。

    批处理是一个破坏性改动,如果你想退出批量更新,可以使用flushSync。

    注意:flushSync函数内部的多个setState仍然为批量更新,这样可以精准控制那些不需要的批量更新。

五、如何避免函数重复执行

  • useCallback将函数进行缓存 ??

防止接口重复调用

  • 防抖
    • 当用户连续多次点击同一按钮,最后一次点击过后一段时间再去请求接口
    • 每次调用方法后产生一个定时器,定时器结束后再发请求。如果重复调用方法,就取消当前定时器,创建一个新的定时器,等结束后再发请求
    • 可以用封装好的第三方工具函数,如lodash的debounce方法来简化代码

六、es6中set和map:

  • set:
    • 类似于数组,但成员的值都是唯一的,没有重复的值,我们一般称为集合。
    • Set本身是一个构造函数,用来生成Set数据结构: const s = new Set();
    • Set的实例关于增删改查的方法: add(), delete(), has(), clear()
    • Set实例遍历的方法:
      • keys(): 返回键名的遍历器
      • values(): 返回键值的遍历器
      • entries(): 返回键值对的遍历器
      • forEach(): 使用回调函数遍历每个成员
      扩展运算符和Set结构相结合实现数组或字符串去重,加filter实现并集、交集和差集
  • map:
    • Map类型是键值对的有序列表,而键和值都可以是任意类型
    • Map本身是一个构造函数,用来生成Map数据结构:const m = new Map();
    • Map结构的实例针对增删改查的方法: size属性, set(), get(), has(), delete(), clear()
    • Map结构原生提供三个遍历器生成函数和一个遍历方法:
      • keys(): 返回键名的遍历器
      • values(): 返回键值的遍历器
      • entries(): 返回所有成员的遍历器
      • forEach(): 遍历Map的所有成员

七、promise及promise.all自己实现

Promise:异步编程的一种解决方案

优点:链式操作降低了编码难度 & 代码可读性明显增强

  • promise对象仅有三种状态:

    • pending (进行中)
    • fulfilled (已成功)
    • rejected (已失败)
  • 特点:

    • 对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态
    • 一旦状态改变(从pending变为fulfilled和从pending变为rejected),就不会再变,任何时候都可以得到这个结果
  • 用法:

    • Promise对象是一个构造函数,用来生成Promise实例
    • const promise = new Promise(function(resolve, reject) {});
    • Promise构造函数接受一个函数作为参数,改函数的两个参数分别是resove和reject
    • Promise构造出来的实例存在以下方法:
      • then()
      • catch()
      • finally()
  • then是实例状态发生改变时的回调函数,第一个参数是resolved状态的回调函数,第二个参数书rejected状态的回调函数

  • then方法返回的是一个新的Promise实例,也就是promise能链式书写的原因

getJSON('/posts.json').then(function(json){
    return json.post
}).then(function(post){
    // ...
});
  • Promise构造函数存在以下方法:

    • all()
    • race()
    • allSettled()
    • resolve()
    • reject()
    • try()
  • Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例

    • const p = Promise.all([p1, p2, p3]);
    • 接受一个数组(迭代对象)作为参数,数组成员都应为Promise实例
    const all = (arr) => {
     const list = []
     let total = 0;
    
     rettun new Promise((resolve, rejuct) => {
    
         for (let i = 0; i < arr.length; i++) {
    
             arr[i]
                 .then(res => {
                     list[i] = res
                     total += 1
                     if (total === arr.length) {
                         resolve(list)
                     }
                 })
                 .catch(err => {
                     rejuct(err)
                 })
         }
     })
     }
    
    

八、函数柯里化

将一个接受多个参数的函数转化为一系列链式函数调用,每个函数接受原函数的一个参数,并返回一个新的参数,直到所有参数都被接收为止

  • 在js中,函数可以像变量一样被赋值给变量,作为参数传递给其他函数,或者从函数中返回,这是实现柯里化的基础
  • 闭包允许一个函数访问并操作其外部作用域中的变量。在柯里化中,每次返回的新函数都会形成一个闭包,保持对外部函数中变量的引用,以便在后续调用中继续使用
  • 在箭头函数中使用...args可以捕获所有传入的参数作为一个数组,这使得函数可以接受一个任意数量的参数,非常适用于实现柯里化求和这样的功能

在实际应用中,它可以用于创建更加灵活和重用的函数。

function add(x) {
    return function(y) {
        console.log(x+y)
    }
}
const sum = add(4)
console.log(sum(8)) // 12

九、为什么hook不能写在嵌套或循环语句中?

  • React依赖于Hooks的调用顺序来管理状态和生命周期。如果违反这条规则,会导致Hooks的调用顺序不一致,从而引发难以调试的Bug.

  • React内部使用一个“调用顺序链表”来跟踪和管理hooks。每次组件渲染时,React都会按照Hooks的调用顺序来更新链表中的值。

  • 在第一次渲染时,React会记录Hooks的调用顺序;

    • 在后续渲染中,React会严格按照这个顺序来匹配Hooks的状态和副作用。

    • 如果在循环、条件或嵌套中调用Hooks,会导致Hooks的调用顺序不一致,从而破坏React对Hooks的管理。

    • 因为hook的执行机制是从上到下顺序执行,放到一个任务队列中去,如果写在嵌套或循环中,那么会造成这个任务内部的混乱

    • 加入react中加入key值,那么就可以写进嵌套或循环中

    将hooks放在嵌套或循环语句中会导致每次条件改变或循环迭代的时候,hooks都可能被重新创建,这会导致状态不稳定、内存泄露等问题。

  • 如何避免这些问题?

  1. 始终在函数组件的顶层调用Hooks:
    • 不要在循环、条件或嵌套函数中调用Hooks。
    • 确保每次渲染时Hooks的调用顺序完全相同。
  2. 将条件逻辑转移到Hooks内部:
    • 如果需要在某些条件下使用Hooks,可以将条件逻辑转移到Hooks内部。
  • 总结:
    • React 依赖于 Hooks 的调用顺序来管理状态和副作用。
    • 在循环、条件或嵌套函数中调用 Hooks 会导致调用顺序不一致,从而引发 bug。
    • 始终在函数组件的顶层调用 Hooks,并将条件逻辑移到 Hooks 内部。
    • 使用 ESLint 插件可以帮助检测和避免违规行为。

十、Fiber架构

  • 为每个任务增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新,注意是重新执行优先级低的任务;

  • 增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执行

  • dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个对列,这都是为找到被中断的任务,重新执行

  • 从架构角度看,Fiber是对React核心算法(即调和过程)的重写

  • 从编码角度看,Fiber是React内部所定义的一种数据结构,它是Fiber树结构的节点单位,也就是React 16新架构下的虚拟DOM

  • 一个fiber就是一个js对象,包含了元素的信息,该元素的更新操作对列、类型

  • React Fiber是React 16提出的一种更新机制,使用链表取代了树,将虚拟dom连接,使得组件更新的流程可以被中断恢复;它把组件渲染的工作分片,到时会主动让出渲染主线程