大纲
- 函数式编程
- 什么是纯函数
- 什么是副作用(Effect)
- 为什么要使用纯函数
- React函数组件和类组件的区别
- 为什么会出现React Hooks
- 前言
- React 组件设计理论
- 什么是Hooks? - React Hooks解决了什么问题
- 常用的hooks
- 前言
- USESTATE + USEEFFECT:初来乍到
- 理解函数式组件的运行过程
- useState: 使用浅析
- useEffect: 使用浅析
- useState + useEffect:渐入佳境
- 深入 useState 的本质
- 深入 useEffect 的本质
- React Hooks源码解析-剖析useState的执行过程
- React Fiber
- React Hooks 如何保存状态(重点)
- React hooks的调度逻辑
- mount 阶段:mountState
- update 阶段:updateState
- React Hooks源码解析-剖析useEffect的执行过程
- mount 阶段:mountEffect
- update 阶段:updateEffect
- 回顾与总结
- 为什么是链表
- 站在巨人肩上
函数式编程
关于纯函数的知识点可以看我之前写的文章前端基础—带你理解什么是函数式编程
React函数组件和类组件的区别
使用上的区别
区别点 | 函数组件 | 类组件 |
---|---|---|
生命周期 | 无 | 有 |
this | 无 | 有 |
state | 无 | 有 |
改变state | React.Hooks : useState | this.setState() |
性能 | 高(不用实例化) | 低(需要实例化) |
其他区别
1.编程方法和设计理念不同
严格地说,类组件和函数组件是有差异的。不同的写法,代表了不同的编程方法论:
以类组件的写法为例:
import React from 'react'
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
componentDidMount() {
alert(this.state.count);
}
componentDidUpdate() {
alert(this.state.count);
}
addcount = () => {
let newCount = this.state.count;
this.setState({
count: newCount +=1
});
}
render() {
return {this.props.name}
}
}
export default Welcome
类(class)是数据和逻辑的封装。 也就是说,组件的状态和操作方法是封装在一起的。如果选择了类的写法,就应该把相关的数据和操作,都写在同一个 class 里面。
函数一般来说,只应该做一件事,就是返回一个值。 如果你有多个操作,每个操作应该写成一个单独的函数。而且,数据的状态应该与操作方法分离。根据这种理念,React 的函数组件只应该做一件事情:返回组件的 HTML 代码,而没有其他的功能。
以函数组件为例。
function Welcome(props) {
return Hello, {props.name}
;
}
export default Welcome
这个函数只做一件事,就是根据输入的参数,返回组件的 HTML 代码。这种只进行单纯的数据计算(换算)的函数,在函数式编程里面称为 "纯函数"(pure function)。
2.组件内部state存在差异
虽然React hooks让函数组件也有了 state,但是 函数组件 state 和 类组件 state 还是有一些差异:
- 函数组件 state 的粒度更细,类组件 state 过于无脑。
- 函数组件 state 保存的是快照,类组件 state 保存的是最新值。
- 要修改的state为引用类型的情况下,类组件 state 不需要传入新的引用,而 function state 必须保证是个新的引用。
2.1快照(闭包) vs 最新值(引用)
先抛出这么一个问题:在 3s 内频繁点击3次按钮,下面代码的执行表现是什么?
class CounterClass extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
handleAddCount = () => {
setTimeout(() => {
setState({count: this.state.count + 1});
}, 3000);
};
render() {
return (
He clicked {this.state.count} times
);
}
如果是这段代码呢?它又会是什么表现?
function CounterFunction() {
const [count, setCount] = useState(0);
const handleAddCount = () => {
setTimeout(() => {
setCount(count + 1);
}, 3000);
};
return (
You clicked {count} times
);
}
如果你能成功答对,那么恭喜你,你已经掌握了 接下来要讲的useState 的用法。在第一个例子中,连续点击3次,页面上的数字会从0增长到3。而第二个例子中,连续点击3次,页面上的数字只会从0增长到1。
这个是为什么呢?其实这主要是引用和闭包的区别。
类组件里面可以通过 this.state 引用到 count,所以每次 setTimeout 的时候都能通过引用拿到上一次的最新 count,所以点击多少次最后就加了多少。
在 函数组件 里面每次更新都是重新执行当前函数,也就是说 setTimeout 里面读取到的 count 是通过闭包获取的,而这个 count 实际上只是初始值,并不是上次执行完成后的最新值,所以最后只加了1次。
2.2快照和引用的转换
如果我想让函数组件也是从0加到3,那么该怎么来解决呢?聪明的你一定会想到,如果模仿类组件里面的 this.state
,我们用一个引用来保存 count 不就好了吗?没错,这样是可以解决,只是这个引用该怎么写呢?我在 state 里面设置一个对象好不好?就像下面这样:
const [state, setState] = useState({ count: 0 })
答案是不行,因为即使 state 是个对象,但每次更新的时候,要传一个新的引用进去,这样的引用依然是没有意义。
setState({ count: count + 1})
想要解决这个问题,那就涉及到另一个新的 Hook 方法 —— useRef。useRef 是一个对象,它拥有一个 current 属性,并且不管函数组件执行多少次,而 useRef 返回的对象永远都是原来那一个。
function CounterFunction() {
const [count, setCount] = useState(0);
const ref = useRef(0);
const handleAddCount = () => {
setTimeout(() => {
setCount(ref.current + 1);
}, 3000);
};
return (
You clicked {count} times
);
}
useRef 有下面这几个特点:
-
useRef
是一个只能用于函数组件的方法。 -
useRef
是除字符串ref
、函数ref
、createRef
之外的第四种获取ref
的方法。 -
useRef
在渲染周期内永远不会变,因此可以用来引用某些数据。 - 修改
ref.current
不会引发组件重新渲染。
为什么会出现React Hooks
1.前言
React Hooks 是 React 16.8 引入的新特性,允许我们在不使用 Class 的前提下使用 state 和其他特性。React Hooks 要解决的问题是状态共享,是继 render-props 和 higher-order components 之后的第三种状态逻辑复用方案,不会产生 JSX 嵌套地狱问题。
2.React 组件设计理论
React以一种全新的编程范式定义了前端开发约束,它为视图开发带来了一种全新的心智模型:
- React认为,UI视图是数据的一种视觉映射,即
UI = F(DATA)
,这里的F
需要负责对输入数据进行加工、并对数据的变更做出响应 - 公式里的
F
在React里抽象成组件,React是以组件(Component-Based)为粒度编排应用的,组件是代码复用的最小单元 - 在设计上,React采用
props
属性来接收外部的数据,使用state
属性来管理组件自身产生的数据(状态),而为了实现(运行时)对数据变更做出响应需要,React采用基于类(Class)的组件设计! - 除此之外,React认为组件是有生命周期的,因此开创性地将生命周期的概念引入到了组件设计,从组件的create到destory提供了一系列的API供开发者使用
这就是React组件设计的理论基础
3.什么是Hooks?
Hooks的单词意思为“钩子”。
React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。而React Hooks 就是我们所说的“钩子”。
那么Hooks要怎么用呢?“你需要写什么功能,就用什么钩子”。对于常见的功能,React为我们提供了一些常用的钩子,当然有特殊需要,我们也可以写自己的钩子。
4.React Hooks能解决什么问题
React 一直在解决一个问题,如何实现分离业务逻辑代码,实现组件内部相关业务逻辑的复用。
一般情况下,我们都是通过组件和自上而下传递的数据流将我们页面上的大型UI组织成为独立的小型UI,实现组件的重用。但是我们经常遇到很难侵入一个复杂的组件中实现重用,因为组件的逻辑是有状态的,无法提取到函数组件当中。当我们在组件中连接外部的数据源,然后希望在组件中执行更多其他的操作的时候,我们就会把组件搞得特别糟糕:
- 问题1:难以共享组件中的与
状态
相关的逻辑,容易产生很多巨大的组件。
对于共享组件中的与状态相关的逻辑,React团队给出过许多的方案,早期使用CreateClass + Mixins,在使用Class Component取代CreateClass之后又设计了Render Props和Higher Order Component。但是都没有很好的解决这个问题。反而让组件体积变得更加臃肿。组件的可读性进一步降低。
HOC使用(老生常谈)的问题:
(1)嵌套地狱,每一次HOC调用都会产生一个组件实例
(2)可以使用类装饰器缓解组件嵌套带来的可维护性问题,但装饰器本质上还是HOC
(3)包裹太多层级之后,可能会带来props属性的覆盖问题
Render Props:
(1)数据流向更直观了,子孙组件可以很明确地看到数据来源
(2)但本质上Render Props是基于闭包实现的,大量地用于组件的复用将不可避免地引入了callback hell问题
(2)丢失了组件的上下文,因此没有this.props
属性,不能像HOC那样访问this.props.children
- 问题2: 我们构建React类组件的方式与组件的生命周期是耦合的。这一鸿沟顺理成章的迫使整个组件中散布着业务相关的逻辑。产生了不可避免的代码冗余。比如在不同的生命周期中处理组件的状态,或者在不同生命周期中执行setTimeOut和clearTimeOut等等。在下面的示例中,我们可以清楚地了解到这一点。我们需要2个生命周期中(componentDidMount、componentDidUpdate)来完成相同的任务——使repos与任何props.id同步。。
class ReposGrid extends React.Component {
constructor (props) {
super(props)
this.state = {
repos: [],
loading: true
}
this.updateRepos = this.updateRepos.bind(this)
}
componentDidMount () {
this.updateRepos(this.props.id)
}
componentDidUpdate (prevProps) {
if (prevProps.id !== this.props.id) {
this.updateRepos(this.props.id)
}
}
updateRepos (id) {
this.setState({ loading: true })
fetchRepos(id)
.then((repos) => this.setState({
repos,
loading: false
}))
}
render() {
if (this.state.loading === true) {
return
}
return (
{this.state.repos.map(({ name, handle, stars, url }) => (
-
- {name}
- @{handle}
- {stars} stars
))}
)
}
}
- 问题3:令人头疼的 this 管理,容易引入难以追踪的 Bug。
React Hooks的出现很好的解决了这些问题:
针对问题1:
前面我们提到过,React难以共享组件中的与状态相关的逻辑,这导致了像高阶组件或渲染道具这样过于复杂的模式。Hooks对此有自己的解决方案---创建我们自己的自定义Hook,自定义Hook以use开头。如下这个useRepos hook将接受我们想要获取的Repos的id,并返回一个数组,其中第一项为loading状态,第二项为repos状态。
function useRepos (id) {
const [ repos, setRepos ] = useState([])
const [ loading, setLoading ] = useState(true)
useEffect(() => {
setLoading(true);
fetchRepos(id)
.then((repos) => {
setRepos(repos);
setLoading(false);
});
}, [id]);
return [loading, repos];
}
这样任何与获取repos相关的逻辑都可以在这个自定义Hook中抽象。现在,不管我们在哪个组件中,每当我们需要有关repos的数据时,我们都可以使用useRepos自定义Hook。
function ReposGrid ({ id }) {
const [ loading, repos ] = useRepos(id);
...
}
function Profile ({ user }) {
const [ loading, repos ] = useRepos(user.id);
...
}
针对问题2和问题3:
当使用ReactHooks时,我们需要忘记所知道的关于通俗的React生命周期方法以及这种思维方式的所有东西。我们已经看到了考虑组件的生命周期时产生的问题-“这(指生命周期)顺理成章的迫使整个组件中散布着相关的逻辑。”相反,考虑一下同步。想想我们曾经用到生命周期事件的时候。不管是设置组件的初始状态、获取数据、更新DOM等等,最终目标总是同步。通常,把React land之外的东西(API请求、DOM等)与Reactland之内的(组件状态)同步,反之亦然。当我们考虑同步而不是生命周期事件时,它允许我们将相关的逻辑块组合在一起。为此,Reaction给了我们另一个叫做useEffect的Hook。
很肯定地说useEffect使我们能在function组件中执行副作用操作。它有两个参数,一个函数和一个可选数组。函数定义要运行的副作用,(可选的)数组定义何时“重新同步”(或重新运行)effect。
React.useEffect(() => {
document.title = `Hello, ${username}`
}, [username]);
在上面的代码中,传递给useEffect的函数将在用户名发生更改时运行。因此,将文档的标题与Hello, ${username}解析出的内容同步。
现在,我们如何使用代码中的useEffect Hook来同步repos和fetchRepos API请求?
function ReposGrid ({ id }) {
const [ repos, setRepos ] = React.useState([])
const [ loading, setLoading ] = React.useState(true)
React.useEffect(() => {
setLoading(true)
fetchRepos(id)
.then((repos) => {
setRepos(repos)
setLoading(false)
})
}, [id])
if (loading === true) {
return
}
return (
{repos.map(({ name, handle, stars, url }) => (
-
- {name}
- @{handle}
- {stars} stars
))}
)
}
相当巧妙,对吧?我们已经成功地摆脱了React.Component, constructor, super, this,更重要的是,我们的业务逻辑不再在整个组件生命周期中散布。
5.常用的hooks
React Hooks常用钩子有如下5种:
- useState() 状态钩子
- useContext() 共享状态钩子
- useReducer(). Action 钩子
- useCallback. function 钩子
- useEffect() 副作用钩子
使用hooks 我们会发现没有了继承,渲染逻辑,生命周期等, 代码看起来更加的轻便简洁了。
React 约定,钩子一律使用 use 前缀命名 (自定义钩子都命名为:useXXXX)
关于常用hooks结束可以看
React Hooks 常用钩子及基本原理
聊聊useCallback
详解 React useCallback & useMemo
USESTATE + USEEFFECT:初来乍到
首先,让我们从最最最常用的两个 Hooks 说起:useState
和 useEffect
。很有可能,你在平时的学习和开发中已经接触并使用过了(当然如果你刚开始学也没关系啦)。不过在此之前,我们先熟悉一下 React 函数式组件的运行过程。
1.理解函数式组件的运行过程
我们知道,Hooks 只能用于 React 函数式组件。因此理解函数式组件的运行过程对掌握 Hooks 中许多重要的特性很关键,请看下图:
可以看到,函数式组件严格遵循 UI = render(data)
的模式。当我们第一次调用组件函数时,触发初次渲染;然后随着 props
的改变,便会重新调用该组件函数,触发重渲染。
你也许会纳闷,动画里面为啥要并排画三个一样的组件呢?因为我想通过这种方式直观地阐述函数式组件的一个重要思想:
每一次渲染都是完全独立的。
后面我们将沿用这样的风格,并一步步地介绍 Hook 在函数式组件中扮演怎样的角色。
2.useState 使用浅析
首先我们来简单地了解一下 useState
钩子的使用,官方文档介绍的使用方法如下:
const [state, setState] = useState(initialValue);
其中 state
就是一个状态变量,setState
是一个用于修改状态的 Setter 函数,而 initialValue
则是状态的初始值。
光看代码可能有点抽象,请看下面的动画:
与之前的纯函数式组件相比,我们引入了 useState
这个钩子,瞬间就打破了之前 UI = render(data)
的安静画面——函数组件居然可以从组件之外把状态和修改状态的函数“钩”过来!并且仔细看上面的动画,通过调用 Setter 函数,居然还可以直接触发组件的重渲染!
提示
你也许注意到了所有的“钩子”都指向了一个绿色的问号,我们会在下面详细地分析那是什么,现在就暂时把它看作是组件之外可以访问的一个“神秘领域”。
结合上面的动画,我们可以得出一个重要的推论:每次渲染具有独立的状态值(毕竟每次渲染都是完全独立的嘛)。也就是说,每个函数中的 state
变量只是一个简单的常量,每次渲染时从钩子中获取到的常量,并没有附着数据绑定之类的神奇魔法。
这也就是老生常谈的 Capture Value 特性。可以看下面这段经典的计数器代码(来自 Dan 的这篇精彩的文章):
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert('You clicked on: ' + count);
}, 3000);
}
return (
You clicked {count} times
);
}
实现了上面这个计数器后(也可以直接通过这个 Sandbox 进行体验),按如下步骤操作:1)点击 Click me 按钮,把数字增加到 3;2)点击 Show alert 按钮;3)在 setTimeout
触发之前点击 Click me,把数字增加到 5。
结果是 Alert 显示 3!
如果你觉得这个结果很正常,恭喜你已经理解了 Capture Value 的思想!如果你觉得匪夷所思嘛……来简单解释一下:
- 每次渲染相互独立,因此每次渲染时组件中的状态、事件处理函数等等都是独立的,或者说只属于所在的那一次渲染
- 我们在
count
为 3 的时候触发了handleAlertClick
函数,这个函数所记住的count
也为 3 - 三秒种后,刚才函数的
setTimeout
结束,输出当时记住的结果:3
这道理就像,你翻开十年前的日记本,虽然是现在翻开的,但记录的仍然是十年前的时光。或者说,日记本 Capture 了那一段美好的回忆。
3.useEffect 使用浅析
你可能已经听说 useEffect
类似类组件中的生命周期方法。但是在开始学习 useEffect
之前,建议你暂时忘记生命周期模型,毕竟函数组件和类组件是不同的世界。官方文档介绍 useEffect
的使用方法如下:
useEffect(effectFn, deps)
effectFn
是一个执行某些可能具有副作用的 Effect 函数(例如数据获取、设置/销毁定时器等),它可以返回一个清理函数(Cleanup),例如大家所熟悉的 setInterval
和 clearInterval
:
useEffect(() => {
const intervalId = setInterval(doSomething(), 1000);
return () => clearInterval(intervalId);
});
可以看到,我们在 Effect 函数体内通过 setInterval
启动了一个定时器,随后又返回了一个 Cleanup 函数,用于销毁刚刚创建的定时器。
OK,听上去还是很抽象,再来看看下面的动画吧:
动画中有以下需要注意的点:
- 每个 Effect 必然在渲染之后执行,因此不会阻塞渲染,提高了性能
- 在运行每个 Effect 之前,运行前一次渲染的 Effect Cleanup 函数(如果有的话)
- 当组件销毁时,运行最后一次 Effect 的 Cleanup 函数
提示
将 Effect 推迟到渲染完成之后执行是出于性能的考虑,如果你想在渲染之前执行某些逻辑(不惜牺牲渲染性能),那么可使用
useLayoutEffect
钩子,使用方法与useEffect
完全一致,只是执行的时机不同。
再来看看 useEffect
的第二个参数:deps
(依赖数组)。从上面的演示动画中可以看出,React 会在每次渲染后都运行 Effect。而依赖数组就是用来控制是否应该触发 Effect,从而能够减少不必要的计算,从而优化了性能。具体而言,只要依赖数组中的每一项与上一次渲染相比都没有改变,那么就跳过本次 Effect 的执行。
仔细一想,我们发现 useEffect
钩子与之前类组件的生命周期相比,有两个显著的特点:
- 将初次渲染(
componentDidMount
)、重渲染(componentDidUpdate
)和销毁(componentDidUnmount
)三个阶段的逻辑用一个统一的 API 去解决 - 把相关的逻辑都放到一个 Effect 里面(例如
setInterval
和clearInterval
),更突出逻辑的内聚性
在最极端的情况下,我们可以指定 deps
为空数组 []
,这样可以确保 Effect 只会在组件初次渲染后执行。实际效果动画如下:
可以看到,后面的所有重渲染都不会触发 Effect 的执行;在组件销毁时,运行 Effect Cleanup 函数。
注意
如果你熟悉 React 的重渲染机制,那么应该可以猜到
deps
数组在判断元素是否发生改变时同样也使用了Object.is
进行比较。因此一个隐患便是,当deps
中某一元素为非原始类型时(例如函数、对象等),每次渲染都会发生改变,从而失去了deps
本身的意义(条件式地触发 Effect)。我们会在接下来讲解如何规避这个困境。
useState + useEffect:渐入佳境
在上一步骤中,我们在 App
组件中定义了一个 State 和 Effect,但是实际应用不可能这么简单,一般都需要多个 State 和 Effect,这时候又该怎么去理解和使用呢?
1.深入 useState 的本质
在上一节的动画中,我们看到每一次渲染组件时,我们都能通过一个神奇的钩子把状态”钩“过来,不过这些钩子从何而来我们打了一个问号。现在,是时候解开谜团了。
注意
以下动画演示并不完全对应 React Hooks 的源码实现,但是它能很好地帮助你理解其工作原理。当然,也能帮助你去啃真正的源码。
我们先来看看当组件初次渲染(挂载)时,情况到底是什么样的:
注意以下要点:
- 在初次渲染时,我们通过
useState
定义了多个状态; - 每调用一次
useState
,都会在组件之外生成一条 Hook 记录,同时包括状态值(用useState
给定的初始值初始化)和修改状态的 Setter 函数; - 多次调用
useState
生成的 Hook 记录形成了一条链表; - 触发
onClick
回调函数,调用setS2
函数修改s2
的状态,不仅修改了 Hook 记录中的状态值,还即将触发重渲染。
OK,重渲染的时候到了,动画如下:
可以看到,在初次渲染结束之后、重渲染之前,Hook 记录链表依然存在。当我们逐个调用 useState
的时候,useState
便返回了 Hook 链表中存储的状态,以及修改状态的 Setter。
提示
当你充分理解上面两个动画之后,其实就能理解为什么这个 Hook 叫
useState
而不是createState
了——之所以叫use
,是因为没有的时候才创建(初次渲染的时候),有的时候就直接读取(重渲染的时候)。
通过以上的分析,我们不难发现 useState
在设计方面的精巧(摘自张立理:对 React Hooks 的一些思考):
- 状态和修改状态的 Setter 函数两两配对,并且后者一定影响前者,前者只被后者影响,作为一个整体它们完全不受外界的影响
- 鼓励细粒度和扁平化的状态定义和控制,对于代码行为的可预测性和可测试性大有帮助
- 除了
useState
(和其他钩子),函数组件依然是实现渲染逻辑的“纯”组件,对状态的管理被 Hooks 所封装了起来
深入 useEffect 的本质
在对 useState
进行一波深挖之后,我们再来揭开 useEffect
神秘的面纱。实际上,你可能已经猜到了——同样是通过一个链表记录所有的 Hook,请看下面的演示:
注意其中一些细节:
-
useState
和useEffect
在每次调用时都被添加到 Hook 链表中; -
useEffect
还会额外地在一个队列中添加一个等待执行的 Effect 函数; - 在渲染完成后,依次调用 Effect 队列中的每一个 Effect 函数。
至此,上一节的动画中那两个“问号”的身世也就揭晓了——只不过是链表罢了!回过头来,我们想起来 React 官方文档 Rules of Hooks 中强调过一点:
Only call hooks at the top level. 只在最顶层使用 Hook。
具体地说,不要在循环、嵌套、条件语句中使用 Hook——因为这些动态的语句很有可能会导致每次执行组件函数时调用 Hook 的顺序不能完全一致,导致 Hook 链表记录的数据失效。具体的场景就不画动画啦,自行脑补吧~
React Hooks源码解析-剖析useState的执行过程
1.React Fiber
关于React Fiber详细解释,可以看我之前写的一篇文章
由浅入深快速掌握React Fiber
我们本节只需了解这2个点即可:
- React现在的渲染都是由Fiber来调度
- Fiber调度过程中的两个阶段(以Render为界)
Fiber是比线程还细的控制粒度,是React 16中的新特性,旨在对渲染过程做更精细的调整。
产生原因:
(1)Fiber之前的reconciler(被称为Stack reconciler)自顶向下的递归mount/update
,无法中断(持续占用主线程),这样主线程上的布局、动画等周期性任务以及交互响应就无法立即得到处理,影响体验
(2)渲染过程中没有优先级可言
React Fiber的调度方式:
把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。
React Fiber把更新过程碎片化,执行过程如下面的图所示,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。
维护每一个分片的数据结构,就是Fiber。
-
有了分片之后,更新过程的调用栈如下图所示,中间每一个波谷代表深入某个分片的执行过程,每个波峰就是一个分片执行结束交还控制权的时机。让线程处理别的事情
Fiber的调度过程分为以下两个阶段:
render/reconciliation阶段 — 里面的所有生命周期函数都可能被中断、执行多次
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
Commit阶段 — 不能被打断,只会执行一次
componentDidMount
componentDidUpdate
compoenntWillunmount
Fiber的增量更新需要更多的上下文信息,之前的vDOM tree显然难以满足,所以扩展出了fiber tree(Current 树),更新过程就是根据输入数据以及现有的fiber tree构造出新的fiber tree(workInProgress 树)。
Current 树和 workInProgress 树
在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,当 React 开始处理更新时,会在内存中再次构建一棵Fiber树,称为workInProgress Fiber树,它反映了要刷新到屏幕的未来状态。current Fiber树中的Fiber节点被称为current fiber。workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,它们通过alternate属性连接。
React应用的根节点通过current指针在不同Fiber树的rootFiber间切换来实现Fiber树的切换。当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新。由于有两颗fiber树,实现了异步中断时,更新状态的保存,中断回来以后可以拿到之前的状态。并且两者状态可以复用,节约了从头构建的时间。
React所有的 work 都是在 workInProgress 树的 Fibler 节点上进行的。当 React 遍历 current 树时,它为每个Fiber节点创建一个替代节点。这些节点构成了 workInProgress 树。一旦处理完所有 update 并完成所有相关 work,React 将把 workInProgress 树刷新到屏幕。一旦在屏幕上渲染 workInProgress 树之后,workInProgress 树将替换 原有current 树成为新的current 树。
hooks挂载在workInProgress树的 Fibler 节点上的memoizedState属性下。当我们函数组件执行之后,hooks
和workInProgress
将是如图的关系:
Fiber节点结构如下:
FiberNode { // fiber结构
memoizedState: any, // 类组件存储最新的state,函数组件存储hooks链表
stateNode: new ClickCounter,
type: ClickCounter, // 判断标签类型是原生或react
alternate: null, // 指向current fiber节点
key: null, 节点key
updateQueue: null, // 更新的队列。
tag: 1, // 判断组件的类型是函数组件还是类组件
child,
return,
sibling,
...
}
stateNode
保存对类组件实例,DOM 节点或与 fiber 节点关联的其他 React 元素类型的引用。一般来说,此属性用于保存与 fiber 关联的 local state。type
定义与此 fiber 关联的函数或类。对于类组件,它指向构造函数,对于 DOM 元素,它指定 HTML 标记。
我把这个字段理解为 fiber 节点与哪些元素相关。tag
定义 fiber节点类型,在 reconciliation 算法中使用它来确定按函数组件还是类组件完成接下来的工作。updateQueue
state 更新,回调以及 DOM 更新的队列。memoizedState
用于创建输出的 fiber 的state。处理更新时,它反映了当前渲染在屏幕上内容的 state。
memoziedState这个字段很重要,是组件更新的唯一依据。在class组件里,它就是this.state的结构,调用this.setState的时候,其实就是修改了它的数据,数据改变了组件就会重新执行。
也就是说,即使是class组件,也不会主动调用任何生命周期函数,而是在memoziedState改变后,组件重新执行,在执行的过程中才会经过这些周期。
所以,这就解释了函数式组件为什么可以通过让hooks(useState)返回的方法改变状态来触发组件的更新,实际上就是修改了对应fiber节点的memoziedState。
memoizedProps
在上一次渲染期间用于创建输出的 fiber 的 props 。pendingProps
在 React element 的新数据中更新并且需要应用于子组件或 DOM 元素的 props。(子组件或者 DOM 中将要改变的 props)key
唯一标识符,当具有一组 children 的时候,用来帮助 React 找出哪些项已更改,已添加或已从列表中删除。与这里所说的React的 “列表和key” 功能有关
2.React Hooks 如何保存状态(重点)
React 官方文档中有提到,React Hooks 保存状态的位置其实与类组件的一致,上面也提到了状态存储在FiberNode的memoziedState
中;翻看源码后,我发现这样的说法没错,但又不全面:
- 两者的状态值都被挂载在组件实例对象FiberNode的
memoizedState
属性中。 - 两者保存状态值的数据结构完全不同;类组件是直接把 state 属性中挂载的这个开发者自定义的对象给保存到
memoizedState
属性中;而 React Hooks 是用Hooks 链表来保存状态的,memoizedState
属性保存的实际上是这个链表的头指针。
下面我们来看看这个Hooks链表的节点是什么样的 :
// Hook类型定义
type Hook = {
memoizedState: any, // useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
baseState: any, // usestate和useReducer中,一次更新中 ,产生的最新state值。
baseUpdate: Update | null, // usestate和useReducer中 保存最新的更新队列。
queue: UpdateQueue | null, // 保存待更新队列 pendingQueue ,更新函数 dispatch 等信息。
next: Hook | null, // 是一个指针、指向下一个 hooks对象
}
官方文档一直强调 React Hooks 的调用只能放在函数组件/自定义 Hooks 函数体的顶层,这是因为我们只能通过 Hooks 调用的顺序来与实际保存的数据结构来关联。举个例子:
function App() {
const [ n1, setN1 ] = useState(1);
if (n1 > 0) {
const [ n2, setN2 ] = useState(2);
}
const [ n3, setN3 ] = useState(3);
}
初始化Hook存储(链表)结构:
组件初始化时,会按顺序从上到下将组件中使用到的hook增加到hooks链表。
一旦在条件语句中声明hooks,在下一次函数组件更新,hooks链表结构将会被破坏,current树的memoizedState缓存hooks信息,和当前workInProgress不一致,如果涉及到读取state等操作,就会发生异常。
- 假如执行了setN1(-1)触发了组件的re-render。re-render时会从第一行代码开始重新执行整个组件,即会按顺序执行整个Hooks链,这时n1小于0,则会执行
useState(3)
分支,相反useState(2)则不会执行到,导致useState(3)
返回的值其实是2,因为首次render之后,只能通过useState返回的dispatch修改对应Hook的memoizedState,通过‘索引’获取缓存的state,因此必须要保证Hooks的顺序不变,所以不能在分支调用Hooks,只有在顶层调用才能保证各个Hooks的执行顺序!
3.React hooks的调度逻辑
调度逻辑如下:
Fiber调度的开始:从beginWork谈起
之前已经说过,React有能力区分不同的组件,所以它会给不同的组件类型打上不同的tag(tag为0-24的数字), 所以在beginWork函数的主要功能就是通过 switch (workInProgress.tag)对不同的组件做不同的更新处理。源码如下:
// ReactFiberBeginWork.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
/** 省略与本文无关的部分 **/
// 根据不同的组件类型走不同的方法
switch (workInProgress.tag) {
// 不确定组件
case IndeterminateComponent: {
const elementType = workInProgress.elementType;
// 加载初始组件
return mountIndeterminateComponent(
current,
workInProgress,
elementType,
renderExpirationTime,
);
}
// 函数组件
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
// 更新函数组件
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
// 类组件
case ClassComponent {
/** 细节略 **/
}
}
renderWithHooks,调用函数组件渲的主要函数
我们从上边可以看出来,beginWork会根据组件类型判断执行mountIndeterminateComponent或执行updateFunctionComponent,这两个方法都会用到renderWithHooks
这个函数.renderWithHooks函数作用是调用function组件函数的主要函数。我们重点看看renderWithHooks做了些什么?
export function renderWithHooks(
current,
workInProgress,
Component,
props,
secondArg,
nextRenderExpirationTime,
) {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.expirationTime = NoWork;
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg);
if (workInProgress.expirationTime === renderExpirationTime) {
// ....这里的逻辑我们先放一放
}
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
currentHook = null
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
return children;
}
// 挂载时的Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
readContext,
// ...
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useMemo: mountMemo,
useState: mountState,
// ...
};
// 更新时的Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
// ...
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useMemo: updateMemo,
useRef: updateRef,
useState: updateState,
// ....
};
所有的函数组件执行,都是在这里方法中,首先我们应该明白几个感念,这对于后续我们理解useState是很有帮助的。
workInProgress.expirationTime: react用不同的expirationTime,来确定更新的优先级。
currentHook : 可以理解 current树上的指向的当前调度的 hooks节点。
workInProgressHook : 可以理解 workInProgress树上指向的当前调度的 hooks节点。
总结了一下Fiber调度逻辑为
- 在renderWithHooks中,会先根据fiber的memoizedState是否为null,来判断组件是否挂载。因为memoizedState在函数式组件中是存放hooks链表的。memoizedState为null则挂载,否则更新
- 在mount(挂载)时,函数式组件执行,Dispatcher为HooksDispatcherOnMount,hooks被调用会初始化hooks链表、initialState、dispatch函数,并返回。(首次渲染过程)
- 在update(更新)时,函数式组件执行,Dispatcher为HooksDispatcherOnUpdate,接着updateWorkInProgressHook获取当前work的Hook。然后根据numberOfReRenders 是否大于0来判断是否处理re-render状态:是的话,执行renderPhaseUpdates,获取第一个update,执行update然后获取下一个update循环执行,直到下一个update为null;(更新过程)
以useState为例,具体执行过程如下
接下来将以useState和useEffect在mount 阶段和update阶段源码为例进一步分析Hooks内部执行过程:
3. mount 阶段:mountState
首先我们需要知道,在组件里,多次调用useState,或者其他hook,那react怎么知道我们当前是哪一个hook呢。其实在react内部,所有的hook api第一次被调用的时候都会先创建一个hook对象,来保存相应的hook信息。然后,这个hook对象,会被加到一个链表上,这样我们每次渲染的时候,只要从这个链表上面依次的去取hook对象,就知道了当前是哪一个hook了。
下面我们就看一下这个hook对象的具体格式。
const hook: Hook = {
memoizedState: null, // 缓存当前state的值
baseState: null, // 初始化initState,以及每次dispatch之后的newState
queue: null, // update quene
baseUpdate: null, //基于哪一个hook进行更新,循环update quene的起点
next: null, // 指向下一个hook
};
我们从引入 hooks开始,以useState为例子,当我们在项目中这么写:
import { useState } from 'react'
...
const [count, setCount] = useState(0);
于是乎我们去找useState,看看它到底是哪路神仙?
打开react源码。我们进入ReactHooks.js来看看,发现useState的实现竟然异常简单,只有短短两行
// ReactHooks.js
export function useState(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
看来重点都在这个dispatcher上,dispatcher通过resolveDispatcher()来获取,这个函数同样也很简单,只是将ReactCurrentDispatcher.current的值赋给了dispatcher,并返回dispatcher;
// ReactHooks.js
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
挂载阶段Dispatcher会指向HooksDispatcherOnMount 对象。也就是说这个阶段的执行useState实际执行的是mountState方法
// 在挂载和更新状态下ReactCurrentDispatcher.current指向的值不同
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 挂载时的Dispatcher
const HooksDispatcherOnMount: Dispatcher = {
readContext,
// ...
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useMemo: mountMemo,
useState: mountState,
// ...
};
以挂载为例,mountState具体如下:
function mountState < S > (initialState: (() = >S) | S, ) : [S, Dispatch < BasicStateAction < S >> ] {
// 创建一个新的hook对象,并返回当前workInProgressHook
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState; // 第二步:获取初始值并初始化hook对象
const queue = hook.queue = { // 新建一个队列
// 保存 update 对象
pending: null,
// 保存dispatchAction.bind()的值
dispatch: null,
// 一次新的dispatch触发前最新的reducer
// useState 保存固定函数: 可以理解为一个react 内置的reducer
// (state, action) => { return typeof action === 'function' ? action(state) : action }
lastRenderedReducer: reducer
// 一次新的dispatch触发前最新的state
lastRenderedState: (initialState: any),
last: null, // 最后一次的update对象
}
// 绑定当前 fiber 和 queue.
const dispatch: Dispatch < BasicStateAction < S > ,
>=(queue.dispatch = (dispatchAction.bind(null, currentlyRenderingFiber, queue, ) : any));
// 返回当前状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
第一步,创建hook对象,并将该hook对象加到hook链的末尾,这一步通过mountWorkInProgressHook函数执行,代码如下:
第二步:初始化hook对象的状态值,也就是我们传进来的initState的值。
第三步:创建更新队列,这个队列是更新状态值的时候用的。
第四步:绑定dispatchAction函数。我们可以看到最后一行返回的就是这个函数。也就是说这个函数,其实就是我们改变状态用的函数,就相当于是setState函数。这里它先做了一个绑定当前quene和fiber对象的动作,就是为了在调用setState的时候,知道该更改的是那一个状态的值。
function mountWorkInProgressHook() {
// 初始化的hook对象
var hook = {
memoizedState: null,
// 存储更新后的state值
baseState: null,
// 存储更新前的state
baseQueue, // 更新函数
queue: null,
// 存储多次的更新行为
next: null // 指向下一次useState的hook对象
};
// workInProgressHook是一个全局变量,表示当前正在处理的hook
// 如果workInProgressHook链表为null就将新建的hook对象赋值给它,如果不为null,那么就加在链表尾部。
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
mountWorkInProgressHook这个函数做的事情很简单,首先每次执行一个hooks函数,都产生一个hook对象,里面保存了当前hook信息,然后将每个hooks以链表形式串联起来,并赋值给workInProgress的memoizedState。也就证实了上述所说的,函数组件用memoizedState存放hooks链表。
至于hook对象中都保留了那些信息?我这里先分别介绍一下 :
memoizedState
: useState中 保存 state 信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和 deps | useRef 中保存的是 ref 对象。
baseQueue
: usestate和useReducer中 保存最新的更新队列。
baseState
: usestate和useReducer中,一次更新中 ,产生的最新state值。
queue
: 保存待更新队列 pendingQueue ,更新函数 dispatch 等信息。
next
: 指向下一个 hooks对象。
从上面的代码可以看到,hook其实是以链表的形式存储起来的。每一个hook都有一个指向下一个hook的指针。如果我们在组件代码中声明了多个hook,那这些hook对象之间是这样排列的:
React会把hook对象挂到Fiber节点的memoizedState
属性上:
初始化完成后,怎样对state值进行更新的呢?实际上就是通过dispatchAction方法进行更新的,如下:
// currentlyRenderingFiber$1是一个全局变量,表示当前正在渲染的FiberNode
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
dispatchAction逻辑如下:
function dispatchAction(
fiber: Fiber,
queue: UpdateQueue,
action: A,
) {
/** 省略Fiber调度相关代码 **/
// 创建新的新的update, action就是我们setCount里面的值(count+1, count+2, count+3…)
const update: Update = {
expirationTime,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// 重点:构建query
// queue.last是最近的一次更新,然后last.next开始是每一次的action
const last = queue.last;
if (last === null) {
// 只有一个update, 自己指自己-形成环
update.next = update;
} else {
const first = last.next;
if (first !== null) {
update.next = first;
}
last.next = update;
}
queue.last = update;
/** 省略特殊情况相关代码 **/
// 创建一个更新任务
scheduleWork(fiber, expirationTime);
}
简单理解:
省略无关代码,我们可以看到实际上,dispatchAction这个函数主要做了两件事情。
第一件就是创建了一个update对象,这个对象上面保存了本次更新的相关信息,包括新的状态值action。
-
第二件,就是将所有的update对象串成了一个环形链表,保存在我们hook对象的queue属性上面。所以我们就知道了queue这个属性的意义,它是保存所有更新行为的地方。
dispatchAction
函数是更新state的关键,在dispatchAction中维护了一份queue的数据结构。queue是一个环形链表,规则:- queue.last指向最近一次更新
- last.next指向第一次更新
- 后面就依次类推,最终倒数第二次更新指向last,形成一个环形链表,如下图。
所以每次插入新update时,就需要将原来的first指向queue.last.next。再将update指向queue.next,最后将queue.last指向update.
- 在这里我们可以看到,我们要更改的状态值并没有真的改变,只是被缓存起来了。那么真正改变状态值的地方在哪呢?答案就是在下一次render时,函数组件里的useState又一次被调用了,这个时候才是真的更新state的时机。
- 理论上可以同时调用多次dispatch,但只有最后一次会生效(queue的last指针指向最后一次update的state)
- 注意
useState
更新数据和setState
不同的是,后者会与old state做merge,我们只需把更改的部分传进去,但是useState
则是直接覆盖!
下面这张图,是我自己画的简易版useState源码的流程图。
至此,mount阶段的useState讲解完毕,接下来讲解状态更新后的useState
update 阶段:updateState
上述介绍了第一次渲染函数组件,react-hooks useState初始化都做些什么,接下来,我们分析一下,react-hooks useState update和re-render:
更新阶段resolveDispatcher函数中Dispatcher会指向HooksDispatcherOnUpdate 对象。也就是说这个阶段的执行useState实际执行的是updateState方法
// 更新时的Dispatcher
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
// ...
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useMemo: updateMemo,
useRef: updateRef,
useState: updateState,
// ....
};
这里就是我们组件更新时,调用useState时真正走的逻辑了。
function updateState(
initialState: (() => S) | S,
): [S, Dispatch>] {
return updateReducer(basicStateReducer, (initialState: any));
}
function updateReducer(
reducer: (S, A) => S, // 对于useState来说就是basicStateReducer
initialArg: I,
init?: I => S,
): [S, Dispatch] {
const hook = updateWorkInProgressHook(); // 获取当前正在工作的hook,Q1
const queue = hook.queue; // 更新队列
// The last update in the entire queue
const last = queue.last; // 最后一次的update对象
// The last update that is part of the base state.
const baseUpdate = hook.baseUpdate; // 上一轮更新的最后一次更新对象
const baseState = hook.baseState; // 上一次的action,现在是初始值
// Find the first unprocessed update.
let first;
if (baseUpdate !== null) {
if (last !== null) {
// For the first update, the queue is a circular linked list where
// `queue.last.next = queue.first`. Once the first update commits, and
// the `baseUpdate` is no longer empty, we can unravel the list.
last.next = null; // 因为quene是一个环形链表,所以这里要置空
}
first = baseUpdate.next; // 第一次是用的last.next作为第一个需要更新的update,第二次之后就是基于上一次的baseUpdate来开始了(baseUpdate就是上一次的最后一个更新)
} else {
first = last !== null ? last.next : null; // last.next是第一个update
}
if (first !== null) { // 没有更新,则不需要执行,直接返回
let newState = baseState;
let newBaseState = null;
let newBaseUpdate = null;
let prevUpdate = baseUpdate;
let update = first;
let didSkip = false;
do { // 循环链表,执行每一次更新
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
...
} else { // 正常逻辑
// This update does have sufficient priority.
// Process this update.
if (update.eagerReducer === reducer) { // 如果是useState,他的reducer就是basicStateReducer
// If this update was processed eagerly, and its reducer matches the
// current reducer, we can use the eagerly computed state.
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);
if (!didSkip) { // 不跳过,就更新baseUpdate和baseState
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
...
hook.memoizedState = newState; // 更新hook对象
hook.baseUpdate = newBaseUpdate;
hook.baseState = newBaseState;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
updateState做的事情,实际上就是拿到更新队列,循环队列,并根据每一个update对象对当前hook进行状态更新。最后返回最终的结果。
对于更新阶段,说明上一次 workInProgress 树已经赋值给了 current 树。存放hooks信息的memoizedState,此时已经存在current树上,react对于hooks的处理逻辑和fiber树逻辑类似。
对于一次函数组件更新,当再次执行hooks函数的时候,比如 useState(0) ,首先要从current的hooks中找到与当前workInProgressHook对应的currentHook,然后复制一份currentHook给workInProgressHook,接下来hooks函数执行的时候,把最新的状态更新到workInProgressHook,保证hooks状态不丢失。
所以函数组件每次更新,每一次react-hooks函数执行,都需要有一个函数去做上面的操作,这个函数就是updateWorkInProgressHook,我们接下来一起看这个updateWorkInProgressHook。
updateWorkInProgressHook
function updateWorkInProgressHook() {
let nextCurrentHook;
if (currentHook === null) { /* 如果 currentHook = null 证明它是第一个hooks */
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else { /* 不是第一个hooks,那么指向下一个 hooks */
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook
if (workInProgressHook === null) { //第一次执行hooks
// 这里应该注意一下,当函数组件更新也是调用 renderWithHooks ,memoizedState属性是置空的
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
/* 这个情况说明 renderWithHooks 执行 过程发生多次函数组件的执行 ,我们暂时先不考虑 */
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook = { //创建一个新的hook
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) { // 如果是第一个hooks
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else { // 重新更新 hook
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
这一段的逻辑大致是这样的:
首先如果是第一次执行hooks函数,那么从current树上取出memoizedState ,也就是旧的hooks。
然后声明变量nextWorkInProgressHook,这里应该值得注意,正常情况下,一次renderWithHooks执行,workInProgress上的memoizedState会被置空,hooks函数顺序执行,nextWorkInProgressHook应该一直为null,那么什么情况下nextWorkInProgressHook不为null,也就是当一次renderWithHooks执行过程中,执行了多次函数组件,也就是在renderWithHooks中这段逻辑。
if (workInProgress.expirationTime === renderExpirationTime) {
// ....这里的逻辑我们先放一放
}
这里面的逻辑,实际就是判定,如果当前函数组件执行后,当前函数组件的还是处于渲染优先级,说明函数组件又有了新的更新任务,那么循坏执行函数组件。这就造成了上述的,nextWorkInProgressHook不为 null 的情况。
最后复制current的hooks,把它赋值给workInProgressHook,用于更新新的一轮hooks状态。
执行updateWorkInProgressHook获取到hook之后,我们继续看接下来的操作:
看起来很复杂,让我们慢慢吃透,首先将上一次更新的pending queue
合并到 basequeue
,为什么要这么做,比如我们再一次点击事件中这么写,
function Index(){
const [ number ,setNumber ] = useState(0)
const handerClick = ()=>{
// setNumber(1)
// setNumber(2)
// setNumber(3)
setNumber(state=>state+1)
// 获取上次 state = 1
setNumber(state=>state+1)
// 获取上次 state = 2
setNumber(state=>state+1)
}
console.log(number) // 3
return
{ number }
}
点击按钮, 打印 3
三次setNumber
产生的update
会暂且放入pending queue
,在下一次函数组件执行时候,三次 update
被合并到 baseQueue
。结构如下图:
setState.jpg
接下来会把当前useState
或是useReduer
对应的hooks
上的baseState
和baseQueue
更新到最新的状态。会循环baseQueue
的update
,复制一份update
,更新 expirationTime
,对于有足够优先级的update
(上述三个setNumber
产生的update
都具有足够的优先级),我们要获取最新的state
状态。,会一次执行useState
上的每一个action
。得到最新的state
。
更新state
sset1.jpg
这里有会有两个疑问️:
- 问题一:这里不是执行最后一个
action
不就可以了嘛?
答案: 原因很简单,上面说了 useState
逻辑和useReducer
差不多。如果第一个参数是一个函数,会引用上一次 update
产生的 state
, 所以需要循环调用,每一个update
的reducer
,如果setNumber(2)
是这种情况,那么只用更新值,如果是setNumber(state=>state+1)
,那么传入上一次的 state
得到最新state
。
- 问题二:什么情况下会有优先级不足的情况(
updateExpirationTime < renderExpirationTime
)?
答案: 这种情况,一般会发生在,当我们调用setNumber
时候,调用scheduleUpdateOnFiber
渲染当前组件时,又产生了一次新的更新,所以把最终执行reducer
更新state
任务交给下一次更新。