- useState 与 setState的区别
- useState 与 useReducer的区别
- effect 的依赖频繁变化,该怎么办?
- useRef、createRef的区别及使用,及useRef妙用
- 竞态
- useCallback&& useMemo 及其使用场景
useState
设置初始值时应该注意的问题
useState设置初始值时,如果初始值是个值,可以直接设置,如果是个函数返回值,建议使用回调函数的方式设置
- 初始值直接设置为函数返回值
const initCount = c => {
console.log('initCount 执行');
return c * 2;
};
function Counter() {
const [count, setCount] = useState(initCount(0));
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
存在问题 即便 Counter 组件重新渲染时没有再给 count 重新赋初始值,但是 initCount 函数却会重复执行
- 🌹 初始值设置为函数函数回调
const initCount = ...
function Counter() {
const [count, setCount] = useState(()=>initCount(0));
...
}
initCount 函数只会在 Counter 组件初始化的时候执行,之后无论组件如何渲染,initCount 函数都不会再执行
与 setState的区别
- state改变时
useState修改state时,同一个useState声明的值会被 覆盖处理,多个useState声明的值会触发 多次渲染setState修改state时,多次setState的对象会被 合并处理
-
useState修改state时,设置相同的值,函数组件不会重新渲染,而继承Component的类组件,即便setState相同的值,也会触发渲染
与 useReducer 的区别
- 修改状态时
useState修改状态时,同一个useState声明的状态会被覆盖处理useReducer修改状态时,多次dispatch会按顺序执行,依次对组件进行渲染
const [count, setCount] = useState(0);
return (
<p
onClick={() => {
setCount(count + 1);
setCount(count + 2);
}}
>
clicked {count} times
</p>
);
}
function Counter() {
const [count, dispatch] = useReducer((x, payload) => x + payload, 0); return (
<p
onClick={() => {
dispatch(1);
dispatch(2);
}}
>
clicked {count} times
</p>
);
}
effect 的依赖频繁变化,该怎么办?
- 使用
setState的函数式更新形式
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // 这个 effect 依赖于 `count` state }, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` 没有被指定为依赖
return <h1>{count}</h1>;
}
传入空的依赖数组 [], hook 只在组件挂载时运行一次,并非重新渲染时。在 setInterval 的回调中,count 的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行 setCount(0 + 1),count是1。
指定 [count] 作为依赖列表能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval 在被清除前(类似于 setTimeout)都会调用一次。但这并不是我们想要的。
要解决这个问题,我们可以使用 setState 的函数式更新形式。它允许我们指定 state 该 如何 改变而不用引用 当前 state:
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量 }, 1000);
return () => clearInterval(id);
}, []); // ✅ 我们的 effect 不适用组件作用域中的任何变量
- 在一些更加复杂的场景中(比如一个 state 依赖于另一个 state)用
useReducer把 state 更新逻辑移到 effect 之外。 - 使用
ref来保存一个可变的变量(类似 class 中的this的功能),对它进行读写。
不建议,因为依赖于变更会使得组件更难以预测。
useRef、createRef的区别及使用,及useRef妙用
useRef基本用法: 在函数组件中操作DOM (比如获取子组件的state/方法)
- 不使用
useRef的情况下,每一帧里的state值是如何打印的
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = function() {
console.log('count: ', count);
}
window.addEventListener('click', handleClick, false)
});
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
复制代码
打印结果:
保证在函数组件的每一帧里访问到的 state 值是相同的
- 使用
useRef之后,每一帧里的ref值是如何打印的
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
// 将最新 state 设置给 countRef.current
countRef.current = count;
const handleClick = function () {
console.log('count: ', countRef.current);
};
window.addEventListener('click', handleClick, false);
});
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
打印结果
——>
export default () => {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count;
const handleClick = function () {
console.log('count: ', countRef.current);
};
useEffect(() => {
window.addEventListener('click', handleClick, false);
}, []);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
改进版 提取
const useStab = (fn) => {
const ref = useRef({});
ref.current = fn;
return useCallback(() => {
ref.current();
}, []);
};
export default function App() {
const [count, setCount] = useState(0);
const add = () => {
setCount(count + 1);
};
const hanldeClick = useStab(() => {
console.log("count", count);
});
useEffect(() => {
window.addEventListener("click", hanldeClick);
}, []);
return (
<div className="App">
<h1>{count}</h1>
<button onClick={add}>Add</button>
</div>
);
}
保存住函数组件实例的属性
函数组件是没有实例的,因此属性也无法挂载到 this 上。那如果我们想创建一个非 state、props 变量,能够跟随函数组件进行创建销毁,该如何操作呢?
同样的,还是可以通过 useRef,useRef 不仅可以作用在 DOM 上,还可以将普通变量转化成带有 current 属性的对象
比如,我们希望设置一个 Model 的实例,在组件创建时,生成 model 实例,组件销毁后,重新创建,会自动生成新的 model 实例
class Model {
constructor() {
console.log('创建 Model');
this.data = [];
}
}
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(new Model());
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
复制代码
按照这种写法,可以实现在函数组件创建时,生成 Model 的实例,挂载到 countRef 的 current 属性上。重新渲染时,不会再给 countRef 重新赋值。
也就意味着在组件卸载之前使用的都是同一个 Model 实例,在卸载之后,当前 model 实例也会随之销毁。
仔细观察控制台的输出,会发现虽然
countRef没有被重新赋值,但是在组件在重新渲染时,Model的构造函数却依然会多次执行
所以此时我们可以借用 useState 的特性,改写一下。
class Model {
constructor() {
console.log('创建 Model');
this.data = [];
}
}
function Counter() {
const [count, setCount] = useState(0);
const [model] = useState(() => new Model());
const countRef = useRef(model);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
复制代码
这样使用,可以在不修改 state 的情况下,使用 model 实例中的一些属性,可以使 flag,可以是数据源,甚至可以作为 Mobx 的 store 进行使用。
class 组件中createRef
// 父组件
class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef(null);
}
render() {
return (
<div>
<ChildComponent ref={this.myRef}></ChildComponent>
<button
onClick={() => {
alert(this.myRef.current.state.childName);
}}
>
get child childName from parent
</button>
<button
onClick={() => {
this.myRef.current.sayHello();
}}
>
get child function from parent
</button>
</div>
);
}
}
// 子组件
class ChildComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
childName: "mongo",
};
}
sayHello() {
alert("hello! " + this.state.childName);
}
render() {
return (
<div>
<button
onClick={() => {
this.sayHello();
}}
>
alert function by child
</button>
</div>
);
}
}
useRef
const ParentComponentByUseRef = () => {
const myRef = useRef();
return (
<div>
<ChildComponentByUseRef ref={myRef}></ChildComponentByUseRef>
<button
onClick={() => {
alert(myRef.current.childName);
}}
>
get childName by parent
</button>
<button
onClick={() => {
myRef.current.sayHello();
}}
>
get function by parent
</button>
</div>
);
};
const ChildComponentByUseRef = forwardRef((props, ref) => {
// 暴露function、state和DOM结构给父元素
useImperativeHandle(ref, () => ({
sayHello,
childName,
ref
}));
const [childName, setChildName] = useState("mongo");
const sayHello = () => {
alert("hello! " + childName);
};
return (
<div ref={ref}>
<button
onClick={() => {
sayHello();
}}
>
get function by child
</button>
<button
onClick={() => {
setChildName("mongowoo");
}}
>
change childName
</button>
</div>
);
});
export default forwardRef(ParentComponentByUseRef);
hooks中如果要进行refs转发,要配合fowardRef和useImperativeHandle使用
- forwardRef React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。
使用场景
1.转发 refs 到 DOM 组件
2.在高阶组件中转发 refs
- useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值
竞态
执行更早但返回更晚的情况会错误的对状态值进行覆盖 在
useEffect中,可能会有进行网络请求的场景,我们会根据父组件传入的id,去发起网络请求,id变化时,会重新进行请求。
function App() {
const [id, setId] = useState(0);
useEffect(() => {
setId(10);
}, []);
// 传递 id 属性
return <Counter id={id} />;
}
// 模拟网络请求
const fetchData = id =>
new Promise(resolve => {
setTimeout(() => {
const result = `id 为${id} 的请求结果`;
resolve(result);
}, Math.random() * 1000 + 1000);
});
function Counter({ id }) {
const [data, setData] = useState('请求中。。。');
useEffect(() => {
// 发送网络请求,修改界面展示信息
const getData = async () => {
const result = await fetchData(id);
setData(result);
};
getData();
}, [id]);
return <p>result: {data}</p>;
}
复制代码
展示结果:
上面的实例,多次刷新页面,可以看到最终结果有时展示的是 id 为 0 的请求结果,有时是 id 为 10 的结果。 正确的结果应该是 ‘id 为 10 的请求结果’。这个就是竞态带来的问题。
解决办法:
- 取消异步操作
- 设置布尔值进行追踪
useCallback
如题,当依赖频繁变更时,如何避免 useCallback 频繁执行呢?
function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return <p onClick={handleClick}>clicked {count} times</p>;
}
复制代码
把 click 事件提取出来,使用 useCallback 包裹,但其实并没有起到很好的效果。
因为 Counter 组件重新渲染目前只依赖 count 的变化,所以这里的 useCallback 用与不用没什么区别。
使用 useReducer 替代 useState
可以使用 useReducer 进行替代。
function Counter() {
const [count, dispatch] = useReducer(x => x + 1, 0);
const handleClick = useCallback(() => {
dispatch();
}, []);
return <p onClick={handleClick}>clicked {count} times</p>;
}
复制代码
useReducer 返回的 dispatch 函数是自带了 memoize 的,不会在多次渲染时改变。因此在 useCallback 中不需要将 dispatch 作为依赖项。
向 setState 中传递函数
function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return <p onClick={handleClick}>clicked {count} times</p>;
}
复制代码
在 setCount 中使用函数作为参数时,接收到的值是最新的 state 值,因此可以通过这个值执行操作。
通过 useRef 进行闭包穿透
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count;
const handleClick = useCallback(() => {
setCount(countRef.current + 1);
}, []);
return <p onClick={handleClick}>clicked {count} times</p>;
}
这种方式也可以实现同样的效果。但是不推荐使用,不仅要编写更多的代码,而且可能会产生出乎预料的问题。
useMemo VS useCallback
useMemo的返回值是一个值,可以是属性,可以是函数(包括组件)useCallback的返回值只能是函数
因此,useMemo 一定程度上可以替代 useCallback,等价条件:useCallback(fn, deps) => useMemo(() => fn, deps)
useMemo VS React.memo
useMemo是对组件内部的一些数据进行优化和缓存,惰性处理。React.memo是对函数组件进行包裹,对组件内部的state、props进行浅比较,判断是否需要进行渲染。
useCallback 和 useMemo 什么时候使用
不建议频繁使用
-
useCallback 和 useMemo 其实在函数组件中是作为函数进行调用,那么第一个参数就是我们传递的回调函数,无论是否使用 useCallback 和 useMemo,这个回调函数都会被创建,所以起不到降低函数创建成本的作用
-
不仅无法降低创建成本,使用 useCallback 和 useMemo 后,第二个参数依赖项在每次 render 的时候还需要进行一次浅比较,无形中增加了数据对比的成本
useCallback 的使用场景
- 场景一:需要对子组件进行性能优化 state变化时,父组件重新 render,而子组件却不需要重新 render
import React, { useCallback, useState } from 'react';
import Foo from './Foo';
function App() {
const [count, setCount] = useState(0);
const fooClick = useCallback(() => {
console.log('点击了 Foo 组件的按钮');
}, []);
return (
<div style={{ padding: 50 }}>
<Foo onClick={fooClick} />
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>click</button>
</div>
);
}
export default App;
Foo.js 中使用 React.memo 对组件进行包裹(类组件的话继承 PureComponent 是同样的效果)
import React from 'react';
const Foo = ({ onClick }) => {
console.log('Foo 组件: render');
return <button onClick={onClick}>Foo 组件中的 button</button>;
};
export default React.memo(Foo);
此时再点击按钮,父组件更新,但是子组件不会重新 render
场景二:需要作为其他 hooks 的依赖
这个例子中,会根据状态 page 的变化去重新请求网络数据,当 page 发生变化,我们希望能触发 useEffect 调用网络请求,而 useEffect 中调用了 getDetail 函数,为了用到最新的 page,所以在 useEffect 中需要依赖 getDetail 函数,用以调用最新的 getDetail
使用 useCallback 处理前的代码
App.js
import React, { useEffect, useState } from 'react';
const request = (p) =>
new Promise(resolve => setTimeout(() => resolve({ content: `第 ${p} 页数据` }), 300));
function App() {
const [page, setPage] = useState(1);
const [detail, setDetail] = useState('');
const getDetail = () => {
request(page).then(res => setDetail(res));
};
useEffect(() => {
getDetail();
}, [getDetail]);
console.log('App 组件:render');
return (
<div style={{ padding: 50 }}>
<p>Detail: {detail.content}</p>
<p>Current page: {page}</p>
<button onClick={() => setPage(page + 1)}>page increment</button>
</div>
);
}
export default App;
按照上面的写法,会导致 App 组件无限循环进行 render,此时就需要用到 useCallback 进行处理
使用 useCallback 处理后的代码
App.js
import React, { useEffect, useState, useCallback } from 'react';
const request = (p) =>
new Promise(resolve => setTimeout(() => resolve({ content: `第 ${p} 页数据` }), 300));
function App() {
const [page, setPage] = useState(1);
const [detail, setDetail] = useState('');
const getDetail = useCallback(() => {
request(page).then(res => setDetail(res));
}, [page]);
useEffect(() => {
getDetail();
}, [getDetail]);
console.log('App 组件:render');
return (
<div style={{ padding: 50 }}>
<p>Detail: {detail.content}</p>
<p>Current page: {page}</p>
<button onClick={() => setPage(page + 1)}>page increment</button>
</div>
);
}
export default App;
App组件可以正常的进行 render 了。
-
useCallback使用场景总结:- 向子组件传递函数属性,并且子组件需要进行优化时,需要对函数属性进行
useCallback包裹 - 函数作为其他
hooks的依赖项时,需要对函数进行useCallback包裹
- 向子组件传递函数属性,并且子组件需要进行优化时,需要对函数属性进行
useMemo 的使用场景
- 向子组件传递 引用类型 属性,并且子组件需要进行优化时,需要对属性进行
useMemo包裹 - 引用类型值,作为其他
hooks的依赖项时,需要使用useMemo包裹,返回属性值 - 需要进行大量或者复杂运算时,为了提高性能,可以使用
useMemo进行数据缓存,节约计算成本
所以,在 useCallback 和 useMemo 使用过程中,如非必要,无需使用,频繁使用反而可能会增加依赖对比的成本,降低性能。