跳到主要内容

面试题积累

useEffect注意事项

  • 如果依赖项是一个对象中的属性,应该直接使用对象的属性而不是对象本身

  • 在 useEffect 中使用 setState(newState) 并依赖 state,内部执行会进入死循环。应该使用 setState(pre => newState) 来更新即应该使用更新函数而不是状态变量本身

  • 在开发环境中,通常会使用热更新的模式进行开发,保证每次代码变动后,页面会及时更新,这就可能导致 useEffect 中的一些副作用(例如计时器)会被重复执行,所以清理函数是必要的。清理函数会在组件 rerender 前被调用一次

  • 严格模式下,组件会被渲染两次,其目的就在于保证热更新后 useEffect 的清理函数能够运行,从而解决上述的问题

  • API Request

组件的数据请求一般会在组件正确挂载后执行

useEffect(() => {
fetch('')
.then((res) => res.json())
.then((data) => {
setPosts(data)
})
}, [])

但当在请求过程中,切换组件、组件卸载或者组件重新渲染后,这个请求仍然发出了且拿到了响应,并更新了 state

对于具有更多状态的更复杂的情况,这种现象可能会引发更多问题

更合理的行为是一旦离开组件或者卸载组件后,请求应该被立刻取消

可以使用 useEffect 的清理函数来解决这个问题

useEffect(() => {
const controller = new AbortConcroller()
const signal = controller.signal

fetch('', { signal })
.then((res) => res.json())
.then((data) => {
setPosts(data)
}).catch((err) => {
if(err.name === 'AbortError') {
console.log('request cancelled')
}
})

return () => {
controller.abort()
}
}, [])

useEffect(() => {
const cancelToken = axios.cancelToken.source()

axios.get('', { cancelToken: cancelToken.token })
.then((data) => {
setPosts(data)
}).catch((err) => {
if(axios.isCancel(err)) {
console.log('request cancelled')
}
})

return () => {
cancelToken.cancel()
}
}, [])

当然,为了更好的开发体验,强烈建议使用 useRequest 或者 useSWR 库来做数据请求与缓存管理

setState 是同步还是异步的

useState 的 setState 函数实际上是同步的,但 React 将多个 setState 调用分组到一个批处理中以优化性能,这可能会使其看起来是异步的

不是严格意义上的 JavaScript 异步。在计算机科学中对该术语的更一般理解中,它是异步的。

DOM Diffing 算法

是什么

DOM diff 就是对比两颗虚拟DOM树的算法。当组件变化时,会render出一个新的虚拟DOM,diff算法对比新旧虚拟DOM之后,得到一个patch,然后React用patch来更新真实DOM

怎么做

  • 先对比根节点
    • 如果根节点的类型变了(div => span),那么就认为整棵树都变了,不再对比子节点,直接删除对应的真实DOM树,根据虚拟DOM树创建新的真实DOM树
    • 如果根节点的类型没变,再对比属性是否发生改变
      • 如果属性没变,就保留,继续进行子节点的diff
      • 如果属性变了,就只更新该节点的属性,不再重新创建新节点
        • 更新style时,如果多个css属性只有一个改变了,那么React只更新改变的
  • 然后对子节点继续做以上操作
  • 举例
    1. 情况1
<ul>
<li>A</li>
<li>B</li>
</ul>

<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
  • React依次对比A-AB-Bnull-C,发现C是新增的,最终会创建真实C节点插入页面
  1. 情况2
<ul>
<li>B</li>
<li>C</li>
</ul>

<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
  • 其实只需要创建 A 文本,保留 B 和 C 即可,但React不是这样的
    • React 对比B-A,会删除 B 文本新建 A 文本;
    • 对比C-B,会删除 C 文本,新建 B 文本;(注意,并不是边对比边删除新建,而是把操作汇总到 patch 里再进行 DOM 操作);
    • 对比null-C,会新建 C 文本。
  • 这也是React需要key的原因:
<ul>
<li key="b">B</li>
<li key="c">C</li>
</ul>

<ul>
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
</ul>
  • React 先对比 key 发现 key 只新增了一个,于是保留 b 和 c,新建 a

双端交叉diff算法(Vue)

  • 将新旧DOM树同一节点的子节点们分别处理为新旧两个数组
  • 头头对比: 对比两个数组的头部,如果找到,把新节点patch到旧节点,头指针后移
  • 尾尾对比: 对比两个数组的尾部,如果找到,把新节点patch到旧节点,尾指针前移
  • 旧尾新头对比: 交叉对比,旧尾新头,如果找到,把新节点patch到旧节点,旧尾指针前移,新头指针后移
  • 旧头新尾对比: 交叉对比,旧头新尾,如果找到,把新节点patch到旧节点,新尾指针前移,旧头指针后移
  • 利用key对比: 用新指针对应节点的key去旧数组寻找对应的节点,这里分三种情况,当没有对应的key,那么创建新的节点,如果有key并且是相同的节点,把新节点patch到旧节点,如果有key但是不是相同的节点,则创建新节点
  • 失败就按流程继续对比,成功就重新进入循环,直到任意一数组的头指针超过尾指针

Diff算法 | Marvin (canyuegongzi.github.io)

React DOM diff和Vue DOM diff的区别

  1. React是从左向右遍历对比,Vue是双端交叉对比
  2. Vue整体的diff效率比React更高,
    • 假设有n个子节点,我们只是把最后的子节点移动至第一个
      • 则React需要借助Map进行key搜索找到匹配项,然后复用节点
      • Vue会发现移动,直接复用该节点

JSX 本质

