我们在之前 React 中的 Mixin 和 React 中的 HOC 的章节当中介绍了 Mixin
和 HOC
的相关内容,了解了它们大致的实现原理和使用方式,也知道了 HOC
的出现就是为了替代 Mixin
,但是 HOC
也并非完美无缺,它其实也是存在着一些缺陷的,比如
HOC
需要在原组件上进行包裹或者嵌套,如果大量使用HOC
,将会产生非常多的嵌套,这让调试变得非常困难HOC
可以劫持props
,在不遵守约定的情况下也可能造成冲突
所以在本章当中我们就来看看 React
中的 Hook
是如何同时解决 Mixin
和 HOC
所带来的问题的
为什么要使用 Hook
如果说为什么要使用 Hook
,那么我们可以先来看看 Hook
主要解决的问题,主要有下面三个
- 在组件之间复用状态逻辑很难,
Hook
使你在无需修改组件结构的情况下复用状态逻辑 - 复杂组件变得难以理解,
Hook
将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据) - 难以理解的
Class
,Hook
使你在非Class
的情况下可以使用更多的React
特性
那么到底什么是 Hook
呢?
什么是 Hook
React
在 16.8
的版本当中新增了 Hook
这个特性,它可以让我们在不编写 Class
的情况下使用 state
以及其他的 React
特性,使用 Hook
,你可以在将含有 state
的逻辑从组件中抽象出来,这将可以让这些逻辑容易被测试,同时 Hook
可以帮助你在不重写组件结构的情况下复用这些逻辑,所以它也可以作为一种实现状态逻辑复用的方案
React
内置了一些像 useState
这样的 Hook
,你也可以创建你自己的 Hook
来复用不同组件之间的状态逻辑,其实简单来说 Hook
就是我们常见的 JavaScript
函数,但是使用它们会有两个额外的规则,如下
- 只能在函数最外层调用
Hook
,不要在循环、条件判断或者子函数中调用 - 只能在
React
函数式组件或自定义Hook
中使用Hook
,不要在其他JavaScript
函数中调用
之所以不要在循环当中去调用 Hook
,这是因为 Hook
是通过数组实现的,每次 useState
都会改变下标,React
需要利用调用顺序来正确更新相应的状态,如果 useState
被包裹循环或条件语句中,那每就可能会引起调用顺序的错乱,从而造成意想不到的错误,我们可以利用 eslint
插件来帮助我们避免这些问题
1 | // 安装 |
当然官方也提供了一些默认的 Hook
,如下
钩子名 | 作用 |
---|---|
useState |
初始化和设置状态 |
useEffect |
componentDidMount/componentDidUpdate/componentWillUnmount 结合体,所以可以监听 useState 定义值的变化 |
useContext |
定义一个全局的对象,类似 Context |
useReducer |
可以增强函数提供类似 Redux 的功能 |
useCallback |
记忆作用,共有两个参数,第一个参数为一个匿名函数,就是我们想要创建的函数体,第二参数为一个数组,里面的每一项是用来判断是否需要重新创建函数体的变量,如果传入的变量值保持不变,返回记忆结果,如果任何一项改变,则返回新的结果 |
useMemo |
作用和传入参数与 useCallback 一致,useCallback 返回函数,useDemo 返回值 |
useRef |
获取 ref 属性对应的 DOM |
useImperativeMethods |
自定义使用 Ref 时公开给父组件的实例值 |
useMutationEffect |
作用与 useEffect 相同,但在更新兄弟组件之前,它在 React 执行其 DOM 改变的同一阶段同步触发 |
useLayoutEffect |
作用与 useEffect 相同,但在所有 DOM 改变后同步触发 |
我们下面来看几个平时使用频率较高的 Hook
useState()
那么为什么要使用 useState
呢?简单来说就是为了在函数组件里面使用 Class
组件才有的 setState
方法,因为当我们一个函数组件想要有自己维护的 state
的时候,不得已只能转换成 Class
组件,这样操作会很麻烦,所以就有了 useState
,下面我们来看看如何使用
1 | export default function Button() { |
在上面示例当中,useState
就是一个 Hook
,通过在函数组件里调用它来给组件添加一些内部 state
,React
会在重复渲染时保留这个 state
useState
会返回一对值,包括『当前状态』和『一个让你更新它的函数』,你可以在事件处理函数中或其他一些地方调用这个函数,它类似 Class
组件的 this.setState
,但是它不会把新的 state
和旧的 state
进行合并,useState
唯一的参数就是初始 state
,值得注意的是,不同于 this.state
,这里的 state
不一定要是一个对象,并且这个初始 state
参数只有在第一次渲染时会被用到
更新方式
另外 useState
的更新方式也有两种,即直接更新和函数式更新,其应用场景的区分点在于
- 直接更新不依赖于旧
state
的值 - 函数式更新依赖于旧
state
的值
比如在上面的示例当中,我们只是简单的调用了 handleClick
,但是如果新的 state
需要通过使用先前的 state
计算得出,那么可以将函数传递给 setState
,该函数将接收先前的 state
,并返回一个更新后的值
1 | export default function App() { |
实现合并
与 Class
组件中的 setState
方法不同,useState
不会自动合并更新对象,而是直接替换它,不过我们可以使用函数式的 setState
结合展开运算符来达到合并更新对象的效果
1 | setState(prevState => { |
惰性初始化 State
initialState
参数只会在组件的初始渲染中起作用,后续渲染时会被忽略,也就是说 useState
的初始值,只在第一次有效,也就是再次更新是无效的,比如下面这个示例
1 | const Child = ({ data }) => { |
其应用场景在于,因为创建初始 state
很昂贵时,例如需要通过复杂计算获得,那么则可以传入一个函数,在函数中计算并返回初始的 state
,此函数只在初始渲染时被调用
小结
- 不像
Class
中的this.setState
,Hook
更新state
变量总是替换它而不是合并它 - 推荐使用多个
state
变量而不是单个state
变量,因为state
的替换逻辑而不是合并逻辑,并且利于后续的相关state
逻辑抽离 - 调用
State Hook
的更新函数并传入当前的state
时,React
将跳过子组件的渲染及effect
的执行(React
使用 Object.is 比较算法来比较state
)
useEffect()
useEffect
主要用来引入具有副作用的操作,比如数据获取、订阅或者手动修改过 DOM
等操作就可以称之为副作用,这里我们就以数据请求为例,在之前我们处理数据请求一般都是放在 componentDidMount
当中来进行的,但是现在我们可以放在 useEffect
当中来进行,useEffect
的用法如下
1 | useEffect(() => { |
useEffect
接受两个参数
- 第一个参数是一个回调函数,在第组件一次
render
和之后的每次update
后运行,React
保证在DOM
已经更新完成之后才会运行回调 - 第二个参数是一个状态依赖数组,当配置了状态依赖项后,只有检测到配置的状态变化时,才会调用回调函数,第二个参数可以省略,这时每次组件渲染时,就会重新执行
useEffect()
执行顺序
另外关于 useEffect
的执行顺序,有下面几点需要我们注意
- 正常情况下,
useEffect
会在render
后按照前后顺序执行 useEffect
内部执行是异步的useEffect
的回调函数也是按照先后顺序同时执行的
下面我们就先来看一个使用 useEffect
的示例,如下
1 | const Book = ({ id }) => { |
在上面示例当中,我们传入 [id]
作为 useEffect
第二个参数,如果 id
的值在重新渲染的时候没有发生变化,React
会跳过这个 effect
,这就实现了性能的优化,不过这里也需要注意
- 如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在
effect
中会使用的变量,否则你的代码会引用到先前渲染中的旧变量,即数组最好包含所有在effect
当中使用的可能变化的变量 - 如果想执行只运行一次的
effect
(仅在组件挂载和卸载时执行),可以传递一个空数组([]
)作为第二个参数,这就告诉React
你的effect
不依赖于props
或state
中的任何值,所以它永远都不需要重复执行
所以我们可以利用传递空数组的特性来模拟 componentDidMount
和 componentWillUnmount
,其中 componentDidMount
等价于 useEffect
的回调仅在页面初始化完成后执行一次,当 useEffect
的第二个参数传入一个空数组时可以实现这个效果,但是我们最好忘掉生命周期,只记副作用
同时,useEffect
的第一个参数还可以返回一个函数,当页面渲染了下一次更新的结果后,执行下一次 useEffect
之前,会调用这个函数(组件卸载的时候也会执行清除操作),这个函数常常用来对上一次调用 useEffect
进行清理
1 | useEffect(() => { |
下面是一个具体的示例
1 | export default function HookTest() { |
在控制台当中我们看到对应的输出,但是如果加上浏览器渲染的情况,结果应该是这样的
1 | 渲染 ==> 1 |
那么这里就存在一个问题了,那就是为什么在浏览器渲染完后,再执行清理方法的时候还能找到上次的 state
呢?原因很简单,因为我们在 useEffect
中返回的是一个函数,这形成了一个闭包,这能保证我们上一次执行函数存储的变量不被销毁和污染,可以参考下面的代码来进行理解
1 | var flag = 1 |
执行后的结果是下面这样的
1 | effect ==> 1 |
注意事项
但是关于 useEffect
的使用也有一些需要注意的地方,主要有以下几点
useEffect
里面使用到的state
的值,是会被固定在useEffect
内部,不会被改变,除非useEffect
刷新,重新固定state
的值,也就是说它是在生成useEffect
时候的当前值,不会随着时间改变而变化
1 | const [count, setCount] = useState(0) |
useEffect
不能被判断包裹
1 | const [count, setCount] = useState(0) |
useEffect
不能被打断
1 | const [count, setCount] = useState(0) |
关于以上几点的原因,其实是跟 useEffect
的生成执行规则有关,我们会在 深入 useEffect 章节当中来深入探讨为什么会这样
小结
- 可以把
useEffect Hook
看做componentDidMount
,componentDidUpdate
和componentWillUnmount
这三个函数的组合 - 在
React
的Class
组件中,render
函数是不应该有任何副作用的,一般来说在这里执行操作太早,我们基本上都希望在React
更新DOM
之后才执行我们的操作
useContext()
useContext()
通常用来处理多层级传递数据的方式,在以前组件树中,跨层级祖先组件想要给孙子组件传递数据的时候,除了一层层 props
往下透传之外,我们还可以使用 React Context API
来帮我们做这件事
也就是说,如果需要在多个组件之间共享状态,那么这种情况下就可以考虑使用 useContext()
,其实和 React
之前提供的 Context
的使用方式有些类似,比如下面这种情况,我们有 A
和 B
两个组件,它们需要共享标题内容
1 | const AppContext = React.createContext({}) |
我们首先使用了 React.createContext({})
在组件外部建立一个 Context
,然后使用 <AppContext.Provider>
来提供了一个 Context
对象,这个对象可以被子组件共享,这里需要注意的是不能直接使用 <AppContext>
,否则会报错,另外需要注意在传递值的时候使用的两层大括号
最后我们在需要被共享内容的子组件内使用 useContext()
钩子函数用来引入 Context
对象,从中来获取 title
属性,这是因为当前的 Context
的值是由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value prop
所决定
但是这里有一个需要我们注意的地方,比如我们上方的 B
组件变成了下面这种情况,即不在依赖 Context
,但是渲染的时候仍然放在 <AppContext.Provider>
当中
1 | const B = () => { |
这样一来就会涉及到下面两个问题
useContext
的组件总会在Context
值变化时重新渲染, 所以<MyContext.Provider>
包裹的越多,层级越深,性能会造成影响<MyContext.Provider>
的value
发生变化时候,包裹的组件无论是否订阅content value
,所有组件都会从新渲染
所以在这种情况下,我们自然而然的可以想到,如果组件没有订阅的话,是不是可以避免不必要的渲染,答案是有的,我们可以使用 React.memo
来进行优化,我们将 B
组件调整如下
1 | const B = React.memo((props) => { |
但是需要注意的是,默认情况下 React.memo()
只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现,这部分内容我们会在下面的 useMemo()
部分来进行介绍,也可以参考 Preventing rerenders with React.memo and useContext hook 了解更多
useReducer()
React
本身不提供状态管理功能,通常我们需要借住一些第三方库(比如 Redux
)来进行实现,而 Redux
的核心概念是,组件不能直接修改共享状态,而是需要发出 action
与状态管理器通信,状态管理器收到 action
以后,使用 reducer
函数计算出新的状态进行返回
通常 reducer
函数的形式是 (state, action) => newState
,而 useReducers()
钩子就是用来引入 reducer
功能的,比如下面这个计数器的示例
1 | const myReducer = (state, action) => { |
以上就是 useReducer
的简单用法,可以发现其相对于 Redux
而言代码简化了不少,所以 useReducer()
在这些方面是可以取代 Redux
的,但是它并没有提供例如中间件(middleware
)和时间旅行(time travel
)等功能,所以还是根据实际情况来进行使用
useRef()
使用 useRef Hook
,我们可以轻松的获取到 DOM
的 ref
1 | export default function Input() { |
但是 useRef
并不仅仅可以用来当作获取 ref
使用,使用 useRef
产生的 ref
的 current
属性是可变的,这意味着你可以用它来保存一个任意值,但是需要注意的是,这样方式有点类似于全局作用域,一处被修改,其他地方全都会更新
1 | const [count, setCount] = useState(0) |
不过相对于上面这个示例,采用下面这种方式更为妥善一些
1 | useEffect(() => { |
这样一来我们就不需要实时保存当前的值了,而是调整成了仅仅告知 React
,让其去递增状态就行,而不用管它现在具体是什么值,更为深层次的运行原理可以参考 深入 useEffect 来了解更多
同样的,我们也可以使用 useRef
来模拟 componentDidUpdate
,componentDidUpdate
就相当于除去第一次调用的 useEffect
,我们可以借助 useRef
生成一个标识,来记录是否为第一次执行
1 | function useDidUpdate(callback, prop) { |
简单总结一下就是
useRef
返回一个可变的ref
对象,它会在每次渲染时返回同一个ref
对象,在整个组件的生命周期内是唯一的useRef
可以保存任何可变的值,即也可以存储那些不需要引起页面重新渲染的数据- 如果你刻意地想要从某些异步回调中读取最新的
state
,你可以用一个ref
来保存它,修改它,并从中读取(关于这点可以参考官方文档当中的 有类似实例变量的东西吗?)
useMemo()
先来看看我们为什么要使用 useMemo
呢?我们还是以上面 useState
章节当中的示例为例,稍微的调整一下
1 | const Child = ({ data }) => { |
仔细观察控制台的输出可以发现,在我们每次点击按钮去更新 count
的时候,我们的子组件每次也都会跟着重新渲染,但是我们此时并没有去修改 name
的值,也就是说子组件用到的值其实是没有改变的,那么这样一来就是多余的渲染了,在这样的情况下我们通常会使用 React.memo
来解决这样的问题
1 | const isEqual = (prevProps, nextProps) => { |
我们使用 React.memo
将我们的子组件包裹了起来,并且传入了第二个参数 isEqual
,它是一个函数,它的作用是判断两次 name
是否一致,只有在不一致的时候才会重新触发渲染
调整以后我们再次运行,可以发现此时子组件在 name
属性不改变的情况下就不会再次重新渲染了,但是在这种情况下,我们需要额外提供一个比对函数来对传入的数据进行比对后再决定是否重新渲染,那么有没有更为简便的方式呢?方法是有的,在这种情况下我们就可以考虑使用 useMemo
,调整后代码如下
1 | const Child = (data) => { |
我们使用 useMemo
将我们子组件的渲染部分包裹了起来,并且传递了第二个参数,它是一个依赖数组,作用与我们上面的比对函数 isEqual
是一致的,这样一来我们就不在需要手动的提供比对函数了,React
在渲染的时候就会事先根据我们提供的依赖数组里面的 name
值先去判断一下,如果没有改变则会跳过这次更新,因为 useMemo
帮助我们暂存了上一次的 name
结果,但是在 useMemo
使用过程当中有一个我们需要注意的地方,那就是 useMemo
是在 render
期间执行的,所以在其中不能进行一些额外的副操作,比如网络请求等
看到这里,你也许会问了,那么 React.memo
与 useMemo
又有什么区别呢?其实简单来说 React.memo
和 useMemo
实现的功能都是一样的,首先就是 React.memo
是在最外层包装了整个组件,并且需要手动的去写一个方法来比对数据是否有所改变来决定是否重新渲染,而在某些场景下,我们只是希望组件的部分内容不进行重新渲染,而不是整个组件全都不去重新渲染,也就是说想要实现局部 Pure
的功能,针对于这种情况就可以使用 useMemo
的方式来替代 React.memo
另外需要注意的一点就是,如果函数组件被
React.memo
包裹,并且其实现中拥有useState
或useContext
的Hook
的时候,那么当context
发生变化时,它是仍会重新渲染的,所以在使用的时候一定要小心
那么什么又是 Pure
功能呢?其实就是之前比较流行使用的 React.PureComponent
功能,它和 React.Component
类似,都是定义一个组件类,不同的是 React.Component
并没有实现 shouldComponentUpdate()
而 React.PureComponent
通过 props
和 state
的浅比较实现了,它与 React.memo/useMemo
最主要的区别就是
React.PureComponent
是作用在类中,而React.memo
是作用在函数中
一般来说,如果组件的 props
和 state
相同时,render
的内容也一致,那么就可以使用 React.PureComponent
来提高组件的性能,下面是一个 React.PureComponent
简单的使用示例
1 | // 组件直接继承 React.PureComponent 即可 |
useCallback()
我们在上面介绍的 useMemo
解决了值的缓存问题,那么如果想要缓存函数的话,我们该如何处理呢?来看下面这个示例,也就是上面 useMemo
当中的示例,我们简单的调整了一下
1 | const App = () => { |
当我们点击按钮的时候,会生成了一个 onChange
函数,赋值给了子组件,但是我们可以发现,子组件还是会随着按钮的点击而不停的渲染,尽管子组件什么都没有做,在这种情况下,我们就可以使用 useCallback
将我们的 onChange
函数包裹起来即可,如下
1 | const onChange = useCallback(e => { |
再次点击后就可以发现,子组件不会再重复的渲染了,其实本质上来说,useCallback
与 useMemo
类似,都是有着缓存的作用,不同之处可能在于
useMemo
是缓存值的useCallback
是缓存函数的
另外如果没有依赖,我们可以添加空的依赖,也就是空数组
Mixin/Render Props/HOC/Hook 的优缺点
我们在之前的内容当中介绍过了 Mixin/Render Props/HOC/Hook
的一些用法和需要注意的地方,所以在本小节当中我们就来简单的汇总一下,看看它们几者之间的优缺点
Mixin
的缺陷- 组件与
Mixin
之间存在隐式依赖(Mixin
经常依赖组件的特定方法,但在定义组件时并不知道这种依赖关系) - 多个
Mixin
之间可能产生冲突(比如定义了相同的state
字段) Mixin
倾向于增加更多状态,这降低了应用的可预测性,导致复杂度剧增- 隐式依赖导致依赖关系不透明,维护成本和理解成本迅速攀升
- 难以快速理解组件行为,需要全盘了解所有依赖
Mixin
的扩展行为,及其之间的相互影响 - 组价自身的方法和
state
字段不敢轻易删改,因为难以确定有没有Mixin
依赖它 Mixin
也难以维护,因为Mixin
逻辑最后会被打平合并到一起,很难搞清楚一个Mixin
的输入输出
- 难以快速理解组件行为,需要全盘了解所有依赖
- 组件与
HOC
相比Mixin
的优势HOC
通过外层组件通过Props
影响内层组件的状态,而不是直接改变其State
不存在冲突和互相干扰,这就降低了耦合度HOC
具有天然的层级结构(组件树结构),这降低了复杂度
HOC
的缺陷- 扩展性限制,
HOC
无法从外部访问子组件的State
因此无法通过shouldComponentUpdate()
滤掉不必要的更新(提供了React.PureComponent()
来解决这个问题) Ref
传递问题,Ref
被隔断(提供了React.forwardRef()
来解决这个问题)Wrapper Hell
,HOC
可能出现多层包裹组件的情况,多层抽象同样增加了复杂度和理解成本- 命名冲突,如果高阶组件多次嵌套,没有使用命名空间的话会产生冲突,然后覆盖老属性
- 不可见性,
HOC
相当于在原有组件外层再包装一个组件,无法得知外层的包装是什么
- 扩展性限制,
Render Props
优点- 上述
HOC
的缺点Render Props
都可以解决
- 上述
Render Props
缺陷- 使用繁琐,
HOC
使用只需要借助装饰器语法通常一行代码就可以进行复用,Render Props
无法做到如此简单 - 嵌套过深,
Render Props
虽然摆脱了组件多层嵌套的问题,但是转化为了函数回调的嵌套
- 使用繁琐,
React Hooks
优点- 减少状态逻辑复用的风险,多个
Hook
之间互不影响,这让我们不需要在把一部分精力放在防止避免逻辑复用的冲突上 - 避免地狱式嵌套,
Hook
解决了HOC
和Render Props
的嵌套问题,更加简洁 - 解耦让组件更容易理解,
Hook
可以更方便地把UI
和状态分离,可以让我们更大限度的将公用逻辑抽离,将一个组件分割成一个个更小的函数,做到更彻底的解耦 - 组合,
Hook
中可以引用另外的Hook
形成新的Hook
- 函数友好,
Hook
为函数组件而生,从而解决了类组件的几大问题this
指向容易错误- 分割在不同声明周期中的逻辑使得代码难以理解和维护
- 代码复用成本高(高阶组件容易使代码量剧增)
- 减少状态逻辑复用的风险,多个
React Hooks
缺陷- 额外的学习成本(
Functional Component
与Class Component
之间的困惑) - 写法上有限制(不能出现在条件、循环中),并且写法限制增加了重构成本
- 破坏了
PureComponent
、React.memo()
浅比较的性能优化效果(为了取最新的props
和state
,每次render()
都要重新创建事件处函数) - 在闭包场景可能会引用到旧的
state
、props
值 React.memo()
并不能完全替代shouldComponentUpdate()
(因为拿不到state change
,只针对props change
)
- 额外的学习成本(
不过还是那句老话,我们应该根据实际使用场景来进行选择
Hook 的本质
其实我们在上面提到的 Hook
,本身就是为了解决组件间逻辑公用的问题的,回顾我们现在的做法,几乎都是面向生命周期编程
而 Hook
的出现是把这种面向生命周期编程变成了面向业务逻辑编程,让我们不用再去关注生命周期
而且在最新的 React
中,预置了大量的 Hook
,其中比较重要两个的就是 useState
和 useEffect
useState
使我们在不借助ES6 Class
的前提下,在组件内部使用state
成为可能useEffect
取代了componentDidMount/componentDidUpdate/componentWillUnmount
,提供了一个统一的API
当然除了这两个比较重要的和上面我们介绍到的几个其他的 Hook
之外,我们还可以在 官方文档 中发现更多 Hook
深入 Hook 的原理
本小节内容主要参考 Under the hood of React’s hooks system
我们在之前的章节当中花了大量篇幅来介绍了官方提供的一些 Hook
,以及在使用它的过程当中的一些注意事项,但是我们并没有太过深入的去介绍 Hook
内部运行的机制,所以在本节当中我们就来简单的了解一些它的运行机制,首先我们先来看看 React Hook
系统的简单示意图,如下
Dispatcher
Dispatcher
是一个包含了 Hook
函数的共享对象,基于 ReactDOM
的渲染状态,它将会被动态的分配或者清理,并且它将会确保用户不能在 React
组件之外获取到 Hook
(源码),在切换到正确的 Dispatcher
来呈现根组件之前,我们通过一个名为 enableHooks
的标志来启用或者禁用 Hook
,在技术上来说,这就意味着我们可以在运行时开启或关闭 Hook
,React 16.6+
版本的实验性功能中也加入了它,但它默认处于禁用状态(源码)
当我们完成渲染工作后,我们会废弃 Dispatcher
并禁止 Hook
,来防止在 ReactDOM
的渲染周期之外不小心使用了它,这个机制能够保证用户不会做傻事(源码),Dispatcher
在每次 Hook
的调用中都会被函数 resolveDispatcher()
解析,如果是在 React
的渲染周期之外,React
将会提示我们说 Hook
只能在函数组件内部调用(源码)
下面是我们模拟的 Dispatcher
的简单实现方式
1 | let currentDispatcher |
Hook 队列
在 React
后台,Hook
会被表示为节点,并以调用顺序连接起来,这样表示的原因是 Hook
并不是被简单的创建然后丢弃,它们有一套独有的机制,一个 Hook
会有数个属性,我们首先需要明确以下几点
- 在初次渲染的时候,它的初始状态会被创建
- 它的状态可以在运行时更新
React
可以在后续渲染中记住Hook
的状态React
能根据调用顺序提供给你正确的状态React
知道当前Hook
属于哪个部分
另外,我们需要重新思考我们看待组件状态的方式,目前,我们只把它看作一个简单的对象,也就是下面这样
1 | { |
但是当处理 Hook
的时候,状态需要被看作是一个队列,每个节点都表示了对象的一个模块
1 | { |
在每个函数组件调用前,一个名为 prepareHooks() 的函数将先被调用,在这个函数中,当前结构和 Hook
队列中的第一个 Hook
节点将被保存在全局变量中,这样我们无论何时调用 Hook
函数(useXXX()
),它都能知道运行上下文
1 | let currentlyRenderingFiber |
Hook 队列的简单实现
一旦更新完成,一个名为 finishHooks() 的函数将会被调用,在这个函数中,Hook
队列的第一个节点的引用将会被保存在渲染了的结构的 memoizedState
属性中,这就意味着 Hook
队列和它的状态可以在外部定位到,也就是说我们可以从外部读取某一组件记忆的状态
1 | const ChildComponent = () => { |
State Hook
下面我们来看看在官方提供的一些 Hook
当中,它们是如何运作的,我们先从使用最为广泛的 State Hook
开始看起
你也许会很吃惊,但是 useState
这个 Hook
在后台其实是使用了 useReducer
,并且它将 useReducer
作为预定义的 reducer
(源码),这意味着 useState
返回的结果实际上已经是 reducer
的状态,同时也是 action dispatcher
1 | function basicStateReducer(state, action) { |
所以正如我们期望的那样,我们可以直接将 action dispatcher
和新的状态传入,也就是说我们也可以传入带 action
函数的 dispatcher
,这个 action
函数可以接收旧的状态并返回新的状态,这意味着,当你向组件树发送状态设置器的时候,你可以修改父级组件修改状态,同时不用将它作为另一个属性传入,比如下面这个根据旧状态返回新状态的示例
1 | const ParentComponent = () => { |
Effect Hook
最后我们再来看看 Effect Hook
是如何工作的,Effect Hook
对于组件的生命周期影响很大,但是它和其他 Hook
的行为有一些区别,并且它有一个附加的逻辑层
- 它在渲染时被创建,但是在浏览器绘制后运行
- 如果给出了销毁指令,它们将在下一次绘制前被销毁
- 它会按照定义的顺序被运行
因此,就应该有另一个队列来保存这些 Effect Hook
,并且在绘制后能够被定位到,通常来说应该是组件保存包含了 effect
节点的队列,每个 effect
节点都是一个不同的类型,并能在适当的时候被定位到
- 在修改之前调用
getSnapshotBeforeUpdate()
实例(源码) - 运行所有插入、更新、删除和
ref
的卸载(源码) - 运行所有生命周期函数和
ref
回调函数,生命周期函数会在一个独立的通道中运行,所以整个组件树中所有的替换、更新、删除都会被调用,这个过程还会触发任何特定于渲染器的初始Effect Hook
(源码) useEffect()
调度的effect
也被称为 被动的 effect
Effect Hook
将会被保存在组件一个称为 updateQueue
的属性上,每个 effect
节点都有如下的结构(源码)
tag
,一个二进制数字,它控制了effect
节点的行为(下面会进行介绍)create
,『绘制之后』运行的回调函数destroy
,它是create()
返回的回调函数,将会在『初始渲染前』运行inputs
,一个集合,该集合中的值将会决定一个effect
节点是否应该被销毁或者重新创建next
,它指向下一个定义在函数组件中的effect
节点
React
官方提供了一些比较特殊的 Hook
,比如 useMutationEffect()
和 useLayoutEffect()
,其实这两个 Effect Hook
内部使用了 useEffect()
,实际上这就意味着它们能创建 Effect Hook
,但是却使用了不同的 tag
属性值,这个 tag
属性值是由二进制的值组合而成(源码),下面是 React
支持的 Effect Hook
类型
1 | export const NoEffect = /* */ 0b00000000; |
这些二进制值中最常用的情景是使用管道符号(|
)连接,将比特相加到单个某值上,然后我们就可以使用符号(&
)检查某个 tag
属性是否能触发一个特定的动作,如果结果是非零的,就表示能触发,关于位运算符更为详细的内容可以参考 标志位与掩码,下面是如何使用 React
的二进制设计模式的示例
1 | const effectTag = MountPassive | UnmountPassive |
下面是 React
支持的 Effect Hook
,以及它们的 tag
属性(源码)
UnmountPassive | MountPassive
(Default effect
)UnmountSnapshot | MountMutation
(Mutation effect
)UnmountMutation | MountLayout
(Layout effect
)
下面是 React
如何检查动作触发的(源码)
1 | if ((effect.tag & unmountTag) !== NoHookEffect) { |
所以,基于我们前面介绍的 Effect Hook
,我们可以实际操作,也就是从外部向组件插入一些 effect
1 | function injectEffect(fiber) { |