Dart学习笔记(9):异步与并发实例(Web彩票应用)

发表于2018-07-04 18:16 阅读(122)

因为工作的关系
已经很长时间没有更新教程

本文地址:http://www.cndartlang.com/724.html

想想,基础部分貌似讲得差不多了
接下来会讲一下Dart核心中的异步和多线程

在此之前,你应该明白什么是同步和异步
以及单线程与多线程

首先讲一下异步

在JavaScript中,你可以使用异步编程模型的回调函数功能
在Dart中你也可以如此

但你会发现,回调函数在可读性、可维护性、以及执行先后顺序等方面均存在问题
于是在Dart中引入了Future和Completer的概念

这里,我们用双色球来举个例子
这确实是一个非常不错的例子
为了制作悬念,号码球生成的时候有一个随机的时间间隔

如果将这个应用建造成同步模式,在程序执行完之前
浏览器直接就卡死了

因此要使用到异步,允许浏览器保持响应

1、同步:Web彩票应用

之前都是命令行下的操作,这里讲一下部署web应用

当你引用了dart:html库的时候,得建一个web文件夹,里面包含有html文件
然后在yaml文件中,添加browser的依赖项

 browser库已停止更新,建议使用dart_to_js_script_rewriter库。dart_to_js_script_rewriter是通过Transformer转换器来实现,相比之下,它并不需要在HTML文件中引用其它js文件,避免了不必要的延迟。最重要的原因是,截止目前并没有浏览器内置Dart VM,因此browser替换标签的操作没有必要。

main.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>彩票</title>
    
    <script async type="application/dart" src="main.dart"></script>
    <script async src="packages/browser/dart.js"></script>
    
    <link rel="stylesheet" href="main.css">
  </head>
  <body>
    <h1>Dart Lottery</h1>
    
    <div class="Numball" id="ball1"></div>
    <div class="Numball" id="ball2"></div>
    <div class="Numball" id="ball3"></div>
    
    <div class="Control">
        <input id="startBtn" type="button" value="Start"/>
        <input id="replayBtn" type="button" value="Replay"/>
</div>

    <br/>
    <input type="text" />
  </body>
</html>

main.css

.Numball {
  text-align: center;
  font-size: 30px;
  margin: 10px;
  padding: 10px;
  float: left;
  line-height: 40px;
  height: 40px;
  width: 40px;
  border-radius: 50%;
  border: 1px solid black;
}

.Control {
  clear:left;
}

.Control input {
  width: 80px;
  height: 40px;
}

main.html和main.css都是基本内容,这里不再讲解

main.dart

import "dart:html";
import "dart:math";

//计算中奖号码
int getWinningNumber() {
  //获取2000以内的一个随机数,表示随机等待的时间
  int millisecsToWait = new Random().nextInt(2000);
  
  //获取1970-01-01T00:00:00Z (UTC) 到当前时间的毫秒数
  var currentMs = new DateTime.now().millisecondsSinceEpoch;

  //Wait
  var endMs = currentMs + millisecsToWait;
  while (currentMs < endMs) {
    currentMs = new DateTime.now().millisecondsSinceEpoch;
  }
  
  //返回中奖号码
  return new Random().nextInt(32) + 1;
}

//填充页面中的中奖号码
startLottery() {
  int num1 = getWinningNumber();
  query("#ball1").innerHtml = "$num1";
  int num2 = getWinningNumber();
  query("#ball2").innerHtml = "$num2";
  int num3 = getWinningNumber();
  query("#ball3").innerHtml = "$num3";
}

//清空页面中的中奖号码
resetLottery() {
  query("#ball1").innerHtml = "";
  query("#ball2").innerHtml = "";
  query("#ball3").innerHtml = "";
}

void main() {
  //通过ID获取页面元素
  var startBtn = query("#startBtn");
  var replayBtn = query("#replayBtn");
  
  //添加onClick的事件
  startBtn.onClick.listen((e){
    //点击开始按钮后,页面元素中开始按钮不可用,重置按钮可用
    startBtn.disabled = true;
    replayBtn.disabled = false;
    
    //填充中奖号码
    startLottery();
  });
  
  replayBtn.onClick.listen((e){
    //点击重置按钮后,页面元素中开始按钮可用,重置按钮不可用
    replayBtn.disabled = true;
    startBtn.disabled = false;
    
    //清空页面中的中奖号码
    resetLottery();
  });
}

运行结果:

运行之后你会发现,点击开始之后界面就完全卡死了
当三个中奖号码全部计算完后才显示出来
并且输入框在这之前是不能输入的,这是同步

2、异步:代码重构

养成好的习惯,代码重构
这里新建 lib/lottery.dart

lottery.dart

library lottery;

import "dart:html";
import "dart:math";
import "dart:async";

//并发在Dart中非常的简便,首先声明函数为Future对象,异步传回的结果为int
//计算中奖号码
Future<int> getFutureWinningNumber() {
    //Completer就像是控制器,决定Future对象后续的流程,then或者catchError

  Completer<int> numberCompleter = new Completer<int>();
  
  //获取中奖号码
  Random r = new Random();
  int randomNum =  r.nextInt(32) + 1;
  
  //这里用Timer模拟一个比较耗时的操作计算
  //异步计算的结果,调用函数complete送出结果
  new Timer(
        new Duration(milliseconds: 2000),
        () => numberCompleter.complete(randomNum));

  return numberCompleter.future;
}

//清空页面中的中奖号码
resetLottery() {
  query("#ball1").innerHtml = "";
  query("#ball2").innerHtml = "";
  query("#ball3").innerHtml = "";
}

