React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React PureComponent引用类型更新

React PureComponent中引用类型修改导致页面不更新的解决方案

作者:北辰alk

React的PureComponent是React.Component的一个变体,它通过浅比较props和state来自动实现shouldComponentUpdate()方法,从而优化性能,本文详细解析React PureComponent中引用类型数据修改导致页面不更新的问题,并提供多种解决方案和最佳实践,需要的朋友可以参考下

PureComponent 的工作原理

什么是 PureComponent

PureComponent 是 React 提供的一个优化性能的组件基类,它通过浅比较(shallow comparison)来自动实现 shouldComponentUpdate 方法。

import React, { PureComponent } from 'react';

class MyComponent extends PureComponent {
  render() {
    return <div>{this.props.value}</div>;
  }
}

浅比较机制

PureComponent 的浅比较机制如下:

// 简化的浅比较实现
function shallowEqual(objA, objB) {
  if (objA === objB) {
    return true;
  }

  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  for (let i = 0; i < keysA.length; i++) {
    const key = keysA[i];
    // 只比较第一层属性
    if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
      return false;
    }
  }

  return true;
}

PureComponent 与 Component 的区别

特性ComponentPureComponent
是否需要手动实现 shouldComponentUpdate
性能优化需要手动优化自动浅比较优化
适用场景需要精细控制更新的组件数据结构简单的展示组件

问题根源分析

引用类型的特点

JavaScript 中的引用类型(对象、数组)在赋值时传递的是引用(内存地址),而不是实际的值。

const obj1 = { count: 0 };
const obj2 = obj1; // obj2 和 obj1 指向同一个内存地址

obj2.count = 1;
console.log(obj1.count); // 输出 1,因为修改的是同一个对象

问题场景再现

class UserList extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      users: [
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Bob' }
      ]
    };
  }

  // 错误的方式:直接修改引用类型
  updateUserName = (userId, newName) => {
    const users = this.state.users;
    const user = users.find(u => u.id === userId);
    user.name = newName; // 直接修改原对象
    
    this.setState({ users }); // users 引用未改变,PureComponent 不会重新渲染
  }

  render() {
    return (
      <div>
        {this.state.users.map(user => (
          <UserItem 
            key={user.id} 
            user={user} 
            onUpdate={this.updateUserName}
          />
        ))}
      </div>
    );
  }
}

问题流程图解

解决方案概览

解决方案对比表

解决方案优点缺点适用场景
不可变数据模式性能好,易于调试需要学习新概念大多数场景
状态管理库功能强大,生态丰富增加项目复杂度大型应用
forceUpdate简单直接违背 React 设计原则紧急修复
函数式子组件灵活可控需要手动优化性能简单组件

方案一:使用不可变数据模式

什么是不可变数据

不可变数据是指一旦创建就不能被修改的数据。任何修改都会返回一个新的数据副本。

使用扩展运算符

updateUserName = (userId, newName) => {
  this.setState(prevState => ({
    users: prevState.users.map(user => 
      user.id === userId 
        ? { ...user, name: newName } // 创建新对象
        : user
    )
  }));
}

使用数组的不可变方法

// 添加用户
addUser = (newUser) => {
  this.setState(prevState => ({
    users: [...prevState.users, newUser] // 创建新数组
  }));
}

// 删除用户
removeUser = (userId) => {
  this.setState(prevState => ({
    users: prevState.users.filter(user => user.id !== userId) // 创建新数组
  }));
}

// 更新用户
updateUser = (userId, updates) => {
  this.setState(prevState => ({
    users: prevState.users.map(user => 
      user.id === userId 
        ? { ...user, ...updates } // 创建新对象
        : user
    )
  }));
}

使用 Object.assign

updateUserName = (userId, newName) => {
  this.setState(prevState => ({
    users: prevState.users.map(user => 
      user.id === userId 
        ? Object.assign({}, user, { name: newName }) // 创建新对象
        : user
    )
  }));
}

处理嵌套对象

对于深层嵌套的对象,需要使用递归或专用库:

// 深层更新示例
updateUserProfile = (userId, field, value) => {
  this.setState(prevState => ({
    users: prevState.users.map(user => 
      user.id === userId 
        ? { 
            ...user, 
            profile: {
              ...user.profile,
              [field]: value
            }
          }
        : user
    )
  }));
}

方案二:使用状态管理库

使用 Immer 简化不可变更新

