前端框架-react

· 9989 words · 20 minute read

react 🔗

react 是一个 js 的库, 简化 html 和 js 操作 dom 的开发过程。 核心是管理组件 make components,管理状态 manage state
react 中引入Flow 是一个静态类型检查器,用于在 JavaScript 中引入静态类型。https://github.com/facebook/react

  • react 核心概念快速入门
    • JQuery 是命令式库(需要指明每一步的命令), React 是声明式库。
    • UI = f(data), data = props + state.
  • Fiber 结构
    • 实现异步可中断的找出变化的组件。
  • VirtualDOM
    • 优势: 不用具体考虑 DOM 操作,可以用申明式的方式写代码;减少页面渲染次数;更好的跨平台能力。
  • Diff 算法
  • Hooks
    • useState 让函数组件具有维持状态的能力。

实际开发过程中,都是在写组件。所以如何提取公共组件就很重要,因为可以服用组件,少写很多代码。

  • 识别重复的 UI 部分
  • 定义 props 接口,明确组件接收的参数,类型的约束
  • 实现组件, 将 UI+逻辑封装到组件内部,通过 props 暴露参数
  • 导出和使用公共组件

React Developer Tools 🔗

Browser extension , react dev tools

核心概念 🔗

component 组件是 js 定义的首字母大写的函数,函数返回一组 hmtl 标签(JSX)。

JSX 是 js+html,属性名称为camelCase, 不是HTML attributes, 一种更自然的方式创建 react element。
jsxbabel 被翻译为React.createElement函数,返回 js 对象( VDOM)。
babel可以将 JSX 转译为等价的 js 代码。

html 为<button class="btn"> , jsx 为<button className="btn">


/*
<Greeting text={'yo'} />
*/
function Greeting(props) {
  return <h1>{props.text}</h1>;
}

/*
<Parent>
  <Child />    // passed as "children"
</Parent>
*/
function Parent(props) {
  return <div>{props.children}</div>;
}

组件的内容是“插槽”。 例如 Vue.js。在 React 中,通常称其为 "children"。都是指将某些内容或元素插入到子组件的指定位置。

组件的属性,状态,事件。

组件通讯:

  • 通过组件的属性 props, 单向数据流(pass data into another component)。
    • 父->子,props, 将 jsx 传给子组件(插槽)
    • 子->父,子调父传进来的函数
  • context hook 多组件共享状态

React Hooks: useState, useContext, useRef, useEffect。

状态管理。

渲染流程。

避免重复渲染。

<div>Hello</div>这段 jsx,被Babel转换为React.createElement('div', null, 'Hello')函数,该函数返回一个对象

react element 的 type 种类: REACT_ELEMENT_TYPEREACT_PROOTAL_TYPE(将节点渲染到父组件之外的 dom 节点上), REACT_FRAGMENT_TYPE(无需添加额外的父节点,将子组件放一起返回), REACT_STRICT_MODE_TYPE, REACT_PROFILER_TYPE(profiler 测量),

REACT_CONSUMER_TYPE(<MyContext.Consumer>创建的虚拟 DOM 对象的类型),REACT_CONTEXT_TYPE (React.createContext(1)返回的对象的类型),

REACT_FORWARD_REF_TYPE (React.forwardRef 创建的组件的类型),

REACT_SUSPENSE_TYPE(暂停组件的渲染, 直到某些操作的完成, 与 React.lazy 配置使用实现代码分割和懒加载), REACT_LAZY_TYPE(React.azy())

REACT_MEMO_TYPE(React.memo()创建的组件,记忆组件渲染结果来避免不必要的重渲染),

REACT_SUSPENSE_LIST_TYPE(控制多个 Suspense 组件的加载顺序),

REACT_SCOPE_TYPE,REACT_ACTIVITY_TYPE与 React 18 的 useTransition 等特性的实现有关。 REACT_LEGACY_HIDDEN_TYPE, REACT_TRACING_MAKER_TYPE,

{
  $$typeof: REACT_ELEMENT_TYPE,
  type: 'div',
  props: {children: 'Hello'},
  key: null,
  ref: null,
  ...
}

react hooks 🔗

hook 是 react 16.8 的新增特性。

每个组件有一个 js 对象(fiber 对象)里存 hook 的链表。

//react/packages/react-reconciler/src/ReactFiberHooks.js
export type Hook = {
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: any,
  next: Hook | null,
};

hooks 的实现原理 🔗

