函数式组件 && React Hook

8,072 阅读19分钟

React Component

binlive前端开发,web开发,node,vue,react,webpack
Class组件是我们现在最常见的组件用法,它拥有组件内的状态和生命周期等一些特性。Class组件是最常见的React组件写法,它需要声明一个class并去继承 react 的Component 类。类组件使用范围广泛,它拥有内部状态、生命周期等一系列方法。
binlive前端开发,web开发,node,vue,react,webpack
函数式组件写法更加简洁,它接收父级传入的 props 对象并返回一个 React 元素,它本质上就是基础的 JavaScript函数

Class Component

React createClass

Class组件的写法创建方法有两种,下图是兼容es5写法的React.createClass定义的组件,这也是react一开始的创建组件的方法。

binlive前端开发,web开发,node,vue,react,webpack

写法说明:
  1. propTypes用来处理校验定义属性的类型校验,PropTypes是从React中获取的,在React16.0.0版本以后废弃了这种写法,PropTypes被单独提取到prop-types包中,所以先比较新的版本中,我们要进行类型校验,需要手动引入 prop-types 这个模块
  2. getInitialState用来初始化state,getDefaultProps用来定义props的默认属性值,这两个函数的用法有点类似于现在vue中的data和props方法
  3. createClass 组件需要定义一个render函数,它会返回 react node用来渲染视图
ES6的class react

这是一个现在常见的ES6的class react组件写法

binlive前端开发,web开发,node,vue,react,webpack

写法说明:
  1. React 在 v0.13.0 引入了 Component API, 配合一些代码转换工具我们可以使用最新的es语法
  2. 组件同样需要定义一个render函数用来渲染视图,由于es6支持了方法简写,render以及其他定义的函数已经不需要声明function 关键字
  3. 在构造函数中初始化了 state,这里能看到有一个 super 调用,ES6 规定,子类的构造函数必须执行一次super方法,如果子类没有定义constructor方法,这个构造函数会被默认添加并调用super。 如果我们声明了constructor函数,就必须在构造函数里调用super,只有在正常调用super(props)方法后才可以访问this对象,在constructor中如果要访问this.props需要在super中传入props,当然也可以直接通过props访问。但是无论有没有定义constructor,super是否传入props参数,在react的其他生命周期中this.props都是可以使用的,这是React自动附带的。
  4. 继承Component API 创建的组件遵循es6 class的规则,react不会帮我们自动绑定this,为了能正确访问this,我们需要在构造函数里绑定this的指向。但是如果方法很多的话,就会周期构造函数里写了很多冗余的绑定函数代码
函数this优雅的解决方案

binlive前端开发,web开发,node,vue,react,webpack
这里要说到类字段语法这个提案 公有类字段的语法定义是 字段名 = 引用值。 在 constructor 方法被执行前, 实例上已经被赋值了该字段内容。

binlive前端开发,web开发,node,vue,react,webpack
所以现在比较流行的写法是这样的 根据公有类字段语法 我们可以直接通过 字段名 = state 对象内容来声明state对象。 由于箭头函数没有自己this,在这里它会指向当前类的实例。借用公有类字段的提案,我们可以直接声明一个 箭头函数来修正方法的this指向。 这样我们就无需在构造函数里去创建state。也不需要在构造函数里写一大堆function绑定this。如果没有其他的操作,我们就完全可以省略构造函数。

React.PureComponent

binlive前端开发,web开发,node,vue,react,webpack

PureComponent 内部是继承React.Component来的,在React.Component组件中我们可以使用shouldComponentUpdate来判断是否重发重新渲染,而 React.PureComponent 内部使用 shallowEqual 进行浅比较。 浅比较会比较更新前后的state和props的长度是否一致,判断是否新增或者减少了props或state的数据个数。 然后它还会调用内部的objectis方法浅层对比前后的state和prop,objectis类似于es6的 Object.is方法, Object.is 类似于 === 全等运算符,只是在比较 +0 跟 -0 时表现的不太一样, === 返回的是true 而他是false。

