在 React 的世界中,有容器组件和 UI 组件之分,在 React Hooks 出现之前,UI 组件我们可以使用函数,无状态组件来展示 UI,而对于容器组件,函数组件就显得无能为力,我们依赖于类组件来获取数据,处理数据,并向下传递参数给 UI 组件进行渲染。使用 React Hooks 相比于从前的类组件有以下几点好处:
- 代码可读性更强,原本同一块功能的代码逻辑被拆分在了不同的生命周期函数中,容易使开发者不利于维护和迭代,通过 React Hooks 可以将功能代码聚合,方便阅读维护
- 组件树层级变浅,在原本的代码中,我们经常使用 HOC/render props 等方式来复用组件的状态,增强功能等,无疑增加了组件树层数及渲染,而在 React Hooks 中,这些功能都可以通过强大的自定义的 Hooks 来实现
Hook 是 React 16.8.0 版本增加的新特性,可以在函数组件中使用 state 以及其他的 React 特性 。 Hooks只能在函数式组件中使用,既无状态组件(所有钩子在用时都要先引入)
1、Hook 使用规则
Hook 就是JavaScript 函数 ,但是使用它们会有两个额外的规则: 1、只能在函数最外层调用 Hook 。不要在循环、条件判断 或者嵌套函数(子函数) 中调用。 2、只能在 React 的函数组件 中调用 Hook 。不要在其他 JavaScript 函数中调用。 3、在多个useState() 调用中,渲染之间的调用顺序 必须相同 。
2、memo
当子组件是类组件 的时候可以使用shouldComponentUpdate 钩子函数或类组件继承PureComponent 来实现不渲染子组件 。但是对于函数组件来说是不能用这两个方法的,因此react官方给函数组件提供了memo来对函数组件包装下,实现不必要的渲染。
(1)官网描述
React.memo 为高阶组件 。
如果你的组件在相同 props 的情况下渲染相同的结果 ,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染 结果的方式来提高组件的性能表现 。 这意味着在这种情况下,React 将跳过渲染组件 的操作并直接复用最近一次 渲染的结果。
React.memo 仅检查 props 变更 。如果函数组件被 React.memo 包裹,且其实现中拥有 useState,useReducer 或 useContext 的 Hook,当 context 发生变化 时,它仍会重新渲染 。
(2)实例说明
import React , {useState} from 'react';
const Parent = () => {
const [count, setCount] = useState(0);
return (
<div>
父组件的count---------{count}
<button onClick={()=>setCount(count + 1)}>点击按钮</button>
<Child />
</div>
)
};
const Child = () => {
console.log('子组件-----render');
return (
<div>
子组件
</div>
)
}
export default Parent;
 这里我们父组件内部改变count,但是count值并没有传递给子组件,但是子组件一样的重新渲染了,这并不是我们希望看到的。
所以我们使用memo 对子组件包装。
import React , {useState , memo} from 'react';
const Parent = () => {
const [count, setCount] = useState(0);
return (
<div>
父组件的count---------{count}
<button onClick={()=>setCount(count + 1)}>点击按钮</button>
<Child />
</div>
)
};
const Child = memo(() => {
console.log('子组件-----render');
return (
<div>
子组件
</div>
)
})
export default Parent;
 使用memo进行包装之后,我们可以发现,子组件没有随着父组件的count改变而多次渲染。
(3)注意点
当父组件传递状态给子组件 的时候,即使子组件并没有使用 父组件传递过来的状态,这种情况下依然会渲染子组件,memo好像没什么效果 。
import React , {useState , memo} from 'react';
const Parent = () => {
const [count, setCount] = useState(0);
return (
<div>
父组件的count---------{count}
<button onClick={()=>setCount(count + 1)}>点击按钮</button>
{}
<Child aaa={count}/>
</div>
)
};
const Child = memo(() => {
console.log('子组件-----render');
return (
<div>
子组件
</div>
)
})
export default Parent;
 所以我们需要配合使用useCallback 和useMemo 来帮助我们进行优化。