let _state;
const myUseState = (initialValue) => {
  _state = _state === undefined ? initialValue : _state;

  const setState = (newValue) => {
    _state = newValue;
    render();
  };
  return [_state, setState];
};

hooks 利用 js 的闭包实现。

let _state: any = [];
// 实际不能定义在全局,其他组件怎么办? 给每个组件创建一个_state数组和index, 并放在组件对应的Fiber Node上。
let index = 0;
const myUseState = (initialState: any) => {
  const currentIndex = index;
  _state[currentIndex] =
    _state[currentIndex] === undefined ? initialState : _state[currentIndex];

  const setState = (newValue: any) => {
    _state[currentIndex] = newValue;
    render(); // 重置index为0,
  };
  index += 1;
  return [_state[currentIndex], setState];
};

为什么 react hooks 的使用规则中不能再循环,条件中调用 hook 函数了? 因为 hooks 函数的存储方式是数组(实际是链表), 加入控制流后,需要通过 index 获取对应的 state, setState 就关系就错了。


fiber 对象实现了状态更新机制,支持任务不同优先级,可中断与恢复。每个任务更新单元为 react element 对应的 fiber 节点。

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode
) {
  // 静态属性
  this.tag = tag;
  this.key = key;
  this.stateNode = null; // 对应的DOM节点。
  this.memoizedState = null; // 对应的hooks链表。
  // 连接其他fiber节点形成Fiber树
  this.return = null; // 父
  this.child = null; // 子
  this.sibling = null; // 兄
  this.index = 0;
  this.ref = null;
  // 动态属性
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
  // 保存更新时造成的DOM操作
  this.effectTag = NoEffect;
  this.nextEffect = null;
  this.firstEffect = null;
  this.lastEffect = null;

  // 更新时对应的fiber, 双缓存。 更新时指向workInProgress Fiber 节点。
  this.alternate = null;
}

state management 🔗

useState(常用), useReducer, useSyncExternalStore

useState 如何更新状态? 为什么 setState 是异步的? 批量处理,延迟执行的异步机制。

effect hooks 🔗

useEffect(常用), useLayoutEffect, useInsertionEffect。

三种调用时机副作用清理常见bug,死循环

useEffect三种调用时机,取决于依赖数组的写法。 useEffect(fn) 首次和每次组件更新时调用, useEffect(fn, [])仅首次渲染时调用, useEffect(fn, [a, b]) 首次+依赖项变化时调用。

import  React, {useEffect, useState} rom 'react';
import {Link} from 'react-router-dom';

function Demo() {
  const [count, setCount] = useState(0);
  useEffect(()=>{
    console.log('1', count);
    return () => {
      console.log('2');
    }
  });
  useEffect(() => {
    console.log('3', count);
    return () => {
      console.log('4');
    }
  }, []);
  useEffect(() => {
    console.log('5', count);
    return () => {
      console.log('6');
    }
  }, [count]);

  return (
    <div>
      <p>count is: {count}</p>
      <button onClick={() => setCount(count + 1)}>add</button>
      <Link to="/management">About</Link>
    </div>
  );
}

export default Demo;

初始化时执行所有 useEffect 的主体函数。 状态更新时: 先执行依赖变化的 useEffect 的清理函数, 再执行主体函数。 组件卸载时: 执行清理函数。

初始化渲染(组件挂载) 10 30 50 点击 add 按钮 2 6 11 51 点击跳转按钮 2 4 6

ref hooks 🔗

useRef, useImperativeHandle

performance hooks 🔗

unnecessary in react 19 . useMemo, useCallback

context hooks 🔗

props 在使用时爷传父,父传子时,每一层都要写 props 中转一下。
useContext 则不用层层传递,可以让任意深度的子组件直接获取数据。

transition hooks 🔗

useTransition, useDeferredValue

random hooks 🔗

useDebugValue, useId

react 19 hooks 🔗

useFormStatus, useFormState, useOptimistic, use

单步调试 react 的入口 🔗

import App from "./App";
import { createRoot } from "react-dom/client";

const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);

cmd+shift+d 打开 vs code 调试面板,创建 launch.json 文件。

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "针对 localhost 启动 Chrome",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}/src",
      "sourceMaps": true,
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}

运行和调试“针对 localhost 启动 Chrome”,即可 debug 程序。

原理 🔗

性能优化。

react compiler 🔗

2024 年在开源实验中。 学习一下 compiler 的设计与实现。

JSX, tsx 如何变成 DOM 的? 🔗