binlive前端开发,web开发,node,vue,react,webpack
由于浅比较的原因,当props中的参数为引用类型时,修改对象数据中的value时, PureComponent 组件不会触发重新渲染,因为他们是引用的同一个内存地址。

Functional Component

binlive前端开发,web开发,node,vue,react,webpack
函数式组件非常的简洁纯粹,一个函数既是一个组件。

  • 函数组件没有生命周期
  • 也没有它自己的内部状态
  • 因为只是一个函数,在调用时没有内部的 this

上图是一个很简单的函数组件例子,虽然说函数组件本质上就是 一个 JavaScript 函数, 在例子里看起来没有任何使用react的api或者方法,但是我们仍然引入了react,这是因为在组件内部返回的react 元素 使用了 jsx,他实质上是React.createElement的语法糖,配置好babel就可以为我们编译jsx,简化了我们写createElement的过程。

Stateless Component

binlive前端开发,web开发,node,vue,react,webpack
函数式组件在以往我们也称其为无状态组件(Stateless Component)。函数式组件中并不需要实例化而且也没有生命周期与自己内部的状态管理,它只简单的接受props然后进行渲染。在处理一些简单的没有UI展示性内容时使用无状态组件能更好的进行性能优化。

Class 与 Functional 差异

binlive前端开发,web开发,node,vue,react,webpack

函数组件不需要声明类,可以避免大量的譬如extends或者constructor这样的代码 也不需要处理 this 指向的问题。 更加纯粹的是一个函数就是一个组件,React 官方说的 React 组件一直更像是函数,我们写函数式组件似乎也是更加贴近react的原则。 引用透明性是函数式编程的一个概念,我个人觉得函数组件遵循了纯函数的概念。

纯函数

binlive前端开发,web开发,node,vue,react,webpack

引用透明性是函数式编程里的概念。 这是一个 redux 里的 reducer 函数。在 redux 里 reducer 需要被定义为一个纯函数,它符合纯函数的几个定义:

  1. 相同的输入总会有相同的输出
  2. 不会修改函数的输入值 reducer不能够修改state,只能返回一个新的state
  3. 不依赖外部环境状态
  4. 无任何副作用

React Hook

很多时候一开始我们写的代码或者组件都是比较简单的,我们可能会选 函数组件来完成一个 简单的功能模块。但是越到后面可能功能就变的愈加的复杂了,函数组件内可能需要一些自己的状态或者生命周期了。 这时候想要实现这些功能可能就需要把它借助 高阶组件 或者 render props 帮它包装一层class 的父组件,这样它就间接的拥有了状态跟生命周期。但是这也都只是在函数组件外借助了 class 组件的能力。

由于函数组件在每次渲染时候,组件内部都会从上到下重新执行一遍,它也没有办法有自己内部的状态,也无法产生副作用,为了增强函数组件,hook就出现了。

Vue Hook

binlive前端开发,web开发,node,vue,react,webpack

Vue 在放出可能到来 3.0 更新的内容,上图就是这次更新变动比较大的 funtion based api.

对比右侧2.0的写法, template 就是 之前的模板语法。比较特殊的就是 setup 函数。 这里的value是一个包装对象,它就是组件内部的状态,这个看起来跟react 的 usestate hook 还是非常像的。 onMounted 对应的就是以前的生命周期函数 mounted,方法最后的返回对象有点像之前的data方法里的内容被绑定到了template模板语法上,setup 看起来就是一个简单的函数。

binlive前端开发,web开发,node,vue,react,webpack
vue作者提到了 function based api 借鉴了 react hook 的思想。 由于typescript对函数的类型推导能力比较好,之前vue组件比较流行的那种对象的写法现在看起来也变成了setup这样一个函数了。 至于更灵活的逻辑复用能力,这个在后面会看到是如何进行逻辑复用的。

React Hook

binlive前端开发,web开发,node,vue,react,webpack
Hook 是 React 16.8 的新增特性。它是在函数组件的基础上引入了一些新API,可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

