阅读视图

发现新文章,点击刷新页面。

Vue中mixin与mixins:全面解析与实战指南

一、引言:为什么需要混入?

在Vue.js开发中,我们经常会遇到多个组件需要共享相同功能或逻辑的情况。例如,多个页面都需要用户认证检查、都需要数据加载状态管理、都需要相同的工具方法等。为了避免代码重复,提高代码的可维护性,Vue提供了混入(Mixin)机制。

今天,我将为你详细解析Vue中mixin和mixins的区别,并通过大量代码示例和流程图帮助你彻底理解这个概念。

二、基础概念解析

1. 什么是mixin?

mixin(混入) 是一个包含可复用组件选项的JavaScript对象。它可以包含组件选项中的任何内容,如data、methods、created、computed等生命周期钩子和属性。

2. 什么是mixins?

mixins 是Vue组件的一个选项,用于接收一个混入对象的数组。它允许组件使用多个mixin的功能。

三、核心区别详解

让我们通过一个对比表格来直观了解二者的区别:

特性 mixin mixins
本质 一个JavaScript对象 Vue组件的选项属性
作用 定义可复用的功能单元 注册和使用mixin
使用方式 被mixins选项引用 组件内部选项
数量 单个 可包含多个mixin

关系流程图

graph TD
    A[mixin定义] -->|混入到| B[Component组件]
    C[另一个mixin定义] -->|混入到| B
    D[更多mixin...] -->|混入到| B
    B --> E[mixins选项<br/>接收mixin数组]

四、代码实战演示

1. 基本mixin定义与使用

创建第一个mixin:

// mixins/loggerMixin.js
export const loggerMixin = {
  data() {
    return {
      logMessages: []
    }
  },
  
  methods: {
    logMessage(message) {
      const timestamp = new Date().toISOString()
      const logEntry = `[${timestamp}] ${message}`
      this.logMessages.push(logEntry)
      console.log(logEntry)
    }
  },
  
  created() {
    this.logMessage('组件/混入已创建')
  }
}

创建第二个mixin:

// mixins/authMixin.js
export const authMixin = {
  data() {
    return {
      currentUser: null,
      isAuthenticated: false
    }
  },
  
  methods: {
    login(user) {
      this.currentUser = user
      this.isAuthenticated = true
      this.$emit('login-success', user)
    },
    
    logout() {
      this.currentUser = null
      this.isAuthenticated = false
      this.$emit('logout')
    }
  },
  
  computed: {
    userRole() {
      return this.currentUser?.role || 'guest'
    }
  }
}

在组件中使用mixins:

<template>
  <div>
    <h1>用户仪表板</h1>
    <div v-if="isAuthenticated">
      <p>欢迎, {{ currentUser.name }} ({{ userRole }})</p>
      <button @click="logout">退出登录</button>
    </div>
    <div v-else>
      <button @click="login({ name: '张三', role: 'admin' })">登录</button>
    </div>
    <div>
      <h3>日志记录:</h3>
      <ul>
        <li v-for="(log, index) in logMessages" :key="index">{{ log }}</li>
      </ul>
    </div>
  </div>
</template>

<script>
import { loggerMixin } from './mixins/loggerMixin'
import { authMixin } from './mixins/authMixin'

export default {
  name: 'UserDashboard',
  
  // mixins选项接收mixin数组
  mixins: [loggerMixin, authMixin],
  
  created() {
    // 合并生命周期钩子
    this.logMessage('用户仪表板组件已创建')
  },
  
  methods: {
    login(user) {
      // 调用mixin的方法
      authMixin.methods.login.call(this, user)
      this.logMessage(`用户 ${user.name} 已登录`)
    }
  }
}
</script>

2. 选项合并策略详解

Vue在处理mixins时遵循特定的合并策略:

// mixins/featureMixin.js
export const featureMixin = {
  data() {
    return {
      message: '来自mixin的消息',
      sharedData: '共享数据'
    }
  },
  
  methods: {
    sayHello() {
      console.log('Hello from mixin!')
    },
    
    commonMethod() {
      console.log('mixin中的方法')
    }
  }
}
<template>
  <div>
    <p>{{ message }}</p>
    <p>{{ componentData }}</p>
    <button @click="sayHello">打招呼</button>
    <button @click="commonMethod">调用方法</button>
  </div>
</template>

<script>
import { featureMixin } from './mixins/featureMixin'

