Dart 极速入门与全景对比指南(面向 TS / Vue 3 开发者)

0 阅读11分钟

Dart 极速入门与全景对比指南(面向 TS / Vue 3 开发者)

前言:如果你已经熟练掌握 TypeScript 并习惯 Vue 3 Composition API 的开发模式,那么恭喜你,你已经掌握了理解 Dart 语言 70% 以上的概念! Dart 与 TypeScript 在静态类型推断、泛型、现代异步语法上有大量共通之处,但 Dart 拥有真正的强运行时类型系统(Sound Type System)、基于类与 Mixin的面向对象架构,以及为 Flutter 深度优化的构造函数语法糖和响应机制。


1. 核心心智模型迁移(TS/Vue 3 vs Dart/Flutter)

image.png

关键运行机制差异速查

维度TypeScript / Node.js / 浏览器Dart / Flutter
类型系统结构化类型(鸭子类型),编译期类型擦除,运行时无保护名义类型(Nominal Typing)+ 健全空安全(Sound Null Safety),运行时类型真实存在
执行方式解释执行 / V8 JIT,依赖 JS 运行时开发期 JIT(实现秒级热重载) / 生产期 AOT(直接编译为 ARM/x64 原生机器码)
并发模型单线程 EventLoop + Web Worker(共享内存需 SharedArrayBuffer)单线程 EventLoop + Isolate(每个 Isolate 独享内存堆,通过 Port 纯消息通信,天然无锁)
UI 表达形式HTML 模板 / JSX + CSS 样式表一切皆代码(UI 即 Widget 树),没有单独的 HTML/CSS,样式通过 Widget 属性配置
响应式逻辑基于 ES6 Proxy 的隐式数据劫持 (ref, reactive)显式通知更新 (setState, ChangeNotifier) 或响应式数据流 (Riverpod, Bloc)

2. 变量、常量与数据类型对照

2.1 声明与不可变性:let / const vs var / final / const

在 TypeScript 中只有 letconst,而 Dart 中对不可变性有更细致的区分:

TypeScriptDart说明与最佳实践
let x = 1;var x = 1;int x = 1;声明可变变量。Dart 的 var 会自动做类型推断,一旦推断为 int,后续不能赋值为字符串。
const x = 1;final x = 1;运行时常量。变量只能赋值一次,值可以在运行时动态计算(例如从 API 获取)。
const x = 1;const x = 1;编译期常量。值必须在编译期间就能完全确定。在 Flutter 中极其重要,常量 Widget 不会在父级刷新时重复重建!
// Dart 示例
var count = 0;              // 推断为 int,后续 count = "hello" 会报错!
final now = DateTime.now(); // 正确:运行时才能计算出时间
// const bad = DateTime.now(); // ❌ 编译报错:DateTime.now() 不是编译期常量
const pi = 3.14159;          // 正确:编译期常量

2.2 基础类型映射表

TypeScript 类型Dart 对应类型注意事项与陷阱
numberintdouble (父类 num)高频陷阱:Dart 严格区分整数与浮点数。不能将 int 隐式传给 double,例如 double x = 1; 需写为 1.01.toDouble()
stringStringDart 字符串支持单引号/双引号;模板字符串使用 $var${expr}
booleanbool只有 truefalse没有 JS 的隐式 Truthy/Falsy 转换if (str) 必须写为 if (str.isNotEmpty)
anydynamic逃避静态检查,具有运行时动态派发能力,容易引发运行时错误,尽量少用。
unknownObject?任何类型的父类(包含 null)。必须进行类型收窄(is 检查)后才能调用方法。
voidvoid表示无返回值。
neverNever表示永不返回(如抛出异常的死循环函数)。
undefined无对应概念Dart 只有 null,没有 undefined

3. 空安全体系深度剖析(TS vs Dart Sound Null Safety)

这是 TS 开发者迁移到 Dart 时最需要适应、也最安全的特性:

  • TypeScript 的空安全是“非健全”的:TS 中即使定义了 name: string,一旦经过 JSON.parse、第三方库或者 as any,运行时它仍可能是 null / undefined,导致经典的 TypeError: Cannot read properties of undefined
  • Dart 的空安全是“健全(Sound)”的:如果一个类型是 String,编译器和运行时做绝对担保——它在内存中永远不可能是 null