我们看到这里使用了一个 useState 的API,它可以在调用时传入参数,并且返回一个数组。数组中有两项内容,第一个是状态(state)、第二个是设置状态(setState),使用es6 数组解构的特性,我们可以根据我们的需要去语义化命名,useState中的init参数只会在方法在第一次初始化以后,即使函数组件被更新,它的state也不会被重置,所以它可以一直保留着当前的状态。 这也就让函数组件拥有了内部状态,以及生命周期。

一些hook用法以及注意点

hook跳过state更新

binlive前端开发,web开发,node,vue,react,webpack

在react 的 class 组件中,我们会定义一个数组类型的 state,在对修改了引用中data里的一些值后,调用this.setState 方法会触发重新渲染,但是在同样的操作在hook里并不会触发更新。

binlive前端开发,web开发,node,vue,react,webpack
调用我们设置 hook 的 setData 更新函数,数并传入当修改过引用类型的data 去触发更新时,React将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较前后 state)由于引用地址没变化 所以不会发生重新渲染。 我们可以使用图中下面的两种方式浅拷贝对象 然后再进行操作,即可解决这一问题。

useEffect介绍

binlive前端开发,web开发,node,vue,react,webpack
useEffect 正如它的名字一样,他可以在函数组件中使用一些副作用,我们可以用来模拟一些生命周期操作。 useEffect 拥有两个参数,第一个参数是一个回调函数,它会在完成一些状态更新以及组件渲染后被触发,第二个参数是一个数组是一个可选的参数.

useEffect的规则如下:

  1. 当第二个不存在时,在第一次初始化和每次重新渲染后都会触发回调。
  2. 当数组存在并有值时,如果数组中的任何值发生更改,则每次渲染后都会触发回调。
  3. 当它是一个空数组时,回调只会被触发一次,类似于 componentDidMount。
  4. 每个 useEffect 都可以返回一个清除函数。

Hooks Rules

binlive前端开发,web开发,node,vue,react,webpack
React 官方文档说明,不要在循环,条件或嵌套函数中调用 Hook,只在最顶层使用 Hook。 只在最顶层使用Hook并不是说要将 hooks 代码写在组件的最上面,而是hooks的代码不能套在一些判断或者循环条件中。这些规范要求最终的目的都是要保证当组件创建跟后续的更新时,每次的 hoos 顺序都是一致的。

为什么Hooks 需要遵循规则

binlive前端开发,web开发,node,vue,react,webpack
这是react源码中对于Hook的类型声明。

  • memoizedState: 存储的上一次全部update队列执行完成之后的状态值
  • baseState: 初始化 initialState 值, 以及每次 调用setter 触发 dispatch 之后 更新的 newState
  • baseUpdate:当前需要更新的 Update
  • Queue: 当前hook的更新队列,它会存储多次更新行为,我们每次 setter 都会 dispatch都会被推到这个队列中

binlive前端开发,web开发,node,vue,react,webpack
我们依次调用三个useState hook

React内部会创建对应的 hook 节点,react 第一次初始化组件时内部会将这些节点结构通过 next 依次连接起来,形成一个单向链表结构,每一次调用setter 就 dispach 一个对应的action方法,将action存储在queue中。

在后续触发渲染时,react 会调用 queue 队列中的内容,queue 也是一个链表结构,它是收集我们调用setter方法,queue 会依次执行内部的action,从而获取到最新的状态值,状态值会被更新到memorizedState上,然后将状态反应到对应的hook字段中形成绑定。所以当我们打乱或者增加减少hooks时,会造成hook内部顺序错误,从而导致无法正常工作。

binlive前端开发,web开发,node,vue,react,webpack

上图是组合成的一个hooks单向链表结构,内部依次通过next指向下一个hook。

尝试通过修改hook数量,在第一次初始化时候我们调用了三个usestate hook,react也会在它内部依次收集了三个hook。 第一次渲染时,页面可以正常的渲染出内容,当我们点击了一个 setter 方法,重新触发了渲染时,会由于 hook 数量比之前的少而导致页面错误,无法正常渲染。所以保证每次都是相同的调用顺序才能正确的使用hook。

