NiYingfeng 的博客

记录技术、生活与思考

0%

\前两天 sublime 出了莫名其妙的BUG,只得重新安装,可谓是惨不忍睹啊。一切屌屌的配置都要从新配起。虽说网上一大堆文献,可惜天下文章一大抄,夸张的说,如果某一篇文章出现文章,整个关键词的文章就废掉了。。。一怒之下,自己整理一篇小记,废话不多说,装起先。

package control

一般 sublime 插件都需要基于 package control,所以首先需要先安装 package control。 两种方法: Ctrl + \ 或者 View - Show Console 打开 sublime 的控制台,网上好多同一版本的链接都已经失效,导致无法安装。这边提供最新有效的控制台安装代码。 sublime 3 输入:`

1
import urllib.request,os,hashlib; h = '2915d1851351e5ee549c20394736b442' + '8bc59f460fa1548d1514676163dafc88'; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); urllib.request.install_opener( urllib.request.build_opener( urllib.request.ProxyHandler()) ); by = urllib.request.urlopen( 'http://packagecontrol.io/' + pf.replace(' ', '%20')).read(); dh = hashlib.sha256(by).hexdigest(); print('Error validating download (got %s instead of %s), please try manual install' % (dh, h)) if dh != h else open(os.path.join( ipp, pf), 'wb' ).write(by)

sublime 2 输入:

1
import urllib2,os,hashlib; h = '2915d1851351e5ee549c20394736b442' + '8bc59f460fa1548d1514676163dafc88'; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); os.makedirs( ipp ) if not os.path.exists(ipp) else None; urllibinstall_opener( urllibbuild_opener( urllibProxyHandler()) ); by = urlliburlopen( 'http://packagecontrol.io/' + pf.replace(' ', '%20')).read(); dh = hashlib.sha256(by).hexdigest(); open( os.path.join( ipp, pf), 'wb' ).write(by) if dh == h else None; print('Error validating download (got %s instead of %s), please try manual install' % (dh, h) if dh != h else 'Please restart Sublime Text to finish installation')

若安装不成功,则先下载 https://github.com/lovenyf/file/blob/master/Package%20Control.sublime-package 将文件放置于 Preferences - Browse Packages 上一级目录的 Installed Packages 中,就ok了。 然后是一大波前端实用的 sublime 插件(均可以在 package control 下安装)

  • HTMLBeautify,CSS Format,JS Format 代码规范格式化插件,功能虽小,但是相当实用。
  • Git 如果你们是基于 git 开发环境的,sublime 的这个小玩意也会让你爱不释手,无需切回 shell控制台来执行一些 git 命令了。
  • Git Gutter 非常爽的玩意,能在编辑器内看到diff的内容等
  • Emmet 无需多说,极速编写html与css, Ctrl + e 搞定一切。(http://emmet.io/)
  • SublimeCodeIntel 代码提示插件,支持多种编程语言。(https://github.com/SublimeCodeIntel/SublimeCodeIntel)
  • CssComb 将杂乱无章的 css 规范 近似一类的样式 归结在一起。
  • MarkdownEditing 如果没有使用过Markdown,建议可以去使用使用。 MarkdownEditing是一个Sublime Text的Markdown插件。提供了一种合适的Markdown着色方案(light 与 dark),包含强大的语法高亮,和实用的Markdown编辑功能。支持3种风格:Standard Markdown, GitHub flavored Markdown, MultiMarkdown。
  • SASS Bulid SASS Build 是一个编写CSS的预处理器。这个特别的插件将帮助你妥善构建包括压缩选项在内的SASS文件。一旦你安装了这个插件
  • SideBarEnhancements 侧栏右键菜单增强插件,添加大量实用功能。
  • DocBlockr ( https://github.com/spadgos/sublime-jsdocs ) , 规范代码注释,编写可维护的代码。

DNS 域名系统,作为域名与IP地址相互映射的分布式数据库。通过域名最终得到相对应的 IP 地址的过程称为域名解析。 主机名到IP地址的映射主要的两种方式:

  1. 静态映射,设备上具有自己的映射表来处理查询 IP 的行为。
  2. 动态映射 通过域名解析系统(DNS),在全球特定的 DNS 服务器上来查询 IP 地址映射。

一般来说基本都是结合静态映射与动态映射来获得我们所需的目标域名的 IP 地址。 查询流程

  1. 在浏览器中输入 www.baidu.com 时 ,先会检测本地 hosts 是否有对应的映射,如果有则直接返回此 IP 地址,完成域名解析。
  2. 如果 hosts 中未配置,则查询本地 DNS 解析器缓存。查找映射关系,返回给客户端机,完成解析。
  3. 如未找到,则会从 TCP/IP 中设置的首选 DNS 服务器,也称为本地DNS服务器。如果所查询映射在本地配置区域资源中,则返回给客户端机,并且此解析具有权威性。
  4. 若在本地配置区域资源中未找到相关映射,而 DNS 服务器缓存中具有映射,则返回结果给客户端机,但该结果不具有权威性。
  5. 当以上过程均为查找到所需的 IP 映射,会根据本地 DNS 服务器是否设置转发器的配置进行查询。
  6. 转发模式:此 DNS 服务器会将请求转发到上一级 DNS 服务器,由上一级来进行解析,以此循环,若查询到映射,在以回路的形式返回结果。
  7. 非转发模式,本地 DNS 服务器将全球发至13台根 DNS 服务器,根DNS服务器收到请求后会判断这个域名 .com 是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后,将会联系负责.com域的这台服务器。这台负责 .com 域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址 baidu.com 给本地DNS服务器。当本地DNS服务器收到这个地址后,就会找 baidu.com 域服务器,重复上面的动作,进行查询,直至找到 www.baidu.com 主机。

jQuery 依赖 sizzle 选择器引擎,将 DOM 处理性能优化到了一个很极致的程度,,但 jQuery 还有另一个亮点的处理就是对于异步的处理封装,Deferred,异步队列模块。 异步队列模块最长用到的场景就是 jQuery 封装的 Ajax 模块,不过经常,我们是这么使用的

1
2
3
4
5
6
7
8
```
$.ajax({
url: 'test.php',
data: {test: 123},
success: function(data, status, xhr){},
error: function(xhr, status, error){},
complete: function(xhr, status){}
});
1
2
3

对于经过了 Deferred 包装过的 $.ajax,其实我们更应该这样写:

1
2
3
4
5
6
7
$.ajax({
url: 'test.php',
data: {test: 123}
})
.done(function(data, status, xhr){})
.fail(function(xhr, status, error){})
.complete(function(xhr, status){})
1
2
3
4
5
6
7
8
9
10

Deferred 实现了将异步任务以及其回调函数的解耦,为 Ajax模块,队列模块,ready模块提供底层的基础。 异步队列模块主要为 $.when -> $.Deferred -> $.callbacks,依赖关系。 `$.Callbacks( flags )` 回调函数列表主要用于对于回调函数的添加,删除,触发,锁定以及禁用。jQuery 中的 Ajax 模块和 Deferred 模块功能强而不乱很大程度上也是归功于 Callbacks 模块的经典设计。 flags 参数:

* once: 确保回调列表只会执行一次。
* memory: 保存上一次的触发值,在回调列表添加新的函数时,使用保存的触发值进行触发。
* unique: 确保每一个触发值,只会被回调列表的同一个函数触发一次。
* stopOnFalse: 当一个回调返回false时 停止运行。

以下:

1
2
3
4
5
6
7
8
9
function fn1 (val) {
console.log('fn1 : '+val);
}
function fn2 (val) {
console.log('fn2 : '+val);
}
function fn3 (val) {
console.log('fn3 : '+val);
}
1
2
3

