React 18 并发特性实战指南:提升大型应用性能的关键技术

前端系统进阶 https://feinterview.poetries.top

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • React 18 并发渲染的底层原理和工作机制
  • useTransitionuseDeferredValue 的实战应用场景
  • 如何使用 Suspense 优化数据加载体验
  • 自动批处理带来的性能提升
  • 大型列表和复杂交互场景的优化方案

在大型 React 应用中,我们经常遇到这样的场景:用户在搜索框输入时,页面需要同时更新输入框内容和下方的搜索结果列表。传统的同步渲染会导致输入框卡顿,用户体验极差。React 18 引入的并发特性(Concurrent Features)从根本上解决了这个问题,它允许 React 中断、暂停和恢复渲染工作,让高优先级的更新(如用户输入)能够立即响应。通过本文,你将掌握如何在实际项目中应用这些特性,显著提升应用的交互性能和用户体验。

一、React 18 并发特性概述

React 18 最重要的更新就是引入了并发渲染(Concurrent Rendering)能力。与 React 17 的同步渲染不同,并发渲染允许 React 同时准备多个版本的 UI,并根据优先级决定哪些更新应该先完成。

核心概念理解

并发渲染并不是指多线程或并行执行,而是指 React 可以在渲染过程中被中断。想象一下,你在写一份文档,突然有紧急电话打来,你可以先放下手头工作去接电话,之后再继续写文档。React 18 的并发特性就是这样工作的。

在 React 17 中,一旦开始渲染,就必须完成整个组件树的渲染才能响应用户交互。这在处理大型列表或复杂计算时会导致明显的卡顿。React 18 通过时间切片(Time Slicing)技术,将渲染工作分解成多个小任务,在每个任务之间检查是否有更高优先级的工作需要处理。

并发特性的核心 API

React 18 提供了几个关键的 API 来使用并发特性:

  • useTransition:标记非紧急的状态更新,允许它们被中断

  • useDeferredValue:延迟更新某个值,保持 UI 的响应性

  • Suspense:配合并发渲染优化数据加载体验

  • 自动批处理(Automatic Batching):自动合并多个状态更新

这些 API 不需要你重写现有代码,它们是渐进式的增强功能。你可以在需要优化的地方逐步引入。

二、自动批处理:无需改动的性能提升

自动批处理是 React 18 中最容易获得收益的特性,因为它完全自动工作,不需要任何代码修改。

React 17 的批处理限制

在 React 17 中,只有在事件处理函数内部的多个状态更新会被批处理。但在 PromisesetTimeout 或原生事件处理中,每个状态更新都会触发一次重新渲染。

javascript 复制代码
// React 17 中的行为
function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 只触发一次重新渲染 ✅
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 触发两次重新渲染 ❌
}, 1000);

这种不一致的行为不仅让人困惑,还会导致不必要的性能损耗。

React 18 的自动批处理

React 18 将批处理扩展到所有场景,无论状态更新发生在哪里,都会自动合并。

javascript 复制代码
// React 18 中的行为
function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 只触发一次重新渲染 ✅
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 只触发一次重新渲染 ✅(React 18 的改进)
}, 1000);

fetch('/api/data').then(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 只触发一次重新渲染 ✅
});

实测性能提升

在我们的生产项目中,仅仅升级到 React 18 并启用自动批处理,就让某些页面的渲染次数减少了 30%-40%。特别是在处理异步数据更新的场景中,效果最为明显。

如果你确实需要同步更新(极少数情况),可以使用 flushSync

javascript 复制代码
import { flushSync } from 'react-dom';

flushSync(() => {
  setCount(c => c + 1);
});
// DOM 已经更新

flushSync(() => {
  setFlag(f => !f);
});
// DOM 再次更新

但在实际开发中,几乎不需要使用 flushSync。自动批处理的默认行为已经足够智能。

三、useTransition:区分紧急和非紧急更新

useTransition 是并发特性中最实用的 Hook,它允许你将某些状态更新标记为"非紧急",从而保持 UI 的响应性。

典型使用场景

最常见的场景是搜索功能。用户在输入框中输入时,我们需要:

  1. 立即更新输入框的值(紧急)

  2. 根据输入过滤大量数据(非紧急)

javascript 复制代码
import { useState, useTransition } from 'react';

function SearchList({ items }) {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const [filteredItems, setFilteredItems] = useState(items);

  function handleChange(e) {
    const value = e.target.value;
    setQuery(value); // 紧急更新:立即更新输入框

    startTransition(() => {
      // 非紧急更新:可以被中断的过滤操作
      const filtered = items.filter(item =>
        item.name.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredItems(filtered);
    });
  }

  return (
    <div>
      <input value="{query}" onchange="{handleChange}" placeholder="搜索...">
      {isPending && <span>搜索中...</span>}
      <ul style="{{" opacity:="" ispending="" ?="" 0.6="" :="" 1="" }}="">
        {filteredItems.map(item => (
          <li key="{item.id}">{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

工作原理详解

useTransition 返回两个值:

  1. isPending:布尔值,表示是否有待处理的 transition

  2. startTransition:函数,用于包裹非紧急的状态更新

当用户快速输入时,React 会:

  • 立即执行 setQuery,更新输入框

  • 开始执行 startTransition 中的过滤逻辑

  • 如果用户继续输入,中断之前的过滤工作,开始新的过滤

  • 只渲染最后一次的过滤结果

这种机制避免了无用的中间状态渲染,显著提升了性能。

真实项目经验

在我们的电商项目中,商品列表有 5000+ 条数据。使用 useTransition 之前,用户输入时会有明显的卡顿,输入框延迟达到 200-300ms。引入 useTransition 后,输入框始终保持流畅,延迟降低到 16ms 以内(一帧的时间)。

需要注意的是,startTransition 中的更新必须是状态更新,不能包含异步操作:

javascript 复制代码
// ❌ 错误:不能在 startTransition 中使用 async
startTransition(async () => {
  const data = await fetchData();
  setData(data);
});

// ✅ 正确:异步操作在外部,只把状态更新放在 startTransition 中
const data = await fetchData();
startTransition(() => {
  setData(data);
});

四、useDeferredValue:延迟更新派生状态

useDeferredValue 是另一个处理非紧急更新的 Hook,它更适合处理派生状态的场景。

与 useTransition 的区别

  • useTransition:你控制何时触发非紧急更新

  • useDeferredValue:React 自动延迟某个值的更新

javascript 复制代码
import { useState, useDeferredValue, memo } from 'react';

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

  return (
    <div>
      <input value="{query}" onchange="{e" ==""> setQuery(e.target.value)}
      />
      <searchresults query="{deferredQuery}">
    </searchresults></div>
  );
}

// 使用 memo 避免不必要的重新渲染
const SearchResults = memo(function SearchResults({ query }) {
  const items = useSearchItems(query); // 耗时的搜索逻辑
  
  return (
    <ul>
      {items.map(item => (
        <li key="{item.id}">{item.name}</li>
      ))}
    </ul>
  );
});

工作机制

query 快速变化时:

  1. query 立即更新,输入框保持响应

  2. deferredQuery 延迟更新,等待 React 有空闲时间

  3. SearchResults 使用延迟的 deferredQuery,不会阻塞输入

useDeferredValue 特别适合配合 memo 使用。因为 deferredQuery 变化频率低,SearchResults 的重新渲染次数也会减少。

选择 useTransition 还是 useDeferredValue?

根据我的实践经验:

  • **使用 **useTransition:当你能直接控制状态更新的代码时

    • 表单提交、按钮点击等明确的用户操作

    • 需要显示 loading 状态(isPending

  • **使用 **useDeferredValue:当你只能接收一个值,无法控制其更新时

    • 接收 props 传入的值

    • 需要对某个值进行昂贵的派生计算

    • 更简洁的代码,不需要手动包裹 startTransition

实际项目中,两者可以组合使用:

javascript 复制代码
function ParentComponent() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  function handleChange(value) {
    setQuery(value); // 立即更新
    startTransition(() => {
      // 触发其他非紧急更新
      updateAnalytics(value);
    });
  }

  return <childcomponent query="{query}" ispending="{isPending}">;
}

function ChildComponent({ query, isPending }) {
  const deferredQuery = useDeferredValue(query);
  // 使用 deferredQuery 进行渲染
}
</childcomponent>

五、Suspense:优化数据加载体验

Suspense 在 React 18 中得到了增强,配合并发渲染能提供更好的加载体验。

基础使用

javascript 复制代码
import { Suspense } from 'react';

function App() {
  return (
    <suspense fallback="{<LoadingSpinner">}>
      <userprofile>
      <postlist>
    </postlist></userprofile></suspense>
  );
}

UserProfilePostList 在加载数据时,会显示 LoadingSpinner。数据加载完成后,自动切换到实际内容。

配合 React Query 使用

在实际项目中,Suspense 通常配合数据获取库使用。以 React Query 为例:

javascript 复制代码
import { Suspense } from 'react';
import { useQuery } from '@tanstack/react-query';

function UserProfile() {
  // 启用 suspense 模式
  const { data } = useQuery({
    queryKey: ['user'],
    queryFn: fetchUser,
    suspense: true,
  });

  return <div>{data.name}</div>;
}

function App() {
  return (
    <suspense fallback="{<Skeleton">}>
      <userprofile>
    </userprofile></suspense>
  );
}

嵌套 Suspense 实现渐进式加载

更高级的用法是使用嵌套的 Suspense,实现不同部分的独立加载:

javascript 复制代码
function Dashboard() {
  return (
    <div>
      <suspense fallback="{<HeaderSkeleton">}>
        <header>
      

      <suspense fallback="{<ChartSkeleton">}>
        <charts>
      </charts></suspense>

      <suspense fallback="{<TableSkeleton">}>
        <datatable>
      </datatable></suspense>
    </header></suspense></div>
  );
}

