React Fiber 架构中的原型模式:为什么这种设计如此重要

摘要:如果你曾经好奇 React 如何高效地更新界面,或者想知道为什么 React 性能这么好,那么这篇文章会给你答案。今天我们要聊的是 React 核心架构中一个重要的设计模式——原型模式。

如果你曾经好奇 React 如何高效地更新界面,或者想知道为什么 React 性能这么好,那么这篇文章会给你答案。今天我们要聊的是 React 核心架构中一个重要的设计模式——原型模式。


什么是原型模式?

先来看一个生活中的例子。假设你要制作一批相同的椅子,最直接的方法是每把椅子都从头开始制作。但更聪明的方法是先做一个完美的样品,然后基于这个样品快速复制。

在编程中,原型模式就是这个思路:创建一个模板对象,然后通过复制这个模板来创建新对象,而不是每次都重新构建。


原型模式解决了什么问题?

想象你在开发一个棋盘游戏。棋盘有 64 个格子,每个格子都有颜色、位置和棋子信息。

最直接的写法是这样的:

class BoardSquare {
  constructor(color, row, file, piece) {
    this.color = color;      // 颜色
    this.row = row;          // 行号
    this.file = file;        // 列号
    this.piece = piece;      // 棋子
  }

  occupySquare(piece) {
    this.piece = piece;
  }

  clearSquare() {
    this.piece = null;
  }
}

// 创建64个格子
const squares = [];
for (let i = 0; i < 8; i++) {
  for (let j = 0; j < 8; j++) {
    const color = (i + j) % 2 === 0 ? 'white' : 'black';
    squares.push(new BoardSquare(color, i, j, null));
  }
}

这种方法有个问题:如果后来需要给格子添加新属性,比如背景图片或者特殊样式,就需要修改所有创建格子的代码。这很容易出错,而且效率不高。


原型模式的解决方案

使用原型模式,我们可以这样做:

class BoardSquarePrototype {
  constructor(prototype) {
    this.prototype = prototype;
  }

  clone() {
    return Object.assign(new BoardSquare(), this.prototype);
  }
}

// 创建模板
const whiteSquareTemplate = new BoardSquare('white', null, null, null);
const prototype = new BoardSquarePrototype(whiteSquareTemplate);

// 快速复制
const square1 = prototype.clone();
const square2 = prototype.clone();

这样做的好处很明显:

  • 修改模板就能影响所有复制的对象

  • 避免重复的初始化代码

  • 代码更易于维护


JavaScript 中的原型机制

需要注意的是,JavaScript 语言本身就有原型机制,但这和我们讨论的设计模式不太一样。

JavaScript 的原型继承:

const squareTemplate = {
  color: 'white',
  row: 0,
  file: 0,
  piece: null,

  occupySquare(piece) {
    this.piece = piece;
  }
};

// 创建基于原型的对象
const square1 = Object.create(squareTemplate);
const square2 = Object.create(squareTemplate);

这里的区别很重要:

  • Object.create() 建立的是原型链关系,对象共享同一个原型

  • 原型模式的 clone() 是创建完全独立的新对象


React Fiber 中的原型模式

React 使用 Fiber 架构来管理组件更新。每次界面需要更新时,React 都会创建新的 Fiber 树。但如果每次都完全重新创建,性能会很差。

React 的解决方案就是使用原型模式的思想。我们来看看简化后的代码:

function createFiber(tag, pendingProps, key, mode) {
  return {
    tag,
    key,
    elementType: null,
    type: null,
    stateNode: null,
    pendingProps,
    memoizedProps: null,
    // ... 很多其他属性
    alternate: null,  // 关键指针
  };
}

function createWorkInProgressFiber(current) {
  let workInProgress = current.alternate;

  if (workInProgress === null) {
    // 首次创建,使用 Object.assign 快速复制
    workInProgress = Object.assign(new FiberNode(), current);
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    // 复用现有对象,只更新必要属性
    workInProgress.pendingProps = current.pendingProps;
    workInProgress.type = current.type;
    // ... 更新其他变化的属性
  }

  return workInProgress;
}

