Hooks

useState

返回一个有状态的值,以及一个更新它的函数,它接收一个新的状态值并将组件的重新渲染排入队列
初始渲染的时候,返回的状态就是传入的第一个参数
useState是替换而不是合并,所以在存储数组或者对象的时候,可以使用扩展运算符等拷贝一下
如果 useState 传入的是一个函数(因为有时候我们会经过一些计算之后获得初始值)这个函数也只会在初始渲染时执行,后续的更新渲染是不会执行的
如果更新函数传入的值和之前的值相同,那么不会更新子组件,通过 Object.is 判断

useEffect

在组件渲染后实现各种不同的副作用,有的副作用可能需要清楚,所以需要返回一个函数。这个副作用的意思是,组件干了一些不该干的事,而且不一定幂等。
比如,本来是想渲染 DOM 展示到页面上,但除了 DOM 之外还有数据,而这些数据必须从外部数据源中获取,这个获取外部数据源的过程就是副作用
依赖项会空数据则只会在挂载时运行效果,卸载时清理,它会在调用一个effect之前对前一个effect进行清理
useEffect = componentDidMount + componentDidUpdate + componentWillUnmount
执行时机:浏览器完成画面渲染之后延迟调用useEffect

useLayoutEffect

有DOM操作的副作用hook,用法和useEffect一样,执行时机是DOM更新之后
可以使用这个来读取DOM布局并同步触发重渲染,浏览器执行绘制前,useLayoutEffect内部的更新计划将被同步刷新
useLayoutEffect 的调用阶段和componentDidMount、componentDidUpdate的调用阶段一样

useMemo

传递一个函数和依赖项,函数返回一个 memoized 值(任何,函数、对象等都可以),只有在依赖项发生改变的时候,才会重新调用这个函数,返回一个新值。

使用场景

  1. 复杂计算逻辑优化,比如一个函数执行的操作是计算量非常大的操作,如果因为其他的更改导致组件更新从而让该函数每次都重新计算一遍,会非常耗时,这时候就可以使用useMemo
  2. 父子组件重复渲染问题优化,当子组件包裹了一个memo,此时父组件中的值作为了子组件的prop,即使有memo子组件也会每次都更新,因为父组件的值在父组件每次重新渲染时都是重新定义这个值,然后子组件进行浅比较的时候,这个值永远是不同的,所以会重新渲染。可以将这个值作为useMemo的返回值来解决

useCallback

返回一个 memoized 回调函数
传递一个内联回调和一组依赖项。 useCallback 将返回回调的记忆版本,仅当其中一个依赖项发生更改时才会更改。这在将回调传递给优化的子组件时很有用,这些子组件依赖于引用相等来防止不必要的渲染

**useCallback(fn, deps) === useMemo(() => fn, deps)**

useRef

useRef 返回一个可变 ref 对象,其 .current 属性初始化为传递的参数 (initialValue)。返回的对象将在组件的整个生命周期内持续存在
作用:访问 DOM、保留任何可变值
useRef() 创建了一个普通的 JavaScript 对象, useRef() 和自己创建 {current: …} 对象之间的唯一区别是 useRef 将在每次渲染时为您提供相同的 ref 对象。也就是说,改变ref.current不会主动触发页面重新渲染

useContext

作用:让子组件之前共享父组件传入的状态。
通过createContext创建一个context,Context.Provider来确定数据共享范围,通过value分发内容,在子组件通过useContext获取数据

useImperativeHandle

需要在 React.forwardRef 下使用,可以自定义暴露 ref 给父组件

路由守卫(React Router V5)

传入 Component 和条件,利用 Route 的 render属性,当条件满足,render 返回 Component,不满足就 Redirect 到指定页面。这里要注意,这个条件,当页面刷新的时候,可能恢复初始值了,所以需要缓存
React Guard

生命周期

  • 挂载时constructor ==> getDerivedStateFromProps ==> render ==> React 更新 DOM 和 refs ==> componentDidMount
  • 更新时New Props/setState()/forceUpdate() ==> getDerivedStateFromProps ==> shouldComponentUpdate ==> render ==> getSnapshotBeforeUpdate ==> React 更新 DOM 和 refs ==> componentDidUpdate
  • 卸载时componentWillUnmount

getDerivedStateFromProps

在调用 render 方法之前调用,并且在 初始挂载和 后续更新时都会被调用。返回一个对象来更新 state,如果返回 null 则不更新任何内容

使用 props 来派生/更新 state。但凡使用该函数,都必须出于该目的,使用它才是正确且符合规范的。
这个生命周期方法是一个静态方法,静态方法不依赖组件实例而存在,故在该方法内部是无法访问 this 的。
因为无法拿到组件实例的 this,这也导致我们无法在函数内部做 this.fetch()请求,或者不合理的 this.setState() 操作导致可能的死循环或其他副作用。React 官方通过这个限制,尽量保持生命周期行为的可控可预测,根源上帮助了我们避免不合理的编程方式。即一个 API 要保持单一性,做一件事的概念。

React Hook 闭包陷阱

比如我们有一个初始的 value,每次点击 value 都会+1,然后有一个 log 函数,里面有一个定时器函数,1s 后会 alert 出 value 的值。现在我们在调用 log 函数期间点击了让 vlaue +1 的按钮,等到 1s 后,log 函数 alert 出来的 value 还是点击之前的 1。这就造成了“闭包陷阱”——函数式组件每次 render 都会残生一个新的 log 函数,这个新的函数会产生一个在当前这个阶段 value 值得闭包。

  1. 初次渲染,生成一个 log 函数,(value = 1)
  2. value = 1 时,点击 alert 按钮执行 log 函数(value = 1)
  3. 点击按钮增加 value,比如 value 增加到 6,组件 render,产生一个新的 log,(value = 6)
  4. 计时器触发,log 函数(value = 1)弹出闭包内得 value 为 1

