React 原理&底层逻辑&源码探析(3)

215 阅读9分钟

React 原理&底层逻辑&源码探析

深入 React-Hooks 工作机制

🍇 **React-Hooks 使用原则**

只在 React 函数组件中调用 Hook 不要在循环、条件或嵌套函数中调用 Hook

强调这个原则的目的是要确保 Hooks 在每次渲染时都保持同样的执行顺序

从现象看问题:若不保证 Hooks 执行顺序,会带来什么麻烦

下面是符合原则的代码

import React, { useState } from 'react';

function PersonalInfoComponent() {
    const [name, setName] = useState('龙宁');
    const [age, setAge] = useState(99);
    const [career, setCareer] = useState("我是一个前端,爱吃小熊饼干");

    console.log("career", career);

    return (
        <div className='personalInfo'>
            <p>姓名:{name}</p>
            <p>年龄:{age}</p>
            <p>职业:{career}</p>
            <button onClick={() => setAge(100)}>
                修改年龄
            </button>
        </div>
    )
}

export default PersonalInfoComponent;

点击修改年龄后,正常执行

将代码进行改动后如下,函数组件只有在第一次执行时后调用三次 Hook,之后都只会调用一次

import React, { useState } from 'react';

let isMounted = false;

function PersonalInfoComponent() {
    let name, age, setName, setAge;

    console.log("isMounted", isMounted);

    if (!isMounted) {
        [name, setName] = useState('龙宁');
        [age, setAge] = useState(99);

        isMounted = true;
    }

    const [career, setCareer] = useState("我是一个前端,爱吃小熊饼干");

    console.log("career", career);

    return (
        <div className='personalInfo'>
            <p>姓名:{name}</p>
            <p>年龄:{age}</p>
            <p>职业:{career}</p>
            <button onClick={() => setName('龙萧')}>
                修改年龄
            </button>
        </div>
    )
}
export default PersonalInfoComponent;
🍐 *只有将相关代码的eslint校验给禁用掉,才能够避免校验性质的报错,从而更直视地看到错误的效果到底是什么样的,进而理解错误的原因。*

该组件能够正常渲染出 view,但是当点击修改年龄后,却报错,原因:组件渲染的 Hook 比预期的少

按道理来说,既然渲染没有问题,那么 React 就没有理由阻止渲染行为呀

那么可以打开控制台看看 console.log 的内容,发现第二次渲染的 career 的值为 “龙宁“

从源码调用流程看原理:Hooks 的正常运作,在底层依赖于顺序链表

  • 以 useState 为例,分析 React-Hooks 的调用链路

useState 首次渲染的流程与更新渲染的流程是不同的,下面是首次渲染的流程

Untitled 14.png

看来这个流程的重点是 mountState()

// mountState 函数源码的简化版本
function mountState(initialState) {
  const hook = mountWorkInProgressHook();
  if (isReRender) {
    const queue = hook.queue;
    let next = queue.lastRenderedState;
    do {
      next = queue.dispatch(queue.lastRenderedState, action);
    } while (next !== queue.lastRenderedState);
    hook.memoizedState = queue.lastRenderedState;
  } else {
    hook.memoizedState = hook.baseState = typeof initialState === 'function'
      ? initialState() // 如果初始值是函数则记录其返回值
      : initialState;
  }
  return [hook.memoizedState, dispatchAction.bind(null, queue)];
}

整个代码最需要关注的是 mountWorkInProgressHook,它为我们道出了 Hooks 背后的数据结构组织形式

在 React 的 Hooks 中,mountWorkInProgressHook 函数是一个内部函数,主要用于创建 Hook 对象,并将其添加到组件的 Hooks 链表中。它的作用是为每个组件实例化一个 Hooks 链表,并在链表中添加新的 Hook 对象。

下面是 mountWorkInProgressHook 函数的简化版本:

function mountWorkInProgressHook() {
  const hook = {
    memoizedState: null,
    baseState: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    // 处理首次渲染的场景
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // 处理更新状态的场景
    workInProgressHook = workInProgressHook.next = hook;
  }

  return workInProgressHook;
}

可以看到,mountWorkInProgressHook 函数主要包含了两个分支逻辑,分别对应首次渲染和更新状态的场景。在首次渲染时,它会为当前正在渲染的组件实例化一个 Hooks 链表,并将第一个 Hook 对象作为链表的头节点。在更新状态时,它会在 Hooks 链表中添加一个新的 Hook 对象。

在创建 Hook 对象时,mountWorkInProgressHook 函数会为其设置一些常用的属性,例如 memoizedState、baseState、queue 和 next 等。其中,memoizedState 属性用于存储 Hook 的状态值,baseState 属性用于存储状态的初始值,queue 属性用于存储 Hook 的更新队列,next 属性用于存储 Hooks 链表中的下一个 Hook 对象。

下面是更新渲染的流程

虽然 updateState 之后涉及的代码有很多,但其实做的事情很容易理解,就是按顺序去遍历之前构建好链表,取出对应的数据信息进行渲染

Untitled 15.png

mountState (首次渲染)构建链表并渲染

updateState 依次遍历链表并渲染

🍐 hooks 的渲染是通过“依次遍历”来定位每个hooks 内容的。如果前后两次读到的链表在顺序上出现差异,那么渲染的结果自然是不可控的。

Hooks 的本质是链表

这也就能解释前面的现象了,career 本是第三个 state 的,但是在第二次渲染中,只执行了一次 useState,那么沿着 state 链表就只能拿到第一个本是 name 的 state

真正理解虚拟 DOM:React 选它真的是为了性能吗?

研发模式不断演进的背后,恰恰蕴含着前端人对”DOM 操作“这一核心动作的持续思考和改进

在 MVVM 架构模式类问题下有一个很经典的问题:

  • 为什么我们需要虚拟 DOM?

常见的回答思路是:

DOM 操作是很慢的,而 JS 却可以很快,直接操作 DOM 可能会导致频繁的回流与重绘,JS 不存在这些问题。因此虚拟 DOM 比原生 DOM 更快

但真的是这样吗

快速搞定虚拟 DOM 的两个大问题

虚拟 DOM(virtual DOM)

本质上是 JS 和 DOM 之间的一个映射缓存,在形态上表现为一个能够描述 DOM 结构及其属性信息的 JS 对象

首先需要大致明白 React 中虚拟 DOM 是如何工作的

1️⃣ **挂载阶段**

React 将结合 JSX 的描述,构建出虚拟DOM树,然后通 ReactDOM.render实现虚拟 DOM 到真实 DOM 的映射(触发渲染流水线)

2️⃣ **更新阶段**

页面的变化会先作用于虚拟 DOM,虚拟 DOM 将在 JS 层借助算法先对比出具体有哪些真实 DOM 需要被改变,然后再将这些改变作用于真实 DOM

“为什么需要虚拟 DOM?” “虚拟 DOM 是否伴随更好的性能?” “虚拟 DOM 的优势何在?”

🍐 不要点对点地去看待问题本身,必须将其放在一个足够长的、合理的上下文中去讨论

历史长河中的 DOM 操作解决方案

  • 原生 JS 支配下的 ”人肉 DOM“ 时期

前端页面“展示”的属性远远强于其“交互”的属性,这就导致 JS 的定位只能是“辅助”

前端工程师们会花费大量的时间去实现静态的 DOM,待一切结束后,再补充少量 JS

早期的 Web 开发中,开发者们通常直接使用原始的 DOM 操作方法,例如document.getElementById()、element.setAttribute() 等,来实现页面的动态交互和数据更新。然而,这种方法往往需要大量的代码量和复杂的逻辑,而且容易出现性能问题,导致页面响应缓慢和卡顿

  • 解放生产力的先导阶段:jQuery 时期

大量DOM操作需求带来的前端开发工作量的激增