TSX ↓ 编译(TS → JSX → JS) React.createElement() ↓ Virtual DOM(纯 JS 对象) ↓ 构建 Fiber Tree(Render Phase) ↓ 找出差异、标记变化(effect list) ↓ 提交到真实 DOM(Commit Phase) ↓ 页面展示

react 与 vue 的对比 🔗

vue 中的 v-model 双向绑定很好用,react 需要 value + onChange 去编写。

props 部分 react 更加直观,就是 function 的参数而已。vue 需要额外定义。

子组件与父组件互相传数据。 props 向下传,事件向上传。

fiber 如何实现了异步可中断更新? 🔗

  • 可中断可恢复
    • 拆分更新任务为「单位工作单元」(unit of work)
  • 有优先级(先渲染重要的任务)
  • 并发调度(让多个任务排队执行)
while (nextUnitOfWork && shouldYield() === false) {
  // shouldYield():是否让出主线程
  // 拆分更新任务为「单位工作单元」(unit of work)
  nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
// 可中断、可恢复的渲染过程

从 Vite 启动到 performUnitOfWork 的路径 🔗

npm run dev  Vite 启动

main.jsx 中执行 ReactDOM.createRoot(...).render(<App />)

 updateContainer
 scheduleUpdateOnFiber
 ensureRootIsScheduled
 performConcurrentWorkOnRoot
 renderRootConcurrent
 workLoopConcurrent
 performUnitOfWork(...)

Procomponents 🔗

npm i @ant-design/pro-components --save

shadcn 🔗

shadcn/ui, 基于 Tailwind CSS + Radix UI 的 React 组件库。

安装和使用 🔗

安装 tailwind css, 配置 Tailwind, npx tailwindcss init -p, 在 tailwind.config.js 里配置,

安装 shadcn/ui CLI, npx shadcn-ui@latest init, 默认会在 components/ui/ 下生成组件。

例如安装 button 组件。 npx shadcn-ui@latest add button, 把 Button.tsx 放到 components/ui/button.tsx 里。

问题 🔗

react 19 新特性 🔗

服务器组件。允许组件在 sever 端渲染,不用发送 js 到客户端。

状态管理。 合并 useState 批量处理。

react compiler,通过静态分析代码,自动实现类似 useMemo, useCallback 的缓存效果,减少不必要的重渲染。

reacthook 闭包陷阱 🔗

由于 javascript 的闭包特性,导致回调函数捕获了旧的状态 states,属性 props,而没有获取到最新值的现象。

解决办法, 明确依赖的数据,让 useEffect, useCallback 在依赖变化时,回调函数能重新创建,捕获到最新的数据。

react 中怎么给 children 添加额外的属性 🔗

React.cloneElement

import React from 'react';
function ParentComponent({children}) {
  const enhancedChild = React.cloneElement(children, {
    newProp: 'new prop',
    onClick: () => console.log("子元素被点了");
    className: 'enhanced-class', // 覆盖old value
  })
  return <div>{enhancedChild}</div>
}

function ChildComponent({newProp, ...props}) {
  return <div {...props}>{newProp}</div>
}
<ParentComponent>
  <ChildComponent className="orignal-class" />
</ParentComponent>

Fiber 怎么是 reac 性能的一个飞跃, fiber 架构的工作原理, fiber 如何实现更新过程可控 🔗

react 的 fiber 如何实现时间切片的? 🔗

react 的 reconciler 为什么采用 fiber 架构? 🔗

react 是否支持给标签设置自定义的属性,比如给 video 标签设置 webkit-playsinline? 🔗

自定义属性的效果取决于浏览器, React 仅将该属性传递到 DOM,不保证其功能生效。

react render 阶段的执行过程 🔗

react commit 阶段的执行过程 🔗

react 中的路由懒加载是什么?原理? 🔗

在访问特定路由时加载对应的组件代码,减少初始加载的 js 体积,提升加载速度。

动态 import, 动态导入语法,在运行时异步加载模块,返回一个 promise。

React.lazy, React 提供的高阶函数,接收一个返回 promise 的动态导入调用,返回一个可被 React 渲染的懒加载组件。

webpack 进行代码分割。

Suspense 配合 React.layz 使用, 处理加载状态,在懒加载组件加载完成之前显示占位内容。

react 中为什不直接使用 requestIdleCallback? 🔗

requestIdleCallback 是浏览器提供的 API,在空闲时间执行低优先级任务,执行时机不确定。React 的核心需求是可预测的调度。

react 组件之间怎么通讯? 🔗

react 组件之间通讯方式根据组件关系,父子,跨层级,兄弟等有所不同。

父子组件通讯 🔗

最基础的通讯方式, 通过 props 传递数据和回调函数传递事件实现。

父传子通过 props 传递数据

function Parent() {
  const [message, setMessage] = useState("a message from parent");
  return <Child content={message} />;
}

function Child({ content }) {
  return <div>{content}</div>;
}

子传父:通过回调函数传递事件

function Parent() {
  const handleChildClick = (data) => {
    console.log("子组件传递的数据", data);
  };
  return <Child onSendData={handleChildClick} />;
}

function Child({ onSendData }) {
  const sendData = () => {
    onSendData("a message from child");
  };
  return <button onClick={sendData}>发送数据</button>;
}

跨层级组件通讯(爷孙,深层级)。用 Context API。

const ThemeContext = React.createContext();

function Grandparent() {
  const [theme, setTheme] = useState("light");
  return (
    // 顶层组件提供数据
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Parent />
    </ThemeContext.Provider>
  );
}

function Parent() {
  // 中间组件无需传递
  return <Child />;
}

function Child() {
  // 深层子组件消费数据
  const { theme, setTheme } = useContext(ThemeContext);
  return (
    <div>
      <p> 当前主题: {theme} </p>
      <button onClick={() => setTheme("dark")}> 切换主题</button>
    </div>
  );
}

兄弟组件通讯。通过共同父组件中转。

function Parent() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <BrotherA onIncrement={() => setCount(count + 1)} />
      <BrotherB count={count} />
    </div>
  );
}

