react版本:16.13.1

1. 概览

基础 Hook

  • useState
  • useEffect
  • useContext

额外的 Hook

  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue

2. 基础 Hook

useState

const [state, setState] = useState(initialState);

setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。

setState(newState);setState((state) => {return {counter: state.counter};
});

在后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。

如果你的更新函数返回值与当前 state 完全相同(如果是对象则比较引用地址是否相同,不相同直接替换,而不是class类中setState的合并),则随后的重渲染会被完全跳过。

//class用法
this.setState({ count: count },function(){console.log(this.state.count)//改变后的值
});this.setState((state, props) => {return {counter: state.counter + props.step};
});

惰性初始 state

initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:

const [state, setState] = useState(() => {const initialState = someExpensiveComputation(props);return initialState;
});

跳过 state 更新

调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。

Object.is(value1, value2);

Object.is() 判断两个值是否相同。如果下列任何一项成立,则两个值相同:
-两个值都是 undefined
两个值都是 null
两个值都是 true 或者都是 false
两个值是由相同个数的字符按照相同的顺序组成的字符串
两个值指向同一个对象
两个值都是数字并且

  • 都是正零 +0
  • 都是负零 -0
  • 都是 NaN
  • 都是除零和 NaN 外的其它同一个数字

Object.is 不会做类型转换。这与 === 运算符的判定方式也不一样。=== 运算符(和== 运算符)将数字值 -0 和 +0 视为相等,并认为 Number.NaN 不等于 NaN。

需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

useEffect

在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。

使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。
默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。

组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回一个清除函数。

useEffect(() => {const subscription = props.source.subscribe();return () => {// 清除订阅subscription.unsubscribe();};
});

为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除。

与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。

然而,并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。

虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。

默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。
但有时候不需要,要实现这一点,可以给 useEffect 传递第二个参数,它是 effect 所依赖的值数组。更新后的示例如下:

useEffect(() => {const subscription = props.source.subscribe();return () => {subscription.unsubscribe();};},[props.source],
);

此时,只有当 props.source 改变后才会重新创建订阅。
如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。

如果你传入了一个空数组([]),effect 内部的 props 和 state 就会一直持有其初始值。所以它永远都不需要重复执行。

依赖项数组不会作为参数传给 effect 函数。虽然从概念上来说它表现为:所有 effect 函数中引用的值都应该出现在依赖项数组中。

useContext

const value = useContext(MyContext);

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memo 或 shouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化。

useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。

useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

const themes = {light: {foreground: "#000000",background: "#eeeeee"},dark: {foreground: "#ffffff",background: "#222222"}
};const ThemeContext = React.createContext(themes.light);function App() {return (<ThemeContext.Provider value={themes.dark}><Toolbar /></ThemeContext.Provider>);
}function Toolbar(props) {return (<div><ThemedButton /></div>);
}function ThemedButton() {const theme = useContext(ThemeContext);return (<button style={{ background: theme.background, color: theme.foreground }}>I am styled by theme context!</button>);
}

3. 额外的Hook

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch。

const initialState = {count: 0};function reducer(state, action) {switch (action.type) {case 'increment':return {count: state.count + 1};case 'decrement':return {count: state.count - 1};default:throw new Error();}
}function Counter() {const [state, dispatch] = useReducer(reducer, initialState);return (<>Count: {state.count}<button onClick={() => dispatch({type: 'decrement'})}>-</button><button onClick={() => dispatch({type: 'increment'})}>+</button></>);
}

有两种不同初始化 useReducer state 的方式:

  1. 将初始 state 作为第二个参数传入 useReducer 是最简单的方法:
  const [state, dispatch] = useReducer(reducer,{count: initialCount});
  1. 你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)。这么做可以将用于计算 state 的逻辑提取到 reducer 外部。

如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。)

需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

useCallback

const memoizedCallback = useCallback(() => {doSomething(a, b);},[a, b],
);

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值

你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。

useRef

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

function TextInputWithFocusButton() {const inputEl = useRef(null);const onButtonClick = () => {// `current` 指向已挂载到 DOM 上的文本输入元素inputEl.current.focus();};return (<><input ref={inputEl} type="text" /><button onClick={onButtonClick}>Focus the input</button></>);
}

本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。

你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以 <div ref={myRef} /> 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。

然而,useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: …} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:

function FancyInput(props, ref) {const inputRef = useRef();useImperativeHandle(ref, () => ({focus: () => {inputRef.current.focus();}}));return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

在本例中,渲染 <FancyInput ref={inputRef} /> 的父组件可以调用 inputRef.current.focus()。

const FancyButton = React.forwardRef((props, ref) => (<button ref={ref} className="FancyButton">{props.children}</button>
));// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

在上述的示例中,React 会将 <FancyButton ref={ref}> 元素的 ref 作为第二个参数传递给 React.forwardRef 函数中的渲染函数。该渲染函数会将 ref 传递给<button ref={ref}> 元素。

因此,当 React 附加了 ref 属性之后,ref.current 将直接指向 <button> DOM 元素实例。

useLayoutEffect

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

尽可能使用标准的 useEffect 以避免阻塞视觉更新。

useDebugValue

useDebugValue(value)

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签

例如,“自定义 Hook” 章节中描述的名为 useFriendStatus 的自定义 Hook:

function useFriendStatus(friendID) {const [isOnline, setIsOnline] = useState(null);// ...// 在开发者工具中的这个 Hook 旁边显示标签// e.g. "FriendStatus: Online"useDebugValue(isOnline ? 'Online' : 'Offline');return isOnline;
}

在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。

因此,useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。

useDebugValue(date, date => date.toDateString());

3. Hook常见问题

从 Class 迁移到 Hook

1. Hook 对于 Redux connect() 和 React Router 等流行的 API 来说,意味着什么?

React Redux 从 v7.1.0 开始支持 Hook API 并暴露了 useDispatch 和 useSelector 等 hook。

React Router 从 v5.1 开始支持 hook。

2. 生命周期方法要如何对应到 Hook?

getSnapshotBeforeUpdate,componentDidCatch 以及 getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会被添加。

3. 我可以只在更新时运行 effect 吗?

这是个比较罕见的使用场景。如果你需要的话,你可以 使用一个可变的 ref 手动存储一个布尔值来表示是首次渲染还是后续渲染,然后在你的 effect 中检查这个标识。(如果你发现自己经常在这么做,你可以为之创建一个自定义 Hook。)

function Timer() {const intervalRef = useRef();useEffect(() => {const id = setInterval(() => {// ...});intervalRef.current = id;return () => {clearInterval(intervalRef.current);};});// ...
}

4. 如何获取上一轮的 props 或 state?

目前,你可以 通过 ref 来手动实现:

function Counter() {const [count, setCount] = useState(0);const prevCountRef = useRef();useEffect(() => {prevCountRef.current = count;});const prevCount = prevCountRef.current;return <h1>Now: {count}, before: {prevCount}</h1>;
}

这或许有一点错综复杂,但你可以把它抽取成一个自定义 Hook:

function Counter() {const [count, setCount] = useState(0);const prevCount = usePrevious(count);return <h1>Now: {count}, before: {prevCount}</h1>;
}function usePrevious(value) {const ref = useRef();useEffect(() => {ref.current = value;});return ref.current;
}

5. 为什么我会在我的函数中看到陈旧的 props 和 state ?

组件内部的任何函数,包括事件处理函数和 effect,都是从它被创建的那次渲染中被「看到」的。例如,考虑这样的代码:

function Example() {const [count, setCount] = useState(0);function handleAlertClick() {setTimeout(() => {alert('You clicked on: ' + count);}, 3000);}return (<div><p>You clicked {count} times</p><button onClick={() => setCount(count + 1)}>Click me</button><button onClick={handleAlertClick}>Show alert</button></div>);
}

如果你先点击「Show alert」然后增加计数器的计数,那这个 alert 会显示在你点击『Show alert』按钮时的 count 变量。这避免了那些因为假设 props 和 state 没有改变的代码引起问题。

如果你刻意地想要从某些异步回调中读取 最新的 state,你可以用 一个 ref 来保存它,修改它,并从中读取。

6. 我该如何实现 getDerivedStateFromProps?

尽管你可能 不需要它,但在一些罕见的你需要用到的场景下,你可以在渲染过程中更新 stateReact 会立即退出第一次渲染并用更新后的 state 重新运行组件以避免耗费太多性能。

这里我们把 row prop 上一轮的值存在一个 state 变量中以便比较:

function ScrollView({row}) {const [isScrollingDown, setIsScrollingDown] = useState(false);const [prevRow, setPrevRow] = useState(null);if (row !== prevRow) {// Row 自上次渲染以来发生过改变。更新 isScrollingDown。setIsScrollingDown(prevRow !== null && row > prevRow);setPrevRow(row);}return `Scrolling down: ${isScrollingDown}`;
}

初看这或许有点奇怪,但渲染期间的一次更新恰恰就是 getDerivedStateFromProps 一直以来的概念。

7. 有类似 forceUpdate 的东西吗?

如果前后两次的值相同,useState 和 useReducer Hook 都会放弃更新。原地修改 state 并调用 setState 不会引起重新渲染。

通常,你不应该在 React 中修改本地 state。然而,作为一条出路,你可以用一个增长的计数器来在 state 没变的时候依然强制一次重新渲染:

  const [ignored, forceUpdate] = useReducer(x => x + 1, 0);function handleClick() {forceUpdate();}

可能的话尽量避免这种模式。

8. 我该如何测量 DOM 节点?

获取 DOM 节点的位置或是大小的基本方式是使用 callback ref。每当 ref 被附加到一个另一个节点,React 就会调用 callback。这里有一个 小 demo:

function MeasureExample() {const [height, setHeight] = useState(0);const measuredRef = useCallback(node => {if (node !== null) {setHeight(node.getBoundingClientRect().height);}}, []);return (<><h1 ref={measuredRef}>Hello, world</h1><h2>The above header is {Math.round(height)}px tall</h2></>);
}

在这个案例中,我们没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的 变化 通知到我们。

如果你愿意,你可以 把这个逻辑抽取出来作为 一个可复用的 Hook:

function MeasureExample() {const [rect, ref] = useClientRect();return (<><h1 ref={ref}>Hello, world</h1>{rect !== null &&<h2>The above header is {Math.round(rect.height)}px tall</h2>}</>);
}function useClientRect() {const [rect, setRect] = useState(null);const ref = useCallback(node => {if (node !== null) {setRect(node.getBoundingClientRect());}}, []);return [rect, ref];
}

性能优化

1. 我可以在更新时跳过 effect 吗?

可以的。

2. 如果处于某些原因你 无法 把一个函数移动到 effect 内部,还有一些其他办法:

只有 当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。

你可以尝试把那个函数移动到你的组件之外。那样一来,这个函数就肯定不会依赖任何 props 或 state,并且也不用出现在依赖列表中了。

如果你所调用的方法是一个纯计算,并且可以在渲染时调用,你可以 转而在 effect 之外调用它, 并让 effect 依赖于它的返回值。

万不得已的情况下,你可以 把函数加入 effect 的依赖但 把它的定义包裹 进 useCallback Hook。这就确保了它不随渲染而改变,除非 它自身 的依赖发生了改变:

function ProductPage({ productId }) {// ✅ 用 useCallback 包裹以避免随渲染发生改变const fetchProduct = useCallback(() => {// ... Does something with productId ...}, [productId]); // ✅ useCallback 的所有依赖都被指定了return <ProductDetails fetchProduct={fetchProduct} />;
}function ProductDetails({ fetchProduct }) {useEffect(() => {fetchProduct();}, [fetchProduct]); // ✅ useEffect 的所有依赖都被指定了// ...
}

注意在上面的案例中,我们 需要 让函数出现在依赖列表中。这确保了 ProductPage 的 productId prop 的变化会自动触发 ProductDetails 的重新获取。

3. 如果我的 effect 的依赖频繁变化,我该怎么办?

有时候,你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state,但这通常会引起 Bug:

function Counter() {const [count, setCount] = useState(0);useEffect(() => {const id = setInterval(() => {setCount(count + 1); // 这个 effect 依赖于 `count` state}, 1000);return () => clearInterval(id);}, []); // 												

React官网Hook API 索引模块知识点整理(五)相关推荐

  1. React 官网为什么那么快?

    当我们打开 React 官网时,会发现从浏览器上输入url 到页面首屏完全展示这一过程所花的时间极短,而且在页面中点击链接切换路由的操作非常顺滑,几乎页面可以达到"秒切"的效果,根 ...

  2. React官网的井字棋游戏

    React官网的井字棋游戏 这个是我在官网再次复习这个小游戏时梳理的一些思路,其中也包含了我在尝试时出的一些bug 文章目录 React官网的井字棋游戏 1.整体分析项目 2.为变量添加state并绑 ...

  3. React官网API模块知识点整理(三)

    react版本:16.13.1 1.React 顶层 API React.Component React.Component 是使用 ES6 classes 方式定义 React 组件的基类 Reac ...

  4. React 官网例子实现一个列表

    官网参考地址:https://reactjs.org/docs/thinking-in-react.html 学习了react的quicksart部分之后跟着官网的文档进行一次react的开发探索. ...

  5. 利用React官网动画库(react-transition-group)实现过渡动画

    官方提供了 react-transition-group 库来实现过渡动画,与vue官方提供的 transition 很类似,利用它实现过渡动画. 一.安装 npm : npm install rea ...

  6. 关于removebg官网与API

    官网网址:在线抠图软件_图片去除背景 | remove.bg – remove.bg remove.bg在线抠图软件轻松实现一键抠图,只需上传图片,无需其他操作,即可100%自动去除图片背景.http ...

  7. React 官网入门教程 - 井字棋小游戏

    刚刚开始学习 React,跟着官网的小教程做了一遍,还做了一些代码的精简和修改 官网教程地址:点击跳转到官网 最终效果: import React from 'react' import ReactD ...

  8. 优傲机器人 官网CB3 在线学习 模块3-设置工具

    优傲机器人 官网提供的CB3 在线学习 模块3-设置工具     练习1       练习2    求工具中心点的确切位置 工具中心点在法兰中心正下方(Z轴方向) 工具中心点向左向右(X轴方向) 工具 ...

  9. React官网核心概念模块知识点整理(一)

    react版本:16.13.1 React 会将以小写字母开头的组件视为原生 DOM 标签,所以自定义组件名称规范是大写字母开头. JSX 里的 class 变成了 className JSX 防止注 ...

最新文章

  1. Centos6.5子域名绑定子目录
  2. 【心路历程】你是这样的“夜间工程师”吗?
  3. 1109: 胥哥的DOTA-水题(直接做,时间也不超限)
  4. 2019牛客暑期多校训练营(第六场)H - Train Driver (最短路)
  5. 【excrt】屠龙勇士(luogu 4774)
  6. 复练-关于面试的科技树-V1004-求职面试的阶段和策略
  7. WCP源码分析 与SpringMVC学习资料
  8. 微软职位内部推荐-Software Engineer II-Senior Software Engineer for Satori
  9. 前端培训丁鹿学堂:nodeJS入门指南(一)
  10. 第25版 OpenStack Yoga 已发布:稳定性与创新并重
  11. python学习教程34-Excel生成折线图
  12. HTML中基于表单的文件上传(post,get)
  13. 上传ipa到appstore的步骤说明​
  14. usbip--局域网内共享的USB设备
  15. 电影《美丽心灵》中纳什的经典演讲(双语)
  16. 通用样式 -表格的每行的复选框选中打印
  17. 执行matlab 部分程序
  18. Docker02 狂神Docker视频学习笔记 :【狂神说Java 哔哩哔哩】Docker最新超详细版教程通俗易懂
  19. 为什么java缩进不能tab_为什么代码缩进时必须要用 Tab 而不能用空格
  20. NUIST 9th 校赛 P4

热门文章

  1. vscode下载慢解决办法
  2. python建立空矩阵_创建空矩阵Python
  3. 腾讯前端组件android,前端之Android入门(2):程序目录及UI简介 - 腾讯ISUX
  4. php 递归递实现无限层级
  5. 微信公众号的7个未来
  6. 基于Python的换脸应用
  7. 【运筹学】线性规划 最优解分析 ( 唯一最优解 | 无穷多最优解 | 无界解 | 无可行解 | 迭代范围 | 求解步骤 )
  8. 2022年自考专业(公关关系)传播学概论练习题
  9. 705.设计哈希集合
  10. Mldonkey的配置