《JavaScript设计模式》——9.13 Flyweight(享元)模式

简介:

本节书摘来自异步社区《JavaScript设计模式》一书中的第9章,第9.13节, 作者: 【美】Addy Osmani 译者: 徐涛 更多章节内容可以访问云栖社区“异步社区”公众号查看。

9.13 Flyweight(享元)模式

Flyweight模式是一种经典的结构型解决方案,用于优化重复、缓慢及数据共享效率较低的代码。它旨在通过与相关的对象共享尽可能多的数据来减少应用程序中内存的使用(如:应用程序配置、状态等,见图9-12)。
screenshot

该模式最早是由Paul Calder和Mark Linton于1990年构思出来,它以拳击重量级别命名,它包括重量不到112磅的拳手。Flyweight这个名字是源自这一重量级别,因为它所指的是:模式旨在帮助我们实现的轻量级(内存占用)。

在实践中,Flyweight数据共享会涉及获取多个对象使用的若干相似对象或数据结构,以及将这些数据放到一个单一的外部对象中。我们可以将该对象传递给依赖这些数据的对象,而不是在每一个对象都存储相同的数据。

9.13.1 使用Flyweight模式
Flyweight模式的应用方式有两种。第一种是用于数据层,处理内存中保存的大量相似对象的共享数据。

第二种是用于DOM层,Flyweight可以用作中央事件管理器,来避免将事件处理程序附加到父容器中的每个子元素上,而是将事件处理程序附加到这个父容器上。

鉴于数据层是Flyweight模式最常使用的地方,我们首先要对它进行了解。

9.13.2 Flyweight和共享数据
对于该应用程序,还有一些经典Flyweight模式的概念我们需要注意。在Flyweight模式中,有个有关两个状态的概念—内部和外部。对象中的内部方法可能需要内部信息,没有内部信息,它们就绝对无法正常运行。但外部信息是可以被删除的或是可以存储在外部的。

具有相同内部数据的对象可以被替换为一个由factory方法创建的单一共享对象。这使我们可以极大减少存储隐式数据的总数量。

这么做的好处是,我们能够密切关注已经被实例化的对象,这样新副本就只需要创建与现有对象不同的部分就可以了。

我们使用管理器来处理外部状态。如何实现管理器是不固定的,但有一种方法就是让管理器对象包含一个外部状态的中央数据库以及这些外部状态所属的享元对象。
**
9.13.3 实现经典Flyweight(享元)**
由于近年来Flyweight模式还没有在JavaScript中大量使用,很多给我们带来启发的相关实现都是来自Java和C++。

在这个实现中我们将利用三种类型的Flyweight组件,它们是:

Flyweight(享元)

描述一个接口,通过这个接口flyweight可以接受并作用于外部状态。

Concrete flyweight(具体享元)

实现Flyweight接口,并存储内部状态。Concrete Flyweight对象必须是可共享的,并能够控制外部状态。

Flyweight factory(享元工厂)

创建并管理flyweight对象。确保合理地共享flyweight,并将它们当作一组对象进行管理,并且如果我们需要单个实例时,可以查询这些对象。如果该对象已经存在则直接返回,否则,创建新对象并返回。

它们与实现中的下列定义相对应:

CoffeeOrder:享元
CoffeeFlavor:具体享元
CoffeeOrderContext:辅助器
CoffeeFlavorFactory:享元工厂
testFlyweight:享元的应用
鸭子补丁“实现”
鸭子补丁(Duck punching)使我们无需修改运行时源,就可以扩展一种语言或解决方案的功能。由于下一个解决方案要求使用 Java 关键字(implements)来实现接口,并且无法在原生JavaScript中找到,所以让我们首先对它进行鸭子补丁。

Function.prototype.implementsFor作用于一个对象构造函数,并将接受一个父类(函数)或对象,或者使用普通继承(函数)或虚拟继承(对象)来继承它。

// 在JS里模拟纯虚拟继承 implement
Function.prototype.implementsFor = function (parentClassOrObject) {
     if (parentClassOrObject.constructor === Function)
{
      // 正常继承
      this.prototype = new parentClassOrObject();
      this.prototype.constructor = this;
      this.prototype.parent = parentClassOrObject.prototype;
}
else {
// 纯虚拟继承
      this.prototype = parentClassOrObject;
      this.prototype.constructor = this;
      this.prototype.parent = parentClassOrObject;
    }
return this;
};

通过使一个函数显式地继承一个接口,可以用它来为缺少的implements关键字打上补丁。在下面的代码里,CoffeeFlavor实现了CoffeeOrder接口,且必须包含它的接口方法,以便将功能的实现赋值给对象。

// 享元对象
var CoffeeOrder = {
  // 接口
  serveCoffee: function (context) { },
     getFlavor: function () { }
};
// 实现CoffeeOrder的具体享元对象
function CoffeeFlavor(newFlavor) {
     var flavor = newFlavor;
     // 如果已经为某一功能定义了接口,则实现该功能
     if (typeofthis.getFlavor === "function") {
          this.getFlavor = function () {
               return flavor;
          };
     }
     if (typeofthis.serveCoffee === "function") {
         this.serveCoffee = function (context) {
             console.log("Serving Coffee flavor "
             + flavor
             + " to table number "
             + context.getTable());
};
}
  }
// 为CoffeeOrder实现接口
CoffeeFlavor.implementsFor(CoffeeOrder);
// 处理coffee订单的table数
function CoffeeOrderContext(tableNumber) {
    return {
        getTable: function () {
           return tableNumber;
        }
   };
}
// 享元工厂对象
function CoffeeFlavorFactory() {
     var flavors = [],
  return {
       getCoffeeFlavor: function (flavorName) {
            var flavor = flavors[flavorName];
            if (flavor === undefined) {
                 flavor = new CoffeeFlavor(flavorName);
                 flavors.pushc [flavorName],flavor]);
            }
            return flavor;
        },
        getTotalCoffeeFlavorsMade: function () {
           return flavors.length;
        }
      }
    };
// 样例用法:
// testFlyweight()
function testFlyweight() {
  // 已订购的flavor.
  var flavors = new CoffeeFlavor(),
  // 订单table
    tables = new CoffeeOrderContext(),
// 订单数量
    ordersMade = 0,
//TheCoffeeFlavorFactory 实例
    flavorFactory;
function takeOrders(flavorIn, table) {
    flavors[ordersMade] = flavorFactory.getCoffeeFlavor(flavorIn);
    tables[ordersMade++] = new CoffeeOrderContext(table);
}
 flavorFactory = new CoffeeFlavorFactory();
 takeOrders("Cappuccino", 2);
 takeOrders("Cappuccino", 2);
 takeOrders("Frappe", 1);
 takeOrders("Frappe", 1);
 takeOrders("Xpresso", 1);
 takeOrders("Frappe", 897);
 takeOrders("Cappuccino", 97);
 takeOrders("Cappuccino", 97);
 takeOrders("Frappe", 3);
 takeOrders("Xpresso", 3);
 takeOrders("Cappuccino", 3);
 takeOrders("Xpresso", 96);
 takeOrders("Frappe", 552);
 takeOrders("Cappuccino", 121);
 takeOrders("Xpresso", 121);
 for (var i = 0; i < ordersMade; ++i) {
       flavors[i].serveCoffee(tables[i]);
 }
 console.log(" ");
 console.log("total CoffeeFlavor objects made: " + flavorFactory. getTotalCoffeeFlavorsMade());
}

9.13.4 转换代码以使用Flyweight(享元)模式
接下来,通过实现一个系统来管理图书馆中的所有书籍,让我们来继续了解一下享元。每本书的重要元数据可以被分解成如下形式:

ID
Title
Author
Genre
Page count
Publisher ID
ISBN
我们还将需要使用以下属性来跟踪哪些成员已借出了哪些书籍,借书日期以及预计返还的日期。

checkoutDate

checkoutMember

dueReturnDate

availability

因此每本书在使用享元模式进行优化之前,都会按如下方式表示:

var Book = function (id, title, author, genre, pageCount, publisherID,
ISBN, checkoutDate, checkoutMember, dueReturnDate, availability) {
    this.id = id;
    this.title = title;
    this.author = author;
    this.genre = genre;
    this.pageCount = pageCount;
    this.publisherID = publisherID;
    this.ISBN = ISBN;
    this.checkoutDate = checkoutDate;
    this.checkoutMember = checkoutMember;
    this.dueReturnDate = dueReturnDate;
    this.availability = availability;
};
Book.prototype = {
   getTitle: function () {
       return this.title;
   },
   getAuthor: function () {
      return this.author;
   },
   getISBN: function () {
      returnthis.ISBN;
   },
   // 鉴于篇幅,其他属性就暂不列出了
    updateCheckoutStatus: function (bookID, newStatus, checkoutDate,
    checkoutMember, newReturnDate) {
        this.id = bookID;
        this.availability = newStatus;
        this.checkoutDate = checkoutDate;
        this.checkoutMember = checkoutMember;
        this.dueReturnDate = newReturnDate;
    },
    extendCheckoutPeriod: function (bookID, newReturnDate) {
          this.id = bookID;
          this.dueReturnDate = newReturnDate;
    },
    isPastDue: function (bookID) {
         var currentDate = new Date();
         return currentDate.getTime() > Date.parse(this.dueReturnDate);
    }
};

刚开始对于少量书籍可能是行得通的,但是,当图书馆扩大到拥有一个更大的库存,并且每本书都有多个版本和副本时,就会发现随着时间的推移,管理系统运行得越来越慢。使用数以千计的书籍对象可能会淹没可用内存,但可以使用享元模式优化系统来改善这个问题。

现在可以将数据分成内部和外部状态,如下所示:与书籍对象(title、author等)相关的数据是内部状态,而借出数据(checkoutMember、dueReturnDate等)是外部状态。实际上这意味着,每个书籍属性组合只需要有一个Book对象。它仍然要处理相当多的对象,但比以前处理的对象明显减少了。

下面书籍元数据组合的单个实例将在指定书名的书籍副本之间共享。

// 享元优化版本
var Book = function (title, author, genre, pageCount, publisherID, ISBN) {
     this.title = title;
     this.author = author;
     this.genre = genre;
     this.pageCount = pageCount;
     this.publisherID = publisherID;
     this.ISBN = ISBN;
};

正如我们可以看到的,外部状态已被删除。图书馆借出有关的所有事情都将转移给管理器,由于对象数据现在已被分割,可以使用工厂进行实例化。

9.13.5 基本工厂
现在让我们来定义一个基本的工厂。首先,必须要检查一下指定书名的书是否已在系统内部创建。如果已经创建,则返回它;如果没有,就会创建并存储这本新书,以便以后可以访问它。这确保我们仅为每一个特定的内部数据块创建一个拷贝:

// 书籍工厂单例
var BookFactory = (function () {
  var existingBooks = {}, existingBook;
  return {
       createBook: function (title, author, genre, pageCount, publisherID, ISBN) {
       // 如果书籍之前已经创建,则找出并返回它
       // !!强制返回布尔值
       existingBook = existingBooks[ISBN];
       if (!!existingBook) {
          return existingBook;
       } else {
         // 如果没找到,则创建一个该书的新实例,并保存
         var book = new Book(title, author, genre, pageCount, publisherID, ISBN);
         existingBooks[ISBN] = book;
         return book;
       }
     }
  };
});

9.13.6 管理外部状态
接下来,我们需要存储从Book对象中删除的状态。幸运的是,可以使用管理器(我们会将它定义为一个单例)来封装它们。一个Book对象和借书成员的组合将被称为书籍记录。管理器会将它们存储起来,它还包括在Book类享元优化期间我们排除的与借出有关的逻辑。

