写在开头

React Hooks在我的上一个项目中得到了充分的使用,对于这个项目来说,我们跳过传统的类组件直接过渡到函数组件,确实是一个不小的挑战。在项目开发过程中也发现项目中的其他小伙伴(包括我自己)有时候会存在使用不当的情况,因此对官方的几个钩子函数做一个较为全面的总结。

函数式组件出现的原因

为什么会出现函数式组件,因为传统的类组件确实有不少缺点:

  • 类组件中的 this 指向有点绕
  • 通过选项去组织代码,在组件比较大的时候会很痛苦,因为类组件天生分离,不符合内聚性原则
  • 组件复用不方便,尤其是 mixin,很容易带来数据来源指向不清楚的问题

函数式组件居然“有状态了”

我们知道,在过去,函数式组件被称作“傻瓜组件”,因为它并不具有自身的状态,通常被用来做一些渲染视图的工作,即UI = render(props)。这是一个纯粹的输入输出模型,无任何副作用。但是React Hooks的出现,让函数式组件拥有自身的状态成为了可能。

函数式组件在运行过程中会被调用很多次,假如我们将状态保存在函数体里面,毫无疑问是不可行的。因为函数是一种“用完即销毁”的东西。

这正是是Hooks所做的事情:将一个函数组件的状态保存在函数外面。准确来说,是这个函数组件对应的Hooks链表。当函数式组件需要用到该状态的时候,通过Hooks这一钩子将状态从函数体外部“钩进来”。

函数式组件其实也有“生命周期”

函数式组件的生命周期可以分为以下三部分:

初次渲染(first-render) ---> 重渲染 (re-render) ---> 销毁(destroy

当我们第一次使用函数式组件的时候,会触发初次渲染(first-render);若其 props 改变,就会调用该 render 函数,触发重渲染(re-render)。

每一次的渲染,都是独立的。这正是函数式组件的美妙之处。

那么react如何决定要不要调用 render 函数来更新 UI 视图呢?这取决于data有没有更新。从整个组件树来看,data指的是整个组件的state;从具体到某个功能组件来看,data也可以被认为是props和自身state的结合体。

render 的执行取决于 data 变化,而 data 中的 state 数据是保存在链表中的。

链表的特性是啥?就是每个元素都有一个next指针指向下一个元素,一环扣一环关联起来。所以为什么 hooks 不能用在条件判断/循环/嵌套中,因为这些都不能保证每次渲染时读取 hooks 链表的顺序是完全一致的。尤其对于状态读取来说,读取顺序和初次渲染链表记录的顺序不一致,会直接导致一些 useState 钩子读取到错误的状态值。

useSate,状态保存之处

用法

const [count, setCount] = useState(0);

原理

首先,useState 会生成一个状态和修改状态的函数。这个状态会保存在函数式组件外面,每次重渲染时,这一次渲染都会去外面把这个状态钩回来,读取成常量并写进该次渲染中。

通过调用修改状态的函数,会触发重渲染。到这里我们总结:props 的改变和 setState 的调用,都会触发 re-render

由于每次渲染都是独立的,所以每次渲染都会读到一个独立的状态值,这个状态值,就是通过钩子钩到的 state 并读取到的常量。

这就是所谓的capture value特性,每次的渲染都是独立的,每次渲染的状态其实都只是常量罢了。

深入本质

让我们看深入一下本质,看看 useStatere-render 到底如何关联起来:

  1. 函数式组件初次渲染,一个个的 useState 依次执行,生成hooks链表,里面记录了每个 state 的初始值和对应的 setter 函数
  2. 这个链表会挂在这个函数式组件的外面,可以被 useState 或相应 setter 访问
  3. 当某个时刻调用了 setSetter,将会直接改变这个hooks链表
  4. hooks链表其实就是这个函数式组件的状态表,它的改变等效于状态改变,会引起函数式组件重渲染
  5. 这个函数式组件重渲染,执行到 useState 时,因为初次执行已经挂载过一个 hooks 链表了,这个时候就会直接读取链表的相应值

这也就是为什么叫useState,而不是createState

useRef,DOM访问与外部状态保存

useRef有啥用

useRef主要有两个作用:

  • 用来访问DOM;
  • 用来保存变量到当前函数式组件外部。

访问DOM

我们先来看看前者怎么用吧:

const inputRef = useRef(null);

const handleClick = () => {
inputRef.current?.focus();
} return (
<input ref={inputRef} />
<button onClick={handleClick}>点击</button>
)

这样就可以方便地访问DOM节点。

保存可变值

前面我们提到,useState可以方便地保存状态值,但是由于函数式组件的capture value特性,使得我们并不能以一种比较方便的形式获取到更改后的状态值。

const [num, setNum] = useState(0);

const increaseNum = () => {
    setNum(prev => prev + 1);
    console.log(num); // 打印的仍然是旧值,因为num在这一帧被常量化了
}

useRef将会创建一个ref对象,并把这个ref对象保存在函数式组件外部,这样的好处在于:

  1. 独立于capture value之外存储,不用担心获得过时变量的问题;
  2. 可以同步修改状态。

我们试验如下:

const numRef = useRef(0);

const increaseNum = () => {
    numRef.current += 1;
    console.log(numRef.current); // 能获取最新值
}

但是要注意️:由于引用没变,上述操作并不会引起函数式组件的重渲染。 这是一个很容易引起错误的地方!

useEffect,生命周期与观察者

用法及建议

useEffect 的模型十分之简洁,如下:

useEffect(effectFn, deps);

useEffect 可以模拟旧时代的三个生命周期:componentDidMountshouldComponentUpdatecomponentWillUnmount,相当于三个生命周期合并为一个 api。

所谓shouldComponentUpdate,其实就是去除deps依赖数组,如此一来这个副作用的 effectFn 会在首次渲染之后和每次重渲染之后执行,相当于模拟了 shouldComponentUpdate 这一生命周期,如下:

useEffect(() => {
  // xxx
});

而所谓componentDidMount,则是传入一个空数组作为依赖,因为当有 deps 数组时,里面 effectFn 是否执行取决于 deps 数组内的数据是否变化,空数组內无数据,所以对比自然也就无变化,使用如下:

useEffect(() => {
  // xxx
}, []);

componentWillUnmount,则是在effectFn中返回一个清除函数,如下:

useEffect(() => {
  // 执行副作用
  // ...
  return () => {
    // 清除上面的副作用
    // ...
  };
}, []);

此外我们应该始终遵循一个原则:那就是不要对 deps 依赖撒谎。否则会引发一系列 bug。当然编辑器的 linter 也不会允许我们这样做,这一点非常关键。

原理

effectFn 就是当依赖变化时执行的副作用函数,这里的副作用,并不是一个贬义词,而是一个中性词。

函数内部与外部发生的任何交互都算副作用,比如打印个日志、开启一个定时器,发一个请求,读取全局变量等等等等。

好,现在这个 effectFn 可以返回一个清理函数cleanUp,用于清除这个副作用。典型的清理函数,如:clearIntervalclearTimeout,如:

useEffect(() => {
  const timer = setTimeout(() => console.log("over"), 1000);
  return () => clearTimout(timer);
});

useEffect 其实是每次渲染完成后都会执行,但是 effectFn 是否执行,就要看依赖有没有变化了。执行 useEffect 的时候,会拿这次渲染的依赖跟上次渲染的对应依赖对比,如果没变化,就不执行 effectFn,如果有变化,才执行 effectFn

如果连依赖都没有,那 react 就认为每次都有变化,每次运行 useEffect 必运行 effectFn

useEffect 有典型的三大特点:

  • 会在每次渲染完成后才执行,不会阻塞渲染,从而提高性能
  • 在每次运行 effectFn 之前,要把前一次运行 effectFn 遗留的cleanUp函数执行掉(如果有的话)
  • 在组件销毁时,会把最后一次运行effectFn 遗留的 cleanUp 函数执行掉。

deps 数组里面的各个依赖与上次的依赖是否相同,需要通过Object.is来比较,比如:

Object.is(22, 22); // true

Object.is([], []); // false

这样就会有一个隐患,当 deps 数组里面的子元素为引用类型的时候,每次对比都会是false,从而执行effectFn。因为 Object.is 对比引用类型的时候,比较的是两个指针是否指向堆内存中的同一个地址。

useEffect 的执行机制,是在初次渲染时,执行到 useEffect 就将内部的 effectFn 放到两个地方:一个是 hooks 链表中,另外一个则是EffectList 队列中。在渲染完成后,会依次执行 EffectList 里面的 effectFn 集合。

所以,说白了,要不要 re-render,完全取决于链表里面的东西有没有变化。

细节

不同于 vue 里面有async mounted,在 useEffect 里面的 effectFn,应该始终坚持一个原则:要么不返回,要么返回一个 cleanUp 清除函数。像下面这样写是不行的:

// 错误的用法
useEffect(async () => {
  const response = await fetch("...");
  // ...
});

另外我们很容易发现:我们并不需要把 useState 返回的第二个 Setter 函数作为useEffect 的依赖。实际上,React 内部已经对 Setter 函数做了 Memoization 处理,因此每次渲染拿到的 Setter 函数都是完全一样的,不需要把这个Setter函数放到deps数组里面。

[React Hooks长文总结系列一]初出茅庐,状态与副作用的更多相关文章

  1. React Hooks 深入系列 —— 设计模式

    本文是 React Hooks 深入系列的后续.此篇详细介绍了 Hooks 相对 class 的优势所在, 并介绍了相关 api 的设计思想, 同时对 Hooks 如何对齐 class 的生命周期钩子 ...

  2. React Hooks 深入系列

    本文基于近段时间对 hooks 碎片化的理解作一次简单梳理, 个人博客.同时欢迎关注基于 hooks 构建的 UI 组件库 -- snake-design. 在 class 已经融入 React 生态 ...

  3. 如何利用 React Hooks 管理全局状态

    如何利用 React Hooks 管理全局状态 本文写于 2020 年 1 月 6 日 React 社区最火的全局状态管理库必定是 Redux,但是 Redux 本身就是为了大型管理数据而妥协设计的- ...

  4. react新特性 react hooks

    本文介绍的是react新特性react hooks,本文面向的是有一定react开发经验的小伙伴,如果你对react还不是很熟悉的话我建议你先学习react并多多联系. 首先我们都知道react有3种 ...

  5. 30分钟精通React今年最劲爆的新特性——React Hooks

    你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗? --拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function. 你还在为搞不清使用哪个生命周 ...

  6. 初探React Hooks & SSR改造

    Hooks React v16.8 发布了 Hooks,其主要是解决跨组件.组件复用的状态管理问题. 在 class 中组件的状态封装在对象中,然后通过单向数据流来组织组件间的状态交互.这种模式下,跨 ...

  7. React hooks实践

    前言 最近要对旧的项目进行重构,统一使用全新的react技术栈.同时,我们也决定尝试使用React hooks来进行开发,但是,由于React hooks崇尚的是使用(也只能使用)function c ...

  8. React Hooks新特性学习随笔

    React Hooks 是 React 16.8 的新增特性.它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性. 前言 本篇主要以讲几个常用的api为主. 1.u ...

  9. 关于React Hooks,你不得不知的事

    React Hooks是React 16.8发布以来最吸引人的特性之一.在开始介绍React Hooks之前,让咱们先来理解一下什么是hooks.wikipedia是这样给hook下定义的: In c ...

随机推荐

  1. npm clear folder

    npm clear folder rm -rf rimraf rmrf & clear build / dist folder caches https://www.npmjs.com/pac ...

  2. JavaScript getter and setter All In One

    JavaScript getter and setter All In One getter & setter JavaScript Object Accessors JavaScript A ...

  3. flutter 混合开发

    flutter 混合开发 https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps https://flutter.de ...

  4. ip & 0.0.0.0 & 127.0.0.1 & localhost

    ip & 0.0.0.0 & 127.0.0.1 7 localhost host https://www.howtogeek.com/225487/what-is-the-diffe ...

  5. DENIEL SOIBIM:真正自律的人都在做这些事情!

    生活节奏的加快,使得很多人无法适从.很多人,浑浑噩噩,庸庸碌碌,觉得一天做了很多事,却总是一事无成.还有些人,觉得得过且过也很好,但是到头来,却让自己陷入慌乱之中.本想要自由自在的生活,但是却往往却被 ...

  6. 无情面试官之包含min函数的栈

    0 我是一个无情的面试官. 面人无数,挂人无数. 若想过我的面试,标准只有一个,那就是公司很缺人. 招新人,填旧坑. 1 今天是我的第1001次当面试官,要求却不是千里挑一,而是一击必中. 因为我招聘 ...

  7. git相关问题

    1.git查看远程分支更新到本地 git clone 项目地址,示例如下: git clone https://github.com/zhongyushi-git/vue-test.git 在拉取时, ...

  8. IDEA总结

    1. 什么是idea? idea是Java开发软件 2. IDEA下载 https://www.jetbrains.com/idea/download/download-thanks.html?pla ...

  9. `curl -L` 解决 GitHub 的 raw.githubusercontent.com 无法连接问题

    解决 GitHub 的 raw.githubusercontent.com 无法连接问题 在使用 curl 下载文件时,如果出现以下情况 curl: (7) Failed to connect to ...

  10. js一周时间表

    <div class="datetext"> <img class="dateLeft" src="./images/dateLef ...