深度学习React Hooks系列 - useState

2,133 阅读9分钟

编者荐语:

本文为学习React Hooks系列的第一篇,以计数器的例子为线索详细地介绍了useStateuseMemouseCallback的用法,原理、实现过程、性能优化,以及它们与类组件的PureComponentshouldComponentUpdate的区别和应用场景,建议大家收藏掌握。

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,useMemouseCallback,它们都能对函数组件进行,减少渲染次数的优化。

区别是: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变化时,造成依赖项变化,组件重复渲染。 useCallback性能优化

与PureComponent,shouldComponentUpdate的区别

shouldComponentUpdate:通过返回truefalse来控制组件是否渲染的,可以有效的避免组件的一些无意义或者重复的渲染,和避免不必要的重复计算,减少资源浪费,提高性能。

PureComponent:当子组件继承PureComponent组件时,会自动帮我比较新props旧的props新的state老的state,如果值相等或者对象含有相同的属性且属性值相等,就不会执行更新阶段的生命周期;否则会执行更新阶段的生命周期

PureComponent组件的不足:

  • 如果属性传递极为复杂,则不能做出准确的判断
  • 当传递过来的state频繁变化且需要执行更新阶段时,PureComponent会多帮我们做一层对比判断,就显得累赘了
  • 函数组件不能继承PureComponent组件

useMemouseCallback的优势:基于缓存的优势,用过简单的依赖项变化,就能减少渲染次数

React.memo()React.PureComponent组件异同:

同:都是对接收的props参数进行浅比较,解决组件在运行时的效率问题,优化组件的重渲染行为。

异: React.memo()是函数组件,React.PureComponent是类组件

memo提供一个参数可以让我们自行配置可以对引用数据做比较然后触发render。

看完三件事❤

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发让更多的人也能看到介绍内容(收藏不点赞,皆是耍流氓!!)
  2. 关注公众号 “前端时光屋”,不定期分享原创知识。
  3. 同时可以期待后续文章ing

也可以来我的个人博客:

前端时光屋:www.javascriptlab.top/