// 书籍记录管理器单例
var BookRecordManager = (function () {
  var bookRecordDatabase = {};
  return {
     // 添加新书到图书馆系统
     addBookRecord: function (id, title, author, genre, pageCount, publisherID, ISBN, checkoutDate,
       //checkoutMember, dueReturnDate, availability) {
       var book = bookFactory.createBook(title, author, genre, pageCount,
       publisherID, ISBN);
       bookRecordDatabase[id] = {
          checkoutMember: checkoutMember,
          checkoutDate: checkoutDate,
          dueReturnDate: dueReturnDate,
          availability: availability,
          book: book
       };
     },
     updateCheckoutStatus: function (bookID, newStatus, checkoutDate,
        checkoutMember, newReturnDate) {
       var record = bookRecordDatabase[bookID];
       record.availability = newStatus;
       record.checkoutDate = checkoutDate;
       record.checkoutMember = checkoutMember;
       record.dueReturnDate = newReturnDate;
     },
     extendCheckoutPeriod: function (bookID, newReturnDate) {
       bookRecordDatabase[bookID].dueReturnDate = newReturnDate;
     },
     isPastDue: function (bookID) {
       var currentDate = new Date();
       return currentDate.getTime() > Date.parse(
        bookRecordDatabase[bookID].dueReturnDate);
     }
  };
});

这些代码修改的结果是,从Book类中提取的所有数据,现在被存储在BookManager单例(BookDatabase)的属性中,这比我们以前使用大量对象时的效率要高很多。现在与书籍出借相关的方法在这里成为了基础,因为它们处理的是外部数据,而不是内部数据。

这个过程给我们的最终解决方案上增加了一点复杂性,但与它所解决的性能问题相比,这只是一个小问题。它具有数据智能性,如果有30本完全相同的书,我们现在只需要存储它一次。同时,每个函数都占用内存。通过使用Flyweight模式,这些函数在一个地方(在管理器上)存在,而不是在每个对象上存在,从而节约更多的内存。

9.13.7 Flyweight(享元)模式和DOM
文档对象模型(DOM)支持两种方式让对象检测事件:自上而下(事件捕捉)和自下而上(事件冒泡)。

在事件捕捉中,事件首先被最外层的元素捕捉,然后传播到最里面的元素。在事件冒泡中,事件被捕捉并传递给最里面的元素,然后传播到外部元素。

在这个上下文中描述享元的最好比喻之一是由Gary Chisholm编写的,它是类似这样的:

试着用池塘的方式思考一下享元。一条鱼张开它的嘴(事件),气泡升到表面(冒泡),当气泡到达表面(动作)时,一只坐在顶部的苍蝇飞走了。在本例中,我们可以很容易地把鱼张开嘴转换成点一个按钮,气泡转换成冒泡效应,苍蝇飞走可转换成运行一些功能。
引入冒泡用于处理这些情况:一个单一的事件(如一次点击)可能是由DOM层级的不同级别所定义的多个事件处理程序进行处理。上述事情发生时,事件冒泡先执行为最低层级特定元素定义的事件处理程序。此后,事件在冒泡到更高级元素之前,先冒泡到包含的这些元素上。

享元可以用来进一步调整事件冒泡过程,正如我们即将要看到的(示例9-9)。

在第一个实际示例中,假设一个文档中有一些相似的元素,在用户对它们执行用户动作(如:点击、鼠标悬停)时执行同样相似的行为。

通常在构建我们自己的accordion组件、菜单或其他基于列表的小部件时,我们要做的就是将一个点击事件绑定至父容器(如$('ul li a').on(..))中的每个链接元素上。其实不需将点击绑定至多个元素,我们就可以很容易地将享元附加到容器的顶部,它可以监听来自下面的事件。然后这些事情可以使用逻辑进行处理,逻辑与否简单取决于要求是否简单或复杂。

由于之前经常提到的组件类型的每个部分都有相同的重复标记(如accordion的每一节代码),有很大的机会是:被点击的每个元素的行为都和附近其他带有同名样式(class)元素的行为非常相似。利用这些信息,我们将使用享元来构建一个基本的accordion。

在jQuery用户将初始化点击绑定到一个容器div的同时,这里使用了一个stateManager命名空间来封装我们的享元逻辑。为了确保页面上没有其他相似逻辑处理程序附加在div容器上,刚开始就应用unbind事件。

现在要确定容器中的哪个子元素被点击,我们利用一个target检查,它提供了一个对被点击元素的引用,和父元素无关。然后,我们利用此信息来处理单击事件,而不是在页面加载时将事件绑定至特定的子元素上。