Immer 让你可以用可变的方式编写不可变更新逻辑。

npm install immer
import produce from 'immer';

class UserList extends PureComponent {
  // 使用 Immer 进行更新
  updateUserName = (userId, newName) => {
    this.setState(produce(draft => {
      const user = draft.users.find(u => u.id === userId);
      if (user) {
        user.name = newName; // 直接修改,Immer 会处理不可变性
      }
    }));
  }
}

使用 Redux 进行状态管理

// actions.js
export const updateUserName = (userId, name) => ({
  type: 'UPDATE_USER_NAME',
  payload: { userId, name }
});

// reducer.js
const initialState = {
  users: [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ]
};

export function userReducer(state = initialState, action) {
  switch (action.type) {
    case 'UPDATE_USER_NAME':
      return {
        ...state,
        users: state.users.map(user =>
          user.id === action.payload.userId
            ? { ...user, name: action.payload.name }
            : user
        )
      };
    default:
      return state;
  }
}

使用 MobX 进行状态管理

npm install mobx mobx-react
import { observable, action } from 'mobx';
import { observer } from 'mobx-react';

class UserStore {
  @observable users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ];

  @action
  updateUserName = (userId, newName) => {
    const user = this.users.find(u => u.id === userId);
    if (user) {
      user.name = newName; // MobX 会检测变化并触发更新
    }
  }
}

const userStore = new UserStore();

// 使用 observer 包装组件
const UserList = observer(({ store }) => (
  <div>
    {store.users.map(user => (
      <div key={user.id}>{user.name}</div>
    ))}
  </div>
));

方案三:使用 forceUpdate 方法

什么是 forceUpdate

forceUpdate() 是 React 组件的一个方法,它会强制组件重新渲染,跳过 shouldComponentUpdate

使用示例

class UserList extends PureComponent {
  updateUserName = (userId, newName) => {
    const users = this.state.users;
    const user = users.find(u => u.id === userId);
    user.name = newName; // 直接修改原对象
    
    this.forceUpdate(); // 强制重新渲染
  }
}

注意事项

  1. 不推荐常规使用forceUpdate 违背了 React 的数据流原则
  2. 性能影响:跳过 shouldComponentUpdate 可能导致不必要的渲染
  3. 使用场景:仅适用于无法通过正常数据流更新的特殊情况

替代方案:使用 key 属性

通过改变 key 值强制重新创建组件:

class UserList extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      users: [...],
      version: 0 // 用作 key 的值
    };
  }

  updateUserName = (userId, newName) => {
    const users = this.state.users;
    const user = users.find(u => u.id === userId);
    user.name = newName;
    
    // 通过改变 version 强制重新渲染
    this.setState(prevState => ({ version: prevState.version + 1 }));
  }

  render() {
    return (
      <div key={this.state.version}>
        {this.state.users.map(user => (
          <UserItem key={user.id} user={user} />
        ))}
      </div>
    );
  }
}

方案四:使用函数式子组件

将可变部分提取为独立组件

// UserItem.js - 使用普通 Component
class UserItem extends Component {
  shouldComponentUpdate(nextProps) {
    // 自定义比较逻辑
    return nextProps.user.name !== this.props.user.name;
  }

  render() {
    const { user } = this.props;
    return <div>{user.name}</div>;
  }
}

// UserList.js - 继续使用 PureComponent
class UserList extends PureComponent {
  // 仍然使用直接修改(不推荐,仅作示例)
  updateUserName = (userId, newName) => {
    const users = this.state.users;
    const user = users.find(u => u.id === userId);
    user.name = newName;
    
    this.setState({ users });
  }

  render() {
    return (
      <div>
        {this.state.users.map(user => (
          <UserItem 
            key={user.id} 
            user={user} 
            onUpdate={this.updateUserName}
          />
        ))}
      </div>
    );
  }
}

使用 React.memo 自定义比较

const UserItem = React.memo(({ user }) => {
  return <div>{user.name}</div>;
}, (prevProps, nextProps) => {
  // 自定义比较函数
  return prevProps.user.name === nextProps.user.name;
});

// 在父组件中
class UserList extends PureComponent {
  // 更新逻辑...
}

性能优化建议

使用不可变数据结构的性能考虑

  1. 结构共享:高级不可变库(如 Immutable.js)使用结构共享来减少内存使用
  2. 避免深层克隆:使用不可变更新时避免不必要的深层克隆
