Flutter 的异步问题,为什么和前端 Promise 问题高度相似?


网罗开发 (小红书、快手、视频号同名)

大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等方向。在移动端开发、鸿蒙开发、物联网、嵌入式、云原生、开源等领域有深厚造诣。

图书作者:《ESP32-C3 物联网工程开发实战》
图书作者:《SwiftUI 入门,进阶与实战》
超级个体:COC上海社区主理人
特约讲师:大学讲师,谷歌亚马逊分享嘉宾
科技博主:华为HDE/HDG

我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用、前沿科技资讯、产品评测与使用体验 。我特别关注云服务产品评测、AI 产品对比、开发板性能测试以及技术报告,同时也会提供产品优缺点分析、横向对比,并分享技术沙龙与行业大会的参会体验。我的目标是为读者提供有深度、有实用价值的技术洞察与分析。

展菲:您的前沿技术领航员

👋 大家好,我是展菲!

📱 全网搜索"展菲",即可纵览我在各大平台的知识足迹。

📣 公众号"Swift社区",每周定时推送干货满满的技术长文,从新兴框架的剖析到运维实战的复盘,助您技术进阶之路畅通无阻。

💬 微信端添加好友"fzhanfei",与我直接交流,不管是项目瓶颈的求助,还是行业趋势的探讨,随时畅所欲言。

📅 最新动态:2025 年 3 月 17 日

快来加入技术社区,一起挖掘技术的无限潜能,携手迈向数字化新征程!

文章目录

前言

最近在做一个 Flutter 项目的时候,遇到了一个经典的异步问题:页面销毁后还在调用 setState,导致应用崩溃。这个问题让我想起了前端开发中经常遇到的 Promise race condition,还有 iOS 开发中的异步任务管理。仔细一想,这三个平台的异步问题其实高度相似,都是异步操作和生命周期管理之间的矛盾。

今天我们就来聊聊这三个平台的异步问题,看看它们为什么这么相似,以及如何用一套通用的防御模式来解决这些问题。

异步和生命周期的通病

不管是 Flutter、前端还是 iOS,异步操作都有一个共同的特点:它们不会立即完成,而是在未来的某个时间点才返回结果。这就带来了一个问题:当异步操作还在进行中的时候,用户可能已经离开了当前页面,或者组件已经被销毁了。

Flutter 中的典型场景

在 Flutter 中,最常见的场景是这样的:

dart 复制代码
class UserProfilePage extends StatefulWidget {
  @override
  _UserProfilePageState createState() => _UserProfilePageState();
}

class _UserProfilePageState extends State<UserProfilePage> {
  String? userName;
  
  @override
  void initState() {
    super.initState();
    // 发起异步请求获取用户信息
    fetchUserInfo();
  }
  
  Future<void> fetchUserInfo() async {
    // 模拟网络请求
    await Future.delayed(Duration(seconds: 2));
    
    // 问题来了:如果用户在这 2 秒内离开了页面,这里调用 setState 就会报错
    setState(() {
      userName = "张三";
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("用户信息")),
      body: Center(
        child: Text(userName ?? "加载中..."),
      ),
    );
  }
}

这段代码看起来没什么问题,但实际上有一个严重的 bug:如果用户在 fetchUserInfo 还没完成的时候就离开了页面,setState 就会在已经销毁的 State 对象上调用,导致应用崩溃。

前端 React 中的类似问题

在前端开发中,这个问题同样存在:

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

function UserProfile() {
  const [userName, setUserName] = useState(null);
  
  useEffect(() => {
    // 发起异步请求
    fetchUserInfo();
  }, []);
  
  async function fetchUserInfo() {
    // 模拟网络请求
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    // 问题:如果组件已经卸载,这里调用 setState 就会报错
    setUserName("张三");
  }
  
  return (
    <div>
      <h1>{userName || "加载中..."}</h1>
    </div>
  );
}

React 会警告你:"Can't perform a React state update on an unmounted component",但不会直接崩溃。不过这个问题在 React Native 中会更严重,因为 RN 的错误处理机制和 Web 不太一样。

iOS 中的类似问题

在 iOS 开发中,这个问题同样存在,但表现形式略有不同:

swift 复制代码
class UserProfileViewController: UIViewController {
    var userNameLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        fetchUserInfo()
    }
    
    func fetchUserInfo() {
        // 发起异步请求
        Task {
            // 模拟网络请求
            try? await Task.sleep(for: .seconds(2))
            
            // 问题:如果视图控制器已经被销毁,这里更新 UI 可能会有问题
            userNameLabel.text = "张三"
        }
    }
}

虽然 Swift 的 async/await 机制相对更安全一些,但如果你不检查视图控制器的状态,同样可能出现问题。

Flutter 的 mounted 检查机制

Flutter 提供了一个 mounted 属性来检查 State 对象是否还在 widget 树中。这是 Flutter 的防御机制,但需要开发者手动检查。

mounted 属性的作用

mounted 是一个布尔值,表示当前的 State 对象是否还在 widget 树中。当 widget 被销毁时,mounted 会变成 false

dart 复制代码
class _UserProfilePageState extends State<UserProfilePage> {
  String? userName;
  
  @override
  void initState() {
    super.initState();
    fetchUserInfo();
  }
  
  Future<void> fetchUserInfo() async {
    await Future.delayed(Duration(seconds: 2));
    
    // 关键:在调用 setState 之前检查 mounted
    if (!mounted) {
      return; // 如果已经销毁,直接返回
    }
    
    setState(() {
      userName = "张三";
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("用户信息")),
      body: Center(
        child: Text(userName ?? "加载中..."),
      ),
    );
  }
}

这样就能避免在已销毁的 State 对象上调用 setState 了。

mounted 检查的最佳实践

在实际开发中,我们应该在每次异步操作完成后都检查 mounted

dart 复制代码
Future<void> fetchUserInfo() async {
  try {
    // 第一步:发起网络请求
    final response = await http.get(Uri.parse('https://api.example.com/user'));
    
    // 检查 mounted
    if (!mounted) return;
    
    // 第二步:解析数据
    final data = json.decode(response.body);
    
    // 再次检查 mounted(因为解析可能也需要时间)
    if (!mounted) return;
    
    // 第三步:更新状态
    setState(() {
      userName = data['name'];
    });
  } catch (e) {
    // 错误处理也要检查 mounted
    if (!mounted) return;
    
    setState(() {
      // 显示错误信息
    });
  }
}

mounted 的局限性

虽然 mounted 能解决大部分问题,但它也有一些局限性:

  1. 需要手动检查:每次异步操作后都要记得检查,容易遗漏
  2. 不能防止所有问题 :如果异步操作中还有其他副作用(比如更新全局状态、发送日志等),mounted 检查也帮不上忙
  3. 代码冗余 :每个异步方法都要写 if (!mounted) return;,代码会变得很冗长

React Native 的 isMounted 检查

React Native 也有类似的机制,但历史上有一些变化。

React Native 的 isMounted 问题

在早期的 React Native 版本中,有一个 isMounted 方法,但后来被移除了,因为 React 团队认为这个方法容易被误用。不过在实际开发中,我们仍然需要类似的检查机制。

javascript 复制代码
import React, { useState, useEffect, useRef } from 'react';

function UserProfile() {
  const [userName, setUserName] = useState(null);
  const isMountedRef = useRef(true);
  
  useEffect(() => {
    // 组件挂载时设置为 true
    isMountedRef.current = true;
    
    fetchUserInfo();
    
    // 组件卸载时设置为 false
    return () => {
      isMountedRef.current = false;
    };
  }, []);
  
  async function fetchUserInfo() {
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    // 检查组件是否还在挂载状态
    if (!isMountedRef.current) {
      return;
    }
    
    setUserName("张三");
  }
  
  return (
    <div>
      <h1>{userName || "加载中..."}</h1>
    </div>
  );
}

React 18 的 StrictMode 双重渲染问题

在 React 18 中,还有一个新问题:StrictMode 会在开发环境下双重渲染组件,这会导致异步请求被执行两次。这虽然不是生命周期问题,但也和异步操作有关。

