React Hook实战(一)
目录:
- 引-为什么用Hook
- 基本使用
- 自定义实现Hook
- Hook-react的真正实现
- Class 和 Hook对比
- 总结-问题思考
引-为什么用Hook
在过去,我们必须使用生命周期方法(如componentDidUpdate)的特殊函数的类组件和特殊状态处理的方法以便处理状态更改。React class中,尤其是this.context的JavaScript对象,对于人和机器来说都很难阅读和理解,因为它总是引用不同的东西,所以有时(例如,在事件处理程序中)我们需要手动将它重新绑定到类对象。计算机不知道类中的哪些方法将被调用,以及如何修改这些方法,这使得性能优化和代码优化变得困难。此外,clsss有时需要我们一次在多个地方编写代码。 例如,如果我们希望在组件初始化或数据更新时获取数据,举个例子:
首先,我们通过扩展React.component类来定义我们的类组件:
class Example extends React.Component {
然后,我们定义componentDidMount生命周期方法,在该方法中,我们从一个API中提取数据
componentDidMount () {
fetch(`http://my.api/${this.props.name}`)
.then(...)
}
我们还需要定义componentDidUpdate生命周期方法,当prop发生变化时判断是否更新状态。
componentDidUpdate (prevProps) {
if (this.props.name !== prevProps.name) {
fetch(`http://my.api/${this.props.name}`)
.then(...)
}
}
}
为了减少代码的重复性,我们可以定义一个名为fetchData的单独方法来获取数据,如下所示:
fetchData () {
fetch(`http://my.api/${this.props.name}`)
.then(...)
}
最后,我们调用componentDidMount和ComponentDidUpdate中的方法
componentDidMount () {
this.fetchData()
}
componentDidUpdate (prevProps) {
if (this.props.name !== prevProps.name) { this.fetchData()
}
}
然而,即使这样,我们仍然需要在两个地方调用fetchData。每当我们更新传递给方法的参数时,我们需要在两个地方更新它们,这使得这个模式很容易出现bug和将来的bug。
在Hook之前,如果我们想封装状态管理逻辑,我们必须使用高阶组件和呈现道具。例如,我们创建一个React组件,该组件使用上下文处理用户身份验证,如下所示:
我们首先导入authenticateUser函数,以便用上下文包装组件,然后导入AuthenticationContext组件,以便访问上下文:
import authenticateUser, { AuthenticationContext } from './auth'
然后,我们定义app组件,在其中我们使用AuthenticationContext.Consumer组件
const App = () => (
<AuthenticationContext.Consumer>
{user =>
现在,我们根据用户是否登录显示不同的文本
user ? `${user} logged in` : 'not logged in'
最后我们补充一下上下文
}
</AuthenticationContext.Consumer>
)
export default authenticateUser(App)
在前面的示例中,我们使用高阶authenticateUser组件向现有组件添加身份验证逻辑。然后我们用一个authenticationcontext.Consumer将user对象注入到组件中。可以想象,使用许多上下文将导致一个包含许多子zu'jian的大型组件。例如,当我们想要使用三个上下文时,wrapper hell如下所示:
<AuthenticationContext.Consumer>
{user => (
<LanguageContext.Consumer>
{language => (
<StatusContext.Consumer>
{status => (
...
)}
</StatusContext.Consumer>
)}
</LanguageContext.Consumer>
)}
</AuthenticationContext.Consumer>
这不是很容易阅读和修改,而且如果我们以后需要更改某些内容,它也容易出错。此外,如果我们查看一个大型组件树,其中许多组件只是充当wrapper,这种传统方式使调试变得困难。
React Hook基于React基本原理,Hook试图通过使用现有的JavaScript特性来封装状态管理。因此,我们不再需要学习和理解专门的React特性;我们可以简单地利用现有的JavaScript知识来使用Hook。
我们可以使用Hook解决前面提到的所有问题。我们不再需要使用类组件,因为Hook只是可以在函数组件中调用的函数。我们也不再需要为上下文使用高阶组件和渲染props,因为我们可以简单地使用Hook上下文来获取所需的数据。此外,Hook允许我们在组件之间重用有状态逻辑,而无需创建高阶组件。
例如,前面提到的生命周期方法的问题可以使用Hook来解决:
function Example ({ name }) {
useEffect(() => {
fetch(`http://my.api/${this.props.name}`)
.then(...)
}, [ name ])
// ...
}
这里实现的效果为Hook将在组件挂载时以及prop更改时自动触发。此外,前面提到的wrapper hell也可以使用Hook解决,如下所示
const user = useContext(AuthenticationContext)
const language = useContext(LanguageContext)
const status = useContext(StatusContext)
现在我们知道了Hook可以解决哪些问题,让我们开始使用吧。
Hook的基本使用:
React中组件可以大体分为类组件和函数组件,在React中如果需要更改一个组件状态的时候,那么这个组件必须是类组件,那么能否让函数组件拥有类组件的功能?这时候我们就需要使用Hook让我们函数组件拥有了类似组件的特性。Hook是React16.8中新增得功能,他们允许我们在不编写类的情况下使用状态和其他React功能。Hook又提供了一种写组件的方法,使编写一个组件更简单更方便,同时可以自定义hook把公共的逻辑提取出来,让逻辑在多个组件之间共享。
我们从一个请求数据的代码示例demo开始切入:
import React, { useState } from 'react';
import "./Welcome.scss";
function Welcome() {
const [data, setData] = useState({ hits: [{
objectID:"001",
url:"https://www.jd.com/",
title:"JD"
}] });
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default Welcome;
该组件是一个项目列表,初始化的data和状态更新函数来自useState这个Hook,通过调用useState,来创建App组件的内部状态。初始状态是一个object,其中的hits为一个空数组。如果我们要添加调用后端数据,我们可以使用axios来发起请求,同样也可以使用fetch。
function Welcome() {
const [data, setData] = useState({ hits: [{
objectID:"001",
url:"https://www.jd.com/",
title:"JD"
}] });
useEffect(async () => {
const result = await axios(
'http://localhost/api/v1/search?query=redux'
);
setData(result.data);
});
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
在useEffect中,我们请求了后端的数据,还通过调用setData来更新了本地的状态,这样会触发界面的更新。但是,运行这个程序的时候,会出现无限循环的情况。假设我们只希望在组件mount时请求数据,那么我们可以传递一个空数组作为useEffect的第二个参数,这样就能避免在组件更新时执行useEffect,只会在组件mount时执行。useEffect的第二个参数可用于定义其依赖的所有变量,如果其中一个变量发生变化,则useEffect会再次运行,如果包含变量的数组为空,则在更新组件时useEffect不会再执行,因为它不会监听任何变量的变更。
function Welcome() {
const [data, setData] = useState({ hits: [{
objectID:"001",
url:"https://www.jd.com/",
title:"JD"
}] });
useEffect(async () => {
const result = await axios(
'http://localhost/api/v1/search?query=redux'
);
setData(result.data);
},[]);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
在代码中,我们使用async / await从第三方API获取数据,由于每个async函数都会默认返回一个隐式的promise。但是,useEffect不希望返回任何内容,这就是为什么不能直接在useEffect中使用async函数,因此,我们可以不直接调用async函数,而是像下面这样:
useEffect(() => {
const fetchData = async () => {
const result = await axios(
'http://localhost/api/v1/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
在useEffect中,我们可以把请求数据前将loading置为true,在请求完成后,将loading置为false.
loading处理完成后,还需要处理错误,这里的逻辑是一样的,使用useState来创建一个新的state,然后在useEffect中特定的位置来更新这个state。由于我们使用了async/await,可以使用一个try-catch, 每次useEffect执行时,将会重置error;在出现错误的时候,将error置为true;在正常请求完成后,将error置为false。
function Welcome() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'http://localhost/api/v1/search?query=redux',
);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [url]);
}
Hook是可以在函数组件中调用的函数。我们也不再需要为上下文使用高阶组件和传统的class的方式,因为我们可以简单地使用Hook上下文来获取所需的数据。此外,hook允许我们在组件之间重用有状态的逻辑,而无需创建高阶组件。我们来简单看一下Hook提供的其他方法:
方法名 | 用法 | 示例 | 思考 |
---|---|---|---|
useRef | 该方法返回一个可变的ref对象,其中.current属性初始化为传递的参数initialValue | import { useRef } from 'react'; const refContainer = useRef(initialValue) | useRef用于处理对React中的元素和组件的引用。我们可以通过将ref属性传递给元素或组件来设置引用。 |
useReducer | 这个是useState的替代方案,其工作方式与Redux库类似 | import { useReducer } from 'react'; const [ state, dispatch ] = useReducer(reducer, initialArg, init) | useReducer常用于处理复杂的状态逻辑。 |
useMemo | Memoization是一种优化技术,它缓存函数调用的结果,useMemo允许我们计算一个值并将其记录下来 | import { useMemo } from 'react'; const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]) | 当我们希望避免重新执行费时的操作时,useMemo对于性能优化非常有用。 |
useCallback | 这个方法允许我们传递一个内联回调函数和一组依赖项,并将返回回调函数的记忆版本。 | import { useCallback } from 'react'; const memoizedCallback = useCallback(() => {doSomething(a, b) }, [a, b]) | 当将回调函数传递给子组件时,useCallback非常有用。它的工作方式类似于useMemo,但用于回调函数。 |
useLayoutEffect | useLayoutEffect与useffect相同,但它只在所有的文档对象模型(Document Object Model,DOM)改变之后才触发。 | import { useLayoutEffect } from 'react'; useLayoutEffect(didUpdate) | useLayoutEffect可用于从DOM读取信息。(最好使用useffect,useLayoutEffect将阻止试图更新并减慢应用程序的渲染速度) |
useDebugValue | useDebugValue可用于在创建自定义Hook时在React DevTools中显示标签。 | import { useDebugValue } from 'react'; useDebugValue(value) | 在自定义Hook中可以使用useDebugValue来显示Hook的当前状态,这样可以更容易地调试组件。 |
除了React官方提供的所有语法糖之外,社区已经发布了很多库。这些库还提供了一些方法,我们可以看一下其中非常受欢迎的几个:
useInput
useInput用于轻松实现输入处理,并将输入字段的状态与变量同步。它可以如下使用:
import { useInput } from 'react-hookedup'
function App () {
const { value, onChange } = useInput('')
return <input value={value} onChange={onChange} />
}
如我们所见,useInput极大地简化了React中输入字段的处理。
useResource
useResource可用于通过应用程序中的请求实现异步数据加载。我们可以使用它如下
import { useResource } from 'react-request-hook'
const [profile, getProfile] = useResource(id => ({ url: `/user/${id}`,
method: 'GET'
})
如我们所见,使用useResource来处理获取数据功能是非常简单的。
Navigation Hooks
Navigation是Navi库的一部分,用于通过React中的Hook实现路由功能。Navi库提供了更多与路由相关的Hook。我们可以使用它们如下
import { useCurrentRoute, useNavigation } from 'react-navi'
const { views, url, data, status } = useCurrentRoute()
const { navigate } = useNavigation()
Navigation Hooks使得路由更容易处理。
Life cycle Hooks
react hookedup库提供各种Hooks,包括react的所有生命周期侦听器。(请注意,在使用Hook进行开发时,不建议考虑组件的生命周期。这些钩子只是提供了一种将现有组件重构为Hook的方法。)在这里,我们列出了其中的两个,如下所示
import { useOnMount, useOnUnmount } from 'react-hookedup'
useOnMount(() => { ... })
useOnUnmount(() => { ... })
react hookedup可以直接替换类组件中的生命周期方法。
Timer Hooks
react hookedup库还为setInterval和setTimeout提供了方法。这些工作方式类似于直接调用setTimeout或setInterval。但作为一个React Hook,它将在重新渲染的实惠保持执行,如果我们在函数组件中直接定义计时器而不使用Hook,那么每次组件重新渲染时,我们都将重置计时器。我们可以将时间以毫秒为单位作为第二个参数传递。我们可以如下使用:
import { useInterval, useTimeout } from 'react-hookedup'
useInterval(() => { ... }, 1000)
useTimeout(() => { ... }, 1000)
实现一个Hook
在实现一个Hook前我们先来深入了解State Hook吧,我们先从State Hook如何在内部工作开始,我们将自己重新实现它。接下来,我们将了解钩子的一些局限性,以及它们存在的原因。然后,我们将了解可能的替代Hook api及其相关问题。最后,我们将学习如何解决由于Hook的限制而导致的常见问题。最后,我们将探讨一下如何使用Hook来实现React中的有状态函数组件。
我们将需要ReactDOM,以便在useState Hook的重新实现中渲染组件。如果我们使用实际的React Hook,这将在内部处理。
import React from 'react'
import ReactDOM from 'react-dom'
现在,我们定义自己的useState函数。useState函数将initialState作为参数:
function useState (initialState) {
然后,我们定义一个值,在其中存储我们的状态。首先,该值将设置为initialState,该值作为参数传递给函数:
let value = initialState
接下来,我们定义setState函数,在该函数中,我们将把值设置为不同的值,并渲染我们的MyName组件
function setState (nextValue) {
value = nextValue
ReactDOM.render(<MyName />,
document.getElementById('root'))
}
最后,我们将value和setState函数作为数组返回:
return [ value, setState ]
}
我们使用数组而不是对象的原因是,我们通常希望重命名value和setState变量。使用数组可以方便地通过解构重命名变量。
const [ name, setName ] = useState('')
我们的Hook函数使用闭包来存储当前值。闭包是变量存在和存储的环境。在我们的例子中,函数提供闭包,value变量存储在闭包中。setState函数也在同一个闭包中定义,这就是为什么我们可以访问该函数中的value变量。在useState函数之外,除非从函数返回value变量,否则无法直接访问该value变量。那么我们实现的简单Hook有什么问题呢?
如果现在运行我们的Hook demo,我们会注意到当我们的组件重新渲染时,状态被重置。这是由于在每次呈现组件时都重新初始化value变量,这是因为每次渲染组件时都调用useState方法。接下来,我们将使用一个全局变量来解决这个问题,然后将value放到一个数组,然后我们定义多个Hook。正如我们所了解到的,该value存储在useState函数定义的闭包中。每次组件重新提交时,闭包都会重新初始化,这意味着我们的value将被重置。要解决这个问题,我们需要将值存储在函数外部的全局变量中。这样,值变量将位于函数外部的闭包中,这意味着当再次调用函数时,闭包将不会重新初始化。我们可以定义全局变量如下:
首先,我们在useState函数定义上方添加以下一行
let value
function useState (initialState) {
然后,用以下代码替换函数中的第一行
if (typeof value === 'undefined') value = initialState
现在,我们的useState函数使用全局值变量,而不是在它的闭包中定义值变量,因此当函数再次被调用时,它不会被重新初始化。
我们的Hook功能是可以使用的,但是,如果我们想添加另一个hook,我们会遇到另一个问题:所有Hook都写入同一个全局值变量,让我们通过在组件中添加第二个Hook来仔细研究这个问题。
假设我们要添加lastName状态,如下所示:
我们首先在当前Hook之后创建一个新的Hook,
const [ name, setName ] = useState('')
const [ lastName, setLastName ] = useState('')
然后,我们定义另一个handleChange函数
function handleLastNameChange (evt) {
setLastName(evt.target.value)
}
接下来,我们将lastName变量放在名字后面:
<h1>My name is: {name} {lastName}</h1>
最后,我们添加另一个input输入框:
<input type="text" value={lastName} onChange=
{handleLastNameChange}
/>
当我们这样写时,我们会注意到我们重新实现的Hook函数对两个状态使用相同的值,因此我们总是同时更改两个字段。为了实现多个Hook,而不是只有一个全局变量,我们应该有一个存放Hook的数组。我们现在要将value变量重构为value数组,以便可以定义多个Hook。
我们删除以下代码行
let value
替换为以下代码段
let values = []
let currentHook = 0
然后,编辑useState函数的第一行,我们现在在其中初始化values数组的currentHook索引处的值:
if (typeof values[currentHook] === 'undefined')
values[currentHook] = initialState
我们还需要更新setter函数,以便更新相应的状态值。在这里,我们需要将currentHook值存储在一个单独的hookIndex变量中,因为currentHook值稍后会更改。这可以确保在useState函数的闭包中创建currentHook变量的副本。否则,useState函数将从外部闭包访问currentHook变量,该闭包在每次调用useState时都会被修改。
let hookIndex = currentHook
function setState (nextValue) {
values[hookIndex] = nextValue
ReactDOM.render(<MyName />,
document.getElementById('root'))
}
编辑useState函数的最后一行,如下所示
return [ values[currentHook++], setState ]
使用values[currentHook++],我们将currentHook的当前值作为索引传递给values数组,然后将currentHook增加1。这意味着从函数返回后currentHook将增加。在开始渲染组件时,仍需要重置currentHook计数器。在组件定义之后添加以下:
function Name () {
currentHook = 0
最后,我们简单地重新实现useState Hook。如我们所见,使用全局数组存储Hook state解决了我们在定义多个Hook时遇到的问题。我们如果想添加一个复选框来切换first name字段的使用呢?
首先,我们添加一个新的Hook来存储复选框的状态:
const [ enableFirstName, setEnableFirstName ] = useState(false)
然后,我们定义一个处理函数
function handleEnableChange (evt) {
setEnableFirstName(!enableFirstName)
}
接下来,我们渲染一个复选框
<input type="checkbox" value={enableFirstName} onChange= {handleEnableChange} />
添加对enableFirstName变量的检查
<h1>My name is: {enableFirstName ? name : ''} {lastName}
</h1>
我们是否可以将Hook定义放入if条件或三元表达式中,就像我们在下面的代码片段中一样?
const [ name, setName ] = enableFirstName ? useState('')
: [ '', () => {} ]
最新版本的react-scripts在定义条件Hooks时实际上会抛出一个错误,因此我们需要通过运行以下命令来降级本例中的库:
> npm install --save react-scripts@^2.1.8
在这里,如果名字被禁用,我们会返回初始状态和一个空的setter函数,这样编辑输入字段就不起作用。我们会注意到编辑last name仍然有效,但是编辑first name 不起作用,在下面的截图中我们可以看到,现在只能编辑 last name。
当我们单击复选框时程序会执行以下操作:
- 复选框已选中
- 启用name输入字段
- last name字段的值现在是first name字段的值
我们可以在以下屏幕截图中看到单击复选框的结果:
我们可以看到 last name状态现在在first name字段中。这些值已经交换,因为Hook的顺序很重要。正如我们从实现中了解到的,我们使用currentHook索引来知道每个Hook的状态存储在哪里。但是,当我们在两个现有的Hook之间插入一个附加Hook时,顺序就会混乱。
在选中复选框之前,values数组如下所示:
- [false, '']
- Hook: enableFirstName, lastName
然后,我们在lastName字段中输入了一些文本:
- [false, 'Hook']
- Hook: enableFirstName, lastName
接下来,我们切换了复选框,它激活了我们的新Hook
- [true, 'Hook', '']
- Hook order: enableFirstName, name, lastName
如我们所见,在两个现有Hook之间插入一个新Hook会使name hook获取下一个Hook(lastName)的状态,因为它现在具有与lastName钩子以前相同的索引。现在,lastName Hook没有值,这导致它设置初始值为空字符串。因此,切换复选框会将lastName字段的值放入name字段。
Hook-react的真正实现
我们简单的Hook实现已经让我们了解了Hook是如何在内部工作的。然而,hook不使用全局变量。相反,它们将状态存储在React component中。它们也在内部处理Hook计数器,因此我们不需要手动重置函数组件中的计数。此外,当状态发生变化时,真正的Hook会自动触发component的重新渲染。然而,要做到这一点,需要从React函数组件调用Hook。不能在React外部或React class组件内部调用React Hook。我们应该始终在函数组件的开头定义Hook,并且永远不要将它们嵌套在if或其他构造函数中。我们应该在React函数内部调用React hook组件,React hook不能有条件地定义,也不能在循环中定义。
那么,我们如何实现条件的Hook呢?我们可以定义Hook并在需要时使用它,而不是使Hook成为条件的Hook。我们可以重新分组我们的组件。解决有条件的hook的另一种方法是将一个组件拆分为多个组件,然后有条件地渲染这些组件。例如,假设我们希望在用户登录后从数据库中获取用户信息。
我们不能执行以下操作,因为使用if条件可以更改Hook的顺序
function UserInfo ({ username }) {
if (username) {
const info = useFetchUserInfo(username)
return <div>{info}</div>
}
return <div>Not logged in</div>
}
我们必须为用户登录时创建一个单独的组件,如下所示:
function LoggedInUserInfo ({ username }) { const info = useFetchUserInfo(username)
return <div>{info}</div>
}
function UserInfo ({ username }) {
if (username) {
return <LoggedInUserInfo username={username} />
}
return <div>Not logged in</div>
}
对非登录和登录状态使用两个独立的组件是有意义的,因为我们希望每个组件都有一个单一的功能的。至于循环中的Hook,我们可以使用包含数组的单个状态Hook,也可以拆分组件。例如,假设我们想显示所有在线用户。
我们可以使用数组包含所有用户数据,如下所示:
function OnlineUsers ({ users }) {
const [ userInfos, setUserInfos ] = useState([])
// ... fetch & keep userInfos up to date ...
return ( <div>
{users.map(username => {
const user = userInfos.find(u => u.username === username)
return <UserInfo {...user} />
})}
</div>
)
}
然而,这可能是有问题的。例如,我们可能不想通过OnlineUsers组件更新所有用户状态,因为我们必须从数组中选择需要修改的用户的状态,然后修改数组。更好的解决方案是在UserInfo组件中使用Hook。这样,我们就可以使每个用户的状态保持最新,而不必处理数组逻辑:
function OnlineUsers ({ users }) {
return (
<div>
{users.map(username => <UserInfo username={username} />)} </div>
)
}
function UserInfo ({ username }) {
const info = useFetchUserInfo(username)
// ... keep user info up to date ...
如我们所见,为每个功能模块使用一个单独的组件可以保持代码的简单和简洁,同时也避免了React Hook的限制。以上我们首先重新实现useState函数,使用全局状态和闭包。然后我们了解到,为了实现多个Hook,我们需要使用状态数组来代替。然而,通过使用状态数组,我们必须在函数调用之间保持hook顺序的一致性。这个限制使得我们不能使用条件中的Hook和循环中的Hook。然后,我们了解了Hook的可能替代方案。那么真正的react是怎么实现Hook的呢,我们来看一段react官方的源码:
在 React 中,实现方式却有一些差异的。React 中是通过类似单链表的形式来代替数组的。(如下图所示)我们知道,react 会生成一棵组件树(或Fiber 单链表)[Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新。],树中每个节点对应了一个组件。hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。memoizedState 数组是按hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。我们只能在函数最外层调用 Hook自定义的共享同一个memoizedState,共享同一个顺序。每一次重新渲染的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。
type Hooks = {
memoizedState: any, // 指向当前渲染节点 Fiber
baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
baseUpdate: Update<any> | null,// 当前需要更新的 Update ,每次更新完之后,会赋值上 一个
update,方便 react 在渲染错误的边缘,数据回溯
queue: UpdateQueue<any> | null,// UpdateQueue 通过
next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
}
type Effect = {
tag: HookEffectTag, // effectTag 标记当前 hook 作用在 life-cycles 的哪一个阶段
create: () => mixed, // 初始化 callback
destroy: (() => mixed) | null, // 卸载 callback
deps: Array<mixed> | null,
next: Effect, // 同上
};
Hook函数组件在第一次渲染时和再次渲染时的实现是不同的,组件所调用的 Hook 实际上指向的是不同的 Hook。函数组件在第一次渲染时所使用的 Hook 指向的是对应的 mountXXX,而在更新时,Hook 指向的是对应的 updateXXX,如下图所示:
Class 和 Hook对比
从生命周期上看
我们来对比汇总一个表格
class 组件 | Hooks 组件 |
---|---|
constructor | useState |
getDerivedStateFromProps | useState 里面 update 函数 |
shouldComponentUpdate | useMemo |
render | 函数本身 |
componentDidMount | useEffect |
componentDidUpdate | useEffect |
componentWillUnmount | useEffect 里面返回的函数 |
componentDidCatch | 无 |
getDerivedStateFromError | 无 |
从编码上看
class 组件 | Hooks 组件 |
---|---|
代码逻辑清晰(构造函数、componentDidMount等) | 需要配合注释和变量名 |
不容易内存泄漏 | 容易发生内存泄漏 |
总结-问题思考:
- React 是如何把对 Hook 的调用和组件联系起来的。
Hook 本质就是 JavaScript 函数,不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。
- React 怎么知道哪个 state 对应哪个 useState?
React 靠的是 Hook 调用的顺序。只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。
- Hook 会因为在渲染时创建函数而变慢吗
不会。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。除此之外,可以认为 Hook 的设计在某些方面更加高效:Hook 避免了 class 需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。符合语言习惯的代码在使用 Hook 时不需要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中非常普遍。组件树小了,React 的工作量也随之减少。
- 使用useMemo ?
这行代码会调用 computeExpensiveValue(a, b)。但如果依赖数组 [a, b] 自上次赋值以来没有改变过,useMemo 会跳过二次调用,只是简单复用它上一次返回的值。可以把 useMemo 作为一种性能优化的手段,但不要把它当做一种语义上的保证。未来,React 可能会选择「忘掉」一些之前记住的值并在下一次渲染时重新计算它们,比如为离屏组件释放内存。建议自己编写相关代码以便没有 useMemo 也能正常工作 —— 然后把它加入性能优化。
- 如何实现 shouldComponentUpdate
可以用 React.memo 包裹一个组件来对它的 props 进行浅比较。这不是一个 Hook 因为它的写法和 Hook 不同。React.memo 等效于 PureComponent,但它只比较 props。
- effect 的依赖频繁变化该怎么处理?
传入空的依赖数组 [],意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval 的回调中,count 的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行 setCount(0 + 1),因此count 永远不会超过 1。
指定 [count] 作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval 在被清除前(类似于 setTimeout)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使用 setState 的函数式更新形式。它允许我们指定state该如何改变而不用引用当前state。
- 和DOM的交互
获取 DOM 节点的位置或是大小的基本方式是使用 callback ref。每当 ref 被附加到一个另一个节点,React 就会调用 callback。在这个案例中,我们没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的变化通知到我们。使用 callback ref 可以确保即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。
- 如何获取上一轮的 props 或 state
可以通过 ref 来手动实现,考虑到这是一个相对常见的使用场景,很可能在未来 React 会自带一个 usePrevious Hook。
- Hook 能否覆盖 class 的所有使用场景
官方给 Hook 设定的目标是尽早覆盖 class 的所有使用场景。目前暂时还没有对应不常用的 getSnapshotBeforeUpdate、getDerivedStateFromError 和 componentDidCatch 生命周期的 Hook 等价写法,但官方计划尽早把它们加进来。目前 Hook 还处于早期阶段,一些第三方的库可能还暂时无法兼容 Hook。
- Hook,class,两者混用?
我们不能在 class 组件内部使用 Hook,但我们可以在组件树里混合使用 class 组件和使用了 Hook 的函数组件。不论一个组件是 class 还是一个使用了 Hook 的函数,都只是这个组件的实现细节而已。长远来看,官方期望 Hook 能够成为我们编写 React 组件的主要方式。
参考文献
1、官方文档