import React, { useState } from 'react';
import { Button } from 'antd';
import './Demo.less';
// Hook实现
export default function UseStateDemo() {
const [num, setNum] = useState(0);
const handleAdd = () => {
setNum(num + 10);
};
return (
<div className="demo">
<span className="num">{num}</span>
<Button type="primary" size="small" onClick={handleAdd}>
add
</Button>
</div>
);
}
// 类组件实现
// export default class UseStateDemo extends React.Component {
// state = {
// n: 0,
// };
// handleAdd = () => {
// let { n } = this.state;
// this.setState({
// n: n + 10,
// });
// };
// render() {
// let { n } = this.state;
// return (
// <div className="demo">
// <span className="num">{n}</span>
// <Button type="primary" size="small" onClick={this.handleAdd}>
// add
// </Button>
// </div>
// );
// }
// }
-
函数组件
- 不具备“状态、ref、周期函数”等内容,第一次渲染完毕后,无法基于组件内部的操作来控制其更新,因此称之为静态组件!
- 但是具备属性及插槽,父组件可以控制其重新渲染!
- 渲染流程简单,渲染速度较快!
- 基于FP(函数式编程)思想设计,提供更细粒度的逻辑组织和复用!
-
类组件
- 具备“状态、ref、周期函数、属性、插槽”等内容,可以灵活的控制组件更新,基于钩子函数也可灵活掌控不同阶段处理不同的事情!
- 渲染流程繁琐,渲染速度相对较慢!
- 基于OOP(面向对象编程)思想设计,更方便实现继承等!
React Hooks 组件,就是基于 React 中新提供的 Hook 函数,可以让函数组件动态化
Hook函数概览
useState
作用
在函数组件中使用状态,修改状态值可让函数组件更新,类似于类组件中的setState
语法
// 返回一个 state,以及更新 state 的函数
const [state, setState] = useState(initialState);
函数组件或者Hooks组件不是类组件,所以没有实例的概念【调用组件不再是创建类的实例,而是把函数执行,产生一个私有上下文而已】,因此在函数组件中不涉及this的处理
示例
import React, { useState } from 'react';
import { Button } from 'antd';
import './Demo.less';
// Hook实现
export default function UseStateDemo() {
const [num, setNum] = useState(0);
const handleAdd = () => {
setNum(num + 10);
};
return (
<div className="demo">
<span className="num">{num}</span>
<Button type="primary" size="small" onClick={handleAdd}>
add
</Button>
</div>
);
}
// 类组件实现
// export default class UseStateDemo extends React.Component {
// state = {
// n: 0,
// };
// handleAdd = () => {
// let { n } = this.state;
// this.setState({
// n: n + 10,
// });
// };
// render() {
// let { n } = this.state;
// return (
// <div className="demo">
// <span className="num">{n}</span>
// <Button type="primary" size="small" onClick={this.handleAdd}>
// add
// </Button>
// </div>
// );
// }
// }
处理机制
函数组件的每一次渲染(或者是更新),都是把函数(重新)执行,产生一个全新的”私有上下文”
- 内部的代码也需要重新执行
- 涉及的函数需要重新的构建(这些函数的作用域【函数执行的上级上下文】,是每一次执行DEMO的闭包)
- 每一次执行DEMO函数,也会把useState重新执行,但是:
- 执行useState,只有第一次设置的初始值会生效,其余以后再执行,获取的状态都是最新的状态值【而不是初始值】
- 返回的修改状态的方法,每一次都是返回一个新的
细节处理
示例
import React, { useState } from 'react';
import { Button } from 'antd';
import './Vote.less';
const Vote = function Vote(props) {
let [num, setNum] = useState({
surNum: 0,
oppNum: 0,
});
const handleClick = (flag) => {
switch (flag) {
case 'sur':
setNum({
...num,
surNum: num.surNum + 1,
});
break;
case 'opp':
setNum({
...num,
oppNum: num.oppNum + 1,
});
break;
default:
break;
}
};
return (
<div className="vote-box">
<div className="header">
<h2 className="title">{props.title}</h2>
<span className="num">{num.surNum + num.oppNum}</span>
</div>
<div className="main">
<p>支持人数:{num.surNum}人</p>
<p>反对人数:{num.oppNum}人</p>
</div>
<div className="footer">
<Button
type="primary"
onClick={() => {
handleClick('sur');
}}
>
支持
</Button>
<Button
type="primary"
danger
onClick={() => {
handleClick('opp');
}}
>
反对
</Button>
</div>
</div>
);
};
export default Vote;
官方建议:需要多个状态,就把useState执行多次即可
import React, { useState } from 'react';
import { Button } from 'antd';
import './Vote.less';
/* 官方建议是:需要多个状态,就把useState执行多次即可 */
const Vote = function Vote(props) {
let [surNum, setSurNum] = useState(0);
let [oppNum, setOppNum] = useState(0);
const handleClick = (flag) => {
switch (flag) {
case 'sur':
setSurNum(surNum + 1);
break;
case 'opp':
setOppNum(oppNum + 1);
break;
default:
break;
}
};
return (
<div className="vote-box">
<div className="header">
<h2 className="title">{props.title}</h2>
<span className="num">{surNum + oppNum}</span>
</div>
<div className="main">
<p>支持人数:{surNum}人</p>
<p>反对人数:{oppNum}人</p>
</div>
<div className="footer">
<Button
type="primary"
onClick={() => {
handleClick('sur');
}}
>
支持
</Button>
<Button
type="primary"
danger
onClick={() => {
handleClick('opp');
}}
>
反对
</Button>
</div>
</div>
);
};
export default Vote;
同步异步
在React18中,基于useState创建出来的”修改状态的方法”,它们的执行也是异步的;原理等同于类组件中的this.setState【基于异步操作与更新队列 ,实现状态的批处理】;在任何地方修改状态,都是采用异步编程的
异步执行
同步执行
函数更新
- useState自带了性能优化的机制:
- 每一次修改状态值的时候,会拿最新要修改的值和之前的状态值做比较【基于 Object.is 作比较】
- 如果发现两次的值是一样的,则不会修改状态,也不会让视图更新【可以理解为:类似于 PurComponent,在 shouldComponentUpdate 中做了比较和优化】
函数更新一次,结果为20
惰性初始state
如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用!
import React, { useState } from "react";
export default function Demo(props) {
let [num, setNum] = useState(() => {
let { x, y } = props;
return x + y;
});
return <div>
<span>{num}</span>
</div>;
} ;
优化机制
调用 State Hook 的更新函数,并传入当前的 state 时,React 将跳过组件的渲染(原因:React 使用 Object.is 比较算法,来比较新老 state;注意不是因为DOM-DIFF;)!
import React, { useState } from "react";
export default function Demo() {
console.log('render');
let [num, setNum] = useState(10);
return <div>
<span>{num}</span>
<button onClick={() => {
setNum(num);
}}>处理</button>
</div>;
};
useEffect
作用
在函数组件中使用生命周期函数
语法
useEffect(callback)
- 第一次渲染完毕后,执行 callback,等同于 componentDidMount
- 在组件每一次更新完毕后,也会执行 callback,等同于 componentDidUpdate
useEffect(callback,[])
- 只有第一次渲染完毕后,才会执行callback,每一次视图更新完毕后,callback不再执行,类似于 componentDidMount
useEffect(callback,[依赖的状态(多个状态)])
- 第一次渲染完毕会执行callback
- 当依赖的状态值(或者多个依赖状态中的一个)发生改变,也会触发callback执行
- 但是依赖的状态如果没有变化,在组件更新的时候,callback是不会执行的
回调函数中嵌套函数
示例
import React, { useState, useEffect } from 'react';
import { Button } from 'antd';
import './Demo.less';
export default function UseStateDemo() {
const [num, setNum] = useState(0);
useEffect(() => {
console.log('回调函数', num);
});
useEffect(() => {
console.log('空数组', num);
}, []);
useEffect(() => {
console.log('设置依赖', num);
}, [num]);
useEffect(() => {
return () => {
// num 获取的是上一个状态值
console.log('回调函数中嵌套函数', num);
};
}, [num]);
const handleAdd = () => {
setNum(num + 1);
};
return (
<div className="demo">
<span className="num">{num}</span>
<Button type="primary" size="small" onClick={handleAdd}>
add
</Button>
</div>
);
}
底层机制
useEffect 在依赖变化时,执行回调函数。这个变化,是「本次 render 和上次 render 时的依赖比较」;因此我们需要:
- 存储依赖,上一次 render 的
- 兼容多次调用
- 比较依赖,执行回调函数
实现:
const lastDepsBoxs = [];
let index = 0;
const useEffect = (callback, deps) => {
const lastDeps = lastDepsBoxs[index];
const changed =
!lastDeps // 首次渲染,肯定触发
|| !deps // deps 不传,次次触发
|| deps.some((dep, index) => dep !== lastDeps[index]); // 正常比较
if (changed) {
lastDepsBoxs[index] = deps;
callback();
}
index ++;
};
增加副作用清除
effect 触发后会把清除函数暂存起来,等下一次 effect 触发时执行。
明确这个顺序就不难实现了
const lastDepsBoxs = [];
const lastClearCallbacks = [];
let index = 0;
const useEffect = (callback, deps) => {
const lastDeps = lastDepsBoxs[index];
const changed = !lastDeps || !deps || deps.some((dep, index) => dep !== lastDeps[index]);
if (changed) {
lastDepsBoxs[index] = deps;
if (lastClearCallbacks[index]) {
lastClearCallbacks[index]();
}
lastClearCallbacks[index] = callback();
}
index ++;
};
总结
- 利用闭包,useState / useEffect 的实现并不深奥
- 巧妙的是对多次调用的组织方式
- 使用 hooks 要避免 if、for 等嵌套
useLayoutEffect
其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。
可以使用它来读取 DOM 布局并同步触发重渲染。
在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
尽可能使用标准的 useEffect 以避免阻塞视觉更新
import { useState, useLayoutEffect } from 'react';
export default function App() {
const [count, setCount] = useState(0);
useLayoutEffect(() => {
console.log(`useLayoutEffect - count=${count}`);
// 耗时的操作
const pre = Date.now();
while (Date.now() - pre < 500) {}
if (count === 0) {
setCount(10 + Math.random() * 200);
}
}, [count]);
return <div onClick={() => setCount(0)}>{count}</div>;
}
- useLayoutEffect会阻塞浏览器渲染真实DOM,优先执行Effect链表中的callback;
- useEffect不会阻塞浏览器渲染真实DOM,在渲染真实DOM的同时,去执行Effect链 表中的callback;
- useLayoutEffect设置的callback要优先于useEffect去执行! !
- 在两者设置的callback中,依然可以获取DOM元素「原因:真实DOM对象已经创建了,区别只是浏览器是否渲染」
- 如果在callback函数中又修改了状态值「视图又要更新」
- useEffect :浏览器肯定是把第一次的真实已经绘制了,再去渲染第二次真实DOM
- useLayoutEffect :浏览器是把两次真实DOM的渲染,合并在一起渲染的
视图更新的步骤
-
第一步:基于babel-preset-react-app把JSX编译为createElement格式
-
第二步:把createElement执行,创建出virtualDOM
-
第三步:基于root. render方法把virtua LDOM变为真实DOM对象「DOM-DIFF」
-
useLayoutEffect阻塞第四步操作,先去执行Effect链表中的方法「同步操作」
-
useEffect第四步操作和Effect链表中的方法执行,是同时进行的「异步操作」
-
-
第四步:浏览器渲染和绘制真实DOM对象
useRef
函数组件中,可以基于 useRef Hook 函数,创建一个ref对象
- React.createRef 也是创建 ref 对象,既可在类组件中使用,也可以在函数组件中使用
- useRef 只能在函数组件中使用【所有的ReactHook函数,都只能在函数组件中使用,在类组件中使用会报错】
示例
import React from 'react';
import { useState, useEffect } from 'react';
import { Button } from 'antd';
import '../Demo.less';
import { useRef } from 'react';
export default function UseStateDemo() {
const [num, setNum] = useState(0);
let box = useRef(null)
useEffect(() => {
console.log(box.current);
},[]);
return (
<div className="demo">
<span className="num" ref={box}>{num}</span>
<Button type="primary" size="small">
add
</Button>
</div>
);
}
React.createRef在函数组件中依然可以使用!
- createRef 每次渲染都会返回一个新的引用
- 而 useRef 每次都会返回相同的引用
import React from 'react';
import { useState, useEffect } from 'react';
import { Button } from 'antd';
import '../Demo.less';
import { useRef } from 'react';
let prev1, prev2;
export default function UseStateDemo() {
const [num, setNum] = useState(0);
let box1 = useRef(null),
box2 = React.createRef();
if (!prev1) {
prev1 = box1;
prev2 = box2;
} else {
console.log(prev1 === box1); // true useRef再每一次组件更新的时候(函数重新执行),再次执行useRef方法的时候,不会创建新的REF对象了,获取到的还是第一次创建的那个REF对象!
console.log(prev2 === box2); // false createRef在每一次组件更 新的时候,都会创建一个全新的REF对象出来,比较浪费性能!
}
useEffect(() => {
console.log(box1.current);
console.log(box2.current);
}, []);
return (
<div className="demo">
<span className="num" ref={box1}>
{num}
</span>
<span className="num" ref={box2}>
{num + 11111}
</span>
<Button
type="primary"
size="small"
onClick={() => {
setNum(num + 1);
}}
>
add
</Button>
</div>
);
}
useImperativeHandle
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。通常与forwardRef一起使用,暴露之后父组件就可以通过
selectFileModalRef.current?.handleCancel();
来调用子组件的暴露方法
语法
useImperativeHandle(ref, createHandle, [deps])
ref
需要被赋值的ref
对象。createHandle
:createHandle
函数的返回值作为ref.current
的值。[deps]
依赖数组,依赖发生变化会重新执行createHandle
函数。
useImperativeHandle(ref, () => ({
handleShowModal,
handleCancel,
}));
useMemo
在前端开发的过程中,我们需要缓存一些内容,以避免在需渲染过程中因大量不必要的耗时计算而导致的性能问题。为此 React 提供了一些方法可以帮助我们去实现数据的缓存,useMemo 就是其中之一!
let xxx = useMemo( callback, [dependencies])
- 第一次渲染组件的时候,callback会执行
- 后期只有依赖的状态值发生改变, callback才会再执行
- 每一次会把callback执行的返回结果赋值给xxx
- useMemo具 备“计算缓存”,在依赖的状态值没有发生改变,callback没有触发执行的时候,xxx获取的是上- -次计算出来的结果
和Vue中的计算属性非常的类似! !
import React, { useState, useMemo } from 'react';
import { Button } from 'antd';
import './Vote.less';
const Vote = function Vote(props) {
let [surNum, setSurNum] = useState(0);
let [oppNum, setOppNum] = useState(0);
let [otherOption, setOtherOption] = useState(0);
let radio = useMemo(() => {
console.log('OK');
let total = surNum + oppNum,
radio = '--';
if (total > 0) {
radio = ((surNum / total) * 100).toFixed(0) + '%';
return radio
}
}, [surNum, oppNum]);
return (
<div className="vote-box">
<div className="header">
<span className="num">{surNum + oppNum}</span>
</div>
<div className="main">
<p>支持人数:{surNum}人</p>
<p>反对人数:{oppNum}人</p>
<p>支持比率:{radio}</p>
<p>其他操作:{otherOption}</p>
</div>
<div className="footer">
<Button
type="primary"
onClick={() => {
setSurNum(surNum + 1);
}}
>
支持
</Button>
<Button
type="primary"
danger
onClick={() => {
setOppNum(oppNum + 1);
}}
>
反对
</Button>
<Button
type="primary"
onClick={() => {
setOtherOption(otherOption + 1);
}}
>
其他操作
</Button>
</div>
</div>
);
};
export default Vote;
useCallback
const xxx = useCallback(callback,[dependencies])
- 组件第一次渲染,
useCallback
执行,创建一个函数callback
,赋值给xxx
- 组件后续每一次更新,判断依赖的状态值是否改变,如果改变,则重新创建新的函数堆,赋值给
xxx
;如果依赖的状态值没有更新(或者没有设置依赖“[]”)则xxx
获取的一直是第一次创建的函数堆,不会创建新的函数出来! - 或者说,基于
useCallback
,可以始终获取第一次创建函数的堆内存地址(或者说函数的引用)
诉求:当父组件更新的时候,因为传递给子组件的属性仅仅是一个函数「特点:基本应该算是不变的」,所以不想再让子组件也跟着更新了!
- 第一条:传递给子组件的属性(函数),每一次需要是相同的堆内存地址(是一致的) . 基于useCallback处理!
- 第二条:在子组件内部也要做一个处理,验证父组件传递的属性是否发生改变,如果没有变化,则让子组件不能更新,有变化才需要更新;
- 类组件是通过继承React.PureComponent即可「在shouldComponentUpdate中对新老属性做了浅比较」!!
- 函数组件是基于 React.memo 函数,对新老传递的属性做比较,如果不一致,才会把函数组件执行,如果一致,则不让子组件更新!!
子组件为类组件的情况
子组件为函数组件的情况
自定义Hook
自定义 Hook 是一个函数,其名称以 “use
” 开头,函数内部可以调用其他的 hook
与组件中一致,请确保只在 自定义 hook 的顶层无条件地调用其他 hook
自定义 Hooks 的核心是共享组件之间的逻辑。使用自定义 Hooks 能够减少重复的逻辑,更重要的是,自定义 Hooks 内部的代码描述了它们想做什么,而不是如何做。当你将逻辑提取到自定义Hooks
中时,你可以隐藏如何处理某些”外部系统”或浏览器 API 的调用的细节,组件的代码表达的是你的意图,而不是实现细节。