javascript 复制代码
useEffect(() => {
  let cancelled = false;
  
  async function fetchUserInfo() {
    const response = await fetch('/api/user');
    const data = await response.json();
    
    // 检查是否被取消
    if (!cancelled) {
      setUserName(data.name);
    }
  }
  
  fetchUserInfo();
  
  // 清理函数
  return () => {
    cancelled = true;
  };
}, []);

为什么 iOS 原生更安全

iOS 的异步机制相对来说更安全一些,主要有以下几个原因:

Swift 的结构化并发

Swift 5.5 引入的 async/await 和结构化并发机制,让异步代码更容易管理:

swift 复制代码
class UserProfileViewController: UIViewController {
    var userNameLabel: UILabel!
    private var fetchTask: Task<Void, Never>?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        fetchUserInfo()
    }
    
    func fetchUserInfo() {
        // 取消之前的任务(如果有)
        fetchTask?.cancel()
        
        // 创建新任务
        fetchTask = Task { @MainActor in
            do {
                // 模拟网络请求
                try await Task.sleep(for: .seconds(2))
                
                // 检查任务是否被取消
                try Task.checkCancellation()
                
                // 更新 UI(@MainActor 确保在主线程)
                userNameLabel.text = "张三"
            } catch {
                // 任务被取消或其他错误
                print("任务被取消或出错: \(error)")
            }
        }
    }
    
    deinit {
        // 视图控制器销毁时取消任务
        fetchTask?.cancel()
    }
}

Task 的取消机制

Swift 的 Task 提供了内置的取消机制,可以通过 Task.checkCancellation() 检查任务是否被取消:

swift 复制代码
func fetchUserInfo() async throws {
    // 第一步:发起网络请求
    let (data, _) = try await URLSession.shared.data(from: url)
    
    // 检查任务是否被取消
    try Task.checkCancellation()
    
    // 第二步:解析数据
    let user = try JSONDecoder().decode(User.self, from: data)
    
    // 再次检查任务是否被取消
    try Task.checkCancellation()
    
    // 第三步:更新 UI
    await MainActor.run {
        userNameLabel.text = user.name
    }
}

@MainActor 的线程安全保证

Swift 的 @MainActor 注解可以确保代码在主线程执行,这对于 UI 更新非常重要:

swift 复制代码
@MainActor
class UserProfileViewController: UIViewController {
    // 所有属性和方法都在主线程执行
    var userNameLabel: UILabel!
    
    func updateUI() {
        // 这里不需要手动切换到主线程
        userNameLabel.text = "张三"
    }
}

强类型系统的帮助

Swift 的强类型系统可以在编译时发现一些问题,比如类型不匹配、可选值未处理等,这可以减少运行时错误。

一套跨端通用的异步防御模式

虽然三个平台的机制不同,但我们可以总结出一套通用的防御模式,让异步代码更安全。

模式一:生命周期检查

在异步操作完成后,检查组件/视图是否还在生命周期内:

Flutter 版本:

dart 复制代码
Future<void> fetchUserInfo() async {
  await Future.delayed(Duration(seconds: 2));
  
  if (!mounted) return; // 生命周期检查
  
  setState(() {
    userName = "张三";
  });
}

React/React Native 版本:

javascript 复制代码
useEffect(() => {
  let isMounted = true;
  
  async function fetchUserInfo() {
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    if (!isMounted) return; // 生命周期检查
    
    setUserName("张三");
  }
  
  fetchUserInfo();
  
  return () => {
    isMounted = false; // 清理
  };
}, []);

iOS 版本:

swift 复制代码
func fetchUserInfo() {
    fetchTask = Task { @MainActor in
        do {
            try await Task.sleep(for: .seconds(2))
            
            try Task.checkCancellation() // 任务取消检查
            
            userNameLabel.text = "张三"
        } catch {
            // 任务被取消
        }
    }
}

deinit {
    fetchTask?.cancel() // 清理
}

模式二:任务取消机制

在组件/视图销毁时,主动取消正在进行的异步任务:

Flutter 版本:

dart 复制代码
class _UserProfilePageState extends State<UserProfilePage> {
  Future<void>? _fetchTask;
  
  @override
  void initState() {
    super.initState();
    _fetchTask = fetchUserInfo();
  }
  
