首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Reactive Programming 学习笔记

Reactive Programming 学习笔记

参考文档:

The Reactive Manifesto

Introduction to Reactive Programming-Reactor

The introduction to Reactive Programming you've been missing

ReactiveX

What is Reactive Programming?

Reactive Programming 带来哪些显著的编程变化

Callback Hell

背景

时代背景

Organisations working in disparate domains are independently discovering patterns for building software that look the same. These systems are more robust, more resilient, more flexible and better positioned to meet modern demands. ——《The Reactive Manifesto》

在不同领域中的公司在构建软件过程当中,对系统提出了更健壮、回弹性、灵活的要求。过去的应用程序部署在多个服务器上,对响应时间的要求不高,数据量不多,而今的应用程序(以大型社交网络facebook为例,每天能够产生25亿的内容条数,500+TB的数据量,100+PB 单个HDFS集群中的磁盘容量),用户期望毫秒级的响应时间,以及服务100%都能够正常运行,传统的软件框架已经不能够满足需求。

The benefit is more evident in modern webapps and mobile apps that are highly interactive with a multitude of UI events related to data events.10 years ago, interaction with web pages was basically about submitting a long form to the backend and performing simple rendering to the frontend. Apps have evolved to be more real-time: modifying a single form field can automatically trigger a save to the backend, "likes" to some content can be reflected in real time to other connected users, and

so forth. ——Andre Staltz

如简单的RESTful框架中的如post新建一个实例是同步且阻塞的,当很多用户发送大量的请求时,会导致服务器响应速度变慢,而采用响应式编程对于这类拥有巨多实时events的App能够很好的提高响应时间。

类似技术Ruby Event-Machine

Event-Machine是一个并发编程的抽象。可以让Ruby用一个单线程来处理高并发的请求。

Actor Model