3、useMemo和useCallback的区别
- useMemo和useCallback都是
具有缓存作用 的,只是他们缓存对象不一样 ,一个是属性 ,一个是缓存函数 。但是,特点都是:当缓存依赖的没变,去获取还是获取曾经的缓存 。 useMemo 是对函数组件中的属性包装 ,返回 一个具有缓存效果的新的属性 ,当依赖的属性没变化 的时候,这个返回新属性就会从缓存中获取之前的 。useCallback 是对函数组件中的方法缓存 ,返回 一个被缓存的方法 。
官网上有这么一句话来描述两者关系:
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
4、useMemo
(1)官网描述
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个 memoized 值 。- 把“
创建”函数 和依赖项数组 作为参数传入 useMemo,它仅会在某个依赖项改变 时才重新计算 memoized 值 。这种优化有助于避免在每次渲染时都进行高开销的计算 。 - 记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值 。
(2)实例说明
- 类似上面的方式我们在父组件更新数据,观察子组件变化
(不使用useMemo)
import React , {useState,memo} from 'react';
const Parent = () => {
const [count, setCount] = useState(0);
const [number, setNumber]=useState(0)
const userInfo = {
age: count,
name: 'hello',
}
const btnHandler = () => {
setNumber(number => number+1);
}
return (
console.log('父组件-----render'),
<div>
number:{number}-----count:{count}
<button onClick={btnHandler}>按钮</button>
<Child userInfo={userInfo}></Child>
</div>
);
};
const Child = memo(() => {
return (
console.log('子组件-----render'),
<div>
子组件
</div>
)
})
export default Parent;
 我们发现,仅仅是在父组件更新了number的值,传递给子组件的userInfo对象值并没有变化,但是每次子组件都重新更新了,虽然我们在子组件上用了React.memo包装还是不行。这是因为在父组件中每次重新渲染,对于对象来说会是重新一个新的对象了 。因此子组件要重新更新 。
- 使用
useMemo对属性的包装
const userInfo = useMemo(() => {
return {
age: count,
name: 'hello',
};
}, [count]);
使用useMemo包装后的对象,重新返回一个具有缓存效果的新对象 。
第二个参数 表示依赖性 ,或者叫观察对象 ,当被观察的没变化 ,返回的就是缓存对象 ;如果被观察的变化了 ,那么就会返回新的 ,现在不管你怎么更新number的值,子组件都不会重新更新了。
这时再看效果图 
(3)注意点
useMemo要配合React.memo来使用 ,不然传递到子组件也是不生效的。
5、useCallback
(1)官网描述
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
- 返回一个 memoized 回调函数。
- 把
内联回调函数 及依赖项数组 作为参数传入 useCallback,它将返回该回调函数的 memoized 版本 ,该回调函数仅在某个依赖项改变时才会更新 。
(2)实例说明
- 类似上面的方式我们在父组件更新数据,观察子组件变化
(不使用useCallback)
import React , {useState,useRef,memo} from 'react';
const Parent = () => {
const [text, updateText] = useState('');
const textRef = useRef(text);
const handleSubmit = () => {
console.log('当前的值', text);
}
return(
console.log('父组件----render'),
<div>
我是父组件
<input type="text" value={text} onChange={(e) => updateText(e.target.value)}/>
<Child onAdd={handleSubmit}/>
</div>
)
};
const Child = memo((props) => {
return (
console.log('子组件----render'),
<button onClick={props.onAdd}>点击按钮获取值</button>
)
})
export default Parent;
 我们发现,仅仅是在父组件输入的时候更新了text值,但是结果是每次输入框输入值的时候,子组件就会重新渲染一次。其实子组件中仅仅是一个按钮,要获取最终输入的值,每次父组件输入值的时候,子组件就更新,很耗性能的。
- 使用
useCallback 来包装一个方法
import React , {useState,useRef,memo,useCallback,useEffect} from 'react';
const Parent = () => {
const [text, updateText] = useState('');
const textRef = useRef(text);
const handleSubmit = useCallback(() => {
console.log('当前输入框的值:', textRef.current);
}, [textRef])
useEffect(() => {
textRef.current = text;
}, [text]);
return(
console.log('父组件----render'),
<div>
我是父组件
<input type="text" value={text} onChange={(e) => updateText(e.target.value)}/>
<Child onAdd={handleSubmit}/>
</div>
)
};
效果图 
(3)注意点
useCallback 最好 要配合React.memo来使用 ,不然会起到反向优化的效果。
|