这样,Header 加载完成后会立即显示,不需要等待 ChartsDataTable。用户能更快看到页面内容,体验更流畅。

配合 useTransition 避免闪烁

直接使用 Suspense 可能会导致页面闪烁(显示 loading → 显示内容 → 又显示 loading)。配合 useTransition 可以解决这个问题:

javascript 复制代码
function App() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <div>
      <button onclick="{()" ==""> selectTab('home')}>首页</button>
      <button onclick="{()" ==""> selectTab('profile')}>个人中心</button>
      
      {isPending && <spinner>}
      
      <suspense fallback="{<PageSkeleton">}>
        {tab === 'home' && <homepage>}
        {tab === 'profile' && <profilepage>}
      </profilepage></homepage></suspense>
    </spinner></div>
  );
}

使用 startTransition 包裹标签切换后,React 会:

  1. 保持显示当前标签的内容

  2. 在后台准备新标签的内容

  3. 只有在新内容准备好后才切换,避免显示 loading

这种模式在我们的后台管理系统中广泛使用,用户切换标签时几乎感觉不到加载过程。

六、并发渲染的工作原理

理解并发渲染的底层原理,能帮助你更好地使用这些 API。

React 17 vs React 18 的渲染差异

React 17 同步渲染:

  • 渲染开始后必须完成整个组件树

  • 渲染期间无法响应用户交互

  • 长时间渲染会导致页面卡顿

React 18 并发渲染:

  • 渲染可以被中断和恢复

  • 高优先级更新可以插队

  • 保持 UI 始终响应用户操作

优先级调度机制

React 18 内部使用 Scheduler 来管理任务优先级。不同类型的更新有不同的优先级:

  1. 同步优先级flushSync 中的更新,立即执行

  2. 用户交互优先级:点击、输入等用户操作,高优先级

  3. 默认优先级:普通的状态更新

  4. Transition 优先级startTransition 中的更新,低优先级

  5. 空闲优先级:可以无限延迟的更新

当多个更新同时发生时,React 会:

  1. 先执行高优先级更新

  2. 渲染高优先级更新的结果

  3. 在浏览器空闲时继续执行低优先级更新

时间切片的实现

