面试题积累
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
<ul>
<li>A</li>
<li>B</li>
</ul>
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
- React依次对比
A-A
,B-B
,null-C
,发现C是新增的,最终会创建真实C节点插入页面
- 情况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 对比
- 这也是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但是不是相同的节点,则创建新节点
- 失败就按流程继续对比,成功就重新进入循环,直到任意一数组的头指针超过尾指针
React DOM diff和Vue DOM diff的区别
- React是从左向右遍历对比,Vue是双端交叉对比
- Vue整体的diff效率比React更高,
- 假设有n个子节点,我们只是把最后的子节点移动至第一个
- 则React需要借助Map进行key搜索找到匹配项,然后复用节点
- Vue会发现移动,直接复用该节点
- 假设有n个子节点,我们只是把最后的子节点移动至第一个
JSX 本质
React.createElement 的函数调用
- 这里就解答了「为什么jsx中不允许写语句,只允许写表达式」
- 基于React元素渲染机制,因为jsx会被编译为react元素对象,其中调用时,我们写的大括号中的内容会作为
React.createElement()
的第三个参数,作为函数的参数,自然不允许使用语句
- 基于React元素渲染机制,因为jsx会被编译为react元素对象,其中调用时,我们写的大括号中的内容会作为
- createElement方法最后返回一个调用ReactElement执行方法,并传入处理过的参数,返回一个
ReactElement实例
,即虚拟DOM
(以 JavaScript 对象形式存在的对 DOM 的描述) - 最后调用
ReactDOM.render
方法将虚拟DOM转换为真实DOM,并挂载到页面中 - 为什么onclick变成了onClick
- 因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用
camelCase
(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。
- 因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用
useState 为什么不能在条件或循环中使用
- 在组件中,useState维护多个state是有序的(可能是数组,链表),react对useState的标识是用index去记录的,如果在if中使用useState可能会导致顺序出错
- 确保 Hook 在每一次渲染中都按照同样的顺序被调用。
- 为什么useState是数组结构的形式返回的,能不能以Object的形式返回?
- 因为useState维护多个state是有序的
虚拟DOM的原理是什么
是什么
- 虚拟DOM就是虚拟节点。React用
JS对象
来模拟DOM节点,然后将其渲染成真实的DOM节点
怎么做
模拟(
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' ]
}
]
}
渲染(
虚拟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
先后顺序,同步还是异步,页面刷新时呢
useEffect
和 useLayoutEffect
都是 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节点