前端面试题大合集4----框架篇(React)

目录

一、React 合成事件

二、React中的setState,什么时候时同步的,什么时候时异步的?

三、React Hook相关知识

1、请勿在循环或者条件语句中使用hook

2、使用map循环数组渲染列表时须指定唯一且稳定的key

3、memo

4、useMemo和useCallback

5、useImperativeHandle (forwardRef)

6、获取最新的state

7、useRef

四、redux为什么把reducer设计成纯函数

五、React和Vue的diff算法的时间复杂度从O(n^3)优化到O(n),是如何优化的?

六、组件通信方式

七、Virtual Dom和Diff算法

一、React 合成事件

Dom事件流分三个阶段:事件捕获阶段,目标阶段,事件冒泡阶段;

React在事件绑定时有一套自身的机制,就是合成事件。如下比较直观:

react中事件绑定:
<div className="dome" onClick={this.handleClick}>
普通的事件绑定:
<div class="dome" onclick="handleClick()">

React合成事件机制:React并不是将click事件直接绑定在dom上面,而是采用事件冒泡的形式冒泡到document上面,然后React将事件封装给正式的函数运行和处理。

React 16 把所有事件都绑定到 document 上;

React 17 把所有事件都绑定到 container 上,ReactDom.render(app, container);

总结起来就是:事件触发、事件冒泡、事件捕获、事件合成、派发。

作用:

  • 底层抹平不同浏览器之间的差异。爆漏稳定,统一,与原生事件相同的接口;
  • 把握主动权,中心化控制;
  • 引入事件池,避免频繁的创建和销毁;

与原生dom事件的区别:

  • 包含对原生dom事件引用 e.nativeEvent

React合成事件原理:

  1. 当用户在为onClick添加函数时,React并没有将Click绑定到Dom上;
  2. 而是document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装交给中间层SyntheticEvent (负责所有事件合成);
  3. 然后使用统一的分发函数dispatchEvent,将封装的事件内容交由真正的处理函数执行。

React中也可以使用原生事件,合成事件和原生事件也可以混合使用:

class Demo extends React.Component {
    componentDidMount() {
        const $this = ReactDOM.findDOMNode(this)
        $this.addEventListener('click', this.onDOMClick, false)
    }

    onDOMClick = evt => {
        console.log('dom event')
    }
    
    onClick = evt => {
        console.log('react event')
    }

    render() {
        return (
            <div onClick={this.onClick}>Demo</div>
        )
    }
}

React中阻止事件冒泡调用:evt.stopPropagation()方法,由于Dom事件被阻止了,无法到达document,所以合成事件自然不会被触发。

  1. 事件绑定在container上
  2. 自身实现了冒泡机制,不能通过 return false 阻止冒泡
  3. 通过SytheticEvent实现事件合成

二、React中的 setState 执行机制

什么时候时同步的,什么时候时异步的?

1. react18版本之前

  • setState在不同情况下可以表现为异步或同步
  1. 在Promise的状态更新、js原生事件、setTimeout、setInterval中是同步的。
  2. 在react的合成事件中,是异步的

2. react18版本之后。

  • setState都会表现为异步(即批处理)。
  1. 批处理:是指 React将多个状态更新分组到单个重新渲染中以获得更好的性能
  2. 如果同一点击事件中有两个状态更新,React 总是将它们批处理为一次重新渲染。如果运行以下代码,您将看到每次单击时,尽管您设置了两次状态,React 只执行一次渲染
function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  function handleClick() {
    setCount(c => c + 1); // Does not re-render yet
    setFlag(f => !f); // Does not re-render yet
    // React will only re-render once at the end (that's batching!)
  }
  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}
  • 批量更新

 是覆盖操作,取最后一次执行的结果,会合并到一起,要想让每次都是有效的,可以通过给setState第一个参数用回调函数的方式来解决。

