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

作者: xiaozhi 发布时间: 2019-09-05 浏览: 2347 次 编辑

使用 Context Hooks

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

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

在 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 定义的是一个函数逻辑是否重复执行。

来个粟子:

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 每次返回的都是同个句柄。不信,可以看下官方文档 :

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

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

小结

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

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

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

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