编者荐语:
本文为学习React Hooks系列的第一篇,以计数器的例子为线索详细地介绍了useState
,useMemo
和useCallback
的用法,原理、实现过程、性能优化,以及它们与类组件的PureComponent
,shouldComponentUpdate
的区别和应用场景,建议大家收藏掌握。
useState
- 通过在函数组件里调用
useState
来给组件添加一些内部state
,React 会在重复渲染时保留这个state
,使得函数式组件有了自己的state
- useState唯一的参数:(initialState)初始状态
- useState返回值
[state, setState]
:state
当前状态,setState
更新它的函数 -
- 在
初始渲染
期间,返回的状态(state)与传入的第一个参数(initialState)值相同 - 从第二次渲染开始之后,setState接受一个新的状态参数
newState
,并将组件依次重新渲染
- 在
- 可以在
事件处理函数中
或其它一些地方调用这个函数,类似于class组件的this.setState
,但是它不会把新的state和旧的state进行合并
原理实现 (结合计时器例子)
let lastState
function useState(initialState) {
// 首次渲染时,返回的状态与传入的的initialState值相同
// 第二次开始渲染时,setState接受一个新的状态参数 newState,并将组件依次重新渲染
// 从第二次开始以后,state就开始取 newState的新值了
let state = lastState || initialState
function setState(newState) {
lastState= newState
render()
}
return [state, setState]
}
function Counter(props){
let [state, setState] = useState(1)
return (
<div>
<p>{state}</p>
<button onClick={() => setState(state+1)}>+</button>
</div>
)
}
function render() {
ReactDOM.render(
<Counter/>,
document.getElementById('root')
);
}
render()
每次渲染都是独立的闭包
还记得我们刚接触for循环里面有异步事件(setTimeout)时,输出i的值是多少那道题吗?让我们回顾一下:
for(var i = 0; i<= 3; i++){
(function(i){
setTimeout(_ => {
console.log(i)
}, i*1000)
})(i)
}
当自执行函数
执行时,会创建一个私有的函数执行上下文,每次用到的i值都是函数私有作用域下的,形成了一个闭包,对内部的变量i保存了起来,所以输出0,1,2,3。
那我们来规范一下形成独立的闭包的几大特性
- 每一次渲染都有它自己的 Props and State
- 每一次渲染都有它自己的事件处理函数
- 函数组件每次渲染都会被调用,但是每一次调用中
state
值都是常量(useState[0]),并且它被赋予了当前渲染中的状态值 - 在单次渲染的范围内,props和state始终保持不变
function Counter(props){
let [state, setState] = useState(1)
let alertNumber = () => {
setTimeout(_ => {
console.log('state',state) // 1
}, 3000)
}
return (
<div>
<p>{state}</p>
<button onClick={() => setState(state+1)}>+</button>
<button onClick={alertNumber}>alertNumber</button>
</div>
)
}
上面例子中,,每次输出的state
都是函数私有执行上下文下的私有变量
state,而这个值永远都是初始值1。
那么有的小伙伴就该问了,我如何在组件中,拿到最新的state
值呢:
函数式更新
为了引出函数式更新,我们先举这样一个例子,添加一个add
按钮,先
点击add按钮,再点"+"
将state加到5,大家猜一猜这个最终的值是多少?
function Counter(props){
let [state, setState] = useState(1)
let getNewState = () => {
setTimeout(_ => {
setState(state + 1)
}, 2000)
}
return (
<div>
<p>{state}</p>
<button onClick={() => setState(state+1)}>+</button>
<button onClick={getNewState}>add</button>
</div>
)
}
从上面例子,我们可以得出每次点击"+"
都会让函数组件重新渲染,所以每次state拿到的都是初始状态0
,和我们上文中那个提到的闭包原理相同。
那如果,我们要是想要这个结果显示最新的值,怎么办呢?
setState中还可以传函数
,内部获取最新的状态,基于新状态再更新,将最新的状态显示:
function Counter(props){
let [state, setState] = useState(1)
let getNewState = () => {
setTimeout(_ => {
setState(state => state + 1)
}, 2000)
}
return (
<div>
<p>{state}</p>
<button onClick={() => setState(state+1)}>+</button>
<button onClick={getNewState}>add</button>
</div>
)
}
这个例子中,我们每次拿到的state
都是新状态。(点击"+"之后,加到几,两秒之后再+1,最终显示)
惰性初始state
所谓的惰性初始化state,只会在初次渲染组件的时候起作用。
- initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略
- 如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
- 与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象
例1:初始状态需要通过复杂的函数计算获得
function Counter(props){
let [state, setState] = useState(_ => {
return 60*60*24
})
return (
<div>
<p>{state}</p>
<button onClick={() => setState(state+1)}>+</button>
</div>
)
}
例2:初始化状态的函数里返回的state是对象形式
function Counter(){
const [{name,number},setValue] = useState(_ =>{
return {name:'计数器',number:0};
});
return (
<>
<p>{name}:{number}</p>
<button onClick={()=>setValue({number:number+1})}>+</button>
</>
)
}
useState 与 class 组件 在setState方法合并更新对象上的差别
useState(func)不会合并对象属性
,而class组件的setState会合并对象的属性
那么有同学就该有疑问了,惰性
体现在哪里了呢?
同样以上面的例1来说明:
function Counter(props){
let [state, setState] = useState(60*60*24)
return (
<div>
<p>{state}</p>
<button onClick={() => setState(state+1)}>+</button>
</div>
)
}
如果要是把函数里的内容
,直接当做参数传递进去,会重复渲染
执行的;
如果把整个函数
传递进去,那么它只会在第一次
渲染的时候执行,提高了性能
。
性能优化1——Object.js
核心:通过Object.js
浅比较新老状态的引用地址
,判断是否触发触发setState更新函数
function Counter(props){
console.log('Counter render')
let [state, setState] = useState({number: 1})
return (
<div>
<p>{state.number}</p>
<button onClick={() => setState(state)}>用自己进行更新</button>
<button onClick={() => setState(() => state)}>用函数返回的老值更新</button>
<button onClick={() => setState({number: state.number + 1})}>+</button>
</div>
)
}
性能优化2——减少渲染次数
在 react 中我们经常面临一个子组件渲染优化的问题,在向子组件传递函数props
时,每次render
都会创建新函数
,导致子组件不必要的渲染,浪费性能。
与useState配套使用的两个hooks,useMemo
和useCallback
,它们都能对函数组件进行,减少渲染次数
的优化。
区别是:useCallback 减少函数创建的次数;useMemo 减少对象创建的次数
useMemo
useMemo(callback, deps)用来缓存函数的返回值,它有两个参数:
- callback 执行函数
- deps:依赖项,
只有依赖项变化
,才会重新计算number
的值,空数组以为着不依赖任何变量,则不会执行callback
,如果[number]里有值,并且值变化,则会执行callback
,重新计算number
的值
核心:通过useMemo
缓存函数的返回值,判断依赖项
是否变化,判断是否渲染组件。
- 依赖项不变,不渲染函数
- 依赖项变化,渲染函数
例子1:依赖项不变减少渲染次数
function Child(props) {
console.log('Child render')
return (
<button>{props.data.number}</button>
)
}
// 为函数组件添加一个功能: 如果属性不变,就渲染函数组件
let MemoChild = React.memo(Child)
function App(props){
let [number, setNumber] = useState(1)
const data = React.useMemo(() => ({number}), [])
return (
<div>
<button onClick={() => setNumber(number => number + 1)}>+</button>
<MemoChild data={data}/>
</div>
)
}
Child组件初始化之后,不会重复渲染。即使每次的number
值都更新,但因为依赖项无变化
。
例子2:依赖项变化时,才会渲染函数组件
function Child(props) {
console.log('Child render')
return (
<button>{props.data.number}</button>
)
}
// 为函数组件添加一个功能: 如果属性不变,就渲染函数组件
let MemoChild = React.memo(Child)
function App(props){
let [number, setNumber] = useState(1)
const data = React.useMemo(() => ({number}), [number])
return (
<div>
<button onClick={() => setNumber(number => number + 1)}>+</button>
<MemoChild data={data}/>
</div>
)
}
Child组件初始化之后,随着number
值的改变,组件重复渲染。
看到这里,大家是不是都感觉掌握了useState
的useMemo性能优化了呢,那么再来看下面这样一个例子:
添加了一个input框,实时获取用户输入的值。
例3:
function Child(props) {
console.log('Child render')
return (
<button>{props.data.number}</button>
)
}
// 为函数组件添加一个功能: 如果属性不变,就渲染函数组件
let MemoChild = React.memo(Child)
function App(props){
let [name, setName] = useState('house')
let [number, setNumber] = useState(1)
const data = React.useMemo(() => ({number}))
return (
<div>
<input type="text" value={name} onChange={event => setName(event.target.value)}></input>
<button onClick={() => setNumber(number => number + 1)}>+</button>
<MemoChild data={data}/>
</div>
)
}
从这个例子中,我们可以看出来input
输入框值的改变,会让App
组件,重复渲染,导致每次data
会被实例一个新值
,新的对象,每次的引用地址不一样,所以就会引起Child
组件的重复渲染。
useCallback
useCallback和useMemo的原理相同,只不过在这个例子中,useMemo是看缓存的对象(引用地址
)变化与否,而useCallback是看缓存的函数(引用地址
)变化与否。
function Child(props) {
console.log('Child render')
return (
<button onClick={props.handleClick}>{props.data.number}</button>
)
}
// 为函数组件添加一个功能: 如果属性不变,就渲染函数组件
let MemoChild = React.memo(Child)
function App(props){
console.log('App render')
let [name, setName] = useState('house')
let [number, setNumber] = useState(1)
const data = useMemo(() => ({number}), [number])
const handleClick = React.useCallback(() => setNumber(number + 1), [number])
return (
<div>
<input type="text" value={name} onChange={event => setName(event.target.value)}></input>
<MemoChild handleClick={handleClick} data={data}/>
</div>
)
}
从这个例子中,我们可以看出来input
输入框值的改变,会让App
组件,重复渲染,导致每次handleClick
函数每次都会更新,但是number依赖项
没有变化时,就不会重复渲染Child
组件;当点击"+"
让number
变化时,造成依赖项变化,组件重复渲染。
与PureComponent,shouldComponentUpdate的区别
shouldComponentUpdate
:通过返回true
或false
来控制组件是否渲染的,可以有效的避免组件的一些无意义或者重复的渲染,和避免不必要的重复计算,减少资源浪费,提高性能。
PureComponent
:当子组件继承PureComponent组件时,会自动帮我比较新props
和旧的props
,新的state
和老的state
,如果值相等或者对象含有相同的属性且属性值相等,就不会执行更新阶段的生命周期
;否则会执行更新阶段的生命周期
。
PureComponent
组件的不足:
- 如果属性传递极为复杂,则不能做出准确的判断
- 当传递过来的
state
频繁变化且需要执行更新阶段时,PureComponent
会多帮我们做一层对比判断,就显得累赘了 - 函数组件不能继承
PureComponent
组件
useMemo
和useCallback
的优势:基于缓存的优势,用过简单的依赖项
变化,就能减少渲染次数
。
React.memo()
和React.PureComponent
组件异同:
同:都是对接收的props参数进行浅比较,解决组件在运行时的效率问题,优化组件的重渲染行为。
异:
React.memo()是函数组件
,React.PureComponent是类组件
。
memo
提供一个参数可以让我们自行配置可以对引用数据做比较然后触发render。
看完三件事❤
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
- 点赞,转发让更多的人也能看到介绍内容(收藏不点赞,皆是耍流氓!!)
- 关注公众号 “前端时光屋”,不定期分享原创知识。
- 同时可以期待后续文章ing
也可以来我的个人博客:
前端时光屋:www.javascriptlab.top/