来看一下代码:

  tick = () => {
    this.setState({ seconds: this.state.seconds + 1 })
    console.log('******', this.state.seconds);
    this.setState({ seconds: this.state.seconds + 1 })
    console.log('******', this.state.seconds);
    this.setState({ seconds: this.state.seconds + 1 })
    console.log('******', this.state.seconds);
    this.setState({ seconds: this.state.seconds + 1 })
    console.log('******', this.state.seconds);
    this.setState({ seconds: this.state.seconds + 1 })
    console.log('******', this.state.seconds);
    this.setState({ seconds: this.state.seconds + 1 })
    console.log('******', this.state.seconds);
    this.setState({ seconds: this.state.seconds + 1 })
    console.log('******', this.state.seconds);

  }

 调整成下面的代码就是有效的了:

  tick = () => {
    this.setState((prev) => {
      return { seconds: prev.seconds + 1 }
    })
    console.log('******', this.state.seconds);
    this.setState((prev) => {
      return { seconds: prev.seconds + 1 }
    })
    console.log('******', this.state.seconds);
    this.setState((prev) => {
      return { seconds: prev.seconds + 1 }
    })
    console.log('******', this.state.seconds);
    this.setState((prev) => {
      return { seconds: prev.seconds + 1 }
    })
    console.log('******', this.state.seconds);
    this.setState((prev) => {
      return { seconds: prev.seconds + 1 }
    })
    console.log('******', this.state.seconds);

  }
  • 总结

setState 只在合成事件和 hook() 中是“异步”的,在 原生事件和 setTimeout 中都是同步的。

三、React Hook相关知识

1、请勿在循环或者条件语句中使用hook

因为React Hook是以单向循环链表的形式存储的,即是有序的。

循环是为了从最后一个节点移动到下一个节点的时候,只需要通过next一步就可以拿到第一个节点。

React Hook的执行,分为mountupdate阶段。在mount阶段,通过mountWorkInProgressHook()创建各个hooks(如useState、 useMemo、useEffect、useCallBack等)并且将当前hook添加到表尾。在update阶段,在获取或者更新hooks只的时候,会先获取当前hook的状态:hook.memoizedState,并且按照顺序读取或更新hook,若在条件判断或者循环中使用hooks,那么在更新阶段若增加或者减少了某个hook,hooks的数量发生变化,而React是按照顺序,通过next读取下一个hook,则导致后面的hooks和挂载阶段对应不上,发生读写错误的情况,从而引发bug。

React为什么要以单向循环链表的形式存储hooks呢?直接以key-value的对象形式存储就可以在循环或条件语句中使用hooks了,岂不更好?

这是因为react scheduler的调度策略如此,以链表的形式存储是为了可以实现并发、可打断、可恢复、可继续执行下一个fiber任务。

2、使用map循环数组渲染列表时须指定唯一且稳定的key

react中的key本质时服务于diff算法的,它的默认值是null,在diff算法中,新旧节点是否可以复用,首先就会判断key是否相同,其后才会进行其他条件的判定(如props),从而提升渲染性能,减少重复无效的渲染。

为什么在渲染列表的时候,不能讲index设置为key?

因为显式的把index设置为key,和不设置效果是一样的,这就是所谓的就地复用原则,即react在diff的时候,如果没有key,就会在老虚拟Dom树中,找到对应顺序位置的组件,然后对比组件的类型和props来决定是否需要重新渲染。

index作为key,不仅会在数组发生变化的时候,造成无效多余的渲染,还可能在组件含有非受控组件的时候,造成UI渲染错误。

如果渲染列表的时候,key重复了会怎么样?

首先react会给你输出警告,告诉你key值应该是唯一的,以便组件在更新期间保持其标识。重复的key可能导致子节点被重复使用或省略,从而引发UI bug。

3、memo

在react中,当我们setState之后,若值发生变化,则会重新render当前组件以及其子组件,在必要的时候,我们可以使用memo进行优化,来减少无效渲染。memo是一个高阶组件,接受一个组件为参数,并返回一个原组件为基础的新组件,而在memo内部,则会使用Object.is来遍历对比新旧props是否发生变化,以决定是否需要重新render。

4、useMemo和useCallback

它两个都是用来缓存数据,优化性能的。

  • 共同作用

在依赖数据发生变化的时候,才会调用传进去的回调函数去重新计算结果,起到一个缓存的作用

  • 两者的区别