与面向对象编程类似,Actor Model是计算机科学的一个重要研究方向,早在七十年代就有了。Actor是计算的一个抽象,可以用于并发系统。Actor之间相互发送消息,因此在某种意义上也是反应式的。Actor和Reactive在概念上有很高的重合度。区别往往在实现层面(例[Akka可以用于进程间通信,是这个框架的显著特点)。

Deferred results (Futures)Map-reduce and fork-join

将并行处理进行抽象是很有意义的,这样的例子很多。Java编程中最近有了Map-reduce 和 fork-join。Map-reduce用于Hadoop,fork-join是jdk1.7之后的版本自带的功能。这2个技术与Deferred results类似,不能应对复杂的组合调用场景。

Coroutines

coroutine可以相互之间传递控制权,不用由调用者统一协调,可以简化并发编程。可以用coroutine来实现Reactive编程。Fibers和Generators都属于coroutine技术。

技术背景-《Reactor Reference Guide》Blocking Can Be Wasteful

现代应用需要应对大量的并发用户,而且即使现代硬件的处理能力飞速发展,软件性能仍然是关键因素。

通常,Java开发者使用阻塞式(blocking)编写代码。这没有问题,在出现性能瓶颈后, 我们可以增加处理线程,线程中同样是阻塞的代码。但是这种使用资源的方式会迅速面临 资源竞争和并发问题。

更糟糕的是,阻塞会浪费资源。具体来说,比如当一个程序面临延迟(通常是I/O方面, 比如数据库读写请求或网络调用),所在线程需要进入 idle 状态等待数据,从而浪费资源。

Asynchronicity to the Rescue

第二种思路——提高执行效率——可以解决资源浪费问题。通过编写异步非阻塞的代码, (任务发起异步调用后)执行过程会切换到另一个使用同样底层资源的活跃任务,然后等 异步调用返回结果再去处理。

Java 提供了两种异步编程方式:

回调(Callbacks):异步方法没有返回值,而是采用一个 作为参数(lambda 或匿名类),当结果出来后回调这个 。常见的例子比如 Swings 的 。

Futures:异步方法立即返回一个 ,该异步方法要返回结果的是 类型,通过封装。这个结果并不是立刻可以拿到,而是等实际处理结束才可用。比如, 执行 任务时会返回 对象。

这些技术够用吗?并非对于每个用例都是如此,两种方式都有局限性。

回调很难组合起来,因为很快就会导致代码难以理解和维护(即所谓的“回调地狱(callback hell)”)。

Reactive Manifesto 反应式宣言

定义

We believe that a coherent approach to systems architecture is needed, and we believe that all necessary aspects are already recognised individually: we want systems that are Responsive, Resilient, Elastic and Message Driven. We call these Reactive Systems. ——《The Reactive Manifesto》

在软件系统中要求以下特质:

即时响应性(Responsive)

回弹性(Resilient)

弹性(Elastic)

消息驱动(Message Driven)。

对于这样的系统,称之为反应式系统(Reactive System)

使用反应式方式构建的反应式系统会更加灵活、松耦合、可伸缩。 这使得它们的开发和调整更加容易。 它们对系统的失败也更加的包容, 而当失败确实发生时, 它们的应对方案会是得体处理而非混乱无序。 反应式系统具有高度的即时响应性, 为用户提供了高效的互动反馈。

反应式系统的特质

即使响应性(Responsive):系统能够及时的作出相应,即时响应式可用性和实用性的基石,即使响应意味着可以快速地检测到问题并且有效地对其进行处理。即使响应的系统提供快速而一致的响应时间,提供一致的服务质量,有助于建立用户的信任并促使用户与系统作进一步的互动。

A responsive system is quick to react to all users — under blue skies

and grey skies — in order to ensure a consistently positive user

experience.——Kevin Webber

回弹性(Resilient):系统在出现失败时依然保持即时响应性。这不仅适用于高可用的、任务关键型系统——任何不具备回弹性的系统就会在发生失败之后丢失即时响应性。回弹性通过复制、遏制、隔离以及委托来实现的。

弹性:系统在不断变化的工作负载之下依然保持即时响应性。 反应式系统可以对输入(负载)的速率变化做出反应,比如通过增加或者减少被分配用于服务这些输入(负载)的资源,使得资源的利用更加高效,节约成本。弹性意味着当资源根据需求按比例地减少或者增加时, 系统的吞吐量将自动地向下或者向上缩放, 从而满足不同的需求。系统需要具有可伸缩性(scalability)以使得其可以从运行时动态地添加或者删除资源中获益,弹性是建立在可伸缩性的基础上的。

消息驱动:反应式系统依赖异步的消息传递,从而确保了松耦合、隔离、位置透明的组件之间有着明确的边界。非阻塞的通信使得通信的接受者可以只在活动时菜消耗资源,从而减少系统开销。

在各种条件下(例如外部系统故障或流量激增)的快速性和积极的用户体验取决于Reactive应用程序的两个特征:弹性和可伸缩性。 消息驱动的体系结构为响应式系统提供了整体基础。

理念

Large systems are composed of smaller ones and therefore depend on the Reactive properties of their constituents. This means that Reactive Systems apply design principles so these properties apply at all levels of scale, making them composable. The largest systems in the world rely upon architectures based on these properties and serve the needs of billions of people daily. It is time to apply these design principles consciously from the start instead of rediscovering them each time.——《The Reactive Manifesto》

大型系统由多个较小型的系统所构成, 因此整体效用取决于它们的构成部分的反应式属性。 这意味着, 反应式系统应用着一些设计原则,使这些属性能在所有级别的规模上生效,而且可组合。世界上各类最大型的系统所依赖的架构都基于这些属性,而且每天都在服务于数十亿人的需求。现在,是时候在系统设计一开始就有意识地应用这些设计原则了,而不是每次都去重新发现它们。

其他定义

维基百科

Incomputing,reactive programmingis a declarativeprogramming paradigmconcerned withdata streamsand the propagation of change. With this paradigm it is possible to express static (e.g., arrays) or dynamic (e.g., event emitters)data streamswith ease, and also communicate that an inferred dependency within the associatedexecution modelexists, which facilitates the automatic propagation of the changed data flow.

维基百科认为 Reactive programming 是一种声明式的编程范式,其核心要素是数据流(data streams )其传播变化( propagation of change),前者是关于数据结构的描述,包括静态的数组(arrays)和动态的事件发射器(event emitters)。

Spring 5 中的定义

The term "reactive" refers to programming models that are built aroundreacting to change — network component reacting to I/O events, UI controller reacting to mouse events, etc. In that sensenon-blockingis reactive because instead of being blocked we are now in the mode of reacting to notifications as operations complete or data becomes available.

ReactiveX 中的定义

ReactiveX extends the observer pattern to support sequences of data and/or events and adds operators that allow you to compose sequences together declaratively while abstracting away concerns about things like low-level threading, synchronization, thread-safety, concurrent data structures, and non-blocking I/O.

微软

The Reactive Extensions (Rx) is a library for composing asynchronous and event-based programs

using observable sequences and LINQ-style query operators. Using Rx, developersrepresentasynchronous data streams withObservables,*query* asynchronous data streams usingLINQ operators, and*parameterize* the concurrency in the asynchronous data streams usingSchedulers. Simply put, Rx = Observables + LINQ + Schedulers.

Andre Staltz

Reactive programming is programming with asynchronous data streams.

You are able to create data streams of anything, not just from click and hover events. Streams are cheap and ubiquitous, anything can be a stream: variables, user inputs, properties, caches, data structures, etc.

响应式编程是使用异步数据流进行编程响应式编程的思路大概是:你可以用包括Click 和 Hover 事件在内的任何东西创建 Data stream。Stream 廉价且常见,任何东西都可以是一个 Stream:变量、用户输入、属性、Cache、数据结构等等。在这个基础上,你还有令人惊艳的函数去组合、创建、过滤这些 Streams。

案例理解

同步、异步、阻塞、非阻塞

老张爱喝茶,废话不说,煮开水。

出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

1 老张把水壶放到火上,立等水开。(同步阻塞)

老张觉得自己有点傻

2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)

