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
最早期,提出了根据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
的缺点
- 不同
mixin
可能会相互依赖,耦合性太强,导致后期维护成本过高 mixin
中的命名可能会冲突,无法使用同一命名的mixin
mixin
即使开始很简单,它们会随着业务场景增多,时间的推移产生滚雪球式的复杂化
具体缺点可以看此链接Mixins是一种祸害
因为mixin
的这些缺点存在,在React
中已经不建议使用mixin
模式来复用代码,React
全面推荐使用高阶组件来替代mixin
模式,同时ES6
本身是不包含任何 mixin
支持。因此,当你在 React
中使用 ES6 class
时,将不支持 mixins
。
高阶组件
高阶组件
(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>
<h1>移动鼠标!</h1>
<Mouse render={mouse => (
)}/>
</div>
);
}
}
然而通常我们说的Render Props
是因为模式才被称为 Render Props
,又不是因为一定要用render
对prop
进行命名。我们也可以这样来表示
<Mouse>
{mouse => (
<Cat mouse={mouse} />
)}
</Mouse>
React Hook动机
React Hook
是官网提出的又一种全新的解决方案,在了解React Hook
之前,我们先看一下React Hook
提出的动机
- 在组件之间复用状态逻辑很难
- 复杂组件变得难以理解
- 难以理解的
class
下面说说我对这三个动机的理解:
在组件之间复用状态逻辑很难,在之前,我们通过高阶组件(Higher-Order Components
)和渲染属性(Render Propss
)来解决状态逻辑复用困难的问题。很多库都使用这些模式来复用状态逻辑,比如我们常用redux
、React
Router
。高阶组件、渲染属性都是通过组合来一层层的嵌套共用组件,这会大大增加我们代码的层级关系,导致层级的嵌套过于夸张。从React
的devtool
我们可以清楚的看到,使用这两种模式导致的层级嵌套程度
复杂组件变得难以理解,在不断变化的业务需求中,组件逐渐会被状态逻辑以及副作用充斥,每个生命周期常常会包含一些不相关的逻辑。我们写代码通常都依据函数的单一原则,一个函数一般只处理一件事,但在生命周期钩子函数中通常会同时做很多事情。比如,在我们需要在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);
-
useState
做了啥我们可以看到,在此函数中,我们通过
useState
定义了一个'state变量',它与class
里面的this.state
提供的功能完全相同.相当于以下代码class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; }
-
useState
参数在代码中,我们传入了
0
作为useState
的参数,这个参数的数值会被当成count
初始值。当然此参数不限于传递数字以及字符串,可以传入一个对象当成初始的state
。如果state
需要储存多个变量的值,那么调用多次useState
即可 -
useState
返回值返回值为:当前
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
- 只在最顶层使用
Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React
函数的最顶层调用他们
- 只在
React
函数中调用Hook
不要在普通的 JavaScript 函数中调用 Hook
这两条规则出现的原因是,我们可以在单个组件中使用多个State Hook
或 Effect Hook
,React
靠的是 Hook
调用的顺序来知道哪个 state
对应哪个useState
function Form() {
const [name1, setName1] = useState('Arzh1');
const [name2, setName2] = useState('Arzh2');
const [name3, setName3] = useState('Arzh3');
// ...
}
// ------------
// 首次渲染
// ------------
useState('Arzh1') // 1. 使用 'Arzh1' 初始化变量名为 name1 的 state
useState('Arzh2') // 2. 使用 'Arzh2' 初始化变量名为 name2 的 state
useEffect('Arzh3') // 3. 使用 'Arzh3' 初始化变量名为 name3 的 state
// -------------
// 二次渲染
// -------------
useState('Arzh1') // 1. 读取变量名为 name1 的 state(参数被忽略)
useState('Arzh2') // 2. 读取变量名为 name2 的 state(参数被忽略)
useEffect('Arzh3') // 3. 读取变量名为 name3 的 state(参数被忽略)
如果我们违反React
的规则,使用条件渲染
if (name !== '') {
const [name2, setName2] = useState('Arzh2');
}
假设第一次(name !== '')
为true
的时候,执行此Hook
,第二次渲染(name !== '')
为false
时,不执行此Hook
,那么Hook
的调用顺序就会发生变化,产生bug
useState('Arzh1') // 1. 读取变量名为 name1 的 state
//useState('Arzh2') // 2. Hook被忽略
useEffect('Arzh3') // 3. 读取变量名为 name2(之前为name3) 的 state
React
不知道第二个 useState
的 Hook
应该返回什么。React
会以为在该组件中第二个 Hook
的调用像上次的渲染一样,对应的是 arzh2
的 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 更改时更新
其他Hooks
由于篇幅原因,就不再此展开了,有兴趣可以自行官网查看