main.dart

import "dart:html";
import "dart:math";
import "dart:async";

import "./lib/lottery.dart";

//通过id查找页面元素,并更新数据
updateResult(int ball, int winningNum) {
  var ballDiv = query("#ball$ball");
  ballDiv.innerHtml = "$winningNum";
}

void main() {
  
  //通过ID获取页面元素
  var startBtn = query("#startBtn");
  var replayBtn = query("#replayBtn");
  
  //添加onClick的事件
  startBtn.onClick.listen((e){
    //点击开始按钮后,页面元素中开始按钮不可用,重置按钮可用
    startBtn.disabled = true;
    replayBtn.disabled = false;
    
        //1、异步,效果为同时显示(肉眼无法察觉先后顺序)
    Future<int> f1 = getFutureWinningNumber();
    Future<int> f2 = getFutureWinningNumber();
    Future<int> f3 = getFutureWinningNumber();
    f1.then((int result1) => updateResult(1, result1));
    f2.then((int result2) => updateResult(2, result2));
    f3.then((int result3) => updateResult(3, result3));
    
    //2、上面的逻辑可以用wait,执行多个函数后,一并将结果返回为list
//    Future.wait([getFutureWinningNumber(), 
//      getFutureWinningNumber(), 
//      getFutureWinningNumber()])
//      .then((list) {
//        for(int i=0;i<list.length; i++) {
//          updateResult(i+1, list[i]);
//        }
//    });
     
  //3、异步,效果为顺序显示
//  getFutureWinningNumber().then((num1) {
//    updateResult(1, num1);
//    //return Future,继续获取中奖号码
//    //并执行下一个then
//      return getFutureWinningNumber();
//    }).then((num2) {
//      updateResult(2, num2);
//      return getFutureWinningNumber();
//    }).then((num3) {
//      updateResult(3, num3);
//    });

  });
  
  replayBtn.onClick.listen((e){
    //点击重置按钮后,页面元素中开始按钮可用,重置按钮不可用
    replayBtn.disabled = true;
    startBtn.disabled = false;
    
    //清空页面中的中奖号码
    resetLottery();
  });
}

运行效果:

这里可以发现,异步模式下
输入框仍然可以输入信息,界面没有卡死
体验效果比同步模式好很多

3、并发:Isolate

其实在Future和Completer中同步与异步嵌套,可以完成很多功能
上面的代码《Dart in Action》这本书中也可以看到,解释得很详细
这里自己写了一个 Isolate版本 ,延伸讲一下Isolate

在Dart中并没有线程的概念,词典翻译isolate,大概意思是隔离区
应该说很形象,实际上isolate在Dart VM中可能是一个线程
在Web中可能是一个Web Worker

Dart中的代码都是在不同的isolate中运行的,包括main函数
每个isolate都有自己的堆(Heap)和栈(Stack)
所以即使是全局数据,也彼此隔离

isolate仅在端口(Port)上通过消息进行通信,并且消息在接收前会进行复制
因此无法通过消息操作同一个对象
于是C++中通过线程间引用传递参数操作对象的方法,在Dart中行不通

接着不得不抱怨一句
Dart的资料太少了,网上很多东西都是已经过时的旧式语法
很多资料详细的用法也没有看到
就拿isolate并发来说,花了一整天的时间研究,网上很多东西基本上无用

言归正传,上代码

main.dart

import "dart:html";
import "dart:isolate";

//清空页面中的中奖号码
void resetLottery() {
  querySelector("#ball1").innerHtml = "";
  querySelector("#ball2").innerHtml = "";
  querySelector("#ball3").innerHtml = "";
}

updateResult(List<int> list) {
  if(list.length < 2) return;
  
  var ballDiv = querySelector("#ball${list[0]}");
  ballDiv.innerHtml = "${list[1]}";
}

void main() {
  ReceivePort receivePort = new ReceivePort();
  
  //接收获奖号码
  receivePort.listen((msg) {
    if(msg is List) {
      updateResult(msg);
    }
  });

  String Costlyprocess = './lib/lottery.dart';

  //通过ID获取页面元素
  var startBtn = querySelector("#startBtn");
  var replayBtn = querySelector("#replayBtn");
  
  //添加onClick的事件
  startBtn.onClick.listen((e){
    //点击开始按钮后,页面元素中开始按钮不可用,重置按钮可用
    startBtn.disabled = true;
    replayBtn.disabled = false;
    
    //开始计算中奖号码
    Isolate.spawnUri(Uri.parse(Costlyprocess), ["START"], receivePort.sendPort);
  });
  
  replayBtn.onClick.listen((e){
    //点击重置按钮后,页面元素中开始按钮可用,重置按钮不可用
    replayBtn.disabled = true;
    startBtn.disabled = false;
    
    //清空页面中的中奖号码
    resetLottery();
  });
}

lottery.dart

library lottery;

import "dart:math";
import "dart:async";
import "dart:isolate";

//计算中奖号码
void sendWinningNumber(SendPort sendPort) {
  
  Random r = new Random();
    
  var currentMs = 0;
  var waitMs = 1000;
  var duraMs = 50;
  
  new Timer.periodic(new Duration(milliseconds: duraMs), (t){
    currentMs += duraMs;
    if(currentMs ~/ waitMs > 2) {
      t.cancle();
    } else {
      sendPort.send([(currentMs ~/ waitMs) + 1, r.nextInt(32) + 1]);
    }
  });
}

main(List<String> args, SendPort sendPort) {
  if(args[0] == "START") {
    sendWinningNumber(sendPort);
  }
}

运行效果: