ReactHook
简略记录版,本文仅提供一些基础内容作为学习和回忆之用,入门学习请查看React Hook官方文档
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
Hook 是什么?
Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState 是允许你在 React 函数组件中添加 state 的 Hook。稍后我们将学习其他 Hook。
什么时候我会用 Hook?
如果你在编写[函数组件](# 函数组件)并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。
Hook的设计动机
在组件之间复用状态逻辑很难
React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如 render props 和 高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。
Hook 使你在无需修改组件结构的情况下复用状态逻辑。
复杂组件变得难以理解
我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount
和 componentDidUpdate
中获取数据。但是,同一个 componentDidMount
中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount
中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
难以理解的 class
除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this
的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。大家可以很好地理解 props,state 和自顶向下的数据流,但对 class 却一筹莫展。即便在有经验的 React 开发者之间,对于函数组件与 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景。
为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。
底层原理
React 保持对当先渲染中的组件的追踪。多亏了 Hook 规范,我们得知 Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)。
每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState()
调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState()
调用会得到各自独立的本地 state 的原因。
State Hook
class
1 | class Example extends React.Component { |
state 初始值为 { count: 0 } ,当用户点击按钮后,我们通过调用 this.setState() 来增加 state.count。整个章节中都将使用该 class 的代码片段做示例
Hook
1 | import React, { useState } from 'react'; |
- 第一行: 引入 React 中的 useState Hook。它让我们在函数组件中存储内部 state。
- 第四行: 在 Example 组件内部,我们通过调用 useState Hook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。我们把变量命名为 count,因为它存储的是点击次数。我们通过传 0 作为 useState 唯一的参数来将其初始化为 0。第二个返回的值本身就是一个函数。它让我们可以更新 count 的值,所以我们叫它 setCount。
- 第九行: 当用户点击按钮后,我们传递一个新的值给 setCount。React 会重新渲染 Example 组件,并把最新的 count 传给它。
state拆分
// TODO
Effect Hook
Effect Hook 可以让你在函数组件中执行[副作用](# 副作用)操作。可以把 useEffect
Hook 看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
class
1 | class Example extends React.Component { |
在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。
Hook
1 | import React, { useState, useEffect } from 'react'; |
useEffect 做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
为什么在组件内部调用 useEffect? 将
useEffect
放在组件内部让我们可以在 effect 中直接访问count
state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。useEffect 会在每次渲染后都执行吗? 是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。(我们稍后会谈到[如何控制它](https://react.docschina.org/docs/hooks-effect.html# tip-optimizing-performance-by-skipping-effects)。)你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
需要清除的 effect
class
1 | class FriendStatus extends React.Component { |
Hook
1 | import React, { useState, useEffect } from 'react'; |
为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。我们稍后将讨论[为什么这将助于避免 bug](https://react.docschina.org/docs/hooks-effect.html# explanation-why-effects-run-on-each-update)以及[如何在遇到性能问题时跳过此行为](https://react.docschina.org/docs/hooks-effect.html# tip-optimizing-performance-by-skipping-effects)。
effect 的条件执行
默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。
然而,在某些场景下这么做可能会矫枉过正。比如,在上一章节的订阅示例中,我们不需要在每次组件更新时都创建新的订阅,而是仅需要在 source
props 改变时重新创建。
要实现这一点,可以给 useEffect
传递第二个参数,它是 effect 所依赖的值数组。更新后的示例如下:
1 | useEffect( |
此时,只有当 props.source
改变后才会重新创建订阅。
Context Hook
1 | const value = useContext(MyContext); |
接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value
prop 决定。
当组件上层最近的 <MyContext.Provider>
更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext
provider 的 context value
值。
别忘记 useContext
的参数必须是 context 对象本身:
- 正确:
useContext(MyContext)
- 错误:
useContext(MyContext.Consumer)
- 错误:
useContext(MyContext.Provider)
调用了 useContext
的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 [通过使用 memoization 来优化](https://github.com/facebook/react/issues/15156# issuecomment-474590693)。
Reducer Hook
useReducer
是 [useState
](https://react.docschina.org/docs/hooks-reference.html# usestate) 的替代方案。它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法。
以下是用 reducer 重写 [useState
](https://react.docschina.org/docs/hooks-reference.html# usestate) 一节的计数器示例:
1 | const initialState = {count: 0}; |
reducer-hook 替代 react-redux?
乍一看好像react利用hook已经可以使用redux的机制了, 状态由派发的action改变,单向数据流。但是hook不会让状态共享,也就是每次useReducer保持的数据都是独立的。比如下面这个例子:
1 | function CountWrapper() { |
两个Count组件内部的数据是独立的,无法互相影响,状态管理也就无从说起。 究其原因,useReducer内部也是用useState实现的
1 | function useReducer(reducer, initialState) { |
Callback Hook
//TODO
Memo Hook
//TODO
Ref Hook
//TODO
useImperativeHandle
//TODO
useLayoutEffect
//TODO
useDebugValue
//TODO
参考 & 引用
https://react.docschina.org/docs/hooks-overview.html
https://react.docschina.org/docs/hooks-state.html
https://juejin.im/post/5bf991236fb9a049f570ce1c
名词解释
函数组件
React 的函数组件是这样的:
1 | const Example = (props) => { |
之前可能把它们叫做“无状态组件”。但现在我们为它们引入了使用 React state 的能力,所以我们更喜欢叫它”函数组件”。
Hook 在 class 内部是不起作用的。但你可以使用它们来取代 class 。
副作用
数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。