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> <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提出的动机
在组件之间复用状态逻辑很难
复杂组件变得难以理解
难以理解的 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('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 更改时更新
有更多资料视频,加小可乐丫
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。