useCallback
const memoizedCallback = useCallback(
() => { # 回调函数仅在某个依赖项改变时才会更新
doSomething(a, b);
},
[a, b],
);
# useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
返回一个 memoized 回调函数。
由于javascript函数的特殊性,当函数签名被作为deps传入useEffect时,还是会引起re-render(即使函数体没有改变),这种现象在类组件里边也存在:
// 当Parent组件re-render时,Child组件也会re-render
class Parent extends Component {
render() {
const someFn = () => {}; // re-render时,someFn函数会重新实例化
return (
<>
<Child someFn={someFn} />
<Other />
</>
);
}
}
class Child extends Component {
componentShouldUpdate(prevProps, nextProps) {
return prevProps.someFn !== nextProps.someFn; // 函数比较将永远返回false
}
}
Function Component(查看demo):
function App() {
const [count, setCount] = useState(0);
const [list, setList] = useState([]);
const fetchData = async () => {
setTimeout(() => { # 2.依赖的这个函数会对 state进行更改,一旦更改会触发组件重绘
setList(initList); # 而组件一旦更新 又会进入useEffect 所以就会无限循环
}, 3000);
};
useEffect(() => { # 1.副作用的依赖是 函数
fetchData();
}, [fetchData]); # 3.解决方法(中断无限循环): 如何让组件re-render的时候 fetchData不是新的
return (
<>
<div>click {count} times</div>
<button onClick={() => setCount(count + 1)}>Add count</button>
<List list={list} />
</>
);
}
解决方法:
1、将fetchData函数移到组件外部(缺点是无法读取组件的状态了)
2、条件允许的话,把函数体移到useEffect
内部 (直接干掉这个函数)
3、通用解决方法:
使用useCallback API包裹函数,useCallback的本质是对函数进行依赖分析,依赖变更时才重新执行
useMemo
useMemo用于缓存一些耗时的计算结果,只有当依赖参数改变时才重新执行计算:
const fibValue = useMemo(() => fibonacci(start), [start]); // 缓存耗时操作
function App(props) {
const list = props.list;
# list 更改的时候 MemoList才会重新计算出 li
# 简单理解:useCallback(fn, deps) === useMemo(() => fn, deps)
const MemoList = useMemo(() => <List list={list} />, [list]);
return (
<>
{MemoList}
<Other />
</>
);
}
# useMemo也可以用React.memo,但相比将整个组件React.memo,用useMemo去渲染memo后的dom,更细粒度
// 只有列表项改变时组件才会re-render
const MemoList = React.memo(({ list }) => {
return (
<ul>
{list.map(item => (
<li key={item.id}>{item.content}</li>
))}
</ul>
);
});
# 相比React.memo,
# useMemo在组件内部调用,可以访问组件的props和state,所以它拥有更细粒度的依赖控制。
useRef
useRef 返回一个ref对象的可变引用,但useRef的用途比ref更广泛,它可以存储任意javascript值而不仅仅是DOM引用。
其 .current 属性被初始化为传入的参数(initialValue)
修改useRef值的唯一方法是修改其current的值,且值的变更不会引起re-render
每一次组件render时useRef都返回固定不变的值,不具有Capture Values特性
useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”
// const refContainer = useRef(initialValue);
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
// 将 ref 对象以 <div ref={myRef} /> 形式传入组件,
// 则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
useRef() 和自建一个 {current: …} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。
概念
Capture Values特性
非useRef相关的Hooks API,(useState、useEffect、event handle)
(state是Immutable的,ref是mutable的)
本质上都形成了闭包,闭包有自己独立的状态,这就是Capture Values的本质。
useSate:
function Counter1() {
const [count, setCount] = useState(0);
const log = () => { # 点击的时候会触发这个函数
setCount(count + 1); # 每点击一次 就setCount一次,页面就更新显示 clicked xx times
# 这个异步函数 也是被执行那么多次 所以3s后 控制台依次输出 1 2 3 4 ..
setTimeout(() => {
console.log(`current count is ${count}.`);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
# 然而你换成class组件
class Counter2 extends Component {
state = { # 用state去管理变量 由于异步更新机制 setState是异步的
count: 0
};
render() {
const log = () => {
this.setState({ count: this.state.count + 1 });
setTimeout(() => {
# 所以这里 3s后 打印的全是n次 cliled is n
console.log(`current count is ${this.state.count}.`);
}, 3000);
};
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={log}>Click me</button>
</div>
);
}
}
这个例子说明了,useState 和 this.setState相比, useState是立竿见影的,本质是维护的闭包,利用了闭包
由于闭包本身的性能优良,所以无需再利用个异步更新队列setState去异步更新状态
useEffect:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
# 连续点击三次button,页面的title将依次改为1、2、3,而不是3、3、3
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
eventhandle
先点击(触发函数3s渲染props.name的回调),然后在控制台输出前去切换select,改变showName的props
问:控制台输出啥?setState排队的异步 肯定先于 settimeout,所以 是 foo
这里只能拿到变更前的!!!
相当于,FC组件在props更新的时候 没有更新
render的时候是更新的
但是 函数里的settimeout 的回调没有是最新的
===> 回调在click事件发生的那瞬间就 将回调放在settimeout里了 那个时候的值就确定了
相比较而言:
是因为 函数是否 实例的原因吗?
==》 no 都只渲染了一次。当上层的props更新的时候
demo:https://codesandbox.io/s/cv-handler-mqsun?file=/src/index.js
不具备的ref的表现
state是Immutable的,ref是mutable的
const [count, setCount] = useState(0);
const countRef = useRef(count); # countRef.current 记录变量
# 每次订阅count变化后,重置变量 = 最新的count
# 3s后控制台输出结果是 n次的 5 (最终拿到的都是最新值)
# 而对比 如果 countRef的变量是 useState管理的
# 肯定是 输出 1 2 3 4
useEffect(() => {
countRef.current = count;
setTimeout(() => {
// setTimeout里边始终能拿到state最新值
console.log(`current count is ${countRef.current}`);
}, 3000);
}, [count]);
匪夷所思
依赖问题
useCallback与useMemo
参考文章
- 从React组件设计理论说起->class 组件问题(组件复用困局、js class的缺陷)、function组件缺失的问题->function+hook
- 性能优化的api:useCallback(针对无线循环代码 为啥useCallback可以解决)、useMemo(渲染list)、useRef
- Capture values
- useState 和 this.setState() 相比,useState更改是立竿见影的
- eventhandle 让我怀疑人生了
- ref看懂了
===> 嗯 Capture values的例子 让我怀疑 function组件 和 class组件的基本功都没掌握好
于是从该文章末尾的参考资料 顺藤摸瓜 到了这两篇文章
精读《Function Component 入门》
精读《useEffect 完全指南》