示例9-9 集中事件处理

如下是HTML代码:
screenshot

如下是JavaScript代码:

var stateManager = {
   fly: function () {
     var self = this;
     $("#container").unbind().on("click", function (e) {
       var target = $(e.originalTarget || e.srcElement);
          if (target.is("div.toggle")) {
             self.handleClick(target);
       }
     });
    },
    handleClick: function (elem) {
       elem.find("span").toggle("slow");
    }
};

这里的好处在于,我们将很多独立的动作转变成一个共享的动作(可能会节省内存)。

在第二个示例中,我们可以通过使用具有jQuery的享元模式进一步提高性能。

James Padolsey之前写了一篇名为《76 bytes for faster jQuery》的文章,文中他提醒我们:每次jQuery触发一个回调,无论何种类型(过滤器、每个、事件处理程序),我们都能够通过this关键字访问函数的上下文(DOM元素与它相关)。

可惜的是,我们中的很多人都已经习惯了在$()或jQuery()中包装this这个想法,这意味着每次构建jQuery的新实例都不是必要的。而不是像如下这样做:(示例9-10)

示例9-10 使用Flyweight进行性能优化

$("div").on("click", function () {
  console.log("You clicked: " + $(this).attr("id"));
});

// 我们需要避免使用DOM元素创建jQuery对象(像上面的代码那样),直接像下面这样使用DOM元素即可:

$("div").on("click", function () {
    console.log("You clicked:" + this.id);
});

James希望在下列上下文中使用jQuery的jQuery.text;但是,他不同意的观点是:在每个迭代循环里创建新的jQuery对象。

$("a").map(function () {
  return $(this).text();
});

在冗余的包装方面(这里可能是使用jQuery实用方法的情况下),最好使用jQuery.methodName(如:jQuery.text),而不是jQuery.fn.methodName(如:jQuery. fn.text)。其中,methodName代表一个实用程序,例如each()或text。这样做不需要在每次调用我们的函数时,都调用更高一级的抽象或创建一个新的jQuery对象,因为jQuery.methodName是库本身在底层抽象所使用的方法,以助力jQuery. fn. methodName。

因为不是所有的jQuery方法都有相应的单节点函数,所以Padolsey想出了jQuery. single工具这一概念。

这里的想法是:单一的jQuery对象被创建,用于每次对jQuery.single的调用(实际上意味着只有一个jQuery对象被创建)。可以在下面找到它的实现,由于我们是将多个可能对象的数据合并到一个更加集中的单一结构中,这在技术上讲也是享元。

jQuery.single = (function (o) {
    var collection = jQuery([1]);
    return function (element) {
        // 将元素赋值给集合:
        collection[0] = element;
          // 返回集合:
        return collection;
    };
});

使用链接的示例如下所示:
screenshot

虽然我们可能相信,简单缓存jQuery代码可能会提供相等的性能受益,Padolsey称仍然值得使用$.single,并且它可以表现的更好。这并不是说不需要使用任何缓存,只是要注意这种方法是对我们有帮助的。要进一步了解$.single方面的细节,我建议大家阅读Padolsey的完整文章。

