☰
手写类React渲染引擎:从零实现虚拟DOM与状态驱动更新
2026/10/21 4:08:51 网站建设 项目流程

1. 为什么我要手动实现一个类 React 的核心

在正式开始之前,先澄清一下项目标题:“rea”。这不是拼写错误,也不是某个开源库的名字。它是我自己定义的一个轻量级前端渲染核心的代号,取“React-like Engine Architecture”的首字母缩写。简单来讲,我利用业余时间实现了一个简化版的类 React 渲染引擎,包含虚拟 DOM、组件化、状态驱动更新这一套核心机制。

这个项目要做的事非常纯粹,就是不依赖任何现成框架,从零实现一套“数据变了,界面自动跟着变”的渲染机制。它能做组件化拆分、状态管理、批量更新、事件绑定,也能解释为什么 React 需要 setState、为什么函数组件要遵循特定规则。适合对框架内部机制好奇的中高级前端开发者,也适合想给自己团队搭建轻量级内部工具、但又不想直接引入庞大框架的架构师参考。

为什么要做这件事?我个人的观点很简单:框架用多了以后,很多概念容易变成“背下来的结论”。比如“虚拟 DOM 转换为真实 DOM 的性能更高”、“不可变数据配合 diff 算法效率更好”,如果你没有亲手实现过一遍,这些结论就是空中楼阁。用一个最小但完整的技术栈,把 React 的核心原理亲手复刻一遍,这套思维模型一旦建立,以后不管是读源码,还是排查生产环境里奇怪的渲染问题,思路都会清晰很多。

2. 整体设计思路与核心架构拆解

2.1 项目定位:做一个“能用”的引擎,而不是一个“可用”的框架

任何从零开始的项目,最怕的就是目标定义得太宽泛。如果我想做一个完整的 React 替代品,那可能需要面对并发渲染、优先级调度、Suspense、异步组件等一系列庞杂工程问题,项目周期完全可以按年计算。我给自己定的目标是:做一个能够在真实浏览器里跑起来、支持组件化和状态更新、代码量控制在合理范围之内的“可运行模型”。

这意味着我在设计阶段就必须做一些取舍。如下几个能力我非常确定一定要有:

  • 函数组件与类组件。函数组件承担 UI 描述,类组件承载生命周期和复杂状态。
  • 虚拟 DOM 与 diff 算法。状态变更时,通过虚拟节点对比,最小化真实 DOM 操作。
  • 状态管理机制。实现一个类似 useState 或 setState 的核心,保证状态变化后能触发精确更新。
  • 事件系统。统一的事件绑定与合成事件机制,至少在内部保证事件的冒泡处理符合直觉。

明确“不做什么”和明确“要做什么”同样重要。我没有去实现 Context、Ref、Suspense 这些特性,也没有去实现 React 内部的并发可中断渲染。这不是偷工减料,而是刻意控制复杂度。先把最核心的“状态到视图”这一环打通,后续的能力都基于这个稳定的核心来扩展。

2.2 模块拆解:四个核心模块的职责边界

整个项目的架构,我按照功能边界拆成四个互相独立、依赖方向明确的模块。这样做的好处是:任何一个模块内部逻辑改动,都不需要牵连其他模块。比如我后来想优化 diff 算法的匹配策略,只需要动渲染协调器,组件装载器完全无感知。

  • 组件模型模块:定义组件类、元素描述对象(类似 React Element)、组件实例的管理。类组件挂载实例时,需要维护实例上的 props 和 state;函数组件则被看成一种特殊类型,渲染结果就是它的返回值。
  • 渲染协调器模块:负责任务调度、虚拟 DOM 的创建、差异计算、真实 DOM 的创建与更新。这是整个引擎的指挥中枢。
  • 状态管理模块:处理更新批量的入队与执行、状态变更后触发重渲染的流程。保证多个连续 setState 只触发一次视图更新,是这个模块的核心目标。
  • 事件协同模块:事件绑定、事件派发流程。简化版本不实现 React 那样复杂的合成事件池,但在根容器上做事件委托,从而让动态新增的节点也能自动响应事件。

这四个模块的依赖关系是单向的:组件模型事件协同模块被渲染协调器调用,渲染协调器被状态管理模块触发。上层模块通过接口调用下层,下层不感知上层存在。用实际开发中的一句话总结:底层不 import 底层,错误不跨层冒泡。