语法对照

语法场景TypeScriptDart
可空类型声明let name: string | null;String? name;
安全调用(Optional Chaining)user?.profile?.avataruser?.profile?.avatar
空值合并(Nullish Coalescing)const name = input ?? "Default";final name = input ?? "Default";
空值赋值赋值符x ??= 10;x ??= 10;
非空断言(危险操作)element!.valueelement!.value(若为 null 会抛出运行时异常)
延迟初始化属性后加 ! 声明或构造函数赋初值late 关键字late String title;(承诺后续一定在访问前初始化)
// Dart 中的空安全代码
String? nickname; // 允许为 null

// 1. 安全解引用与兜底
String displayName = nickname ?? "Guest";

// 2. 类型自动提升(Type Promotion)
if (nickname != null) {
  // 在此作用域内,nickname 自动被提升为非空的 String 类型!
  print("长度: ${nickname.length}"); 
}

4. 集合与常用高阶函数对照

4.1 核心集合类型

概念TypeScript / JSDart
数组 / 列表[1, 2, 3] / Array<number><int>[1, 2, 3] / List<int>
对象 / 字典{ a: 1, b: 2 } / Record<string, number><String, int>{'a': 1, 'b': 2} / Map<String, int>
集合(去重)new Set([1, 2])<int>{1, 2} / Set<int>

4.2 Flutter 开发神技:Collection-if 与 Collection-for

在 TS/React/Vue 中,我们常通过数组拼接或三元表达式控制列表项;Dart 原生支持在集合字面量内写条件与循环(构建 UI 树时极度常用):

// 1. 扩展运算符(Spread Operator)
final listA = [1, 2];
final listB = [0, ...listA, 3]; // [0, 1, 2, 3]
List<int>? nullableList;
final listC = [0, ...?nullableList]; // 安全扩展

// 2. Collection-if(条件元素)
bool isVip = true;
final menus = [
  'Home',
  'Profile',
  if (isVip) 'VIP Center', // 如果 isVip 为 false,此项直接不存在
];

// 3. Collection-for(循环生成元素)
final rawNumbers = [1, 2, 3];
final labels = [
  'Header',
  for (var n in rawNumbers) 'Item #$n',
  'Footer',
];

4.3 常用高阶函数对照表(⚠️ 注意惰性求值陷阱)

[!WARNING] 最大踩坑点:JS 的 arr.map() 返回的是新 Array;而 Dart 的 list.map()list.where() 返回的是 Iterable<T>(惰性迭代器)! 如果你不调用 .toList(),它不仅不会真正求值,而且每次访问都会重新遍历计算!

操作JavaScript / TypeScriptDart
遍历arr.forEach(x => console.log(x))list.forEach((x) => print(x));
映射变换arr.map(x => x * 2)list.map((x) => x * 2).toList()
过滤查找arr.filter(x => x > 0)list.where((x) => x > 0).toList()
查找单个arr.find(x => x.id === 1)list.firstWhere((x) => x.id == 1, orElse: () => fallback)
存在性检查arr.some(x => x > 10)list.any((x) => x > 10)
全员检查arr.every(x => x > 0)list.every((x) => x > 0)
累加归纳arr.reduce((acc, cur) => acc + cur, 0)list.fold(0, (acc, cur) => acc + cur)

5. 函数与面向对象编程(OOP)

5.1 参数体系:命名参数(Flutter 组件的核心基石)

在 TS 中传配置项我们通常传一个对象 { name, age };而在 Dart 中原生内置了命名参数

// TypeScript: 通过对象解构模拟命名参数
interface UserParams {
  name: string;
  age?: number;
}
function createUser({ name, age = 18 }: UserParams) {}
createUser({ name: "Alice" });
// Dart: 原生支持命名参数(花括号包裹)
void createUser({
  required String name, // 必填命名参数
  int age = 18,         // 默认值
  String? address,      // 可空命名参数
}) {
  print('$name, $age, $address');
}