使用 useRef 解决

因为 Ref 返回的都是同一个对象

  1. const FunctionComponent = () => {
  2. const [value, setValue] = useState(1);
  3. const countRef = useRef(value);
  4. useEffect(() => {
  5. countRef.current = value;
  6. }, [value]);
  7. const log = useCallback(() => {
  8. setTimeout(() => {
  9. alert(countRef.current);
  10. }, 1000);
  11. }, [value]);
  12. return (
  13. <div>
  14. <p>FunctionComponent</p>
  15. <div>value: {value}</div>
  16. <button onClick={() => setValue(value + 1)}>add</button>
  17. <br />
  18. <button onClick={log}>alert</button>
  19. </div>
  20. );
  21. };

更新 state 的回调函数

React 闭包陷阱的最大问题就是无法获取最新的 state 的值

useState 更新时可以传入一个回调函数,回调函数里参数取得是最新得值。

  1. const [value, setValue] = useState(0);
  2. useEffect(() => {
  3. const timer = setInterval(() => {
  4. // 回调函数的最新值
  5. setValue(value => value + 1);
  6. }, 1000);
  7. return () => {
  8. clearInterval(timer);
  9. };
  10. }, []);

Hooks 依赖

  1. useEffect 、useLayoutEffect 内部的副作用函数会执行,并且副作用函数可以获取到当前所有依赖的最新值。
  2. useCallback、useMemo 会返回新的函数或对象,并且内部的函数也能获取到当前所有依赖的最新值
  1. const [value, setValue] = useState(0);
  2. useEffect(() => {
  3. const timer = setInterval(() => {
  4. // 回调函数的最新值
  5. setValue(value + 1);
  6. }, 1000);
  7. return () => {
  8. clearInterval(timer);
  9. };
  10. }, [value]);

但是这样每次都会经理 clearInterval =》 setValue =》clearInterval 的循环。造成不必要的性能浪费。

如何减少 render 次数?

使用 Immutable

Immutable 中有 is方法,会比较两个 Immutable 对象是否完全相同。当 某个 state 变化的时候,不会渲染所有的节点。

设置防抖

使用私有属性

shouldComponentUpdate

内部可以判断组件外部接收的最新属性和之前的属性是否一致,从而约束 render 调用的时机

pureComponent

内部机制通过浅比较去实现。

React.memo

useCallback

传递一个函数和依赖项,只有依赖项改变,才会创建新的函数。

减少计算量

useMemo

第一个参数是一个函数,第二个参数是依赖项。useMemo 会缓存第一个函数的结果并作为自己的返回值。只有依赖项改变的时候,才会重新执行第一个函数然后缓存新的结果。

为什么 React 中 setState() 是异步的?

  1. 保证内部的一致性
    即使 state 是同步更新的,props 也不是。(你只有在父组件重新渲染时才能知道 props)
  2. 性能优化
    将 state 的更新延缓到最后批量合并再去渲染对于应用的性能优化是有极大好处的,如果每次状态的改变都要去重新渲染 dom,那将带来巨大的性能消耗。

React 渲染流程概述

  1. 首先 jsx 经过 babel 的 ast 词法解析之后变成 React.createElement,React.createElement 函数执行之后就是 jsx 对象,也就是 virtual-dom。
  2. 不管是在首次渲染还是状态更新的时候,这些渲染的任务都会经过 Scheduler 的调度,Scheduler 会根据跟吴的优先级来决定将哪些任务优先进入 render 阶段,比如用户触发的更新优先级非常高,如果当前正在进行一个比较耗时的任务,这个任务就会被用户触发的更新打断。Scheduler 会分配一个时间片给需要渲染的任务,如果是一个非常耗时的任务,在一个时间片内还没有执行完成,则会从当前渲染到的 Fiber 节点暂停计算,让出执行权给浏览器,在之后浏览器空闲的时候从之前暂停的那个 Fiber 节点继续后面的计算。
  3. 在 render 阶段:主要是 Reconciler,在 mount 和 update 阶段,它会比较 jsx 和当前 Fiber 节点的差异(diff 算法),将带有副作用的 Fiber 节点标记出来,这些副作用有 Placement(插入)、Update(更新)、Deletion(删除)等,而这些带有副作用的 fiber 节点会加入一条 EffectList 中,在 commit 阶段就会遍历这条 EffectList,处理相应的副作用,应且应用到真实的节点上,而 Scheduler 和 Reconciler 都是在内存中工作的,所以不影响最后的呈现。
  4. commit 阶段:会遍历 EffectList,处理相应的生命周期,将这些副作用应用到真实节点,这个过程会对应不同的渲染器,比如在浏览器中的环境就是 react-dom

ErrorBoundary

只能捕获一种错误——React 运行流程中的错误

  • 在 render 阶段的错误
  • 在 commit 阶段(操作 DOM )的错误(类比 git commit)

其他的在点击事件里面的错误是不会被捕获的

useLayoutEffect 相当于 componentDidComponent/componentDidUpdate,在操作 DOM 之后

useInsertionEffect
dispatcher 会知道你 hook 使用是否有错误
useEffect 执行的是同步的副作用, useLayoutEffect 执行的是异步的副作用