对于React Hook的思考探索

最近一直在学React相关的东西,React基于组件的编码方式,让写界面省了很多事儿。难怪如今FlutterCompose都开始拥抱这种开发方式。顺便也重拾起了荒废已久的js,js通过这几年的更新已经变得像一门新语言了,还支持了class这个语法,让咱们熟悉面向对象开发的人更容易上手。可是恼人多变的this一直都在,一开始用类写组件的时候常常会莫名其妙地遇到对象找不到的问题,最后发现要bind(this)javascript

并且还有个问题是好多复杂的场景为了传递数据只能用高阶组件或者渲染属性来实现,像我这种刚接触前端的人确定一脸懵逼。好比业务复杂以后咱们有好多个Context相关的高阶组件,一层套一层,重重嵌套让我想起了在写Flutter时的恐惧。html

像这样:前端

<AuthenticationContext.Consumer>    
       {user => (        
          <LanguageContext.Consumer>         
               {language => (                
                    <StatusContext.Consumer>                  
                        {status => (                    
                                ...                  
                        )}                
                    </StatusContext.Consumer>           
               )}       
         </LanguageContext.Consumer>   
       )}
    </AuthenticationContext.Consumer>
复制代码

因此在React提供的几种编写组件的方式中,我最喜欢函数组件,代码更加简洁,没有什么花里胡哨的新概念,并且可让我避免跟this打交道。固然了,所以它的能力也十分有限,函数组件没有状态,大部分业务逻辑须要跟生命周期打交道,我仍是须要经过类来写组件,管理生命周期跟状态,哪怕它只是个很小的组件。java

###起色 而后某天我发现了Hook,打开了新大门!React内置了几个Hook,*100%*向后兼容, 对全部的React 咱们熟知的概念提供了直接支持: props, state, context, refs, 以及生命周期。并且, Hook提供了更好的方式去组合这些概念,封装你的逻辑,避免了嵌套地狱或者相似的问题。咱们能够在函数组件中使用状态,也能够在渲染后执行一些网络请求。react

Hook其实就是普通的函数,是对类组件中一些能力在函数组件的补充,因此咱们能够在函数组件中直接使用它,在类组件中,咱们是不须要它的。git

React提供的Hook不算多,咱们最经常使用的Hook要数useStateuseEffectuseContext了,其余的都是适用更加通用的或者更加边界的场景的HookuseState可让咱们在函数组件中管理状态。github

import { useState } from 'react'
const [ state, setState ] = useState(initialState)
复制代码

以后咱们就能够经过state直接访问状态,经过setState来设置状态,组件会自动从新渲染。数组

useEffect相似于向componentDidMountcomponentDidUpdate添加代码,咱们常在这两个方法中设置网络请求或者Timer,如今统一写到一个地方就行了,同时咱们也能够返回一个清理函数,它将会在在相似componentWillUnmount的时机被调用,执行一些清理操做。使用useEffect就能够替代这三个方法。markdown

import { useEffect } from 'react'
useEffect(didUpdate)
复制代码

useContext接受一个Context对象,返回一个Context的值。网络

import { useContext } from 'react'
const value = useContext(MyContext)
复制代码

能够用来取代以前的Context Consumer。具体的使用方式咱们之后再说,以前的嵌套地狱可使用useContext来化解:

const user = useContext(AuthenticationContext)    
const language = useContext(LanguageContext)    
const status = useContext(StatusContext)
复制代码

看到这儿,你们应该对Hook开始感兴趣了。与其写那么多ProviderConsumer,去熟悉一大堆花里胡哨的概念,你们都更喜欢这种直接的方式吧。我将展现给你们看,分别用类的方式跟Hook的方式来实现一个组件,进一步展现Hook带来的便利。

  • 类的方式

采用类去实现组件,咱们要在构造器中去定义状态,并且须要修改this去作事件处理,代码以下:

import React from 'react'
class MyName extends React.Component {
	constructor(props) {
		super(props)
		this.state = { name: '' }
		this.handleChange = this.handleChange.bind(this)
	}

	handleChange(evt) {
		this.setState({ name: evt.target.value })
	}

	render() {
		const { name } = this.state
		return (
			<div> <h1>My name is: {name}</h1> <input type="text" value={name} onChange={this.handleChange} /> </div>
		)
	}
}

export default MyName
复制代码
  • 咱们如今来看看函数组件的方式:
import React, { useState } from 'react'
function MyName() {
	const [name, setName] = useState('')

	function handleChange(evt) {
		setName(evt.target.value)
	}

	return (
		<div> <h1>My name is: {name}</h1> <input type="text" value={name} onChange={handleChange} /> </div>
	)
}
export default MyName
复制代码

代码量变少了,咱们使用了useState,减小了不少模版代码,也不用处理构造器跟修改this了,想要修改状态直接调用setName就行了。整个代码看起来更加简洁易于理解,咱们再也不关心要怎么维护保存状态,安安心心经过useState函数使用状态就好了。并且函数的形式让编译器更容易去分析优化代码,移除无用的代码块,使生成的文件更小。

###香不香? 咱们能够发现,Hook更偏向于咱们向React声明咱们想要什么,这一点相似于咱们的界面描述方式,咱们只说咱们要什么,而不是告诉框架该怎么作,代码也更加简洁,方便其余人理解跟后期维护,经过函数的方式咱们也能够在组件间共享逻辑。

###深刻 那么Hook是怎么作到这么神奇的事情的呢,为了深刻理解这背后的原理,咱们从头开始实现一个咱们本身的useState函数来理解这个过程。这个实现不会跟React的实现彻底相同,我会尽可能简化,将核心原理展现出来。

首先定义一个咱们本身的useState函数,方法签名你们都知道了,要传递一个参数做为初始值。

function useState (initialState) {
复制代码

而后咱们定义一个值来保存咱们的状态,一开始,它的值会是咱们传给函数的initialState

let value = initialState
复制代码

而后咱们要定义一个setState函数,当咱们改变状态值时,从新渲染组件。

function setState (nextValue) {        
    value = nextValue        
    ReactDOM.render(<MyName />, document.getElementById('root'))   
 }
复制代码

这边的ReactDOM是用来从新渲染用的。 最终咱们要把这个状态值跟设置方法以数组的形式返回出去:

return [ value, setState ]
}
复制代码

一个简单的Hook就实现了,Hook其实就是简单的js函数,用来执行一些有反作用的操做,好比用来设置一个有状态的值。咱们的Hook使用了一个闭包来保存状态值,由于setStatevalue在同一个闭包下,因此咱们的setState能够访问它,同理不把它传递出去的话在这个闭包外咱们是没办法直接访问的。

###来问题了 若是咱们如今运行咱们的代码,咱们会发现组件从新渲染的时候状态重置了,而后咱们就不能输入任何文字。这是由于每次从新渲染都调用了useState,而后致使value初始化了那咱们得想办法把状态保存在别的地方避免由于从新渲染而受到影响了。

咱们先尝试在函数外使用一个全局变量来保存咱们的状态,那这样的话咱们的状态就不会由于从新渲染而初始化了。

let value
function useState (initialState) {
复制代码

在useState上定义了一个全局变量后,咱们的初始化代码也要改一改:

if (typeof value === 'undefined') value = initialState
复制代码

这样就没问题了。 可是紧接着,咱们又发现,当咱们想多调用几回useState来管理多个状态时,它总在往同一个全局变量上写值,全部的useState方法都在操做同一个value!这确定不是咱们想要的结果。

那为了支持多个useState调用,咱们要想办法改进一下,把变量替换成一个数组试试?

let values = []
let currentHook = 0
复制代码

而后赋初始值的地方也要修改:

if (typeof values[currentHook] === 'undefined') 
      values[currentHook] = initialState
复制代码

最重要的是咱们的setState方法要修改好,这样咱们只会更新该更新的状态值。咱们须要把当前Hook对应的currentHook保存起来,由于currentHook是一直会变的。

let hookIndex = currentHook    
    function setState (nextValue) {        
        values[hookIndex] = nextValue        
        ReactDOM.render(<MyName />, document.getElementById('root'))   
     }
复制代码

最终返回:

return [ values[currentHook++], setState ]
复制代码

而后咱们还要在开始渲染的时候初始化一下currentHook:

function Name () {    
        currentHook = 0
复制代码

如今咱们的Hook能够说是正常工做了

使用一个全局数组保存Hookvalue能够知足屡次调用useState的需求,React内部实现也是相似,不过它的实现更加复杂跟优化,它本身处理好了计数器跟全局变量,并且也不须要咱们手动去重置计数器,不过大致原理咱算是把它摸清楚了。

###那复杂场景来了 其实也不是什么复杂的场景啦,想象这样一个状况,咱们须要把输入的姓名展现出来,姓跟名分开用状态保存,同时咱们想把姓作成选填那该怎么办? 咱们能够先用一个状态记录姓是否是必需的:

const [ enableFirstName, setEnableFirstName ] = useState(false)
复制代码

而后咱们定义一个处理函数:

function handleEnableChange (evt) {        
    setEnableFirstName(!enableFirstName)    
}
复制代码

若是checkbox没有勾选上咱们就不打算渲染姓了,

<h1>My name is: {enableFirstName ? name : ''} {lastName}</h1>
复制代码

咱们能不能把Hook定义放进一个if条件或者三目运算符中去呢?像这样:

const [ name, setName ] = enableFirstName        
    ? useState('') : [ '', () => {} ]
复制代码

如今yarn start来运行咱们的代码,咱们能够发现复选框没有勾选时,名仍是能够修改的,姓随你怎么改都没用,这是咱们想要的结果。

当咱们再次选中复选框时,咱们能修改姓了。可是奇怪的事发生了,名的值跑到姓那儿去了。

这是由于Hook的顺序很重要,咱们都记得咱们实现useState的时候,经过currentHook来肯定当前调用的状态所在位置的,如今咱们凭空插入了一个Hook调用,致使顺序被打乱了,Hook在从新渲染时会从新肯定索引,可是咱们的全局数组并不会变,致使姓去取了名的状态。

勾选复选框以前的状态:

  • [false, '客']
  • 依次是:enableFirstName, lastName

勾选以后:

  • [true, '客', ' ']
  • 依次是:enableFirstName, name, lastName

因此调用Hook的顺序很重要! 这个限制在React官方提供的Hook中也存在,并且React也决定坚持如今的设计。咱们要避免这种写法,真有这种状况选择的状况,无论用不用,都直接把可能要用的Hook声明好,或者拆分出独立的组件,在组件里使用Hook,把问题转换成要不要渲染某个组件,这也是React团队推荐的作法。

虽然有时候咱们会以为能在条件语句或者循环中这样使用Hook更好,可是React团队为何这么设计呢?有木有更好的方案呢?

有人提出了 NamedHook:

// 注意: 不是真实的React Hook API
const [ name, setName ] = useState('nameHook', '')
复制代码

这样作能够避免上面那种数据混乱的状况,每一个Hook调用咱们都设了一个独特的名字,可是这样作咱们就得花时间想出独一无二的名字,解决命名冲突,并且当一个条件变成false的时候咱们该怎么作?若是一个元素从循环中删除了咱们该怎么作?咱们该清理状态吗?若是不清理状态,内存泄漏怎么办?

咱们能够看到,这样并无让事情变得简单,也引入了不少复杂的问题,因此React团队最后坚持了如今的设计,让API尽量保持简单简单,而咱们,在使用时要注意顺序。

看到这儿的同窗可能已经跃跃欲试了,可能有同窗会问道,既然Hook能大大地简化代码结构,让代码更加可维护,咱们是否是该把全部的组件都用Hook来重写呢? 固然不—Hook是可选的。你能够在你的部分组件里面尝试HookReact团队如今尚未打算移除类组件。如今不急着把全部东西都重构成基于Hook。并且Hook并非银弹,咱们能够在以为用Hook最恰当的地方用Hook来实现,好比, 你有许多组件处理类似的逻辑, 你能够把逻辑抽象成一个Hook,或者一个小组件用Hook实现会比较简单,有些地方状态管理比较复杂那仍是用类组件会比较好。因此大部分状况下咱们仍是会函数组件跟类组件一块儿混用。

###结语

最后,相信你们对于Hook的做用跟实现原理想必有了个大致的了解,Hook就是一些简单的js函数,你们看一眼文档就知道怎么用啦,如今咱们了解了Hook的优势跟限制,能够在平常开发中更好地作出选择,本文的代码看这里:示例代码

相关文章
相关标签/搜索