// 调用时非常清晰直观,Flutter 所有 Widget 都是这样构建的!
createUser(name: 'Alice', age: 20);

5.2 类与构造函数语法糖

class User {
  final String id;
  String name;
  int _internalScore; // 下划线 _ 开头表示私有成员(文件作用域私有)

  // 1. 语法糖构造函数(直接将参数赋值给同名字段)
  User(this.id, this.name, this._internalScore);

  // 2. 命名构造函数(类似工厂方法,但语义更清晰)
  User.guest() 
      : id = '0',
        name = 'Guest',
        _internalScore = 0; // 初始化列表(在构造体执行前执行)

  // 3. 工厂构造函数(类似静态工厂,可以返回已有实例或子类实例)
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      json['id'] as String,
      json['name'] as String,
      json['score'] as int? ?? 0,
    );
  }

  // 4. Getter 与 Setter(对比 TS 的 get / set)
  int get score => _internalScore;
  set score(int value) {
    if (value >= 0) _internalScore = value;
  }
}

5.3 接口、抽象类与 Mixin(能力混入)

  • 在 Dart 中没有 interface 关键字:任何一个普通过或者抽象类,自身隐式就是一个接口!其他类可以通过 implements 实现它所有的字段和方法。
  • mixin(混入):解决单继承限制,复用跨类行为(极其强大,Flutter 中大量使用,如 with SingleTickerProviderStateMixin)。
mixin LoggerMixin {
  void log(String message) {
    print('[LOG]: $message');
  }
}

class NetworkService with LoggerMixin {
  void fetchData() {
    log('正在获取数据...'); // 直接调用混入的能力
  }
}

5.4 Dart 3 现代语法:元组(Records)与模式匹配

如果你在 TS 中习惯了元组 [number, string] 和解构赋值,Dart 3 带来了完全等价且更强大的特性:

// 1. Records(多元组支持,可命名或按位置)
(String, int) getUser() => ('Alice', 25);
({String name, int age}) getNamedUser() => (name: 'Bob', age: 30);

final (name, age) = getUser(); // 解构赋值

// 2. Pattern Matching 与 Switch 表达式(替代冗长的 if-else)
String getStatusDescription(int statusCode) => switch (statusCode) {
  200 => '成功',
  400 || 404 => '客户端错误',
  >= 500 && <= 599 => '服务端异常',
  _ => '未知状态', // _ 对应 default
};

6. 异步编程体系:Promise vs Future & Stream

6.1 Promise 与 Future 对照

概念JavaScript / TypeScriptDart
基础类型Promise<T>Future<T>
异步函数声明async function(): Promise<T>Future<T> func() async
等待完成await promiseawait future
捕获异常try { ... } catch (e) { ... }try { ... } catch (e, stackTrace) { ... }
并发等待全部Promise.all([p1, p2])Future.wait([f1, f2])
竞态等待首个Promise.race([p1, p2])Future.any([f1, f2])
微任务与延时queueMicrotask / setTimeout(fn, 1000)scheduleMicrotask / Future.delayed(Duration(seconds: 1))

6.2 Dart 特色:Stream(响应式事件流)

在 JS 前端中如果需要处理持续流式事件,我们通常需要引入 RxJS(Observable)。而 Dart 将 Stream 内置为第一公民

// 类似 async* 生成器函数产生数据流
Stream<int> countStream(int max) async* {
  for (int i = 1; i <= max; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i; // 持续吐出数据
  }
}

// 消费 Stream
void listenStream() async {
  await for (final value in countStream(5)) {
    print('接收到: $value');
  }
}
// 在 Flutter 中可以直接使用 StreamBuilder 组件自动响应 Stream 并渲染 UI!

7. 从 Vue 3 Composition API 到 Flutter 的心智映射

对于习惯了 Vue 3 的前端同学,理解 Flutter 的组件架构其实只需对应转换两个概念:“数据驱动视图”“生命周期”

