fs2是scalaz-stream的最新版本,沿用了scalaz-stream被动式(pull model)数据流原理但采用了全新的实现方法。fs2比较scalaz-stream而言具备了:更精简的基础组件(combinator)、更安全的类型、资源使用(type safe, resource safety)、更高的运算效率。由于fs2基本沿用了scalaz-stream的原理,所以我们会在下面的讨论里着重介绍fs2的使用。根据fs2的官方文件,fs2具备了以下新的特点:

1、完全不含任何外部依赖(third-party dependency)

2、流元素增加了节组(chunk)类型和相关的操作方法

3、fs2不再只局限于Task一种副作用运算方式(effect)。用户可以提供自己的effect类型

4、更精简的流转换组件(stream transformation primitives)

5、增加了更多并行运算组件(concurrent primitives)

6、通过bracket函数增强了资源使用安全,特别是异线程资源占用的事后处理过程。用onFinalize取代了onComplete

7、stream状态转换采用了全新的实现方式,使用了新的数据结构:Pull

8、Stream取代了Process。fs2中再没有Process1、Tee、Wye、Channel这些类型别名,取而代之的是:

  • type Pipe[F,A,B] = Stream[F,A] => Stream[F,B]
  • type Pipe2[F,A,B,C] = (Stream[F,A], Stream[F,B]) => Stream[F,C]
  • Pipe 替代了 Channel 和 Process1
  • Pipe2 替代了 Tee 和 Wye

下面我们来看看fs2的一些基本操作:

  1. Stream() //> res0: fs2.Stream[Nothing,Nothing] = Segment(Emit(Chunk()))
  2. Stream(,,) //> res1: fs2.Stream[Nothing,Int] = Segment(Emit(Chunk(1, 2, 3)))
  3. Stream.emit() //> res2: fs2.Stream[Nothing,Int] = Segment(Emit(Chunk(4)))
  4. Stream.emits(Seq(,,)) //> res3: fs2.Stream[Nothing,Int] = Segment(Emit(Chunk(1, 2, 3)))

Stream的类型款式是:Stream[F[_],A]。从上面的例子我们看到所有的F[_]都是Nothing,我们称这样的流为纯数据流(pure stream)。再值得注意的是每个流构建都形成了一个Chunk,代表一节元素。fs2增加了Chunk类型来提高数据元素处理效率。这是fs2的一项新功能。

我们可以用toList或者toVector来运算纯数据流中的元素值:

  1. Stream.emits(Seq(,,)).toList //> res3: List[Int] = List(1, 2, 3)
  2. Stream.emits(Seq(,,)).toVector //> res4: Vector[Int] = Vector(1, 2, 3)

纯数据流具备了许多与List相似的操作函数:

  1. (Stream(,,) ++ Stream(,)).toList //> res5: List[Int] = List(1, 2, 3, 4, 5)
  2. Stream(,,).map { _ + }.toList //> res6: List[Int] = List(2, 3, 4)
  3. Stream(,,).filter { _ % == }.toList //> res7: List[Int] = List(2)
  4. Stream(,,).fold()(_ + _).toList //> res8: List[Int] = List(6)
  5. Stream(None,Some(),Some(),None).collect {
  6. case None =>
  7. case Some(i) => i
  8. }.toList //> res9: List[Int] = List(0, 1, 3, 0)
  9. Stream.range(,).intersperse().toList //> res10: List[Int] = List(1, 42, 2, 42, 3, 42, 4)
  10. Stream(,,).flatMap {x => Stream(x,x)}.toList //> res11: List[Int] = List(1, 1, 2, 2, 3, 3)
  11. Stream(,,).repeat.take().toList //> res12: List[Int] = List(1, 2, 3, 1, 2)

以上都是一些基本的List操作函数示范。