`once:` ![](http://blog.chinaunix.net/attachment/201602/29/26672038_1456754259nLf8.png) `memory:` ![](http://blog.chinaunix.net/attachment/201602/29/26672038_1456754280EOuw.png) `unique:` ![](http://blog.chinaunix.net/attachment/201602/29/26672038_14567543041rKg.png) `stopOnFalse:` ![](http://blog.chinaunix.net/attachment/201602/29/26672038_1456754329tJXK.png) 将回调函数模块处理分割成 4 种状态这种思想,可以看到各个状态是互不排斥,并且可以随意搭配来生成一个回调函数列表,使其具有一种或者多种的状态值: 所依赖的底层功能函数的灵巧性,也造就了顶层代码功能的强大。其源码结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
jQuery.Callbacks = function( options ) {
var options = createOptions(options);
var memory,
// 是否已经 fire 过
fired,
// 当前是否还处于 firing 过程中
firing,
// fire 调用的起始下标
firingStart,
// 需要 fire 调用的队列长度
firingLength,
// 当前正在 firing 的回调在队列的索引
firingIndex,
// 回调队列
list = [],
// 如果不是 once 的,stack 实际上是一个队列,用来保存嵌套事件 fire 所需的上下文跟参数
stack = !options.once && [],
_fire = function( data ) {
};

var self = {
add : function(){},
remove : function(){},
has : function(){},
empty : function(){},
fireWith : function(context, args){
_fire([context, args]);
};
fire : function(args){
this.fireWith(this, args);
}
/* 其他方法 */
}
return self;
};
1
2
3
4
5
6
7
8
9

其主要原理就是使用闭包特性,缓存各类状态值,在 add 方法中加入大量的逻辑判断实现。 再来简单说说 promise ,一个 promise 一般来说是处于三个互斥状态中的一个:

* pending:进行中。
* fulfilled:通过状态。
* rejected:拒绝状态。

jQuery 的 Deferred 就是一个基于 Callbacks 的实现遵循 Common Promise/A 规范的一个功能。是的 jQuery 中许多功能方法得以实现异步的任务与回调函数的解耦。具体的,Deferred 有什么作用? 比如说某一个情况,我需要在某个异步行为完成之后,执行特定的回调函数,那么基本会这么写

1
2
3
4
5
6
7
8
function asyn( callback ){
setTimeout(function(){
//....... // 执行的逻辑代码
callback('123'); // 异步回调函数
}, 100);
}

asyn( function(val){ alert(val) } );
1
2
3

就是说,我必须在异步函数调用的时候,就要写好,传入回调函数,导致了上面所执行的逻辑代码必须和异步回调函数放在一起,导致了一定的耦合性,当遇到回调中嵌回调的时候,就可能出现嵌套地狱的情况。

1
2
3
4
5
6
7
8
9
10
function asyn(){
var dtd = $.Deferred();
setTimeout(function(){
//....... // 执行的逻辑代码
dtd.resolve('123'); // 异步回调函数
}, 100);
return dtd;
}
var dtd = asyn();
dtd.done(function(val){ alert(val) });
1
2
3

其源码则是基于 Callbacks 的演化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Deferred: function( func ) {
var tuples = [
// action, add listener, listener list, final state
[ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
[ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
[ "notify", "progress", jQuery.Callbacks("memory") ]
],
state = "pending",
promise = {
state: function() {
},
always: function() {
},
then: function( /* fnDone, fnFail, fnProgress */ ) {
},
promise: function( obj ) {
}
},
deferred = {};

promise.pipe = promise.then;
...
return deferred;
}
1
2
3

Deferred 的 done 和 fail 传递回调函数分别对应在 resolve 和 reject 调用之后触发,或者在resolve 和 reject 调用之后,done fail 绑定回调函数的时候触发。当然 Deferred 还包括 then,promise,always等功能方法。 所以说,当无穷的嵌套地狱困扰回调函数的时候,不妨试试 promise 的解耦回调。 当然还有个不得不说的 $.when,当回调依赖回调形成嵌套回调的时候,可以使用 Deferred 很好的来解决,当回调依赖于多块异步逻辑代码的时候,经过 Deferred 封装的 when 就展现光彩了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var d1 = $.Deferred();
var d2 = $.Deferred();

$.when( d1, d2 ).done(function ( v1, v2 ) {
console.log( v1 ); // "Fish"
console.log( v2 ); // "Pizza"
});
setTimeout(function(){
dresolve( "Fish" );
}, 1000);

setTimeout(function(){
dresolve( "dog" );
}, 3000);

```

很对情况就是像,当我们展现某块数据,而数据依赖多个数据接口的时候,非常有用。

目前我们关于实现动画的方式,无非就是类似 jQuery的动画, CSS 的 animation,transition,tranform 或者 requestAnimationFrame 以及触及GPU的硬件加速动画。 jQuery的动画不用说,就普通的 CSS 动画而言使用浏览器自身的渲染引擎来处理动画的渲染,而触发 CSS 硬件加速则是通过 GPU 来提升 CSS 动画的性能,将有关于动画的运算从 CPU 移植到 GPU 上,大大提高渲染动画的效率。GPU 在对于渲染图形方便的优势,所以能在渲染动画上更容易达到每秒60帧的顺滑效果,使得视觉上顺畅无比。 简单来讲,当一个页面初次加载完毕的时候,浏览器的渲染引擎便开始了它的工作:

  • 浏览器解析HTML, 构建一棵 DOM 树,其每个节点对应页面的每个元素。
  • 渲染引擎对 DOM树,以浏览器解析后的 CSS ,会对应构建出一棵 Render 树,其每个节点 RenderObject包含了所对应 DOM 的最终样式信息。
  • 基于 Render 树,渲染引擎会将其中的某些特定节点同时创建新的 RenderLayer,从而还会生成一棵 RenderLayer 树。
  • 当 Render 树和 RenderLayer 树构建完毕之后,便依据两者,遍历各个节点开始渲染绘制出页面。

再简单介绍下 chrome 的 timeline 的几个名词:

  • Timer Fire: 定时器触发,很简单,比如 jquery 的动画均是通过 setTimeout 来处理生产动画效果(一般性的动画),所以每次展现完毕后都会触发下一个定时器事件 JS 的过程。
  • Recalculate Style:将 CSS 构建与 DOM 对应的的树,一般称之为 Render 树的过程。
  • Layout:依据 Render 树,以及 RenderLayer 树进行页面布局的过程。
  • Print:更具布局,绘制出页面的过程
  • Composite Layers:渲染展现到屏幕的过程

最古老的动画形式: HTML:

1
<div class="square"></div>

CSS:

1
2
3
4
5
6
7
8
9
.square{
position:relative;
top:50px;
left: 50px;
margin-bottom: 10px;
height:80px;
width:80px;
background-color:#999;
}

JS:

1
2
3
4
5
6
7
8
9
10
$('.jquery').click(function(){
$('.square').animate({
height: 300,
width: 300,
left:200,
top:200,
opacity: 5},
3000, 'linear', function() {
});
});

以上是一个很简单 jq 动画效果,3秒钟从原有图形样式转变到指定的图形样式,但是,浏览器所处理的却是一段 jq 分割的复杂的过程, 每一次浏览器都要处理包括 Timer Fire,Recalculate Style,Layout,Paint,Composite Layers,一次又一次的不断重复又循环,内存占用率也是随之增大。 如今的时代,css的动画帮我们解决了很多难题, 普通的2D CSS动画: CSS:

1
2
3
4
5
6
7
8
.css2d{
width:300px;
height:300px;
left:200px;
top:200px;
transition: width 3s linear, height 3s linear,top 3s linear,left 3s linear;
-webkit-transition: width 3s linear, height 3s linear,top 3s linear,left 3s linear,;
}

JS:

1
2
3
$('.css-2d').click(function(){
$('.square').addClass('css2d');
});

直接上图,计算样式以及重新布局均有浏览器自身完成,并且少了定时器触发事件这一块,使得动画运行方式更为自然, 还有内存图: 或许感觉还不错了,但是由于涉及到了一些或导致重排的属性,使得动画的时候一直重排重绘,那么来看看硬件加速的样式动画吧, 吊炸天的硬件加速CSS动画: CSS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.hardware{
-webkit-animation: hardware 2s infinite linear;
animation: hardware 2s infinite linear;
}

@-webkit-keyframes hardware {
0% {
transform: translate3d(0,0,0) scale3d(1,1,1);
-webkit-transform: translate3d(0,0,0) scale3d(1,1,1);
}


50% {
transform: translate3d(150px,150px,0) scale3d(3,3,1);
-webkit-transform: translate3d(150px,150px,0) scale3d(3,3,1);
}
}


@keyframes hardware {
0% {
transform: translate3d(0,0,0) scale3d(1,1,1);
-webkit-transform: translate3d(0,0,0) scale3d(1,1,1);
}


50% {
transform: translate3d(150px,150px,0) scale3d(3,3,1);
-webkit-transform: translate3d(150px,150px,0) scale3d(3,3,1);
}
}

JS:

1
2
3
$('.css-hardware').click(function(){
$('.square').addClass('hardware');
});

对于硬件加速的动画,浏览器渲染的过程: 屌不屌其实都一样啦,只不过没有使用处理元素的一些,如height, width,left,top 等元素值的动画,而使用了一些3D CSS属性达到了我们所需的动画效果,但是浏览器处理动画却已经完全不一样。动画过程优化到了仅剩Recalculate Style,Composite Layers 两个过程。 这边为什么会这么吊呢,这就全依仗与 对应所生成的第三种树 RenderLayer 树,该元素节点被渲染引擎根据 CSS 创建了对应的 RenderLayer 树节点,并将其绑定到 GPU,当该 RenderLayer 的 transform 等变化时候,跳过浏览器的 Layout,以及 Print,直接有 GPU 对其做变换。这个也是直接提升动画性能,内存突然降低(计算被移植到了GPU)的原因。 那么如何来处理 CSS,将我们所需要进行动画的元素构建时拥有新建的 RenderLayer 节点呢? 目前查询到的资料来看,chrome 以下:

  • 3D或透视变换(perspective transform)CSS属性
  • 使用加速视频解码的\
  • 拥有3D(WebGL)上下文或加速的2D上下文的\节点
  • 混合插件(如Flash)
  • 对自己的opacity做CSS动画或使用一个动画webkit变换的元素
  • 拥有加速CSS过滤器的元素
  • 元素有一个包含复合层的后代节点(一个元素拥有一个子元素,该子元素在自己的层里)
  • 元素有一个z-index较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)

一般我们都会尝试对需要进行动画的元素添加一个 3D 的属性从而来触发其硬件加速的动画效果,现在又有一个专门用作触发生成新 RenderLayer 的属性值 will-change。 当然以上动画最优情况只正对于部分 GPU 加速处理的 CSS 属性,对于涉及到重排 relayout 和 重绘 repaint 的属性我们可能需要慎重考虑以及优化了。 重排:重排是页面结构调整,获取或者设置元素某些属性值时,触发了浏览器 Layout 过程重新定义的情况,比如调整元素的几何大小,位置,获取宽高等。重排必然导致重绘,浏览器渲染总会依照 Layout,Paint,Composite Layers 的流程来处理。下面是触发重排的一些主要情况:

  • 脚本操作DOM,增加删除可视的DOM节点
  • 操作 class 属性,或者设置 style 属性
  • 计算获取当前 offsetTop/Left/Width/Height, scrollTop/Left/Width/Height, clientTop/Left/Width/Height, width, height 等属性值
  • 内容变化, 窗口调整等

触发重排的属性: 盒子模型的相关属性:width, height, padding, margin, display, border 定位特性的相关属性:top, bottom, left, right, position, float, clear 内容的相关属性:text-align, overflow, font-family, line-height,font-size 重绘:重排必然导致重绘,并且对于仅调整各类属性色值等时,浏览器重新绘制输出到浏览器的情况。 触发重排的属性:color, border-style, visibility, background, outline, box-shadow 等 一些动画的重排重绘我们不可避免,我们要做的并不是禁止它,存在即合理,事实只有我们滥用。以下是一些简单的优化重排,重绘的方法:

  • 将多次需要频繁改变的样式一次性完成,不要频繁的获取计算样式。
  • 将大量的操作 DOM 操作放置到页面之外进行。
  • 尽可能在最末端需要调整的 DOM 上改变 class,减小对其它 DOM 的影响。
  • 动画尽量应用到 position 为 absolute 或者 fixed 的元素上,以便当前元素的动画造成大规模的重排。

之前一直针对于PC端开发,自适应方面接触甚少,一般来说用些的 css 方法就直接解决一些设计问题,直到目前现状开始转变为纯移动端开发,于是乎开始慢慢接触移动端的新特性的实战,今天主要做一下 flexbox 的笔记。 Flexbox 布局模块提供了我们一个在未知或者动态容器中自适应出我们所希望的布局形式。垂直居中,重新排序,布局动态的伸展以及收缩等。目前在移动端大体来说兼容性还不错 can I use(Flex)。 Flexbox 实质来说并不能仅仅作为一种属性,而是作为一类模块,包括作为父元素的伸缩容器以及子元素的伸缩元素。伸缩元素会依据伸缩容器具体变化,类似于在一些情况下我们盒大小使用百分比情况类似,不过使用百分比总会有那么一些时候需要我们使用不太规范的方式来处理某些特定的情况情况。 前提是容器的宽高已定: 对于上面很常见的布局逻辑,一般来说会简单的使用下面形式的 html ```

``` 配合以下的 css ``` .container{ width: 300px; height: 100px; margin: 50px; } .item{ float: left; width: 33.33%; height: 100%; } .item:nth-child(1){ background-color: #333; } .item:nth-child(2){ background-color: #666; } .item:nth-child(3){ background-color: #999; } ``` 但是 我们更多的是会遇到这种形式 那么 好吧 借助于一些通用的 css3 的伪类选择器 还是可以写的稍微好看一点,不过 html 也需要稍微恶心的包上一层,当然也有好多简单的 hack写法 ```

``` css 如下 ``` .container1{ width: 300px; height: 100px; margin: 50px; border: 1px solid #000; } .item1{ float: left; width: 33.33%; height: 100%; } .innerItem1{ height: 90px; margin: 5px 5px 5px 0; background-color: #CCC; } .item1:nth-child(1) .innerItem1{ margin-left: 5px; } ``` 好的,flexbox 闪亮登场 如何使用例1中的 html 结构 优美的写出例2的布局呢? ```

``` 首先,对于 flexbox 我们需要创建一个 flex 容器,设置其 display 属性 ``` .container{ width: 300px; height: 100px; margin: 50px; border: 1px solid #000; display:-webkit-flex; display:-ms-flexbox; display:flex; } ``` 在对其子元素设置 flex: ``` .item{ flex:1; background-color: #999; margin: 5px 5px 5px 0; } .item:nth-child(1){ margin-left: 5px; } ``` 使得我们既不需要多放置一层div,也无需来处理写死内部伸缩元素的高度,最主要的却是内部元素的宽高已经完全依赖自适应于容器元素。将容器元素宽高随意配置均可使其子元素得到自适应(之前多嵌套一层的 高度上依旧不是很好处理)。 然后,处理高度的来了,先来个垂直居中吧。 好吧,一般形式的已经不想写了,直接看下 flexbox 的形式 ``` //html

// css .container-h{ height: 300px; width: 100px; margin: 50px; border: 1px solid #000; display:-webkit-flex; display:-ms-flexbox; display:flex; /垂直居中/ -webkit-align-items: center; align-items: center; } .item-h{ flex:1; height:100px; background-color: #999; margin: 5px; } ``` 终于写个垂直居中,我可以不用量 line-height,不用设置margin,也不用去攀比各种css的奇思妙想了。突然感觉世界的起跑线都一样了。 好了,先来熟悉下了解 felxbox 之前的一些简单概念: * 伸缩容器:一个设置了 display 为 flex 或者 inline-flex 的外层元素 * 伸缩元素:在伸缩容器中的子元素 * 主轴:延伸缩容器配置伸缩元素的方向 * 侧轴:主轴的垂直线 #### 伸缩容器属性简单介绍: > flex-direction : row | row-reverse | column | column-reverse; > 在伸缩容器上定义用来定义主轴方向。 row:为默认方向,排版方向 row-reverse:与 row 反方向 column:从上到下排版 column-reverse:与 column 排版方向相反 > flex-wrap : nowrap | wrap | wrap-reverse; > 在伸缩容器上用来定义容器内伸缩元素是单行还是多行显示。(各个伸缩元素设置最小高宽,在每个元素最小宽度下伸缩容器无法一行内放置的时候所做的处理) nowrap:默认值,伸缩容器为单行显示,当容器太小时,内部元素会跑出 wrap:内部元素会多行显示,伸缩容器一行内放置不下的元素会被放置到下一行(与排版方向有关) wrap-reverse:内部元素会多行显示,伸缩容器一行内放置不下的元素会被往上位置放置 flex-flow : || 为上面两种属性的缩写版。 > justify-content : flex-start | flex-end | center | space-between | space-around; > 为可伸缩模型的重头戏,也是真正优雅实现各种居中的css3属性。 flex-start:伸缩元素向起始位置靠齐 flex-end:伸缩元素向结束位置靠齐 center:伸缩元素居中 space-between:均匀分布,第一个向起始位置靠齐,最后一个向结束位置靠齐 space-around:伸缩元素平均分布在行里,首尾保留一半空间 > align-items:flex-start | flex-end | center | baseline | stretch; > 用来定义伸缩容器内部的伸缩项目在侧轴上的对齐方式 flex-start:伸缩元素向侧轴起始位子靠拢 flex-end:伸缩元素向侧轴结束位子靠拢 center: 伸缩元素居中对齐 baseline:侧轴方向根据基线对齐 stretch: 为默认值,会更具伸缩的状态尽可能填充伸缩容器。 #### 项目的属性: order: 标示伸缩项目在容器中的排序先后。 flex : none | [ ? || ] **flex-grow : ** 伸缩元素的无单位的伸缩比例 **flex-shrink : ** 根据弹性盒子元素所设置的收缩因子来作比例收缩空间。 **flex-basis : ** 根据弹性盒子元素所设置的收缩因子来作比例收缩剩余的空间。

原文地址: http://www.2ality.com/2014/10/es6-promises-api.html
原文作者:Dr. Axel Rauschmayer

译者:倪颖峰

博文下半部分为纯干活内容,包括介绍的 ES6 中 Promise API,以及其简单的实现方式与思想,以及一些拓展内容。

9. 速查表:ES6的 promise API

这边简单的给出一下 ES6 promise 的API 简述,详细描述请看[文档]

9.1. 术语

promise 的 API 就是关于异步获取结果。一个 promise 对象是一个独立的对象,但会通过该对象传递结果。

状态:

  • 一个promise 一般来说会处于以下三种互斥状态中的一种:
    • 结果未完成时,promise 状态为 pending。
    • 结果通过时,promise 状态为 fulfilled。
    • 当错误发生时,promise 状态变为 rejected。
  • 一个promise 一旦被设置则为“运行完成”(包括 fulfilled 或者 rejected)。
  • 一个peomise 一旦被设置之后便不能在改变。

状态变化反应:

Promise reastions 就是你是用 promise 方法 then 来注册的回调函数,用来监听状态 fulfillment 和 rejected。

一个 then式即为一个含有 promise 风格方法 then 的对象。即使该方法仅仅只是监听状态的,依然为 then 式。

状态改变:

有两种改变 promise 状态的方式。一旦你调用了其中的一个,那么进一步调用将会无效。

  • 拒绝一个 promise 表示这个promise 被设为 rejected。
  • 通过一个 promise 有两种情况,取决于以什么值来解决的:
    • 使用一个普通值(非 then式)来解决 promise。
    • 解决 promise P 以一个 then式的 T,意味着P 不再可以设置 resolved,而将会随着 T的状态走,即其 fulfillment 和 rejection 值。P 的回调会在T 被设置状态后合适的时候触发(或者如果T 已经被设置状态了,那么就会立即触发)。

9.2. 构造函数

promise 的构造函数为以下形式:

1
var p = new Promise(executor(resolve, reject));

上面创建了一个行为由回调函数 exector 决定的 promise。使用参数来处理解决或者拒绝 p:

  • resolve(x),以 x 来解决 p:
    • 如果 x 是 then式的,那么它的结果会转到 p(包括通过 then() 注册的触发反馈)。
    • 否者,p 就以 x 处理 fulfilled 状态。
  • reject(x),以 e 值来拒绝 p(通常为一个 Error 的子类)。

9.3. 静态方法

所有的 Promise 的静态方法都支持实例化:通过接收器来创建一个实例(像:new this),并且通过它来访问这些静态方法(this.resolve 与 Promise.resolve)。

创建 promises: 下面两种方式来创建接收器的实例。

  • Promise.resolve(x):
    • 如果 x 是 then式的,它将转化为一个 promise(接收器的实例)。
    • 如果 x 是一个 promise,返回将没有变化。
    • 否者将会返回一个接收器的实例并且以 x 来处理 fulfilled 状态。
  • Promise.reject(reason):创建一个新的promise,并且以 reason 值来处理拒绝状态。

组合 promises: 直观来说,静态方法 Promise.all() 和 Promise.race() 组合迭代的 promises 变为一个单一的 promise。即:

  • 它们采用迭代。迭代的元素通过 this.resolve() 来转化为 promises。
  • 它们返回一个新的 promise。这个 promise 返回一个新的接收器的实例。

方式有:

  • Promise.all(iterable):如此返回一个 promise
    • 如果迭代的元素均为 fulfilled 那么将其设置为 fulfilled。成功值:各个成功值生成的数组。
    • 当元素中有任何一个失败时便设置其为 rejected。拒绝值:第一个解决状态的值。
  • Promise.race(iterable):迭代的第一个被设置完毕的元素被用来解决返回的 promise。

9.4. 继承原型方法

Promise.prototype.then(onFulfilled, onRejected);

  • 回调函数 onFulfilled 和 onRejected 被车称为 reactions。
  • 如果该 promise 已经被设为 fulfilled,或者一旦变成 fulfilled 状态,那么回调函数 onFulfilled 就会被立即执行。相同,被触发之后 onRejected 也一样。
  • then() 返回一个新的 promise Q(通过接收器的构造函数创建):
    • 任意 reactions 返回一个结果,Q 便使用其来通过。
    • 任意 reactions 抛出一个错误,Q 便使用其来拒绝。
  • 触发 reactions:
    • 如果 onFulfilled 被触发了,那么一个通过状态的接收器就会被转发到 then() 的结果中。
    • 如果 onRejected 被触发了,那么一个拒绝状态的接收器就会被转发到 then() 的结果中。

触发 reactions 的默认值可以以以下方式执行:

1
2
3
4
5
6
function defaultOnFulfilled(x){
return x;
}
function defaultOnRejected(e){
throw e;
}

Promise.prototype.catch(onRejected): 与 then(null, onRejected) 一样。

10. promises 的优势与劣势

10.1. 优势

统一的异步 APIs
promises 的一个重要的优点就是被越来越多的浏览器的异步 API 使用,统一了当前多样化和不兼容的各种模式和约定。看一下即将出现的两种基于 promise 的API。

基于 promise 的 ferch API 替代 XMLHttpRequest:

1
2
3
fetch(url)
.then(request => request.text())
.then(str => ...)

fetch() 会对实际的请求返回一个 promise,text() 会对内容作为字符串返回一个 promise。

ECMAScript 6 API 编程式导入模块也是基于 promise:

1
2
3
4
System.import('some_modle.js')
.then(some_module => {
...
})

promises VS events

对比 events,promises 更适合做一次性结果的监听。不管你是在得到结果之前或者之后注册都不会影响你监听到结果。这是 promises 更本上的优势。另一方面,你不能使用它来处理一些反复性的事件。链式是 promise 的另一个优势,但是每次只能添加一个。

promises VS callbacks

对比回调函数,promises 具有更简洁的函数(方法)参数。在 callbacks 中,参数包含输入输出:

1
fs.readFile(name, opts?, function(err, data))

而 promise 中,所有的参数均只是输入:

1
2
readFilePromisified(name, opts?)
.then(dataHandler, errorHandler)

此外,promises 的优势还包括更好的错误处理机制(异常的集成)和组合更任意更容易(因为你可以使用一些同步的工具,比如 Array.prototype.map())。

10.2. 劣势

Promises 对于单一的异步结果能处理的很好。但是它不擅长于:

ECMAScript 6 的 promises 缺少一下的两个基本功能点:

  • 你无法取消。
  • 你无法确定他们要在多久之后发生(例如在客户端用户界面展现进度条)。

Q promises 库支持后者,并且也将计划添加到 Promises/A+。

11. promises 与 生成器

由于一个实用功能 Q.spawn(),你可以通过生成器来实现在浅协程里使用基于 promises 的函数。这是一份重要的优点使得代码看起来类似于同步,并且可以使用一些同步机制,比如说 try-catch:

1
2
3
4
5
6
7
8
9
10
11
12
Q.spawn(function* () {
try {
let [foo, bar] = yield Promise.all([ // (A)
httpGet('foo.json'),
httpGet('bar.json')
]);
render(foo);
render(bar);
} catch (e){
console.log('Read failed: '+ e );
}
});

Q.spawn() 的参数是生成器函数。如果 yield 标识符是有效的,那么将发生下面的事情:

  • 函数执行将被暂停。
  • 操作符 yield 被函数返回(返回,描述的不确切,暂时忽略)。
  • 随后函数会被恢复成一个值或者异常。前者情况继续执行之前暂停的那个点,yield 返回一个值。后者情况表达式被抛入函数中,类似 yield 操作符从内部被扔入。

从而,很清楚的明白 Q.spawn() 所做的事情:当生成器函数产生一个 promise,spawn 便注册 reaction,等待处理。如果 promise 通过状态,生成器便会恢复一个结果。如果 promise 为拒绝,一个异常便会抛入注册器中。

这是一个通过新的语法结构-异步函数。来添加支持 JS spawn 的提案(https://github.com/lukehoban/ecmascript-asyncawait)。之前例子作为异步函数应该如下。底层,并没有较多不同 - 异步函数基于生成器。

1
2
3
4
5
6
7
8
9
10
11
12
asyc function(){
try {
let [foo, bar] = await Promise.all([
httpGet('foo.json'),
httpGet('bar.json')
]);
render(foo);
render(bar);
} catch (e){
console.log('Read failed: '+ e );
}
}

12. 调试 promises

主要的挑战是调试异步代码中包含异步函数和方法的调用。异步调用源自于一个任务执行了一个新的任务。如果该新任务中有异常,堆栈跟踪只会覆盖该任务,不会包含之前任务的信息。所以在异步编程中你只能获得很少的调试信息。

Google 的 chrome 最近开始可以调试异步代码。并不完全支持 promises,但是处理一般的异步编程做得非常不错。比如下面的实例,first 异步调用 返回调用 third 的 second。

1
2
3
4
5
6
7
8
9
10
function first(){
setTimeout(function () { second('a') }, 0); //(A)
}
function second(){
setTimeout(function () { third('b') }, 0); //(B)
}
function third(){
debugger;
}
first();

正如截图中所示,调试器展现一个包含三个函数的跟踪堆栈。它包含了 line A 和 line B 处的异步函数。

13. promises 内部窥秘

本段,我们将从不同的角度来看一下 promises:不再学习如何使用 promise API,我们将了解一下它的简单实现。这个视角将帮助我们实现一个 promises。

promises 的实现被称为 DemoPromise 和 在 GitHub 上可用的实例(https://github.com/rauschma/demo_promise)。为了更容易理解,将不会完全匹配 API。但是它将足够使你了解在实现 promises 过程会中面对的各种难题。

DemoPromises 是一个含有三个原型方法的构造函数:

  • DemoPromises.prototype.resolve(value)
  • DemoPromises.prototype.reject(reason)
  • DemoPromises.prototype.then(onFulfilled, onRejected)

在此,resolve 和 reject 为方法(而不是传递给传递给构造函数作为参数的回调函数)。

13.1 一个独立的 promise

首先我们需要实现一个最少功能的独立的 promise :

  • 你可以创建一个 promise。
  • 你可以通过或者拒绝一个 promise 并且只能处理它一次。
  • 你可以通过 then() 来注册 reactions(回调函数)。该方法不支持链式(该处应该是支持链式),就是说,其不返回任何东西。它需要被立即执行不管该 promise 是否已经被处理。

以下是使用第一步所实现的:

1
2
3
4
5
var dp = new DemoPromise();
dp.resolve('abc');
dp.then(function(value){
console.log(value); // abc
});

下面图标说明了我们第一个 DemoPromise 是如何实现的:

让我们首先研究下 then()。它必须处理两个情况:

  • 如果 promise 还在执行中,那么将在 promise 被处理完毕之后调用 onFulfilled 或者 onRejected 的时候使用。
  • 如果 promise 已经被通过或者拒绝,onFulfilled 或者 onRejected 将被立即调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DemoPromise.prototype.then = function (onFulfilled, onRejected) {
var self = this;
var fulfilledTask = function () {
onFulfilled(self.promiseResult);
};
var rejectedTask = function () {
onRejected(self.promiseResult);
};
switch (this.promiseState) {
case 'pending':
this.fulfillReactions.push(fulfilledTask);
this.rejectReactions.push(rejectedTask);
break;
case 'fulfilled':
addToTaskQueue(fulfilledTask);
break;
case 'rejected':
addToTaskQueue(rejectedTask);
break;
}
};
function addToTaskQueue(task) {
setTimeout(task, 0);
}

resolve() 工作原理如下:如果 promise 已经被处理完毕,将不做任何事情(确保一个 promise 只被处理一次)。否则,promise 的状态转变为 fulfilled 并且结果缓存到 this.promiseResult。所有入队到通过状态 reactions 的将被立即执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
Promise.prototype.resolve = function (value) {
// 为执行回调函数方法 如果不为状态 pending 为在 then 时刻已经处理执行过,立即跳出
if (this.promiseState !== 'pending') return;
this.promiseState = 'fulfilled';
this.promiseResult = value;
this._clearAndEnqueueReactions(this.fulfillReactions);
return this; // 链式
};
Promise.prototype._clearAndEnqueueReactions = function (reactions) {
this.fulfillReactions = undefined;
this.rejectReactions = undefined;
reactions.map(addToTaskQueue);
};

reject() 与 resolve() 类似。

13.2 链式(注意,这部分已经为下一步扁平化做好了基础)

接下来,我们将实现链式:

  • then() 返回的 promise 实际上是 onFulfilled 或者 onRejected 被执行后所返回的。
  • 如果 onFulfilled 或者 onRejectecd 缺失,那么不管它们所传递的是什么, promise 通过 then() 返回出来。

显然,只需要调整下 then():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
DemoPromise.prototype.then = function (onFulfilled, onRejected) {
var returnValue = new DemoPromise(); // (A)
var self = this;

var fulfilledTask;
if (typeof onFulfilled === 'function') {
fulfilledTask = function () {
var r = onFulfilled(self.promiseResult);
returnValue.resolve(r); // (B)
};
} else {
fulfilledTask = function () {
returnValue.resolve(self.promiseResult); // (C)
};
}

var rejectedTask;
if (typeof onRejected === 'function') {
rejectedTask = function () {
var r = onRejected(self.promiseResult);
returnValue.resolve(r); // (D)
};
} else {
rejectedTask = function () {
// Important: we must reject here!
// Normally, result of `onRejected` is used to resolve
returnValue.reject(self.promiseResult); // (E)
};
}
...
return returnValue; // (F)
};

then() 创建并返回一个新的 promises(在 A 处和 F 处)。此外,fulfilledTask 和 rejectedTask 以不同方式设置:在处理结束时。

  • onFulfilled 的结果被用在通过 returnValue(在 B 处)。如果 onFulfilled 为缺失的,我们使用通过的值来处理 returnValue(在 C 处)。
  • onRejected 的结果被用于通过(并非拒绝) returnValue(在 D 处)。如果,onRejected 缺失,我们使用拒绝的值来拒绝 returnValue(在 E 处)。

13.3 扁平化

扁平化思想主要是使链式方式更为便捷:一般,将一个 reaction 返回的值传递到下一个 then()。如果我们返回一个 promise,不包裹的形式是最好的,像下面的例子:

1
2
3
4
5
6
7
8
asyncFunc1()
.then(function (value1){
return asyncFunc2(); // A
})
.then(function (value2){
// value2 为 asyncFunc2() promise 的通过状态。
console.log(value2);
});

我们在 A 处返回一个promise,就没有必要在当前方法中嵌套 then() 的调用,我们可以对方法的结果进行 then() 的调用。那么,不嵌套 then(),就保持扁平化。

我们通过使 resolve() 方法扁平化来实现整体扁平化:

  • 以一个 promise Q 通过一个 promise P 意味着 Q 的处理结果要在 P 的 reactions 之前。
  • 在 Q 中,P 应该被锁定:它不能被通过(或者拒绝)。并且它的状态和结果和 Q 的保持同步。

如果我们使用 then式 (而非一个 promise)能以更通用的形式实现扁平化。

实现 锁住,我们需要使用一个布尔值标签 this.already 。一旦值为 true,this 将被锁住,不能在被通过,记录如果 this 仍在进行中,因为它的状态是和锁住的 promise 是一致的。

1
2
3
4
5
6
DemoPromise.prototype.resolve = function (value) {
if (this.alreadyResolved) return;
this.alreadyResolved = true;
this._doResolve(value);
return this; // enable chaining
};

真实的解决代码在私有方法 _doResolve() 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DemoPromise.prototype._doResolve = function (value) {
var self = this;
// Is `value` a thenable?
if (value !== null && typeof value === 'object'
&& 'then' in value) {
addToTaskQueue(function () { // (A)
value.then(
function onFulfilled(value) {
self._doResolve(value);
},
function onRejected(reason) {
self._doReject(reason);
});
});
} else {
this.promiseState = 'fulfilled';
this.promiseResult = value;
this._clearAndEnqueueReactions(this.fulfillReactions);
}
};

扁平化处理在 A 处进行:如果 value 为通过状态,那么当前 self 为通过状态,如果 value 为拒绝的,我们就希望 self 为拒绝状态。前面所述通过私有方法 _doResolve 和 _doReject 来实现,通过 alreadyResolved 来绕开阻挡。

13.4. 更详细的 Promise 状态

由于链式形式,promises 的状态变得更为复杂。

如果你只是使用 promises,你只需要以简单的视角来看待,忽略锁定就好。最主要的状态相关概念是“settledness”: 一个 promise 在被通过或者拒绝的时候被设置完毕。在一个 promise 被设置之后,将不再变化(状态为通过状态或者拒绝状态)。

如果你希望先处理 promsie,然后再将其通过,这便会很难理解:

  • 直观的说,‘resolved’(已处理)意味着不能再次被(直接)的处理。一个 promise 在只在即未被设置过也不在锁定的状态下被解决。引用下规范:一个未被处理的 promise 一般处在 pending 状态。一个已经处理的 promise 可能为 等待,通过或者拒绝状态。
  • 处理中不一定会使其被设置:你可以通过一个 promise 以另一个状态为 pending的。
  • 处理中现在包含拒绝:你可以拒绝一个 promise 通过一个拒绝的 promise 处理。

13.5. 异常

在我们不久的将来,我们将会在 user code 中以 rejections 形式处理异常。目前来说,user code 只代表 then 中的两个回调函数参数。

下面代码片段展现了我们在 onFulfilled 内部处理异常转到 拒绝状态 - 通过 A 处调用时的 try-catch 包裹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var fulfilledTask;
if (typeof onFulfilled === 'function') {
fulfilledTask = function () {
try {
var r = onFulfilled(self.promiseResult); // (A)
returnValue.resolve(r);
} catch (e) {
returnValue.reject(e);
}
};
} else {
fulfilledTask = function () {
returnValue.resolve(self.promiseResult);
};
}

13.6. 揭示构造函数模式

如果我们希望将 DemoPromise 转变为切实可用的 promise,我们还需要实现揭示构造函数模式:ES6 promises 不是通过方法来进行通过或者拒绝,而是通过监听在 executor 上的函数实现的,构造函数的参数。

如果执行函数抛出一个异常,该 promise 一定被拒绝。

14. 两个常用的 promise 附加方法

本段介绍一下两个在 ES6 中添加到 promsie 的方法。promise 库的大部分都支持他们。

14.1. done()

当你将数个 promise 方法链式调用时,你可能会无心之中忽略错误。看实例:

1
2
3
4
5
6
function doSomeThing(){
asyncFunc()
.then(f1)
.catch(r1)
.then(f2); // A
}

如果在 A 处的 then() 生成拒绝状态,那么将不会被处理。 promise 库 Q 提供了一个方法 done(),放置在链式最后一个方法调用的后面。其或者替换最后一个 then()(也有一个到两个参数):

1
2
3
4
5
6
function doSomeThing(){
asyncFunc()
.then(f1)
.catch(r1)
.done(f2);
}

又或者仅仅是插到最后一个 then() 之后(无参数):

1
2
3
4
5
6
7
function doSomeThing(){
asyncFunc()
.then(f1)
.catch(r1)
.then(f2)
.done();
}

引用 Q 的文档:

done 与 then 使用的黄金规则:既不返回任何一个 promise,或者结束链式调用,那么使用 done 来终止。使用 catch 来终止不是很好,因为 catch 的可能是其自己内部的错误。

这也是在 ES6 中实现 done 的方式:

1
2
3
4
5
6
Promise.prototype.done = function(onFulfilled, onRejected){
this.then(onFulfilled, onRejected)
.catch(function(reason){
setTimeout( ()=>{ throw reason }, 0 );
});
};

虽然 done 功能极为有用,但是没有添加到 ES6 中,因为在将来这种检测可以被调试器自动调起(一个 ES 讨论版本中)。

14.2. finally()

有时你希望某个行为不管有没有错误抛出都立即执行。例如在一系列行为后的处理。这便是 promise 方法 finally() 所做的,极为类似与错误处理机制中的 finally 。其回调方法不接受参数,但是会有 通过状态或者拒绝状态的通知。

1
2
3
4
createPeaource(...)
.then(function(value1){ ... })
.then(function(value2){ ... })
.finally(function(){ ... });

这是 Domenic Denicola 计划实现的 finally():

1
2
3
4
5
6
7
8
9
10
11
Promise.prototype.finally = function (callback) {
let p = this.constructor;
// 不在此调用回调函数,
// 希望使用 then 来处理
return this.then(
// Callback fulfills: 传递参数结果
// Callback rejects: 传递拒绝状态
value => p.resolve(callback()).then(() => value),
reason => p.resolve(callback()).then(() => { throw reason })
);
};

回调函数决定了 接收器(this)如何设置处理:

  • 如果回调函数抛出一个错误或者返回一个拒绝状态的 promise,那么变为 拒绝状态的值。
  • 否则,接收器的设置变为的 finally 返回的 promise 的设置。使用 finally() 的链式方法。

例一:使用 finally() 来隐藏 spinner:

1
2
3
4
5
showSpinner();
fetchGalleryData()
.then(data => updateGallery(data) )
.catch(showNoDataError)
.finnally(hideSpinner);

例二:使用 finally 来清除测试

1
2
3
4
5
var HTTP = require('q-io/http');
var server = HTTP.Server(app);
return server.listen(0)
.then(function(){ ... })
finally(server.stop);

15. ES6 兼容的 promise 库

下面有一些 promise 的库,有一些 ES6 的API,意味着你在将来也可以用原生 ES6 代码替换。

  • RSVP.js 来自 Stefen Penner,为 ES6 promise API 的超集。
  • Native Promise Only 来自 Kyle Simpson 是原生 ES6 promise 的 polyfill,以严格模式来定义,尽可能接近而不扩展。
  • Lie 来自 Calvin Metcalf 小巧,完善的 promise 库,遵循 Promises/A+ 规范。
  • Q.promise 来自 Kowal 实现 ES6 API。
  • 最近的,来自 Paul Millr 包含 promise 的 ES6 Shim。

16. 传统的异步代码的接口

放你使用一个 promsie 库时,有时是基于不支持 promsie 的异步代码。这段讲述一下 Node.js 风格的异步函数与 jQuery deferreds。

16.1 Node.js 的接口

promise 库 Q 有几个工具方法来转换函数使其使用 Node.js 风格(err, result)的回调函数然后返回一个promise(还有一些做一些相反的事情,转换基于 promise 的函数来为一些接受回调函数的函数)。例如:

1
2
3
4
5
var readFile = Q.denideify( FS.readFile );
readFile('foo,txt', 'utf-8')
.then(function(text){
....
});

denodify 为一个符合 ES6 promise API ,提供 nodification 函数式化功能。

16.2. jQuery 的接口

jQuery 的 deferred 类似于 promise,但是也有几个兼容性方面的不同。方法 then() 都类似与 ES6 promises(主要不同之处是:不会在 reactions 中监控错误抛出)。我们可以通过 Promise.resolve() 将 一个 jQuery deferred 为 ES6 的 promise:

1
2
3
4
5
6
7
Promise.resolve(
jQuery.ajax({...})
).then(funciton(data){
console.log(data);
}).catch(function(reason){
console.error(reason)
});

17. 延伸阅读

  1. Promises/A+(http://promisesaplus.com/):Brian Cavalier 与 Domenic Denicola 编辑(JS promise 事实标准)
  2. Javascript Promises:再次强势归来(http://www.html5rocks.com/en/tutorials/es6/promises/):来自 Jake Archibad(挺不错的 promise 简要介绍)
  3. Promsie 反模式(http://taoofcode.net/promise-anti-patterns/):来自 Tao(要点与技术)
  4. Promise 模式(https://www.promisejs.org/patterns/)来自 Forbes Lindeasy
  5. 揭示构造函数模式(http://domenic.me/2014/02/13/the-revealing-constructor-pattern/):来自 Domenic Denicola(为 Promise 构造函数使用的模式)
  6. Chrome 开发者工具调试异步 JS 代码(http://www.html5rocks.com/en/tutorials/developertools/async-call-stack/):来自 Pearl Chen
  7. ES6 中的迭代器和生成器(http://www.2ality.com/2013/06/iterators-generators.html)

原文地址: http://www.2ality.com/2014/10/es6-promises-api.html
原文作者:Dr. Axel Rauschmayer

译者:倪颖峰

(文章第二部分实在是太长,所以在此分成两部分翻译)
本文是通过普通的 promises 和 ES6的 promise API 来介绍异步编程。这是两篇系列文章的第二部分 - 第一部分介绍了一下异步编程的基础(你需要充分理解一下以便明白这篇文章)。

鉴于 ES6 的 promise API 比较容易从 ES5 标准模拟生成过来,所以这边主要使用函数表达式,而不使用更为简洁的 ES6 中的箭头函数。

1. Promises

Promises 是一种来解决特定异步编程的模式:函数(或者方法)异步返回其结果。实现一个对返回结果的具有占位符意义的对象 promise。函数的调用者注册回调函数,一旦结果运算完毕就立即通知 promise。函数会由 promise 来传递结果。

JavaScript 的 promises 事实标准称为 Promises/A+。ES6 的 promise API 便遵循这个标准。

2. 第一个实例

看一下第一个实例,来了解下 promises 是如何运行的。

NodeJS 风格的回调函数,异步读取文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
fs.readFile('config.json', function (error, text) {
if (error) {
console.error('Error while reading config file');
} else {
try {
var obj = JSON.parse(text);
console.log(JSON.stringify(obj, null, 4));
} catch (e) {
console.error('Invalid JSON in file');
}
}
});

使用 promises,相同功能的实现可以是这样:

1
2
3
4
5
6
7
8
9
readFilePromisified('config.json')
.then(function (text) { // (A)
var obj = JSON.parse(text);
console.log(JSON.stringify(obj, null, 4));
})
.catch(function (reason) { // (B)
// File read error or JSON SyntaxError
console.error('An error occurred', reason);
});

这边依旧是有回调函数,但是这里通过方法来提供的,避免了放在结果中(then() 和 catch())。在 B 处的报错的回调函数有两方面的优势:第一,这是一种单一风格的监听错误。 第二,你可以同时处理 readFilePromisified() 的 和 A 处回调函数的错误。

3. promises 的创建和使用

从生成形式和消耗形式两方面来看一下 promises 是如何操作的。

3.1. 生成一个 promise

作为一个生成形式,你创建一个 promise 然后由它传递结果:

1
2
3
4
5
6
7
8
var promise = new Promise( function( resolve, reject ){
...
if( ... ){
resolve( value );
} else {
reject( reason );
}
} );

一个 promise 一般具有一个到三个(互斥)的状态:

Pending:还没有得到结果,进行中
Fulfilled:成功得到结果,通过
Rejected:在计算过程中发生一个错误,拒绝

一个 promise 被设置过后( 代表运算已经完成 )那么它要么是 fulfilled 要么是 rejected。每一个 promise 只能被处理一次,然后保持处理后的状态。之后在处理它将不会触发任何效果。

new Promise() 的参数( 在 A 处开始的 )称为 executor(执行者):

  1. 如果运算成功,执行器会将结果传递给 resolve()。那属于 promise 通过(后面将会解释,如果你处理一个 promise 可能会不同)。
  2. 如果错误发生了,执行器就会通过 reject() 通知 promise 消耗形式。就会解决该 promise。

3.2. 消耗一个promise

作为 promise 的消耗形式,你会将一个完成态或者通过态通知给 reactions - 通过 then 方法注册的回调函数。

1
2
3
4
5
promise.then( function( value ){
/* fulfillment */
}, function( reason ){
/* rejection */
} );

正是由于一个 promise 仅能被处理一次,之后再也不能变化使得 promises 对于异步函数来说异常有用(一次性使用结果)。此外,还没有任何其他的条件,因为在 promise 被处理时间点的前后调用 then() 是一样的:

  1. 前者的情况,合适的反馈调用会在 promise 被处理时立即调用。
  2. 后者情况,promise 的结果(通过的值或者拒绝的值)会被缓存起来,在合适的请跨过下立即处理反馈(排列一个任务)

3.3. 只处理通过态或者拒绝态

如果你只关心通过状态,你可以忽略 then() 的第二个参数:

1
2
3
promise.then(function( value ){
/* fulfillment */
});

如果你只需要通过状态,你可以忽略第一个参数。用 catch() 方法也可以更紧凑的来实现。

1
2
3
4
5
6
7
promise.then( null, function( reason ){
/* rejection */
} );
// 等价于
promise.catch( function( reason ){
/* rejection */
} );

这里推荐只是用 then() 处理成功状态,使用 catch() 处理错误,因为这样可以更加优雅的标记回调函数并且你可以同时对多个 promises 进行通过态处理(稍后解释)。

4. 实例

让我们在一些例子中使用一下基本构成形式。

4.1. 例:promise 化 XMLHttpRequest

下面是一个基于 XMLHttpRequest API 事件,通过 promise 编写的 HTTP GET函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function httpGet( url ){
return new Promise( function( resolve, reject ){
var request = new XMLHttpRequest();
request.onreadystatechange = function(){
if( this.status === 200 ){
// success
resolve( this.response );
}else {
reject( new Error( this.statusText ) );
}
}
request.onerror = function(){
reject( new Error( 'XMLHttpRequest Error: ' + this.statusText ) );
}
request.open( 'GET', url );
request.send();
} );
}

下面是如何使用 httpGet():

1
2
3
4
5
6
httpGet("http://example.com/file.txt")
.then(function(){
console.log('contents: '+ value);
}, function( reason ){
console.log('something error', reason);
});

4.2. 例子:延迟执行

使用 setTimeout() 来实现基于 promise 的 delay()(类似于Q.delay())。

1
2
3
4
5
6
7
8
9
function delay( ms ){
return new Promise(function( resolve, reject ){
sertTimeout( resolve, ms ); // (A)
});
}
// 使用 delay()
delay( 5000 ).then(function(){ // (B)
console.log('5s have passed');
});

注意 A 处我们调用 resolve 没有传递参数,相当于我们这么调用 resolve( undefined )。在 B 处我们不需要通过的返回值,就可以简单的忽略它。仅仅通知就已经OK了。

4.3. 暂停 promise

1
2
3
4
5
6
7
8
function timeout( ms, promise ){
return new Promise(function( resolve, reject ){
promise.then( resolve );
setTimeout(function(){
reject(new Error('Timeout after ' + ms + ' ms')); // (A)
}, ms)
});
}

注意在 A 处的超时失败并不会阻止这个请求,但是可以确保 promise 的成功状态结果。

如下方式使用 timeout():

1
2
3
4
5
6
7
timeout( 5000, httpGet("http://example.com/file.txt") )
.then(function( value ){
console.log('contents: ' + value);
})
.catch(function( reason ){
console.log('error or timeout: ' , reason);
});

5. 链式调用 then()

方法调用结果 P.then( onFulfilled, onRejected ) 是一个新的 promise Q。这意味着在 Q 中,你可以通过调用 then() 来保持对基于 promise 流的控制:

  1. Q 在 onFulfilled 或者 onRejected 返回结果的时候,即为通过。
  2. Q 在 onFulfilled 或者 onRejected 抛出错误的时候,即为拒绝。

5.1. 一般值来通过

如果你通过使用 then() 返回一个一般值来处理一个 Q,那你就可以通过下一个 then 来取到这个值:

1
2
3
4
5
asyncFunc().then( function(){
return 123;
} ).then( function( value ){
console.log( value ); // 123
} )

5.2. 通过 then式 来通过

你也可以通过 then() 来返回一个 then式的 R对象 来将 promise Q 通过。then式表示有 promise 风格方法 then() 的任何对象。即, promises 便是 then式的。处理 R (例如将其以 onFulfilled 来返回)意味着他是被插入到 Q 之后的:R的结果将被作为 Q onFulfilled 或者 onRejected 的回调函数。也就是说 Q 转变为 R。

这个形式主要是用来扁平化嵌套式调用 then(),比如下面的例子:

1
2
3
4
5
6
7
asyncFunc1()
.then(function(value1){
asyncFunc2()
.then(function(value2){
...
});
});

那么扁平化形式可以变为这样:

1
2
3
4
5
6
7
asyncFunc1()
.then(function(value1){
return asyncFunc2();
})
.then(function(value2){
...
});

6. 错误处理

如之前提到的,不管你在错误中返回什么都讲成为一个 fulfillment 的值(或者 rejection 值)。这使得你可以定义失败执行的默认值:

1
2
3
4
5
6
retrieveFileName()
.catch(function(){
return 'Untitled.txt';
}).then(function(fileName){
...
});

6.1. 捕获异常

执行过程中的异常将会传递到下一个错误处理中:

1
2
3
4
5
6
new Promise(function(resolve, reject){
throw new Error();
})
.catch(function(err){
// Handle error here
});

作为异常,会将其作为then的一个参数抛出:

1
2
3
4
5
6
7
asyncFunc()
.then(funcrion(value){
throw new Error();
})
.catch(function(reason){
// Handle error here
});

6.2. 链式的错误处理

将会有一个或者多个的 then() 调用但并没有提供一个错误处理。那么直到出现错误处理该错误才会被传递出来:

1
2
3
4
5
6
asyncFunc1()
.then(asyncFunc2)
.then(asyncFunc3)
.catch(function(reason{
// something went wrong above
});

7. 组合

本段会描述你如何将先现有的 promises 来组合创建新的 promise。我们已经使用过一种组合 promise 的方式了:通过 then() 连续的链式调用。Promise.all() 和 Promise.race() 提供了另一些组合的形式。

7.1. 通过 Promise.all() 实现 map()

庆幸的是,基于 promise 返回结果,很多同步的工具仍然可以使用。比如你可以使用数组的方法 map():

1
2
3
4
5
var fileUrls = [
'http://example.com/file1.txt',
'http://example.com/file2.txt'
];
var promisedTexts = fileUrls.map(httpGet);

promisedTexts 为一个 promises 对象的数组。Promise.all() 可以处理一个 promises 对象的数组(then式 和 其他值可以通过 Promise.resolve() 来转换为 promises)一旦所有的项状态都为 fulfilled,就会处理一个他们值得数组:

1
2
3
4
5
6
7
8
9
Promise.all(promisedTexts)
.then(function(texts){
texts.forEach(function(text){
console.log(text);
});
})
.catch(function(reason){
// 接受 promises 中第一个 rejection 状态
});

7.2. 通过 Promise.race() 实现延时

Promise.race() 使一个 promises 对象数组(then式 和 其他值可以通过 Promise.resolve() 来转换为 promises)返回一个 promise 对象 P。输入第一个的 promises 会将其结果通过参数传递给输出的 promise。

举个例子,来使用 Promise.race() 来实现一个 timeout:

1
2
3
4
5
6
7
8
Promise.race([
httpGet('http://example.com/file.txt'),
delay(5000).then(function(){
throw new Error('Time out');
})
])
.then(function(text){ ... })
.catch(function(reason){ ... });

8. Promises 基本都是异步的

一个 promise 库,不管结果是同步(正常方式)还是异步(当前延续的代码块完成之后)进行的传递都做好控制。然而,Promises/A+ 规范约定后一种方式应总是可用的。它以以下的条件来将状态传给 then():

当执行上下文栈中只包含平台代码时才执行 onFulfilled 或者 onRejected。

这意味着你可以依赖运行到完成的语态(第一部分中提到的),使得链式的 promises 不会使其他任务在闲置时处于等待状态。

原文地址: http://www.2ality.com/2014/09/es6-promises-foundations.html
原文作者:Dr. Axel Rauschmayer

译者:倪颖峰

本文主要介绍一下JS中异步编程的基础。这是一个主体两篇博文的第一部分,第二部分会涵盖在 ES6 中 promises 的API。

1. JavaScript 中的调用堆栈

当函数 f 调用一个函数 g,g 就需要知道它完成后在哪里返回(在 f 内部)。这里的主要方式是使用栈进行管理,即调用堆栈。来看一个实例。

1
2
3
4
5
6
7
8
9
10
11
function h( z ){
console.log( new Error.stack ); // (A)
}
function g( y ){
h( y+1 ); // (B)
}
function f( x ){
g( x+1 ); // (C)
}
f( 3 ); // (D)
return;

开始,当上面的程序运行时,调用堆栈为空。 当在 D 处函数调用 f( 3 )时,栈中有了一个项:

当前的全局作用域

当在 C 处调用函数 g( x+1 )时,栈中就有了两个项:

当前的 f
当前的全局作用域

在当 B 处调用函数 h( y+1 )时,栈中含有了三项:

当前的 g
当前的 f
当前的全局作用域

在 A 处的栈跟踪内会展示出下面的调用堆栈:

at Error at h at g at f

接下来,每当一个函数执行结束,栈中就会相应的删除顶部的对应项。当函数 f 结束后,就会回到全局作用域,然后调用栈变为空。在 E 处,返回是栈为空,意味着程序的结束。

2. 浏览器的事件循环

简而言之,每一个浏览器页面tab都有独立的进程,事件循环。该循环会执行浏览器相关的由一个任务队列分发的事务(称为 任务)。任务实例有:

  1. 解析 HTML
  2. 执行 script 标签中的 JS 代码
  3. 处理用户交互(点击鼠标,键盘输入等)
  4. 处理网络异步请求结果

2 - 4任务均是通过浏览器内置引擎来执行JS代码。在代码结束时结束。然后队列中的下一项任务开始被执行。下面图片展示了所有的机制是如何连接起来的。
事件循环是掺杂在其他并行运行的各种进程中(定时器,输入处理器等)。这些进程同通过向其队列添加任务进行通信。

2.1. 定时器

浏览器具有定时器,setTimeout() 来创建定时器,等到触发时刻就会向其队列添加一个任务。如下标识:
setTimeout( callback, ms );

在ms毫秒之后, callback 回调函数就被添加到任务队列中。需要注意的是 ms 只是指定回调函数添加的事件,并不保证被执行。特别是如果事件循环被阻塞,那可能会在很久才执行(文章后面会提到)。

当 setTimeout() ms 设置为零是一个较为通用的方法来向队列立即添加任务。然而一些浏览器并不与许 ms 设置低于最小限度(FF 中为4ms);如果小于最小限度,那也会将被设置成最小限度值。

2.2. 显示DOM的变化

对于许多的DOM变化(特别是会引起重排的),也不会立即更新显示。“布局没16ms才会刷新一次”而且必须给予其通过事件循环执行的机会。

有一些办法来协调浏览器频繁的DOM更新,来避免布局结果的冲突。查询一下关于 requestAnimationFrame() 的详细文档吧。

2.3. 运行至完成 的语义

JS 所谓的运行至完成语义:当前任务总是在下一个任务执行前完成。这意味着每一个任务都可以完全控制所有的当前状态,并且不需要担心并发的修改。
看个例子:

1
2
3
4
setTimeout( function(){
console,log('Second'); // (A)
}, 0 );
console,log('First'); // (B)

在A处开始的函数是被立即添加到队列中的,但是只有在当前代码块完成之后(具体就是 B 处),才能执行。意味着代码输出永远是:

First Second

2.4. 阻塞事件循环

我们可以发现,每个tab(在一些浏览器,完整的浏览器)被单独的进程控制-包括永辉二面和其它所有的运算。这意味着你在进程中执行长时间的运算将会使得用户界面被定格。延时代码如下:

1
2
3
4
5
6
7
8
9
10
11
function testBlocking(){
console.log( 'Blocking...' );
sleep( 5000 );
console.log( 'Done' );
}
function sleep( ms ){
var start = Date.now();
while( (Date.now() - start) < ms );
}

testBlocking();

当函数执行时,同步的函数 sleep() 阻塞事件循环5秒钟,在这些时间里用户界面将被锁定。

2.5. 避免阻塞

你可以使用两种方式来避免阻塞事件循环:

首先,不要在主进程中执行长时间的计算,将其移到其他进程中。可以通过 Worker API 来实现。
其次。不应该(以同步方式)等待一个长时间运算的结果( 在Worker进程中运行你的算法,或者以网络请求等 ),你可以继续执行事件循环,是运算在完成的时候通知你。事实上,在浏览器中你没有什么选择,而只能这么做。比如,并没有内置的 sleep 同步(例如之前使用的 sleep() 方法)。取而代之的是 setTimeout() 异步的sleep方法。

下面部分将会讲解一下关于异步等待结果的技术。

3. 异步处理结果

异步处理结果的两个常见部分:事件与回调函数。

3.1 通过事件异步处理结果

在这部分的异步处理结果内容中,你需要为每一个请求创建一个对象,对其注册事件绑定:一个为计算成功,一个为出错。下面代码展示对于 XMLHttpRequest API的工作原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var req = new XMLHttpRequest();
req.open('GET', url);

req.onload = function(){
if( req.status === 200 ){
processData( req.response );
}else{
console.log( 'ERROR', req.statusText );
}
};

req.onerror = function(){
console.log('Network Error');
};

req.send();// add request to task queue

记住,事实上最后一行并不执行结果,它只是将其添加到任务队列。因此你也可以在 设置 onload和onerror之前,open() 之后正确的调用该方法。将会一样的执行,由于JS代码的 运行至完成的特性。

如果你使用多线程语言,indexedDB(索引型数据库)请求可能会引起相竞争的情况。因此,运行至完成的特性使得在JS变得安全可靠。

1
2
3
4
5
6
7
8
var openRequest = indexedDB.open( 'test', 1 );
openRequest.onsuccess = function( event ){
console.log('Success!');
var db = event.target.result;
}
openRequest.onerror = function( error ){
cosole.log( error );
}

open() 不会立即打开数据库,它会在队列中添加一个任务,该任务会在当前所有任务执行完毕之后执行。这就是为何你可以(事实上也是必须可以)在调用 open() 之后注册事件句柄的原因。

3.2. 通过回调函数异步处理结果

如果使用回调函数来异步处理结果,你需要将回调函数作为链接参数传递给异步函数或者方法进行调用。
下面为 Node.js 的实例。通过 fs.readFile() 异步读取文件内容:

1
2
3
4
5
6
7
// Node.js
fs.readFile( 'myfile.txt', { encoding: 'utf8' }, function( error, text ){
if( error, text ){ // (A)
// ...
}
console.log( text );
} );

如果 readFile() 成功,在A处的回调函数就会通过参数 text 来传递结果值。如果失败,回调函数会通过第一个参数获取一个错误(一般来说是一个Error或者其子类)。

在经典的函数式编程风格中,代码可以是这样的:

1
2
3
4
5
6
readFileFunctional( 'myfile.txt', { encoding: 'utf8' }, function( text ){
console.log( text ); // success
}, function( error ){ // failure
// .....
}
);

3.3. 连续传递风格

使用回调函数的编程风格(特别是在功能式证明之前)又叫做连续传递式(CPS),因为下一步是需要传递一个参数的。这使得调用函数可以得到更多的对于接下来运行的和什么时候运行的控制。
下面代码显示了所谓的CPS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console.log('A');
identity('B', function step2(result2) {
console.log(result2);
identity('C', function step3(result3) {
console.log(result3);
});
console.log('D');
});
console.log('E');

// Output: A E B D C

function identity(input, callback) {
setTimeout(function () {
callback(input);
}, 0);
}

对于每一步,都在程序的控制流内进行回调。这就导致了有时被称为回调地狱的嵌套函数。当然,你完全可以避免嵌套,因为JS的函数声明是被提升的(它们被计算定义是在它们作用域的最开始阶段)。这意味着你可以提前调用和使用定义在程序后面的函数声明。下面代码就是使用提升使得之前的例子扁平化。

1
2
3
4
5
6
7
8
9
10
11
12
console.log('A');
identity('B', step2);
function step2(result2) {
// The program continues here
console.log(result2);
identity('C', step3);
console.log('D');
}
function step3(result3) {
console.log(result3);
}
console.log('E');

3.4. CPS中编写代码

在一般的JS风格中,我们使用以下方式来组合代码块:

  1. 将其一个接一个放置。使代码变得一目了然,普通风格的级联式代码可以帮助我们提醒记住代码的正确组合顺序。
  2. Array 的方法,比如 map(),filter() 和 forEach()。
  3. 如 for 或者 while 的循环。

Async.js 这个库提供了让我们用 CPS 做的更简单的操作符,NodeJS 风格的回调函数。下面是一个被使用来加载存在数组中的三个文件内容。

1
2
3
4
5
6
7
8
9
10
11
var async = require('async');
var fileNames = [ 'foo.txt', 'bar.txt', 'baz.txt' ];
async.map( fileNames, function( fileName, callback ){
fs.readFile( fileName, { encoding: 'utf8' }, callBack );
}, function( error, textArray ){
if( error ){
cosole.log( error );
return;
}
console.log( 'TEXTS:' + textArray.join( " " ) );
} );

3.5. 回调函数的利弊

使用回调函数处理结果是一种完全不同的编程风格,CPS。CPS主要优点是很容易理解。当然,他有一些缺点:

  1. 错误监控变的较为复杂:一般通过两种方式来处理报错,通过回调函数和异常。也需要小心两者相结合的情况。
  2. 更少的信息:在同步函数中对于输入(参数)和输出(函数的返回值)会有一个明确的关系。在异步函数中使用回调函数,他们被混在一起:函数结果并不重要,并且一些参数作为输入,一些参数作为输出。
  3. 组合变的更为复杂:因为有些输出就在参数中,这变成了更为复杂的组合器。

在 NodeJS 风格中回调函数有三个弊端(与函数式风格相比较):

  1. 使用 if 语句处理错误状态增加了复杂程度。
  2. 重用绑定函数较为困难。
  3. 提供一个默认的错误处理函数也很困难。如果你对自己的函数调用不想写处理函数,那么默认的错误处理函数是非常有用的。它可以被使用在函数调用者没有指定特定的处理函数的时候。

4. 展望一下

第二部分主要包括了 promises 以及 ES6 中 promise API。Promises 底层的实现比回调函数复杂很多。当然,它也带来了挺多优势之处。

观察者模式对于前端开发来说也是一种接触较为频繁的软件设计模式,如果没注意过,那说明你还要好好学习啦。

观察者模式又称做发布订阅模式,基本定义为一对多或者多对多的依赖关系,由一个或者多个观察者对象来对一个或者多个发布者进行监听,由发布者的行为来触发各个观察者的行为。

观察者模式使的可以支持简单的广播通信,自动通知所有订阅过的对象,从而使得目标对象和各个观察者可以各自独立,减弱耦合度,使两者更容易扩展和重用。

简单的观察者模式也就无非为一个简单的对象,内部有一个存放函数的数组 list, add以及fire方法,add方法用来添加监听的回调函数进入lists数组,fire则是传递参数触发回调函数列表。这是JS中以回调函数形式实现观察者模式最简单的一种方式。

1
2
3
4
5
6
7
8
9
10
11
var Observable = {
lists : [],
add : function( fn ){
this.lists.push( fn );
},
fire : function( args ){
this.lists.forEach(fucntion(){
fn( args );
});
}
}

结果当然是biubiu弹出两个console了。下面是一个稍微复杂点的观察者模式,包含了订阅,发布以及退订三个方法,基本可以实现一些简单的观察者模式。以围绕存储回调函数列表的对象 topics 进行订阅,发布和退订。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
(function () {

var q = window.pubsub = {};

var topics = {},
subUid = 0;

// 发布
q.publish = function (topic, args) {
if (!topics[topic]) {
return false;
}
var subscribers = topics[topic],
len = subscribers ? subscribers.length : 0,
i = 0;
while (i < len) {
subscribers[i].func(args);
i++;
}
return true;
};

//订阅方法
q.subscribe = function (topic, func) {
if( !func ){
return false;
}
if (!topics[topic]) {
topics[topic] = [];
}
var token = subUid++;
topics[topic].push({
token: token,
func: func
});
return token;
};

//退订方法
q.unsubscribe = function (topic, token) {
if (!topics[topic]) {
return false;
}
var subscribers = topics[topic],
len = subscribers ? subscribers.length : 0,
i = 0;
for (; i < len; i++) {
if (subscribers[i].token === token) {
subscribers.splice(i, 1);
return token;
}
}
return false;
};
})();

一般来说这是JS中实现观察者模式最基本的形式,数组来存放回调,手动进行回调函数的添加,删除和触发。有点类似于一些库中的回调函数的形式。在复杂一点就是处理一下对于数据的储存方式等,下面是之前写过的一个自己的观察者模式实现方式:

subpub 模块,实现发布者与订阅者双向记录绑定 即:发布者中记录订阅该发布者的所有订阅者, 订阅者中记录该订阅者订阅的所有发布者。

实现创建 publisher 发布者 和 subscriber 订阅者 。

publisher 实现

  1. publish 发布信息
  2. addSubscriber 添加订阅者
  3. deleteSubscriber 删除订阅者
  4. clear 清空订阅者。

subscriber 实现

  1. subscribe 订阅发布者
  2. unsubscribe 取消订阅发布者
  3. triggle 触发订阅者执行函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
(function( subpub ){
if( typeof define === "function" && define.amd ){
define( subpub );
}else if( window.jQuery ){
var old = jQuery.subpub,
sp = subpub();
jQuery.subpub = sp;
jQuery.template.noConflict = function () {
jQuery.subpub = old;
return sp;
}
}else if( !window.subpub ){
window.subpub = subpub();
}else{
throw new Error('Variable cookie is already exit in window');
}
}(function(){

// 由于发布者 订阅者是双向存储标记的 所以使用发布者ID和订阅者ID进行标记
// 发布和订阅总对象来关联 ID 与 对象
var PUBID = 0,
pubObj = {},
SUBID = 0,
subObj = {};

// 关于对象类型的均不采用鸭子辨别法 只用全等
function has( array, item ){
var value,i,len, hasOwn = {}.hasOwnProperty;
if(array == null) return;

if( array instanceof Array ){
for(i=0, len=array.length; i<len; i++){
if( array[i] === item ){
return true;
}
}
}else{
for(var key in array){
if(hasOwn.call(array, key) && array[key] === item){
return true;
}
}
}
return false;
}

function each( array, iterator, context ){
var value,i,len,
forEach = [].forEach,
hasOwn = {}.hasOwnProperty;

context = context||this;

if(array == null) return;

if(forEach && array.forEach === forEach){
array.forEach(iterator, context);
}else if( array instanceof Array ){
for(i=0, len=array.length; i<len; i++){
iterator.call(context, array[i], i, array);
}
}else{
for(var key in array){
if(hasOwn.call(array, key)){
iterator.call(context, array[key], key, array);
}
}
}
}

function publisher(){

// 本来想将该属性私有封装,不过代码量会增加很多,并且也没有什么必要
this.subIds = [];
this.pubID = PUBID;
}

publisher.prototype = {

// 发布 触发所有的 subIds id 列表成员的 trigger
publish : function( info ){
var subIds = this.subIds;

each( subIds, function( subId ){
subObj[subId].trigger( info );
});

return this;
},

// 发布者中添加 订阅者, 并在订阅者中记录发布者 单一订阅原则
addSubscriber : function( subscriber ){
var subIds = this.subIds,
subId = subscriber.subID;

// 单一订阅 也可以避免 订阅者 发布者中相互记录的无限循环
if( has( subIds, subId ) ){
return;
}

if(subscriber && subscriber.subscribe){
subIds.push( subId );
subscriber.subscribe( this );
}else{
throw new Error('subscriber is illegal');
}
},

deleteSubscriber : function( subscriber ){
var subIds = this.subIds,
subId = subscriber.subID;

if( !has( subIds, subId ) ){
return;
}

for (var i = subIds.length - 1; i >= 0; i--) {
if( subIds[i] === subId ){
subIds.splice(i, 1);
}
};

subscriber.unsubscribe( this );

return this;
},

clear : function(){
var subIds = this.subIds;
for (var i = subIds.length - 1; i >= 0; i--) {
subObj[ subIds[i] ].unsubscribe( this );
};

this.subIds = [];
}
}


function subscriber( func ){
this.pubIds = [];
this.subID = SUBID;
this.callbck = func;
}

subscriber.prototype = {
subscribe : function( publisher ){
var pubIds = this.pubIds,
pubId = publisher.pubID;

if( has( pubIds, pubId ) ){
return;
}

if( publisher && publisher.addSubscriber ){
pubIds.push( pubId );
publisher.addSubscriber(this);
}else{
throw new Error('publisher is illegal');
}
},

unsubscribe : function( publisher ){
var pubIds = this.pubIds,
pubId = publisher.pubID;

if( !has( pubIds, pubId ) ){
return;
}

for (var i = pubIds.length - 1; i >= 0; i--) {
if( pubIds[i] === pubId ){
pubIds.splice(i, 1);
}
};

publisher.deleteSubscriber( this );

return this;
},

clear : function(){
var pubIds = this.pubIds;
for (var i = pubIds.length - 1; i >= 0; i--) {
pubObj[ pubIds[i] ].deleteSubscriber( this );
};

this.pubIds = [];
},

trigger : function( args ){
this.callbck( args );
}
}

return {
creatPublisher : function(){
var pub = new publisher();
pubObj[ PUBID++ ] = pub;

return pub;
},
creatSubscriber : function( func ){
var sub = new subscriber( func );
subObj[ SUBID++ ] = sub;

return sub;
}
}
}));

实例:

var sp = window.subpub;

var a1 = sp.creatPublisher(), a2 = sp.creatPublisher(), a3 = sp.creatPublisher();
var b1 = sp.creatSubscriber(function(a){ console.log('b1 '+a) }),
b2 = sp.creatSubscriber(function(a){ console.log('b2 '+a) }),
b3 = sp.creatSubscriber(function(a){ console.log('b3 '+a) });

a1.addSubscriber(b1)
a1.addSubscriber(b3)
a2.addSubscriber(b2)
a3.addSubscriber(b1)
a3.addSubscriber(b2)
a3.addSubscriber(b3)


< a1.publish("this is a1 publish")
> "b1 this is a1 publish"
> "b3 this is a1 publish"

< a3.deleteSubscriber(b1)
< a3.deleteSubscriber(b2)
< a3.publish("this is a3 publish")
> "b3this is a3 publish"

< b3.unsubscribe(a3)
< b2.subscribe(a3)
< a3.publish("this is a3 publish")
> "b2 this is a3 publish"

简单学习了那么些设计模式之后,其实越来越觉得设计模式开始越来越抽象化,成为一种类似于现实如何转化为逻辑,思维的一种方式。所以个人认为,如何将事物抽象化,各种方式主要解决的针对性问题是什么,或许就是设计模式存在的真正意义吧。

不足之处,望读者指出,谢谢~

5. 导入导出更多信息

5.1. 导入

ECMAScript 6 提供了以下的导入方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义式导出和命名式导出
import theDefault, { named1, named2 } from 'src/mylib';
import theDefault from 'src/mylib';
import { named1, named2 } from 'src/mylib';

// 重命名:导入named1 作为 myNamed1
import { named1 as myNamed1, named2 } from 'src/mylib';

// 作为兑现导入模块(每个命名式导出均作为一个属性)
import * as mylib from 'src/mylib';

// 仅仅加载模块而不导出
import 'src/mylib'

5.2. 导出

在当前模块内部有两种方式导出。一种就是以关键字 export 标记导出。

1
2
3
4
5
6
7
export var myVar1 = ...;
export let myVar2 = ...;
export const myVar3 = ...;

export function myFunc(){ ... }
export function* myGeneratorFunc(){ ... }
export class MyClass{ ... }

操作符 default 导出的是表达式(包括函数表达式,类表达式)。例如:

1
2
3
4
5
6
7
8
9
10
11
export default 123;
export default function (x){
return x;
}
export default x => x;
export default class{
constructor( x, y ){
this.x = x;
this.y = y;
}
}

另一种方式就是将所有你想导出的列出来放置在模块最下方(风格与模块模式比较类似)。

1
2
3
4
5
6
const MY_CONST = ... ;
function myFunc(){
...
}

export { MY_CONST, myFunc };

也可以使用不同的名称导出:

1
export { MY_CONST as THE_CONST, myFunc as theFunc };

记住不能使用保留字作为变量名称(如 default 和 new),却可以作为导出名称来使用(在 ECMAScript 5 中也可以将其作为属性名称来使用)。如果你是直接导入这些命名式导出,那么你就需要使用变量名称来重命名。

5.3. 重导出

重导出意味着你在当前模块添加另一个模块的导出。你可以看添加所有的模块导出。

1
export * from 'src/other_moule';

或者你可以添加选着(通过重命名):

1
2
3
export { foo, bar } from 'src/other_moule';
// 以 myFoo 来导出模块 other_module 的 foo
export { foo as myFoo, bar } from 'src/other_moule';

6. 模块的元数据

ECMAScript 6 也提供了在模块内部访问当前模块数据的方式(比如模块的 URL),如下:

1
2
import { url } from this module;
console.log( url );

this module 表示一个简单的作为一个模块导入元数据的标识。它也可以作为模块元数据。

也可以通过对象来访问元数据:

1
2
import * as metaData from this module;
console.log( metaData.url );

Node.js 使用模块局部变量 __fileName 来作为这类元数据。

7. eval() 和 模块

eval() 不支持模块语法。它将参数按照脚本语法规则解析,而脚本是不支持模块语法的(稍后说明原因)。如果你想运行模块代码,你可以使用模块加载器的API(稍后说明)。

8. ECMAScript 6 模块加载器 API

除了定义了模块语法的工作之外,还有一个编程式的API,它可以使:

  1. 编程式的使用模块和脚本
  2. 配置模块加载。

加载器解决了模块修饰符(在 import… from 后面的字符串 ID)的加载模块。构造函数是 Reflect.Loader 。每个平台都有其自己特定的全局变量 System(系统加载器),实现其平台特定的模块加载方式。

8.1. 导入模块和加载脚本

你可以通过 ES6 promises 式的 API,编程式的导入一个模块:

1
2
3
4
5
6
7
System.import( 'some_moule' )
.then( some_module => {
...
} )
.catch( error => {
...
} )

System.import() 使你可以:

1. 在 标签内部使用模块(不支持模块语法)。
2. 限制性加载模块。

System.import() 可以载入单个模块,导入多个模块你可以使用 Promise.all():

1
2
3
4
5
6
Promise.all(
[ 'module1', 'module2', 'module3' ]
.map( x => System.import( x ) ) )
.then( {
...
} );

更多加载方式:

  1. System.module( source, options? ) 在 source 的模块的中运行 JavaScript 代码(通过 promise 实现异步形式)
  2. System.set( name, module ) 为注册一个模块(一个你通过 System.module() 创建的)。
  3. System.define( name, source, option? ) 运行模块代码并且注册结果。

8.2. 配置模块加载

模块加载器API的配置有各种钩子。完成工作依旧在进行中。第一个为浏览器开发的系统加载器当前正在实施测试。目标是找到最好的模块加载配置方式。

加载器API需要允许多种自定义加载,如:

  1. 导入时检测模块是否符合Lint(如 通过 JSLint 或者 JSHint)
  2. 导入时自动转译模块(模块内可能有 CoffeeScript 或者 TypeScript 代码)
  3. 使用传统的模块(AMD,Node.js的)

配置模块加载在 Node.js 和 CommonJS 中是受限制的。

9. 蓝图

下文内容回答了两个关于 ECMAScript 6 模块的相关问题:如何运作?如何插入到HTML中?

  1. “现在如何使用 ECMAScript 6”【译文在此】提供了一个 ECMAScript 6 的概述,解释了如何将其编译为 ECMAScript 5。如果有兴趣可以从这里开始阅读。一个小而有趣的解决方案 ES6 Module Transplier 仅仅添加了将 ES6 模块语法编译成 ES5,既不是 AMD 也非 CommonJS。
  2. 在 HTML 中插入 ES6:在 script> 标签中的代码不支持模块语法,因为元素的同步特性与模块的异步特性是相冲突的。取而代之的你可以使用新的 标签。文章“未来浏览器中的ECMAScript 6 模块化 ”介绍了 的工作原理。相对于 script>来说有几个明显的优点,并且可以选着替代版本