jQuery 首先解决的就是“API 不好使”这个问题,将 DOM API 封装为了相对简单和优雅的形式,同时一口气做掉了跨浏览器的兼容工作,并且提供了链式 API 调用、插件扩展等一系列能力用于进一步解放生产力

jQuery 提供了一种简洁的语法和丰富的功能,可以帮助开发者快速实现页面的交互和动态效果。jQuery 的出现大大简化了 DOM 操作的代码量,提高了开发效率,但是它仍然需要开发者手动管理 DOM 元素和事件绑定等问题,容易出现性能问题。

  • 民智初启:早期模板引擎方案

jQuery 并不能从根本上解决 DOM 操作量过大情况下前端侧的压力。

也就是说对于 DOM 操作而言,jQuery 依然是命令式的,而想要减轻编程心智负担,声明式才是最需要的。

🍐 模板引擎更倾向于点对点解决繁琐 DOM 操作的问题,它在能力和定位上既不能够、也不打算替换掉 jQuery ,两者是和谐共存的。因此这里不存在“模板引擎时期”,只有“模板引擎方案

模板引擎的工作流程一般如下:

  1. 读取 HTML 模板并解析它,分离出其中的 JS 信息
  2. 将解析出的内容拼接成字符串,动态生成 JS 代码
  3. 运行动态生成的 JS 代码,吐出“目标HTML”
  4. 将“目标HTML”赋值给 innerHTML,触发渲染流水线,完成真实 DOM 的渲染

使用模板引擎方案来渲染数据需要关注的仅仅是数据和数据变化本身

模板引擎实际的应用场景,基本局限在“实现高效的字符串拼接”这一个点上,因此不能指望它去做太复杂的事情,它在性能上的表现并不尽如人意。

🍐 本课时所讨论的“模板引擎”概念,指的是虚拟D○M思想推而广之以前,相对原始的一类模板引擎

虚拟 DOM 是如何解决问题的

🍐 真实历史中的虚拟DOM创作过程,到底有没有向模板引擎去学习,这个暂时无从考证。但是按照前端发展的过程来看,模板/擎和虚拟DOM确实在思想上存在递进关系

模板引擎:

Untitled 16.png

虚拟 DOM:

Untitled 17.png

虚拟 DOM 解决问题并不总是需要模板,比如 React 就使用的是 JSX

Untitled 18.png

虚拟 DOM 和 Redux 一样,不依附于任何具体的框架

所以学习到这里了,你会发现使用虚拟 DOM 其实是前端开发们为了只求更好的研发体验和研发效率而创造出来的高阶产物

React 选择虚拟 DOM,并非其带来了多高的性能提升,而是其能够在提供更爽、更高效的研发模式的同时仍然保持一个还不错的性能

性能问题属于前端领域复杂度比较高的问题,量化性能的时候要结合各种要素来作分情况的讨论

下面是模板渲染工作流与虚拟 DOM 工作流的对比:

Untitled 19.png

模板渲染工作流的 1 与虚拟 DOM 工作流的 1 和 2 都是 js 范畴的行为,这两者是具有可比性的。

动态生成字符串的性能消耗毕竟有限,而构建虚拟 DOM 和 diff 算法的复杂度却很高,消耗性能必然更多

那么 DOM 层面,模板渲染是全部替换,虚拟 DOM 是差量更新。乍一看好像虚拟 DOM 要更好,但是考虑一下这样的情况,差量更新的量趋近于全部更新时,显然虚拟 DOM 就没有任何优势了

可是只要虚拟 DOM 在更新 DOM 上与模板渲染拉开了一点差距,其实虚拟 DOM 的性能就更好,因为 DOM 操作跟 js 行为完全不是一个量级。

虚拟 DOM 的价值不在性能,而在别处

一是提高了研发体验和研发效率

二是虚拟 DOM 能实现跨平台,虚拟 DOM 是对真实渲染内容的一层抽象,若没有这层抽象,那么视图层将与平台紧密耦合,而这层抽象却可以描述多种真实平台的内容。