React 新特性 Hooks 讲解及实例(三)

前端小智

本文是 React 系列的第三篇

React 新特性讲解及实例(一)

React 新特性 Hooks 讲解及实例(二)

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

使用 Context Hooks

clipboard.png

使用 Context ,首先顶层先声明 Provier 组件,并声明 value 属性,接着在后代组件中声明 Consumer 组件,这个 Consumer 子组件,只能是唯一的一个函数,函数参数即是 Context 的负载。如果有多个 Context ,ProviderConsumer 任意的顺序嵌套即可。

此外我们还可以针对任意一个 Context 使用 contextType 来简化对这个 Context 负载的获取。但在一个组件中,即使消费多个 Context,contextType 也只能指向其中一个

clipboard.png

在 Hooks 环境中,依旧可以使用 Consumer,但是 ContextType 作为类静态成员肯定是用不了。Hooks 提供了 useContext,不但解决了 Consumer 难用的问题同时也解决了 contextType 只能使用一个 context 的问题。

来个使用类形式的例子:

class Foo extends Component {
  render() {
    return (
      <CountContext.Consumer>
        {
          count => <h1>{count}</h1>
        }
      </CountContext.Consumer>
    )
  }
}

function App (props) {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button type="button"
        onClick={() => {setCount(count + 1) }}
      >
        Click({count})
      </button>
      <CountContext.Provider value={count}>
        <Counter />
      </CountContext.Provider>
    </div>
  )
}

以上就不说解释了,第一篇已经讲过了,接着将 Foo 改成用 contextType 的形式:

class Foo extends Component {
  static contextType = CountContext;
  render() {
    const count = this.context
    return (
      <h1>{count}</h1>
    )
  }
}

接着使用 useContext 形式:

function Foo () {
  const count = useContext(CountContext)
  return (
    <h1>{count}</h1>
  )
}

useContext 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <CountContext.Provider> 的 value prop 决定。

当组件上层最近的 <CountContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 CountContext provider 的 context value 值。

别忘记 useContext 的参数必须是 context 对象本身:

  • 正确: useContext(MyContext)
  • 错误: useContext(MyContext.Consumer)
  • 错误: useContext(MyContext.Provider)

调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化。

使用 Memo Hooks

meno 用来优化函数组件重渲染的行为,当传入属性值都不变的情况下,就不会触发组件的重渲染,否则就会触发组件重渲染。

useMemo 与 memo

meno针对的是一个组件的渲染是否重复执行,而 useMemo 定义的是一个函数逻辑是否重复执行。

clipboard.png

来个粟子:

function Foo (props) {
  return (
    <h1>{props.count}</h1>
  )
}
function App (props) {
  const [count, setCount] = useState(0);

  const double = useMemo(() => {
    return count * 2
  }, [count])
  
  return (
    <div>
      <button type="button"
        onClick={() => {setCount(count + 1) }}
      >
        Click({count}) double: ({double})
      </button>
      <Foo count={count}/>
    </div>
  )
}

运行结果:

图片描述

如上所示,useMemo 语法与 useEffect 是一致的。第一个参数是需要执行的逻辑函数,第二个参数是这个逻辑依赖输入变量组成的数组,如果不传第二个参数,这 useMemo 的逻辑每次就会运行,useMemo 本身的意义就不存在了,所以需要传入参数。所以传入空数组就只会运行一次,策略与 useEffect 是一样的,但有一点比较大的差异就是调用时机,useEffect 执行的是副作用,所以一定是渲染之后才执行,但 useMemo 是需要返回值的,而返回值可以直接参与渲染,因此 useMemo 是在渲染期间完成的。

接下来改造一下 useMemo,让它依赖 count 如下:

const double = useMemo(() => {
  return count * 2
}, [count])

接着只有当 count 变化时,useMemo 才会执行。

图片描述

再次修改 useMemo, 如下:

const double = useMemo(() => {
  return count * 2
}, [count === 3])

现在能断定,count 在等于 3 之前,由于这个条件一直保存 false 不变,double 不会重新计算,所以一直是 0,当 count 等于 3,double 重新计算为 6,当 count 大于 3,double 在重新计算,变成 8,然后就一直保存 8 不变。

图片描述

记住,传入的 useMemo 的函数会在渲染期间执行,请不要在这个函数内部执行与渲染无关的听任,诸如副作用这类操作属于 useEffect 的适用范畴,而不是 useMemo

你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。

使用 useCallback Hooks

接下先看一下使用 memo 优化子组件的例子。