老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。

3 老张把响水壶放到火上,立等水开。(异步阻塞)

老张觉得这样傻等意义不大

4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)

老张觉得自己聪明了。

所谓同步异步,只是对于水壶而言。普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。

所谓阻塞非阻塞,仅仅对于老张而言。立等的老张,阻塞;看电视的老张,非阻塞。情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

响应,一定是对一个事件、一个信号(诸如此类的描述)产生了反应。响水壶的响应是什么呢?水温达到一定程度,水壶的反应是会响。水壶响了,声音传递给老张,老张的反应是去关水壶。

再看普通水壶,水温达到一定程度,水壶没有反应,水的反应是冒气泡,冒水雾。只是这个信号不太容易传递,要跑过来看,所以老张只能以轮训的方式来办事情,没法跑到一边等通知。

对于两个水壶而言,烧水都是阻塞的,水没烧完就干不了其他的事情

Why is a message-driven architecture so important for responsiveness

世界是异步的,这里有一个例子:你打算煮一壶咖啡,但你意识到你已经没有奶油和糖了

一种可能的方法是:

开始煮一壶咖啡

在煮的过程中去商店

买奶油和糖

回到家里

立刻喝咖啡

享受

另一种可能的方法是:

去商店

买奶油和糖

回到家里

开始煮咖啡

不耐烦地看着煮咖啡

咖啡因不足

奔溃

显而易见,消息驱动框架为你提供了一个异步边界,将你的时间和空间分离

"Clicks on a button" Event stream

Stream 是一个按时间排序的 Events 序列,它可以放射三种不同的 Events:(某种类型的)Value、Error 或者一个" Completed" Signal。考虑一下"Completed"发生的时机,例如,当包含这个按钮的窗口或者视图被关闭时。

通过分别为 Value、Error、"Completed"定义事件处理函数,我们将会异步地捕获这些 Events。有时可以忽略 Error 与"Completed",你只需要定义 Value 的事件处理函数就行。监听一个 Stream 也被称作是订阅 ,而我们所定义的函数就是观察者,Stream则是被观察者——Observer Design Pattern