export default {
  mixins: [featureMixin],
  
  data() {
    return {
      message: '来自组件的消息', // 与mixin冲突,组件数据优先
      componentData: '组件特有数据'
    }
  },
  
  methods: {
    // 与mixin中的方法同名,组件方法将覆盖mixin方法
    commonMethod() {
      console.log('组件中的方法')
      // 如果需要调用mixin中的原始方法
      featureMixin.methods.commonMethod.call(this)
    },
    
    componentOnlyMethod() {
      console.log('组件特有方法')
    }
  }
}
</script>

3. 生命周期钩子的合并

生命周期钩子会被合并成数组,mixin的钩子先执行

// mixins/lifecycleMixin.js
export const lifecycleMixin = {
  beforeCreate() {
    console.log('1. mixin的beforeCreate')
  },
  
  created() {
    console.log('2. mixin的created')
  },
  
  mounted() {
    console.log('4. mixin的mounted')
  }
}
<script>
import { lifecycleMixin } from './mixins/lifecycleMixin'

export default {
  mixins: [lifecycleMixin],
  
  beforeCreate() {
    console.log('1. 组件的beforeCreate')
  },
  
  created() {
    console.log('3. 组件的created')
  },
  
  mounted() {
    console.log('5. 组件的mounted')
  }
}
</script>

// 控制台输出顺序:
// 1. mixin的beforeCreate
// 2. 组件的beforeCreate
// 3. mixin的created
// 4. 组件的created
// 5. mixin的mounted
// 6. 组件的mounted

4. 全局混入

除了在组件内使用mixins选项,还可以创建全局mixin:

// main.js或单独的文件中
import Vue from 'vue'

// 全局混入 - 影响所有Vue实例
Vue.mixin({
  data() {
    return {
      globalData: '这是全局数据'
    }
  },
  
  methods: {
    $formatDate(date) {
      return new Date(date).toLocaleDateString()
    }
  },
  
  mounted() {
    console.log('全局mixin的mounted钩子')
  }
})

五、高级用法与最佳实践

1. 可配置的mixin

通过工厂函数创建可配置的mixin:

// mixins/configurableMixin.js
export function createPaginatedMixin(options = {}) {
  const {
    pageSize: defaultPageSize = 10,
    dataKey = 'items'
  } = options
  
  return {
    data() {
      return {
        currentPage: 1,
        pageSize: defaultPageSize,
        totalItems: 0,
        [dataKey]: []
      }
    },
    
    computed: {
      totalPages() {
        return Math.ceil(this.totalItems / this.pageSize)
      },
      
      paginatedData() {
        const start = (this.currentPage - 1) * this.pageSize
        const end = start + this.pageSize
        return this[dataKey].slice(start, end)
      }
    },
    
    methods: {
      goToPage(page) {
        if (page >= 1 && page <= this.totalPages) {
          this.currentPage = page
        }
      },
      
      nextPage() {
        if (this.currentPage < this.totalPages) {
          this.currentPage++
        }
      },
      
      prevPage() {
        if (this.currentPage > 1) {
          this.currentPage--
        }
      }
    }
  }
}
<template>
  <div>
    <h1>用户列表</h1>
    <ul>
      <li v-for="user in paginatedData" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
    
    <div class="pagination">
      <button @click="prevPage" :disabled="currentPage === 1">上一页</button>
      <span>第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</span>
      <button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
    </div>
  </div>
</template>

<script>
import { createPaginatedMixin } from './mixins/configurableMixin'

export default {
  name: 'UserList',
  
  mixins: [createPaginatedMixin({ pageSize: 5, dataKey: 'users' })],
  
  data() {
    return {
      users: [] // 会被mixin处理
    }
  },
  
  async created() {
    // 模拟API调用
    const response = await fetch('/api/users')
    this.users = await response.json()
    this.totalItems = this.users.length
  }
}
</script>

2. 合并策略自定义

// 自定义合并策略
import Vue from 'vue'

// 为特定选项自定义合并策略
Vue.config.optionMergeStrategies.customOption = function(toVal, fromVal) {
  // 返回合并后的值
  return toVal || fromVal
}

// 自定义方法的合并策略:将方法合并到一个数组中
Vue.config.optionMergeStrategies.myMethods = function(toVal, fromVal) {
  if (!toVal) return [fromVal]
  if (!fromVal) return toVal
  return toVal.concat(fromVal)
}

六、mixin与mixins的完整执行流程

sequenceDiagram
    participant G as 全局mixin
    participant M1 as Mixin1
    participant M2 as Mixin2
    participant C as 组件
    participant V as Vue实例
    
    Note over G,M2: 初始化阶段
    G->>M1: 执行全局mixin钩子
    M1->>M2: 执行Mixin1钩子
    M2->>C: 执行Mixin2钩子
    C->>V: 执行组件钩子
    
    Note over G,M2: 数据合并
    V->>V: 合并data选项<br/>(组件优先)
    
    Note over G,M2: 方法合并
    V->>V: 合并methods选项<br/>(组件覆盖mixin)
    
    Note over G,M2: 钩子函数合并
    V->>V: 合并生命周期钩子<br/>(全部执行,mixin先执行)

七、替代方案与Composition API

虽然mixins非常有用,但在大型项目中可能导致一些问题:

  1. 命名冲突
  2. 隐式依赖
  3. 难以追踪功能来源

Vue 3引入了Composition API作为更好的替代方案:

<template>
  <div>
    <p>计数: {{ count }}</p>
    <p>双倍: {{ doubleCount }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script>
import { ref, computed, onMounted } from 'vue'

// 使用Composition API复用逻辑
function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const doubleCount = computed(() => count.value * 2)
  
  function increment() {
    count.value++
  }
  
  onMounted(() => {
    console.log('计数器已挂载')
  })
  
  return {
    count,
    doubleCount,
    increment
  }
}

export default {
  setup() {
    // 明确地使用功能,避免命名冲突
    const { count, doubleCount, increment } = useCounter(10)
    
    return {
      count,
      doubleCount,
      increment
    }
  }
}
</script>

八、总结与最佳实践

mixin vs mixins总结:

  • mixin是功能单元,mixins是使用这些功能单元的接口
  • 一个组件可以通过mixins选项使用多个mixin
  • 合并策略:组件选项通常优先于mixin选项
  • 生命周期钩子会合并执行,mixin钩子先于组件钩子

最佳实践:

  1. 命名规范:为mixin使用特定前缀,如mixinwith
  2. 单一职责:每个mixin只关注一个特定功能
  3. 明确文档:记录mixin的依赖和副作用
  4. 避免全局混入:除非确实需要影响所有组件
  5. 考虑Composition API:在Vue 3项目中优先使用

适用场景:

  • 适合使用mixin:简单的工具函数、通用的生命周期逻辑、小型到中型项目
  • 考虑替代方案:复杂的状态管理、大型企业级应用、需要明确依赖关系的场景

希望通过这篇文章,你已经全面理解了Vue中mixin和mixins的区别与用法。在实际开发中,合理使用混入可以显著提高代码复用性和可维护性,但也要注意避免过度使用导致的复杂性问题。

如果你觉得这篇文章有帮助,欢迎分享给更多开发者!

React 的 setState 批量更新机制详解

React 的 setState 批量更新是 React 优化性能的重要机制,它通过减少不必要的渲染次数来提高应用性能。下面我将详细解释这一过程。

1. 批量更新的基本概念

批量更新(Batching)是指 React 将多个 setState 调用合并为单个更新,从而减少组件重新渲染的次数。

示例代码:

class MyComponent extends React.Component {
  state = { count: 0 };
  
  handleClick = () => {
    this.setState({ count: this.state.count + 1 }); // 不会立即更新
    this.setState({ count: this.state.count + 1 }); // 不会立即更新
    // React 会将这两个 setState 合并
  };
  
  render() {
    return <button onClick={this.handleClick}>Count: {this.state.count}</button>;
  }
}

2. 批量更新的实现原理

2.1 更新队列机制

React 维护一个待处理的 state 更新队列,而不是立即应用每个 setState

graph TD
    A[setState调用] --> B[将更新加入队列]
    B --> C[React事件循环]
    C --> D[批量处理队列中的所有更新]
    D --> E[合并state更新]
    E --> F[执行单一重新渲染]

2.2 具体过程

  1. 更新入队:每次调用 setState,更新会被加入一个待处理队列
  2. 批量处理:在事件处理函数执行结束时,React 会批量处理所有队列中的更新
  3. 合并更新:对于同一 state 键的多个更新,React 会进行浅合并
  4. 触发渲染:最终只进行一次重新渲染

3. 批量更新的触发时机

3.1 自动批处理场景

  • React 事件处理函数(如 onClick)
  • 生命周期方法
  • React 能控制的入口点

3.2 不会自动批处理的情况

  • 异步代码:setTimeout、Promise、原生事件处理等
  • React 18 之前:只有在 React 事件处理函数中才会批处理
// 不会批处理的例子(React 17及之前)
handleClick = () => {
  setTimeout(() => {
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    // React 17中会触发两次渲染
  }, 0);
};

4. React 18 的自动批处理改进

React 18 引入了全自动批处理,覆盖更多场景:

// 在React 18中,这会批量处理
fetchData().then(() => {
  setState1();
  setState2();
  // 只会触发一次渲染
});

5. 强制同步更新的方法