  Future<void> fetchUserInfo() async {
    await Future.delayed(Duration(seconds: 2));
    
    if (!mounted) return;
    
    setState(() {
      userName = "张三";
    });
  }
  
  @override
  void dispose() {
    // 取消任务(虽然 Future 不能直接取消,但可以通过检查 mounted 来模拟)
    _fetchTask = null;
    super.dispose();
  }
}

React/React Native 版本:

javascript 复制代码
useEffect(() => {
  const controller = new AbortController();
  
  async function fetchUserInfo() {
    try {
      const response = await fetch('/api/user', {
        signal: controller.signal // 使用 AbortSignal
      });
      const data = await response.json();
      setUserName(data.name);
    } catch (error) {
      if (error.name === 'AbortError') {
        console.log('请求被取消');
      }
    }
  }
  
  fetchUserInfo();
  
  return () => {
    controller.abort(); // 取消请求
  };
}, []);

iOS 版本:

swift 复制代码
private var fetchTask: Task<Void, Never>?

func fetchUserInfo() {
    fetchTask?.cancel() // 取消之前的任务
    fetchTask = Task {
        // 异步操作
    }
}

deinit {
    fetchTask?.cancel() // 清理
}

模式三:使用状态管理库

使用状态管理库(如 Provider、Riverpod、Redux 等)来管理异步状态,这些库通常内置了生命周期管理:

Flutter + Riverpod 版本:

dart 复制代码
final userProvider = FutureProvider<User>((ref) async {
  // Riverpod 会自动处理生命周期
  final response = await http.get(Uri.parse('https://api.example.com/user'));
  return User.fromJson(json.decode(response.body));
});

class UserProfilePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider);
    
    return userAsync.when(
      data: (user) => Text(user.name),
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => Text('错误: $error'),
    );
  }
}

React + React Query 版本:

javascript 复制代码
import { useQuery } from 'react-query';

function UserProfile() {
  // React Query 会自动处理生命周期和缓存
  const { data, isLoading, error } = useQuery('user', fetchUserInfo);
  
  if (isLoading) return <div>加载中...</div>;
  if (error) return <div>错误: {error.message}</div>;
  
  return <div>{data.name}</div>;
}

模式四:封装通用工具函数

我们可以封装一些通用的工具函数,让异步操作更安全:

Flutter 版本:

dart 复制代码
extension SafeSetState on State {
  void safeSetState(VoidCallback fn) {
    if (mounted) {
      setState(fn);
    }
  }
}

// 使用
Future<void> fetchUserInfo() async {
  await Future.delayed(Duration(seconds: 2));
  
  safeSetState(() {
    userName = "张三";
  });
}

React 版本:

javascript 复制代码
function useSafeState(initialState) {
  const [state, setState] = useState(initialState);
  const isMountedRef = useRef(true);
  
  useEffect(() => {
    return () => {
      isMountedRef.current = false;
    };
  }, []);
  
  const safeSetState = useCallback((newState) => {
    if (isMountedRef.current) {
      setState(newState);
    }
  }, []);
  
  return [state, safeSetState];
}

// 使用
function UserProfile() {
  const [userName, setUserName] = useSafeState(null);
  
  useEffect(() => {
    fetchUserInfo().then(setUserName);
  }, []);
}

iOS 版本:

swift 复制代码
extension UIViewController {
    func safeAsyncUpdate<T>(_ task: Task<T, Error>, update: @escaping (T) -> Void) {
        Task { @MainActor in
            do {
                let result = try await task.value
                if !isViewLoaded || view.window == nil {
                    return // 视图未加载或不在窗口中
                }
                update(result)
            } catch {
                // 处理错误
            }
        }
    }
}

// 使用
func fetchUserInfo() {
    let task = Task {
        // 异步操作
        return "张三"
    }
    
    safeAsyncUpdate(task) { name in
        userNameLabel.text = name
    }
}

实际场景中的应用

让我们看看这些防御模式在实际项目中的应用。

场景一:列表页面的数据加载

在列表页面中,用户可能会快速滚动,导致多个异步请求同时进行。我们需要确保只有最新的请求结果才会更新 UI。

Flutter 版本:

dart 复制代码
class ProductListPage extends StatefulWidget {
  @override
  _ProductListPageState createState() => _ProductListPageState();
}

class _ProductListPageState extends State<ProductListPage> {
  List<Product> products = [];
  int currentPage = 1;
  bool isLoading = false;
  Future<void>? _loadTask;
  
  @override
  void initState() {
    super.initState();
    loadProducts();
  }
  
  Future<void> loadProducts() async {
    if (isLoading) return;
    
    setState(() {
      isLoading = true;
    });
    
    _loadTask = _fetchProducts();
    await _loadTask;
    
    if (!mounted) return;
    
    setState(() {
      isLoading = false;
    });
  }
  
  Future<void> _fetchProducts() async {
    // 模拟网络请求
    await Future.delayed(Duration(seconds: 1));
    
    if (!mounted) return;
    
    final newProducts = List.generate(10, (i) => Product(id: i));
    
    if (!mounted) return;
    
    setState(() {
      products.addAll(newProducts);
      currentPage++;
    });
  }
  
  @override
  void dispose() {
    _loadTask = null;
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: products.length,
      itemBuilder: (context, index) {
        return ListTile(title: Text(products[index].name));
      },
    );
  }
}

React 版本:

javascript 复制代码
function ProductList() {
  const [products, setProducts] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [isLoading, setIsLoading] = useState(false);
  const abortControllerRef = useRef(null);
  
  useEffect(() => {
    loadProducts();
    
    return () => {
      // 清理:取消正在进行的请求
      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
      }
    };
  }, []);
  
  async function loadProducts() {
    if (isLoading) return;
    
    setIsLoading(true);
    
    // 取消之前的请求
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }
    
    // 创建新的 AbortController
    abortControllerRef.current = new AbortController();
    
    try {
      const response = await fetch(`/api/products?page=${currentPage}`, {
        signal: abortControllerRef.current.signal
      });
      
      const data = await response.json();
      
      setProducts(prev => [...prev, ...data.products]);
      setCurrentPage(prev => prev + 1);
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('加载失败:', error);
      }
    } finally {
      setIsLoading(false);
    }
  }
  
  return (
    <div>
      {products.map(product => (
        <div key={product.id}>{product.name}</div>
      ))}
    </div>
  );
}

场景二:搜索功能的防抖处理

在搜索功能中,用户输入时会触发多个搜索请求,我们需要确保只处理最新的搜索结果。

Flutter 版本:

dart 复制代码
class SearchPage extends StatefulWidget {
  @override
  _SearchPageState createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  String _searchQuery = '';
  List<String> _searchResults = [];
  Timer? _debounceTimer;
  Future<void>? _searchTask;
  
  void onSearchChanged(String query) {
    _searchQuery = query;
    
    // 取消之前的防抖定时器
    _debounceTimer?.cancel();
    
    // 设置新的防抖定时器
    _debounceTimer = Timer(Duration(milliseconds: 500), () {
      performSearch(query);
    });
  }
  
  Future<void> performSearch(String query) async {
    if (query.isEmpty) {
      setState(() {
        _searchResults = [];
      });
      return;
    }
    
    // 取消之前的搜索任务
    _searchTask = null;
    
    _searchTask = _fetchSearchResults(query);
    await _searchTask;
  }
  
  Future<void> _fetchSearchResults(String query) async {
    // 模拟网络请求
    await Future.delayed(Duration(seconds: 1));
    
    if (!mounted) return;
    
    // 检查查询是否还是最新的
    if (_searchQuery != query) return;
    
    final results = ['结果1', '结果2', '结果3'];
    
    if (!mounted) return;
    
    setState(() {
      _searchResults = results;
    });
  }
  
  @override
  void dispose() {
    _debounceTimer?.cancel();
    _searchTask = null;
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          TextField(
            onChanged: onSearchChanged,
            decoration: InputDecoration(hintText: "搜索"),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: _searchResults.length,
              itemBuilder: (context, index) {
                return ListTile(title: Text(_searchResults[index]));
              },
            ),
          ),
        ],
      ),
    );
  }
}

React 版本:

javascript 复制代码
function SearchPage() {
  const [searchQuery, setSearchQuery] = useState('');
  const [searchResults, setSearchResults] = useState([]);
  const abortControllerRef = useRef(null);
  
  useEffect(() => {
    const timer = setTimeout(() => {
      performSearch(searchQuery);
    }, 500);
    
    return () => {
      clearTimeout(timer);
    };
  }, [searchQuery]);
  
  async function performSearch(query) {
    if (!query) {
      setSearchResults([]);
      return;
    }
    
    // 取消之前的请求
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }
    
    abortControllerRef.current = new AbortController();
    
    try {
      const response = await fetch(`/api/search?q=${query}`, {
        signal: abortControllerRef.current.signal
      });
      
      const data = await response.json();
      
      // 检查查询是否还是最新的
      if (abortControllerRef.current.signal.aborted) return;
      
      setSearchResults(data.results);
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('搜索失败:', error);
      }
    }
  }
  
  return (
    <div>
      <input
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
        placeholder="搜索"
      />
      <ul>
        {searchResults.map((result, index) => (
          <li key={index}>{result}</li>
        ))}
      </ul>
    </div>
  );
}

总结

Flutter、前端和 iOS 的异步问题确实高度相似,核心都是异步操作和生命周期管理之间的矛盾。虽然每个平台的解决方案略有不同,但我们可以总结出一套通用的防御模式:

  1. 生命周期检查:在异步操作完成后,检查组件/视图是否还在生命周期内
  2. 任务取消机制:在组件/视图销毁时,主动取消正在进行的异步任务
  3. 使用状态管理库:利用成熟的状态管理库,它们通常内置了生命周期管理
  4. 封装通用工具函数:创建一些通用的工具函数,让异步操作更安全

在实际开发中,我们应该根据项目的具体情况选择合适的方案。对于简单的场景,手动检查生命周期就足够了;对于复杂的场景,使用状态管理库会更省心。

最重要的是,我们要时刻记住:异步操作是有延迟的,在异步操作完成之前,用户可能已经离开了当前页面。所以,每次异步操作完成后,都要检查一下组件/视图是否还在生命周期内,这样才能避免崩溃和内存泄漏。

希望这篇文章能帮助你更好地理解跨平台的异步问题,写出更安全的异步代码!

完整可运行 Demo 代码

下面是一个完整的 Flutter Demo,展示了如何正确处理异步操作和生命周期:

dart 复制代码
import 'package:flutter/material.dart';
import 'dart:async';

// 工具类:安全的 setState 扩展
extension SafeSetState on State {
  void safeSetState(VoidCallback fn) {
    if (mounted) {
      setState(fn);
    }
  }
}

// 用户信息模型
class User {
  final String name;
  final String email;
  
  User({required this.name, required this.email});
}

// 用户信息页面
class UserProfilePage extends StatefulWidget {
  @override
  _UserProfilePageState createState() => _UserProfilePageState();
}

class _UserProfilePageState extends State<UserProfilePage> {
  User? user;
  bool isLoading = false;
  String? errorMessage;
  Future<void>? _fetchTask;
  
  @override
  void initState() {
    super.initState();
    fetchUserInfo();
  }
  
  Future<void> fetchUserInfo() async {
    safeSetState(() {
      isLoading = true;
      errorMessage = null;
    });
    
    _fetchTask = _performFetch();
    await _fetchTask;
  }
  
  Future<void> _performFetch() async {
    try {
      // 模拟网络请求延迟
      await Future.delayed(Duration(seconds: 2));
      
      // 检查生命周期
      if (!mounted) return;
      
      // 模拟数据解析
      await Future.delayed(Duration(milliseconds: 500));
      
      // 再次检查生命周期
      if (!mounted) return;
      
      // 模拟获取到的用户数据
      final fetchedUser = User(
        name: "张三",
        email: "zhangsan@example.com",
      );
      
      // 更新 UI
      safeSetState(() {
        user = fetchedUser;
        isLoading = false;
      });
    } catch (e) {
      // 错误处理也要检查生命周期
      if (!mounted) return;
      
      safeSetState(() {
        errorMessage = "加载失败: $e";
        isLoading = false;
      });
    }
  }
  
  @override
  void dispose() {
    // 清理:虽然 Future 不能直接取消,但可以通过检查 mounted 来模拟
    _fetchTask = null;
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("用户信息"),
      ),
      body: Center(
        child: _buildContent(),
      ),
    );
  }
  
  Widget _buildContent() {
    if (isLoading) {
      return CircularProgressIndicator();
    }
    
    if (errorMessage != null) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            errorMessage!,
            style: TextStyle(color: Colors.red),
          ),
          SizedBox(height: 16),
          ElevatedButton(
            onPressed: fetchUserInfo,
            child: Text("重试"),
          ),
        ],
      );
    }
    
    if (user == null) {
      return Text("暂无数据");
    }
    
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          "姓名: ${user!.name}",
          style: TextStyle(fontSize: 20),
        ),
        SizedBox(height: 16),
        Text(
          "邮箱: ${user!.email}",
          style: TextStyle(fontSize: 16),
        ),
      ],
    );
  }
}

// 搜索页面示例
class SearchPage extends StatefulWidget {
  @override
  _SearchPageState createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  String _searchQuery = '';
  List<String> _searchResults = [];
  Timer? _debounceTimer;
  Future<void>? _searchTask;
  
  void onSearchChanged(String query) {
    setState(() {
      _searchQuery = query;
    });
    
    // 取消之前的防抖定时器
    _debounceTimer?.cancel();
    
    // 设置新的防抖定时器
    _debounceTimer = Timer(Duration(milliseconds: 500), () {
      performSearch(query);
    });
  }
  
  Future<void> performSearch(String query) async {
    if (query.isEmpty) {
      safeSetState(() {
        _searchResults = [];
      });
      return;
    }
    
    _searchTask = _fetchSearchResults(query);
    await _searchTask;
  }
  
  Future<void> _fetchSearchResults(String query) async {
    try {
      // 模拟网络请求
      await Future.delayed(Duration(seconds: 1));
      
      // 检查生命周期
      if (!mounted) return;
      
      // 检查查询是否还是最新的
      if (_searchQuery != query) return;
      
      // 模拟搜索结果
      final results = List.generate(
        5,
        (index) => "搜索结果 ${index + 1} for '$query'",
      );
      
      // 更新 UI
      safeSetState(() {
        _searchResults = results;
      });
    } catch (e) {
      if (!mounted) return;
      
      safeSetState(() {
        _searchResults = [];
      });
    }
  }
  
  @override
  void dispose() {
    _debounceTimer?.cancel();
    _searchTask = null;
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("搜索"),
      ),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(16),
            child: TextField(
              onChanged: onSearchChanged,
              decoration: InputDecoration(
                hintText: "输入搜索关键词",
                border: OutlineInputBorder(),
              ),
            ),
          ),
          Expanded(
            child: _searchResults.isEmpty
                ? Center(
                    child: Text(
                      _searchQuery.isEmpty
                          ? "请输入搜索关键词"
                          : "搜索中...",
                    ),
                  )
                : ListView.builder(
                    itemCount: _searchResults.length,
                    itemBuilder: (context, index) {
                      return ListTile(
                        title: Text(_searchResults[index]),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}

// 主应用
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '异步防御模式 Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("异步防御模式 Demo"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => UserProfilePage(),
                  ),
                );
              },
              child: Text("用户信息页面"),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => SearchPage(),
                  ),
                );
              },
              child: Text("搜索页面"),
            ),
          ],
        ),
      ),
    );
  }
}

这个 Demo 展示了:

  1. 安全的 setState 扩展 :通过 safeSetState 方法,自动检查 mounted 状态
  2. 用户信息页面:展示了如何在异步操作中正确检查生命周期
  3. 搜索页面:展示了防抖处理和生命周期检查的结合使用
  4. 任务管理 :通过 _fetchTask_searchTask 来跟踪异步任务

你可以运行这个 Demo,快速切换页面来测试异步操作的防御机制是否正常工作。

相关推荐
网络点点滴3 小时前
前端与后端的区别与联系
前端
EnCi Zheng3 小时前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen3 小时前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技3 小时前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人3 小时前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实3 小时前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha4 小时前
三目运算符
linux·服务器·前端
晓晨的博客4 小时前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect4 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding4 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化