首先,让我们做一个能记录一个按钮点击了多少次的计数器 Stream。在常见的响应式编程库中,每个Stream都会有多个方法,如 , , , 等等。当你调用其中一个方法时,例如 ,它就会基于原来的 Click stream 返回一个新的 Stream。它不会对原来的 Click steam 作任何修改。这个特性称为不可变性,它对于响应式编程 Stream,就如果汁对于薄煎饼。我们也可以对方法进行链式调用,如 :

会根据你提供的 函数把原 Stream 中的 Value 分别映射到新的 Stream 中。在我们的例子中,我们把每一次 Click 都映射为数字 1。 会根据你提供的 函数把 Stream 中的所有 Value 聚合成一个 Value ,这个示例中 只是一个简单的添加函数。然后,每 Click 一次, 就会把点击的总次数发给它的观察者。

为了展示响应式编程真正的实力,让我们假设你想得到一个包含“双击”事件的 Stream。为了让它更加有趣,假设我们想要的这个 Stream 要同时考虑三击(Triple clicks),或者更加宽泛,连击(两次或更多)。深呼吸一下,然后想像一下在传统的命令式且带状态的方式中你会怎么实现。我敢打赌代码会像一堆乱麻,并且会使用一些变量保存状态,同时也有一些计算时间间隔的代码。

灰色的方框是用来转换 Stream 函数的。首先,简而言之,我们把连续 250 ms 内的 Click 都积累到一个列表中(就是 做的事。不要在意这些细节,我们只是展示一下响应式编程而已)。结果是一个列表的 Stream ,然后我们使用 把每个列表映射为一个整数,即它的长度。最终,我们使用 把整数 1 给过滤掉。就这样,3 个操作就生成了我们想要的 Stream。然后我们就可以订阅(“监听”)这个 Stream,并以我们所希望的方式作出反应。

你可以把同样的操作应用到不同种类的 Stream 上,例如,一个 API 响应的 Stream;另一方面,还有很多其它可用的函数。

相关知识

异步(asynchronous)

牛津词典把“asynchronous(异步的)”定义为“不同时存在或发生的”。 在反应式宣言的上下文中, 意为: 在来自客户端的请求被发送到了服务端之后, 对于该请求的处理可以发生这之后的任意时间点。 对于发生在服务内部的执行过程, 客户端不能直接对其进行观察, 或者与之同步。

非阻塞的(Non-Blocking)

在并发编程中, 如果争夺资源的线程并没有被保护该资源的互斥所无限期地推迟执行, 那么该算法则被认为是非阻塞的。 在实践中, 这通常缩影为一个 API, 当资源可用时, 该API将允许访问该资源,否则它将会立即地返回, 并通知调用者该资源当前不可用, 或者该操作已经启动了,但是尚未完成。 某个资源的非阻塞 API 使得其调用者可以进行其他操作, 而不是被阻塞以等待该资源变为可用。 此外,还可以通过允许资源的客户端注册, 以便让其在资源可用时,或者操作已经完成时获得通知。

回压(back-pressure)

当某个组件正竭力维持响应能力时, 系统作为一个整体就需要以合理的方式作出反应。 对于正遭受压力的组件来说, 无论是灾难性地失败, 还是不受控地丢弃消息, 都是不可接受的。既然它既不能(成功地)应对(压力), 又不能(直接地)失败, 那么它就应该向其上游组件传达其正在遭受压力的事实, 并让它们(该组件的上游组件)降低负载。 这种回压(back-pressure)是一种重要的反馈机制, 使得系统得以优雅地响应负载, 而不是在负载下崩溃。 回压可以一路扩散到(系统的)用户, 在这时即时响应性可能会有所降低, 但是这种机制将确保系统在负载之下具有回弹性 , 并将提供信息,从而允许系统本身通过利用其他资源来帮助分发负载。

失败(Failure)

失败是一种服务内部的意外事件, 会阻止服务继续正常地运行。 失败通常会阻止对于当前的、 并可能所有接下来的客户端请求的响应。 和错误相对照, 错误是意料之中的,并且针各种情况进行了处理( 例如, 在输入验证的过程中所发现的错误), 将会作为该消息的正常处理过程的一部分返回给客户端。

