JYsdan


  • 首页

  • 归档

  • 标签

  • 搜索
close

JavaScript 异步编程

发表于 2016-06-20   |   分类于 learning~   |     |   阅读次数

所谓的异步指的是函数的调用并不直接返回执行的结果,而往往是通过回调函数异步的执行。异步的主要目的是处理非阻塞,在和HTML交互的过程中,会需要一些IO操作(典型的就是Ajax请求,脚本文件加载),如果这些操作是同步的,就会阻塞其它操作,用户的体验就是页面失去了响应。avascript通过事件驱动机制,在单线程模型下,以异步回调函数的形式来实现非阻塞的IO操作。

常见的异步模型

  • 高阶函数
  • 事件监听

    1
    2
    3
    4
    5
    6
    f.on("evt", g);
    function f(){
    setTimeout(function(){
    f.trigger("evt");
    })
    }
  • 发布/订阅

    1
    2
    3
    4
    5
    6
    7
    E.subscribe("evt", g);
    function f(){
    setTimeout(function () {
      // f的任务代码
      E.publish("evt");
    }, 1000);
    }
  • Promise

Promise

传统的回调函数写法使得代码混成一团,变得横向发展而不是向下发展。Promises规范就是为了解决这个问题而提出的,目标是使用正常的程序流程(同步),来处理异步操作。

Promise把类似的异步处理对象和处理规则进行规范化, 并按照采用统一的接口来编写,采取规定方法之外的写法都会出错。promise的功能是可以将复杂的异步处理轻松地进行模式化。
简单来说,它起到代理作用(proxy),充当异步操作与回调函数之间的中介。它使得异步操作具备同步操作的接口,使得程序具备正常的同步运行的流程,回调函数不必再一层层嵌套。它的思想是,每一个异步任务立刻返回一个Promise对象,由于是立刻返回,所以可以采用同步操作的流程。这个Promises对象有一个then方法,允许指定回调函数,在异步任务完成后调用。
ECMAScript 6将其写入语言标准,因此目前JavaScript语言原生支持Promise对象。
更多Promise操作-Promise

异步操作的流程控制

1、串行执行

可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行

1
2
3
4
5
6
7
8
9
10
11
12
13
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
function series(item) {
if(item) {
async( item, function(result) {
results.push(result);
return series(items.shift());
});
} else {
return final(results);
}
}
series(items.shift());

函数series就是串行函数,它会依次执行异步任务,所有任务都完成后,才会执行final函数。items数组保存每一个异步任务的参数,results数组保存每一个异步任务的运行结果

2、并行执行

1
2
3
4
5
6
7
8
9
10
11
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];

items.forEach(function(item) {
async(item, function(result){
results.push(result);
if(results.length == items.length) {
final(results);
}
})
});

上面代码中,forEach方法会同时发起6个异步任务,等到它们全部完成以后,才会执行final函数。

并行执行的好处是效率较高,比起串行执行一次只能执行一个任务,较为节约时间。但是问题在于如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。因此有了第三种流程控制方式

3、并行与串行的结合
所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行n个异步任务。这样就避免了过分占用系统资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0;
var limit = 2;

function launcher() {
while(running < limit && items.length > 0) {
var item = items.shift();
async(item, function(result) {
results.push(result);
running--;
if(items.length > 0) {
launcher();
} else if(running == 0) {
final();
}
});
running++;
}
}

launcher();

JavaScript 多线程

JavaScript 中的 Worker 对象可以创建一个独立线程来处理数据,很自然的处理了阻塞问题。我们可以把繁重的计算任务交给 Worker,等他处理完了再把数据 Post 过来。

Worker 是一个有用的工具,可以创建 ajax 对象和 WebSocket 对象,也就是说他可以直接向服务器请求数据。不过他不能访问 DOM 的信息,更不能直接处理 DOM。

更多的worker-worker

Generator 对象解决异步

参考:https://segmentfault.com/a/1190000002545312
http://www.ruanyifeng.com/blog/2012/12/asynchronous%EF%BC%BFjavascript.html

立即执行函数表达式

发表于 2016-06-12   |   分类于 learning~   |     |   阅读次数

function foo(){ / code / }();//SyntaxError: Unexpected token
function(){ / code /}(); //SyntaxError: Unexpected token

圆括号为了调用函数出现在函数后面时,无论在全局环境或者局部环境里遇到了这样的function关键字,默认的,它会将它当作是一个函数声明,而不是函数表达式.因为 JavaScript 文法明确规定表达式语句不得以 function 或者 { 开头.

所以便出现了下面几种形式:
(function(){/ code /}())
(function(){/ code /})()
!function(){/ code /}();
~function(){/ code /}();
-function(){/ code /}();
+function(){/ code /}();
…

最流行的也最被接受的方法是将函数声明包裹在圆括号里来告诉语法分析器去表达一个函数表达式,因为在Javascript里,圆括号不能包含声明。因为这点,当圆括号为了包裹函数碰上了 function关键词,它便知道将它作为一个函数表达式去解析而不是函数声明。

函数表达式 && 函数声明

函数表达式中的函数可以为匿名函数,也可以有函数名,但是不能直接使用,只能通过表达式左边的变量 a 来调用。
var a = function(){ alert('Function expression'); } var b = new a();

函数声明时必须有函数名
function a(){ alert('Function declaration'); } a();
`


`
匿名函数通过一元操作符变成了函数表达式,因而可以通过()来调用。
因此,执行匿名函数可以通过+,-,!,() 这样的形式来转化为函数表达式,就可以通过()来运行了。

Module

发表于 2016-06-10   |   分类于 learning~   |     |   阅读次数

关于模块化:

  • 可重用
  • 封装了变量和function,和全局的namaspace不接触,松耦合
  • 只暴露可用public的方法,其它私有方法全部隐藏
    阅读全文 »

Node.js(四)-内存控制

发表于 2016-06-08   |   分类于 learning~   |     |   阅读次数

再次阅读《深入浅出Node.js》的笔记系列,内容主要摘自书中

在node中使用内存时只能使用部分内存(64位为1.4GB,32位为0.7GB),造成这个问题的主要原因是Node基于V8构建,所以在Node中使用的javascript对象基本上是通过V8自己的方式来进行分配和管理的(Buffer对象不经过V8的内存分配限制)。
在V8中,所有的javascript对象都是通过堆来进行分配的。在Node启动时可以传递
–max-old-space-size / –max-new-space-size调整内存限制

v8的内存管理模式

一个运行的程序通常是通过在内存中分配一部分空间来表示的。这部分空间被称为驻留集(Resident Set)。 V8的内存管理模式有点类似于Java虚拟机(JVM),它会将内存进行分段:

  • 代码 Code:实际被执行的代码
  • 栈 Stack:包括所有的携带指针引用堆上对象的值类型(原始类型,例如整型和布尔),以及定义程序控制流的指针。
  • 堆 Heap:用于保存引用类型(包括对象、字符串和闭包)的内存段

v8的垃圾回收机制

垃圾回收器解决基本问题就是,识别需要回收的内存。一旦辨别完毕,这些内存区域即可在未来的分配中重用,或者是返还给操作系统。一个对象当它不是处于活跃状态的时候它就死了。一个对象处于活跃状态,当且仅当它被一个根对象或另一个活跃对象指向。根对象被定义为处于活跃状态,是浏览器或V8所引用的对象。比如说全局对象属于根对象,因为它们始终可被访问;浏览器对象,如DOM元素,也属于根对象,尽管在某些场合下它们只是弱引用。

堆的构成

在 node-v4.x 之后,堆主要由以下及部分构成:

  • 新生区:大多数对象开始时被分配在这里。新生区是一个很小的区域,垃圾回收在这个区域非常频繁,与其他区域相独立。
  • 老生区:保存原始数据对象,大多数从新生代晋升的对象会被移动到这里
  • 大对象区:这里存放体积超过1MB大小的对象。每个对象有自己的内存。垃圾回收器从不移动大对象
  • Map区
  • Code区:代码对象,也就是包含JIT之后指令的对象,会被分配到这里

识别指针和数据

垃圾回收器面临的第一个问题是,如何才能在堆中区分指针和数据,因为指针指向着活跃的对象。大多数垃圾回收算法会将对象在内存中挪动(以便减少内存碎片,使内存紧凑),因此即使不区分指针和数据,我们也常常需要对指针进行改写。
V8采用了标记指针法:这种方法需要在每个指针的末位预留一位来标记这个字代表的是指针或数据。

写屏障

如果新生区中某个对象,只有一个指向它的指针,而这个指针恰好是在老生区的对象当中,我们如何才能知道新生区中那个对象是活跃的呢? 为了解决这个问题,实际上在写缓冲区中有一个列表 store-buffer{.cc,.h,-inl.h},列表中记录了所有老生区对象指向新生区的情况。新对象诞生的时候,并不会有指向它的指针,而当有老生区中的对象出现指向新生区对象的指针时,我们便记录下来这样的跨区指向。由于这种记录行为总是发生在写操作时,它被称为写屏障.

回收策略

V8采用了一种分代回收的策略,将内存分为两个生代:新生代和老生代。新生代的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。分别对新生代和老生代使用不同的垃圾回收算法来提升垃圾回收的效率。对象起初都会被分配到新生代,当新生代中的对象满足某些条件时,会被移动到老生代(晋升)

1、V8的分代内存
默认情况下,64位环境下的V8引擎的新生代内存大小32MB、老生代内存大小为1400MB,而32位则减半,分别为16MB和700MB。V8内存的最大保留空间分别为1464MB(64位)和732MB(32位)。具体的计算公式是4*reserved_semispacespace + max_old_generationsize,新生代由两块reserved_semispacespace组成,每块16MB(64位)或8MB(32位)

2、新生代
大多数的对象被分配在这里,这个区域很小但是垃圾回收特别频繁。新生代使用半空间(Semi-space)分配策略,其中新对象最初分配在新生代的活跃半空间内。

  • Scavenge算法
    新生代使用Scavenge算法进行回收。在Scavenge算法的实现中,主要采用了Cheney算法

Cheney算法算法是一种采用复制的方式实现的垃圾回收算法。它将内存一分为二,每一部分空间称为semispace。在这两个semispace中,一个处于使用状态,另一个处于闲置状态。处于使用状态的semispace空间称为From空间,处于闲置状态的空间称为To空间,当我们分配对象时,先是在From空间中进行分配。当开始进行垃圾回收算法时,会检查From空间中的存活对象,这些存活对象将会被复制到To空间中(复制完成后会进行紧缩),而非活跃对象占用的空间将会被释放。完成复制后,From空间和To空间的角色发生对换。简而言之,在垃圾回收的过程中,就是通过将存活对象在两个semispace之间进行复制。
Scavenge算法(牺牲空间换取时间)的缺点是只能使用内存的一半,但它只复活存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以它的时间效率比较高

执行过程:
a、将From空间中所有能从根对象到达的对象复制到To区(scanPtr和allocationPtr指针,分别指向即将扫描的活跃对象和即将为新对象分配内存的地方)
b、开始循环,查找当前scanPtr所指向的对象的内部指针指向,指向From区,就把这个所指向的对象从From区复制到To区
c、复制完成后将scanPtr所指对象内的指针修改为新复制对象存放的地址,并移动allocationPtr
d、下一轮循环,直到scanPtr和allocationPtr相遇,开始清理垃圾

3、对象晋升
具体移动的标准有两种:

  • 对象从From空间复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一个新生代的清理,如果是,则复制到老生代中,否则复制到To空间中
  • 对象从From空间复制到To空间时,如果To空间已经被使用了超过25%,那么这个对象直接被复制到老生代

4、老生代
V8在老生代中的垃圾回收策略采用Mark-Sweep和Mark-Compact相结合
Mark-Sweep:标记清除分为标记和清除两个阶段。在标记阶段需要遍历堆中的所有对象,并标记那些活着的对象,然后进入清除阶段。在清除阶段总,只清除没有被标记的对象。由于标记清除只清除死亡对象,而死亡对象在老生代中占用的比例很小,所以效率较高
问题:内存碎片

Mark-Compact(标记整理):标记整理正是为了解决标记清除所带来的内存碎片的问题。标记整理在标记清除的基础进行修改。在整理的过程中,将活着的对象向内存区的一段移动,移动完成后直接清理掉边界外的内存。紧缩过程涉及对象的移动,所以效率并不是太好,但是能保证不会生成内存碎片

V8的老生代使用标记清除和标记整理结合的方式,主要采用标记清除算法,如果空间不足以分配从新生代晋升过来的对象时,才使用标记整理

5、优化
Incremental Marking(增量标记)
拆分成很多部分,每做完一部分就停下来,让JS的应用逻辑执行一会,这样垃圾回收与应用逻辑交替完成。经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原来的1/6左右

思维导图

cache

Node.js(三)-异步I/O

发表于 2016-06-04   |   分类于 learning~   |     |   阅读次数

再次阅读《深入浅出Node.js》的笔记系列,内容主要摘自书中

Node 利用单线程,远离多线程死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,以更好的使用CPU。

阻塞I/O的一个特点是调用完成后一定要等到系统内核层面完成所有操作后。调用才结束。阻塞I/O造成CPU等待,浪费等待时间,CPU处理能力得不到充分利用,为了提高性能,内核提供了非阻塞I/O。非阻塞I/O调用之后会立即返回.返回之后,cpu的时间片可以用来处理其他事务。

基于libuv的架构

node提供了libuv作为抽象层,使得所有平台兼容性的判断都由这一层完成,并保证上层的Node与下层的自定义线程池及IOCP之间各自独立,Node在编译期间会判断平台条件。在Node v0.9.3,*nix自行实现了线程池来完成。
libuv

从图中可以看出,对于Network I/O和以File I/O为代表的另一类请求,异步处理的底层支撑机制是完全不一样的。

对于Network I/O相关的请求, 根据OS平台不同,分别使用Linux上的epoll,OSX和BSD类OS上的kqueue,SunOS上的event ports以及Windows上的IOCP机制。

而对于File I/O为代表的请求,则使用thread pool。利用thread pool的方式实现异步请求处理,在各类OS上都能获得很好的支持。

要强调的是,我们时常提到的Node是单线程的,这里的单线程指的是javascript执行在单线程中,在Node中,无论是*nix还是windows平台,内部完成I/O的另有线程池。

1…456…8
JYsdan

JYsdan

唯美食与代码不可辜负~

40 日志
2 分类
28 标签
© 2016 JYsdan
由 Hexo 强力驱动
主题 - NexT.Pisces