binlive前端开发,web开发,node,vue,react,webpack
Hooks API是专门为函数组件设计的,所以我们只能在函数式组件内部或者自定义的hook中使用hooks,包括usestate、useeffect、userref 等以及自定义的hook api。而且自定义hook,也是无法在class组件内部使用的。 当然正常的函数组件,即使在内部使用了hook,我们可以也class组件内部复用该函数组件,这个是不会影响的。

Hook 生命周期

binlive前端开发,web开发,node,vue,react,webpack
Constructor: 我们以前在constructor里更多的是用来初始化state,在使用hook创建state时可以传入 initialState 初始化即可, 如果你有一些复杂的计算逻辑,初始化的参数还可以是一个函数,只要返回计算后的值即可

componentDidMount: 当第二个参数为空数组时,回调只会被触发一次,类似于componentDidMount。

binlive前端开发,web开发,node,vue,react,webpack
componentDidUpdate: 当 useEffect 不传入第二个参数时,在第一次初始化和每次重新渲染后都会触发回调。这与 componentDidUpdate 有些不同,componentDidUpdate 生命周期在初始化挂载的时候不会被调用,我们可以使用 useRef 钩子来存储一个值来判断函数是否为第一次调用

useRef返回一个可变的ref对象,它不仅可以绑定dom的引用,也可以用来存储一些值。其.current属性被初始化为传递的参数(initialValue)。返回的对象将持续整个组件的生命周期。变更 .current 属性不会引发组件重新渲染。

componentWillUnmount 前面提到每个 useEffect 都可以返回一个清除函数。配合这一特性,我们可以任然将第二个参数设置为一个空数组,这样清除函数会在组件卸载前被调用,我们可以在这里面清除一些事件监听器等。更方便的是我们可以将 didMount 和 unMount 写在同一个 useEffect 钩子中。

binlive前端开发,web开发,node,vue,react,webpack
componentWillReceiveProp: 我们想要模拟之前的 componentWillReceiveProps 生命函数,可以将第二个参数设为需要观察的props参数。为了比对前后的 props 变化,可以用 useRef 来存储旧的props来达到目的。

setState的第二个参数 我们知道在使用setState方法时,可以传入第二个参数,这个参数是个回调函数,它在设置完 state 以后 会被调用。 我们在hook里也可以通过用useEffect 钩子传入要监听的 state,当该state更新以后,回调函数会被调用。

逻辑复用

binlive前端开发,web开发,node,vue,react,webpack
在vue和react官方都提到了hook可以更好的逻辑复用,他们的动机都是因为组件间一些重复的逻辑代码无法复用,我们看下如何使用react hooks完成逻辑代码的复用。

自定义Hook

binlive前端开发,web开发,node,vue,react,webpack
这是一个自定义 hook,也就是我们自己创建一个hook。

React 规定 自定义hook 需要用 use 开头来定义,自定义hook虽然不是一个组件,它不需要返回 React Node 元素节点,如果返回了元素,它就又变成的一个函数组件,但是虽然这样,它同样的可以在方法内部使用 状态 usestate 和生命周期 useeffect 等其他的 hooks。他拥有自己的内部状态和生命周期。

比较特殊的是 自定义hook 返回的内容可以任何类型,你可以单纯的返回一个值,也可以返回一个对象或者数组甚至是方法,返回的内容取决于在调用你创建的自定义hook时所需要的一些属性。

用法说明:

我们在这里定义了一个名为 useOnline 的自定义hook, 我们用它来判断当前的网络连接状态。首先hook的方法名是以use开头的,后面的名字可以根据你的意愿来定义,大部分时候hook都会遵循驼峰命名法,只要是以use开头,后面的定义是没有限制的。不过我尝试了一下即使不使用use开头创建的自定义hook其实也是正常运行的。react 约定以use开头是为了通过一些自动检查工具来来校验 这些use开头的hook内部是否违反了 hook的使用规则。不过还是最好遵循使用use开头,驼峰命名这样一个约定。

