React18原理 (二) -- LegacyMode

775 阅读7分钟

前言

为什么需要学习react的原理呢?是为了装(不是)

作为现代主流的前端框架,react改变了我们的开发方式。以往我们直接写js,html,css,并交由浏览器渲染,但这种方式不利于开发大型前端应用,因此我们引入了react

image.png

然而随着工作年限的增长,越来越可能遇到一些因为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) {}

image.png 实际上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节点挂载到对应的位置上

image.png

函数组件与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节点

  1. 用户点击Add 1,这时hook链表中唯一的useState节点的update链表新增一个item,表示最新的值是0 + 1 = 1
  2. 由于这时改动还没有生效,因此jsx中的obj.value仍然是0
  3. Fiber Tree遍历结束,开始渲染,此时渲染出来的值是0
  4. 重新遍历Fiber Tree,当执行useState的时候,会去遍历update链表,并且计算出state最新的值也就是{ value: 1 },因此本次的obj.value取值为1
  5. 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 当用户点击一下按钮会发生什么?

  1. a的useState中新增一个update,表示a要变成1
  2. 开始渲染,此时a和b都还是0
  3. 开始新的一轮Fiber Tree更新,此时执行a的useState,计算a的最新值是1
  4. 执行b的useState,计算b的最新值是0
  5. 执行useEffect,由于a发生了变化(对于依赖变化,react使用的是object.is来判断依赖是否变化),因此执行setB,往b的useState里面新增一个update,表示b要变成0 - 1 = -1 6.开始渲染,此时a是1,b还是0
  6. 开始第二轮Fiber Tree更新,此时执行a的useState,a仍然是1
  7. 执行b的useState,根据update把b变成-1
  8. 由于a没有变化,不执行useEffect
  9. 开始渲染,a = 1, b = -1

至于其他hook,感兴趣的朋友可以关注我接下来的更新

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