前言
为什么需要学习react的原理呢?是为了装(不是)
作为现代主流的前端框架,react改变了我们的开发方式。以往我们直接写js,html,css,并交由浏览器渲染,但这种方式不利于开发大型前端应用,因此我们引入了react
然而随着工作年限的增长,越来越可能遇到一些因为react机制造成的bug。这时如果非常了解react的原理,能帮助我们解决问题以及验证解决方案的准确性
原理类的知识并不总能在工作中用到,但是一旦用到,很有可能帮助我们解决一些匪夷所思的bug,属于是未雨绸缪
LegacyMode简介
react有两种运行模式
- LegacyMode: 也就是同步更新模式,在该模式下,所有的更新具有同一优先级,react会把一帧中全部数据变动都计算完后,操作FiberTree,然后再让浏览器渲染,但如果遇到大批量的数据变更,可能会出现卡顿的现象
- ConcurrentMode: 是react18新推出的异步模式,在该模式下,不同的更新有着不同的优先级,react会优先执行高优先级的变更并渲染到页面上。并且根据最新的文档,react甚至会让高优先级的更新打断低优先级的更新
React更新机制
小知识: 浏览器在执行js的时候,是不会进行渲染的,不信的话可以在f12里面执行下面的代码,这个p节点不会被渲染出来
var p = document.createElement('p');
p.textContent = '啦啦啦';
document.body.appendChild(p);
console.log(document.body.lastChild);
while (true) {}
实际上react的更新机制就是一个简单的无限循环,每次渲染的间隔都会去深度优先全量遍历Fiber Tree的每一个节点,并把Fiber Tree的改动映射到dom上,当对dom的修改完成之后,放开浏览器(停止执行js),浏览器会开始渲染。渲染结束后,又会重新再一次全量遍历Fiber Tree
FiberTree
Fiber Tree是react内部的维护的一棵三指针树,是对dom树的另一种抽象
为什么不用shadow dom?因为shadow dom只能描述html标签之间的关系,但react中的节点不止html标签,还有函数组件、类组件、懒加载组件等层级,这些shadow dom无法描述
要注意,每个html标签都是一个独立的fiber节点。对于html标签类的节点,react会解析jsx获取到对应的标签(比如p标签,div标签等),然后解析jsx中注入的属性,然后调用原生的dom api把html节点挂载到对应的位置上
函数组件与hook
那么当Fiber Tree遍历到函数组件的时候会发生什么呢?那么就涉及一个问题:函数组件到底是什么
很简单,函数组件是一个封装了内部逻辑和数据,并且返回jsx的函数。返回的jsx就是按照html节点来解析,因此就不再赘述了,那么重点来看看函数组件内部的数据和逻辑是怎么运作的。
函数组件里有两个最重要的hook,分别是useState以及useEffect,这两个hook是我们开发应用的基础。
hook
hook的原理很简单,在每个函数组件类的fiber节点里,都维护了一个hook链表,这个链表中每个item都对应着我们在函数组件里面调用的hook,并且链表中的顺序与我们在函数组件中调用hook的顺序是一致的,链表中也会记录每个hook的数据和状态。
由于es6的export是引用,实际上react会在运行时根据组件的状态替换每个hook的实现,每个hook的实现都分三种,以useState为例:mountState(挂载时)、rerenderState(重渲染时)、updateState(数据更新时)。
在Fiber Tree遍历到函数组件时,每当执行到hook的地方,react会结合hook链表中的数据,以及组件对应状态下hook的逻辑,来实现hook的功能
具体可以参考我的这篇文章 React18 原理 (一) -- 深入浅出hook
useState
const [state, setState] = useState()
react有两个特点:单向数据流以及数据驱动视图,这就说明了数据是react中组件变化的源头,那么数据是如何变化的?我们来详细看看useState这个hook
useState是函数组件和hook的基础,这个hook重新定义了声明变量的方式。这个hook返回的第一个参数是一个state,第二个参数则是修改state的函数。
众所周知对state的修改不是即时的,是因为hook链表中,全部useState类的节点都还保存了一个当前hook的update链表,setState这个函数只是把修改值的动作保存到了update链表里,以我下面的这个组件为例,下面这个组件每点击一次Add 1这个div,obj.value就会加一,我们来看看这个过程是如何实现的
import { useState } from 'react';
export function Sub() {
const [obj, setObj] = useState({ value: 0 });
return (
<>
<p>{obj.value}</p>
<div onClick={() => {
setObj({ value: obj.value + 1 });
}}>Add 1</div>
</>
);
}
由于只用到了一个钩子,因此在该函数组件的fiber节点中,hook链表只有一个节点,这个节点就对应着唯一的这个useState节点
- 用户点击Add 1,这时hook链表中唯一的useState节点的update链表新增一个item,表示最新的值是0 + 1 = 1
- 由于这时改动还没有生效,因此jsx中的obj.value仍然是0
- Fiber Tree遍历结束,开始渲染,此时渲染出来的值是0
- 重新遍历Fiber Tree,当执行useState的时候,会去遍历update链表,并且计算出state最新的值也就是{ value: 1 },因此本次的obj.value取值为1
- Fiber Tree遍历结束,开始渲染,此时渲染出来的值是1
由此我们可以看到,每次调用setState函数来设置最新的state,其实都要等到下一次Fiber Tree遍历到当前函数组件时,useState才会去遍历之前的全部state,并且得到最新的state
useEffect
相比之下,useEffect则没有太多值得说的地方,其实大家可以思考一下,什么情况下我们会使用useEffect?其实useEffect的使用场景可以总结为:数据联动
我们用到useEffect最多的场景,就是一个state变化了,于是我们调用另一个setState去更新另一个state,比如下面这个例子
import { useState, useEffect } from 'react';
export function Sub() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
useEffect(() => {
setB(b - a);
}, [a]);
return (
<>
<p>{a}</p>
<p>{b}</p>
<div onClick={() => {
setA(a + 1);
}}>Add 1</div>
</>
);
}
由于这里用到了三次hook,因此hook链表中会有三个item:a的useState、b的useState、useEffect 当用户点击一下按钮会发生什么?
- a的useState中新增一个update,表示a要变成1
- 开始渲染,此时a和b都还是0
- 开始新的一轮Fiber Tree更新,此时执行a的useState,计算a的最新值是1
- 执行b的useState,计算b的最新值是0
- 执行useEffect,由于a发生了变化(对于依赖变化,react使用的是object.is来判断依赖是否变化),因此执行setB,往b的useState里面新增一个update,表示b要变成0 - 1 = -1 6.开始渲染,此时a是1,b还是0
- 开始第二轮Fiber Tree更新,此时执行a的useState,a仍然是1
- 执行b的useState,根据update把b变成-1
- 由于a没有变化,不执行useEffect
- 开始渲染,a = 1, b = -1
至于其他hook,感兴趣的朋友可以关注我接下来的更新
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。