classDiagram
    class Vue3_SFC {
        +ref() / reactive()
        +computed()
        +watch()
        +onMounted()
        +onUnmounted()
        +template DOM
    }
    class Flutter_Widget {
        +StatefulWidget + State
        +Getter / Riverpod StateProvider
        +didUpdateWidget() / Listener
        +initState()
        +dispose()
        +Widget build(context)
    }
    Vue3_SFC ..> Flutter_Widget : 概念一一映射

7.1 代码编写与状态对比范例

Vue 3 Composition API 写法:
<template>
  <div class="counter">
    <p>计数: {{ count }}</p>
    <p>翻倍: {{ doubleCount }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';

const count = ref(0);
const doubleCount = computed(() => count.value * 2);

const increment = () => {
  count.value++;
};

onMounted(() => console.log('组件已挂载'));
onUnmounted(() => console.log('组件已销毁'));
</script>
Flutter 等价实现(StatefulWidget 模式):
import 'package:flutter/material.dart';

class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0; // 对应 ref(0)

  // 对应 computed
  int get _doubleCount => _count * 2;

  void _increment() {
    // 显式触发 UI 重建(对比 Vue 的 proxy 自动触发)
    setState(() {
      _count++;
    });
  }

  @override
  void initState() {
    super.initState();
    print('组件已挂载 (对应 onMounted)');
  }

  @override
  void dispose() {
    print('组件已销毁 (对应 onUnmounted)');
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // 对应 template,全代码声明式 UI 树
    return Column(
      children: [
        Text('计数: $_count'),
        Text('翻倍: $_doubleCount'),
        ElevatedButton(
          onPressed: _increment,
          child: const Text('+1'),
        ),
      ],
    );
  }
}

7.2 概念映射矩阵

