React深入学习「持续更新中」

182 阅读33分钟

我正在参加「掘金·启航计划」

文档初探

文档地址

  • React tree React 开始工作的地方,React 的工作就是围绕着 dom 树进行的
  • React 一般会用到两个核心包
    1. React 存放所有的核心功能,不包含任何宿主环境代码,例如浏览器可以称为宿主,它提供了很多的 API,像是操作 dom 的能力,这个是由宿主提供的而不是 js 提供的,这样的好处就是核心包的代码不会有改动,移动端(原生 app)项目中没有 dom,但是可以使用移动设备提供的能力(相机,录音)就会使用 React Native 包
    2. ReactDOM

简单使用

我们通过 script tag 的方式引入 React

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <div id="root"></div>
        <script
            src="https://unpkg.com/react@18/umd/react.development.js"
            crossorigin
        ></script>
        <script
            src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
            crossorigin
        ></script>
        <script>
            console.log("React", React, ReactDOM);
            // 第一个参数: React工作节点,也就是我们说的React工作开始的地方
            // 第二个参数:必须是React元素,(通过React核心包的createElement所生成的元素叫做React元素)
            // render会拿到对应的react工作节点,然后将对应的react元素变成浏览器宿主所能识别的真实dom
            const reactDivElement = React.createElement(
                "div",
                {},
                "hello react"
            ); // 生成的对象可以在任何宿主环境下进行转换
            console.log("reactDivElement", reactDivElement);
            const root = ReactDOM.createRoot(document.getElementById("root"));
            root.render(reactDivElement);
        </script>
    </body>
</html>

JSX 和 babel

  • JSX 是一种表达式,也是 React 的一种标准书写方式,这是一种类似 html 的 JavaScript 语法,它允许我们使用书写 html 的方式来书写 React 元素
  • babel 是做语法转换的,它将你书写的一种语法变成另一种语法,更多的时候我们是用来做语法降级,例如将箭头函数的写法转换成普通函数的写法
  • 在我们使用场景, babel 是用来转换 JSX
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <div id="root"></div>
        <script
            src="https://unpkg.com/react@18/umd/react.development.js"
            crossorigin
        ></script>
        <script
            src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
            crossorigin
        ></script>
        <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
        <script type="text/babel">
            // 添加上type="text/babel",表示babel将接管这块的所有代码的解析
            // 浏览器会通过script的type属性来决定是否解析,如果不写type type默认为text/javascript,此时浏览器以js的方式来解析
            // type=module 浏览器通过esmodule的方式来解析
            // 除了不写type,type=text/javascript和type=module这三种情况下下浏览器会解析,其他的浏览器不会解析

            // babel会监听全局的document.contentLoad【意味着当前页面的所有script标签全部生成完毕】
            // babel直接拿所有的script标签document.getElemnntByTagName
            // 读取script上的属性getAttributes("type")
            // 如果它发现type=text/babel
            // 它就会将script标签包裹的的代码读取,然后用自己的transform方法转换一遍,将转换的代码写入到一个新建的script标签中再把标签插入到head标签中
            // JSX写法
            const reactDivElement = (
                <div>
                    hello jsx
                    <hr />
                    <span>i am child</span>
                </div>
            );
            console.log("reactDivElement", reactDivElement);
            const root = ReactDOM.createRoot(document.getElementById("root"));
            root.render(reactDivElement);
        </script>
    </body>
</html>

react-cli【官方脚手架】

脚手架:在工程学里,脚手架提供了一系列预设,让施工者无需再考虑除了建造以外的其他外部问题,在编程学里,脚手架同样提供了一系列的预设,让开发者无需再考虑除了自身业务代码外的其他外部问题

package.json:帮助我们维护我们对应的项目以及和 npm 进行交互

# npm的附带产物
# 使用npx执行命令时,首先会看第一个参数对应的工具是否安装
# 如果没有安装,npx会通知npm临时安装工具(安装到内存中),当安装好后,npx会再次执行整段命令
npx create-react-app my-app

关于 web vitals

用于性能监控的,国内极少公司会用到,因为别的方式进行监控,例如第三方监控系统或自研的监控系统

const reportWebVitals = (onPerfEntry) => {
    if (onPerfEntry && onPerfEntry instanceof Function) {
        import("web-vitals").then(
            ({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
                getCLS(onPerfEntry);
                getFID(onPerfEntry);
                getFCP(onPerfEntry);
                getLCP(onPerfEntry);
                getTTFB(onPerfEntry);
            }
        );
    }
};

export default reportWebVitals;

组件相关

组件初探

组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。

在 React 中,一个组件大多数情况下为一个函数(React 还有类组件,是 React hooks 不是很流行的时候的产物,React 官方拥抱函数式编程,有意将类组件退出历史舞台)

在 React 中,每一次对组件的使用,都是直接调用对应的组件函数

React 对组件的要求:

  1. 组件名必须是大写,React 认为小写的是原生的 dom 元素,大写的是 React 元素

  2. React 组件必须返回可以渲染的 React Elemnt

    • null
    • React 元素
    • 组件
    • 可以被迭代的对象【数组,set,map。。。】,只要一个对象具备迭代接口,那它就可以被渲染
      • {a:1}对象不能被渲染,因为它不具备迭代接口,对象具备迭代必须要是原型或者自己身上有[Symbol.iterator]知名符号
      • 如果想要对象可以被渲染呢?只需要追加迭代接口就可以了
const obj = {
    a: 1,
    b: 2,
};
obj[Symbol.iterator] = function* () {
    for (let prop in obj) {
        yield [prop, obj[prop]];
    }
};

function App() {
    return obj;
}

export default App;

组件状态

  • component state:组件状态

  • state 在不同语境下又不同的释义

  • 我们读作组件状态,但是我们要知道组件状态表示组件内部的数据

如果我们想要页面跟随数据的变化而变化,我们就需要使用 useState 构建组件状态,当我们调用 useState 返回的结果里的修改数据的函数时,React 内部就会帮我们去重新渲染该组件,这就会有一个问题:每次重新渲染都会将变量重新声明,声明的函数(例如定义的事件函数)是引用类型,重复创建会带来很大的性能问题,React 提供了 useCallback 的 hook 来协助我们缓存引用,但是涉及到 hook 依赖,后面讲到 hooks 时会详细介绍

import { useState } from "react";

export default function Counter() {
    const [count, setCount] = useState(0);
    const increase = () => {
        setCount((prev) => prev + 1);
    };
    const decrease = () => {
        setCount((prev) => prev - 1);
    };
    window.getCount = () => {
        console.log("count:", count);
    };
    return (
        <div>
            <button onClick={decrease}>-</button>
            <span>{count}</span>
            <button onClick={increase}>+</button>
        </div>
    );
}
  1. 组件状态的更新是异步的,当更新状态的函数执行后,我们没有办法同步的马上得到它更新后的值

    • 那么如何获取最新的状态呢?useEffect / useLayouEffect
  2. useState 简单介绍

    • useState 内部的初始化有区分,只要不是在该函数组件内部第一次调用 useState,就不会执行初始化操作
    • useState 在调用的时候可以传递函数,也可以传递具体的值。如果传递的是函数,则会直接执行这个函数,将函数的返回值作为初始的状态。但是不推荐通过调用一个函数将返回值作为参数传递,例如:useState(getInitialState())根据上面可知,每次更新组件状态,组件就会重新渲染,相当于组件函数重新执行,useState 也会执行,但是它不会再进行初始化操作,如果传入的一直函数,则此函数的计算没有意义
    // 伪代码
    function useState(initValue) {
        let state;
        if (isInit) {
            state = initValue;
        }
        const dispatch = (newState) => {
            state = newState;
            // 重新渲染
            render();
        };
        return [state, dispatch];
    }
    
    • useState 会返回一个数组,数组里面有两个成员

      • 以初始化为值的变量
      • 修改该变量的函数,这个函数的调用会让组件重新渲染。调用该函数的时候可以传递值,也可以传递函数,如果传递的函数,React 会将上一次的值传给你的函数,函数需要返回一个值
    • 状态的更新是批量进行的,而不是一个一个进行的,这是为了性能考虑,多次变更状态,只会重新渲染一次

组件属性

  • undefined,null,false 是不会再页面中渲染任何东西的
// App.jsx
import Counter from "./components/Counter";
import { useState } from "react";

function App() {
    const [countValue, setCountValue] = useState(10);
    const handleClick = () => {
        setCountValue((prev) => prev + 1);
    };
    return (
        <div>
            <Counter defaultValue={countValue} />
            <button onClick={handleClick}>click me</button>
        </div>
    );
}

export default App;
// Counter.jsx
import { useState } from "react";

export default function Counter(props) {
    const [count, setCount] = useState(props.defaultValue);
    console.log(count);
    const increase = () => {
        setCount((prev) => prev + 1);
    };
    const decrease = () => {
        setCount((prev) => prev - 1);
    };
    window.getCount = () => {
        console.log("count:", count);
    };
    console.log(props);
    return (
        <div>
            <button onClick={decrease}>-</button>
            <span>{count}</span>
            <button onClick={increase}>+</button>
            <span>{props.defaultValue}</span>
        </div>
    );
}

组件事件

  • 和原生事件行为相差不大,基本上原生事件能干的事,React 都复刻了一遍
  • 标签属性会被 React 自行处理
  • 组件属性需要自行处理
// App.jsx
// 会被React转成xxx.addEventListener("click", handleClick)的形式
<button onClick={handleClick}>click me</button>

// onClick={handleClick}给Counter组件传递了onClick属性
<Counter onClick={handleClick} defaultValue={countValue} />
<div
    onClick={props.onClick}
    style={{ width: "100px", height: "30px", backgroundColor: "skyblue" }}
></div>

事件机制

  • 冒泡:一个事件从子元素依次向父元素传递,这个叫做冒泡,如果阻止冒泡,则事件不会向该元素的父级传递

  • 在实际的项目开发中,可能会为元素绑定成百上千的事件,如果把事件都绑定到确定的元素上,事件的回调函数就会占用很多内存,造成性能问题,且无法动态监听元素事件

  • 为解决这些问题 React 采用了事件委托机制(通俗的讲就是把绑定到确定元素的事件放到它的祖先元素上,让祖先元素来委托处理),event.target 指向触发事件的确定元素

  • React 把事件绑定在对应的 root 元素上,当某个真实 dom 触发事件以后,dom 事件会因为冒泡触达到 root 元素上,root 元素对应的事件处理函数又可以通过 event.target 知道真正触发事件的元素是谁

  • JSX 转换成的真实 dom 身上不会绑定任何的真实事件,React 会把 JSX 上和事件相关的标签属性收集起来做一个映射,最终在页面上的真实 dom 被点击时,事件就会一直冒泡(事件冒泡不需要绑定事件也会出现),最终到达 root,root 根据 event.target 去收集到的映射里找出匹配的事件函数,然后去执行函数

  • 所以当我们给 app 设置禁止冒泡,冒泡无法触达到 root,就会导致 app 的子元素的事件不会触发

  • React17 版本以前存在事件池机制

    • React 里的标签属性事件的 event 是 React 生成的(复刻原生)
    • React17 以前为了更好的性能考虑尝试了复用事件
    • 缓存事件函数的引用,事件执行完后,将函数里面的值置为空,再给下一次事件使用(重新赋值)
    • 这会导致异步的情况下无法获取到事件源(e.target)
    • 基于 React 事件池机制,当项目使用的 React 版本是 17 以前的,你代码访问到 event.xxx 是 null 时,你就要注意到是不是在异步环境下访问了事件源对象的属性
    • 如果非要在 React17 以下版本,异步访问 evnet,也有办法,你可以调用 e.persist(),取消事件池的重用
// index.js

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

// React.StrictMode React严格模式
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
    <div className="app">
        <App />
    </div>
);
// 因为render是异步的,所以无法获取到对应app类名的真实dom
console.log("app dom", document.getElementsByClassName("app")[0]);

requestIdleCallback(() => {
    console.log("app dom", document.getElementsByClassName("app")[0]);
    const app = document.getElementsByClassName("app")[0];
    app.addEventListener("click", (e) => {
        e.stopPropagation();
    });
});

受控元素和非受控元素

React 中,受控和非受控我们只在表单元素中去谈

  • 在表单元素中,判断受控和非受控的标准是什么?
    • 受标签属性的植入,标签受控属性可以理解为一个标记,出现这个标记表示该组件为受控组件
    • input 的输入框类型 的受控属性是 value
      • 当在 input 设置上 value 的值后,input 框里面出现什么值,不在由用户说了算,而是由这个 value 值决定
    • input 的 checkbox 类型和 radio 类型的受控属性是 checked
  • 在实际开发中,我们可能会使用第三方 UI 库,例如 antd。antd 对应的所有表单组件都集成了原生 React 元素具备的一切能力,我们称这些组件为受控组件和非受控组件
  • 大多数情况下我们使用非受控元素,一些特殊场景下回使用受控元素,例如:清空输入(涉及到 value 值要被外部控制)
// App.jsx
import { useState } from "react";

function App() {
    const [inputVal, setInputVal] = useState("");

    // 这么写是不对,会频繁造成这个handleInput的引用重复创建
    // 并且这不仅仅是性能上的问题,还有渲染方面的问题以及防抖节流这类工具函数的使用造成影响
    // 只不过我们现在还没学习hooks。所以暂时这么写
    const handleInput = (e) => {
        console.log("e value", e.target.value);
    };
    const handleControlledInput = (e) => {
        console.log("e value", e.target.value);
        setInputVal(e.target.value);
    };
    return (
        <div>
            {/* 非受控 是自由的 */}
            {/* onChange在React中和onInput是一个意思 */}
            {/* 非受控组件只能通过defaultValue去设置初始值,然后通过绑定对应的事件去监听值 */}
            非受控 <input
                type="text"
                onChange={handleInput}
                defaultValue="初始值"
            />
            <hr />
            {/* 受控的 */}
            {/* 键盘输入东西,但是输入框没有变化,因为开发者没有让他变化 */}
            受控 <input
                value={inputVal}
                type="text"
                onChange={handleControlledInput}
            />
            <input type="checkbox" name="" id="" checked={false} />
        </div>
    );
}

export default App;

hooks

hooks 是 React16.8 推出的一个新特性,这个特性允许开发者在不写类组件的情况下去生成 state 以及一些其他曾经是类组件专属的东西,变相的削弱类组件,加强函数组件

使用

useState

  • useState 是一个 React hook,允许我们在组件内定义状态
  • Call useState at the top level of your component to declare a state variable.
    • React 文档中的这句话有人会误解为必须在 React 组件的顶层书写 useState
    • 这其实是不正确的,这句话的意思其实是在整个作用域的最顶层,而不是代码书写的最顶层
    • 就是说不要把 useState 写到块级作用域
    • React 官方更推荐把相关逻辑的代码写到一起
  • 如果给 useState 传递的初始化的值是一个函数,那么他必须是一个纯函数,并且没有参数
    • 纯函数就是如果调用时参数一致,那么函数的返回值永远一致,换句话说就是函数的执行不会依赖任何其他外部因素
    • 因为在 React 调用这个 initializer function 时是不会传参数的
  • The set function that lets you update the state to a different value and trigger a re-render
    • useState 返回的数组的第二个值是一个函数,它可以帮助你更新状态,并且重新渲染组件
  • The set function only updates the state variable for the next render
    • set function 在你调用以后他不会立即更新,而是在 React 的下一次渲染阶段更新
// 纯函数
function add(a, b) {
    return a + b;
}
// 无论我调用多少次,返回值都是一样的
add(1, 2);
add(1, 2);
add(1, 2);

// 非纯函数
function addRandom(a, b) {
    return a + b + Math.random();
}
// 每次调用调用返回的值可能不一样
addRandom(1, 2);
addRandom(1, 2);
addRandom(1, 2);
// 对象的更新,需要返回一个新的引用值
import { useState } from "react";

function App() {
    const [obj, setObj] = useState({
        a: 1,
        b: 2,
    });
    const updateObjValue = () => {
        setObj((prev) => {
            return {
                ...prev,
                b: 10,
            };
        });
    };
    return (
        <div onClick={updateObjValue}>
            a: {obj.a}
            b: {obj.b}
        </div>
    );
}

export default App;

useEffect

useEffect 被官方定义为用来处理副作用的

副作用:完全不依赖 React 功能的外部操作【这些外部操作不经过 React 的手,但是却对 React 产生了影响】

  1. http 请求
  2. dom 操作
  3. 异步操作多数都会产生副作用

虽然我们不是所有的副作用操作都放在 useEffect 里进行,但是官方推荐我们尽可能的将副作用处理放在 useEffect 中运行

因为副作用的操作他是会产生意料之外的结果的,如果我们想要更好的去追踪我们的副作用执行时机,就可以将他们归纳进 useEffect 里面方便追踪

在我们实际项目开发中,不一定将所有操作都放在 useEffect 中,但是如果你使用到了 useEffect,就一定要用它来处理副作用,不然的话就不要随便使用

  • useEffect 接受两个参数:
    • setup:初始化的意思,是一个函数
    • dependencies?:依赖,必须是一个数组,里面的每一项数据必须是使用 useState 构建出来的数据,否则 React 无法感知变化
  • useEffect 的执行时机:
    1. 当我们使用 useEffect 去注册了 setup 以后,React 会在该组件每次挂载到页面中时(挂载完毕)都会执行 setup 函数,但是是异步执行 setup
      • 挂载:React 将一个组件渲染到页面的过程叫做挂载,渲染完毕叫做挂载完毕
    2. 当依赖发生变更的时候,useEffect 会重新执行对应的 setup 函数
import { useEffect, useState } from "react";

function App() {
    const [bool, setBool] = useState(true);
    useEffect(() => {
        console.log("hello app");
    }, [bool]);
    return (
        <input
            type="checkbox"
            checked={bool}
            onChange={() => setBool((prev) => !prev)}
        />
    );
}

export default App;

关于副作用的清除

  • setup 函数有一个返回值,这个返回值被称之为清理函数,清理函数会在组件卸载时被执行

  • 下面的几种场景,副作用需要我们自行清除

    1. dom 事件的绑定,组件卸载时需要解绑事件,就需要在清理函数中解绑
    import { useState, useEffect } from "react";
    export default function StudentList() {
        const [studentList, setStudentList] = useState([]);
        const getStudentListFromServer = () => {
            // 模拟网络请求
            new Promise((resolve, reject) => {
                setTimeout(() => {
                    setStudentList([{ name: "李二狗" }, { name: "张大柱" }]);
                    resolve(true);
                }, 1000);
            });
        };
        useEffect(() => {
            // 调用后台接口获取数据
            getStudentListFromServer();
            const eventHandle = () => {
                console.log("hello key down");
            };
            document.addEventListener("keydown", eventHandle);
            // 要不返回undefined,要不返回一个函数
            return () => {
                document.removeEventListener("keydown", eventHandle);
            };
        }, []);
        return (
            <div className="student-list-wrapper">
                {/* 学生列表 */}
                {studentList.map((student) => (
                    <div>{student.name}</div>
                ))}
            </div>
        );
    }
    
    1. 定时器
    import { useEffect } from "react";
    import { useState } from "react";
    export default function Tick() {
        const [tickTime, setTickTime] = useState(100);
        useEffect(() => {
            let timer = setInterval(() => {
                setTickTime((prev) => prev - 1);
                console.log("计时器工作中");
            }, 1000);
            return () => {
                clearInterval(timer);
                timer = null;
            };
        }, []);
        return <div>剩余时间:{tickTime}秒</div>;
    }
    

实际应用场景

  1. http 请求
  2. 操作真实的 dom
import { useState, useEffect } from "react";
export default function StudentList() {
    const [studentList, setStudentList] = useState([]);
    const getStudentListFromServer = () => {
        // 模拟网络请求
        new Promise((resolve, reject) => {
            setTimeout(() => {
                setStudentList([{ name: "李二狗" }, { name: "张大柱" }]);
                resolve(true);
            }, 1000);
        });
    };
    useEffect(() => {
        // 调用后台接口获取数据
        getStudentListFromServer();
        // 获取真实的dom
        const studentListDom = document.getElementsByClassName(
            "student-list-wrapper"
        );
        console.log("studentListDom", studentListDom[0]);
    }, []);
    return (
        <div className="student-list-wrapper">
            {/* 学生列表 */}
            {studentList.map((student) => (
                <div>{student.name}</div>
            ))}
        </div>
    );
}

useCallback

  • 每次组件的重新渲染都意味着内部所有的引用值都被重新构建
  • useCallback 是用来长期稳定的维护某一个函数引用的,它会将函数创建后的引用保存,当函数组件下一次重新渲染时,它会直接返回之前保存的引用,而不是重新创建引用
  • useCallback 只在创建函数引用的时候使用
    1. 第一个函数是你要对应赋值给变量的函数体(函数声明)
    2. 第二个参数是依赖项,当依赖项发生变动以后,对应的函数引用就会被重新生成。为什么要这样做呢?
      • 每次函数组件重新渲染就是函数的重新执行,那么重新执行的话内部的函数上下文就会整体变化
      • 当没有依赖项时,getCountValue 的引用值是永远不会变的,它就会和函数组件首次渲染所生成的上下文产生闭包,当组件状态改变导致函数组件重新渲染,count 的值修改了,但是 getCountValue 的引用还是上一个时间切片的,getCountValue 如果访问了 count,获取的值也是上一次时间切片的,始终是 0
      • 当有依赖项时(count),依赖项发生改变时,对应的函数引用就会被重新生成
  • 依赖的作用:当依赖发生变化时在不同的 hook 里有不同的效果
    • useEffect:导致注册的初始化函数重新执行
    • useCallback:导致函数引用重新生成
import { useState, useCallback } from "react";

export default function Counter() {
    const [count, setCount] = useState(0);
    const addCount = useCallback(() => {
        setCount((prev) => prev + 1);
    }, []);
    const getCountValue = useCallback(() => {
        console.log("最新的count value:", count);
    }, [count]);
    return (
        <div>
            <span>{count}</span>
            <button onClick={addCount}>add count</button>
            <button onClick={getCountValue}>get count value</button>
        </div>
    );
}

useMemo

  • useMemo 类似 Vue 的计算属性
  • useCallback 就是基于 useMemo 实现的
  • 用来做缓存的,功能上和 useCallback 完全一致,只不过它除了可以缓存函数以外,任何东西都可缓存,但是最佳实践是使用 useCallback 去缓存函数
  • useMemo 有两个参数
    • 第一个参数是一个函数,这个函数会被 React 直接执行,然后将其返回值进行缓存
    • 第二个参数是依赖项,当依赖项发生变化时,React 会重新执行第一个参数的函数,然后拿到最新的返回值再次缓存
import { useState, useCallback, useMemo } from "react";

export default function Counter() {
    const [count, setCount] = useState(0);
    const addCount = useMemo(
        () => () => {
            setCount((prev) => prev + 1);
        },
        []
    );
    const getCountValue = useCallback(() => {
        console.log("最新的count value:", count);
    }, [count]);
    return (
        <div>
            <span>{count}</span>
            <button onClick={addCount}>add count</button>
            <button onClick={getCountValue}>get count value</button>
        </div>
    );
}
import { useEffect, useState, useMemo } from "react";
import { getStudentList } from "../../request/index";
import StudentItem from "./components/StudentItem";
import useRequestLoadingDispatcher from "../../hooks/useRequestLoadingDispatcher";
import { useCallback } from "react";

export default function StudentList() {
    const [studentList, setStudentList] = useState([]);
    const { loading, executRequest } = useRequestLoadingDispatcher();
    const studentNameList = useMemo(
        () => studentList.map((v) => v.name),
        [studentList]
    );

    const fetchStudentFromServer = useCallback(() => {
        executRequest(async () => {
            const studentResponse = await getStudentList();
            setStudentList(studentResponse.data);
        });
    }, [executRequest]);
    useEffect(() => {
        fetchStudentFromServer();
    }, []);
    return (
        <div>
            {loading ? (
                <div>正在加载中...</div>
            ) : (
                studentList.map((student) => {
                    return <StudentItem {...student} />;
                })
            )}
            {studentNameList.map((v) => (
                <div>{v}</div>
            ))}
        </div>
    );
}

useRef

useRef 构建一个状态出来,但是这个状态是直接脱离 React 控制的,它的变化也不会造成重新渲染,同时状态还不会因为组件的重新渲染而被初始化

  • 组件导出有且只有一个根组件,可以使用 React.Fragment 去包裹,最终生成的 dom 不会出现 React.Fragment,更简便的写法是<></>,这是 React.Fragment 的语法糖
// 不使用useRef
import { useState, useCallback, useRef } from "react";

export default function Ticker() {
    const [time, setTime] = useState(60);
    const [timer, setTimer] = useState(null);
    const startTick = useCallback(() => {
        const _timer = setInterval(() => {
            setTime((prev) => prev - 1);
        }, 1000);
        // timer的变化会造成组件的重新渲染,但是这是不必要的
        // 前面有讲过,什么时候该使用组件状态,就是当组件里的数据要和React产生链接时,就要使用状态
        // timer是不需要和React产生链接的,如何判断是否需要产生链接呢?那就是不产生链接会不会对页面造成什么影响
        // 没有必要的重新渲染就是对页面性能的浪费
        setTimer(_timer);
    }, []);
    const stopTick = useCallback(() => {
        if (timer) {
            console.log("清除定时器");
            clearInterval(timer);
            setTimer(null);
        }
    }, [timer]);
    return (
        <>
            <button onClick={startTick}>start</button>
            <button onClick={stopTick}>stop</button>
            <span>{time}</span>
        </>
    );
}
// 使用useRef
import { useState, useCallback, useRef } from "react";

export default function Ticker() {
    const [time, setTime] = useState(60);
    const timerRef = useRef(null);
    console.log(timerRef);
    const startTick = useCallback(() => {
        timerRef.current = setInterval(() => {
            setTime((prev) => prev - 1);
        }, 1000);
    }, []);
    const stopTick = useCallback(() => {
        if (timerRef.current) {
            clearInterval(timerRef.current);
        }
    }, []);
    return (
        <>
            <button onClick={startTick}>start</button>
            <button onClick={stopTick}>stop</button>
            <span>{time}</span>
        </>
    );
}
  • 处理真实 dom(处理真实 dom 只是 useRef 的应用场景之一)
import { useCallback } from "react";
import { useRef, useEffect } from "react";

export default function TestInput() {
    const inputDomRef = useRef(null);
    //   useEffect(() => {
    //     inputDomRef.current = document.getElementsByClassName("input-example")[0];
    //   }, []);
    const handleClick = useCallback(() => {
        inputDomRef.current.focus();
    }, []);
    return (
        <div>
            {/* React使用了useEff帮助我们获取真实的dom并赋值 */}
            <input ref={inputDomRef} type="text" className="input-example" />
            <button onClick={handleClick}>click me</button>
        </div>
    );
}
  • useRef 不是仅为真实 dom 服务的,但是需要操作真实 dom,使用 useRef 是最优解
  • 给函数组件挂 ref 是没有必要的,我们知道函数组件就是一个函数,思考一下,如果挂了 ref,能够获得什么呢
    • 函数的引用?
      • 我们 import 进来的组件其实就是函数的引用,所以没必要获得
    • 函数的返回值?
      • 返回值是要渲染到页面的元素,所以无法获得
    • 函数的上下文?
      • 函数的上下文是随着函数的执行而创建,随着函数的执行完毕而销毁,所以也无法获得
  • 但是有些场景下需要在父组件拿到子组件里面的东西的时候要怎么办呢(例如:子组件里面有一个输入框,父组件的一个按钮点击了能够让子组件里的输入框聚焦),这个时候就需要用到 forwardRef,我们在下一节说明
  • 给组件挂 ref 相当于给组件传了一个属性,但是我们打印发现,组件的 props 里没有 ref 的属性,这是为什么呢?
    • 因为 React 始终希望组件 props 是纯净的,就是属性一旦发生变化,就会造成组件的重新渲染
    • 但是我们知道 ref 的变动是不会造成组件的重新渲染的
    • 所以这两者的理念就违背了,所以组件的 props 里没有 ref
    • 所以组件对应的函数还有第二个参数 ref,
  • 过去会给类组件挂 ref 的,因为类组件 import 的是一个类(构造函数),渲染时(调用)会返回一个实例,ref 得到的就是这个实例

forwardRef

  • forwardRef 是一个高阶组件
  • 高阶组件:接受一个组件作为参数,返回一个新的组件
  • 给子组件挂 ref 是要求子组件去追加 forwardRef 的,forwardRef 会将得到的 ref 通过第二个参数传递给真实的函数组件
  • forwardRef 一般都是和组件 ref 连用的,不会单独使用
import { forwardRef } from "react";

function TestInput(props, parentRef) {
    console.log("TestInput组件渲染", props);
    return (
        <div>
            {/* React使用了useEff帮助我们获取真实的dom并赋值 */}
            <input ref={parentRef} type="text" className="input-example" />
        </div>
    );
}

export default forwardRef(TestInput);
/**
 * forwardRef的伪代码
 * component:函数组件
 * 返回一个新的组件
 */
function forwardRef(component) {
    return function (props) {
        const { ref, realProps } = props;
        return component(realProps, ref);
    };
}

useImperativeHandle

  • 子组件拿父组件的东西很容易,使用组件属性就可以了

  • 那如果我们想要父组件从子组件拿东西呢?

    • 上一节说过可以通过 ref 和 forwardRef 连用来实现
    import { forwardRef } from "react";
    import { useState, useCallback, useMemo } from "react";
    import useForceUpdate from "../../hooks/useForceUpdate";
    function Counter(props, ref) {
        const [count, setCount] = useState(props.defaultCount);
        const forceUpdate = useForceUpdate();
        console.log("Counter渲染");
        // 假设我们有一个需求,父组件要拿到子组件提供的一个秘钥,这秘钥通过count + 一个随机数生成的
        // 同时还要满足count如果不变,则这个秘钥也不变
        // 如果只靠forwardRef + ref是没法实现的,因为,每次不是count引发的重新渲染会导致秘钥发生改变(我这里使用之前自定义的强制刷新hook来模拟)
        ref.current = count + Math.random();
        const addCount = useMemo(
            () => () => {
                setCount((prev) => prev + 1);
            },
            []
        );
        return (
            <div>
                <span>{count}</span>
                <button onClick={addCount}>add count</button>
                <button onClick={forceUpdate}>force update</button>
            </div>
        );
    }
    export default forwardRef(Counter);
    
  • 为实现这样的需求,React 提供了 useImperativeHandle 这个 hook

    • 使用 useImperativeHanle 需要三个参数
      1. 第一个参数是 ref,React 底层会帮我们修改 ref 的 current 值
      2. 第二个参数是一个函数,这个函数的返回值最终会赋值给 ref.current 属性
      3. 第三个是依赖项,这是重点,当依赖项不变时,ref 的 current 就不会被重新赋值。这就满足我们上面的需求了,count 变,秘钥变。count 不变,秘钥不变
      useImperativeHandle(
          ref,
          () => {
              return count + Math.random();
          },
          [count]
      );
      
      // useImperativeHandle的伪代码
      function useImperativeHandle(ref, fn, dep) {
          if (dep改变 || 首次加载) {
              ref.current = fn();
          }
      }
      

useContext

  • context:上下文
  • 因为后面要学习的 react-router、redux 之类的库基本用的都是上下文,如果后期学习这些库的原理或源码或手写这些库,就必须要有上下文的基础
  • 上下文的定义:允许组件之间通过除了 props 以外的情况去共享数据(通过 props 一层一层的传下去能让子组件共享祖先组件的数据)
  • 使用
    • 先创建上下文
    import { createContext } from "react";
    const ThemeContext = createContext("light");
    export default ThemeContext;
    
    • 使用上下文包裹组件
    // App.jsx
    <ThemeContext.Provider value={theme}>
        <StudentList />
        {/* <ForceUpdateTest /> */}
        <Counter ref={counterRef} defaultCount={10} />
        {/* <Ticker /> */}
        {/* <TestInput ref={testInputRef} /> */}
        <button onClick={changeTheme}>click me</button>
    </ThemeContext.Provider>
    
    • 接入上下文
    import { useContext } from "react";
    import ThemeContext from "../../../../context/themeContext";
    export default function StudentItem(props) {
        // 接入上下文
        const contextValue = useContext(ThemeContext);
        console.log(" contextValue", contextValue);
        return (
            <div
                style={{
                    background: contextValue === "light" ? "#fff" : "#666",
                }}
            >
                name: {props.name}
                age: {props.age}
            </div>
        );
    }
    
  • 最佳实践
    • 一旦你的属性传递超过了 4 层,你得考虑是否使用上下文
    • 上下文大多数情况是用来做全局数据管理的 【vuex】
    • React 生态中的很多库都内置了上下文,而且这些库都很好【router redux】所以我们平时直接去使用上下文的场景不是很多

useLayoutEffect

  • useLayoutEffect 和 useEffect 的功能几乎一致,只有一个细小的区别【运行规则】,日常工作中 99%都是使用 useEffect,但是如果你在使用 useEffect 的时候遇到了一些问题,不妨试试 useLayoutEffect
    • useEffect 的运行规则:组件首次渲染工作完成并且将真实 dom 生成到页面以后,将对应的回调函数推入异步队列等待执行
    • useLayoutEffect 的运行规则:组件首次渲染工作完成并且将真实 dom 生成到页面以后,将对应的回调函数推入同步队列等待执行【意味着 useLayoutEffect 会完全阻塞后续的更新工作】
import { useEffect, useState, useMemo, useLayoutEffect } from "react";
import { getStudentList } from "../../request/index";
import StudentItem from "./components/StudentItem";
import useRequestLoadingDispatcher from "../../hooks/useRequestLoadingDispatcher";
import { useCallback } from "react";

export default function StudentList() {
    const [studentList, setStudentList] = useState([]);
    const { loading, executRequest } = useRequestLoadingDispatcher();
    const studentNameList = useMemo(
        () => studentList.map((v) => v.name),
        [studentList]
    );

    const fetchStudentFromServer = useCallback(() => {
        executRequest(async () => {
            const studentResponse = await getStudentList();
            setStudentList(studentResponse.data);
            //   setStudentList([]);
        });
    }, [executRequest]);
    useEffect(() => {
        fetchStudentFromServer();
    }, []);
    // 增加一个逻辑:如果studentList为空(studentList.length === 0)
    // 显示暂无学生数据,否则显示学生列表
    for (let i = 0; i < 50000; i++) {
        console.log(i);
    }
    return (
        <div>
            {!loading && studentList.length === 0 && <div>暂无学生数据</div>}
            {loading ? (
                <div>正在加载中...</div>
            ) : (
                studentList.map((student) => {
                    return <StudentItem {...student} />;
                })
            )}
            {studentNameList.map((v) => (
                <div>{v}</div>
            ))}
        </div>
    );
}
  • 学生列表展示的现象
    1. 先展示了暂无学生数据
    2. 展示正在加载
    3. 展示学生列表
  • 出现的原因
    • 当我们使用 useEffect 时:
      1. 组件开始渲染,这个使用 useEffect 还没执行,studentList.length === 0,loading 为 false,所以一开始必定显示【暂无学生数据】
      2. 组件首次渲染完毕,useEffect 注册的回调函数推入异步队列,异步队列的任务什么时候执行呢?当主线程空闲时才会处理异步队列的任务,但是我们代码中还有 50000 次循环,所以主线程要处理这 50000 次循环,所以页面就一直显示【暂无学生数据】,直到主线程处理完这 50000 次循环,空闲下看来,才会执行异步队列的任务
      3. 执行 fetchStudentFromServer 函数,loading 设置 true,页面显示【正在加载中】
      4. 等待数据请求回来了,重新渲染页面,【展示学生列表】
    • 当我们使用 useLayoutEffect 时:
      1. 组件开始渲染,这个使用 useEffect 还没执行,studentList.length === 0,loading 为 false,所以一开始必定显示【暂无学生数据】
      2. 组件首次渲染完毕,useLayoutEffect 注册的回调立马同步执行,逻辑上会立即执行 fetchStudentFromServer 函数,设置 loading 为 true,这个过程耗时小于 16ms,页面从【暂无学生数据】变成【正在加载中】,人的肉眼是无法识别这么短时间的变化的,所以看到的【正在加载中】
      3. 等待数据请求回来了,重新渲染页面,【展示学生列表】
  • 最佳实践
    • 正常情况下我们全部使用 useEffect,只有在逼不得已的情况下(上面的情况)才考虑是否使用 useLayoutEffect
      • 因为 useLayoutEffect 是同步的,也就是说如果它注册的回调函数执行的很慢,它也会造成页面的阻塞