这个设计的巧妙之处在于:

  • 第一次更新时快速复制现有 Fiber

  • 后续更新时复用对象,只修改变化的属性

  • 通过 alternate 指针连接两棵树,实现双缓冲


其他库中的应用

Redux 的 immer 库也使用了类似的思想:

// 创建草案对象
const draft = Object.assign(Object.create(base), base);

// 修改草案
draft.user.name = '新名字';

// 最终生成新状态
const nextState = finalizeDraft(draft);

相比深拷贝,这种方法大大减少了内存使用。


实际应用场景

假设我们要管理应用配置:

传统方式(效率低):

const createConfig = () => ({
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retry: 3,
  headers: { 'Content-Type': 'application/json' }
});

// 每次都要重新初始化所有属性
const configs = Array(100).fill().map(() => createConfig());

使用原型模式:

class ConfigFactory {
  constructor(template) {
    this.template = template;
  }

  clone(overrides = {}) {
    return Object.assign({}, this.template, overrides);
  }
}

const baseConfig = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

const configFactory = new ConfigFactory(baseConfig);

// 快速创建
const config1 = configFactory.clone();
const config2 = configFactory.clone();

// 创建变体
const devConfig = configFactory.clone({ apiUrl: 'http://localhost:3000' });

性能对比:

  • 传统方式:100 个配置 × 5 个属性 = 500 次赋值

  • 原型模式:使用 Object.assign 优化,性能提升明显


需要注意的问题

使用 Object.assign 进行浅拷贝时要注意:

const template = {
  user: { name: 'Alice' },
  tags: ['js', 'react']
};

const copy1 = Object.assign({}, template);
const copy2 = Object.assign({}, template);

// 问题:嵌套对象是共享的
copy1.user.name = 'Bob';
console.log(template.user.name);  // 也变成了 'Bob'

解决方案:

// 需要完全独立时使用深拷贝
import _ from 'lodash';

class ConfigFactory {
  constructor(template) {
    this.template = template;
  }

  // 浅拷贝(性能好)
  clone() {
    return Object.assign({}, this.template);
  }

  // 深拷贝(完全独立)
  deepClone() {
    return _.cloneDeep(this.template);
  }
}


实用建议

在实际项目中:

  • 大多数情况使用浅拷贝就够了

  • 只有在需要完全隔离时才使用深拷贝

  • 考虑使用现成的工具库,如 lodash 的 cloneDeep


完整的实用示例

这里是一个可以在项目中直接使用的对象工厂:

class ObjectFactory {
  constructor(template) {
    this.template = template;
  }

  // 创建新对象
  create(overrides = {}) {
    return Object.assign({}, this.template, overrides);
  }

  // 批量创建
  createBatch(count, overridesFn) {
    return Array(count)
      .fill()
      .map((_, i) => this.create(overridesFn?.(i) || {}));
  }

  // 更新模板
  updateTemplate(updates) {
    this.template = Object.assign({}, this.template, updates);
  }
}

// 使用示例
const userFactory = new ObjectFactory({
  id: null,
  name: '',
  role: 'user',
  permissions: []
});

// 批量创建用户
const users = userFactory.createBatch(1000, (i) => ({
  id: i,
  name: `用户${i}`
}));


总结

原型模式在现代前端框架中仍然非常重要:

  • React Fiber 使用它来高效更新界面

  • 状态管理库使用它来优化性能

  • 它让代码更易于维护和扩展

下次当你看到 React 源码中的 Object.assign 或 alternate 时,就知道这是经过深思熟虑的设计选择。这种看似简单的模式,正是支撑现代前端框架高性能的关键技术之一。

理解这些底层原理,不仅能帮助你更好地使用框架,还能在需要自己实现复杂功能时,做出更明智的技术选择。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://shenqiku.cn/article/FLY_13043