// 不好的做法:深层克隆整个对象
const newState = JSON.parse(JSON.stringify(prevState));

// 好的做法:浅层扩展
const newState = { ...prevState, users: updatedUsers };

使用 reselect 优化选择器

npm install reselect
import { createSelector } from 'reselect';

// 输入选择器
const getUsers = state => state.users;
const getFilter = state => state.filter;

// 记忆化选择器
export const getVisibleUsers = createSelector(
  [getUsers, getFilter],
  (users, filter) => {
    return users.filter(user => 
      user.name.toLowerCase().includes(filter.toLowerCase())
    );
  }
);

// 在组件中使用
const mapStateToProps = state => ({
  visibleUsers: getVisibleUsers(state)
});

使用 React Profiler 分析性能

import { Profiler } from 'react';

function onRenderCallback(
  id, // 发生提交的 Profiler 树的 "id"
  phase, // "mount" (如果组件树刚加载) 或者 "update" (如果它重渲染了)之一
  actualDuration, // 本次更新 committed 花费的渲染时间
  baseDuration, // 估计不使用 memoization 的情况下渲染整颗子树需要的时间
  startTime, // 本次更新中 React 开始渲染的时间
  commitTime, // 本次更新中 React committed 的时间
  interactions // 属于本次更新的 interactions 的集合
) {
  // 合计或记录渲染时间...
}

<Profiler id="UserList" onRender={onRenderCallback}>
  <UserList {...props} />
</Profiler>

总结与最佳实践

问题解决总结

  1. 根本原因:PureComponent 的浅比较无法检测引用类型内部的变化
  2. 核心解决方案:使用不可变数据模式创建新对象/数组而不是修改原对象
  3. 辅助方案:状态管理库、forceUpdate、组件结构优化

最佳实践推荐

  1. 优先使用不可变数据模式:使用扩展运算符、map、filter 等方法
  2. 复杂场景使用 Immer:简化深层不可变更新的编写
  3. 大型应用使用状态管理库:Redux + 不可变更新或 MobX
  4. 避免使用 forceUpdate:除非在极其特殊的情况下
  5. 合理使用 PureComponent:在数据结构简单、渲染成本高的组件中使用

代码示例:完整解决方案

import React, { PureComponent } from 'react';

class UserManager extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      users: [
        { id: 1, name: 'Alice', profile: { age: 25, city: 'Beijing' } },
        { id: 2, name: 'Bob', profile: { age: 30, city: 'Shanghai' } }
      ]
    };
  }

  // 添加用户 - 使用不可变更新
  addUser = (newUser) => {
    this.setState(prevState => ({
      users: [...prevState.users, { ...newUser, id: Date.now() }]
    }));
  }

  // 更新用户信息 - 使用不可变更新
  updateUser = (userId, updates) => {
    this.setState(prevState => ({
      users: prevState.users.map(user =>
        user.id === userId
          ? { ...user, ...updates }
          : user
      )
    }));
  }

  // 更新用户配置 - 深层不可变更新
  updateUserProfile = (userId, profileUpdates) => {
    this.setState(prevState => ({
      users: prevState.users.map(user =>
        user.id === userId
          ? {
              ...user,
              profile: {
                ...user.profile,
                ...profileUpdates
              }
            }
          : user
      )
    }));
  }

  // 删除用户 - 使用不可变更新
  removeUser = (userId) => {
    this.setState(prevState => ({
      users: prevState.users.filter(user => user.id !== userId)
    }));
  }

  render() {
    return (
      <div>
        <UserForm onSubmit={this.addUser} />
        <UserList 
          users={this.state.users}
          onUpdate={this.updateUser}
          onUpdateProfile={this.updateUserProfile}
          onRemove={this.removeUser}
        />
      </div>
    );
  }
}

// 使用 React.memo 优化子组件
const UserList = React.memo(({ users, onUpdate, onUpdateProfile, onRemove }) => {
  return (
    <div>
      {users.map(user => (
        <UserItem
          key={user.id}
          user={user}
          onUpdate={onUpdate}
          onUpdateProfile={onUpdateProfile}
          onRemove={onRemove}
        />
      ))}
    </div>
  );
});

export default UserManager;

以上就是React PureComponent中引用类型修改导致页面不更新的解决方案的详细内容,更多关于React PureComponent类型修改页面不更新的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文