前言
最近也是在重新学习 React Hook。发现在工作中好多东西你虽然用了也出现效果了,但是这个过程中都干了些什么却都不知道。这就会导致你在写项目的时候会出现一些奇怪的 bug 而找不到原因。文章主要都是自己在学习过程中的一个笔记。希望能够帮助也在学习中的你。
React 的更新机制
React 使用 Virtual DOM(虚拟 DOM)和 Diff 算法来实现高效的 UI 更新机制。
Virtual DOM 是一种轻量级的、虚拟的 DOM 表示,它是由 React 自己实现的,而不是浏览器提供的。React 通过比较新旧 Virtual DOM 树的差异,来确定需要更新的部分,从而避免了直接操作浏览器 DOM 带来的性能问题。
在更新过程中,React 首先将新的 props 和 state 与旧的 props 和 state 进行比较,如果它们发生了变化,React 会触发组件的重新渲染。在重新渲染组件之后,React 会生成一个新的 Virtual DOM 树,并将其与旧的 Virtual DOM 树进行比较,以确定需要进行哪些 DOM 操作来更新实际的 DOM 树。
React 使用 Diff 算法来比较新旧 Virtual DOM 树之间的差异,并确定哪些部分需要进行更新。Diff 算法是一种高效的算法,它可以快速地找到 Virtual DOM 树之间的差异,并最小化 DOM 操作的数量,从而提高性能。
总之,React 使用 Virtual DOM 和 Diff 算法来实现高效的 UI 更新机制,这使得 React 能够在 Web 应用程序中提供快速、动态的用户界面。
- 额...说白话就是:每次修改后会将
新的东西
和旧的东西
进行比较,哪个地方有差异就更新哪里。
每一次 set 都发生了啥
在 React 函数组件中我们想要更新视图就必须使用 useState
返回的 set 方法才能使视图更新。
import { useState } from "react";
function App() {
const [state, setState] = useState(0);
return (
<>
<h2>state:{state}</h2>
<button onClick={() => setState(2)}>state+1</button>
</>
);
}
在 React 中,函数组件每次更新时都会形成一个新的闭包,这是因为每个函数组件都是一个 JavaScript 函数
,每次调用该函数都会创建一个新的执行上下文,并且该执行上下文将形成一个闭包。可以理解为当你每一次 set
(指 setState 方法调用) 的时候 React 都会调用这个 App 组件函数,并得到一个新的视图。
浅比较
我们都知道 Js 中的数据类型大致分为两大类 基础数据类型
和 引用数据类型
。
在React中当你每 set
一次时 React 会使用浅比较
来检测 props 和 state 是否发生变化,来决定更新。
基础数据类型(值对比)
- 当组件的 props 和 state 是基础数据类型(如字符串、数字、布尔值)时,它就会进行
值对比
。比如上面的列子,最初开始的时候是0
,当你点击了按钮调用set方法
并传入的需要更新的值2
。发挥一下想象力每次set
会形成一个新的闭包,React 会将新的闭包和旧的闭包就行对比,当它发现 state 的值从0 变成了 2
,值改变了。那么 react 就会去更新视图。
import { useState } from "react";
function App() {
const [state, setState] = useState(0);
return (
<>
<h2>state:{state}</h2>
<button onClick={() => setState(state)}>cont+1</button>
{/* <button onClick={() => setState(0)}>cont+1</button> */}
</>
);
}
如果你是这样写那么 React 是不会更新的,因为更新的值还是原来的值
引用数据类型(地址对比)
- 如果是引用数据类型(如对象、数组)或者是更深层次的嵌套对象时,那么 React 就会对比他们的地址,如果地址变动了那么就会更新视图。如果你值不变但是地址变了,它也会更新。
import { useState } from "react";
function App() {
const [list, setList] = useState([1, 2, 3, 5]);
const modifyList = () => {
// 1. 单独修改某一项 (不会更新)
list[0] = 99;
setList(list);
// 2. slice方法返回一个新数组 (更新)
setList(list.slice());
setList([1, 2, 3, 5]);
// 3. splice 改变原数组 (不会更新)
list.splice(1, 1);
setList(list);
// 4. splice 返回删除的数组 (更新)
setList(list.splice(1, 1));
// ...
};
return (
<>
<ul>
{list.map((item) => (
<li>{item}</li>
))}
</ul>
<button onClick={modifyList}>change List</button>
</>
);
}
下面我对 modifyList
中的 set 的方法讲解一下
-
虽然你改变了 list 的第一个元素的值,但是它并没用创建一个新的地址。 在 js 我们将引用数据类型的赋值,传参其实并不是真的赋值,只是将一个值的地址给复制了过去(这也就浅拷贝)。所以在这里的
set 方法相当于你把当前 App 函数中 list 数组地址,传递给新的 App 函数
。React 会发现两个地址都是一样的所以他就不会更新了 -
在第二个方法中,我们调用了
slice 方法
(截取返回一个新数组) 和 set 时传递了一个值跟原来一样的数组
。虽然这两种方法传递的数组中的值都是一样的,但是 React 还是会更新。这是因为你虽然传递的值是一样的 ,可你传递是一个新的数组!!
新数组和原来的数组地址不一样了 ,所以这里会造成渲染。(新跟旧的肯定是不一样)
最后两种方法我就不解释了留着你们自己思考吧,都是一样的道理。总之就是一句话:set 方法 传递的是一个 新的数组/对象
, 而不是原来的,React 就会产生更新。
也有很多文章说用 ...(扩展运算符) 进行浅拷贝数组就可以使 React 更新。
const [list, setList] = useState([1, 2, 3, 5]);
setList([...list]);
这说得也是对的,它相当于把原先的 list 在新的 [](数组)中展开了,就相当于把旧的东西放到一个新的容器里面,只要是新的数组地址就会变,那么就会更新。
这就是 React 中函数组件更新时会进行的浅比较过程
。
最后
其实 react 中更深层的还是 Diff 算法来进行比较的。文章以个人理解的程度上来写的,当然我也自己实际验证过。文章中没有跟大家演示 引用数据类型值不变但是还是更新的效果,我打算放到 useCallback
这个 Hook 文章中讲解,这也是我同事在工作中出现的问题。最后给大家推荐几个 Diff 算法讲解得很好的文章。