Hooks 就是把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。它在一定程度上更好地体现了 React 的开发思想,即从 State => View 的函数式映射。

useState:让函数具有维持状态的能力。
// 定义一个年龄的 state,初始值是 42
const [age, setAge] = useState(42);
// 定义一个水果的 state,初始值是 banana
const [fruit, setFruit] = useState('banana');
// 定一个一个数组 state,初始值是包含一个 todo 的数组
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
useEffect:执行副作用, 在useEffect中执行的代码是不影响渲染出来的 UI 的。useEffect 是每次组件 render 完后判断依赖并执行的。用法如下
useEffect(callback, dependencies)
useEffect(() => {
// 组件首次渲染时执行,等价于 class 组件中的 componentDidMount
console.log('did mount');
}, [])
// 设置一个 size 的 state 用于保存当前窗口尺寸
const [size, setSize] = useState({});
useEffect(() => {
// 窗口大小变化事件处理函数
const handler = () => {
setSize(getSize());
};
// 监听 resize 事件
window.addEventListener('resize', handler);
// 返回一个 callback 在组件销毁时调用
return () => {
// 移除 resize 事件
window.removeEventListener('resize', handler);
};
}, []);
useEffect(() => { // useEffect 的 callback 要避免直接的 async 函数,需要封装一下
const doAsync = async () => { // 当 id 发生变化时,将当前内容清楚以保持一致性
setBlogContent(null); // 发起请求获取数据
const res = await fetch(`/blog-content/${id}`);
// 将获取的数据放入 state
setBlogContent(await res.text()); };
doAsync();
}, [id]);
useCallBack的本质工作不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址。不论是否使用useCallBack都无法阻止组件render时函数的重新创建!!
每一个被useCallBack的函数都将被加入useCallBack内部的管理队列。而当我们大量使用useCallBack的时候,管理队列中的函数会非常之多,任何一个使用了useCallBack的组件重新渲染的时候都需要去便利useCallBack内部所有被管理的函数找到需要校验依赖是否改变的函数并进行校验。
在以上这个过程中,寻找指定函数需要性能,校验也需要性能。所以,滥用useCallBack不但不能阻止函数重新构建还会增加“寻找指定函数和校验依赖是否改变”这两个功能,为项目增添不必要的负担。
import {useCallBack,memo} from 'react';
/**父组件**/
const Parent = () => {
const [parentState,setParentState] = useState(0); //父组件的state
// 未被useCallback包裹 需要传入子组件的函数
// const toChildFun = () => {
// console.log("需要传入子组件的函数");
// }
// 使用useCallback包裹
const toChildFun = useCallback(() => {
console.log("需要传入子组件的函数");
}, []);
return (<div>
<Button onClick={() => setParentState(val => val+1)}>
点击我改变父组件中与Child组件无关的state
</Button>
//将父组件的函数传入子组件
<Child fun={toChildFun}></Child>
<div>)
}
/**被memo保护的子组件**/
const Child = memo(() => {
console.log("我被打印了就说明子组件重新构建了")
return <div><div>
})
总结
// 子组件
import React from "react";
interface ChildProps {
name: { name: string; color: string };
onClick: Function;
}
const ChildUseMemo = ({ name, onClick }: ChildProps): JSX.Element => {
console.log("子组件?");
return (
<>
<div style={{ color: name.color }}>
我是一个子组件,父级传过来的数据:{name.name}
</div>
<button onClick={onClick.bind(null, "新的子组件name")}>改变name</button>
</>
);
};
// 建议useMemo配合这memo使用
export default React.memo(ChildUseMemo);
import React, { useState, useCallback, useMemo } from "react";
import ChildUseMemo from "./childMemo";
const Father: React.FC = () => {
const [count, setCount] = useState(0);
const [name, setName] = useState("Child组件2");
return (
<div>
<p>父组件:{count}</p>
<ChildUseMemo
// 使用useMemo,返回一个和原本一样的对象,第二个参数是依赖性,当name发生改变的时候,才产生一个新的对象
name={useMemo(
() => ({
name,
color: name.indexOf("name") !== -1 ? "red" : "green"
}),
[name]
)}
// name={{ name, color: name.indexOf("name") !== -1 ? "red" : "green" }}
// 这里使用了useCallback优化了传递给子组件的函数,只初始化一次函数,下次不产生新的函数
onClick={useCallback((newName: string) => setName(newName), [])}
></ChildUseMemo>】
<button onClick={(e)=> {setCount(count + 1)}>+</button>
</div>
);
};
export default Example;
总结
在子组件不需要父组件的值和函数的情况下,只需要使用memo函数包裹子组件即可。而在使用函数的情况,需要考虑有没有函数传递给子组件使用useCallback。而在值有所依赖的项,并且是对象和数组等值的时候而使用useMemo(当返回的是原始数据类型如字符串、数字、布尔值,就不要使用useMemo了)。不要盲目使用这些hooks。
import React, { useState, useCallback, useRef } from "react";
export default function Timer() {
// 定义 time state 用于保存计时的累积时间
const [time, setTime] = useState(0);
// 定义 timer 这样一个容器用于在跨组件渲染之间保存一个变量
const timer = useRef(null);
// 开始计时的事件处理函数
const handleStart = useCallback(() => {
// 使用 current 属性设置 ref 的值
timer.current = window.setInterval(() => {
setTime((time) => time + 1);
}, 100);
}, []);
// 暂停计时的事件处理函数
const handlePause = useCallback(() => {
// 使用 clearInterval 来停止计时
window.clearInterval(timer.current);
timer.current = null;
}, []);
return (
<div>
{time / 10} seconds.
<br />
<button onClick={handleStart}>Start</button>
<button onClick={handlePause}>Pause</button>
</div>
);
}
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// current 属性指向了真实的 input 这个 DOM 节点,从而可以调用 focus 方法
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
优点:
1. 提供了一个方便在多个组件之间共享数据的机制。context能够进行数据绑定,当Context的数据发生变化的时候,使用这个数据的组件就能够自动刷新。
缺点:
1. 会让调试变的困难,因为你很难跟踪某个 Context 的变化究竟是如何产生的。
2. 让组件的复用变得困难,因为一个组件如果使用了某个 Context,它就必须确保被用到的地方一定有这个 Context 的 Provider 在其父组件的路径上。
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
// 创建一个 Theme 的 Context
// ThemeContext拥有一个Provider属性
const ThemeContext = React.createContext(themes.light);
function App() {
// 使用 state 来保存 theme 从而可以动态修改
const [theme, setTheme] = useState("light");
// 切换 theme 的回调函数,当state的值修改后,动态的切换Context的值,当用到这个context的地方都会自动刷新
const toggleTheme = useCallback(() => {
setTheme((theme) => (theme === "light" ? "dark" : "light"));
}, []);
// 整个应用使用 ThemeContext.Provider 作为根组件
return (
// 使用 themes.dark 作为当前 Context
<ThemeContext.Provider value={themes.dark}>
<button onClick={toggleTheme}>Toggle Theme</button>
<Toolbar />
</ThemeContext.Provider>
);
}
// 在 Toolbar 组件中使用一个会使用 Theme 的 Button
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
// 在 Theme Button 中使用 useContext 来获取当前的主题
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{
background: theme.background,
color: theme.foreground
}}>
I am styled by theme context!
</button>
);
}
只能在函数组件的顶级作用域使用;
Hooks不能在循环、条件判断或者嵌套函数内执行,而必须在顶层。同时Hooks在组件多次渲染之间,必须按顺序被执行。
只能在函数组件或其他Hooks中使用。如果一定要在Class组件中使用函数式组件,可以利用高级组件的模式,将Hooks封装成高阶组件,从而让类组件使用
import React from 'react';
import { useWindowSize } from '../hooks/useWindowSize';
export const withWindowSize = (Comp) => {
return props => {
const windowSize = useWindowSize();
return <Comp windowSize={windowSize} {...props} />;
};
};
import React from 'react';
import { withWindowSize } from './withWindowSize';
class MyComp {
render() {
const { windowSize } = this.props;
// ...
}
}
// 通过 withWindowSize 高阶组件给 MyComp 添加 windowSize 属性
export default withWindowSize(MyComp);
优点:
定义:
声明一个名字以use开头的函数。例如你创建了一个usexxx的函数,但是内部没有使用任何其他的hooks,那么这个函数就是一个普通函数,如果里面用了其他hooks,那么它就是一个hook
// useCount.js
import { useState, useCallback }from 'react';
export default function useCounter() {
// 定义 count 这个 state 用于保存当前数值
const [count, setCount] = useState(0);
// 实现加 1 的操作
const increment = useCallback(() => setCount(count + 1), [count]);
// 实现减 1 的操作
const decrement = useCallback(() => setCount(count - 1), [count]);
// 重置计数器
const reset = useCallback(() => setCount(0), []);
// 将业务逻辑的操作 export 出去供调用者使用
return { count, increment, decrement, reset };
}
// index.js
import React from 'react';
import useCounter from './useCount.js'
function Counter() {
// 调用自定义 Hook
const { count, increment, decrement, reset } = useCounter();
// 渲染 UI
return (
<div>
<button onClick={decrement}> - </button>
<p>{count}</p>
<button onClick={increment}> + </button>
<button onClick={reset}> reset </button>
</div>
);
}
把条件判断的结果放到两个组件之中,确保真正 render UI 的组件收到的所有属性都是有值的。
// 定义一个容器组件用于封装真正的 UserInfoModal
export default function UserInfoModalWrapper({
visible,
...rest, // 使用 rest 获取除了 visible 之外的属性
}) {
// 如果对话框不显示,则不 render 任何内容
if (!visible) return null;
// 否则真正执行对话框的组件逻辑
return <UserInfoModal visible {...rest} />;
}
render props 就是把一个 render 函数作为属性传递给某个组件,由这个组件去执行这个函数从而 render 实际的内容。
// counterRender.jsx
import { useState, useCallback } from "react";
function CounterRenderProps({ children }) {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
const decrement = useCallback(() => {
setCount(count - 1);
}, [count]);
return children({ count, increment, decrement });
}
// counterReanderExample.jsx
function CounterRenderPropsExample() {
return (
<CounterRenderProps>
{({ count, increment, decrement }) => {
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}}
</CounterRenderProps>
);
}
由于虚拟 DOM 的存在,在 React 中即使绑定一个事件到原生的 DOM 节点,事件也并不是绑定在对应的节点上,而是所有的事件都是绑定在根节点上。然后由 React 统一监听和管理,获取事件后再分发到具体的虚拟 DOM 节点上。
在 React 17 之前,所有的事件都是绑定在 document 上的,而从 React 17 开始,所有的事件都绑定在整个 App 上的根节点上,这主要是为了以后页面上可能存在多版本 React 的考虑。
具体来说,React 这么做的原因主要有两个。
第一,虚拟 DOM render 的时候, DOM 很可能还没有真实地 render 到页面上,所以无法绑定事件。
第二,React 可以屏蔽底层事件的细节,避免浏览器的兼容性问题。同时呢,对于 React Native 这种不是通过浏览器 render 的运行时,也能提供一致的 API。这里有一点我要多解释下。
那就是为什么事件绑定在某个根节点上,也能触发实际 DOM 节点的事件。我们知道,在浏览器的原生机制中,事件会从被触发的节点往父节点冒泡,然后沿着整个路径一直到根节点,所以根节点其实是可以收到所有的事件的。这也称之为浏览器事件的冒泡模型。