React 使用 MessageChannelsetTimeout 将渲染工作分解成 5ms 的小任务。每个任务执行后,React 会检查:

  • 是否有更高优先级的工作?

  • 浏览器是否需要处理用户输入或绘制?

如果有,React 会暂停当前工作,让浏览器处理更重要的事情。这就是为什么使用并发特性后,即使在渲染大量组件时,UI 仍然保持响应。

渲染阶段的可中断性

需要注意的是,只有渲染阶段(Render Phase)可以被中断,提交阶段(Commit Phase)仍然是同步的。这保证了:

  • DOM 更新是原子性的,不会出现不一致的状态

  • 副作用(useEffect)按正确的顺序执行

这也意味着,渲染阶段的函数(组件函数、useMemo 等)可能被多次调用,所以它们必须是纯函数,不能有副作用。

七、实战案例:大型列表优化

让我们通过一个完整的案例,看看如何综合运用并发特性优化大型列表。

场景描述

假设我们有一个包含 10,000 条商品的列表,需要支持:

  • 实时搜索过滤

  • 按类别筛选

  • 按价格排序

传统实现会导致严重的性能问题。

优化前的代码

javascript 复制代码
function ProductList({ products }) {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');
  const [sortBy, setSortBy] = useState('name');

  // 每次状态变化都会重新计算,阻塞 UI
  const filteredProducts = products
    .filter(p => p.name.includes(query))
    .filter(p => category === 'all' || p.category === category)
    .sort((a, b) => a[sortBy].localeCompare(b[sortBy]));

  return (
    <div>
      <input value="{query}" onchange="{e" ==""> setQuery(e.target.value)}
      />
      <select value="{category}" onchange="{e" ==""> setCategory(e.target.value)}>
        <option value="all">全部</option>
        <option value="electronics">电子产品</option>
        <option value="clothing">服装</option>
      </select>
      <select value="{sortBy}" onchange="{e" ==""> setSortBy(e.target.value)}>
        <option value="name">按名称</option>
        <option value="price">按价格</option>
      </select>
      <ul>
        {filteredProducts.map(p => (
          <li key="{p.id}">{p.name} - ¥{p.price}</li>
        ))}
      </ul>
    </div>
  );
}

这段代码的问题:

  • 用户输入时,过滤、排序操作会阻塞输入框

  • 每次状态变化都重新计算整个列表

  • 没有任何优化措施

优化后的代码

javascript 复制代码
import { useState, useMemo, useTransition, useDeferredValue } from 'react';

function ProductList({ products }) {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');
  const [sortBy, setSortBy] = useState('name');
  const [isPending, startTransition] = useTransition();

  // 使用 deferredValue 延迟搜索词的更新
  const deferredQuery = useDeferredValue(query);

  // 使用 useMemo 缓存计算结果
  const filteredProducts = useMemo(() => {
    console.log('重新计算列表'); // 用于调试
    
    return products
      .filter(p => 
        p.name.toLowerCase().includes(deferredQuery.toLowerCase())
      )
      .filter(p => category === 'all' || p.category === category)
      .sort((a, b) => {
        if (sortBy === 'price') {
          return a.price - b.price;
        }
        return a.name.localeCompare(b.name);
      });
  }, [products, deferredQuery, category, sortBy]);

  function handleQueryChange(e) {
    setQuery(e.target.value); // 立即更新输入框
  }

  function handleCategoryChange(e) {
    startTransition(() => {
      setCategory(e.target.value); // 非紧急更新
    });
  }

  function handleSortChange(e) {
    startTransition(() => {
      setSortBy(e.target.value); // 非紧急更新
    });
  }

  return (
    <div>
      <div classname="filters">
        <input value="{query}" onchange="{handleQueryChange}" placeholder="搜索商品...">
        <select value="{category}" onchange="{handleCategoryChange}" disabled="{isPending}">
          <option value="all">全部分类</option>
          <option value="electronics">电子产品</option>
          <option value="clothing">服装</option>
        </select>
        <select value="{sortBy}" onchange="{handleSortChange}" disabled="{isPending}">
          <option value="name">按名称排序</option>
          <option value="price">按价格排序</option>
        </select>
        {isPending && <span classname="loading">更新中...</span>}
      </div>

      <productgrid products="{filteredProducts}" ispending="{isPending}">
    </productgrid></div>
  );
}

