听说你还不懂React Hook?

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

从官网的这句话中,我们可以明确的知道,Hook增加了函数式组件中state的使用,在之前函数式组件是无法拥有自己的状态,只能通过props以及context来渲染自己的UI,而在业务逻辑中,有些场景必须要使用到state,那么我们就只能将函数式组件定义为class组件。而现在通过Hook,我们可以轻松的在函数式组件中维护我们的状态,不需要更改为class组件。

React Hooks要解决的问题是状态共享,这里的状态共享是指只共享状态逻辑复用,并不是指数据之间的共享。我们知道在React Hooks之前,解决状态逻辑复用问题,我们通常使用higher-order components和render-props,那么既然已经有了这两种解决方案,为什么React开发者还要引入React Hook?对于higher-order components和render-props,React Hook的优势在哪?

React Hook例子

我们先来看一下React官方给出的React Hook的demo

import { useState } from 'React';
function Example() {
 // Declare a new state variable, which we'll call "count"
 const [count, setCount] = useState(0);
 return (
 <div>
 <p>You clicked {count} times</p>
 <button onClick={() => setCount(count + 1)}>
 Click me
 </button>
 </div>
 );
}
复制代码

我们再来看看不用React Hook的话,如何实现

class Example extends React.Component {
 constructor(props) {
 super(props);
 this.state = {
 count: 0
 };
 }
 render() {
 return (
 <div>
 <p>You clicked {this.state.count} times</p>
 <button onClick={() => this.setState({ count: this.state.count + 1 })}>
 Click me
 </button>
 </div>
 );
 }
}
复制代码

可以看到,在React Hook中,class Example组件变成了函数式组件,但是这个函数式组件却拥有的自己的状态,同时还可以更新自身的状态。这一切都得益于useState这个Hook,useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并

React复用状态逻辑的解决方案

Hook是另一种复用状态逻辑的解决方案,React开发者一直以来对状态逻辑的复用方案不断提出以及改进,从Mixin到高阶组件到Render Props 到现在的Hook,我们先来简单了解一下以前的解决方案

Mixin模式

听说你还不懂React Hook?

在React最早期,提出了根据Mixin模式来复用组件之间的逻辑。在Javascript中,我们可以将Mixin继承看作是通过扩展收集功能的一种途径.我们定义的每一个新的对象都有一个原型,从中它可以继承更多的属性.原型可以从其他对象继承而来,但是更重要的是,能够为任意数量的对象定义属性.我们可以利用这一事实来促进功能重用。

React中的mixin主要是用于在完全不相关的两个组件中,有一套基本相似的功能,我们就可以将其提取出来,通过mixin的方式注入,从而实现代码的复用。例如,在不同的组件中,组件需要每隔一段时间更新一次,我们可以通过创建setInterval()函数来实现这个功能,同时在组件销毁的时候,我们需要卸载此函数。因此可以创建一个简单的 mixin,提供一个简单的 setInterval() 函数,它会在组件被销毁时被自动清理。

var SetIntervalMixin = {
 componentWillMount: function() {
 this.intervals = [];
 },
 setInterval: function() {
 this.intervals.push(setInterval.apply(null, arguments));
 },
 componentWillUnmount: function() {
 this.intervals.forEach(clearInterval);
 }
};
var createReactClass = require('create-React-class');
var TickTock = createReactClass({
 mixins: [SetIntervalMixin], // 使用 mixin
 getInitialState: function() {
 return {seconds: 0};
 },
 componentDidMount: function() {
 this.setInterval(this.tick, 1000); // 调用 mixin 上的方法
 },
 tick: function() {
 this.setState({seconds: this.state.seconds + 1});
 },
 render: function() {
 return (
 <p>
 React has been running for {this.state.seconds} seconds.
 </p>
 );
 }
});
ReactDOM.render(
 <TickTock />,
 document.getElementById('example')
);
复制代码

mixin的缺点

  1. 不同mixin可能会相互依赖,耦合性太强,导致后期维护成本过高

  2. mixin中的命名可能会冲突,无法使用同一命名的mixin

  3. mixin即使开始很简单,它们会随着业务场景增多,时间的推移产生滚雪球式的复杂化

具体缺点可以看此链接Mixins是一种祸害

因为mixin的这些缺点存在,在React中已经不建议使用mixin模式来复用代码,React全面推荐使用高阶组件来替代mixin模式,同时ES6本身是不包含任何 mixin 支持。因此,当你在 React 中使用 ES6 class 时,将不支持 mixins 。

听说你还不懂React Hook?

高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式

高级组件并不是React提供的API,而是React的一种运用技巧,高阶组件可以看做是装饰者模式(Decorator Pattern)在React的实现。装饰者模式: 动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案.

具体而言,高阶组件是参数为组件,返回值为新组件的函数。

组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件

我们可以通过高阶组件动态给其他组件增加日志打印功能,而不影响原先组件的功能

function logProps(WrappedComponent) {
 return class extends React.Component {
 componentWillReceiveProps(nextProps) {
 console.log('Current props: ', this.props);
 console.log('Next props: ', nextProps);
 }
 render() {
 return <WrappedComponent {...this.props} />;
 }
 }
}
复制代码

