React Hooks
函数组件实现动态组件类组件的功能
React 最新版本 v18.2,Hook 是 React 16.8 新增特性。可以在不编写 class 的情况下使用 state 以及其他 React 特性。
引入 Hook 的动机——解决看起来不相关的问题
Hoos 是执行函数,产生函数上下文
useState
在函数组件中使用状态,并且后期基于状态修改,进而更新组件
1 | /** |
Demo
1 | // 函数组件中使用状态,更新视图 |
关于函数组件渲染更新
flowchart TB subgraph ExcuteContext1 subgraph 渲染EC1 EC1[" // 私有变量 num = 0; setNum = function(){/* 修改状态函数 */}; handle = function(){/* 事件处理函数 */} "] --> parse[编译JSX视图] parse --> virtualDOM[创建VirtualDOM] virtualDOM --> realDOM["创建真实DOM,页面渲染"] end subgraph handle CLICK[" // 私有变量 setNum(++num) // 修改状态 + 更新视图 "] -.-> EC1 end end subgraph ExcuteContext2 subgraph 更新EC2 EC2[" // 私有变量 num = 1; setNum = function(){/* 修改状态函数 */}; handle = function(){/* 事件处理函数 */} "] --> parse2[编译JSX视图] parse2 --> virtualDOM2[创建VirtualDOM] virtualDOM2 -->|"diff 算法"| realDOM2["创建真实DOM,页面渲染"] end subgraph handle2 CLICK2[" // 私有变量 setNum(++num) // 修改状态 + 更新视图 "] -.-> EC2 end end 渲染EC1 -->|"重新执行DOM函数,产生新的函数执行上下文"| 更新EC2
疑问
点击按钮 3 秒钟之后,num 输出是几???
答案是 0。在 ExcuteContext1 作用域下,按照作用域链,访问的到的是 num = 0;不会访问 ExcuteContext2 作用域,所以 num 也不会为1。
1 | export default function Demo() { |
useState 源码分析
1 | /** |
函数组件处理多状态
- 方法一,useState 不能部分修改状态
1
2
3
4
5
6
7
8
9
10
11let [state, setState] = useState({
deprecation: 1,
approve: 2
})
const handle = () => {
setState({
...state,
approve: 3
})
} - 方法二,useState 分别初始化多个状态 【官方推荐】
1
2
3
4
5
6let [deprecation, setDeprecation] = useState(1)
let [approve, setApprove] = useState(2)
const handle = () => {
setDeprecation(5)
}
useState 参数为函数
业务处理逻辑仅在第一次组件渲染时触发,更新不使用。可以对赋初值的动作进行【惰性处理】,如下代码
1 | ... |
setState 是同步还是异步
React 18中,无论是使用 Hook 函数 useState 设置更新状态还是类组件 this.setState,多个状态修改是异步的;而在 React 16 中,异步操作中(如:定时器,手动事件绑定)修改状态为同步的。
flowchart TB start((开始)) --> setState[setState] setState --> isAsync{" 是否异步代码修改状态\n(如:定时器,手动事件绑定) setTimeout(() => { setReduce(--reduce) setSum(plus + reduce) }, 1000); "} isAsync -->|No| res1["state加入队列,再渲染【异步】"] isAsync -->|Yes| version{"is react 16"} version -->|Yes| res2["修改 state,渲染【同步】"] version -->|No| res1
setState 的性能优化机制
1 | export default function Demo() { |
不会再次渲染,因为每次修改状态值时,会拿最新修改的值与之前的状态值基于 Object.is 比较,Object.is(NaN, NaN) 返回 true; 两次修改值相同,则不会修改状态,视图也不会更新。类似于 PureComponent 中,在 shouldComponent 中作了浅比较优化。
思考
以下代码会渲染几次,最终结果是几
1 | ... |
点击后渲染 1次,最终渲染结果是 11.
以下代码会渲染几次,最终结果是几
1 | ... |
点击后渲染 2,最终渲染结果是 11. 原因:优化机制,消息队列中按照优化机制,不会多次渲染相同的值
以下代码会渲染几次,最终结果是几
1 | ... |
点击后渲染 1,最终渲染结果是 20. updater 队列中存储的是 10 个回调函数,每次执行回调时,拿到的是上一个计算出的 x 的值。合并处理后,渲染一次,值为 20
useEffect
1 | import React, {useEffect, useState} from "react" |
flowchart TB subgraph "第一次渲染【componentDidMount】" render --> useEffect end subgraph "更新【componentDidUpdate】" render1[render] --> useEffect1[useEffect] end
useEffect(callback)
第一次渲染完后,执行 callback。更新完成后,同样再次执行 callback
依赖项为空
1 | ... |
多个依赖项
任一依赖项变化,触发 callback 执行
1 | ... |
依赖项类似与 ComponentShouldUpdate
effect 第一渲染不执行,更新的时候运行 callback 返回的函数
1 | ... |
useEffect 原理分析
1 | import React, {useEffect, useState} from "react" |
flowchart TB subgraph 第一次渲染 subgraph render s1["num=0 setNum(){}"] s2["useEffect(()=>{ console.log('effect 无依赖') })"] s3["useEffect(()=>{ console.log('effect 依赖为空') },[])"] s4["useEffect(()=>{ console.log('effect 依赖为num') },[num])"] s5["useEffect(()=>{ return () => { console.log('effect 无依赖,无输出,返回函数') } },[num])"] end subgraph effect链表 se1["effect 无依赖"] se2["effect 依赖为空"] se3["effect 依赖为num"] se4["effect 无依赖,无输出,返回函数"] end render -->|"MountEffect callback 中的依赖项加入链表"| effect链表 subgraph output a["effect 无依赖"] b["effect 依赖为空"] c["effect 依赖为num"] end end subgraph 更新 subgraph updater s11["num=0 setNum(){}"] s21["useEffect(()=>{ console.log('effect 无依赖') })"] s31["useEffect(()=>{ console.log('effect 依赖为空') },[])"] s41["useEffect(()=>{ console.log('effect 依赖为num') },[num])"] s51["useEffect(()=>{ return () => { console.log('effect 无依赖,无输出,返回函数') } },[num])"] end subgraph effect链表1 se11["effect 无依赖"] se21["effect 依赖为空"] se31["effect 依赖为num"] se41["effect 无依赖,无输出,返回函数"] end subgraph output1 a1["effect 无依赖,无输出,返回函数"] b1["effect 无依赖"] c1["effect 依赖为num"] end end 第一次渲染 -->|"点击按钮,修改 num 状态"| 更新
Error - 1
Line 26:9: React Hook “useEffect” is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
每次渲染 react hooks 都要以相同的顺序在组件中调用
1 | if(num > 5) { |
/** 正确方式 **/
1 | useEffect(() => { |
Error - 2
react-dom.development.js:86 Warning: useEffect must not return anything besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => …) or returned a Promise. Instead, write the async function inside your effect and call it immediately:
useEffect(() => {
async function fetchData() {
// You can await here
const response = await MyAPI.getData(someId);
// …
}
fetchData();
}, [someId]); // Or [] if effect doesn’t need props or state
1 | useEffect(() => { |
Error-3 快速点击 btn 后,样式或内容会有短暂的闪烁
1 | export default function Demo() { |
原因分析:
%%{init: {"flowchart": {"htmlLabels": true}} }%% flowchart TB subgraph "组件渲染步骤" step1["react-app 编译"] step2["创建 virtualDOM"] step3["DOM-DIFF,渲染真实 DOM"] step4["useEffect 异步执行链表中方法执行"] step1 --> step2 step2 --> step3 step3 --> step4 step4 --> step1 end
那么真实 DOM 会渲染两次,所以会有内容和样式上的闪烁,使用 useLayoutEffect 解决:
%%{init: {"flowchart": {"htmlLabels": true}} }%% flowchart LR subgraph "组件渲染步骤" step1["babel-preset-react-app 编译 createElement"] step2["createElement 创建 virtualDOM"] step3["root.render 把 virtualDOM 变为真实 Diff 运算"] step4["useLayoutEffect阻塞渲染,同步执行EFFECT 链表中方法"] step5["渲染真实 DOM"] step1 --> step2 step2 --> step3 step3 --> step4 step4 --> step5 end
使用 useLayoutEffect 可以解决闪烁问题,如上图真实 dom 只渲染一次,所以不会闪烁
useEffect 与 useLayoutEffect 区别
useLayoutEffect 会阻塞浏览器真实 DOM,优先执行 Effect 链表中的 callback;
useEffect 不会阻塞浏览器渲染真实 DOM,在渲染真实 DOM 的同时,去执行 Effect 链表中的 callback.
useLayoutEffect 优先于 useEffect 执行
都可以获取 DOM 元素,原因在于真实 DOM 已经生成,区别只是 useLayoutEffect 在执行完 effect 链表后渲染 DOM 到浏览器
useEffect 会渲染两次,useLayoutEffect 会合并真实 DOM 渲染。