useTransition

  • 这个 hook 非常关键,因为你如果想要让你的 React 项目的丝滑程度更上一层楼,当你所有的优化手段都用完以后,你必须面对的一个东西就是 React 的调度策略,而 useTransition 是目前 React 提供给我们的唯一和调度策略打交道的入口
  • 要搞懂 useTransition 就必须先去了解以下三个东西(先看原理相关部分的前三节,再回来看这个
    1. 浏览器渲染原理
    2. React 渲染原理
    3. React 的 concurrent mode

自定义 hook

  • 消除冗余代码,提高代码的可维护性,同时将复杂的逻辑进行内聚,减少出错的可能
  • 跨域:网页地址和请求接口的地址不在同源策略的规范下,就会报跨域错误
  • 同源策略:网页地址和请求的接口地址必须是同协议、同域名、同端口
  • 请求发送出去,服务器能够收到,服务器也返回了数据
  • 响应结果来到浏览器,浏览器就会判断,如果是跨域请求就会看响应头,如果服务器设置了允许跨域,就不会报跨域错误了,如果没有允许,就会报错

请求 loading hook

// useRequestLoadingDispatcher.js
import { useState } from "react";

export default function useRequestLoadingDispatcher() {
    const [loading, setLoading] = useState();

    const executRequest = async (promiseFn) => {
        setLoading(true);
        await promiseFn();
        setLoading(false);
    };
    return {
        loading,
        executRequest,
    };
}
const { loading, executRequest } = useRequestLoadingDispatcher();
const fetchStudentFromServer = () => {
    executRequest(async () => {
        const studentResponse = await getStudentList();
        setStudentList(studentResponse.data);
    });
};

强制刷新 hook

  • Vue $forceupdate:强制刷新组件
  • 强制刷新组件:这种情况一般很少使用,如果需要使用强制刷新组件说明操作真实 dom 的情况比较多
  • React 中让一个组件重新渲染,无非就是组件状态或组件属性发生改变
// useForceUpdate.js
import { useState } from "react";

export default function useForceUpdate() {
    const [, setValue] = useState({});
    const forceUpdate = () => {
        setValue({});
    };
    return forceUpdate;
}

监听浏览器滚动 hook

import { useEffect } from "react";

export default function useWindowScrollWatcher(scrollCallback) {
    useEffect(() => {
        const scrollHandler = (evnet) => {
            scrollCallback && scrollCallback(evnet);
        };
        document.addEventListener("scroll", scrollHandler);
        return () => {
            document.removeEventListener("scroll", scrollHandler);
        };
    }, []);
}

原理相关

浏览器渲染帧探究

  • 我们以电影为例,我们在看电影时感觉是一个动态的画面,实际上电影是由 n 个静态的画面来组成,只不过这些画面切换的极快(切换速度超过我们人眼的感知),所以我们感知上它就是动态的而非静态的
  • 这里面的一个静态画面我们就称之为一帧
  • 浏览器也是一样,它也是一帧一帧渲染的
  • 浏览器的渲染帧:浏览器一次完整绘制过程
  • 切换画面的速度称之为帧率
  • 帧率越高,切换画面的速度越快,肉眼的感知就越流畅
  • 浏览器的帧率:1 秒钟 60 帧,16ms 一帧
  • 掉帧:某一帧做的事情太多,导致这一帧的执行时间远超过预期时间【例如预期一帧是 16ms,但是该帧实际花费 100ms,那么他就会顶掉其他本该绘制的帧】
  • 浏览器掉帧也是指一帧做的事太多了,占用的时间太长,导致其他的帧被卡掉
  • 浏览器一帧大概会做什么事?
    1. 处理用户的交互事件【上一帧传递过来的事件的回调函数】
    2. 调用 requestAnimationFrame
    3. 执行重排重绘【重新绘制一帧】
    4. 调用 requestIdleCallback【如果还有空闲时间的话】
  • 浏览器掉了一帧:这一帧不在重排重绘,这一帧的画面直接就没了,同时注册的 requestAnimationFeame 和 requestIdleCallback 不再执行,用户的交互事件不会处理
  • 如果我们想要页面流畅,就要尽可能的稳定浏览器的帧率,那么就要确保每一帧做的事不要太多
import { useCallback } from "react";
import { forwardRef } from "react";

function TestInput(props, parentRef) {
    const handleInput = useCallback(() => {
        console.log("用户输入事件触发");
    }, []);
    // 模拟掉帧的情况
    const handleClick = useCallback(() => {
        for (let i = 0; i < 10000; i++) {
            console.log("111");
        }
    }, []);
    return (
        <div>
            {/* React使用了useEff帮助我们获取真实的dom并赋值 */}
            <input
                onChange={handleInput}
                ref={parentRef}
                type="text"
                className="input-example"
            />
            <button onClick={handleClick}>click me</button>
        </div>
    );
}

export default forwardRef(TestInput);

React 渲染原理

简单描述 React 首次渲染发生的事情(不考虑 babel 编译 JSX 的流程,因为这个是 webpack 做的事):

  1. 拿到 React.createElement 返回的 React 节点(这是一个对象)
const rootElement = React.createElement(
    "div",
    {},
    React.createElement("span", {}, "span text"),
    React.createElement("h1", {}, "title")
);
  1. 通过 render 方法进行渲染
    • 如果是组件,会在执行渲染的过程中保存对应的 hooks 和执行对应的 hoos(例如:useState 是立即触发的,useEffect 要留存下来等到 dom 挂载完毕后再触发)
    • 如果是普通的标签元素(div,h1 等)不会生成对应的真实 dom,而是生成一个描述对象,这个描述对象描述了当前要创建的真实 dom 一些信息,以及这个描述对象要做的操作(首次渲染所以只能是 create),这个描述对象叫做 fiber
  2. 将描述对象依次编译成真实 dom,然后插入到父元素 appendChild
  3. 等到整个渲染流程结束就会得到一个完整的真实 dom 树,最后将其插入到对应的 root 元素下
  4. 触发对应的生命周期函数

更新发生的事:

生成一个新的 React 节点

  • 不会全部重新生成,例如 Counter 的组件状态改变了,那么 Counter 及它的子组件或子元素会重新渲染(重新生成 React 节点)
  • 进入 diff 阶段(diff 算法)比较以 Counter 节点为根元素的两棵树的差异(重新渲染只是生成新的 React 节点对象,最终不一定会转换成真实的 dom),
  • diff 算法结束后也会生成一个清单:这个清单里面都是 fiber,每一个 fiber 节点的操作状态可以是 create、delete、update 中的的一种
  • 将差异应用到真实的 dom
  • 触发对应的生命周期函数

通过上面的分析,我们可以得出一个结论,当我们的元素或组件写的很多时,那么执行 react.createElement 和 render 这两个方法的时间就会很长,我们知道浏览器的一帧是要控制在 16ms 以内的,如果超过这个时间就会掉帧,用户的交互就会失效

React 为解决这个问题,将整个渲染流程拆分成了两个阶段,并且在 render 阶段做了特殊操作解决问题(concurrency)

  1. render 阶段:执行业务代码的逻辑和 react 代码的逻辑,生成一个描述对象(类似于一个表格描述了哪些地方要塞入 dom,哪些地方要删除 dom),以方便第二阶段知道要如何创建真实 dom
  2. commit 阶段:根据第一阶段提供的描述进行 dom 的操作(创建真实 dom 塞入页面,删除 dom,更新 dom)

concurrency

concurrency 是 React18 的叫法,之前版本称为 concurrent mode,中文翻译为并发性(在一些文章会称为“可中断渲染”)

React 将渲染流程分为两个阶段:

  • render:负责将需要渲染的组件(首次渲染是 App 组件,更新阶段是哪个组件需要更新就是哪个组件)的内部逻辑以及 react 的内部逻辑进行执行并得出一份 fiber 清单,记录了最终要展示给用户看的真实 dom 树是什么样的,要增加哪些 dom,要删除哪些 dom,要更新的 dom
  • commit:负责将 fiber 清单转换为真实 dom

因为 commit 阶段是非常简单且常规的工作也没什么逻辑,所以 commit 阶段的工作百分之九十九的项目都能在一帧内完成,所以 React 着重对 render 阶段做了优化

主要使用了 requestIdleCallback 的特性

  • requestIdleCallback 函数传入一个回调函数,回调函数会接受到一个 IdleDeadLine 的参数,这个参数可以获取到当前帧的空闲时间和当前帧已经没有空闲时间,
type IdleDeadLine = {
    timeRemaining: () => number // 当前帧剩余时间
    didTimeout: boolean // 是否没有空闲时间
}

timeRemaining 的返回值会实时改变的。实例代码的递归里面可以直接传递下去进行判断

requestIdleCallback(cb); // 每一帧还有剩余时间时执行

下面掉帧情况是表示发生在 render 阶段执行时长超过 16ms 的场景下

  • 首次渲染的 render 阶段

    • 如果整个页面由 React 写的且只有一个根组件,页面是白屏的,用户无法和页面进行交互。用户就无法感知掉帧
    • 页面只有一部分是由 React 管理的或者页面由多个根节点(多个 React 容器),这种情况用户是可以和页面交互的,因为不被 React 管理的地方已经渲染出来了。这个时候 React 还在进行工作(js 引擎在工作),但是用户已经在渲染出来的区域进行交互了,必然会造成掉帧的情况,那么用户的交互事件就丢失了
  • 更新时的 render 阶段

    • 用户看到的是更新前的那个画面,这个时候用户可以和页面交互,但是此时 render 还在工作,所以会造成掉帧
// 实现React的可中断渲染

// JSX写的代码
// function Counter() {
//     return (
//         <div>
//             <span>hello world</span>
//             <button>click me</button>
//         </div>
//     )
// }

// babel转换后的代码
// function Counter() {
//     return (
//         React.createElement("div", {}, React.createElement("span", {}, "hello world"), React.createElement("button", {}, "click me"))
//     )
// }

function Counter() {
    return {
        type: "span",
        value: "hello world",
        next: {
            type: "button",
            value: "click me",
            next: {
                type: "button",
                value: "click me",
                next: {
                    type: "button",
                    value: "click me",
                    next: {
                        type: "button",
                        value: "click me",
                    },
                },
            },
        },
    };
}

// createElement转换的结果,不难发现children是一个链表的结构
const CounterElementDescriptors = {
    type: "Function",
    fn: Counter,
};

// concurrency实际上就是对createElement的执行结果进行可中断化
let presentWork = null;
let rootElementDescriptor = null;
let elementsContainer = null;

function performUnitOfWork(deadline) {
    console.log(deadline.timeRemaining());
    // 双等于号会强制类型转换,undefined == null
    if (presentWork == null) {
        commitRoot(rootElementDescriptor);
        // 当前没有工作要做
        return;
    }
    // 已经没有剩余时间了
    if (deadline.timeRemaining() === 0) {
        // deadline.timeRemaining 当前帧剩余的空闲时间
        // 假设到span的时候,当前帧已经没有空闲时间了
        // 我们就把任务推进到下一帧执行
        // 这样子就能把我们本该一帧完成的渲染任务分到多帧去完成
        // 这样就能保证无论组件写的有多大,它实际上会被拆分成很多个小任务去分散到每一帧里执行
        // 这样就能保证不会出现掉帧,用户的交互就不会失效
        console.log("当前帧没空闲时间了,推送到下一帧执行");
        requestIdleCallback(executeWorkLoop);
        return;
    }

    // 执行真正的工作
    if (presentWork.type === "Function") {
        // 判断一下根组件
        rootElementDescriptor = presentWork; // 保存一下根引用
        // 代表是组件
        const firstChildren = presentWork.fn();
        console.log("children", firstChildren);
        firstChildren.parent = presentWork;
        presentWork.children = firstChildren;
        presentWork = firstChildren;
        performUnitOfWork(deadline);
    } else {
        // 代表是标签元素
        const dom = document.createElement(presentWork.type);
        dom.innerHTML = presentWork.value;
        presentWork.dom = dom;
        // 当前处于render阶段,不会塞入到页面中
        // 等待commit阶段一次性提交到页面去
        presentWork = presentWork.next;
        performUnitOfWork(deadline);
    }
}

function executeWorkLoop(deadline) {
    console.log("executeWorkLoop----", deadline);
    performUnitOfWork(deadline);
}

// render阶段
function render(element) {
    elementsContainer = element;
    presentWork = CounterElementDescriptors;
    requestIdleCallback(executeWorkLoop);
}
// commit阶段
function commitRoot(_rootElement) {
    console.log("开始commit阶段,遍历生成真实dom", _rootElement);
    let renderChildrenElemnts = _rootElement.children;
    do {
        elementsContainer.appendChild(renderChildrenElemnts.dom);
        renderChildrenElemnts = renderChildrenElemnts.next;
    } while (renderChildrenElemnts);
    {
    }
}

render(document.getElementById("root"));

生态

  • react-router 路由解决方案
  • redux / redux-toolkit 数据仓库解决方案

结合 ts 使用

项目实战