2.3 为什么选择“不可变数据 + 浅比较”这条路

在设计状态更新机制时,我在“可变数据 + 深度监听”和“不可变数据 + 浅比较”之间犹豫过。前者的实现直觉上更简单:直接修改数据,然后触发一个全局更新事件就行。但很快我发现一个问题:当你对象里嵌套了多层属性时,必须深度遍历一次才能确定到底哪里变了。数据量一大,这种遍历开销非常可观。

后者则是空间换时间的典型案例。每次更新状态时,生成一个新的对象/数组引用,旧引用原封不动。这样在 diff 过程中只需比较引用地址是否变化,就能快速判断某个组件是否需要重新渲染。React 的快速跳过不需要的子树,靠的就是这一套机制。

我在这次实现里也完全走了不可变路线:

// 不推荐:直接修改原状态 state.list.push(newItem); setState(state); // 推荐:产生新的引用 setState(prev => ({ ...prev, list: [...prev.list, newItem] }));

从底层视角看,第二种写法的优势立竿见影:prev.list与next.list的引用不相等,说明列表变了;而如果prev.other和next.other的引用相同,diff 算法就可以完全跳过那棵子树。一个浅比较就完成判断,不需要知道内部具体如何变化。

2.4 从 React 中学到的设计教训

老实说,React 源码里最让我觉得巧妙的设计,不是虚拟 DOM 或者 diff,而是“更新不是一个瞬间动作,而是一个被调度的过程”。同样一个 setState,它不会立刻执行重渲染,而是进入一个待处理队列,在当前任务结束之后统一下载。这带来两个直观收益:同一事件里多次 setState 被合并成一次渲染;diff 计算被延迟到浏览器有空闲时执行,避免阻塞交互。

我在 rea 的时间片调度借鉴了同样的思路。每次状态更新,我就向调度器注册一个更新任务,调度器维护一个任务队列。在这个微任务或者宏任务的回调里,统一执行所有待处理的更新:

class Scheduler { constructor() { this.pending = false; } schedule(updateFn) { this.updateFn = updateFn; if (!this.pending) { this.pending = true; Promise.resolve().then(() => { this.pending = false; this.updateFn && this.updateFn(); }); } } }

这段代码本身很简单,但它完整的解释了一个核心概念:为什么在同一个事件处理函数里连续调用三次 setState,最终页面只渲染一次。理解了这一点,后面遇到“状态不实时更新”这类问题时,你就知道不是 bug,而是框架的正常调度策略。

3. 核心细节解析与关键机制实现

3.1 虚拟 DOM 与 diff 算法的实现要点

虚拟 DOM 是什么?我的理解是:它是真实 UI 结构的轻量级描述,就是一个 JavaScript 对象。真实节点有庞大的属性和方法,而虚拟节点只需要记录标签类型、属性、子节点列表。为什么要这层描述?因为它让我们能够在纯 JavaScript 层面进行高效的对比计算,执行算法时才操作真实 DOM,减少浏览器布局和绘制的次数。

我的虚拟 DOM 结构里只包含三个必须字段:

function createElement(type, props, ...children) { // 过滤掉 null、undefined、布尔值,这些在渲染时没有意义 const filteredChildren = children .flat() .filter(child => child !== null && child !== undefined && child !== false); return { type, // 标签名,如 'div',或者组件函数/类 props: props || {}, children: filteredChildren }; }

这里有一个比较容易忽略的细节:当type是字符串时,它代表原生标签;当type是函数时,它代表函数组件;当type是类时,它代表类组件。这样三种完全不同的渲染路径就能在同一个 createElement 函数上统一起来。

diff 算法的核心逻辑,我把它拆成三个维度的对比:

  • 节点类型不同:直接替换整个节点,不再深入比较子节点。
  • 标签名不同:直接替换;标签名相同则更新属性。
  • 子节点序列:这里最容易踩坑。简单版可以对子节点进行双端对比,但为了控制代码复杂度,我采用了基于 key 的比对策略。有 key 就按 key 找到对应旧节点;没 key 则按顺序一一对应。

为什么要用 key?这是我的切身教训。最开始我天真地认为 diff 完全可以不用 key,按位置对比就行。直到我写了一个带条件的列表渲染,发现当数组中某项被删除后,后续所有项的位置都变了,结果是每个节点都重建了一遍,性能非常差。加上了 key 之后,绝多数节点可以被复用,只有真正变化的节点才被处理。

属性更新也有一个经典坑点:className、style、事件绑定、value 这些特殊属性不能直接 setAttribute。其中 style 需要单独解析成对象逐项赋值;事件需要走事件委托;受控组件的 value 赋值需要特殊处理。

我总结的 props 更新流程如下:

function updateDOMProperties(domNode, oldProps, newProps) { // 1. 移除旧属性(遍历旧 props) // 2. 更新常规属性 for (let propName in newProps) { if (propName === 'children') continue; if (propName === 'style') { // 遍历 style 对象,逐项赋值 } else if (propName === 'className') { domNode.className = newProps[propName]; } else if (propName.startsWith('on')) { // 委托给根容器处理,此处只记录回调 } else { domNode.setAttribute(propName, newProps[propName]); } } }

3.2 函数组件与类组件的平等对待

在初学者眼里,函数组件和类组件仿佛是两种物种。但从引擎的角度看,它们有一个共同的抽象:组件描述对象。无论是函数组件还是类组件,引擎关心的只是“你的渲染输出是什么”,你的入参是 props,你的产出是虚拟 DOM 节点。

函数组件的处理是最直观的:

function renderWithHooks(componentFunction, props) { // 设置当前正在渲染的组件实例,供 hooks 使用 currentlyRenderingFiber = new FiberNode(componentFunction, props); // 清空 hook 链表 currentHookIndex = 0; // 执行函数组件,获得虚拟 DOM const output = componentFunction(props); return output; }

类组件的处理多一步实例化流程。类组件不能被直接执行,需要 new 一个实例出来,设置 props,然后调用实例上的 render 方法:

class Component { constructor(props) { this.props = props; this.state = {}; } setState(partialState) { // 推入更新队列,由状态管理层处理 enqueueUpdate(this, partialState); } render() { throw new Error('子类必须实现 render 方法'); } }

引擎在处理类组件时,第一个问题是如何保证同样的组件实例被复用,而不是在每次渲染时重新 new 一个。答案是用一个“组件实例缓存表”。当同一个组件在多次渲染中反复出现时,优先从缓存表里取出旧实例,更新 props 后直接调用 render。

每个框架都有自己的生命周期设计,我简化后保留了三个核心节点:组件挂载前、组件挂载后、组件更新后。为什么只保留这三个?因为它们覆盖了日常开发里 90% 的场景。所谓生命周期,本质上就是“某个特定时间点会被框架自动调用的钩子”。了解了引擎如何在几个时机调用这些钩子,再看各种框架的生命周期都一目了然。

3.3 状态管理与更新队列的批量执行

状态管理是整个项目里最贴近生产实践的部分。用户连续触发多次 setState,页面不能每次更新。我实现的一个更新计数器 + 批量处理标记方案如下:

let isBatchingUpdates = false; const updateQueue = []; function enqueueUpdate(component, partialState) { updateQueue.push({ component, partialState }); if (!isBatchingUpdates) { isBatchingUpdates = true; // 利用微任务批量执行,或者用 requestAnimationFrame scheduleBatchFlush(); } } function flushBatchUpdates() { updateQueue.forEach(({ component, partialState }) => { // 合并 partialState 到 component.state // 标记组件需要更新 }); // 收集所有需要更新的根节点,执行渲染 updateQueue.length = 0; isBatchingUpdates = false; performRootRenders(); }

这也就是将级联更迭串行的基础。你 setState 之后拿到的 state 没有变,不是被吞了,而是还在队列里,等待统一的合并和提交。

这里还有一个细节值得展开:状态更新的合并逻辑。React 的 setState 既支持直接传对象,也支持传函数。函数写法能解决连续更新依赖于前值的问题。比如计数器要连续加三次,直接传对象的写法只会生效最后一次,而传函数的写法能把三次操作依次合并:

// 错误的直观写法:三次都基于同一个旧值 setState({ count: this.state.count + 1 }); setState({ count: this.state.count + 1 }); setState({ count: this.state.count + 1 }); // 正确的函数式写法:每次基于上一次的结果 setState(prev => ({ count: prev.count + 1 })); setState(prev => ({ count: prev.count + 1 })); setState(prev => ({ count: prev.count + 1 }));

我的实现里面对这两种写法都做了兼容,用Object.assign处理对象,用函数累加处理函数。这个逻辑用不了 20 行代码,却要求你真正理解状态更新的时序。

3.4 手写 useState 与自定义 Hooks 的实现

如果说组件和状态机制是骨架,那 Hooks 机制就是关节。我最初的实现里没有 Hooks,只用了类组件。但后来对比了一个简单计数器在函数组件和类组件中的代码量后,我发现函数组件不增加状态管理能力,表达力始终受限。于是痛下决心,在引擎里加入了一个精修版 useState。

它的工作原理其实非常像一个“闭包 + 链表”的组合。每次渲染时,hooks 链表的顺序决定了你拿到的 state 是谁。这就是为什么 Hooks 不能在条件语句或循环里调用,顺序一旦被打乱,链表就串位了。

let currentlyRenderingFiber = null; let hookIndex = 0; function useState(initialValue) { const fiber = currentlyRenderingFiber; // 旧节点上已经存在对应 hook,就复用;否则新创建一个 let hook; if (hookIndex < fiber.hooks.length) { hook = fiber.hooks[hookIndex]; } else { hook = { state: initialValue, queue: [] }; fiber.hooks.push(hook); } // 处理本次渲染前累积的更新 while (hook.queue.length > 0) { const action = hook.queue.shift(); hook.state = typeof action === 'function' ? action(hook.state) : action; } const currentHookIndex = hookIndex; hookIndex++; return [ hook.state, (action) => { hook.queue.push(action); scheduleRender(fiber); } ]; }

这段代码让我第一次真正理解了为什么 React 文档要反复强调 hooks 的调用规则。不是框架不想让你灵活,而是引擎的机制决定了灵活性会破坏状态与调用位置的绑定关系。写出自定义 Hook 时,也要遵循同样的顺序原则,本质上是把自己要用的 useState 内聚到一个函数里,由那个函数统一调用,不打断 hooks 链表。

4. 实操过程:手工从零搭建渲染引擎的全流程

4.1 搭建基础渲染管线(从 JSX 到真实 DOM)

虽然标准的 JSX 编译过程可以借助 Babel 或者所谓“新的编译方案”处理,但为了理解本质,我首先写了一个最简的 createElement 函数,并在入口处用它替代 JSX。也就是说,我直接从虚拟 DOM 对象入手,建立一条“虚拟对象 → 真实节点”的最短渲染路径。

第一步是 render 函数。它的输入是虚拟节点,输出是挂载到指定容器里的真实 DOM:

function render(vnode, container) { if (!container._root) { // 首次挂载 container._root = createDOMNode(vnode); container.appendChild(container._root); } else { // 更新阶段,走 diff updateDOMNode(container._root, vnode); } } function createDOMNode(vnode) { if (typeof vnode.type === 'function') { // 函数组件 return createDOMNode(vnode.type(vnode.props)); } if (typeof vnode.type === 'string') { const domNode = document.createElement(vnode.type); // 设置属性 for (let [key, value] of Object.entries(vnode.props || {})) { if (key === 'children') continue; if (key === 'className') { domNode.className = value; } else if (key.startsWith('on')) { const eventType = key.toLowerCase().substring(2); attachEvent(domNode, eventType, value); } else { domNode.setAttribute(key, value); } } // 递归处理子节点 vnode.children.forEach(child => { if (typeof child === 'string' || typeof child === 'number') { domNode.appendChild(document.createTextNode(child)); } else { domNode.appendChild(createDOMNode(child)); } }); return domNode; } throw new Error('未知节点类型'); }

从这段简单的代码里能够看出,渲染的本质不是魔法,而是“类型分支判断 + 递归”。你写 JSX 时那些看上去很神奇的标签,最终不过是某个函数对不同输入类型的处理。真实开发中你会发现,理解这条链路的映射关系,对排查 JSX 报错或模板调试都极其有帮助。

4.2 实现组件的实例管理与生命周期挂钩

有了基础渲染管线之后,我把组件模型插进去。函数组件已经能直接通过执行函数得到输出,类组件则需要多一个实例缓存机制:

class ComponentNode { constructor(ComponentClass, props) { this.ComponentClass = ComponentClass; this.instance = new ComponentClass(props); this.vnode = null; this.domNode = null; this.hooks = []; // 函数组件用 } render() { this.vnode = this.instance.render(); return this.vnode; } }

生命周期挂钩的插入点,我用一张表记住了每个勾子应该被调用的时机:

时机调用点常见用途
挂载前组件实例化后、首次 render 前初始化数据、设置初始状态
挂载后真实 DOM 创建完成、插入页面后发起数据请求、操作真实 DOM、绑定额外监听
更新后状态变化触发重渲染、diff 完成后数据同步、根据 prop 变化触发额外逻辑

一个比较常被忽略的问题:类组件的 setState 和函数组件的 useState 在引擎内部最终都指向同一个调度入口。所以两者的更新流程完全一致,只是在“状态存放在哪里”上不同。理解这一点,你就能解释为什么在类组件里写函数组件式 hooks 会报错,因为 hooks 链表的载体是 FiberNode,状态存寄的宿主不对。

4.3 挂载事件系统:事件委托与批量更新联动

浏览器里的原生事件绑定对动态生成的节点很不友好。每次新增一个节点,你都要手动去 addEventListener。为此我实现了事件委托:所有事件都绑定在应用根容器上,真正触发时,通过事件的 target 属性向上查找回调函数。

这一做法的优势非常明显:不需要关心节点何时被创建或销毁,只需保证根容器的事件监听始终存在。事件对象在冒泡过程中向上遍历时,我们把对应的事件处理函数取出来,放进同一个批量更新环境中执行。

function setupRootEventDelegation(rootDOMNode) { const eventRegistry = {}; rootDOMNode.addEventListener('click', (e) => { // 向上回溯,收集所有绑定了 click 回调的组件 let currentTarget = e.target; const handlers = []; while (currentTarget && currentTarget !== rootDOMNode) { if (currentTarget._eventHandlers && currentTarget._eventHandlers.click) { handlers.push(currentTarget._eventHandlers.click); } currentTarget = currentTarget.parentNode; } // 在批量更新环境里依次执行 batchUpdate(() => { handlers.forEach(handler => handler(e)); }); }); return eventRegistry; }

事件委托和批量更新的联动,是生产项目里状态更新性能的关键。在一个事件回调里,你可能既调用了组件的 setState,又调用了自定义的更新函数。如果没有批量环境,每个 setState 都会立刻触发一次渲染,性能极差。而批量环境把多次更新收集到一次渲染里执行,整体减少了不少无用的 diff 计算。

4.4 从零开始的 demo 验证:计数器到 TodoList

当我搭建完基础渲染和状态机制后,做的第一个验证 Demo 就是计数器。计数器虽然看起来简单,但它能完整覆盖渲染、事件、状态更新的全过程。步骤如下:

  1. 根容器上通过 render 挂载计数器组件。
  2. 组件 render 函数返回一个 div,包含显示区域和两个按钮。
  3. 按钮通过 onClick 事件回调调用 setState 更新计数。
  4. 状态更新触发重渲染,新的数值出现在页面。

计数器跑通后,我紧接着写了 TodoList。TodoList 更有挑战性,因为它包含列表的增删、过滤、条件渲染。这部分需要把 key 的机制用上,测试 diff 算法在列表变化时的表现。实践下来的结论是:只要 key 设置正确,列表项删除后其他项不会重建;没有 key,整个列表就会重新渲染,页面上肉眼可见的闪烁就是证据。

5. 踩坑记录与问题排查实录

5.1 状态更新不触发重渲染的根因

项目前期遇到最多的问题,就是明明调用了 setState,视图却不更新。排查下来发现多半是两种原因:一是直接修改了 state 对象内部属性但没有变更对象引用;二是更新时用了旧的组件实例引用,导致调度器找不到目标组件。

排查这种问题的思路其实很清晰。先看 setState 有没有进入更新队列,再看渲染调度有没有被触发,最后看 diff 算法有没有认为“数据没变”。大部分时候问题出在不可变数据这个环节:开发者习惯了可变数据的心智模型,很容易写出state.items.push(newItem)这类代码。在框架层面,这种写法并没有产生新引用,浅比较判定没变化,最终视图自然不更新。

5.2 diff 算法导致节点复用时状态丢失的现象

另一个比较揪心的问题:当列表重新排序后,某些输入框里的临时状态丢失了。我第一反应是代码写错了,但深究以后发现这是 diff 策略的默认行为。如果没有 key,新旧列表按位置对应,如果位置没有变化,节点直接复用;但如果位置变了,旧节点被移走,新节点在原位置生成,原来节点的内部状态就丢了。

由此我总结出一条核心经验:只要列表里的数据是有唯一标识的,永远不要省略 key。省掉的不是工作量,而是节点复用的准确性。对于用户输入类的列表项,key 的作用更是不可替代,它能保证输入状态跟随数据项身位走,而不是跟随数组下标走。

5.3 事件响应异常与批量更新环境的关联问题

在实现事件委托后,有一个 bug 困扰了我比较久:快速连续点击两个不同按钮时,第二次点击有时不触发更新。最后定位发现,是批量更新环境被嵌套调用导致的问题。

我最初的实现里,每次事件都会创建新的批量环境,而在批量更新尾部调用渲染函数时,如果渲染过程又触发了事件或者新的更新,环境就会被意外重置,导致队列丢失。修复方式很简单:引入一个批量更新的深度计数器,只在最外层批量结束时才执行刷新。

let batchDepth = 0; function batchUpdate(fn) { batchDepth++; try { fn(); } finally { batchDepth--; if (batchDepth === 0) { flushUpdateQueue(); } } }

这段修复代码解释了一个非常实用的生产经验:如果在一个批量更新里又触发另一个批量更新,内部的更新一定不要提前提交,而是等外部批量更新结束后统一处理。这也解释了 React 文档里为什么一再强调不要在渲染阶段去触发 setState——它会打乱批量更新的提交时序。

5.4 性能优化的最小有效手段

项目收尾后,我又做了一轮性能优化。最开始推进的一大步是大范围引入 shouldComponentUpdate 类似的过滤机制。后来发现一个更粗暴有效的方法:在每一次 diff 计算之前,先判断当前节点的 props 是否与上一次完全相等。如果引用相等,说明没变,直接跳过整棵子树的对比。

function shouldMemoize(prevProps, nextProps) { if (prevProps === nextProps) return true; // 浅比较一层 const prevKeys = Object.keys(prevProps); const nextKeys = Object.keys(nextProps); if (prevKeys.length !== nextKeys.length) return false; return prevKeys.every(key => prevProps[key] === nextProps[key]); }

这个浅比较的手段代价极低,但对于大部分场景来说已经足够。它不能代替完整的细粒度收集更新,但优化效果立竿见影。特别是对于列表项这种频繁复用的组件,浅比较能够拦截掉大量不必要的子节点重建。

6. 项目复盘与实际开发中的经验延伸

6.1 rea 与成熟框架之间的心意相通

如果把我做的 rea 看作一件手工作品,那 React 就是经年打磨的工业品。两者的核心技术栈是同构的,差异主要体现在并发渲染、优先级调度、错误边界、Suspense 等更多高级特性。但对我个人而言,手工实现一次的价值,远超阅读十篇源码解析。

为什么这么说?因为当你自己动手跌跌撞撞实现过一遍之后,再看框架文档里的心智模型,你会知道你正在调用的每一个 API 背后正在发生什么。当setState的合并更新、hooks 的顺序约束、key 对 diff 的影响变成你亲手写过、调过错、修复过的经验,再碰见任何框架层面的诡异问题,都能快速缩小排查范围。

6.2 从项目推导到日常业务的三个可用结论

第一,任何场景下尽量使用不可变数据,这不仅是框架层规范化要求,同时是避免页面上出现隐性问题的最简单手段。大家在协作过程中,如果有人写过直接修改 state 内部属性的代码,数个小时的排查时间就搭进去了。第二,列表渲染里的 key,要当作与代码逻辑同等重要的正确性要素来对待。第三,批量更新和事件系统的协同关系,决定了性能瓶颈往往不在框架而在使用方式。事件里一次更新多个状态是最经济的行为,拆成多次同步更新反而更浪费。

6.3 后续可以继续扩展的方向

rea 当前还没有实现 Concurrent 模式那样的时间切片。如果后续继续推进,我可能会把渲染任务拆成可中断的小份,在每个时间片内处理部分更新,并同步检查是否存在更高优先级的用户交互。这是现代框架最核心的复杂度来源,也是“调度”这个概念真正发光的地方。

另外,我还会把错误边界加到组件树里,保证某个子组件渲染异常时不会拖垮整个应用。成熟的框架已经给了我们巨大的便利,但理解底层原理带来的掌控感,是任何便捷工具都无法替代的。我强烈建议每一个在前端领域长期发展的开发者,都可以花一段时间做一次类似的“慢功夫”,把常用框架的某个子集自己实现一遍。慢,其实才是快。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询