[React Hooks长文总结系列一]初出茅庐,状态与副作用
写在开头
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特性,每次的渲染都是独立的,每次渲染的状态其实都只是常量罢了。
深入本质
让我们看深入一下本质,看看 useState 和 re-render 到底如何关联起来:
- 函数式组件初次渲染,一个个的
useState依次执行,生成hooks链表,里面记录了每个state的初始值和对应的setter函数 - 这个链表会挂在这个函数式组件的外面,可以被
useState或相应setter访问 - 当某个时刻调用了
setSetter,将会直接改变这个hooks链表 - hooks链表其实就是这个函数式组件的状态表,它的改变等效于状态改变,会引起函数式组件重渲染
- 这个函数式组件重渲染,执行到
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对象保存在函数式组件外部,这样的好处在于:
- 独立于
capture value之外存储,不用担心获得过时变量的问题; - 可以同步修改状态。
我们试验如下:
const numRef = useRef(0);
const increaseNum = () => {
numRef.current += 1;
console.log(numRef.current); // 能获取最新值
}
但是要注意️:由于引用没变,上述操作并不会引起函数式组件的重渲染。 这是一个很容易引起错误的地方!
useEffect,生命周期与观察者
用法及建议
useEffect 的模型十分之简洁,如下:
useEffect(effectFn, deps);
useEffect 可以模拟旧时代的三个生命周期:componentDidMount、shouldComponentUpdate、componentWillUnmount,相当于三个生命周期合并为一个 api。
所谓shouldComponentUpdate,其实就是去除deps依赖数组,如此一来这个副作用的 effectFn 会在首次渲染之后和每次重渲染之后执行,相当于模拟了 shouldComponentUpdate 这一生命周期,如下:
useEffect(() => {
// xxx
});
而所谓componentDidMount,则是传入一个空数组作为依赖,因为当有 deps 数组时,里面 effectFn 是否执行取决于 deps 数组内的数据是否变化,空数组內无数据,所以对比自然也就无变化,使用如下:
useEffect(() => {
// xxx
}, []);
而componentWillUnmount,则是在effectFn中返回一个清除函数,如下:
useEffect(() => {
// 执行副作用
// ...
return () => {
// 清除上面的副作用
// ...
};
}, []);
此外我们应该始终遵循一个原则:那就是不要对 deps 依赖撒谎。否则会引发一系列 bug。当然编辑器的 linter 也不会允许我们这样做,这一点非常关键。
原理
effectFn 就是当依赖变化时执行的副作用函数,这里的副作用,并不是一个贬义词,而是一个中性词。
函数内部与外部发生的任何交互都算副作用,比如打印个日志、开启一个定时器,发一个请求,读取全局变量等等等等。
好,现在这个 effectFn 可以返回一个清理函数cleanUp,用于清除这个副作用。典型的清理函数,如:clearInterval、clearTimeout,如:
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长文总结系列一]初出茅庐,状态与副作用的更多相关文章
- React Hooks 深入系列 —— 设计模式
本文是 React Hooks 深入系列的后续.此篇详细介绍了 Hooks 相对 class 的优势所在, 并介绍了相关 api 的设计思想, 同时对 Hooks 如何对齐 class 的生命周期钩子 ...
- React Hooks 深入系列
本文基于近段时间对 hooks 碎片化的理解作一次简单梳理, 个人博客.同时欢迎关注基于 hooks 构建的 UI 组件库 -- snake-design. 在 class 已经融入 React 生态 ...
- 如何利用 React Hooks 管理全局状态
如何利用 React Hooks 管理全局状态 本文写于 2020 年 1 月 6 日 React 社区最火的全局状态管理库必定是 Redux,但是 Redux 本身就是为了大型管理数据而妥协设计的- ...
- react新特性 react hooks
本文介绍的是react新特性react hooks,本文面向的是有一定react开发经验的小伙伴,如果你对react还不是很熟悉的话我建议你先学习react并多多联系. 首先我们都知道react有3种 ...
- 30分钟精通React今年最劲爆的新特性——React Hooks
你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗? --拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function. 你还在为搞不清使用哪个生命周 ...
- 初探React Hooks & SSR改造
Hooks React v16.8 发布了 Hooks,其主要是解决跨组件.组件复用的状态管理问题. 在 class 中组件的状态封装在对象中,然后通过单向数据流来组织组件间的状态交互.这种模式下,跨 ...
- React hooks实践
前言 最近要对旧的项目进行重构,统一使用全新的react技术栈.同时,我们也决定尝试使用React hooks来进行开发,但是,由于React hooks崇尚的是使用(也只能使用)function c ...
- React Hooks新特性学习随笔
React Hooks 是 React 16.8 的新增特性.它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性. 前言 本篇主要以讲几个常用的api为主. 1.u ...
- 关于React Hooks,你不得不知的事
React Hooks是React 16.8发布以来最吸引人的特性之一.在开始介绍React Hooks之前,让咱们先来理解一下什么是hooks.wikipedia是这样给hook下定义的: In c ...
随机推荐
- React Hooks: useMemo All In One
React Hooks: useMemo All In One useMemo https://reactjs.org/docs/hooks-reference.html#usememo refs x ...
- js data type checker
js data type checker js 数据类型检测 "use strict"; /** * * @author xgqfrms * @license MIT * @cop ...
- WebXR All in One
WebXR All in One VR / WebVR WebXR https://www.w3.org/TR/webxr/ WebXR Device API https://immersiveweb ...
- Java 动态调试技术原理及实践
本文转载自Java 动态调试技术原理及实践 导语 断点调试是我们最常使用的调试手段,它可以获取到方法执行过程中的变量信息,并可以观察到方法的执行路径.但断点调试会在断点位置停顿,使得整个应用停止响应. ...
- www.yimitv.cc免费观看2020最新电影、电视剧、综艺栏目
神奇的微信公众号 '德佑小站', 可以看最新上映电影.看小说.看直播!重要的是免费,csdn已加速. 壹米影视:www.yimitv.cc 德佑小说:www.deyouxs.cc
- 【原创】Linux虚拟化KVM-Qemu分析(十)之virtio驱动
背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: KVM版本:5.9 ...
- 6. vue组件详解(一)
主要内容: 1. 组件的基本使用 2. 全局组件和局部组件 3. 父组件和子组件 4. 组件语法糖的写法 5. 组件data关联的写法 6. 父子组件的通信 组件系统是 Vue 的一个重要概念,因为它 ...
- Java Socket编程基础及深入讲解
原文链接:https://www.cnblogs.com/yiwangzhibujian/p/7107785.html 原文写的特别好,非常详细,但是禁止转载,那我就不再复制粘贴了! socket实现 ...
- Docker 一键安装及Docker管理面板Portainer中文汉化
前言 Docker接触了一段时间了,批量操作过程中感觉太繁琐,所以找到了好评率比较高的Portainer面板,使用后感觉的确不错所以准备拿出来精力来做个汉化版,过程中发现词条非常多,所以暂时先汉 ...
- JavaScript疑难点
什么是闭包 我个人理解闭包就是函数中嵌套函数,但是嵌套的那个函数必须是返回值,才构成闭包: //标准的闭包 function fn(){ var i=1; return function fnn(){ ...