如果需要立即获取更新后的状态,可以使用回调函数形式或 flushSync(React 18+):

// 回调函数形式
this.setState({ count: this.state.count + 1 }, () => {
  console.log('更新后的值:', this.state.count);
});

// React 18的flushSync
import { flushSync } from 'react-dom';

flushSync(() => {
  this.setState({ count: this.state.count + 1 });
});
// 这里state已经更新

6. 函数式组件的批量更新

函数式组件中 useState 也有类似的批量更新行为:

function MyComponent() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(c => c + 1); // 更新1
    setCount(c => c + 1); // 更新2
    // React会批量处理,最终count增加2
  };
  
  return <button onClick={handleClick}>{count}</button>;
}

7. 源码层面的简要分析

React 内部通过 enqueueUpdate 函数将更新加入队列:

// 伪代码简化版
function enqueueUpdate(component, partialState) {
  if (!batchingStrategy.isBatchingUpdates) {
    // 如果不处于批量模式,立即更新
    batchingStrategy.batchedUpdates(enqueueUpdate, component, partialState);
    return;
  }
  // 否则加入队列
  dirtyComponents.push(component);
  component._pendingStateQueue.push(partialState);
}

8. 为什么需要批量更新?

  1. 性能优化:减少不必要的渲染次数
  2. 保证一致性:避免中间状态导致的UI不一致
  3. 提升用户体验:更流畅的界面更新

9. 注意事项

  1. 不要依赖 this.state 获取最新值,因为它可能还未更新
  2. 对于连续依赖前一次状态的更新,使用函数形式:
    this.setState(prevState => ({ count: prevState.count + 1 }));
    
  3. 在React 18之前,异步操作中的多个 setState 不会批量处理

React 的批量更新机制是其高效渲染的核心特性之一,理解这一机制有助于编写更高效的React代码和避免常见陷阱。

在这里插入图片描述

React 开发全面指南:核心 API、方法函数及属性详解

React 作为当前最流行的前端框架之一,凭借其组件化、声明式编程和高效的虚拟 DOM 机制,成为构建复杂用户界面的首选工具。本文将深入解析 React 的核心 API、方法函数及属性,覆盖从基础到高级的各个方面,助你全面掌握 React 开发技巧。


1. React 核心概念

1.1 组件化开发

React 应用由组件构成,分为函数组件和类组件:

  • 函数组件:通过纯函数定义,无状态(Hooks 出现后可通过 useState 管理状态)。
  • 类组件:继承 React.Component,具有生命周期方法和状态管理。
// 函数组件
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// 类组件
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

1.2 JSX 语法

JSX 是 JavaScript 的语法扩展,用于描述 UI 结构:

const element = <div className="container">Hello React</div>;
  • 表达式嵌入:使用 {} 包裹 JavaScript 表达式。
  • 属性命名:采用驼峰式(如 className 代替 class)。

1.3 虚拟 DOM

React 通过虚拟 DOM 实现高效更新:

  1. 每次状态变更生成新的虚拟 DOM 树。
  2. 通过 Diff 算法对比新旧树差异。
  3. 仅更新实际 DOM 中变化的部分。

2. 组件生命周期方法(类组件)

2.1 挂载阶段(Mounting)

  • constructor(props):初始化状态和绑定方法。
  • static getDerivedStateFromProps(props, state):根据 props 更新 state。
  • render():返回 JSX,必须为纯函数。
  • componentDidMount():组件挂载后执行,适合发起网络请求。

2.2 更新阶段(Updating)

  • shouldComponentUpdate(nextProps, nextState):决定是否重新渲染。
  • getSnapshotBeforeUpdate(prevProps, prevState):捕获 DOM 更新前的状态。
  • componentDidUpdate(prevProps, prevState, snapshot):更新完成后执行。

2.3 卸载阶段(Unmounting)

  • componentWillUnmount():清理定时器、取消订阅等。

2.4 错误处理

  • static getDerivedStateFromError(error):更新状态以显示错误 UI。
  • componentDidCatch(error, info):记录错误信息。

3. Hooks API 详解

3.1 基础 Hooks

  • useState(initialState):管理组件状态。
    const [count, setCount] = useState(0);
    
  • useEffect(effect, dependencies):处理副作用(数据获取、订阅等)。
    useEffect(() => {
      document.title = `Count: ${count}`;
    }, [count]); // 依赖项变化时重新执行
    
  • useContext(Context):访问 Context 值。
    const theme = useContext(ThemeContext);
    

3.2 高级 Hooks

  • useReducer(reducer, initialArg, init):复杂状态逻辑管理。
    const [state, dispatch] = useReducer(reducer, initialState);
    
  • useCallback(fn, dependencies):缓存回调函数。
  • useMemo(() => value, dependencies):缓存计算结果。
  • useRef(initialValue):访问 DOM 或保存可变值。
    const inputRef = useRef();
    <input ref={inputRef} />
    

3.3 自定义 Hook

封装可复用的逻辑:

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  return width;
}

4. Context API 与状态管理

4.1 创建 Context

const ThemeContext = React.createContext('light');

4.2 提供 Context 值

<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>

4.3 消费 Context

  • 类组件:通过 static contextTypeConsumer
  • 函数组件:使用 useContext Hook。

5. Refs 与 DOM 操作

5.1 创建 Refs

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}

5.2 访问 Refs

const node = this.myRef.current;

5.3 转发 Refs(Forwarding Refs)

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="fancy">
    {props.children}
  </button>
));

6. 事件处理与合成事件

6.1 事件绑定

<button onClick={handleClick}>Click</button>

6.2 合成事件(SyntheticEvent)

React 封装了跨浏览器的事件对象,支持冒泡机制:

function handleChange(e) {
  console.log(e.target.value); // 输入框的值
}

6.3 事件池(Event Pooling)

合成事件对象会被重用,需通过 e.persist() 保留事件。


7. 高阶组件(HOC)与 Render Props

7.1 高阶组件

接收组件返回新组件:

function withLogging(WrappedComponent) {
  return class extends React.Component {
    componentDidMount() {
      console.log('Component mounted');
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

7.2 Render Props

通过函数 prop 共享代码:

<Mouse render={mouse => (
  <Cat position={mouse} />
)} />

8. 性能优化 API

8.1 React.memo()

缓存函数组件,避免不必要的渲染:

const MemoComponent = React.memo(MyComponent);

8.2 useMemouseCallback

缓存值和函数:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);

8.3 PureComponent

类组件自动浅比较 props 和 state:

class MyComponent extends React.PureComponent { ... }

9. 错误边界与调试工具

9.1 错误边界组件

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  componentDidCatch(error, info) {
    logErrorToService(error, info);
  }
  render() {
    if (this.state.hasError) {
      return <FallbackUI />;
    }
    return this.props.children;
  }
}

9.2 React Developer Tools

Chrome/Firefox 扩展,用于审查组件树、状态和性能。


10. React Router 核心 API

10.1 路由配置

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/users" element={<Users />} />
  </Routes>
</BrowserRouter>

10.2 导航

<Link to="/about">About</Link>
const navigate = useNavigate();
navigate('/profile');

11. 服务端渲染与 ReactDOMServer

11.1 renderToString()

将组件渲染为 HTML 字符串:

ReactDOMServer.renderToString(<App />);

11.2 renderToStaticMarkup()

生成静态 HTML(无额外 DOM 属性)。


12. TypeScript 与 React 集成

12.1 组件 Props 类型

interface ButtonProps {
  label: string;
  onClick: () => void;
}
const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);

13. 常见问题与最佳实践

13.1 避免不必要的渲染

  • 使用 React.memoPureComponent
  • 合理设置依赖项数组(useEffect, useMemo)。

13.2 状态管理选择

  • 简单应用使用 Context + useReducer
  • 复杂场景采用 Redux 或 MobX。

13.3 代码分割

const LazyComponent = React.lazy(() => import('./Component'));
<Suspense fallback={<Spinner />}>
  <LazyComponent />
</Suspense>

结语

React 的 API 生态庞大而灵活,本文涵盖了从基础到高级的核心知识点。掌握这些内容后,你将能够高效构建可维护的 React 应用。持续关注官方文档和社区动态,保持技术敏感度,是提升开发能力的关键。

React 性能优化十大总结

1.memo memo允许组件在 props 没有改变的情况下跳过重新渲染默认通过Object.is比较每个prop,可通过第二个参数,传入自定义函数来控制对比过程

const Chart = memo(function Chart({ dataPoints }) {
  // ...
}, arePropsEqual);

function arePropsEqual(oldProps, newProps) {
  return (
    oldProps.dataPoints.length === newProps.dataPoints.length &&
    oldProps.dataPoints.every((oldPoint, index) => {
      const newPoint = newProps.dataPoints[index];
      return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
    })
  );
}

2.useMemo 在每次重新渲染的时候能够缓存计算的结果

import { useState, useMemo } from "react";

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

  const memoizedValue = useMemo(() => {
    //创建1000位数组
    const list = new Array(1000).fill(null).map((_, i) => i);

    //对数组求和
    const total = list.reduce((res, cur) => (res += cur), 0);

    //返回计算的结果
    return count + total;

    //添加依赖项,只有count改变时,才会重新计算
  }, [count]);

  return (
    <div>
      {memoizedValue}
      <button onClick={() => setCount((prev) => prev + 1)}>按钮</button>
    </div>
  );
}

