vlambda博客
学习文章列表

写给 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 中质量较差的包太多了。

写给 js 程序员的 Dart 语言教程
充实的脑洞
让脑洞更加充实。
41篇原创内容
Official Account

Dart 中的变量和类型

初次看 Dart 代码段后,你可能会发现一个概念,如果你只懂 JS 可能会不熟悉:Dart 是类型安全的。

这意味着,当你要定义变量时,要么必须提供一个初始值,然后让编译器找出与之匹配的类型(隐式类型),或者明确提供变量的类型(这是最佳情况)。

在编程中,类型定义了你要在变量中存储的数据类型,例如用 int 类型可以存储整数(例如 7)。在 Dart 中,最常用的基本类型是 intdoublestringboolean。以下是一些例子:

// 小心! 这是一些讨厌的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 中,可以通过三个关键字创建常量:finalstatic 和。constfinal 只能在运行时创建一次,而 const 只能在编译时创建。你可以将 const 看作是更加严格的 final。(当你不确定要用哪个时,用 final 就行了。要详细了解关键字 finalstaticconst,可以查看[官方文档](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.21.4); // => will return 4.6

如果你的函数不返回任何内容,可以使用 void 关键字。比如可以用在 Dart 程序的执行入口(主函数):void main()

void main() {
    print(addNumbers(23));  // console.log() 👉print()

    // 这个函数没有返回值
}

那么什么是执行入口? 在JavaScript中,代码从第一行开始执行,然后逐行线性地一直到文件的末尾。在 Dart 中,必须有一个 main() 函数,它将会作为程序的主体。编译器会从 main 函数开始执行你的代码,因此叫执行入口。

写给 js 程序员的 Dart 语言教程 交易担保 前端面试星球 面试官偷偷用的题库,你不来刷?

流程控制语句: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"null3.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<Stringint> prices = <Stringint>{
    "apple"100,
    "pear"80,
    "watermelon"400
};

// typed Map constructor:
final Map<StringString> response = new Map<StringString>();

现在就知道这些方法就足够了。如果想了解 HashMaps 之类的高级内容,可以查看 Map API 官方文档[5]

写给 js 程序员的 Dart 语言教程
充实的脑洞
让脑洞更加充实。
41篇原创内容
Official Account

Imports 与 exports

在 JavaScript 中可以简单地通过 exportmodule.exports 导出文件中的值,并用 importrequire(...) 在其他文件中引用。在 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 中常用的 publicprotectedprivate 关键字,甚至缺少在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 相当严格。

下面这张表格,能帮你掌握基于函数的编程和面向对象编程之间的主要区别:

写给 js 程序员的 Dart 语言教程

综上所述:在 OOP 出现之前是面向过程编程,这种方式基于大量的变量和函数,非常简单,但是容易产生面条代码。为了解决这个问题出现了 OOP,我们将相关的函数和变量分组为一个单元。这个单元称为对象,内部有被称为属性的变量和被称为方法的函数。

例如一辆汽车将有品牌、颜色、重量、马力、车牌号和其他可描述汽车的属性。同时,它还有有加速、刹车、转弯等方法。

当然你的代码中并没有汽车,所以要把抽象的想法放入代码中。在 JS 中的一个很好的例子是 window 对象。它有窗口的宽度和高度之类的属性,还有调整大小和滚动的方法。

OOP 的四个原则是:

  • 封装:将变量(属性)和函数(方法)分组为称为对象的单元,这降低了复杂性并提高了可重用性。
  • 抽象:你不应直接修改属性或访问所有的方法,而是应该考虑为对象编写一个简单的接口。这可以帮助你隔离对象内部所做更改的影响。
  • 继承:通过从另一个对象或类继承东西来消除冗余代码。(Dart 通过 mixins 实现了这一点,后面会研究具体的例子)。这样能帮你使代码库更小,更容易维护。
  • 多态:由于继承,一件事可能会根据所引用对象的类型而有所不同。这可以帮助你重构和消除难看的 ifswitch...case 语句。

  • 写给 js 程序员的 Dart 语言教程 交易担保 前端面试星球 面试官偷偷用的题库,你不来刷?

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 实现了几个类似的元素,例如 TextBoxSelectCheckbox。它们都共用了一些通用的方法和属性,例如 click()focus()innerHTMLhidden。使用类继承,你可以编写一个像 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 循环)。

看下图:

写给 js 程序员的 Dart 语言教程

那怎样才能从 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

[1]

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/




强力推荐前端面试刷题神器


写给 js 程序员的 Dart 语言教程

精彩文章回顾,点击直达