我们知道,纯数据流就是scalaz-stream里的Process1,即transducer,是负责对流进行状态转换的。在fs2里transducer就是Pipe(也是channel),我们一般用through来连接transducer。上面示范中的take,filter等都是transducer,我们可以在object pipe里找到这些函数:

  1. object pipe {
  2. ...
  3. /** Drop `n` elements of the input, then echo the rest. */
  4. def drop[F[_],I](n: Long): Stream[F,I] => Stream[F,I] =
  5. _ pull (h => Pull.drop(n)(h) flatMap Pull.echo)
  6. ...
  7. /** Emits `true` as soon as a matching element is received, else `false` if no input matches */
  8. def exists[F[_], I](p: I => Boolean): Stream[F, I] => Stream[F, Boolean] =
  9. _ pull { h => Pull.forall[F,I](!p(_))(h) flatMap { i => Pull.output1(!i) }}
  10.  
  11. /** Emit only inputs which match the supplied predicate. */
  12. def filter[F[_], I](f: I => Boolean): Stream[F,I] => Stream[F,I] =
  13. mapChunks(_ filter f)
  14.  
  15. /** Emits the first input (if any) which matches the supplied predicate, to the output of the returned `Pull` */
  16. def find[F[_],I](f: I => Boolean): Stream[F,I] => Stream[F,I] =
  17. _ pull { h => Pull.find(f)(h).flatMap { case o #: h => Pull.output1(o) }}
  18.  
  19. /**
  20. * Folds all inputs using an initial value `z` and supplied binary operator,
  21. * and emits a single element stream.
  22. */
  23. def fold[F[_],I,O](z: O)(f: (O, I) => O): Stream[F,I] => Stream[F,O] =
  24. _ pull { h => Pull.fold(z)(f)(h).flatMap(Pull.output1) }
  25. ...
  26. /** Emits all elements of the input except the first one. */
  27. def tail[F[_],I]: Stream[F,I] => Stream[F,I] =
  28. drop()
  29.  
  30. /** Emit the first `n` elements of the input `Handle` and return the new `Handle`. */
  31. def take[F[_],I](n: Long): Stream[F,I] => Stream[F,I] =
  32. _ pull Pull.take(n)
  33. ...

我们可以用through来连接这些transducer:

  1. Stream(,,).repeat
  2. .throughPure(pipe.take())
  3. .throughPure(pipe.filter(_ % == ))
  4. .toList //> res13: List[Int] = List(2, 2, 2)

以上的throughPure等于是through + pure。Pure是没有任何作用的F[_],是专门为帮助compiler进行类型推导的类型。其实我们可以用pure先把纯数据流升格后再用through:

  1. Stream(,,).repeat.pure
  2. .through(pipe.take())
  3. .through(pipe.filter(_ % == ))
  4. .toList //> res14: List[Int] = List(2, 2, 2)

这时compiler不再出错误信息了。在fs2 pipe对象里的函数通过方法注入或者类型继承变成了Stream的自身函数,所以我们也可以直接在Stream类型上使用这些transducer:

  1. Stream(,,).repeat.take().filter(_ % == ).toList
  2. //> res15: List[Int] = List(2, 2, 2)

我们在前面提到过fs2使用了全新的方法和数据类型来实现transducer。transducer的类型是Pipe,即:

  1. type Pipe[F[_],-I,+O] = Stream[F,I] => Stream[F,O]

我们看到Pipe就是一个Function1的类型别名,一个lambda:提供一个Stream[F,I],返回Stream[F,O]。那么在fs2里是如何读取一个Stream[F,I]里的元素呢?我们前面提到是通过一个新的数据结构Pull来实现的,先来看看fs2是如何实现Stream >> Pull >> Stream转换的:

  1. val pll = Stream(,,).pure.open //> pll : fs2.Pull[fs2.Pure,Nothing,fs2.Stream.Handle[fs2.Pure,Int]] = fs2.Pull
  2. de5031f
  3. val strm = pll.close //> strm : fs2.Stream[fs2.Pure,Nothing] = evalScope(Scope(Bind(Eval(Snapshot),<
  4. function1>))).flatMap(<function1>)