const Foo = memo (function Foo (props) {
  console.log('Counter render')
  return (
    <h1>{props.count}</h1>
  )
})

function App (props) {
  const [count, setCount] = useState(0);

  const double = useMemo(() => {
    return count * 2
  }, [count === 3])

  return (
    <div style={{padding:'100px'}}>
      <button type="button"
        onClick={() => {setCount(count + 1) }}
      >
        Click({count}) double: ({double})
      </button>
      <Foo count={double}/>
    </div>
  )
}

使用 memo 包裹 Foo 组件,这样只有当 double 变化时,Foo 组件才会重新渲染,执行里面的 log,运行结果如下:

图片描述

现在在给 Foo 中的 h1 添加一个 click 事件:

const Foo = memo (function Foo (props) {
  console.log('Counter render')
  return (
    <h1 onClick={props.onClick}>{props.count}</h1>
  )
})

然后在 App 组件中声明 onClick 并传给 Foo 组件:

function App (props) {
  ...
  const onClick = () => {
    console.log('Click')
  }

  return (
    <div style={{padding:'100px'}}>
      ...
      <Foo count={double} onClick={onClick}/>
    </div>
  )
}

看下运行效果:

图片描述

可以看出,每次点击,不管 double 是否有变化, Foo 组件都会被渲染。那就说明每次 App 重新渲染之后, onClick 句柄的变化,导致 Foo 也被连带重新渲染了。count 经常变化可以理解,但是 onClick 就不应该经常变化了,毕竟只是一个函数而已,所以我们要想办法让 onClick 句柄不变化。

想想我们上面讲的 useMemo,可以这样来优化 onClick:

const onClick = useMemo(() => {
  return () => {
    console.log('Click')
  }
}, [])

由于我们传给 useMemo 的第二个参数是一个空数组,那么整个逻辑就只会运行一次,理论上我们返回的 onClick 就只有一个句柄。

运行效果:

图片描述

现在我们把 useCallback 来实现上页 useMemo 的逻辑。

const onClick = useCallback(() => {
  console.log('Click')
},[])

如果 useMemo 返回的是一个函数,那么可以直接使用 useCallback 来省略顶层的函数。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

大家可能有一个疑问,useCallback 这几行代码明明每次组件渲染都会创建新的函数,它怎么就优化性能了呢。

注意,大家不要误会,使用 useCallback 确实不能阻止创建新的函数,但这个函数不一定会被返回,也就是说这个新创建的函数可能会被抛弃。useCallback解决的是解决的传入子组件的函数参数过多变化,导致子组件过多渲染的问题,这里需要理解好。

上述我们第二个参数传入的空数组,在实际业务并没有这么简单,至少也要更新一下状态。举个粟子:

function App (props) {
  ... 
  const [clickCount, setClickCount] = useState(0);
  const onClick = useCallback(() => {
    console.log('Click')
    setClickCount(clickCount + 1)
  },[clickCount, setClickCount])
  ...
}

在 APP 组件中在声明一个 useState,然后在 onClick 中调用 setClickCount,此时 onClick 依赖 clickCount,setClickCount

其实这里的 setClickCount 是不需要写的,因为 React 能保证 setState 每次返回的都是同个句柄。不信,可以看下官方文档 :

clipboard.png

这里的场景,除了直接使用 setClickCount + 1 赋值以外, 还有一种方式甚至连 clickCount都不用依赖。setState 除了传入对应的 state 最新值以外,还可以传入一个函数,函数的参数即这个 state 的当前值,返回就是要更新的值:

const onClick = useCallback(() => {
  console.log('Click')
  setClickCount((clickCount) => clickCount + 1)
},[])

小结

memo 根据属性来决定是否重新渲染组件一样,useMemo 可以根据指定的依赖不决定一段函数逻辑是否重新执行,从而优化性能。

如果 useMemo 的返回值是函数的话,那么就可以简写成 useCallback 的方式,只是简写而已,实际并没有区别。

需要特别注意的是,当依赖变化时,我们能断定 useMemo 一定重新执行。但是,即使依赖不变化我们不能假定它就一定不会重新执行,也就是说,它可以会执行,就是考虑内在优化结果。

我们可以把 useMemo, useCallback 当做一个锦上添花优化手段,不可以过度依赖它是否重新渲染,因为 React 目前没有打包票说一定执行或者一定不执行。

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

clipboard.png

阅读 2.4k

终身学习者
我要先坚持分享20年,大家来一起见证吧。
64.6k 声望
102k 粉丝
0 条评论
64.6k 声望
102k 粉丝
文章目录
宣传栏