写给 js 程序员的 Dart 语言教程
// 每日前端夜话 第540 篇
// 正文共:5000 字
// 预计阅读时间:15 分钟
让我们以 JS 程序员的身份学习 Dart 语言:深入研究 OOP、类、继承和 mixins、异步、回调、async/await 和流。
本系列针对那些了解 React-Native,JavaScript 或 Web 开发并正在尝试跨平台移动开发的程序员,因为我在文中会放很多比较 Dart 语言与 JavaScript 的例子,还有 Flutter with React 和 React-Native。
为什么要学习 Flutter 和 Dart?
Flutter 和 Dart 由 Google 发布。Dart 是一种编程语言,而 Flutter 是一种 UI 工具包,可以为 Android 和 iOS 编译原生代码,具有实验性的 Web 和桌面程序支持,并且它也是开发 Google 的 Fuchsia OS[1] 程序的原生框架。
这意味着你不用关心平台,只需要专注于产品本身就行了。当 Dart 编译到 ARM 时,已编译的程序始终是本机原生代码,能够提供**超过 60 fps **的跨平台性能。Flutter 还能过通过“有状态的热重装”来帮助加快开发周期。
在本系列的最后,你会对 Dart、基本的数据结构、面向对象、异步性和流有基本的了解。另外你还将了解 Flutter 的小部件、主题、导航、网络、路由、使用第三方包、原生 API 等。
本文重点介绍 Dart 部分,然后在下一篇文章中研究 Flutter,在最后一篇中将它们整合到一个有意思的小游戏中。
在文中,我会用 “👉” 表情符号比较 JS 和 Dart 语言。一般左侧为 JS,右侧为 Dart 等效代码,例如:
console.log("hi!");
👉 print("hello!");
Dart 与 JavaScript 的优缺点
我们不能对 JavaScript 和 Dart 做直接的比较,因为它们都有不同的用途和目标受众。但是它们都有各自的优点和缺点,在用这两种技术做了几个项目之后,你就会知道它们在哪些方面表现的好。
但是,在进入 Flutter 生态时,你会注意到 Dart 有更陡峭的学习曲线,其中包括类型、抽象概念和 OOP——但是不要让这些阻止你前进的脚步。
JavaScript 的社区更大,所以在网上会有更多更多的包、资源、学习资料和社区。
但是一旦掌握了 Dart,你就会发现 Dart 和 Flutter 有更好的开发工具,它的运行速度更快,与 pub.dev[2] 相比,(Dart 的包存储库) npm 中质量较差的包太多了。
Dart 中的变量和类型
初次看 Dart 代码段后,你可能会发现一个概念,如果你只懂 JS 可能会不熟悉:Dart 是类型安全的。
这意味着,当你要定义变量时,要么必须提供一个初始值,然后让编译器找出与之匹配的类型(隐式类型),或者明确提供变量的类型(这是最佳情况)。
在编程中,类型定义了你要在变量中存储的数据类型,例如用 int
类型可以存储整数(例如 7
)。在 Dart 中,最常用的基本类型是 int
,double
,string
和 boolean
。以下是一些例子:
// 小心! 这是一些讨厌的Dart代码!
var num = 0; // Dart将隐式为该变量提供int类型。var,let 👉var
int myInt = 3; //这是一个显式类型的变量
final double pi = 3.14; // const 👉final
myInt = 3.2; // 将抛出错误,因为3.2不是整数
pi = 3.2; // 将 pi 标记为 final 将引发错误
String name = "Mark";
还有一种“后备类型”或非类型类型:dynamic
。在 Dart 中,只要在编写代码时无法确定形参、实参、列表项或其他任何内容的确切类型,就可以用 dynamic
类型。使用动态类型的变量时,一定要格外小心,并在代码中添加额外的安全屏障,这样才能使程序在传递意外类型时不会崩溃。应该尽量避免使用 dynamic
。
小提示:如果要体验 Dart,可以试试 DartPad(https://dartpad.dev/),这是一个 Dart 在线编译器,或者 Dart 团队制作的 “playground”。
final,static 和 const
在 Dart 中,可以通过三个关键字创建常量:final
、static
和。const
。final
只能在运行时创建一次,而 const
只能在编译时创建。你可以将 const
看作是更加严格的 final
。(当你不确定要用哪个时,用 final
就行了。要详细了解关键字 final
,static
和 const
,可以查看[官方文档](https://news.dartlang.org/2012/06/const-static-final-oh-my.html)。
更多有关 Dart 中变量和内置类型的信息,在官方手册[3]中有详细的描述。
开始写第一个 Dart 函数
类型安全会在出现在很多地方。例如在编写函数时,你必须定义返回值和参数的类型。
// return type, function name, parameters with their types and names
double addDoubles(double a, double b) {
return a + b;
}
addDoubles(3.2, 1.4); // => will return 4.6
如果你的函数不返回任何内容,可以使用 void
关键字。比如可以用在 Dart 程序的执行入口(主函数):void main()
。
void main() {
print(addNumbers(2, 3)); // console.log() 👉print()
// 这个函数没有返回值
}
那么什么是执行入口? 在JavaScript中,代码从第一行开始执行,然后逐行线性地一直到文件的末尾。在 Dart 中,必须有一个 main()
函数,它将会作为程序的主体。编译器会从 main
函数开始执行你的代码,因此叫执行入口。
流程控制语句:if、for、while 等
它们的语法和工作方式与 JavaScript 一样。下面是一些例子:
int age = 20;
if(age >= 18) {
print("here’s some beer! 🍻");
} else {
print("🙅♂️sorry, no alcohol for you...");
}
// let’s count from 1 to 10!
// p.s.: notice the `int i`
for (int i = 1; i <= 10; i++) {
print("it’s number $i"); // ${} 👉 $ (变量名)
}
// while loops:
// 请不要运行此代码段,它可能会导致崩溃或资源不足。
while("🍌" == "🍌") { // Dart 中不需要 ===
print("Hey! 👋 I’m a banana!");
}
数组和对象
在 JavaScript中,我们用数组和对象把多个数据保存在一起。在 Dart 中对应的是 list 和 map,它们的工作方式略有不同(它们还有一些额外的 API)。
Array 👉 List
Dart 中的 List
是一个用于存储相同类型数据的数组。不能用 [1,“ banana”,null,3.44]
这种形式。当然你仍然可以用自己熟悉的 JS 语法 []
和 new List()
构造函数来创建一个数组。
// 通常的隐式类型 [] 语法
var continents = ["Europe", "North America", "South America", "Africa", "Asia", "Australia"];
continents.add("Antarctica"); // .push() 👉 .add()
// 注意,当抛出多种类型的数据时,Dart 将退回到 list 的 `dynamic` 类型:
var maybeBanana = [1, "banana", null, 3.44];
// 新的 List()语法,有动态长度:
// List<T> 语法: 需要把所需的值类型填在<>中
List<int> someNiceNumbers = new List();
someNiceNumbers.add(5);
// fixed-length list:
List<int> threeNiceNumbers = new List(3); // 此列表最多可以容纳3个项目。
List<dynamic> stuff = new List();
stuff.add(3);
stuff.add("apple"); // 由于<dynamic>类型,这仍然是完全合法的
关于 Dart 数组的详细 API 在可以查看官方文档[4]。
Object 👉 Map
在 JavaScript 中,对象可以存储键值对,而在 Dart 中最接近这个数据结构的对象是 Map
,可以用 {...}
文字和 new Map()
构造函数来定义。
// the usual { ... } literal
var notesAboutDart = {
objects: "hey look ma! just like in JS!",
otherStuff: "idc we’ll look into them later"
};
// the new Map constructor
Map notesAboutJs = new Map();
// typed Map literal:
Map<String, int> prices = <String, int>{
"apple": 100,
"pear": 80,
"watermelon": 400
};
// typed Map constructor:
final Map<String, String> response = new Map<String, String>();
现在就知道这些方法就足够了。如果想了解 HashMaps 之类的高级内容,可以查看 Map API 官方文档[5]。
Imports 与 exports
在 JavaScript 中可以简单地通过 export
或 module.exports
导出文件中的值,并用 import
或 require(...)
在其他文件中引用。在 Dart 中类似但是更简单。
要想简单地导入库,可以用 import
语句并引用核心包名、库名称或路径:
import 'dart:math'; // import math from “math” 👉import “math”;
// 从外部包导入库
import 'package:test/test.dart'; // import { test } from “test” 👉import “test/test”;
// 导入文件
import 'path/to/my_other_file.dart'; // this one is basically the same
// 指定前缀
import 'dart:math' as greatMath;
但是怎样创建自己的库或导出内容呢?Dart 没有类似 Java 中常用的 public
、protected
或 private
关键字,甚至缺少在JavaScript中我们习惯的 export
关键字。相反每个文件都天然的是 Dart 库,这意味着在写好代码后无需显式导出内容,只需要其导入另一个文件中就可以正常工作。
如果不希望 Dart 公开你的变量,则应该使用 _
前缀。下面是例子:
// /dev/a.dart
String coolDudes = "anyone reading this";
String _hiddenSuffix = “...with sunglasses on 😎";
// /dev/b.dart
import "./b.dart";
print("cool dudes: $coolDudes"); //
print("cool dudes: $coolDudes $_hiddenSuffix") // => 将会失败,因为在此上下文中_hiddenSuffix未定义
面向对象编程(OOP) 和 class 概述
Dart 是一种面向对象的语言
如果你还不了解 OOP,就必须学习一种全新的编程方式。首先 JavaScript 既不是严格的OOP也不是函数型的,它包含来自两种概念中的元素。
你可以根据自己的喜好、项目以及所需的框架,在这两个概念之间进行选择。Dart 对于 OOP 相当严格。
下面这张表格,能帮你掌握基于函数的编程和面向对象编程之间的主要区别:
综上所述:在 OOP 出现之前是面向过程编程,这种方式基于大量的变量和函数,非常简单,但是容易产生面条代码。为了解决这个问题出现了 OOP,我们将相关的函数和变量分组为一个单元。这个单元称为对象,内部有被称为属性的变量和被称为方法的函数。
例如一辆汽车将有品牌、颜色、重量、马力、车牌号和其他可描述汽车的属性。同时,它还有有加速、刹车、转弯等方法。
当然你的代码中并没有汽车,所以要把抽象的想法放入代码中。在 JS 中的一个很好的例子是 window
对象。它有窗口的宽度和高度之类的属性,还有调整大小和滚动的方法。
OOP 的四个原则是:
-
封装:将变量(属性)和函数(方法)分组为称为对象的单元,这降低了复杂性并提高了可重用性。 -
抽象:你不应直接修改属性或访问所有的方法,而是应该考虑为对象编写一个简单的接口。这可以帮助你隔离对象内部所做更改的影响。 -
继承:通过从另一个对象或类继承东西来消除冗余代码。(Dart 通过 mixins 实现了这一点,后面会研究具体的例子)。这样能帮你使代码库更小,更容易维护。 -
多态:由于继承,一件事可能会根据所引用对象的类型而有所不同。这可以帮助你重构和消除难看的 if
和switch...case
语句。
交易担保 前端面试星球 面试官偷偷用的题库,你不来刷? Mini Program
Dart 面向对象编程的例子
如果你对这个概念很不熟悉也没关系,下面这个 Dart 案例可以帮你理解 OOP 的概念。先看一个带有一些属性和一个构造函数的简单类。
class Developer {
final String name;
final int experienceYears;
//带有一些语法糖的构造函数
//构造函数创建该类的新实例
Developer(this.name, this.experienceYears) {
// 在构造 Developer 类的新实例时,将会执行这里的代码
// 例如 Developer dev = new Developer(“Daniel”, 12); 语法!
// 注意,没有必要一个一个的去设置 this.name = name; 这样的语句
// 这是因为Dart语法糖
}
int get startYear =>
new DateTime.now().year - experienceYears; // 只读属性
// 方法
void describe() {
print(
'The developer is $name. They have $experienceYears years of experience so they started development back in $startYear.');
if (startYear > 3) {
print('They have plenty of experience');
} else {
print('They still have a lot to learn');
}
}
}
在其他地方,你可以创建此类的新实例:
void main() {
Developer peter = new Developer("Peter", 12);
Developer aaron = Developer("Aaron", 2); // 在Dart 2中,关键字 new 是可选的
peter.describe();
// 这样很好地将其打印到控制台:
// The developer is Peter. They have 12 years of experience so they started development back in 2008.
// They have plenty of experience.
aaron.describe();
// =>
// The developer is Aaron. They have 2 years of experience so they started development back in 2018.
// They still have a lot to learn.
}
就这样我们用属性和方法制写出了第一个 Dart 类。其中用了类型变量、受保护的变量,控制语句,获得了当前年份并将一些内容输出到控制台。
Dart中的继承和 mixins
一旦你对类有了扎实的知识并开始思考更复杂的系统,将会需要用到继承,这样就无需到处复制和粘贴代码,也不会产生面条代码。
所以 OOP 有继承性。把代码从一个类继承到另一个类时,基本上是让编译器复制并粘贴该类的成员(类的“成员”是一个类中的方法和属性),并在上一个类的顶部添加其他代码。这就是多态的体现:相同的核心代码可以通过从基类继承而以多种方式存在。
想想HTML。HTML 实现了几个类似的元素,例如 TextBox
,Select
和 Checkbox
。它们都共用了一些通用的方法和属性,例如 click()
,focus()
,innerHTML
或 hidden
。使用类继承,你可以编写一个像 HtmlElement
这样的通用类,并从那里继承重复的代码。
在Dart 中使用 extends
关键字从基类继承代码。看一个简单的例子:
// 注意extends关键字。
// 我们继承了上一段代码中定义的 Developer 类
class RisingStackEngineer extends Developer {
final bool cool = true;
String sunglassType;
RisingStackEngineer(String name, int experienceYears, this.sunglassType)
: super(name, experienceYears); // super() 调用父类的构造函数
void describeSunglasses() {
print("$name has some dope-ass $sunglassType-type sunglasses.");
}
}
这个类能做什么?看一下这段代码:
void main() {
RisingStackEngineer berci = RisingStackEngineer("Bertalan", 300, "cool");
berci.describe(); // .describe(); 不是直接在 RisingStackEngineer 类上定义,而是从 Developer 类继承的。
berci.describeSunglasses(); // => Bertalan has some dope-ass cool-type sunglasses
}
是不是很神奇,让我们通过 mixins 让它其变得更好。Mixins 帮助你将多个类混合到你的层次结构中。例如让我们为程序员创建一些键盘:
class Keyboard {
int numberOfKeys = 101;
void describeKeyboard() {
print("The keyboard has $numberOfKeys keys.");
}
}
然后用 mixin 与 Dart 的 with
关键字创建一种程序员和键盘的混合体:
class WalkingKeyboard extends Developer with Keyboard {
// ...
}
你可以自己编写一些代码,创建一些类,甚至去继承一些代码。不要只是看,只有亲自敲代码才能学会。当你理解了前面所讲到的这些基本概念之后,才能继续学习后面的 Dart 异步编程。
Dart语言中的异步编程
与服务器通信、处理文件或使用某些原生 API 时,必须编写异步代码。在 JavaScript 中,我们用回调和 async
/await
来定时执行自己的代码。幸运的是 Dart 也用了完全相同的概念,并使用 async
/await
来避免回调地狱。
先看一个回调的例子:
// Promise 👉 Future
// 方法的返回类型是一个异步void
Future<void> printWithDelay(String message) {
return Future.delayed(Duration(seconds: 1)).then((_) {
print(message);
});
}
void main() {
print("hey hi hello");
printWithDelay("this message is printed with delay");
}
用 async
/await
实现的相同功能的代码:
// 注意,必须添加 async 关键字才能 await Future
Future<void> printWithDelay(String message) async {
await Future.delayed(Duration(seconds: 1));
print(message);
}
void main() {
print("hey hi hello");
printWithDelay("this message is printed with delay");
}
这就是 Promise 👉 Future 部分。如果你想进一步了解 Future API,请阅读官方文档[6]。但是 Dart 有另一个用于处理异步的 API:流。🤯
Dart 中的流
与其他大多数语言相比,Dart 在异步方面的主要优势是对流的原生支持。如果你想用一种简单的方法来搞清楚 Future 和 Streams 之间的差异,需要考虑以下几点:Future
使用单个值处理“完成的数据” (例如,Web API 响应),而 Streams 用零个或多个值处理连续的数据(例如,异步 for 循环)。
看下图:
那怎样才能从 Dart 流中接收到数据呢?每当流中发生新事件时(接收到新数据或发生错误),Dart 都会通知 listener。listener 是一段代码,可订阅流中的事件并在接收到事件时处理数据。你可以使用 .listen()
函数订阅流,并提供回调。
// 这个流每秒钟产生一个整数
final exampleStream = NumberCreator().stream;
// e.g. 1, 2, 3, 4, ...
// 打印从流中接收到的数据
final subscription = exampleStream.listen((data) => print(data););
默认情况下,Dart 流仅支持一个侦听器。向该流添加另一个侦听器会引发异常-但是,有一个工具可以帮助我们把多个侦听器添加到单个流,那就是广播流。只需在流的末尾添加 .asBroadcastStream
就可以在流中添加多个侦听器:
// 相同的代码,但有广播流。注意最后的 .asBroadcastStream!
final exampleStream = NumberCreator().stream.asBroadcastStream;
//这样就可以添加多个侦听器了
final subscription = exampleStream.listen((data) => print(data););
final subscription2 = exampleStream.listen((data) => print(data););
仔细看一下这个 API。前面提到过我们能够接收数据或流中出现错误,那么该如何处理错误呢?在下面代码中的错误处理中用了一个更高级的侦听器。我们还可以在流发完数据(不再发送数据)后运行代码,可以明确定义是否要在发生错误时取消监听等。以下是代码:
final advancedSubscription = exampleStream.listen(
// 当接收到新数据时运行
(data) {
print("data: $data");
},
// 错误处理
onError: (err) {
print("error: $err");
},
// 发生错误时不取消订阅
cancelOnError: false,
// 流完成后运行一些代码。
onDone: () {
print("done!");
}
);
如果这还不够,还可以使用订阅对象本身来搞些事情:
advancedSubscription.pause(); // 暂停订阅
advancedSubscription.resume(); // 恢复订阅
advancedSubscription.cancel(); // 取消订阅
Dart 中的流还能做很多事:你可以操纵并过滤它们的数据,当然我们没有提到异步迭代器和怎样创建流,但是这足以使你上手 Flutter 了。如果你想进一步了解 Dart 中的异步特性,可以看看 Flutter 团队制作的视频:
-
Isolates and event loops [7] -
Dart Futures [8] -
Dart Streams [9] -
Async/Await [10] -
Generators [11]
总结
在本文中我们了解了很多东西——从变量、类型和控制语句到列表、映射、导入和导出。
然后是 Dart 生态系统中比较重要的部分。首先是为什么要用OOP(面向对象编程),它的优点是什么,它在哪里表现良好,然后是类、继承和 mixins,最后是异步、回调、async/await 和流。
最后别忘了:你可以随时在DartPad[12] 学习这些知识.
Reference
Fuchsia OS:https://en.wikipedia.org/wiki/Google_Fuchsia
[2]pub.dev:https://pub.dev/
[3]官方手册:https://dart.dev/guides/language/language-tour#built-in-types
[4]可以查看官方文档:https://api.dart.dev/stable/2.8.4/dart-core/List-class.html
[5]Map API 官方文档:https://api.dart.dev/stable/2.8.4/dart-core/Map-class.html
[6]阅读官方文档:https://api.dart.dev/stable/2.8.4/dart-async/Future-class.html
[7]Isolates and event loops:https://youtu.be/vl_AaCgudcY
[8]Dart Futures:https://youtu.be/OTS-ap9_aXc
[9]Dart Streams:https://youtu.be/nQBpOIHE4eE
[10]Async/Await:https://youtu.be/SmTCmDMi4BY
[11]Generators:https://youtu.be/TF-TBsgIErY
[12]DartPad:https://dartpad.dev/