失败的例子有:硬件故障、 由于致命的资源耗尽而引起的进程意外终止,以及导致系统内部状态损坏的程序缺陷。

回调地狱

Asynchronous JavaScript, or JavaScript that uses callbacks, is hard to get right intuitively. A lot of code ends up looking like this:

See the pyramid shape and all the at the end? Eek! This is affectionately known ascallback hell.

Observer Design Pattern

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。观察者模式属于行为型模式。

意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。

何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

如何解决:使用面向对象技术,可以将这种依赖关系弱化。

关键代码:在抽象类里有一个 ArrayList 存放观察者们。

编程范式

编程范型编程范式程序设计法(英语:Programming paradigm),(即模范、典范之意,范式即模式、方法),是一类典型的编程风格,是指从事软件工程的一类典型的风格。如:函数式编程、程序编程、面向对象编程、指令式编程等等为不同的编程范型。

应用

应用场景Reactive Streams JVM认为的使用场景

The main goal of Reactive Streams is to govern the exchange of stream data across an asynchronous boundary.

Reactive Streams JVM 认为 Reactive Streams 用于在异步边界(asynchronous boundary)管理流式数据交换( govern the exchange of stream data)。异步说明其并发模型,流式数据则体现数据结构,管理则强调它们的它们之间的协调。

Spring 5 认为的使用场景

Reactive and non-blocking generally do not make applications run faster. They can, in some cases, for example if using the to execute remote calls in parallel. On the whole it requires more work to do things the non-blocking way and that can increase slightly the required processing time.

The key expected benefit of reactive and non-blocking is the ability to scale with a small, fixed number of threads and less memory. That makes applications more resilient under load because they scale in a more predictable way.

Spring 认为 Reactive 和非阻塞通常并非让应用运行更快速(generally do not make applications run faster),甚至会增加少量的处理时间,因此,它的使用场景则利用较少的资源,提升应用的伸缩性(scale with a small, fixed number of threads and less memory)。

ReactiveX认为的使用场景

The ReactiveX Observable model allows you to treat streams of asynchronous events with the same sort of simple, composable operations that you use for collections of data items like arrays. It frees you from tangled webs of callbacks, and thereby makes your code more readable and less prone to bugs.

ReactiveX 所描述的使用场景与 Spring 的不同,它没有从性能入手,而是代码可读性和减少 Bugs 的角度出发,解释了 Reactive Programming 的价值。同时,强调其框架的核心特性:异步(asynchronous)、同顺序(same sort)和组合操作(composable operations)。它也间接地说明了,Java 8 在组合操作的限制,以及操作符的不足。

Reactor 认为的使用场景

Composability and readability

Data as a flow manipulated with a rich vocabulary of operators

Nothing happens until you subscribe

Backpressure or the ability for the consumer to signal the producer that the rate of emission is too high

High level but high value abstraction that is concurrency-agnostic

Reactor 同样强调结构性和可读性(Composability and readability)和高层次并发抽象(High level abstraction),并明确地表示它提供丰富的数据操作符( rich vocabulary of operators)弥补 API 的短板,还支持背压(Backpressure)操作,提供数据生产者和消费者的消息机制,协调它们之间的产销失衡的情况。同时,Reactor 采用订阅式数据消费(Nothing happens until you subscribe)的机制,实现 所不具备的数据推送机制。

Reactor

Reactor 是一个用于JVM的完全非阻塞的响应式编程框架,具备高效的需求管理(即对 “背压(backpressure)”的控制)能力。它与 Java 8 函数式 API 直接集成,比如 , , 以及 。它提供了异步序列 API (用于[N]个元素)和 (用于 [0|1]个元素),并完全遵循和实现了“响应式扩展规范”(Reactive Extensions Specification)。

Reactor 的 组件还支持非阻塞的进程间通信(inter-process communication, IPC)。 Reactor IPC 为 HTTP(包括 Websockets)、TCP 和 UDP 提供了支持背压的网络引擎,从而适合 应用于微服务架构。并且完整支持响应式编解码(reactive encoding and decoding)。