// 将列表渲染抽离成独立组件,配合 memo 优化
const ProductGrid = memo(function ProductGrid({ products, isPending }) {
  return (
    <ul style="{{" opacity:="" ispending="" ?="" 0.6="" :="" 1="" }}="">
      {products.map(p => (
        <productitem key="{p.id}" product="{p}">
      ))}
    </productitem></ul>
  );
});

// 单个商品项也使用 memo
const ProductItem = memo(function ProductItem({ product }) {
  return (
    <li classname="product-item">
      <h3>{product.name}</h3>
      <p classname="price">¥{product.price}</p>
      <span classname="category">{product.category}</span>
    </li>
  );
});

优化效果对比

在我们的测试中(10,000 条数据):

指标 优化前 优化后 提升
输入延迟 250ms 16ms 93% ↓
筛选响应时间 300ms 50ms 83% ↓
渲染次数(快速输入 10 个字符) 10 次 2-3 次 70% ↓
首次渲染时间 180ms 180ms 无变化

关键优化点:

  1. useDeferredValue 延迟搜索词更新,减少过滤计算次数

  2. useTransition 将筛选和排序标记为非紧急,保持 UI 响应

  3. useMemo 缓存计算结果,避免重复计算

  4. memo 避免子组件不必要的重新渲染

  5. 视觉反馈 使用 isPending 提供加载状态

进一步优化:虚拟滚动

对于超大列表(10,000+ 条),即使使用并发特性,渲染所有 DOM 节点仍然会有性能问题。这时可以配合虚拟滚动库(如 react-windowreact-virtual):

javascript 复制代码
import { FixedSizeList } from 'react-window';

function ProductGrid({ products, isPending }) {
  const Row = ({ index, style }) => (
    <div style="{style}">
      <productitem product="{products[index]}">
    </productitem></div>
  );

  return (
    <fixedsizelist height="{600}" itemcount="{products.length}" itemsize="{80}" width="100%" style="{{" opacity:="" ispending="" ?="" 0.6="" :="" 1="" }}="">
      {Row}
    </fixedsizelist>
  );
}

虚拟滚动 + 并发特性的组合,可以轻松处理 100,000+ 条数据的列表。

八、最佳实践与注意事项

基于实际项目经验,总结以下最佳实践。

何时使用并发特性

应该使用的场景:

  • 搜索框实时过滤大量数据

  • 复杂表单的联动计算

  • 标签页切换加载数据

  • 大型列表的排序和筛选

  • 图表数据的实时更新

不需要使用的场景:

  • 简单的表单输入(几个字段)

  • 小型列表(< 100 条)

  • 静态内容展示

  • 已经足够快的操作

记住:并发特性是优化工具,不是必需品。不要为了使用而使用。

常见陷阱

1. 在 startTransition 中使用 async/await

javascript 复制代码
// ❌ 错误
startTransition(async () => {
  const data = await fetchData();
  setData(data);
});

// ✅ 正确
async function loadData() {
  const data = await fetchData();
  startTransition(() => {
    setData(data);
  });
}

2. 忘记使用 memo

useDeferredValue 配合 memo 才能发挥最大效果:

javascript 复制代码
// ❌ 效果不佳:子组件每次都重新渲染
function Parent() {
  const deferredValue = useDeferredValue(value);
  return <child value="{deferredValue}">;
}

// ✅ 正确:子组件只在 deferredValue 变化时渲染
const Child = memo(function Child({ value }) {
  // ...
});
</child>

3. 过度使用 useTransition

不是所有状态更新都需要 useTransition。只有当更新确实耗时且会阻塞 UI 时才使用。

javascript 复制代码
// ❌ 没必要
startTransition(() => {
  setCount(count + 1); // 简单的计数器不需要
});

// ✅ 有必要
startTransition(() => {
  setFilteredList(expensiveFilter(list, query)); // 耗时操作
});

