前端面试题大合集4----框架篇(React)
一、React 合成事件
Dom事件流分三个阶段:事件捕获阶段,目标阶段,事件冒泡阶段
React在事件绑定时有一套自身的机制,就是合成事件。如下比较直观:
react中事件绑定: 普通的事件绑定:
React合成事件机制:React并不是将click事件直接绑定在dom上面,而是采用事件冒泡的形式冒泡到document上面,然后React将事件封装给正式的函数运行和处理。
React合成事件原理:
- 当用户在为onClick添加函数时,React并没有将Click绑定到Dom上;
- 而是document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装交给中间层SyntheticEvent (负责所有事件合成);
- 然后使用统一的分发函数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 ( Demo ) } }
React中阻止事件冒泡调用:evt.stopPropagation()方法,由于Dom事件被阻止了,无法到达document,所以合成事件自然不会被触发。
二、React中的setState,什么时候时同步的,什么时候时异步的?
1. react18版本之前
- setState在不同情况下可以表现为异步或同步
- 在Promise的状态更新、js原生事件、setTimeout、setInterval中是同步的。
- 在react的合成事件中,是异步的
2. react18版本之后。
- setState都会表现为异步(即批处理)。
- 批处理:是指 React将多个状态更新分组到单个重新渲染中以获得更好的性能
- 如果同一点击事件中有两个状态更新,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 ( Next { color: flag ? "blue" : "black" }}{count} ); }
- 总结
setState 只在合成事件和 hook() 中是“异步”的,在 原生事件和 setTimeout 中都是同步的。
三、React Hook相关知识
1、请勿在循环或者条件语句中使用hook
因为React Hook是以单向循环链表的形式存储的,即是有序的。
循环是为了从最后一个节点移动到下一个节点的时候,只需要通过next一步就可以拿到第一个节点。
React Hook的执行,分为mount和update阶段。在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的重渲染。
- 两者的区别
- 共同作用