自 React16.8 正式发布React Hook以后,已通过去了5个版本(本博客于React 16.13.1版本时发布)。本身使用Hook已经有了一段时间,不得不说在最初使用Hook的时候也进入了不少误区。在这篇文章中,我会抛出并解答我遇到的一些问题,同时整理对Hook的心得理解。react
在解释后续内容以前,首先咱们要明确Hook的执行流。git
咱们在写 Hook Component 的时候,本质是 Function Component + Hook ,Hook只是提供了状态管理等能力。对 Function Component 来讲,每一次渲染都会从上到下将全部内容从新执行一次,若是有变量,就会创造新变量。github
来看一个简单的例子数组
// father.js
import Child from './child';
function Father() {
const [num, setNum] = useState(0);
function handleSetNum() {
setNum(num+1);
}
return (
<div> <div onClick={handleSetNum}>num: {num}</div> <Child num={num}></Child> </div>
);
}
复制代码
// child.js
function Child(props) {
const [childNum, setChildNum] = useState(0);
return (
<> <div>props.num: {props.num}</div> <div onClick={() => {setChildNum(childNum + 1)}}> childNum: {childNum} </div> </> ); } 复制代码
而后咱们来看看执行流:浏览器
father执行
useState
,useState
返回一个数组,而后解构赋值给num
和setNum
,而后建立函数handleSetNum
,把 jsx代码 返回出去交给 react 处理。缓存
child接受到来自father传递的数据,建立props变量并赋值,执行
useState
,解构赋值给childNum
和setChildNum
,把 jsx代码 返回出去交给 react 处理。性能优化
接下来,点击father中那个绑定了点击事件的变量,触发执行 handleSetNum
方法,修改Hook State,让num值加一。由于状态发生改变,会去触发react的重渲染。session
而后咱们再来看看第二次重渲染时的部分执行流:闭包
father会 **再次执行
useState
**;useState
返回一个 新数组 ,而后解构赋值给 **新建立的num
和setNum
**;随后 建立一个新的函数handleSetNum
函数
child接受到来自father传递的数据,建立 新变量props并赋值;再次执行
useState
,useState
会 返回一个新数组,但由于childNum没有发生变化,新数组里面的值全等于旧数组里面的值,解构赋值给 新变量childNum
和setChildNum
咱们能够经过全局变量进行验证
// father.js
window.selfStates = []; // 建立一个全局的变量存储
window.selfStates2 = []; // 建立一个全局的变量存储
function Father() {
const [num, setNum] = useState([0]); // 把数字改为一个数组
const [num2, setNum2] = useState([0]); // 设置一个对照组,对照组就初始化,不进行修改
window.selfStates.push(num);
window.selfStates2.push(num);
function handleSetNum() {
setNum([num[0] + 1]) // 不直接改值,避免影响旧数组
}
...
}
复制代码
以后能够在浏览器的控制台里输出,看看结果
固然,咱们建立的 handleSetNum
函数也能够用这样的方法进行验证。
因此,看到这里,咱们已经了解了hook的执行流:每次渲染,每次更新,都会让整个内容所有更新一次,而且建立新的变量。
直接上代码
function initState() {
console.log('run'); // 执行一次就知道了
return 1;
}
function Father() {
const [num, setNum] = useState(initState()); // ❎
const [num, setNum] = useState(initState); // ✅
}
复制代码
由于每次更新,都会执行一次useState,根据useState的机制,它会进行数据存储:若是没有数据,进行初始化,并建立新数据,存储起来;若是有数据,不进行初始化操做,返回数据的值。
可是若是咱们使用 useState(func())
这样形式进行初始化,那么每次都会先执行func,再把执行后获得的值做为参数传递给useState,可是若是已经初始化过了,那么会跳过对这个参数的初始化处理,每次更新时都会浪费一次跑func函数的时间。
来看下面这个代码
function Father() {
const [num, setNum] = useState(0);
function handleSetNum() {
setNum(num+1);
}
// 延迟3秒后输出num值
function handleGetNum() {
setTimeout(() => {
alert(num);
}, 3000);
}
return (
<div> <div onClick={handleSetNum}>num: {num}</div> <div onClick={handleGetNum}>点我输出内容</div> </div>
);
}
复制代码
假设如今num值为0,我先触发 handleGetNum
,而后再触发1次 handleSetNum
修改num的值,3秒倒计时结束后,输出的num值是多少?
留点空间来思考一下
下面公布答案
答案是0
若是咱们使用 Class Component 来作,获得的结果却相反,输出的值为1。
在Hook Component中,这种现象被称做 Capture value
为何会有这样的状况?
解答这个问题,这仍是得回到最初的起点:Hook执行流
咱们知道每次渲染都会建立该次渲染的新变量,所以在最初的状态下,我用handleGetNum_0来表示最初的 handleGetNum 函数,num_0表示最初的num。
咱们先触发 handleGetNum,随后触发 handleSetNum ,以后数据更新,建立新函数 handleGetNum_1 和 新变量 num_1。
在这个过程当中咱们实际点击的是handleGetNum_0,handleGetNum_0里操做的是num_0,因此 alert 的内容仍是 num_0 的值。
那么问题来了,为何 Class Component 不会有这样的状况发生?
咱们来看一下若是用 Class Component 会怎么写
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
num: 0
}
}
handleSetNum = () => {
this.setState({num: this.state.num+1});
}
handleGetNum = () => {
setTimeout(() => {
alert(this.state.num);
}, 3000);
}
render() {
return (
<div> <div onClick={this.handleSetNum}>num: {this.state.num}</div> <div onClick={this.handleGetNum}>点我输出内容</div> </div>
)
}
}
复制代码
咱们在 Class Component 中获取参数时,是经过 this.state.num
进行获取,this会指向最新的state值,所以不会出现 Capture Value的状况。
若是咱们想在 Class Component 中实现 Capture Value,一个简单的办法就是作一个闭包,这个比较简单,所以不在文中详细陈述。
那么若是咱们不想在 Hook Component 中触发 Capture Value 应该怎么作?
答案就是用 useRef
function App() {
const [num, setNum] = useState(0);
const nowNum = useRef(0); // 额外建立一个Ref对象,由于Ref对象更新不会触发react的重渲染
function handleSetNum() {
setNum(num + 1);
nowNum.current = num + 1; // 给num赋值的同时也给nowNum赋值
}
function handleGetNum() {
setTimeout(() => {
alert(nowNum.current);
}, 3000);
}
...
}
复制代码
咱们使用 nowNum.current
就相似 this.state
,由于Ref会建立一个对象,这个对象会指向最新的值。
useCallback 和 useMemo 比较相似,咱们放在一块儿来讲。
他们都是用来作数据缓存的,区别就在于useCallback返回的是函数,useMemo返回的是函数执行后的返回值。
先上结论
何时须要用他们:
- 成本很高的计算
- 避免子组件无心义的重渲染
- 数据须要传递给其余组件,且数据为对象、函数
何时不须要用他们:
- 仅仅在组件内部使用,不存在向下传递数据。
- 若是要向下传递数据,但数据值是非对象、非函数的值
ex.
function Father() {
const [num, setNum] = useState([0]);
const [num2, setNum2] = useState([0]);
function handleSetNum() {
setNum([num[0] + 1]);
}
function handleSetNum2() {
setNum2([num2[0] + 1]);
}
return (
<div> <div onClick={handleSetNum}>num: {num[0]}</div> <div onClick={handleSetNum2}>num2: {num2[0]}</div> <Child num={num}></Child> </div>
);
}
复制代码
上面的代码就是没有使用useMemo,Child接受num参数,每当num参数更新的时候,会触发Child的更新,同时还有一个num2,num2的更新触发Father的更新,同时形成Child的更新。
处理后的代码:
function Father() {
const [num, setNum] = useState([0]);
const [num2, setNum2] = useState([0]);
function handleSetNum() {
setNum([num[0] + 1]);
}
function handleSetNum2() {
setNum2([num2[0] + 1]);
}
const ChildHtml = useMemo(() => {
return <Child num={num}></Child>
}, [num])
return (
<div> <div onClick={handleSetNum}>num: {num[0]}</div> <div onClick={handleSetNum2}>num2: {num2[0]}</div> {ChildHtml} </div>
);
}
复制代码
这样咱们用useMemo包裹了子组件,useMemo会存储return的值,当num变化时,会从新执行useMemo第一个参数里的函数,返回新值,并保存新值。 当num2变化时,会把以前保存的值取出来,这样就能避免子组件的重渲染。
有时候会有这样的处理
// father.js
function Father() {
...
function func() {};
return {
<Child func={func} obj={{name: abc, num:2}}></Child>
}
}
// child.js
function Child(props) {
useEffect(() => {
...
}, [props])
return (
...
);
}
复制代码
在子组件里,须要模拟 componentDidUpdate
处理各类参数,用当props改变的时候,进行一些操做。
若是咱们已经用了useMemo包裹了子组件,那么解决了一个隐藏问题。若是咱们没有用useMemo包裹子组件,那么就要当心了
Father传递给Child的props里有2个属性,一个函数,一个自定义对象
props: {
func: func,
obj: {
name: abc,
num: 2
}
}
复制代码
当子组件接受的props发生了变化,会执行useEffect函数里的内容。
若是咱们props.obj里的值发生了改变,引发了useEffect的执行,这是正常的。
可是咱们要明确一点,Hook每次执行都会建立新的对象。也就是说,若是Father每次更新,都会建立新的func函数和新的obj对象,即便咱们认为func函数和obj对象没有发生变化,可是props里的变量指针都会指向新对象,而后触发了本不应触发的useEffect
2种解决方法:
也许有人会采用 React.memo
和 React.PureComponent
,可是他们的缓存策略也只是用全等符比较props里的值,所以即便使用了这2个方法,若是不用useCallback和useMemo包裹props中传递的值,依然也会触发上文代码里写的 useEffect
tip: 经过useState获得的变量不须要使用useMemo,由于useState已经进行了处理,保证未更新的值引用不变
const num = useState(0); // 由于返回的是一个数组,每次更新num会变,可是若是具体的state值没有变化,num[0]和num[1]的引用不会变。 const [num, setNum] = useState(0); // 这种状况下,若是num的值没变,每次更新num和setNum的引用就不会变 复制代码
tip2: 若是传递的是非对象、非函数的内容,好比number、string,就不必包裹。
全等符比较他们是比较值是否相等,而不是去比较地址是否相等
这里只对props传递进行了距离,一些非props传递的地方,只要用上了对象,都有可能埋下这种隐藏问题。 因此必定要当心对象!
还要明确一点,不少时候,从新执行一段代码(不用useMemo/useCallback)远比存储、比较、取值(使用useMemo/useCallback)来得更快。并且在不少状况,即便用 useMemo/useCallback 进行优化,优化效果也根本看不出来(现代浏览器和计算机速度并不慢)
因此若是不是为了优化多组件嵌套或者高成本计算,不少时候其实也不须要刻意去使用useMemo/useCallback
感兴趣的能够浏览 When to useMemo and useCallback 以获取更多信息
Hook Component 每一次渲染都会从上到下将全部内容从新执行一次,若是有变量,就会创造新变量。
useState(func())
不要这样写代码!
Hook Component 具有 Capture value 的特性
useref
避开 Capture valueuseCallback 和 useMemo 的注意事项
何时须要用他们:
- 成本很高的计算
- 避免子组件无心义的重渲染
- 数据须要传递给其余组件,且数据为对象、函数
何时不须要用他们:
- 仅仅在组件内部使用,不存在向下传递数据。
- 若是要向下传递数据,但数据值是非对象、非函数的值
必定要当心对象!!
有时候 从新执行一段代码远比从缓存中获取一段结果来得更快
若是文中有错误/不足/须要改进/能够优化的地方,但愿能在评论里友善提出,做者看到后会在第一时间里处理
若是你喜欢这篇文章,👍点个赞再走吧,github的星星⭐是对做者持续创做的支持❤️️
相关资料