React.createElement 的函数调用

  • 这里就解答了「为什么jsx中不允许写语句,只允许写表达式」
    • 基于React元素渲染机制,因为jsx会被编译为react元素对象,其中调用时,我们写的大括号中的内容会作为React.createElement()的第三个参数,作为函数的参数,自然不允许使用语句
  • createElement方法最后返回一个调用ReactElement执行方法,并传入处理过的参数,返回一个ReactElement实例,即虚拟DOM(以 JavaScript 对象形式存在的对 DOM 的描述)
  • 最后调用ReactDOM.render方法将虚拟DOM转换为真实DOM,并挂载到页面中
  • 为什么onclick变成了onClick
    • 因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。

useState 为什么不能在条件或循环中使用

  • 在组件中,useState维护多个state是有序的(可能是数组,链表),react对useState的标识是用index去记录的,如果在if中使用useState可能会导致顺序出错
  • 确保 Hook 在每一次渲染中都按照同样的顺序被调用
  • 为什么useState是数组结构的形式返回的,能不能以Object的形式返回?
    • 因为useState维护多个state是有序的

虚拟DOM的原理是什么

是什么

  • 虚拟DOM就是虚拟节点。React用JS对象模拟DOM节点,然后将其渲染成真实的DOM节点

怎么做

  1. 模拟JSX => 虚拟DOM对象

    • 用JSX语法写出来的div其实就是一个虚拟节点

    • <div id={"x"}>
      <span className={'red'}>hi</span>
      </div>
    • 通过调用React.createElement(),可以将JSX语法转译得到一个虚拟DOM对象

    • { 
      tag: 'div',
      props: { id: 'x' },
      children: [
      {
      tag: 'span',
      props: {
      className: 'red'
      },
      children: [ 'hi' ]
      }
      ]
      }
  2. 渲染虚拟DOM => 真实DOM

function render(vdom) {
// 如果是字符串或者数字,创建一个文本节点
if (typeof vdom === 'string' || typeof vdom === 'number') {
return document.createTextNode(vdom)
}
const { tag, props, children } = vdom
// 创建真实DOM
const element = document.createElement(tag)
// 设置属性
setProps(element, props)
// 递归遍历子节点,并获取创建真实DOM,插入到当前节点
children
.map(render)
.forEach(element.appendChild.bind(element))

// 虚拟 DOM 中缓存真实 DOM 节点
vdom.dom = element

// 返回 DOM 节点
return element
}

function setProps // 略
function setProp // 略
  • 如果节点发生变化,并不会直接把新虚拟节点渲染到真实节点,而是先通过diff算法得到一个patch(补丁)再更新到真实节点上

解决了什么问题

  • DOM操作性能问题,通过虚拟DOM和diff算法减少不必要的DOM操作,保证性能下限
  • DOM操作不方便问题,以前各种DOM API要记,现在只有setState就可以解决DOM更新的问题

优点

  • 为React带来了跨平台能力,因为虚拟节点的存在,除了可以将其渲染为真实节点,还可以渲染为其他东西
  • 让DOM操作的整体性能更好,能(通过diff)减少不必要的DOM操作

缺点

  • React为虚拟DOM创造了

    合成事件

    ,与原生DOM事件不太一样

    • 所有React事件都绑定到根元素,自动实现事件委托
    • 如果混用合成事件和原生DOM事件,有可能会出bug

如何解决缺点

不用React,用Vue3。。。

分别会打印出什么

setState((n) => {
console.log(n)
return n + 1
})
setState((n) => {
console.log(n)
return n + 1
})
setState((n) => {
console.log(n)
return n + 1
})

useEffect 和 useLayoutEffect

先后顺序,同步还是异步,页面刷新时呢

useEffectuseLayoutEffect 都是 React Hooks,用于在组件中处理副作用(Side Effects),但它们在执行时机和执行方式上有所不同。

  • useEffect:
    • 执行时机:在所有 DOM 变更之后异步执行(延迟执行),不会阻塞浏览器对屏幕的渲染。
    • 订阅刷新:在渲染结果被提交到屏幕后运行。
    • 用途:用来执行那些不需要立即使用或修改DOM的副作用,如请求数据、设置订阅以及手动修改 DOM。
  • useLayoutEffect:
    • 执行时机:与 useEffect 类似,但它在所有 DOM 变更之后同步执行(在浏览器绘制前),会阻塞组件的渲染。
    • 订阅刷新:在 DOM 更新完成后立即运行,但在浏览器进行任何绘制之前。
    • 用途:用于读取或修改DOM布局等操作,如计算DOM节点的位置或大小,因为如果在 useEffect 中执行这些操作,可能会导致用户可见的布局抖动

当组件首次渲染和后续更新时,这两个钩子函数都会按照相应的生命周期执行。组件更新时,它们都会运行,但 useLayoutEffect先于 useEffect 完成,确保在浏览器绘制之前完成必要的DOM操作。

页面刷新时,即全页面重载时,组件会重新挂载,这两个钩子都将按照其规定的行为执行:useLayoutEffect 将同步执行,useEffect 会在渲染的内容呈现到屏幕之后异步执行。

简而言之,useEffect 适合那些不会产生可见的布局改变的副作用,而 useLayoutEffect 适用于可能会影响布局、需要同步执行的操作。在绝大多数情况下,推荐使用 useEffect,因为它不会阻塞页面渲染,从而提供更平滑的用户体验。如果你不确定哪个钩子更适合你的场景,通常来说 useEffect 是更安全的选择。

useRef 与 useState 区别

为什么不会更新

这是设计上的决定,用于确保对 ref 的引用可以在不同的渲染中保持稳定。

与 常量的区别

React 中的 Refs提供了一种方式,允许我们访问 DOM节点或在 render方法中创建的 React元素

本质为ReactDOM.render()返回的组件实例,如果是渲染组件则返回的是组件实例,如果渲染dom则返回的是具体的dom节点

受控与非受控