性能监控

使用 React DevTools Profiler 监控并发特性的效果:

  1. 打开 React DevTools

  2. 切换到 Profiler 标签

  3. 点击录制按钮

  4. 执行你要测试的操作

  5. 查看渲染时间和次数

关注以下指标:

  • Commit duration:每次提交的耗时

  • Render count:渲染次数

  • Interactions:用户操作的响应时间

兼容性考虑

React 18 的并发特性是可选的,不会破坏现有代码。但需要注意:

  • 确保使用 ReactDOM.createRoot 而不是 ReactDOM.render

  • 第三方库需要支持并发模式(大部分主流库已支持)

  • 严格模式下,组件会被双重调用以检测副作用

javascript 复制代码
// React 18 的正确启动方式
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(<app>);
</app>

调试技巧

当并发特性行为不符合预期时:

  1. 添加日志 :在组件中添加 console.log 查看渲染次数

  2. 使用 Profiler API:编程方式收集性能数据

  3. 检查 key:确保列表项有稳定的 key

  4. 验证纯函数:确保渲染函数没有副作用

javascript 复制代码
import { Profiler } from 'react';

function onRenderCallback(
  id, // 发生提交的 Profiler 树的 "id"
  phase, // "mount" (首次挂载) 或 "update" (重新渲染)
  actualDuration, // 本次更新花费的渲染时间
  baseDuration, // 不使用 memoization 的情况下渲染整棵子树需要的时间
  startTime, // 本次更新开始渲染的时间
  commitTime, // 本次更新提交的时间
  interactions // 本次更新的 interactions 集合
) {
  console.log(`${id} ${phase} took ${actualDuration}ms`);
}

<profiler id="ProductList" onrender="{onRenderCallback}"><productlist></productlist></profiler>

总结

React 18 的并发特性是前端性能优化的重要里程碑。通过本文,我们深入学习了:

  1. 自动批处理让所有场景下的状态更新都能自动合并,无需修改代码即可获得性能提升

  2. useTransition 允许我们区分紧急和非紧急更新,保持 UI 始终响应用户操作,特别适合搜索、过滤等场景

  3. useDeferredValue 提供了更简洁的方式来延迟派生状态的更新,配合 memo 使用效果最佳

  4. Suspense 配合并发渲染能提供更流畅的加载体验,避免页面闪烁

  5. 并发渲染的底层原理基于优先级调度和时间切片,让 React 能够中断和恢复渲染工作

在实际项目中,建议采用渐进式的方式引入这些特性。先从最明显的性能瓶颈入手,使用 useTransitionuseDeferredValue 优化,然后逐步扩展到其他场景。记住,并发特性是优化工具,不是必需品,只在真正需要时使用才能发挥最大价值。

参考

  1. React 18 官方文档 - Concurrent Features

  2. React Working Group - Concurrent Rendering

  3. useTransition API 参考

  4. useDeferredValue API 参考

  5. React Conf 2021 - Concurrent Features

相关推荐
恋猫de小郭1 小时前
Android 性能迎来提升:内核引入 AutoFDO 普惠所有 15-16 设备
android·前端·flutter
小霍同学1 小时前
Vue 动态表单(Dynamic Form)
前端·vue.js
Dragon Wu1 小时前
Taro 小程序开发注意事项(不定期记录更新)
前端·javascript·小程序·typescript·taro
wangfpp2 小时前
多端统一你真的会了吗?
前端·javascript·架构
小霍同学2 小时前
Vue 动态组件(Dynamic Components)
前端·vue.js
代码煮茶2 小时前
Vue3 组件封装实战 | 从 0 封装一个可复用的表格组件(附插槽 / Props 设计)
前端·vue.js
兜兜风2 小时前
从零部署 OpenClaw:打造你的第二大脑
前端·后端
Maimai108082 小时前
Next.js 16 缓存策略详解:从旧模型到 Cache Components
开发语言·前端·javascript·react.js·缓存·前端框架·reactjs
凌览2 小时前
OpenClaw创始人炮轰腾讯"只抄不养",腾讯喊冤
前端·后端