类似 Reactor 这样的响应式库的目标就是要弥补上述“经典”的 JVM 异步方式所带来的不足, 此外还会关注一下几个方面:

可编排性(Composability)以及可读性(Readability)

可编排性,指的是编排多个异步任务的能力。比如我们将前一个任务的结果传递给后一个任务作为输入, 或者将多个任务以分解再汇总(fork-join)的形式执行,或者将异步的任务作为离散的组件在系统中 进行重用。

这种编排任务的能力与代码的可读性和可维护性是紧密相关的。随着异步处理任务数量和复杂度 的提高,编写和阅读代码都变得越来越困难。就像我们刚才看到的,回调模式是简单的,但是缺点 是在复杂的处理逻辑中,回调中会层层嵌入回调,导致回调地狱(Callback Hell)。你能猜到 (或有过这种痛苦经历),这样的代码是难以阅读和分析的。

Reactor 提供了丰富的编排操作,从而代码直观反映了处理流程,并且所有的操作保持在同一层次 (尽量避免了嵌套)。

使用丰富的操作符来处理形如的数据

在 Reactor 中,操作符(operator)就像装配线中的工位(操作员或装配机器人)。每一个操作符 对 进行相应的处理,然后将 包装为一个新的 。就像一个链条, 数据源自第一个 ,然后顺链条而下,在每个环节进行相应的处理。

订阅(subscribe)之前什么都不会发生

在 Reactor 中,当你创建了一条 处理链,数据还不会开始生成。事实上,你是创建了 一种抽象的对于异步处理流程的描述(从而方便重用和组装)。

当真正“订阅(subscrib)”的时候,你需要将 关联到一个 上,然后 才会触发整个链的流动。这时候, 会向上游发送一个 信号,一直到达源头 的 。

背压(backpressure)具体来说即消费者能够反向告知生产者生产内容的速度的能力

向上游传递信号这一点也被用于实现背压,就像在装配线上,某个工位的处理速度如果慢于流水线 速度,会对上游发送反馈信号一样。

在响应式流规范中实际定义的机制同刚才的类比非常接近:订阅者可以无限接受数据并让它的源头 “满负荷”推送所有的数据,也可以通过使用 机制来告知源头它一次最多能够处理 个元素。

中间环节的操作也可以影响 。想象一个能够将每10个元素分批打包的缓存()操作。 如果订阅者请求一个元素,那么对于源头来说可以生成10个元素。此外预取策略也可以使用了, 比如在订阅前预先生成元素。

这样能够将“推送”模式转换为“推送+拉取”混合的模式,如果下游准备好了,可以从上游拉取 n 个元素;但是如果上游元素还没有准备好,下游还是要等待上游的推送。

高层次(同时也是有高价值的)的抽象,从而达到并发无关的效果

ReactiveX

ReactiveX是Reactive Extensions的缩写,一般简写为Rx,最初是LINQ的一个扩展,由微软的架构师Erik

Meijer领导的团队开发,在2012年11月开源,Rx是一个编程模型,目标是提供一致的编程接口,帮助开发者更方便的处理异步数据流,Rx库支持.NET、JavaScript和C++,Rx近几年越来越流行了,现在已经支持几乎全部的流行编程语言了,Rx的大部分语言库由ReactiveX这个组织负责维护,比较流行的有RxJava/RxJS/Rx.NET,社区网站是

reactivex.io

总结

本次课程选题响应式编程范式进行了学习,由于对这类编程思想毫无接触,在理解上有一定的难度,加上很多实例以Java代码的形式出现,更增添了一些难度。在阅读了大量的相关资料后,对于响应式系统思想有了一定的了解,特别是观察者模式和同步异步的学习,对于过去编程过程中采用的一些算法和模式有了很大的启发,特别是结合在课程上大数据分析spark,mapreduce,rest等知识点的结合,有了更好的理解,不过对于这一编程手法的了解也只是冰山一角,有待在未来的实践中更好的学习。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20190107G00YA300?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券