export default App;

3.useMemo 缓存函数的引用地址,仅在依赖项改变时才会更新

import { useState, memo } from 'react';

const App = () => {
  const [count, setCount] = useState(0);

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

  return (
    <div>
      {count}
      <MyButton handleClick={handleClick} />
    </div>
  );
};

const MyButton = memo(function MyButton({ handleClick }: { handleClick: () => void }) {
  console.log('子组件渲染');
  return <button onClick={handleClick}>按钮</button>;
});

export default App;

点击按钮,可以发现即使子组件使用memo包裹了,但还是更新了,控制台打印出“子组件渲染”。这是因为父组件App每次更新时,函数handleClick每次都返回了新的引用地址,因此对于子组件来说每次传入的都是不一样的值,从而触发重渲染。

同样的,减少使用通过内联函数绑定事件。每次父组件更新时,匿名函数都会返回一个新的引用地址,从而触发子组件的重渲染.

<MyButton handleClick={() => setCount((prev) => prev + 1)} />

使用useCallback可以缓存函数的引用地址,将handleClick改为

const handleClick = useCallback(()=>{
  setCount(prev=>prev+1)
},[])

再点击按钮,会发现子组件不会再重新渲染。

4.useTransition 使用useTransition提供的startTransition来标记一个更新作为不紧急的更新。这段任务可以接受延迟或被打断渲染,进而去优先考虑更重要的任务执行页面会先显示list2的内容,之后再显示list1的内容

import { useState, useEffect, useTransition } from "react";

const App = () => {
  const [list1, setList1] = useState<null[]>([]);
  const [list2, setList2] = useState<null[]>([]);
  const [isPending, startTransition] = useTransition();
  useEffect(() => {
    startTransition(() => {
       //将状态更新标记为 transition  
      setList1(new Array(10000).fill(null));
    });
  }, []);
  useEffect(()=>{
    setList2(new Array(10000).fill(null));
  },[])
  return (
    <>
      {isPending ? "pending" : "nopending"}
      {list1.map((_, i) => (
        <div key={i}>{i}</div>
      ))}
      -----------------list2
      {list2.map((_, i) => (
        <div key={i}>6666</div>
      ))}
    </>
  );
};

export default App;

5、useDeferredValue

可以让我们延迟渲染不紧急的部分,类似于防抖但没有固定的延迟时间

import { useState, useDeferredValue } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  // ...
}

6、Fragment

当呈现多个元素而不需要额外的容器元素时,使用React.Fragment可以减少DOM节点的数量,从而提高呈现性能

const MyComponent = () => {
  return (
    <React.Fragment>
      <div>Element 1</div>
      <div>Element 2</div>
      <div>Element 3</div>
    </React.Fragment>
  );
};

7、合理使用Context Context 能够在组件树间跨层级数据传递,正因其这一独特机制,Context 可以绕过 React.memo 或 shouldComponentUpdate 设定的比较过程。也就是说,一旦 Context 的 Value 变动,所有使用 useContext 获取该 Context 的组件会全部 forceUpdate。即使该组件使用了memo,且 Context 更新的部分 Value 与其无关

为了使组件仅在 context 与其相关的value发生更改时重新渲染,将组件分为两个部分。在外层组件中从 context 中读取所需内容,并将其作为 props 传递给使用memo优化的子组件。

8、尽量避免使用index作为key

在渲染元素列表时,尽量避免将数组索引作为组件的key。如果列表项有添加、删除及重新排序的操作,使用index作为key,可能会使节点复用率变低,进而影响性能使用数据源的id作为key

const MyComponent = () => {
  const items = [{ id: 1, name: "Item 1" }, { id: 2, name: "Item 2" }, { id: 3, name: "Item 3" }];

  return (
      <ul>
        {items.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    );
};

9、懒加载

通过React.lazy和React.Suspense实施代码分割策略,将React应用细分为更小的模块,确保在具体需求出现时才按需加载相应的部分

定义路由

import { lazy } from 'react';
import { createBrowserRouter } from 'react-router-dom';

const Login = lazy(() => import('../pages/login'));

const routes = [
  {
    path: '/login',
    element: <Login />,
  },
];

//可传第二个参数,配置base路径 { basename: "/app"}
const router = createBrowserRouter(routes);

export default router;

引用路由

import { Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';

import ReactDOM from 'react-dom/client';

import router from './router';

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

root.render(
  <Suspense fallback={<div>Loading...</div>}>
    <RouterProvider router={router} />
  </Suspense>,
);

10、组件卸载时的清理

在组件卸载时清理全局监听器、定时器等。防止内存泄漏影响性能

import { useState, useEffect, useRef } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);
  const timer = useRef<NodeJS.Timeout>();

  useEffect(() => {
    // 定义定时器
    timer.current = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);

    const handleOnResize = () => {
      console.log('Window resized');
    };

    // 定义监听器
    window.addEventListener('resize', handleOnResize);

    // 在组件卸载时清除定时器和监听器
    return () => {
      clearInterval(timer.current);
      window.removeEventListener('resize', handleOnResize);
    };
  }, []);

  return (
    <div>
      <p>{count}</p>
    </div>
  );
}