Render Propss

术语 “Render Props” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术

具有 Render Props 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑

以下我们提供了一个带有prop的<Mouse>组件,它能够动态决定什么需要渲染,这样就能对<Mouse>组件的逻辑以及状态复用,而不用改变它的渲染结构。

class Cat extends React.Component {
 render() {
 const mouse = this.props.mouse;
 return (
 <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
 );
 }
}
class Mouse extends React.Component {
 constructor(props) {
 super(props);
 this.handleMouseMove = this.handleMouseMove.bind(this);
 this.state = { x: 0, y: 0 };
 }
 handleMouseMove(event) {
 this.setState({
 x: event.clientX,
 y: event.clientY
 });
 }
 render() {
 return (
 <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
 {this.props.render(this.state)}
 </div>
 );
 }
}
class MouseTracker extends React.Component {
 render() {
 return (
 <div>
 <h2>移动鼠标!</h2>
 <Mouse render={mouse => (
 
 )}/>
 </div>
 );
 }
}
复制代码

然而通常我们说的Render Props 是因为模式才被称为 Render Props ,又不是因为一定要用render对prop进行命名。我们也可以这样来表示

<Mouse>
 {mouse => (
 <Cat mouse={mouse} />
 )}
</Mouse>
复制代码

React Hook动机

React Hook是官网提出的又一种全新的解决方案,在了解React Hook之前,我们先看一下React Hook提出的动机

  1. 在组件之间复用状态逻辑很难

  2. 复杂组件变得难以理解

  3. 难以理解的 class

下面说说我对这三个动机的理解:

在组件之间复用状态逻辑很难,在之前,我们通过高阶组件(Higher-Order Components)和渲染属性(Render Propss)来解决状态逻辑复用困难的问题。很多库都使用这些模式来复用状态逻辑,比如我们常用redux、React Router。高阶组件、渲染属性都是通过组合来一层层的嵌套共用组件,这会大大增加我们代码的层级关系,导致层级的嵌套过于夸张。从React的devtool我们可以清楚的看到,使用这两种模式导致的层级嵌套程度

听说你还不懂React Hook?

复杂组件变得难以理解,在不断变化的业务需求中,组件逐渐会被状态逻辑以及副作用充斥,每个生命周期常常会包含一些不相关的逻辑。我们写代码通常都依据函数的单一原则,一个函数一般只处理一件事,但在生命周期钩子函数中通常会同时做很多事情。比如,在我们需要在componentDidMount中发起ajax请求获取数据,同时有时候也会把事件绑定写在此生命周期中,甚至有时候需要在componentWillReceiveProps中对数据进行跟componentDidMount一样的处理。

相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

难以理解的class,个人觉得使用class组件这种还是可以的,只要了解了class的this指向绑定问题,其实上手的难度不大。大家要理解,这并不是 React 特有的行为;这其实与 JavaScript 函数工作原理有关。所以只要了解好JS函数工作原理,其实this绑定都不是事。只是有时候为了保证this的指向正确,我们通常会写很多代码来绑定this,如果忘记绑定的话,就有会各种bug。绑定this方法:

1.this.handleClick = this.handleClick.bind(this);
2.<button onClick={(e) => this.handleClick(e)}>
 Click me
 </button>
复制代码

于是为了解决以上问题,React Hook就被提出来了

state Hook使用

我们回到刚刚的代码中,看一下如何在函数式组件中定义state

import React, { useState } from 'React';
const [count, setCount] = useState(0);
复制代码
  1. useState做了啥

  2. 我们可以看到,在此函数中,我们通过useState定义了一个’state变量’,它与 class 里面的 this.state 提供的功能完全相同.相当于以下代码

class Example extends React.Component {
 constructor(props) {
 super(props);
 this.state = {
 count: 0
 };
 }
复制代码
  1. useState参数

  2. 在代码中,我们传入了0作为useState的参数,这个参数的数值会被当成count初始值。当然此参数不限于传递数字以及字符串,可以传入一个对象当成初始的state。如果state需要储存多个变量的值,那么调用多次useState即可

  3. useState返回值

  4. 返回值为:当前 state 以及更新 state 的函数,这与 class 里面 this.state.count 和 this.setState 类似,唯一区别就是你需要成对的获取它们。看到[count, setCount]很容易就能明白这是ES6的解构数组的写法。相当于以下代码

let _useState = useState(0);// 返回一个有两个元素的数组
let count = _useState[0];// 数组里的第一个值
let setCount = _useState[1];// 数组里的第二个值
复制代码

读取状态值

只需要使用变量即可

以前写法

<p>You clicked {this.state.count} times</p>
复制代码

现在写法

<p>You clicked {count} times</p>
复制代码

更新状态

通过setCount函数更新

以前写法

<button onClick={() => this.setState({ count: this.state.count + 1 })}>
 Click me
 </button>
复制代码

现在写法

