实战+源码 带你快速掌握React Hooks

React Hooks

大纲

  • 😁 函数式编程
    • 🏆 什么是纯函数
    • 🏆 什么是副作用(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 <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 有下面这几个特点:

  1. useRef 是一个只能用于函数组件的方法。
  2. useRef 是除字符串 ref、函数 refcreateRef 之外的第四种获取 ref 的方法。
  3. useRef 在渲染周期内永远不会变,因此可以用来引用某些数据。
  4. 修改 ref.current 不会引发组件重新渲染。

为什么会出现React Hooks

Why React Hooks.png
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 <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,更重要的是,我们的业务逻辑不再在整个组件生命周期中散布。

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 说起:useStateuseEffect 。很有可能,你在平时的学习和开发中已经接触并使用过了(当然如果你刚开始学也没关系啦)。不过在此之前,我们先熟悉一下 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。

image

结果是 Alert 显示 3!

如果你觉得这个结果很正常,恭喜你已经理解了 Capture Value 的思想!如果你觉得匪夷所思嘛……来简单解释一下:

  • 每次渲染相互独立,因此每次渲染时组件中的状态、事件处理函数等等都是独立的,或者说只属于所在的那一次渲染
  • 我们在 count 为 3 的时候触发了 handleAlertClick 函数,这个函数所记住的 count 也为 3
  • 三秒种后,刚才函数的 setTimeout 结束,输出当时记住的结果:3

这道理就像,你翻开十年前的日记本,虽然是现在翻开的,但记录的仍然是十年前的时光。或者说,日记本 Capture 了那一段美好的回忆。

3.useEffect 使用浅析

你可能已经听说 useEffect 类似类组件中的生命周期方法。但是在开始学习 useEffect 之前,建议你暂时忘记生命周期模型,毕竟函数组件和类组件是不同的世界。官方文档介绍 useEffect 的使用方法如下:

useEffect(effectFn, deps)

effectFn 是一个执行某些可能具有副作用的 Effect 函数(例如数据获取、设置/销毁定时器等),它可以返回一个清理函数(Cleanup),例如大家所熟悉的 setIntervalclearInterval

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 里面(例如 setIntervalclearInterval),更突出逻辑的内聚性

在最极端的情况下,我们可以指定 deps 为空数组 [] ,这样可以确保 Effect 只会在组件初次渲染后执行。实际效果动画如下:

image

可以看到,后面的所有重渲染都不会触发 Effect 的执行;在组件销毁时,运行 Effect Cleanup 函数。

注意

如果你熟悉 React 的重渲染机制,那么应该可以猜到 deps 数组在判断元素是否发生改变时同样也使用了 Object.is 进行比较。因此一个隐患便是,当 deps 中某一元素为非原始类型时(例如函数、对象等),每次渲染都会发生改变,从而失去了 deps 本身的意义(条件式地触发 Effect)。我们会在接下来讲解如何规避这个困境。


useState + useEffect:渐入佳境

在上一步骤中,我们在 App 组件中定义了一个 State 和 Effect,但是实际应用不可能这么简单,一般都需要多个 State 和 Effect,这时候又该怎么去理解和使用呢?

1.深入 useState 的本质

在上一节的动画中,我们看到每一次渲染组件时,我们都能通过一个神奇的钩子把状态”钩“过来,不过这些钩子从何而来我们打了一个问号。现在,是时候解开谜团了。

注意

以下动画演示并不完全对应 React Hooks 的源码实现,但是它能很好地帮助你理解其工作原理。当然,也能帮助你去啃真正的源码。

我们先来看看当组件初次渲染(挂载)时,情况到底是什么样的:

image

注意以下要点:

  1. 在初次渲染时,我们通过 useState 定义了多个状态;
  2. 每调用一次 useState ,都会在组件之外生成一条 Hook 记录,同时包括状态值(用 useState 给定的初始值初始化)和修改状态的 Setter 函数;
  3. 多次调用 useState 生成的 Hook 记录形成了一条链表
  4. 触发 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,请看下面的演示:

image

注意其中一些细节:

  1. useStateuseEffect 在每次调用时都被添加到 Hook 链表中;
  2. useEffect 还会额外地在一个队列中添加一个等待执行的 Effect 函数;
  3. 在渲染完成后,依次调用 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中最多会同时存在两棵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属性下。当我们函数组件执行之后,hooksworkInProgress将是如图的关系:

图片

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<any, any> | null, // usestate和useReducer中 保存最新的更新队列。
  queue: UpdateQueue<any, any> | 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存储(链表)结构:

image
  • 组件初始化时,会按顺序从上到下将组件中使用到的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的调度逻辑

调度逻辑如下:

image.png

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为例,具体执行过程如下

image.png

接下来将以useState和useEffect在mount 阶段和update阶段源码为例进一步分析Hooks内部执行过程:

3. mount 阶段:mountState

首先我们需要知道,在组件里,多次调用useState,或者其他hook,那react怎么知道我们当前是哪一个hook呢。其实在react内部,所有的hook api第一次被调用的时候都会先创建一个hook对象,来保存相应的hook信息。然后,这个hook对象,会被加到一个链表上,这样我们每次渲染的时候,只要从这个链表上面依次的去取hook对象,就知道了当前是哪一个hook了。


image.png

下面我们就看一下这个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<S>(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属性上:

image.png

初始化完成后,怎样对state值进行更新的呢?实际上就是通过dispatchAction方法进行更新的,如下:

// currentlyRenderingFiber$1是一个全局变量,表示当前正在渲染的FiberNode
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);

dispatchAction逻辑如下:

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
 
   /** 省略Fiber调度相关代码 **/
 
  // 创建新的新的update, action就是我们setCount里面的值(count+1, count+2, count+3…)
    const update: Update<S, A> = {
      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是一个环形链表,规则:

    1. queue.last指向最近一次更新
    2. last.next指向第一次更新
    3. 后面就依次类推,最终倒数第二次更新指向last,形成一个环形链表,如下图。
image.png

所以每次插入新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源码的流程图。


image.png

至此,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<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}
function updateReducer<S, I, A>(
  reducer: (S, A) => S, // 对于useState来说就是basicStateReducer
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  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<A> = (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 <div>
       <div>{ number }</div>
       <button onClick={ ()=> handerClick() } >点击</button>
   </div>
}

点击按钮, 打印 3

三次setNumber产生的update会暂且放入pending queue,在下一次函数组件执行时候,三次 update被合并到 baseQueue。结构如下图:

图片

setState.jpg

接下来会把当前useState或是useReduer对应的hooks上的baseStatebaseQueue更新到最新的状态。会循环baseQueueupdate,复制一份update,更新 expirationTime,对于有足够优先级的update(上述三个setNumber产生的update都具有足够的优先级),我们要获取最新的state状态。,会一次执行useState上的每一个action。得到最新的state

更新state

图片

sset1.jpg

这里有会有两个疑问🤔️:

  • 问题一:这里不是执行最后一个action不就可以了嘛?

答案: 原因很简单,上面说了 useState逻辑和useReducer差不多。如果第一个参数是一个函数,会引用上一次 update产生的 state, 所以需要循环调用,每一个updatereducer,如果setNumber(2)是这种情况,那么只用更新值,如果是setNumber(state=>state+1),那么传入上一次的 state 得到最新state

  • 问题二:什么情况下会有优先级不足的情况(updateExpirationTime < renderExpirationTime)?

答案: 这种情况,一般会发生在,当我们调用setNumber时候,调用scheduleUpdateOnFiber渲染当前组件时,又产生了一次新的更新,所以把最终执行reducer更新state任务交给下一次更新。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容