export default MyComponent;

附:

React 性能优化十大总结

@[toc]

1. 引言

为什么需要 React 性能优化?

React 是一个高效的前端框架,但在复杂应用中,性能问题仍然可能出现。通过性能优化,可以提升应用的响应速度和用户体验。

React 性能优化的基本概念

React 性能优化主要关注减少不必要的渲染、优化 DOM 操作、减少内存占用等方面。


2. React 性能优化的十大方法

1. 使用 React.memo 优化组件渲染

React.memo 是一个高阶组件,用于缓存组件的渲染结果,避免不必要的重新渲染。

const MyComponent = React.memo(function MyComponent(props) {
  // 组件逻辑
});

2. 使用 useMemouseCallback 缓存计算结果和函数

useMemo 用于缓存计算结果,useCallback 用于缓存函数,避免在每次渲染时重新计算或创建。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

3. 使用 React.lazySuspense 实现代码分割

React.lazySuspense 可以实现组件的懒加载,减少初始加载时间。

const LazyComponent = React.lazy(() => import('./LazyComponent'));

function MyComponent() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

4. 使用 shouldComponentUpdatePureComponent 避免不必要的渲染

shouldComponentUpdatePureComponent 可以避免组件在 props 或 state 未变化时重新渲染。

class MyComponent extends React.PureComponent {
  render() {
    // 组件逻辑
  }
}

5. 使用 key 优化列表渲染

为列表项设置唯一的 key,可以帮助 React 识别哪些项发生了变化,减少不必要的 DOM 操作。

const listItems = items.map(item => (
  <li key={item.id}>{item.name}</li>
));

6. 使用 React.Fragment 减少不必要的 DOM 节点

React.Fragment 可以避免在渲染时添加额外的 DOM 节点。

function MyComponent() {
  return (
    <React.Fragment>
      <ChildA />
      <ChildB />
    </React.Fragment>
  );
}

7. 使用 useReducer 替代 useState 管理复杂状态

useReducer 可以更好地管理复杂的状态逻辑,减少状态更新的次数。

const [state, dispatch] = useReducer(reducer, initialState);

8. 使用 React.memouseContext 优化上下文传递

通过 React.memouseContext,可以避免在上下文变化时重新渲染所有子组件。

const MyComponent = React.memo(function MyComponent() {
  const value = useContext(MyContext);
  // 组件逻辑
});

9. 使用 React.memouseRef 优化 DOM 操作

useRef 可以保存 DOM 引用,避免在每次渲染时重新获取 DOM 元素。

const myRef = useRef(null);

useEffect(() => {
  myRef.current.focus();
}, []);

10. 使用 React.memouseEffect 优化副作用

通过 React.memouseEffect,可以避免在每次渲染时执行不必要的副作用。

const MyComponent = React.memo(function MyComponent() {
  useEffect(() => {
    // 副作用逻辑
  }, [dependency]);
  // 组件逻辑
});

3. 实战:在 React 项目中应用性能优化

项目初始化

使用 Create React App 创建一个新的 React 项目:

npx create-react-app my-react-app
cd my-react-app
npm start

使用 React.memo 优化组件渲染

src/components/MyComponent.js 中使用 React.memo 优化组件渲染:

import React from 'react';

const MyComponent = React.memo(function MyComponent(props) {
  return <div>{props.value}</div>;
});

export default MyComponent;

使用 useMemouseCallback 缓存计算结果和函数

src/components/MyComponent.js 中使用 useMemouseCallback

import React, { useMemo, useCallback } from 'react';

function MyComponent({ a, b }) {
  const memoizedValue = useMemo(() => a + b, [a, b]);
  const memoizedCallback = useCallback(() => {
    console.log(a, b);
  }, [a, b]);

  return (
    <div>
      <p>{memoizedValue}</p>
      <button onClick={memoizedCallback}>Click me</button>
    </div>
  );
}

export default MyComponent;