function BrotherA({ onIncrement }) {
  return <button onClick={onIncrement}>增加</button>;
}
function BrotherB({ count }) {
  return <div> 当前值 {count} </div>;
}

非关联组件通讯(全局状态管理)。 zustand。

react 和 vue 在技术层面上有哪些区别? 🔗

UI=f(state)。

维度 React Vue
设计理念 单向数据流 渐进式,响应式
模版语法 JSX = JS 中写 html 独立模版+指令(html 中写逻辑)
响应式原理 手动触发更新 数据劫持 Proxy+依赖收集

子组件是一个 protal, 发生点击事件能冒泡到父组件里吗? 🔗

useState 是如何实现的? 🔗

react 中 diff 算法 🔗

如何让 useEffect 支持 async,await? 🔗

react 中懒加载的实现原理是? 🔗

React.lazy(),

组件懒加载,

路由懒加载,

图片懒加载,

数据懒加载。

react 中怎么实现状态自动保存 keepalive? 🔗

表单填了一半,切换到其他页面再回来,输入的内容还在。

一,利用父组件+条件渲染, 将子组件的状态提升到父组件中。

二,路由场景时,使用 react router + 状态持久化, react-activation, 专门设计的 keepalive 库。

三,持久化到 localStorage

四,全局状态管理

react 有哪些性能优化的方法? 🔗

减少不必要的渲染 🔗

使用 React.memo 缓存组件,useMemo 缓存计算结果,useCallback 缓存函数引用。

拆分成独立小组件,使得状态变化仅影响局部。

优化列表渲染 🔗

给列表项添加唯一 key, 减少 react 虚拟 dom 的对比开销。

使用虚拟列表只渲染可视区域。

状态管理优化

事件与副作用优化 🔗

防抖,节流高频事件,清理副作用(清理 timer 防止内存泄漏)

资源加载优化 🔗

通过 React.lazy, Suspense 实现按需加载,减少初始加载体积。

react 中 Virtual DOM 一定提高性能吗? 🔗

当组件状态变化时,先构建新的 virtual dom 树,通过 diff 算法对比新旧 virtual dom 树,计算出最小变更,将变更批量应用到真实 dom 上。

react 的事件代理机制? 🔗

react 会在根节点统一注册事件监听器, 事件冒泡到根节点,根据事件源和组件树找到组件和事件处理函数并执行。

reack hooks 当中的 useEffect 是如何区分声明周期钩子的 🔗

通过依赖数组来模拟不同的生命周期行为,统一了类组件中的 componentDidMount, componentDidUpdate, componentWillUnmount 三个生命周期方法。

无依赖数组,组件挂载后执行一次,后续每次组件更新时重新执行。模拟了 componentDidMount, componentDidUpdate。

空依赖数据(空数组[]), 组件只在挂载后执行一次。模拟了 componentDidMount。

