大家好,很高兴又见面了,我是"高级前端?进阶?",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。
从 16.8.0 开始,React 包含了 React Hooks 的稳定实现,包括以下环境:
- React DOM
- React Native
- React DOM Server
- React Test Renderer
- React Shallow Renderer
但是请注意,要启用 Hooks,所有 React 包都需要为 16.8.0 或更高版本,否则 React DOM,Hooks 将无法工作。下面是 React Hooks 学习必须掌握的一些核心知识点。
1.hooks 本质是闭包
不管经过多少时间,外部变量变化了多少次,闭包的本质是只跟创建闭包时值的状态有关,即数据 “捕获”。
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1)
}, 1000)
// 否则这里一直执行,但是 setCount 一直为 1
return () => clearInterval(id)
}, [])
第一种方法是通过将变量 count 放在依赖数组中重新创建一个闭包函数重新渲染:
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1)
}, 1000)
// 每次 count 变化都会重新执行 useEffect 函数逻辑
return () => clearInterval(id)
}, [count])
但是,问题更明显,即 setInterval 每次都会重新清除然后重新创建,最终导致性能问题。因此,第二种方式是采用将 count 从依赖数组中清除,然后借助于函数更新模式(functional updater form)state。
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
可以将 setCount(c => c + 1) 视为向 React“发送指令”,了解 state 应如何改变, 这种函数更新模式在批量进行多个更新时特别有用,而且也确实没有依赖 count 的值。
同时,如果更新函数返回与当前状态完全相同的值,则将完全跳过后续重新渲染。与类组件中的 setState 方法不同,useState 不会自动合并更新对象,开发者可以通过将函数更新形式与对象扩展语法相结合来实现此行为:
// 另一个选项是 useReducer,其更适合管理包含多个子值的状态对象。
const [state, setState] = useState({});
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
当然,最后一种防止 setInterval 重新创建的方法就是下面介绍的 useReducer,其相当于函数更新模式的一种替代方案。
2. React 中的副作用很重要
函数组件预期为一个纯函数,即组件的渲染不依赖外部,也不影响外部,纯看输入参数。
但如果这样,组件就很难动起来,因为相同输入相同输出,则永远是一个样子。但大部分组件都是动态的,根据 state 会发生变化,又或者要修改外部,例如:发起请求等等。这意味着组件不能只是纯函数,即需要引入副作用。
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1);
// 每秒更新 counter 的值
}, 1000)
return () => clearInterval(intervalId);
}, [count]);
// ... but specifying `count` as a dependency always resets the interval.
副作用 (注意:更新状态不是副作用,state 数据 React 数据流) 表示在典型的 React 渲染过程之外发生的操作或交互, 脱离 Props、State 等 React 单向数据流模型。主要包括:
- 发出 API 请求以获取数据
- 直接更新 DOM
- 设置和管理 setTimeout 或 setInterval
- 订阅外部数据源,例如 WebSocket 连接
这些操作通常涉及异步或与浏览器、外部服务交互,对于现代 Web 应用程序中的诸多功能至关重要,如果管理不当则会带来复杂性和潜在问题。
而 useEffect hooks 专门负责副作用的执行。因为组件的副作用往往与 state 挂钩,根据 state 的变化决定是否要发起副作用,所以可与 useEffect 配合。
3. 生命周期和时间线 -> 以状态为中心
使用 Hooks 模式进行编程时,需要忘记生命周期和时间线的概念,使用以状态为中心,以及对应状态发生变化时,哪些副作用需要重新执行的思想来进行编码。
- Each Render Has Its Own Props and State
- Each Render Has Its Own Event Handlers
- Each Render Has Its Own Effects
- Each Render Has Its Own… Everything
比如下面的例子,如果首先单击 “Show alert”,然后点击计数器,则 alert 将显示单击 “Show alert” 按钮时的 count 变量,从而防止假设 props 和 state 不变的情况下代码引起的错误。
function Example() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on:' + count);
}, 3000);
}
return (
<>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</>
);
}
而如果想从某个异步回调中读取最新状态,可以将其保留在引用中,对其进行修改,然后读取。
4.React 仅在让浏览器绘制(Paint)后才运行清理 Effect
React 仅在浏览器 Paint 后才运行清理 Effect 从而使应用程序速度更快,因为大多数 Effect 不需要阻止屏幕更新,Effect 清理也会延迟。使用新的 Props 重新渲染后,之前的 Effect 被清除:
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
Props 从 {id: 10} 切换到 {id: 20} 后将发生如下流程:
- React 渲染 {id: 20} 的 UI
- 浏览器绘制,即在屏幕上看到 {id: 20} 的 UI
- React 清理 {id: 10} 的 Effect (因为闭包一直持有)
- React 运行 {id: 20} 的 Effect
5. 从函数式更新尽快转移到 useReducer
// 函数式更新
setCount(c => c + 1);
函数式更新有助于仅将 Effect 内部最少的必要信息发送到组件中,即 setCount(c => c + 1) 更新程序形式传递的信息远少于 setCount(count + 1),因为其没有被 count 数 “污染”,而仅仅只表达动作(“递增”)。
和 React 中的理念涉及寻找最小 state 是相同的原理,只是用于更新。
而且函数式更新看起来有点奇怪,能做的事情也非常有限, 例如:当设置一个状态变量取决于另一个状态变量的当前值。比如下面的例子:
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>{count}</h1>
<input value={step} onChange={e => setStep(Number(e.target.value))} />
</>
);
}
假设以上代码希望 setInterval 不会在 step 发生更改时重置,那么如何从 Effect 中删除 step 依赖性?
重要声明:当设置一个 state 变量依赖于另一个 state 变量的当前值时,可以尝试用 useReducer 替换,其是函数式更新的姐妹模式。下面代码将 step 依赖项换成 dispatch 依赖项:
const [state, dispatch] = useReducer(reducer, initialState);
const {count, step} = state;
useEffect(() => {
const id = setInterval(() => {
dispatch({type: 'tick'});
// 替换掉 setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
// 注意:可以省略 deps 中的 dispatch、setState 和 useRef
// React 可保证其是静态的,但指定也没有坏处
而这么做的好处是 React 保证 dispatch 函数在整个组件生命周期中保持不变,所以上面的例子不需要重新订阅 step。
以上代码不是读取 Effect 内的 state,而是 dispatche 一个对有关所发生事件的信息进行编码的 action,从而使 Effect 能够与 step 状态保持解耦。 即 Effect 并不关心如何更新 state,它只是告诉开发者发生了什么,而 reducer 中内置了更新逻辑:
onst initialState = {
count: 0,
step: 1,
};
function reducer(state, action) {
const {count, step} = state;
if (action.type === 'tick') {
return {count: count + step, step};
} else if (action.type === 'step') {
return {count, step: action.step};
} else {
throw new Error();
}
}
更多关于 Hooks 的知识点会在下一篇文章继续更新,欢迎大家持续关注。
参考资料
https://overreacted.io/a-complete-guide-to-useeffect/
https://www.robinwieruch.de/react-hooks-fetch-data/