一、useState和useEffect
1. 什么是Hook
Hook 是一个特殊的函数,它可以让你“钩⼊” React 的特性。例如,useState 是允许你在 React 函数组件中添加state的Hook。
什么时候我会用 Hook? 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其转化为 class组件。现在你可以在现有的函数组件中使⽤Hook。
import React, { useState } from 'react'
export default function HookPage(props) {
// 声明一个叫“count”的state变量,初始化为0
const [count, setCount] = useState(0)
return (
<div>
<h3>HookPage</h3>
<p>{count}</p>
<button onClick={() => setCount(count+1)}>add</button>
</div>
)
}
2. 什么是副作用
Effect Hook 可以让你在函数组件中执行副作用操作。
数据获取,设置订阅以及⼿动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作或是“副作⽤”这个名字,应该都在组件中使⽤过它们。
import React, { useState, useEffect } from 'react'
export default function HookPage(props) {
// 声明一个叫“count”的state变量,初始化为0
const [count, setCount] = useState(0)
// useEffect相当于componentDidMount、componentDidUpdate、componentWillUnmount的集合
useEffect(() => {
console.log("effect count");
// 只需要在count发生改变的时候执行
document.title = `点击了${count}次`
}, [count]);
return (
<div>
<h3>HookPage</h3>
<p>{count}</p>
<button onClick={() => setCount(count+1)}>add</button>
</div>
)
}
在函数组件主体内(这⾥指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产⽣莫名其妙的 bug 并破坏 UI 的⼀致性。
使用 useEffect 完成副作⽤操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执⾏
effect 的条件执⾏
默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发⽣变化,它就会被重新创建。
如果我们不需要在每次组件更新时创建新的订阅,⽽是仅需要在 source props 改变时重新创建。 要实现这⼀点,可以给 useEffect 传递第⼆个参数,它是 effect 所依赖的值数组。更新后的示例如下:
import React, { useState, useEffect } from 'react'
export default function HookPage(props) {
// 声明一个叫“count”的state变量,初始化为0
const [count, setCount] = useState(0)
const [date, setDate] = useState(new Date())
// useEffect相当于componentDidMount、componentDidUpdate、componentWillUnmount的集合
useEffect(() => {
console.log("effect count");
// 只需要在count发生改变的时候执行
document.title = `点击了${count}次`
}, [count]);
useEffect(() => {
console.log("effect date");
// 只需要在didMount的时候执行
const timer = setInterval(() => {
setDate(new Date())
}, 1000);
// 清除定时器,类似willUnmount
return () => clearInterval(timer)
}, []);
return (
<div>
<h3>HookPage</h3>
<p>{count}</p>
<button onClick={() => setCount(count+1)}>add</button>
<p>{date.toLocaleTimeString()}</p>
</div>
)
}
此时,只有当 useEffect第二个参数数组里的数值改变后才会重新创建订阅。
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect 函数需返回⼀个清除函数,以防止内存泄漏,清除函数会在组件卸载前执行。
二、自定义Hook
有时候我们会想要在组件之间重⽤一些状态逻辑。目前为止,有两种主流方案来解决这个问题:⾼阶组件和 render props。自定义 Hook 可以让你在不增加组件的情况下达到同样的⽬的。
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调⽤其他的 Hook。
import React, { useState, useEffect } from 'react'
export default function HookPage(props) {
const [count, setCount] = useState(0)
// 错误写法
// if(count) {
// const [num, setNum] = useState(0)
// }
useEffect(() => {
console.log("effect count");
// 只需要在count发生改变的时候执行
document.title = `点击了${count}次`
}, [count]);
return (
<div>
<h3>HookPage</h3>
<p>{count}</p>
<button onClick={() => setCount(count+1)}>add</button>
<p>{useClock().toLocaleTimeString()}</p>
</div>
)
}
// 错误写法
// function getNum() {
// const [num, setNum] = useState(0)
// return num;
// }
// 自定义个hook,命名要以use开头
function useClock() {
const [date, setDate] = useState(new Date())
useEffect(() => {
console.log("effect date");
// 只需要在didMount的时候执行
const timer = setInterval(() => {
setDate(new Date())
}, 1000);
// 清除定时器,类似willUnmount
return () => clearInterval(timer)
}, []);
return date
}
Hook 使⽤规则
Hook 就是 JavaScript 函数,但是使⽤它们会有两个额外的规则:
只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
只能在React 的函数组件中调用 Hook,不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是⾃定义的 Hook 中)
三、useMemo和useCallback
1. useMemo
把 “创建”函数 和 依赖项数组 作为参数传入 useMemo ,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进⾏⾼开销的计算。
import React, { useState, useMemo } from 'react'
export default function UseMemoPage(props) {
const [count, setCount] = useState(0);
const [value, setValue] = useState("");
// 当前计算只和count有关
const expensive = useMemo(() => {
console.log("compute")
let sum = 0;
for(let i = 0; i < count; i++) {
sum += i;
}
return sum;
// 只有count改变的时候,当前函数才会重新执行
}, [count]);
return (
<div>
<h3>UseMemoPage</h3>
<p>count: {count}</p>
<p>expensive: {expensive}</p>
<button onClick={() => setCount(count+1)}>add</button>
<input value={value} onChange={(e) => setValue(e.target.value)} />
</div>
)
}
以上代码执行结果分析:
1.点击add按钮的时候,count的值会增加,会重新渲染页面;
2.改变输入框里面的内容,也会改变value的值,也会重新渲染页面;
3.但是由于加了 useMemo ,所以只依赖于 count 值的变化,不依赖 value 的值变化;
4. count值的变化的时候,会打印compute;
5. value值的变化的时候,不会打印compute;
2. useCallback
把 内联回调函数 及 依赖项数组 作为参数传⼊ useCallback ,它将返回该回调函数的 memoized 版本, 该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给,经过优化的,并使用引用相等性去避免⾮必要渲染(例如 shouldComponentUpdate )的⼦组件时,它将⾮常有用。
import React, { useState, useCallback, PureComponent } from 'react'
export default function UseCallBackPage(props) {
const [count, setCount] = useState(0);
const [value, setValue] = useState("");
const addClick = useCallback(() => {
let sum = 0;
for(let i = 0; i < count; i++) {
sum += i;
}
return sum;
}, [count]);
return (
<div>
<h3>UseCallBackPage</h3>
<p>count: {count}</p>
<button onClick={() => setCount(count+1)}>add</button>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<Child addClick={addClick} />
</div>
)
}
class Child extends PureComponent {
render() {
console.log("child render");
const { addClick } = this.props;
return (
<div>
<h3>Child</h3>
<button onClick={() => console.log(addClick())}>modify</button>
</div>
)
}
}
以上代码执行结果分析:
1.点击add按钮的时候,count的值会增加,会重新渲染页面;
2.改变输入框里面的内容,也会改变value的值,也会重新渲染页面;
3.但是由于加了 useCallback ,所以只依赖于 count 值的变化,不依赖 value 的值变化;
4. count值的变化的时候,会打印 child render;
5. value值的变化的时候,不会打印 child render;
6.点击modify按钮的时候,打印返回的sum值,不会重新渲染页面
注意,子组件Child一定要继承纯组件PureComponent,而不是Component,否则即使加了useCallback,当count值和value值发生变化时,还是会重新渲染页面。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps) 。
注意
依赖项数组不会作为参数传给“创建”函数。虽然从概念上来说它表现为:所有“创建”函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。