目录
二、React中的setState,什么时候时同步的,什么时候时异步的?
5、useImperativeHandle (forwardRef)
五、React和Vue的diff算法的时间复杂度从O(n^3)优化到O(n),是如何优化的?
一、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合成事件原理:
- 当用户在为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 (
<div onClick={this.onClick}>Demo</div>
)
}
}
React中阻止事件冒泡调用:evt.stopPropagation()
方法,由于Dom事件被阻止了,无法到达document,所以合成事件自然不会被触发。
- 事件绑定在container上
- 自身实现了冒泡机制,不能通过 return false 阻止冒泡
- 通过SytheticEvent实现事件合成
二、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 (
<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的执行,分为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的重渲染。
四、redux为什么把reducer设计成纯函数
redux的设计思想就是不产生副作用,数据更改的状态可回溯,所以redux中处处都是纯函数
五、React和Vue的diff算法的时间复杂度从O(n^3)优化到O(n),是如何优化的?
三种优化来降低复杂度:
- 如果父节点不同,放弃对子节点的比较,直接删除旧节点然后添加新节点重新渲染;
- 如果子节点有变化,Virtual Dom不会计算变化的什么,而是直接重新渲染;
- 通过唯一的key策略。
六、组件通信方式
-
父传子,通过props,也可以通过ref方式传递数据;
-
子传父,子组件通过回调函数向父组件传递数据,父组件可以在回调函数中处理接收到的数据;
- 使用ContextAPI:ContextAPI允许在组件之间共享数据,而不必通过props手动传递。可以在父组件中创建一个Context对象,然后在子组件中通过Context.Provider提供数据,子组件可以通过Context.Consumer或者useContext钩子函数来接收数据;
- 使用状态管理工具stroe,比如mobx,redux;
- 使用Event Bus来实现组件通信。
- 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策略:
- Web UI中Dom节点跨层级的移动操作特别少,所以基本是比较同级两个节点的树的差异,即tree diff;
- 拥有相同类的两个组件,将会生成相似的树形结构,拥有不同类的两个组件,将会生成不同的树形结构,即component diff;
- 对于同一层级的一组子节点,它们可以通过唯一id进行区分,通过key同于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识,即element diff。
React分别对tree diff、component diff以及element diff进行算法优化。
React在比较出差异之后,再去进行视图更新,是批量操作,异步更新,这一点在本片文章的第二条已经讲过了。