useMemo  缓存的结果是回调函数中return回来的值,主要用于缓存计算结果的值,应用场景如需要计算的状态。
useCallback  缓存的结果是函数,主要用于缓存函数,应用场景如需要缓存的函数,因为函数式组件每次任何一个state发生变化,会触发整个组件更新,一些函数是没有必要更新的,此时就应该缓存起来,提高性能,减少对资源的浪费;另外还需要注意的是,useCallback应该和React.memo配套使用,缺了一个都可能导致性能不升反而下降。

5、useImperativeHandle (forwardRef)

某个组件想要暴漏一些方法,来供外部组件来调用。就需要用这个hook,需要和forwardRef来配合使用。

6、获取最新的state

在react中,setState之后,是采用异步调度、批量更新的策略,导致我们无法直接获取最新的state。

在使用class组件的时候,我们可以通过传递第二个参数,传一个回调函数来获取最新的state,但是在React18版本之后,就算在class component里面,setTimeout,原生事件回调里,也是异步批量更新了。

在hooks里面,我们目前只能通过useEffect,把当前state当作依赖传入,来在useEffect回调函数里面获取最新的state。

7、useRef

如果我们想在hooks里面获取同步最新的值,可以使用useRef;创建一个ref对象,然后挂载到hook.memoizedState,我们在修改的时候,就是直接修改ref.current。useRef其实就是提供一个稳定的变量,在组件的整个生命周期都是持续存在且是同一个引用。

注意:修改useRef返回的状态不会引起UI的重渲染。

四、redux为什么把reducer设计成纯函数

redux的设计思想就是不产生副作用,数据更改的状态可回溯,所以redux中处处都是纯函数

五、React和Vue的diff算法的时间复杂度从O(n^3)优化到O(n),是如何优化的?

三种优化来降低复杂度:

  1. 如果父节点不同,放弃对子节点的比较,直接删除旧节点然后添加新节点重新渲染;
  2. 如果子节点有变化,Virtual Dom不会计算变化的什么,而是直接重新渲染;
  3. 通过唯一的key策略。

六、组件通信方式

  1. 父传子,通过props,也可以通过ref方式传递数据;

  2.  子传父,子组件通过回调函数向父组件传递数据,父组件可以在回调函数中处理接收到的数据;

  3. 使用ContextAPI:ContextAPI允许在组件之间共享数据,而不必通过props手动传递。可以在父组件中创建一个Context对象,然后在子组件中通过Context.Provider提供数据,子组件可以通过Context.Consumer或者useContext钩子函数来接收数据;
  4. 使用状态管理工具stroe,比如mobx,redux;
  5. 使用Event Bus来实现组件通信。
  6. ref和useImperativeHandle互相结合。

七、Virtual Dom和Diff算法

  • Virtual Dom

Virtual Dom即虚拟Dom,是从真实的Dom树中抽象出来的一个JS对象,因为每次更新数据,页面也每次都频繁的重排与重绘,给页面带来极大的性能方面的开销,在大规模应用下维护起来会很困难。

所以React这个框架就在真实Dom树的基础上,抽象出来了一个虚拟Dom。用一个JS对象即Virtual Dom来描述真实Dom树结构,然后用它来构建真正的Dom树再渲染到页面上。

当状态发生变化之后,重新构造新的JS对象和旧的JS对象作对比得出差异。

针对差异之处进行重新的构建,更新视图。

  • Diff算法

上面标红的字体,就是通过Diff算法来实现的;即:新的VirtualDom和旧的VirtualDom对比的过程,是通过Diff算法来实现的。

React的Diff策略:

  1. Web UI中Dom节点跨层级的移动操作特别少,所以基本是比较同级两个节点的树的差异,即tree diff;
  2. 拥有相同类的两个组件,将会生成相似的树形结构,拥有不同类的两个组件,将会生成不同的树形结构,即component diff;
  3. 对于同一层级的一组子节点,它们可以通过唯一id进行区分,通过key同于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识,即element diff。

React分别对tree diff、component diff以及element diff进行算法优化。

React在比较出差异之后,再去进行视图更新,是批量操作,异步更新,这一点在本片文章的第二条已经讲过了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值