Vue 3 Composition APIFlutter / Dart 等价机制核心认知转变
<template>Widget build(BuildContext context)放弃模板字符串与 HTML 标签,UI 本身就是由构造函数层层嵌套实例化的 Widget 对象树。
ref(x) / reactive({})StatefulWidget 内的成员变量Vue 是在底层通过 Proxy 侦测变更并细粒度更新 DOM;Flutter 是通过调用 setState(() { ... }) 标记当前 State 为 dirty 并触发 build() 重新执行。
computed(() => ...)Getter 属性 (int get doubleCount => ...)在 build 时按需计算,或者配合状态管理(如 Riverpod 的 Provider)。
propsWidget 构造函数中的 final 成员变量Flutter Widget 必须是不可变(Immutable)对象,传入的参数都是 final
emit('update', payload)回调函数属性(如 final ValueChanged<int>? onChanged;子组件触发 widget.onChanged?.call(newVal) 通知父级。
onMountedinitState()仅在 State 首次插入树时执行一次,用于初始化网络请求、控制器等。
onUnmounteddispose()组件从树中彻底移除时执行,必须在此处销毁 Controller、Timer、StreamSubscription 避免内存泄漏!
Pinia / VuexRiverpod / Bloc / Provider跨组件全局状态管理。对于 Vue 3 开发者,Riverpod 的思维模式与函数式组合最契合。

8. TS / Vue 开发者踩坑 TOP 10 避坑指南

⚠️ 坑 1:集合相等性比较(== 默认只比内存引用)

  • 现象[1, 2] == [1, 2] 在 Dart 中结果是 false
  • 解决:如需比较内容相等,使用 Flutter 的 listEquals(a, b) 或 package collection 中的 DeepCollectionEquality

⚠️ 坑 2:不要把数字当布尔值用

  • 现象:JS 中 if (count)if (list.length)if (str) 非常自由。
  • 解决:Dart 绝不允许隐式转换,必须写显式布尔表达式:if (count != 0)if (list.isNotEmpty)

⚠️ 坑 3:私有成员是“文件作用域”,不是“类作用域”

  • 现象:类中以 _ 开头的属性 _foo 是私有的,但同一文件内的其他类仍然可以直接访问它
  • 解决:Dart 的封装边界是**文件(Library)**而不是单个 Class。如果想真正隔离,拆分到不同 .dart 文件即可。

⚠️ 坑 4:as 类型转换是强校验的运行时操作

  • 现象:TS 中 x as string 只是骗过编译器,运行时什么都没做;而在 Dart 中 x as String运行时类型转换,如果 x 实际上不是 String,会立即抛出 TypeError 崩溃!
  • 解决:养成先用 is 检查的好习惯:
    if (data is String) {
      // 进入此块后自动智能提升为 String,无需手动 as
      print(data.toUpperCase());
    }
    

⚠️ 坑 5:构造函数中必须显式处理空安全与默认值

  • 现象:在 TS 接口中属性默认可选;在 Dart 类的命名参数中,如果没声明默认值且非空,必须加 required 关键字,否则编译直接报错。

⚠️ 坑 6:const 构造函数的关键性能意义

  • 现象:初学 Flutter 经常会忽略 IDE 提示的 Prefer const with constant constructors
  • 说明:加了 const Text('Hello') 后,Flutter 引擎会将其作为编译期单例常量放入内存缓存,当父组件重绘时,该 Widget 会被完全跳过比较和重建,能极大节省帧率消耗。

⚠️ 坑 7:不要在 setState 内部使用 await

  • 反模式
    // ❌ 错误示范
    setState(() async {
      final res = await api.fetch();
      data = res;
    });
    
  • 正确做法:先在外部 await 异步结果,然后将纯同步赋值包裹在 setState 中:
    // ✅ 正确示范
    final res = await api.fetch();
    if (!mounted) return; // 检查组件是否仍在屏幕上,避免已销毁时 setState 报错
    setState(() {
      data = res;
    });
    

⚠️ 坑 8:JSON 序列化没有开箱即用的 JSON.stringify / parse 变成带类型类

  • 现象:JS 中调用 API 返回的对象天然就是普通对象。而在 Dart 中,jsonDecode(str) 只能返回 Map<String, dynamic>,访问属性不能像 res.name 那样点出来,而是 res['name']
  • 解决:在 Dart 中通常需要为数据模型编写 fromJson 工厂构造函数,或者借助官方代码生成工具 json_serializable / freezed 自动生成。

⚠️ 坑 9:List 默认为可变,如果需要只读需显式保护

  • 说明final list = [1, 2]; 只代表 list 不能重新指向新数组,但仍然可以 list.add(3)。如果需要不可变列表,可以使用 List.unmodifiable([1, 2])const [1, 2]

⚠️ 坑 10:级联操作符(Cascade notation ..

  • 现象:经常在别人的 Flutter 代码中看到 .. 双点语法。
  • 说明:这是 Dart 独特的语法糖,允许对同一个对象连续执行多次操作,最终仍返回该对象本身:
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 2.0
      ..style = PaintingStyle.stroke;
    

9. 工程配置与生态工具对应

场景JavaScript / TypeScript / VueDart / Flutter
包管理配置文件package.jsonpubspec.yaml
包安装命令npm install / pnpm add <pkg>flutter pub add <pkg> / dart pub add <pkg>
依赖锁文件package-lock.json / pnpm-lock.yamlpubspec.lock
官方包仓库npmjs.compub.dev
代码格式化prettier --write .dart format .
代码静态检查eslint .dart analyze
测试运行vitest / jestflutter test / dart test
主入口文件main.ts / index.tslib/main.dart(必须包含顶层 void main() => runApp(...)

10. 给 Vue 3 开发者的 Flutter 上手推荐路线

  1. 第一阶段(语言筑基 - 1天)
    • 阅读本指南中的第 2、3、4、5 节。
    • 在浏览器打开 DartPad (dartpad.dev),动手敲一遍:类定义、命名参数、Collection-if、Records 与模式匹配。
  2. 第二阶段(Flutter UI 核心 - 2~3天)
    • 熟悉核心布局 Widget:Container, Column, Row, Stack, ListView, Padding, Expanded, Flex
    • 掌握 StatelessWidget(纯展示组件,对应 Vue 的无状态纯 Props 组件)与 StatefulWidget(有状态组件)。
  3. 第三阶段(状态管理跃迁 - 2天)
    • 强烈推荐直接学习 Riverpod(由 Provider 作者重构)。其核心理念也是通过声明各种全局/局部 Provider,使用 ref.watch(provider) 自动建立响应绑定,开发体验和 Vue 3 Composition API 最为神似!