使用 React.lazySuspense 实现代码分割

src/App.js 中使用 React.lazySuspense

import React, { Suspense } from 'react';

const LazyComponent = React.lazy(() => import('./components/LazyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

export default App;

使用 shouldComponentUpdatePureComponent 避免不必要的渲染

src/components/MyComponent.js 中使用 PureComponent

import React, { PureComponent } from 'react';

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

export default MyComponent;

使用 key 优化列表渲染

src/components/MyList.js 中使用 key 优化列表渲染:

import React from 'react';

function MyList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

export default MyList;

使用 React.Fragment 减少不必要的 DOM 节点

src/components/MyComponent.js 中使用 React.Fragment

import React from 'react';

function MyComponent() {
  return (
    <React.Fragment>
      <ChildA />
      <ChildB />
    </React.Fragment>
  );
}

export default MyComponent;

使用 useReducer 替代 useState 管理复杂状态

src/components/MyComponent.js 中使用 useReducer

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

export default MyComponent;

使用 React.memouseContext 优化上下文传递

src/components/MyComponent.js 中使用 React.memouseContext

import React, { useContext } from 'react';
import MyContext from './MyContext';

const MyComponent = React.memo(function MyComponent() {
  const value = useContext(MyContext);
  return <div>{value}</div>;
});

export default MyComponent;

使用 React.memouseRef 优化 DOM 操作

src/components/MyComponent.js 中使用 React.memouseRef

import React, { useRef, useEffect } from 'react';

const MyComponent = React.memo(function MyComponent() {
  const myRef = useRef(null);

  useEffect(() => {
    myRef.current.focus();
  }, []);

  return <input ref={myRef} />;
});

export default MyComponent;

使用 React.memouseEffect 优化副作用

src/components/MyComponent.js 中使用 React.memouseEffect

import React, { useEffect } from 'react';

const MyComponent = React.memo(function MyComponent({ dependency }) {
  useEffect(() => {
    console.log('Effect triggered');
  }, [dependency]);

  return <div>{dependency}</div>;
});

export default MyComponent;

4. 进阶:React 性能优化的策略

使用 React.memo 优化组件渲染

通过 React.memo 缓存组件的渲染结果,避免不必要的重新渲染。

使用 useMemouseCallback 缓存计算结果和函数

通过 useMemouseCallback 缓存计算结果和函数,避免在每次渲染时重新计算或创建。

使用 React.lazySuspense 实现代码分割

通过 React.lazySuspense 实现组件的懒加载,减少初始加载时间。

使用 shouldComponentUpdatePureComponent 避免不必要的渲染

通过 shouldComponentUpdatePureComponent 避免组件在 props 或 state 未变化时重新渲染。

使用 key 优化列表渲染

为列表项设置唯一的 key,帮助 React 识别哪些项发生了变化,减少不必要的 DOM 操作。

使用 React.Fragment 减少不必要的 DOM 节点

通过 React.Fragment 避免在渲染时添加额外的 DOM 节点。

使用 useReducer 替代 useState 管理复杂状态

通过 useReducer 更好地管理复杂的状态逻辑,减少状态更新的次数。

使用 React.memouseContext 优化上下文传递

通过 React.memouseContext 避免在上下文变化时重新渲染所有子组件。

使用 React.memouseRef 优化 DOM 操作

通过 useRef 保存 DOM 引用,避免在每次渲染时重新获取 DOM 元素。

使用 React.memouseEffect 优化副作用

通过 React.memouseEffect 避免在每次渲染时执行不必要的副作用。


5. 常见问题与解决方案

性能优化的兼容性问题

  • 问题:某些旧版浏览器可能不支持 React 的某些功能。
  • 解决方案:确保浏览器兼容性,或使用兼容性更好的方法。

性能优化的性能问题

  • 问题:频繁操作可能导致性能问题。
  • 解决方案:优化操作逻辑,减少不必要的操作。

性能优化的使用误区

  • 问题:误用性能优化可能导致逻辑混乱。
  • 解决方案:理解性能优化的原理,避免误用。

6. 总结与展望

React 性能优化的最佳实践

  • 明确使用场景:根据需求选择合适的性能优化方法。
  • 优化性能:合理使用性能优化,避免频繁操作。
  • 确保兼容性:确保性能优化在不同浏览器和环境中兼容。

未来发展方向

  • 更强大的性能优化:支持更复杂的开发场景。
  • 更好的性能优化:提供更高效的实现方式。

通过本文的学习,你应该已经掌握了 React 性能优化的十大方法及实战应用。希望这些内容能帮助你在实际项目中更好地提升应用性能,提升用户体验!

❌