大纲
- 😁 函数式编程
- 🏆 什么是纯函数
- 🏆 什么是副作用(Effect)
- 🏆 为什么要使用纯函数
- 😁 React函数组件和类组件的区别
- 😁 为什么会出现React Hooks
- 🏆 前言
-🏆 React 组件设计理论 - 🏆 React Hooks解决了什么问题
- 🏆 常用的hooks
- 🏆 前言
- 😁 USESTATE + USEEFFECT:初来乍到
- 🏆 理解函数式组件的运行过程
- 🏆 useState: 使用浅析
- 🏆 useEffect: 使用浅析
- 😁 useState + useEffect:渐入佳境
- 🏆 深入 useState 的本质
- 🏆 深入 useEffect 的本质
- 😁 React Hooks源码解析-剖析useState的执行过程
- 🏆 React Fiber
- 🏆 从memoizedState分析为什么必须在函数组件顶部作用域调用Hooks API(重点)
- 🏆 React hooks的调度逻辑
- 🏆 useState hook如何更新数据
- 🏆 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 <h1>{this.props.name}</h1>
}
}
export default Welcome
类(class)是数据和逻辑的封装。 也就是说,组件的状态和操作方法是封装在一起的。如果选择了类的写法,就应该把相关的数据和操作,都写在同一个 class 里面。
image函数一般来说,只应该做一件事,就是返回一个值。 如果你有多个操作,每个操作应该写成一个单独的函数。而且,数据的状态应该与操作方法分离。根据这种理念,React 的函数组件只应该做一件事情:返回组件的 HTML 代码,而没有其他的功能。
image以函数组件为例。
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
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 (
<div>
<p>He clicked {this.state.count} times</p>
<button onClick={this.handleAddCount.bind(this)}>
Show count
</button>
</div>
);
}
如果是这段代码呢?它又会是什么表现?
function CounterFunction() {
const [count, setCount] = useState(0);
const handleAddCount = () => {
setTimeout(() => {
setCount(count + 1);
}, 3000);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleAddCount}>Show count</button>
</div>
);
}
如果你能成功答对,那么恭喜你,你已经掌握了 接下来要讲的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 (
<div>
<p>You clicked {count} times</p>
<button onClick={handleAddCount}>Show count</button>
</div>
);
}
image
useRef 有下面这几个特点:
-
useRef
是一个只能用于函数组件的方法。 -
useRef
是除字符串ref
、函数ref
、createRef
之外的第四种获取ref
的方法。 -
useRef
在渲染周期内永远不会变,因此可以用来引用某些数据。 - 修改
ref.current
不会引发组件重新渲染。
为什么会出现React Hooks
Why React Hooks.png1.前言
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.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 <Loading />
}
return (
<ul>
{this.state.repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
}
- 问题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 <Loading />
}
return (
<ul>
{repos.map(({ name, handle, stars, url }) => (
<li key={name}>
<ul>
<li><a href={url}>{name}</a></li>
<li>@{handle}</li>
<li>{stars} stars</li>
</ul>
</li>
))}
</ul>
)
}
相当巧妙,对吧?我们已经成功地摆脱了React.Component, constructor, super, this,更重要的是,我们的业务逻辑不再在整个组件生命周期中散布。
常用的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 中许多重要的特性很关键,请看下图:
image可以看到,函数式组件严格遵循 UI = render(data)
的模式。当我们第一次调用组件函数时,触发初次渲染;然后随着 props
的改变,便会重新调用该组件函数,触发重渲染。
你也许会纳闷,动画里面为啥要并排画三个一样的组件呢?因为我想通过这种方式直观地阐述函数式组件的一个重要思想:
每一次渲染都是完全独立的。
后面我们将沿用这样的风格,并一步步地介绍 Hook 在函数式组件中扮演怎样的角色。
2.useState 使用浅析
首先我们来简单地了解一下 useState
钩子的使用,官方文档介绍的使用方法如下:
const [state, setState] = useState(initialValue);
其中 state
就是一个状态变量,setState
是一个用于修改状态的 Setter 函数,而 initialValue
则是状态的初始值。
光看代码可能有点抽象,请看下面的动画:
image与之前的纯函数式组件相比,我们引入了 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 (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={handleAlertClick}>
Show alert
</button>
</div>
);
}
实现了上面这个计数器后(也可以直接通过这个 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,听上去还是很抽象,再来看看下面的动画吧:
image动画中有以下需要注意的点:
- 每个 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 的源码实现,但是它能很好地帮助你理解其工作原理。当然,也能帮助你去啃真正的源码。
我们先来看看当组件初次渲染(挂载)时,情况到底是什么样的:
image注意以下要点:
- 在初次渲染时,我们通过
useState
定义了多个状态; - 每调用一次
useState
,都会在组件之外生成一条 Hook 记录,同时包括状态值(用useState
给定的初始值初始化)和修改状态的 Setter 函数; - 多次调用
useState
生成的 Hook 记录形成了一条链表; - 触发
onClick
回调函数,调用setS2
函数修改s2
的状态,不仅修改了 Hook 记录中的状态值,还即将触发重渲染。
OK,重渲染的时候到了,动画如下:
image可以看到,在初次渲染结束之后、重渲染之前,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。
-
有了分片之后,更新过程的调用栈如下图所示,中间每一个波谷代表深入某个分片的执行过程,每个波峰就是一个分片执行结束交还控制权的时机。让线程处理别的事情
image.png
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渲染Hook是从renderWithHooks
函数开始的:
function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
...
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
...
}
-
从renderWithHooks函数可以看出:在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属性下,Fiber节点结构如下:
Fiber节点
FiberNode { // fiber结构
stateNode: new ClickCounter,
type: ClickCounter, // 判断标签类型是原生或react
alternate: null, // 指向current fiber节点
key: null, 节点key
updateQueue: null, // state 更新,回调以及 DOM 更新的队列。
memoizedState: any, // 类组件存储最新的state,函数组件存储hooks列表
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.从memoizedState分析为什么必须在函数组件顶部作用域调用Hooks API(重点)
4. useState hook如何更新数据
本节主要概念:
- useState是如何被引入以及调用的
- useState为什么能触发组件更新
所有的Hooks在React.js中被引入,挂载在React对象中
// React.js
import {
useCallback,
useContext,
useEffect,
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState,
} from './ReactHooks';
我们进入ReactHooks.js来看看,发现useState的实现竟然异常简单,只有短短两行
// ReactHooks.js
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
看来重点都在这个dispatcher上,dispatcher通过resolveDispatcher()来获取,这个函数同样也很简单,只是将ReactCurrentDispatcher.current的值赋给了dispatcher
// ReactHooks.js
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
这个dispatcher
对象包含了所有的官方内置hook。
dispatcher:{
readContext: ƒ (context, observedBits),
useCallback: ƒ (callback, deps),
useContext: ƒ (context, observedBits),
useEffect: ƒ (create, deps),
useImperativeHandle: ƒ (ref, create, deps),
useLayoutEffect: ƒ (create, deps),
useMemo: ƒ (create, deps),
useReducer: ƒ (reducer, initialArg, init),
useRef: ƒ (initialValue),
useState: ƒ (initialState),
useDebugValue: ƒ (value, formatterFn),
useResponder: ƒ (responder, props),
useDeferredValue: ƒ (value, config),
useTransition: ƒ (config)
}
先暂时不管这个dispatcher是怎么来的,我们接着看下useState的内部逻辑。
与类组件中setState能够触发更新的类似,useState是组件能够触发更新的关键原因,这一点我们可以从useState执行过程和源码来分析:
useState(hooks)的具体执行过程如下:
image.png
- updateContainer → … → beginWork
- beginWork中会根据当前要执行更新的fiber的tag来判断组件是函数组件还是类组件,分别执行不同的逻辑,在函数式组件,执行了updateFunctionComponent来渲染组件。函数组件的更新分
组件初次渲染
和组件更新
2种:
1.3组件初次渲染
我们从Fiber调度的开始:ReactFiberBeginwork来谈起
之前已经说过,React有能力区分不同的组件,所以它会给不同的组件类型打上不同的tag, 详见shared/ReactWorkTags.js所以在beginWork的函数中,就可以根据workInProgess(就是个Fiber节点)上的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 {
/** 细节略 **/
}
}
在updateFunctionComponent中,对hooks的处理如下:
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderExpirationTime,
);
所以,React Hooks 的渲染核心是renderWithHooks,在renderWithHooks函数中,初始化了Dispatcher。
export function renderWithHooks < Props, SecondArg > (current: Fiber | null, workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) = >any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ) : any {
// 若Fiber为空,则认为是首次加载
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 挂载时的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,
// ....
};
}
- 在renderWithHooks中,会先根据fiber的memoizedState是否为null,来判断是否已经初始化。因为memoizedState在函数式组件中是存放hooks的。是则mount,否则update(判断是否执行过,没有则挂载,有则更新)
- 在mount(挂载)时,函数式组件执行,ReactCurrentDispatcher.current为HooksDispatcherOnMount,被调用,会初始化hooks链表、initialState、dispatch函数,并返回。这里就完成了useState的初始化,后续函数式组件继续执行,完成渲染返回。(首次渲染过程)
- 在update(更新)时,函数式组件执行,ReactCurrentDispatcher.current为HooksDispatcherOnUpdate,被调用,updateWorkInProgressHook用于获取当前work的Hook。然后根据numberOfReRenders 是否大于0来判断是否处理re-render状态:是的话,执行renderPhaseUpdates,获取第一个update,然后循环执行,获取新的state,直到下一个update为null;否的话,获取update链表的第一个update,进行循环,判断update的优先级是否需要更新,对于优先级高的进行更新。(更新过程)
- 结果返回当前状态和修改状态的方法
以挂载为例,生成一个hook对象(mountState),并对hook对象进行初始化(mountWorkInProgressHook),具体如下:
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),
}
// 绑定当前 fiber 和 queue.
const dispatch: Dispatch < BasicStateAction < S > ,
>=(queue.dispatch = (dispatchAction.bind(null, currentlyRenderingFiber, queue, ) : any));
// 返回当前状态和修改状态的方法
return [hook.memoizedState, dispatch];
}
首先来看hook对象是如何生成:
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;
}
初始化完成后,怎样对stateA值进行更新的呢?实际上就是通过dispatchAction方法进行更新的,如下:
// currentlyRenderingFiber$1是一个全局变量,表示当前正在渲染的FiberNode
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
dispatchAction逻辑如下:
function dispatchAction(fiber, queue, action) {
// ...
// 1. 创建update对象
// 该对象保存的是调度优先级/state/reducer以及用户调用dispatch/setState 时传入的action
const update: Update < S,
A > ={
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// 2. 将update更新到queue.pending中,最后的update.next 指向第一个update对象,形成一个闭环。
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
简单理解:
初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中,共享同一个 memoizedState,共享同一个顺序。
更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。
网友评论