然后调用 usestate 创建一个 自定义hook内部的状态, 传入了 navigator online 初始化当前的网络状态,并且在自定义 hook 内部拥有了自己的状态。

接着在使用 useeffect 钩子内,我们需要监听网络状态的变化,联网或者断网的监听方法只需要在组件创建时调用一次即可,所以我们在传递useeffect的第二个参数时,只传递了一个空的数组,这样它就只会在 didmount 时被调用一次。

同样的在销毁组件时,我们也需要清除掉这些监听事情。在 effect 钩子的返回函数中清理掉这两个监听方法即可

最后我们将 当前的状态值 作为自定义hook的返回值,这样就完成了一个监听网络状态的自定义hook。

调用 useOnline 自定义hook

binlive前端开发,web开发,node,vue,react,webpack

我们在一个最简单的函数组件中引入这个hook。 然后在函数组件内我们调用这个自定义hook,hook会返回当前的网络状态,这是一个很简单的函数组件,我们只渲染当前的网络状态。

当我们渲染出组件时候他渲染出了当前的网络状态, 当我们切换为离线状态时,可以看组件被重新渲染出了当前的网络状态值。这样,当我们创建出一个自定义hook时 就可以在很多地方调用直接复用这段代码,达到了逻辑复用的目的。

不过大家可能觉得这不就是抽象一个方法么? 好像没有自定义hook 我们也可以把一些方法抽象到一些工具函数中, 写一些 helper 或者 utils来完成这些功能 。 但是其实是不一样的,自定义的hook强大之处在于拥有状态,当状态值改变时,它可以触发调用组件的重新渲染,而且自定义hook可以跟随着组件的生命周期,在不同的生命钩子阶段,我们可以处理一些事件。

自定义生命周期Hook

binlive前端开发,web开发,node,vue,react,webpack
我们在之前也用hook的useeffect API 实现过模拟 class 组件中的生命周期,但是如果函数组件多了,我们可能在很多组件中都会用到这些生命周期,虽然使用 useeffect 实现几个生命周期也只有几行代码,但是在每个组件里都写一遍显然不符合逻辑复用的原则。

我们可以选择自己去 实现 一个 useDidMount 自定义hook,实现起来也非常简单。只需要在钩子内部调用 useeffect hook,将它的第一个参数传递为我们自定义钩子的回调函数参数,将第二个可选的参数设为一个空数组,这样就只会在 didmount时候被调用一次。

开源社区里的自定义Hook

binlive前端开发,web开发,node,vue,react,webpack
这是github上star较多的一个 use hook 项目, 叫 react-use,该项目到目前为止已经有 8.7K的star。

这个开源项目帮我们收集了一系列已经封装好的自定义hook,比如这些生命周期钩子,我们只需要引入即可实现这些生命周期。包括一些 didMount, unmount, update 等钩子。

使用React Hook写的进度条组件

binlive前端开发,web开发,node,vue,react,webpack

分享一个React Hook弧形进度条组件react-arc-progress,这个之前是一个es6方式写的插件,然后之前为了跟进 react hook,将它改造成了一个 react 组件。

  1. 因为使用了 react hooks 的api,所以只能使用在 react 版本 >= 16.8.0 版本
  2. 使用的typescript,如果同样使用了ts时,使用组件对参数以及方法会自动有类型推导提示
  3. 源码使用了tslint 以及 爱彼迎规范 进行了行代码风格校验

重复的造轮子肯定是没有意义的,所以要添加一些独特的功能让它变得更不一样些:

  • 大部分圆环进度条都是一个整圆,这个组件是可以是弧形的,可以自由定义他的起始结束位置。
  • 可以传入一个数值,它会随着进度条的增长自动增加。
  • 可以传入一个或多个文字节点
  • 可以定义进度条的厚度,让内层超过外层
  • 进度条背景可以贴图
  • 动画速度可以通过一个速度阀值去调整,也可以固定一个动画时长

GitHub地址,请给个star吧 ◔ ‸◔