对一个Stream施用open后得到一个Pull类型。pll是个Pull数据结构,它的类型定义如下:

  1. class Pull[+F[_],+O,+R](private[fs2] val get: Free[P[F,O]#f,Option[Either[Throwable,R]]])

在Pull的类型参数中F是一个运算,O代表输出元素类型,R代表Pull里的数据资源。我们可以从R读取元素。在上面的例子里pll的R值是个Handle类型。这个类型里应该提供了读取元素的方法:

  1. implicit class HandleOps[+F[_],+A](h: Handle[F,A]) {
  2. def push[A2>:A](c: Chunk[A2])(implicit A2: RealSupertype[A,A2]): Handle[F,A2] =
  3. self.push(h: Handle[F,A2])(c)
  4. def push1[A2>:A](a: A2)(implicit A2: RealSupertype[A,A2]): Handle[F,A2] =
  5. self.push1(h: Handle[F,A2])(a)
  6. def #:[H](hd: H): Step[H, Handle[F,A]] = Step(hd, h)
  7. def await: Pull[F, Nothing, Step[Chunk[A], Handle[F,A]]] = self.await(h)
  8. def await1: Pull[F, Nothing, Step[A, Handle[F,A]]] = self.await1(h)
  9. def awaitNonempty: Pull[F, Nothing, Step[Chunk[A], Handle[F,A]]] = Pull.awaitNonempty(h)
  10. def echo1: Pull[F,A,Handle[F,A]] = Pull.echo1(h)
  11. def echoChunk: Pull[F,A,Handle[F,A]] = Pull.echoChunk(h)
  12. def peek: Pull[F, Nothing, Step[Chunk[A], Handle[F,A]]] = self.peek(h)
  13. def peek1: Pull[F, Nothing, Step[A, Handle[F,A]]] = self.peek1(h)
  14. def awaitAsync[F2[_],A2>:A](implicit S: Sub1[F,F2], F2: Async[F2], A2: RealSupertype[A,A2]):
  15. Pull[F2, Nothing, AsyncStep[F2,A2]] = self.awaitAsync(Sub1.substHandle(h))
  16. def await1Async[F2[_],A2>:A](implicit S: Sub1[F,F2], F2: Async[F2], A2: RealSupertype[A,A2]):
  17. Pull[F2, Nothing, AsyncStep1[F2,A2]] = self.await1Async(Sub1.substHandle(h))
  18. def covary[F2[_]](implicit S: Sub1[F,F2]): Handle[F2,A] = Sub1.substHandle(h)
  19. }
  20.  
  21. implicit class HandleInvariantEffectOps[F[_],+A](h: Handle[F,A]) {
  22. def invAwait1Async[A2>:A](implicit F: Async[F], A2: RealSupertype[A,A2]):
  23. Pull[F, Nothing, AsyncStep1[F,A2]] = self.await1Async(h)
  24. def invAwaitAsync[A2>:A](implicit F: Async[F], A2: RealSupertype[A,A2]):
  25. Pull[F, Nothing, AsyncStep[F,A2]] = self.awaitAsync(h)
  26. def receive1[O,B](f: Step[A,Handle[F,A]] => Pull[F,O,B]): Pull[F,O,B] = h.await1.flatMap(f)
  27. def receive[O,B](f: Step[Chunk[A],Handle[F,A]] => Pull[F,O,B]): Pull[F,O,B] = h.await.flatMap(f)
  28. }

果然在Handle提供的函数里有await,receive等这些读取函数。我们试着来实现一个简单的transducer:一个filter函数:

  1. import scala.language.higherKinds
  2. def myFilter[F[_],A](f: A => Boolean): Pipe[F, A, A] = {
  3. def go(h: Stream.Handle[F,A]): Pull[F,A,Unit] = {
  4. // h.receive1 {case Step(a,h) => if(f(a)) Pull.output1(a) >> go(h) else go(h)}
  5. h.await1.flatMap { case Step(a,h) => if(f(a)) Pull.output1(a) >> go(h) else go(h)}
  6. }
  7. // sin => sin.open.flatMap {h => go(h)}.close
  8. sin => sin.pull(go _)
  9. } //> myFilter: [F[_], A](f: A => Boolean)fs2.Pipe[F,A,A]
  10.  
  11. Stream.range(,).pure.through(myFilter(_ % == )).toList
  12. //> res17: List[Int] = List(0, 2, 4, 6, 8)

我们从Pull里用await1或者receive1把一个Step数据结构从Handle里扯(pull)出来然后再output到Pull结构里。把这个Pull close后得到我们需要的Stream。我们把例子使用的类型及函数款式陈列在下面:

  1. type Pipe[F[_],-I,+O] = Stream[F,I] => Stream[F,O]
  2.  
  3. def await1[F[_],I]: Handle[F,I] => Pull[F,Nothing,Step[I,Handle[F,I]]] = {...}
  4.  
  5. def receive1[F[_],I,O,R](f: Step[I,Handle[F,I]] => Pull[F,O,R]): Handle[F,I] => Pull[F,O,R] =
  6. _.await1.flatMap(f)
  7.  
  8. def pull[F[_],F2[_],A,B](s: Stream[F,A])(using: Handle[F,A] => Pull[F2,B,Any])(implicit S: Sub1[F,F2])
  9. : Stream[F2,B] =
  10. Pull.close { Sub1.substPull(open(s)) flatMap (h => Sub1.substPull(using(h))) }

再示范另一个Pipe的实现:take

  1. def myTake[F[_],A](n: Int): Pipe[F,A,A] = {
  2. def go(n: Int): Stream.Handle[F,A] => Pull[F,A,Unit] = h => {
  3. if (n <= ) Pull.done
  4. else h.receive1 { case a #: h => Pull.output1(a).flatMap{_ => go(n-)(h)}}
  5. }
  6. sin => sin.pull(go(n))
  7. } //> myTake: [F[_], A](n: Int)fs2.Pipe[F,A,A]
  8. Stream.range(,).pure.through(myTake()).toList //> res18: List[Int] = List(0, 1, 2)

我们曾经提过fs2功能提升的其中一项是增加了节组(Chunk)数据类型和相关的操作函数。Chunk是fs2内部使用的一种集合,这样fs2就可以一节一节(by chunks)来处理数据了。Chunk本身具备了完整的集合函数:

  1. /**
  2. * Chunk represents a strict, in-memory sequence of `A` values.
  3. */
  4. trait Chunk[+A] { self =>
  5. def size: Int
  6. def uncons: Option[(A, Chunk[A])] =
  7. if (size == ) None
  8. else Some(apply() -> drop())
  9. def apply(i: Int): A
  10. def copyToArray[B >: A](xs: Array[B]): Unit
  11. def drop(n: Int): Chunk[A]
  12. def take(n: Int): Chunk[A]
  13. def filter(f: A => Boolean): Chunk[A]
  14. def foldLeft[B](z: B)(f: (B,A) => B): B
  15. def foldRight[B](z: B)(f: (A,B) => B): B
  16. def indexWhere(p: A => Boolean): Option[Int] = {
  17. val index = iterator.indexWhere(p)
  18. if (index < ) None else Some(index)
  19. }
  20. def isEmpty = size ==
  21. def toArray[B >: A: ClassTag]: Array[B] = {
  22. val arr = new Array[B](size)
  23. copyToArray(arr)
  24. arr
  25. }
  26. def toList = foldRight(Nil: List[A])(_ :: _)
  27. def toVector = foldLeft(Vector.empty[A])(_ :+ _)
  28. def collect[B](pf: PartialFunction[A,B]): Chunk[B] = {
  29. val buf = new collection.mutable.ArrayBuffer[B](size)
  30. iterator.collect(pf).copyToBuffer(buf)
  31. Chunk.indexedSeq(buf)
  32. }
  33. def map[B](f: A => B): Chunk[B] = {
  34. val buf = new collection.mutable.ArrayBuffer[B](size)
  35. iterator.map(f).copyToBuffer(buf)
  36. Chunk.indexedSeq(buf)
  37. }
  38. def mapAccumulate[S,B](s0: S)(f: (S,A) => (S,B)): (S,Chunk[B]) = {
  39. val buf = new collection.mutable.ArrayBuffer[B](size)
  40. var s = s0
  41. for { c <- iterator } {
  42. val (newS, newC) = f(s, c)
  43. buf += newC
  44. s = newS
  45. }
  46. (s, Chunk.indexedSeq(buf))
  47. }
  48. def scanLeft[B](z: B)(f: (B, A) => B): Chunk[B] = {
  49. val buf = new collection.mutable.ArrayBuffer[B](size + )
  50. iterator.scanLeft(z)(f).copyToBuffer(buf)
  51. Chunk.indexedSeq(buf)
  52. }
  53. def iterator: Iterator[A] = new Iterator[A] {
  54. var i =
  55. def hasNext = i < self.size
  56. def next = { val result = apply(i); i += ; result }
  57. }
  58. ...

fs2的大部分转换函数都考虑了对Chunk数据的处理机制。我们先看看fs2是如何表现Chunk数据的:

  1. (Stream(,) ++ Stream(,,) ++ Stream(,)).chunks.toList
  2. //> res16: List[fs2.Chunk[Int]] = List(Chunk(1, 2), Chunk(3, 4, 5), Chunk(6, 7))

fs2是按照Stream的构建批次来分节的。我们来示范一下如何使用Pull的Chunk机制:

  1. def myTakeC[F[_],A](n: Int): Pipe[F,A,A] = {
  2. def go(n: Int): Stream.Handle[F,A] => Pull[F,A,Unit] = h => {
  3. if ( n <= ) Pull.done
  4. else Pull.awaitLimit(n)(h).flatMap {case Step(chunk,h) =>
  5. if (chunk.size <= n) Pull.output(chunk) >> go(n-chunk.size)(h)
  6. else Pull.output(chunk.take(n)) }
  7. }
  8. sin => sin.pull(go(n))
  9. } //> myTakeC: [F[_], A](n: Int)fs2.Pipe[F,A,A]
  10. val s1 = (Stream(,) ++ Stream(,,) ++ Stream(,))
  11. //> s1 : fs2.Stream[Nothing,Int] = append(append(Segment(Emit(Chunk(1, 2))), S
  12. egment(Emit(Chunk(()))).flatMap(<function1>)), Segment(Emit(Chunk(()))).fla
  13. tMap(<function1>))
  14. s1.pure.through(myTake()).chunks.toList //> res20: List[fs2.Chunk[Int]] = List(Chunk(1), Chunk(2), Chunk(3), Chunk(4))
  15. s1.pure.through(myTakeC()).chunks.toList //> res21: List[fs2.Chunk[Int]] = List(Chunk(1, 2), Chunk(3, 4))

myTake和myTakeC产生了不同的结果。

fs2的特长应该是多线程编程了。在Stream的类型款式中:Stream[F[_],A],F[_]是一种可能产生副作用的运算方式,当F[_]等于Nothing时,Stream[Nothing,A]是一种纯数据流,而Stream[F[_],A]就是一种运算流了。我们可以在对运算流进行状态转换的过程中进行运算来实现F的副作用如:数据库读写、IO操作等。fs2不再绑定Task一种运算方式了。任何有Catchable实例的Monad都可以成为Stream的运算方式。但是,作为一种以多线程编程为主导的工具库,没有什么运算方式会比Task更合适了。
我们可以把一个纯数据流升格成运算流:

  1. val s2 = Stream.emits(Seq(,,)).covary[Task] //> s2 : fs2.Stream[fs2.Task,Int] = Segment(Emit(Chunk(1, 2, 3)))

我们先运算这个运算流,结果为一个Task,然后再运算Task来获取运算值:

  1. val s2 = Stream.emits(Seq(,,)).covary[Task] //> s2 : fs2.Stream[fs2.Task,Int] = Segment(Emit(Chunk(1, 2, 3)))
  2. val t2 = s2.runLog //> t2 : fs2.Task[Vector[Int]] = Task
  3. t2.unsafeRun //> res22: Vector[Int] = Vector(1, 2, 3)

现在使用myTake和myFilter就不需要pure升格了:

  1. s3.through(myFilter(_ % == )).through(myTake()).runLog.unsafeRun
  2. //> res23: Vector[Int] = Vector(2, 2, 2)

下面的例子里展示了fs2的运算流从源头(Source)到传换(Transducer)一直到终点(Sink)的使用示范:

  1. def stdOut: Sink[Task,String] =
  2. _.evalMap { x => Task.delay{ println(s"milli: $x")}}
  3. //> stdOut: => fs2.Sink[fs2.Task,String]
  4. Stream.repeatEval(Task.delay{System.currentTimeMillis})
  5. .map(_.toString)
  6. .through(myTake())
  7. .to(stdOut)
  8. .run.unsafeRun //> milli: 1472001934708
  9. //| milli: 1472001934714
  10. //| milli: 1472001934714

在上面的例子里我们使用了through,to等连接函数。由于数据最终发送到终点stdOut,我们无须用runLog来记录运算结果。

Scalaz(55)- scalaz-stream: fs2-基础介绍,fs2 stream transformation的更多相关文章

  1. Nodejs基础:stream模块入门介绍与使用

    本文摘录自<Nodejs学习笔记>,更多章节及更新,请访问 github主页地址.欢迎加群交流,群号 197339705. 模块概览 nodejs的核心模块,基本上都是stream的的实例 ...

  2. Node.js学习笔记(一)基础介绍

    什么是Node.js 官网介绍: Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js us ...

  3. Node.js 基础介绍

    什么是Node.js 官网介绍: Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js us ...

  4. Erlang基础 -- 介绍 -- Wordcount示例演示

    在前两个blog中,已经说了Erlang的历史.应用场景.特点,这次主要演示一个Wordcount的示例,就是给定一个文本文件,统计这个文本文件中的单词以及该单词出现的次数. 今天和群友们讨论了一个问 ...

  5. Web服务基础介绍

    Web服务基础介绍 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.正常情况下的单次web服务访问流程 博主推荐阅读: https://www.cnblogs.com/yinzh ...

  6. Java 8 新特性之 Stream 流基础体验

    Java 8 新特性之 Stream 流基础体验 package com.company; import java.util.ArrayList; import java.util.List; imp ...

  7. XML基础介绍【一】

    XML基础介绍[一] 1.XML简介(Extensible Markup Language)[可扩展标记语言] XML全称为Extensible Markup Language, 意思是可扩展的标记语 ...

  8. Web3D编程入门总结——WebGL与Three.js基础介绍

    /*在这里对这段时间学习的3D编程知识做个总结,以备再次出发.计划分成“webgl与three.js基础介绍”.“面向对象的基础3D场景框架编写”.“模型导入与简单3D游戏编写”三个部分,其他零散知识 ...

  9. C++ 迭代器 基础介绍

    C++ 迭代器 基础介绍 迭代器提供对一个容器中的对象的访问方法,并且定义了容器中对象的范围.迭代器就如同一个指针.事实上,C++的指针也是一种迭代器.但是,迭代器不仅仅是指针,因此你不能认为他们一定 ...

  10. php常用Stream函数集介绍

    php常用Stream函数集介绍 作者: 字体:[增加 减小] 类型:转载 时间:2013-06-24   本篇文章是对php中的常用Stream函数集进行了详细的分析介绍,需要的朋友参考下     ...

随机推荐

  1. PHP面向对象05_接口与多态

    抽象类与接口 2014-9-2 9:14:32 摘要: 1.抽象类(abstract) 2.接口(interface) 3.多态的应用 1.抽象类(abstract) 特点: 1)抽象类使用abstr ...

  2. Atitit 通过调用gui接口杀掉360杀毒 360卫士  qq保镖等难以结束的进程(javac# php )

    Atitit 通过调用gui接口杀掉360杀毒 360卫士  qq保镖等难以结束的进程(javac# php ) 1.1. 这些流氓软件使用操作系统os提供的普通api根本就杀不掉啊1 1.2. 使用 ...

  3. iOS-----Xcode-Debug尝试

    在Xcode中,Debug时,不能像eclipse ,或VS那些集成开发那样,能直接查看变量的值.那怎么在调试的时候查看XCode的变量呢? 有一些方法的. 1.新建一个Single View App ...

  4. .net生成随机字符串

    生成随机字符串的工具类: /// <summary> /// 随机字符串工具类 /// </summary> public class RandomTools { /// &l ...

  5. 如何优雅的使用vue+vux开发app -01

    如何优雅的使用vue+vux开发app -01 很明显下面是个错误的示范: <!DOCTYPE html> <html> <head> <title>v ...

  6. Jquery实现AJAX拦截

    前几天项目需要实现一个AJAX拦截,于是就用jquery写了一个,这里分享一下. 需求是这样的,ajax不是我来写,所有说我是不能动ajax的,并且我也不知道什么时候它会发生,为了方便项目经理让我把它 ...

  7. HTTP协议从入门到大牛,初识HTTP协议(学习笔记)

    HTTP数据传输协议 当访问一个网页时,浏览器会向服务器发起一条HTTP请求,接着服务器会去寻找相应的资源,如果请求成功,就会把这个对象,对象类型,对象长度以及其他的信息放在HTTP响应中,发送给客户 ...

  8. JavaScript内存优化

    JavaScript内存优化 相对C/C++ 而言,我们所用的JavaScript 在内存这一方面的处理已经让我们在开发中更注重业务逻辑的编写.但是随着业务的不断复杂化,单页面应用.移动HTML5 应 ...

  9. .NET面试题解析(05)-常量、字段、属性、特性与委托

      系列文章目录地址: .NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引 弱小和无知不是生存的障碍,傲慢才是!——<三体> 常见面试题目: 1. const和reado ...

  10. 自制jquery可编辑的下拉框

    昨天看到QQ登录的时候,可以选择以前登录过的账户,这个东西也可以在网站登录的时候用到,所以我就想做一个这样的插件:在网上查了很多,没有找到合适自己的,所以决定自动制作一个. 原理就是一个textbox ...