相关文章
|
15天前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
|
2月前
|
设计模式 数据库连接 PHP
PHP中的设计模式:提升代码的可维护性与扩展性在软件开发过程中,设计模式是开发者们经常用到的工具之一。它们提供了经过验证的解决方案,可以帮助我们解决常见的软件设计问题。本文将介绍PHP中常用的设计模式,以及如何利用这些模式来提高代码的可维护性和扩展性。我们将从基础的设计模式入手,逐步深入到更复杂的应用场景。通过实际案例分析,读者可以更好地理解如何在PHP开发中应用这些设计模式,从而写出更加高效、灵活和易于维护的代码。
本文探讨了PHP中常用的设计模式及其在实际项目中的应用。内容涵盖设计模式的基本概念、分类和具体使用场景,重点介绍了单例模式、工厂模式和观察者模式等常见模式。通过具体的代码示例,展示了如何在PHP项目中有效利用设计模式来提升代码的可维护性和扩展性。文章还讨论了设计模式的选择原则和注意事项,帮助开发者在不同情境下做出最佳决策。
|
17天前
|
设计模式 开发者 Python
Python编程中的设计模式:工厂方法模式###
本文深入浅出地探讨了Python编程中的一种重要设计模式——工厂方法模式。通过具体案例和代码示例,我们将了解工厂方法模式的定义、应用场景、实现步骤以及其优势与潜在缺点。无论你是Python新手还是有经验的开发者,都能从本文中获得关于如何在实际项目中有效应用工厂方法模式的启发。 ###
|
10天前
|
设计模式 安全 Java
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
27 1
|
21天前
|
前端开发 JavaScript UED
探索JavaScript中的异步编程模式
【10月更文挑战第21天】在数字时代的浪潮中,JavaScript作为一门动态的、解释型的编程语言,以其卓越的灵活性和强大的功能在Web开发领域扮演着举足轻重的角色。本篇文章旨在深入探讨JavaScript中的异步编程模式,揭示其背后的原理和实践方法。通过分析回调函数、Promise对象以及async/await语法糖等关键技术点,我们将一同揭开JavaScript异步编程的神秘面纱,领略其带来的非阻塞I/O操作的魅力。让我们跟随代码的步伐,开启一场关于时间、性能与用户体验的奇妙之旅。
|
9天前
|
前端开发 JavaScript UED
探索JavaScript的异步编程模式
【10月更文挑战第33天】在JavaScript的世界里,异步编程是提升应用性能和用户体验的关键。本文将带你深入理解异步编程的核心概念,并展示如何在实际开发中运用这些知识来构建更流畅、响应更快的Web应用程序。从回调函数到Promises,再到async/await,我们将一步步解锁JavaScript异步编程的秘密,让你轻松应对各种复杂的异步场景。
|
1月前
|
设计模式 Java Kotlin
Kotlin教程笔记(51) - 改良设计模式 - 构建者模式
本教程详细讲解Kotlin语法,适合希望深入了解Kotlin的开发者。对于快速学习Kotlin语法,推荐查看“简洁”系列教程。本文重点介绍了构建者模式在Kotlin中的应用与改良,包括如何使用具名可选参数简化复杂对象的创建过程,以及如何在初始化代码块中对参数进行约束和校验。
21 3
|
1月前
|
JavaScript 前端开发 API
探索Node.js中的异步编程模式
【10月更文挑战第4天】在JavaScript的世界中,异步编程是提升应用性能和用户体验的关键。本文将深入探讨Node.js中异步编程的几种模式,包括回调函数、Promises、async/await,并分享如何有效利用这些模式来构建高性能的后端服务。
|
1月前
|
JavaScript 前端开发 调度
探索Node.js中的异步编程模式
在Node.js的世界里,异步编程是核心。本文将带你深入了解异步编程的精髓,通过代码示例和实际案例分析,我们将一起掌握事件循环、回调函数、Promises以及async/await等关键概念。准备好迎接挑战,让你的Node.js应用飞起来!
|
1月前
|
设计模式 JavaScript 前端开发
JavaScript设计模式--访问者模式
【10月更文挑战第1天】
31 3

热门文章

最新文章

  • 1
    C++一分钟之-设计模式:工厂模式与抽象工厂
    42
  • 2
    《手把手教你》系列基础篇(九十四)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-下篇(详解教程)
    46
  • 3
    C++一分钟之-C++中的设计模式:单例模式
    54
  • 4
    《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
    38
  • 5
    《手把手教你》系列基础篇(九十二)-java+ selenium自动化测试-框架设计基础-POM设计模式简介(详解教程)
    62
  • 6
    Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
    57
  • 7
    Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
    41
  • 8
    Java面试题:如何使用设计模式优化多线程环境下的资源管理?Java内存模型与并发工具类的协同工作,描述ForkJoinPool的工作机制,并解释其在并行计算中的优势。如何根据任务特性调整线程池参数
    50
  • 9
    Java面试题:请列举三种常用的设计模式,并分别给出在Java中的应用场景?请分析Java内存管理中的主要问题,并提出相应的优化策略?请简述Java多线程编程中的常见问题,并给出解决方案
    106
  • 10
    Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
    78