有值的依赖数组, 组件挂载后执行一次,依赖数据更新后执行一次。 模拟了 componentDidMount, componentDidUpdate。

清理函数, useEffect( return () => { return ()=>{...} }, [deps] ) 模拟 componentWillUnmount, 清理函数在组件卸载前,或者下次 effect 执行前触发,清理定时器,事件监听等。

一个 api 覆盖了类组件中的多个生命周期的功能, 让副作用逻辑的组织更清晰。

useEffect 的闭包陷阱。 依赖数组必须包含 effect 中使用的外部变量, state, props, 函数。遗漏依赖导致了读到久值。使用 ESLint 规则自动检测声明的依赖。

react 中怎么实现组件间的过渡动画? 🔗

react-transition-group 库。

react 服务端渲染怎么做 🔗

SSR 是在服务器端将 React 组件渲染成 HTML 字符串,发送给客户端的技术。 服务端使用 ReactDOMServer.renderToString(), renderToPipeableStream() 将组件渲染为 html。 客户端使用 ReactDOM.hydrateRoot()激活 HTML,使其变为可交互的 React 应用。

Next.js, SSR 框架。Remix,全栈 react 框架。

什么情况下使用 useMemo,usecallback 🔗

useMemo 缓存计算结果。 useCallback 缓存函数引用。

react 的 memo 和 memoize 函数的区别 🔗

react 的 memo 对于纯展示的组件(仅依赖 props 渲染, 无内部状态和副作用),减少渲染。

const MemoizedComponent = React.memo(({ name, age }) => {
  return (
    <div>
      {name}-{age}
    </div>
  );
});

useRef, ref, forwardsRef 的区别? 🔗

ref 是一个属性用于获取 DOM 元素的引用。

useRef 是 React Hook, 访问 DOM 元素。特点是存储的值发生变化不会导致组件重新渲染。

forwardsRef 是函数,用于在函数组件间转发引用。 将父组件的 ref 转发到子组件中的 dom 元素上,解决函数组件不能直接接收 ref 属性的问题,使得父组件能够访问子组件内部的 dom 元素。 forwardsRef 必须配合 ref 属性使用。父组件传递的 ref 可以是 useRef 创建的 ref 对象,也可以是回调形式的 ref(如 (el)=> {...})。

回调式 ref 自动执行。

const Child = forwardsRef((props, ref)=>{
  return <input ref={ref} />;
});

const Parent() {
  const handleRef = (el) => {
    if (el) el.focus(); // 直接操作DOM
  }
  return <Child ref={handleRef} />; //当子组件的dom元素被挂载到页面时,将该dom元素作为el传入,并执行。
}

最常见的是 forwardsRef 与 useRef 一起配合使用。组件挂载之后 ref.current 被赋值为 DOM 元素。

const FancyInput = forwardRef((props, ref) => <input ref={ref} {...props} />);

function ParentComponent() {
  const inputRef = useRef(null);
  const handleClick = () => {
    inputRef.current.focus();
  };
  return (
    <>
      <FancyInput ref={intupRef} />
      <button onClick={handleClick}> Focus Input </button>
    </>
  );
}

对 immutable 的理解,如何应用到项目上 🔗

实现 useTimeout 的 hook 🔗

怎么获取函数组件的实例? 🔗

函数组件本质是纯函数,react 不会创建其实例。

  • forwardRef 让函数组件能接受父组件传递的 ref
  • useImperativeHandle 自定义通过 ref 暴露的内容。
import { useRef, useImperativeHandle, forwardRef } from "react";

const childComponent = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);
  const inputRef = useRef(null);

  const increment = () => {
    setCount((prev) => prev + 1);
  };

  useImperativeHandle(
    ref,
    () => ({
      getCount: () => count,
      triggerIncrement: increment,
      focusInput: () => inputRef.current.focus(),
    }),
    [count]
  );

  return (
    <div>
      <p>计数:{count}</p>
      <input ref={inputRef} />
    </div>
  );
});

const ParentComponent = () => {
  const childRef = useRef(null);
  const handleClick = () => {
    if (childRef.cuurent) {
      childRef.current.triggerIncrement();
      console.log("当前计数:", childRef.current.getCount());
      childRef.current.focusInput();
    }
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleClick}>操作子组件</button>
    </div>
  );
};

react 项目如何捕获错误? 🔗

分层捕获, 局部错误 try, catch。组件树错误用 ErrorBoundary。全局错误 unhandledrejection。

错误边界 error boundaries 可以捕获子组件树中的 js 错误,展示降级 UI(fallback UI),不让整个 UI 组件树崩溃。

受保护的路由 protected route 🔗

检查用户是否满足特定条件(如登录),才允许访问其下的组件和页面。

react 中引入 css 的方式有几种? 🔗

全局 css。创建 global.css 文件,在入口文件中引入 index.js,在组件中使用。

模块化 css。将 css 文件命名为文件名.module.css,使样式仅作用于引入它的组件。

Sass, SCSS 预处理器。

内联样式 style 属性上定义样式。

CSS 框架 Tailwind CSS。

单应用如何提高加载速度? 🔗

资源体积优化。 🔗

代码分割(code splitting), 将代码按路由或组件拆分,只加载当前页面所需的代码。原理: webpack 等构建工具会将动态导入的模块拆分为独立 chunk,访问对应路由时才加载。

压缩与混淆。jss,css 压缩, tree shaking,图片 webp,avif。

首屏渲染优化 🔗

SSR 通过 next.js 在服务端生成 HTML,首屏直接返回渲染好的内容。

运行时优化 🔗

避免长任务阻塞主线程。将复杂计算拆分到 web worker 中, 使用 requestIdleCallback 处理非紧急任务。

虚拟列表。当渲染大量数据(1000 条+列表),只渲染可视区域内的。

import { FixedSizeList } from "react-window";
function BigList({ items }) {
  return (
    <FixedSizeList
      height={500}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {({ index, style }) => <div style={style}> {items[index].name} </div>}
    </FixedSizeList>
  );
}

能否直接将 props 的值复制给 state? 🔗

可以将 props 的值复制给 state。

如果需要将 props 复制到 state 且保持同步, 需要用 useEffect 监听 props 变化并更新 state。

都用于导航。

特点 link 标签 a 标签
导航方式 不刷新页面 会刷新页面
URL 变化处理 html5 history api 改 url 触发页面跳转,发送新 http 请求

单页面应用 SPA 的核心是在一个页面中完成所有交互, link 标签为此而设计。 a 标签更适合外部链接,非 SPA 场景。

react 的 JSx 转换真实 DOM 的过程 🔗

react 的事件代理机制和原生事件绑定的区别? 🔗

react 中 Element, component,Node, Instance 🔗

React 元素是一个不可变的 javascript 对象,格式为{type, props: {attribute: "x", children: ""}, key, ref}

React 组件是函数,封装了 UI 逻辑和状态, 接收 props 并返回 React 元素。

React Node 更宽泛, 是渲染中的任何值,可以是 react 元素,字符串,数字, 数组等。 react 的 render 方法返回了 react node。

React Instance 类组件实例化后的对象,函数组件无实例。

全局状态管理方案 🔗

context是 react 内置的,但是 context 值变化时,所有消费者组件会重新渲染。

Redux, Zustand 的 api 简单上手快。

状态提升 🔗

虚拟列表的实现 🔗

代码分割在 react 中的实现方式 🔗

题目 🔗

react 的合成事件 🔗

React 为了屏蔽不同浏览器之间事件系统的差异,自己实现了一套“事件系统”, 在这套系统中,所有事件对象都不是浏览器原生事件对象,而是 React 封装的 合成事件对象 —— SyntheticEvent。

例如 在 JSX 里写 onClick={handleClick} 时,React 实际上不是直接绑定到 button 的原生 click 事件。

React 不在每个 DOM 上都绑定事件,而是把所有事件绑定在根节点(如 document 或 React Root)上,通过事件冒泡统一调度。

function App() {
  function handleClick(e) {
    console.log(e); // SyntheticEvent
    console.log(e.nativeEvent); // 原生事件
  }

  return <button onClick={handleClick}>点我</button>;
}

事件池。

填充 type, target, nativeEvent, preventDefault() 等属性;

react 源码 🔗

下载源码,查看 package.json,查看构建的脚本。

源码包 🔗

packages/react, packages/react-dom, packages/react-reconciler, packages/schduler

jsx 的编译 🔗

@babel/plugin-transform-react-jsx 编译为普通 js 代码, 如React.createElement的代码。

createElement

串流程 🔗

初始化流程, ReactDOM.render, createRoot

ReactElement 上有 ower 属性去指向 fiber

数据更新流程,setState,

scheduler 中的 minheap 实现优点队列的调度。

实现简单版的 reactfiber 🔗

const taskQueue = [];
const rootFiber = null;
const currentFiber = null;
// 创建第一个fiber
function createFiber(element, parentFiber) {
  return {
    element,
    parent: parentFiber,
    child: null,
    sibing: null,
  };
}

//
function updateFiberTree() {
  if (!rootFiber) {
    rootFiber = createFiber("app", null);
    currentFiber = rootFiber;
  }
  while (taskQueue.length > 0) {
    const task = taskQueu.shift();
  }
}

function performUnitOfWork(fiber) {
  // 执行当前任务
  const elements = fiber.element;
  if (elements && elements.length > 0) {
    let child = null;
    elements.forEach((element, index) => {
      const newFiber = createFiber(element, fiber);
      if (index == 0) {
        fiber.child = newFiber;
      } else {
        child.sibing = newFiber;
      }
      child = newFiber;
    });
  }
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibing) {
      return nextFiber.sibing;
    }
    nextFiber = nextFiber.parent;
  }
  return null;
  // 返回下一个任务
}

function commitRoot(finishWork: Fiber) void {
  commitWork(rootFiber.child);
  currentFiber = null;
}

function commitWork(fiber) void {
  if(!fiber) {
    return;
  }
  // 渲染
  console.log(fiber.element);
  commitWork(fiber.child);
  commitWork(fiber.sibing);
}

// 将任务添加到任务队列
function schduler(task) {
  taskQueue.push(task);
}

// 模拟任务
const exampleTask = {
  element: ["div", "p", "span"],
};

// 启动
schduler(exampleTask);
updateFiberTree();

从项目启动过程到状态更新过程(useState) 🔗

从零开始的 React 项目 Demo 🔗

src/
  main.jsx
  App.jsx

main.jxs 如下:

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

ReactDOM.createRoot(document.getElementById("root")).render(<App />);

App.jsx 如下:

import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>add</button>
    </div>
  );
}

React 项目启动过程(从加载 → Fiber 构建 → 首次渲染) 🔗

// 管理对象(FiberRoot)
const fiberRoot = {
  containerInfo: document.getElementById('root'), // React 挂载的 DOM 容器
  current: rootFiber,     // 指向当前的根 Fiber(两者互相引用)
  pendingLanes: 0,        // 调度相关的元信息(lane)
  callbackNode: null,     // 当前调度回调的引用
  finishedWork: null,     // render 完成后要 commit 的 fiber 链表
  // ...还有其他调度/队列字段
};

// 伪代码:Fiber 节点(简化)
const rootFiber = {
  tag: "HostRoot",        // 根节点类型
  child: ...,             // 子 Fiber 链表
  sibling: null,
  return: null,
  stateNode: null,        // <-- 会指向 FiberRoot(管理对象)
  updateQueue: {...},     // 根节点的更新队列(也可以放在 FiberRoot)
  ...otherFiberFields
};

// 两者互相关联
rootFiber.stateNode = fiberRoot;
fiberRoot.current = rootFiber;

下面按真实 React 内部流程来走,包括 performWork, beginWork, completeWork。

  1. 浏览器加载 main.jsx → 执行 ReactDOM.createRoot()

createRoot创建 RootFiber

  1. root.render() → 创建 Update 对象,放入 UpdateQueue

render() 并不是立即渲染,是:为 RootFiber 创建一个更新(Update),把 作为 pending update,调度一个任务,让 React 的 scheduler 在空闲/高优先级时执行,然后才会进入:performConcurrentWorkOnRoot

  1. 进入 Fiber 构建(渲染阶段) render phase(可中断),构建 Fiber Tree(虚拟 DOM 的 Fiber 树)。 commit phase(不可中断)把 Fiber 变成真实 DOM。

render 阶段 — performUnitOfWork

根 Fiber 开始:

performUnitOfWork(rootFiber)
  → beginWork
  → return child (App Fiber)
performUnitOfWork(AppFiber)
  → beginWork
  → return child (div Fiber)
performUnitOfWork(divFiber)
  → beginWork
  → return child (p Fiber)
...

beginWork 做什么?根据 props/状态 生成子 Fiber。 beginWork(AppFiber)

a, 执行 App()

b,进入 React hooks dispatcher

c,useState(0) 产生:hook 对象(链表),初始 state = 0

d,返回 React Element 结构:

<div>
  <p>0</p>
  <button>add</button>
</div>

f,React 根据 element 生成子 Fiber

completeWork 做什么?每个 Fiber 的 completeWork 会处理:创建 DOM 节点(首次渲染),收集 effectList(要执行的 DOM 操作)。

最终 rootFiber 会带着一串 DOM effectList 进入 commit 阶段。

commit 阶段(不可中断,也是唯一操作 DOM 的地方) 🔗

三个小阶段:before mutation(ref 清理等),mutation(把 DOM 插入页面!最关键),layout(调用 useLayoutEffect)。

最终浏览器看到页面中的:

0
[add]

React Hooks 的核心实现机制 🔗

ReactFiberHooks.js

ReactSharedInternals.H =
  current === null || current.memoizedState === null
    ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;

所有 useState、useEffect、useMemo 等 hook 都是通过ReactSharedInternals.H来调用的。

每个函数组件 Fiber 节点上都有:fiber.memoizedState, 存放了类似useState → useEffect → useMemo → useRef → ...

hook 节点如下:

{
  memoizedState: 0,          // 对 useState = 初始值
  queue: {...},              // 对 useState = updateQueue
  next: HookNode,            // 下一个 hook

  baseState: any,
  baseQueue: Update<any, any> | null,
}

React 为什么“只能在顶层调用 hook”, 每次 render 为什么所有 hook 必须按顺序调用?

✔ 初次渲染(HooksDispatcherOnMount)

  • 为每个 hook 创建 fiber.memoizedState 节点(hook 链表)
  • 为 useState 创建新的 state、queue
  • 为 useEffect 建立 effect 链表
  • 为每个 hook 分配一个“链表节点”

✔ 更新渲染(HooksDispatcherOnUpdate)

  • 不再创建新节点,而是“按顺序取出”旧的 hook 节点
  • useState 取出旧 state 并应用 update 队列
  • useEffect diff 依赖并决定是否重新执行 effect
  • mount / update 时为什么有两个不同的 dispatcher

current.memoizedState 是怎样保存 useState 状态链表的

React 状态更新过程(useState)原理 🔗

当点击onClick={() => setCount(count + 1)}时,触发 setCount 的内部流程。

  1. setState 不是立即更新 State,它创建 Update 对象。

setCount(newValue) 实际上做的是:

  • 创建更新对象 { payload: newValue }
  • 加入 Fiber.updateQueue
  • 调度 render 任务

状态并没有立即变。

  1. 调度器(Scheduler)开始一次新的渲染任务 scheduler 再次启动:
performConcurrentWorkOnRoot
   render phasebeginWork  completeWork
   commit phase

更新阶段与首次渲染的区别 🔗

首次渲染 beginWork(FunctionComponent)

  • 运行 App()
  • 创建 hook 链表
  • 记录 initialState

更新时 beginWork(FunctionComponent)

  • 运行 App()
  • 遍历 hook 链表
  • useState 会:oldState + updateQueue → newState. 每次 setState 都会让 updateQueue 参与计算。

commitRootImpl 🔗

整个渲染流程的“最后一步”—— Commit 阶段的核心入口。要完成所有 DOM 更新、同步/异步 Effect、生命周期回调、commit 事件等。

x

完整总结流程 🔗

首次启动:

createRoot → 创建 rootFiber

root.render() → 为 rootFiber 创建 update

Scheduler 调度任务

render phase, beginWork:执行 App(),构建 Fiber, completeWork:创建 DOM

commit phase:把 DOM 插入页面

状态更新:

setState → 创建 update → 放进 updateQueue

Scheduler 调度 render

render phase, beginWork:重新执行 App(), useState 读取旧 state + update 得到 new state, 生成新的 Fiber

commit phase, 对比 fiber → 更新 DOM(只更新变化的地方)

react 中涉及到的核心算法和设计 🔗

Diff 算法在reconcileChildrenArray, 只进行同层对比,不同类型的元素产生不同的树reconcileSingleElement,通过 key 稳定标识mapRemainingChildren

在工程上对 web 应用的观察发现: 很少出现 DOM 元素跨层级移动;如果改变了组件类型,UI 的内容发生根本性变化,没必要细比下去。

Fiber 架构,模拟任务切片的堆栈。 把 react 元素变为工作单元(就是一个个 Fiber), 支持中断和恢复, 可以执行 5ms 的渲染逻辑,之后把控制权还给浏览器。

双缓存机制,同时存在 current tree 和 workInProgress tree, 所有的 diff 和计算在 workInProgress 上完成,之后一次替换到屏幕上。

参考 🔗

react 的官网 doc