 <button onClick={() => setCount(count + 1)}>
 Click me
 </button>
复制代码

这里setCount接收的参数是修改过的新状态值

声明多个state变量

我们可以在一个组件中多次使用state Hook来声明多个state变量

function ExampleWithManyStates() {
 // 声明多个 state 变量!
 const [age, setAge] = useState(42);
 const [fruit, setFruit] = useState('banana');
 const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
 // ...
}
复制代码

React 假设当你多次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的

为什么React要规定每次渲染它们时的调用顺序不变呢,这个是一个理解Hook至关重要的问题

Hook 规则

Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则。并且React要求强制执行这两条规则,不然就会出现异常的bug

  1. 只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们

  1. 只在 React 函数中调用 Hook

不要在普通的 JavaScript 函数中调用 Hook

这两条规则出现的原因是,我们可以在单个组件中使用多个State Hook 或 Effect Hook,React 靠的是 Hook 调用的顺序来知道哪个 state 对应哪个useState

function Form() {
 const [name1, setName1] = useState('Arzh2');
 const [name2, setName2] = useState('Arzh3');
 const [name3, setName3] = useState('Arzh4');
 // ...
}
// ------------
// 首次渲染
// ------------
useState('Arzh2') // 1. 使用 'Arzh2' 初始化变量名为 name1 的 state
useState('Arzh3') // 2. 使用 'Arzh3' 初始化变量名为 name2 的 state
useEffect('Arzh4') 	// 3. 使用 'Arzh4' 初始化变量名为 name3 的 state
// -------------
// 二次渲染
// -------------
useState('Arzh2') // 1. 读取变量名为 name1 的 state(参数被忽略)
useState('Arzh3') // 2. 读取变量名为 name2 的 state(参数被忽略)
useEffect('Arzh4') // 3. 读取变量名为 name3 的 state(参数被忽略)
复制代码

如果我们违反React的规则,使用条件渲染

if (name !== '') {
 const [name2, setName2] = useState('Arzh3');
}
复制代码

假设第一次(name !== ”)为true的时候,执行此Hook,第二次渲染(name !== ”)为false时,不执行此Hook,那么Hook的调用顺序就会发生变化,产生bug

useState('Arzh2') // 1. 读取变量名为 name1 的 state
//useState('Arzh3') // 2. Hook被忽略
useEffect('Arzh4') // 3. 读取变量名为 name2(之前为name3) 的 state
复制代码

React 不知道第二个 useState 的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应的是 arzh3 的 useState,但并非如此。所以这就是为什么React强制要求Hook使用必须遵循这两个规则,同时我们可以使用 eslint-plugin-React-Hooks来强制约束

Effect Hook使用

我们在上面的代码中增加Effect Hook的使用,在函数式组件中增加副作用,修改网页的标题

 useEffect(() => {
 document.title = `You clicked ${count} times`;
 });
复制代码

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

也就是我们完全可以通过useEffect来替代这三个生命钩子函数

我们来了解一下通常需要副作用的场景,比如发送请求,手动变更dom,记录日志等。通常我们都会在第一次dom渲染完成以及后续dom重新更新时,去调用我们的副作用操作。我们可以看一下以前生命周期的实现

 componentDidMount() {
 document.title = `You clicked ${this.state.count} times`;
 }
 componentDidUpdate() {
 document.title = `You clicked ${this.state.count} times`;
 }
复制代码

这也就是我们上面提到的React Hook动机的第二个问题来源之一,需要在第一次渲染以及后续的渲染中调用相同的代码

Effect在默认情况下,会在第一次渲染之后每次更新之后都会执行,这也就让我们不需要再去考虑是componentDidMount还是componentDidUpdate时执行,只需要明白Effect在组件渲染后执行即可

清除副作用

有时候对于一些副作用,我们是需要去清除的,比如我们有个需求需要轮询向服务器请求最新状态,那么我们就需要在卸载的时候,清理掉轮询的操作。

 componentDidMount() {
 this.pollingNewStatus()
 }
 componentWillUnmount() {
 this.unPollingNewStatus()
 }
复制代码

我们可以使用Effect来清除这些副作用,只需要在Effect中返回一个函数即可

 useEffect(() => {
 pollingNewStatus()
 //告诉React在每次渲染之前都先执行cleanup()
 return function cleanup() {
 unPollingNewStatus()
 };
 });
复制代码

有个明显的区别在于useEffect其实是每次渲染之前都会去执行cleanup(),而componentWillUnmount只会执行一次。

Effect性能优化

useEffect其实是每次更新都会执行,在某些情况下会导致性能问题。那么我们可以通过跳过 Effect 进行性能优化。在class组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决

componentDidUpdate(prevProps, prevState) {
 if (prevState.count !== this.state.count) {
 document.title = `You clicked ${this.state.count} times`;
 }
}
复制代码

在Effect中,我们可以通过增加Effect的第二个参数即可,如果没有变化,则跳过更新

useEffect(() => {
 document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

有更多资料视频,加小可乐丫

听说你还不懂React Hook?

听说你还不懂React Hook?

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/192109.html

(0)
上一篇 2021年11月14日
下一篇 2021年11月14日

相关推荐

发表回复

登录后才能评论