state的时效性
state
在react
中的含义是“记忆”,或可以理解为“状态”,但是不是即时状态
触发click
,执行的步骤是
setIndex
把react
内部的index
改为新值- 打印原本的
index
setIndex
触发react
计算,重新渲染List
组件- 渲染时重新调用,打印
index
所以一般情况下,可以想象成react
在函数内部跑了一个tempIndex = index
,handleClick
里console.log(index)
实际上是conosle.log(tempIndex)
触发页面渲染需要调用setState
和Vue
不同,react
并不是响应式的,在下面的例子中,如果不调用setObj
,则不会触发list
的重新渲染
react
称,不做成响应式
useState修改对象
react
不是响应式,所以他不会监听直接对对象、数组的修改,通常都需要整个替换来实现,官方建议使用useImmer
来简化代码,如:
import { useImmer } from 'use-immer';
export default function List() {
const [obj, setObj] = useImmer({ num: 1 });
const [arr, setArr] = useImmer([]);
return (
<article>
<h2
onClick={() => {
setArr((draft: number[]) => {
draft.push(3);
});
setObj(draft => {
draft.num = 5
})
}}
>
{arr.length}
</h2>
</article>
);
}
它的原理是,直接修改draft
草稿对象之后,immer
内部再帮我们进行全量替换,draft
是immer
创建出来的一个对象 / 数组 copy
state设置原则
类比vue
的话,state
相当于data
,但是需要手动修改(调用setXXX
);computed
则直接写对应的表达式即可,因为调用setXX
时会触发组件重新渲染,即组件内部的表达式会重新计算
组件渲染替换的特殊性
react
渲染出来的东西,一般被称作UI树
,如果前后两次更新某个特定位置的组件
没有变化,那么这个组件内部的state
会保留下来:
例一
渲染两个计数器,第二个计数器的State
每次渲染都会重置
官方的解释是:
但是当我这么一改,他就不会清空那个位置的State
,这是因为一个相同的组件被渲染在了相同的位置
只有当我加上key
,react
才会把他们当成两个不同的组件
例二
export default function App() {
const [isFancy, setIsFancy] = useState(false);
if (isFancy) {
return (
<div>
<Counter isFancy={true} />
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
使用好看的样式
</label>
</div>
);
}
return (
<div>
<Counter isFancy={false} />
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
使用好看的样式
</label>
</div>
);
}
对于上面这样的代码,点击后不会重置Counter
组件里的State
,即点击的前后,UI
树结构没有发生变化,相同的位置还是渲染了相同的组件
例三
export default function App() {
const [showB, setShowB] = useState(true);
console.log('render')
return (
<div>
{showB && <Counter />}
{!showB ? <Counter />}
<label>
<input
type="checkbox"
checked={showB}
onChange={e => {
setShowB(e.target.checked)
}}
/>
渲染第二个计数器
</label>
</div>
);
}
如果这样子写,在react UI
树中,初始时他是
点击后,变成
这样子属于在不同的位置渲染不同的组件
,所以此时不会保留State
想硬要他们保留的话,那可以给他们设置一个相同的key
来实现
{show && <ListItem key="1" />}
{!show && <ListItem key="1" />}
useReducer
useReducer
相当于语法糖,与reduce
方法类似,但是要求必定返回一个state
,并且不能在其中执行异步或其他操作
比如官方的示例中,点击发送到
按钮,会触发sent_message
的dispatch
,实现的逻辑是“点击按钮后清空输入框”,所以返回一个{...state, message: ''}
的对象来重新设置state
示例代码中在点击按钮时还会alert
出对应的消息
需要注意的是,alert
不能移动到sent_message
的dispatch
中执行,否则会出现死循环的情况
useContext
这是一种快捷实现组件透传的hooks
,使用上有三步
1.创建context
,官方示例是单个文件中导出
2. 在要使用透传的组件中用useContext
实现
3. 提供Context
,如果有逐层传递的情况,则会使用距离最近的context
提供的值作为context
的值
比如这里,传到PlaceImage
组件时就应该是100 +500 + 500 = 1100
在react
中,兄弟组件间要进行通信,通常需要进行状态提升
,即把要互通的变量、操作等提到两者共同的父组件或更顶层的父组件来实现,比如下面这个todoList
使用useContext
的话就无需每个组件都向下传递了,直接在子组件内通过useContext
调用即可
useRef
useRef
通常用于设定一个不会出发页面渲染的值,他的值永远都是最新的,通常用于获取dom
元素,与vue
中ref
调用DOM
元素类似
由于没有响应式,所以思考这样的场景:输入一段文字,点击“发送”,3s后才发送,但是期间修改输入框的值,要求发送的值为最新值
如果是在vue
中,直接setTimeout(() => { alert(this.message) })
即可
但是在react
中,因为state
是渲染时快照,点击“发送”,message
绑定的是此时输入框中的值
所以这里要额外引入一个useRef
来实现:
或者声明一个文件内的变量
跨组件调用ref
主要通过forwardRef
来实现,它会向外部暴露ref
属性
参考下面这个例子,封装了一个SearchInput
组件,使用forwardRef
实现了将ref
“代理”到真实的input
元素上
useImperativeHandle
如果想要向外暴露某些方法或变量,就要用到useImperativeHandle
+ forwardRef
来实现
在vue
中可以直接通过ref.xxx
来访问子组件,但是在react
里则需要先进行一层套用
不过实际使用中不应该滥用ref
useEffect
useEffect
通常用于实现在组件渲染后执行一些特定的操作,但他并不和渲染挂钩,只取决于组件函数的执行
这里的关键在于:依赖变化是否触发了组件函数重新执行
useEffect
和vue - watch
很像,但是不能等同,这里举个例子说明:
const TodoList: React.FC = () => {
const [state, setState] = useState(0);
const ref = useRef(0);
const handleClick = () => {
console.log("点击!");
};
useEffect(() => {
console.log("state 修改", state);
}, [state]);
return (
<div className={styles.container}>
<Button type="primary" onClick={handleClick}>
点击
</Button>
</div>
);
};
export default TodoList;
一个可以确定的特性是,首次渲染时,无论是否指定依赖数组,都会触发一次
useEffect
绑定的函数
现在为按钮绑定点击的回调,让按钮点击时修改state
,则实际的执行步骤为:
- 触发点击事件,调用
setState
setState
引起组件重新渲染(或者说组件函数重新调用)- 走到
useEffect
,内部判断state
前后状态下是否相等 state
不相等,执行useEffect
绑定的函数
代码:
const TodoList: React.FC = () => {
const [state, setState] = useState(0);
const handleClick = () => {
console.log("点击! oldState:", state);
setState((state) => state + 1);
};
useEffect(() => {
console.log("触发effect");
}, [state]);
return (
<div className={styles.container}>
<Button type="primary" onClick={handleClick}>
点击
</Button>
</div>
);
};
效果:
这里要注意,一定是要跑了组件函数才会触发useEffect
里的函数执行,假如绑定的依赖是useRef
声明的变量,那么每次更新如果不跑组件函数,useEffect
绑定的函数一样是不会调用的,比如下面这个例子,代码:
const TodoList: React.FC = () => {
const [state, setState] = useState(0);
const ref = useRef(0);
const handleClick = () => {
console.log("点击! oldState:", state);
setState((state) => state + 1);
};
const handleSecondClick = () => {
console.log("点击! oldRef:", ref.current);
ref.current += 1;
};
useEffect(() => {
console.log("触发effect");
}, [ref.current]);
return (
<div className={styles.container}>
<Button type="primary" onClick={handleClick}>
点击
</Button>
<Button type="primary" onClick={handleSecondClick}>
点击
</Button>
</div>
);
};
点击第二个按钮,触发ref
增加,但是没有触发组件重新渲染,因此不会跑useEffect
此时点击第一个按钮,调用了setState
触发页面重绘,所以跑了useEffect
,useEffect
去判断ref.current
前后两次不同,所以要执行一次
但是需要注意,他是放在某次组件渲染后才执行的,比如下面这个场景:
这里没有渲染两次,是因为使用框架跑的,框架中把这个特性给关了
在react
严格模式下,组件会默认渲染两次,对此官方的解释是
以此可以让开发者发现一些错误并修复
假如我们在useEffect
中创建一个到服务器的连接,每个useEffect
需要在下一次组件渲染时才会进行cleanups
,如果用户此时切换页面,而我们没有设置useEffect
的cleanup
回调,就会导致这条连接一直保持着存活性
然后以此你会发现到“上一个连接没有终止”的问题,但我个人感觉有点牵强了,以前用
vue
的时候本身就会在beforeDestroy
钩子里写各种销毁操作了
解决方法就是设置一个返回函数,在里面给他断开
用法一:仅在组件初次渲染时调用
看下图,value
在父组件中用useState
声明,onChange
会触发setState
导致组件重新渲染
这意味着每次输入或勾选 / 反选,都会导致输入框重复聚焦
如果不用
useEffect
会读不到ref.current
所以解决方案就是,第二个参数,它是一个需要忽略的依赖数组,将其设为空数组则表示只需要在该组件初次渲染时调用
传空数组应该这么理解,空数组可以当作是[‘’],每次useEffect
的调用会与上一次useEffect
时指定的值作对比
重复渲染触发对比,发现两次的值相同 '' === ''
,所以第二次渲染时就不会执行useEffect
里的代码
如果上面改成下面这样,那就会疯狂调用回调,因为每次渲染时value
都变了
useEffect(() => {
console.log(123)
}, [value]);
用法二:销毁effect
useEffect
的回调可以设置一个返回的函数,在其中执行一些操作;每个useEffect
回调执行前都会清理上一个Effect
(如果有的话)
常用的场景有两种,一个是清空计时器,一个是解决请求覆盖
清空计时器:
解决请求覆盖:
不过一般请求都是在handleXXX
回调里发起的,在那边解决请求覆盖应该是结合AbortController
来实现,或者用singletonPromise
实现;就看是否需要连请求一起中断了
useEffect生命周期
每个useEffect
都是一个独立的同步过程,他的第二个参数接收一个依赖数组,表示当数组中任一依赖发生了变化时,执行指定的计算逻辑
useEffectEvent(实验性的,未上正式版)
主要是用于提取useEffect
中非响应式的逻辑
对于下面这段代码,应该只有切换roomId
时才重新连接聊天室,不希望切换theme
的时候也触发effect
,但同时又需要在展示"Connected"的时候显示此时的主题,所以这里就产生了矛盾
也许你会想到可以把他提取出去,但是这本身已经违背业务逻辑了(连接到聊天室是异步的,不应该放在外面提示LOL)
所以为了解决这个问题,react
提供了useEffectEvent
来实现
这段代码让useEffect
仅依赖于roomId
的变化,而在connected
的回调中又能确保showNotification
提示的是最新的theme
useLayoutEffect
useEffect
是放在组件渲染完成后执行的,可以简单对照到vue - nextTick
后执行的代码,即下一次tick
才清空useEffect
的队列
useLayoutEffect
是在浏览器绘制页面前执行,但在layoutEffect
的内部依然能获取到DOM
元素的最新的状态,这是因为本身react
就是先算出要怎么渲染
,最后再提交
到浏览器中绘制出来的
假设某个操作修改了页面DOM
元素的状态,在useLayoutEffect
中依然获取到的是元素最新的值
对于下面这段代码:
const TodoList: React.FC = () => {
const [state, setState] = useState(400);
const divRef = useRef<HTMLDivElement>(null);
const handleClick = () => {
setState(600);
};
useEffect(() => {
console.log(
"useEffect div height:",
divRef.current?.getBoundingClientRect().height
);
}, [state]);
useLayoutEffect(() => {
console.log(
"useLayoutEffect div height:",
divRef.current?.getBoundingClientRect().height
);
}, [state]);
return (
<div ref={divRef} style={{ height: state }} className={styles.container}>
<Button type="primary" onClick={handleClick}>
点击
</Button>
</div>
);
};
点击按钮后的执行结果:
在官方文档中给出的一个实际的使用案例是在计算tooltip实际应该展示的位置时,避免一闪而过
这是useLayoutEffect
的效果:
这是useEffect
的效果:
区别参考另一个博主的描述,我觉得挺OK的:
出现页面闪烁时可以考虑用useLayoutEffect
优化
useCallback
该hook
主要用于实现对函数进行缓存,是性能优化时常用的一种手段
因为react
会在setState
调用时重新渲染所有使用该state
的组件,所以组件内的各类声明,按理说有一些函数或状态是不需要重新声明的(比如某些业务方法,提交表单,二次确认信息等),此时就可以使用useCallback
来实现
useCallback
接收两个参数,fn
与依赖项数组,如果依赖项没有变化,那么useCallback
会返回原本声明的函数,否则才重新创建函数
一个性能优化的最佳实践是结合memo
使用,memo
可以创建一个组件,并且该组件仅在传入的props
发生修改了的情况下重新渲染,如
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
function ProductPage({ productId, referrer, theme }) {
// 在多次渲染中缓存函数
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // 只要这些依赖没有改变
return (
<div className={theme}>
{/* ShippingForm 就会收到同样的 props 并且跳过重新渲染 */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
思考另一个场景,假如现在有一个按钮可以往页面的列表中插入一条数据,比如todo
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
在这种写法中,我们的useCallback
每次都会因为其他地方对todos
修改而创建新的函数,所以常用的办法是使用updater
函数来优化setTodos
的调用,使函数的声明不依赖todos
的修改
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, nextTodo]);
}, []);
// ...
另外还有一种场景,如果是在useEffect
里使用了组件内声明的函数,需要把这个函数当作依赖项传入
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 问题:这个依赖在每一次渲染中都会发生改变
// ...
但是当组件因为其他原因重新渲染时,createOption
都是新的值,导致useEffect
每次都会调用,所以也可以用useCallback
来解决
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ 仅当 roomId 更改时更改
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ 仅当 createOptions 更改时更改
// ...
或者可以将这个方法提到组件外进行声明
const createOptions = (roomId = '') => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
};
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
const options = createOptions(roomId);
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ 仅当 roomId更改时更改
// ...
useMemo
类似于vue
的computed
,但是也在组件函数重新执行时才会去调用并计算出值
几乎所有的
useXXX
钩子,都要求是运行时才“计算”,并不会像vue
那样由watcher
去监听和自动计算,对于刚从vue
转过来的我还需要一定的时间慢慢适应
useMemo
一般用来记忆“值”,也可以记忆函数,避免每次组件函数调用时重复声明函数导致重新渲染。
对于函数的记忆,使用useCallback
即可
自定义HOOK
核心在于下面这句话: