资讯专栏INFORMATION COLUMN

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

zero / 2668人阅读

摘要:还可以返回另一个回调函数,这个函数的执行时机很重要。对于第二个我们可以通过返回一个回调函数来注销事件的注册。回调函数在视图被销毁之前触发,销毁的原因有两种重新渲染和组件卸载。

本文是 React 系列的第二篇

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

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

什么是 Hooks

Hook 是 React 16.8 的新增特性。它可以让你在不编写 类组件 的情况下使用 state 以及其他的 React 特性。

类组件的不足

状态逻辑复用难

缺少复用机制

渲染属性和高阶组件导致层级冗余

趋向复杂难以维护

生命周期函数混杂不相干逻辑

相干逻辑分散在不同生命周期

this 指向困扰

内联函数过度创建新句柄

类成员函数不能保证 this

Hooks 优势

优化类组件的三大问题

函数组件无 this 问题

自定义 Hook 方便复用状态逻辑

副作用的关注点分离

使用 State Hook
import React, {Component} from "react"

class App extends Component {
  state = {
    count: 0
  };
  render() {
    const {count} = this.state;
    return (
      
    )
  }
}
export default App;

以上代码很好理解,点击按钮让 count 值加 1

接下来我们使用 useState 来实现上述功能。

import React, {useState} from "react"

function App () {
  const [count, setCount] = useState(0)
  return (
    
  )
}

在这里,useState 就是一个 Hook。通过在函数组件里调用它来给组件添加一些内部 state,React 会在重复渲染时保留这个 state

useState 会返回一对值:当前状态和一个让你更新它的函数。你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。useState 唯一的参数就是初始 state

useState 让代码看起来简洁了,但是我们可能会对组件中,直接调用 useState 返回的状态会有些懵。既然 userState 没有传入任何的环境参数,它怎么知道要返回的的是 count 的呢,而且还是这个组件的 count 不是其它组件的 count

初浅的理解: useState 确实不知道我们要返回的 count,但其实也不需要知道,它只要返回一个变量就行了。数组解构的语法让我们在调用 useState 时可以给 state 变量取不同的名字。

useState 怎么知道要返回当前组件的 state?

因为 JavaScript 是单线程的。在 useState 被调用时,它只能在唯一一个组件的上下文中。

有人可能会问,如果组件内有多个 usreState,那 useState 怎么知道哪一次调用返回哪一个 state 呢?

这个就是按照第一次运行的次序来顺序来返回的。

接着上面的例子我们在声明一个 useState:

...
const [count, setScount] = useState(0)
const [name, setName] = useState("小智")
...

然后我们就可以断定,以后APP组件每次渲染的时候,useState 第一次调用一定是返回 count,第二次调用一定是返回 name。不信的话来做个实验:

let id = 0

function App () {
  let name,setName;
  let count,setCount;
  
  id += 1;
  if (id & 1) {
    // 奇数
    [count, setCount] = useState(0)
    [name, setName] = useState("小智")
  } else {
    // 偶数
    [name, setName] = useState("小智")
    [count, setCount] = useState(0)
  }

  return (
    
  )
}

首先在外部声明一个 id,当 id为奇数和偶数的时候分别让 useState 调用方式相反,运行会看到有趣的现象。

当前版本如果写的顺序不一致就会报错。

会发现 countname 的取值串了。我们希望给 count 加 1,现在却给 name 加了 1,说明 setCount 函数也串成了 setName 函数。

为了防止我们使用 useState 不当,React 提供了一个 ESlint 插件帮助我们检查。

eslint-plugin-react-hooks

优化点

通过上述我们知道 useState 有个默认值,因为是默认值,所以在不同的渲染周期去传入不同的值是没有意义的,只有第一次传入的才有效。如下所示:

...
const defaultCount = props.defaultCount || 0
const [count, setCount] = useState(defaultCount)
...

state 的默认值是基于 props,在 APP 组件每次渲染的时候 const defaultCount = props.defaultCount || 0 都会运行一次,如果它复杂度比较高的话,那么浪费的资料肯定是可观的。

useState 支持传入函数,来延迟初始化:

const [count, setCount] = useState(() => {
  return props.defaultCount || 0
})

使用 Effect Hook

Effect Hook 可以让你在函数组件中执行副作用操作。数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是"副作用"这个名字,应该都在组件中使用过它们。

副作用的时机

Mount 之后 对应 componentDidMount

Update 之后 对应 componentDidUpdate

Unmount 之前 对应 componentWillUnmount

现在使用 useEffect 就可以覆盖上述的情况。

为什么一个 useEffect 就能涵盖 Mount,Update,Unmount 等场景呢。

useEffect 标准上是在组件每次渲染之后调用,并且会根据自定义状态来决定是否调用还是不调用。

第一次调用就相当于componentDidMount,后面的调用相当于 componentDidUpdateuseEffect 还可以返回另一个回调函数,这个函数的执行时机很重要。作用是清除上一次副作用遗留下来的状态。

比如一个组件在第三次,第五次,第七次渲染后执行了 useEffect 逻辑,那么回调函数就会在第四次,第六次和第八次渲染之前执行。严格来讲,是在前一次的渲染视图清除之前。如果 useEffect 是在第一次调用的,那么它返回的回调函数就只会在组件卸载之前调用了,也就是
componentWillUnmount

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

举粟说明一下:

class App extends Component {
  state = {
    count: 0,
    size: {
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    }
  };
  onResize = () => {
    this.setState({
      size: {
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
      }
    })
  }
  componentDidMount () {
    document.title = this.state.count;
    window.addEventListener("resize", this.onResize, false)
  }
  componentWillMount () {
    window.removeEventListener("resize", this.onResize, false)
  }
  componentDidUpdate () {
    document.title = this.state.count;
  }
  render() {
    const {count, size} = this.state;
    return (
      
    )
  }
}

上面主要做的就是网页 title 显示count 值,并监听网页大小的变化。这里用到了componentDidMountcomponentDidUpdate 等副作用,因为第一次挂载我们需要把初始值给 title, 当 count 变化时,把变化后的值给它 title,这样 title 才能实时的更新。

注意,我们需要在两个生命周期函数中编写重复的代码。

这边我们容易出错的地方就是在组件结束之后要记住销毁事件的注册,不然会导致资源的泄漏。现在我们把 App 组件的副作用用 useEffect 实现。

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

  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
  });

  const onResize = () => {
    setSize({
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
      }
    )
  }

  useEffect(() => {
    document.title = count;
  })

  useEffect(() => {
    window.addEventListener("resize", onResize, false);
    return () => {
      window.removeEventListener("resize", onResize, false)
    }
  }, [])

  return (
    
  )
}

对于上述代码的第一个 useEffect,相比类组件,Hooks 不在关心是 mount 还是 update。用useEffect统一在渲染后调用,就完整追踪了 count 的值。

对于第二个 useEffect,我们可以通过返回一个回调函数来注销事件的注册。回调函数在视图被销毁之前触发,销毁的原因有两种:重新渲染和组件卸载

这边有个问题,既然 useEffect 每次渲染后都执行,那我们每次都要绑定和解绑事件吗?当然是完全不需要,只要使用 useEffect 第二个参数,并传入一个空数组即可。第二个参数是一个可选的数组参数,只有数组的每一项都不变的情况下,useEffect 才不会执行。第一次渲染之后,useEffect 肯定会执行。由于我们传入的空数组,空数组与空数组是相同的,因此 useEffect 只会在第一次执行一次。

这也说明我们把 resize 相关的逻辑放在一直写,不在像类组件那样分散在两个不同的生命周期内。同时我们处理 title 的逻辑与 resize 的逻辑分别在两个 useEffect 内处理,实现关注点分离。

我们在定义一个 useEffect,来看看通过不同参数,第二个参数的不同作用。

...
useEffect(() => {
  console.log("count:", count)
}, [count])
...

第二个参数我们传入 [count], 表示只有 count 的变化时,我才打印 count 值,resize 变化不会打印。

运行效果如下:

第二个参数的三种形态,undefined,空数组及非空数组,我们都经历过了,但是咱们没有看到过回调函数的执行。

现在有一种场景就是在组件中访问 Dom 元素,在 Dom元素上绑定事件,在上述的代码中添加以下代码:

 ...
 const onClick = () => {
  console.log("click");
 }

 useEffect(() => {
   document.querySelector("#size").addEventListener("click", onClick, false);
 },[])
 
  return (
    
... size: {size.width}x{size.height}
)

新增一个 DOM 元素,在新的 useEffect 中监听 span 元素的点击事件。

运行效果:

假如我们 span 元素可以被销毁重建,我们看看会发生什么情况,改造一下代码:

return (
  
... { count%2 ? 我是span :

我是p

}

运行效果:

可以看出一旦 dom 元素被替换,我们绑定的事件就失效了,所以咱们始终要追踪这个dom 元素的最新状态。

使用 useEffect ,最合适的方式就是使用回调函数来处理了,同时要保证每次渲染后都要重新运行,所以不能给第二次参数设置 [],改造如下:

useEffect(() => {
 document.querySelector("#size").addEventListener("click", onClick, false);
   return () => {
     document.querySelector("#size").removeEventListener("click", onClick, false);
   }
})

运行结果:

参考

React 官方文档

《React劲爆新特性Hooks 重构去哪儿网》

交流

React 官方文档

《React劲爆新特性Hooks 重构去哪儿网》

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

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

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/104868.html

相关文章

  • React 特性 Hooks 讲解实例(四)

    摘要:粟例说明一下获取子组件或者节点的句柄指向已挂载到上的文本输入元素本质上,就像是可以在其属性中保存一个可变值的盒子。粟例说明一下渲染周期之间的共享数据的存储上述使用声明两个副作用,第一个每隔一秒对加,因为只需执行一次,所以每二个参为空数组。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! React 新特性讲解及实例(一) React 新特性 Hooks 讲解及实...

    aboutU 评论0 收藏0
  • React 特性 Hooks 讲解实例(三)

    摘要:来个使用类形式的例子以上就不说解释了,第一篇已经讲过了,接着将改成用的形式接着使用形式接收一个对象的返回值并返回该的当前值。如果的返回值是函数的话,那么就可以简写成的方式,只是简写而已,实际并没有区别。 本文是 React 系列的第三篇 React 新特性讲解及实例(一) React 新特性 Hooks 讲解及实例(二) 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着...

    _Dreams 评论0 收藏0
  • React 特性讲解实例(一)

    摘要:接收一个属性,这个组件会让后代组件统一提供这个变量值。因此对于同一个对象而言,一定是后代元素。解决方法就是把内联函数提取出来,如下讲了这么多,我们还没有讲到其实我们已经讲完了的工作原理了。 本节主要讲解以下几个新的特性: Context ContextType lazy Suspense 错误边界(Error boundaries) memo 想阅读更多优质文章请猛戳GitHub博...

    Betta 评论0 收藏0
  • React系列 --- 从Mixin到HOC再到HOOKS(四)

    摘要:返回元素的是将新的与原始元素的浅层合并后的结果。生命周期方法要如何对应到函数组件不需要构造函数。除此之外,可以认为的设计在某些方面更加高效避免了需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。 React系列 React系列 --- 简单模拟语法(一)React系列 --- Jsx, 合成事件与Refs(二)React系列 --- virtualdom diff算法实...

    Lionad-Morotar 评论0 收藏0
  • React Hooks 解析(下):进阶

    摘要:第一次了解这项特性的时候,真的有一种豁然开朗,发现新大陆的感觉。在绝大多数情况下,是更好的选择。唯一例外的就是需要根据新的来进行操作的场景。会保证在页面渲染前执行,也就是说页面渲染出来的是最终的效果。上面条规则都是为了保证调用顺序的稳定性。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、...

    APICloud 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<