mumuzhenzhen

mumuzhenzhen 查看完整档案

北京编辑复旦大学  |  心理学 编辑北京耶烨共享科技有限公司  |  Founder 编辑 www.yeyetech.net 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

mumuzhenzhen 发布了文章 · 2016-05-09

写给想做前端的你

P.S. 喷神请绕道,大神勿喷,不引战,不攻击,不钻牛角尖。

大二时第一次接触前端。许多同学估计都想过要做一个网站,大部分又是从PHP开始的(谁让它是世界上最好的语言呢)。后端语言参与渲染HTML一直很主流,跟着教程做,你会写一堆样式表,到后来也许是需要在提交表单时进行客户端验证,你开始写一些JS。
想做个网站啥的(以前app没有那么多),必须要学会HTML、CSS、JS,HTML构造结构,CSS表现样式,JS决定行为。JS似乎充满奇技淫巧,可以做各种效果啊,飞雪花片啊,搞咱们选课、评老师的网站啊。

后来接触了jQuery,用起来无比顺手,特别是看完《DOM编程艺术》以后。那段时间担心JS掌握得不好,心想总是用jQuery以后不会写Native怎么办?也会关注“可以直接学习jQuery吗”这样一些问题。学习了Ajax后又做过瀑布流图片墙,觉得无比兴奋。不过认识也仅仅停留在异步加载局部更新DOM可以创造更好地用户体验。

实习期间,看到公司前端做雪碧图、切片、搞div、css布局,然后花很多时间在浏览器兼容的问题的调试上。尤其是活动页面,写HTML、CSS基本占到工程的80%,JS写起来很快。后来Bootstrap逐渐流行起来,后端同学可以直接写后台,甚至都不需要前端和射鸡师了。加点栅格,加几个类,轮播组件啥的样样有,再引入jQuery,前端便成为了顺带做的事了。

我接触前端的过程没有系统性,充满了探(瞎)索(搞),也缺少引导。不过细想想,我邮只有前端的选修课啥的,课程也不是那么就业导向。就像论坛里的同学,自己搞,自己提升。

我们把上面这些点提取出来:HTML、CSS、JS、jQuery、Bootstrap,再刷刷题,看看基础知识,基本就可以参加校招了。

zp

那些求入门、求实习、赶春招、赶秋招的同学一定来得及:不过泼一盆冷水,进入大公司只是开始,你的认识、习惯、思维方式最终会决定你事业或者专业的高度。

面对新技术我觉得了解它为什么产生,解决什么问题,会怎么发展,如何在现有的工程中进行实践,比讨论它们的优劣更有意义:

首先 html5 不是一个新技术,而是在现代浏览器中使用CSS 3等特性进行前端开发的过程。以前我们更关注浏览器的差异性,而现代的浏览器对标准的支持越来越统一。

回到jQuery,在web app中使用越来越少了,一方面这个库太“大”了(吃流量),从页面加载、打开速度理论看,英明的老大会把它砍掉。针对库大小的问题,Zepto.js是一个解决方案。这个库与jQuery API相对统一,抛却了很多浏览器的兼容性的代码。

但是现在浏览器的querySelector方法,已经很好地解决了jQuery中的“Query”,使用原生的fetch方法请求数据,返回Promise也能比jQuery.ajax()的实现更好、更清晰的解决问题。

看待jQuery,我觉得应该更多看到它的历史意义。jQuery,一定程度上成为了工业标准,影响了JS语言的发展和其他JS库的构建。如同coffeescript对ES2015的影响。至于实践,越来越多的web已经不依赖jQuery进行开发了。

前端这几年进步太快了,我尝试按照不同方向讨论一下这些技术栈。

脱离浏览器的JavaScript

Node.js

我们讨论的JavaScript更多是以浏览器为宿主的ECMAScript,同源的ActionScript以Adobe的Flash作为宿主的。

浏览器中的JavaScript提供了大量与浏览器相关的API。脱离这些特定API看JavaScript,异步是它特别重要的一个特性。Google的V8引擎,让JavaScript的运行时性能大大提升,是Node.js的产生另一个必要的条件。

Jser突然可以全栈了,面对新的技术,不乏布道师。国内第一本Node.js书籍是BYVoid写的,当时盛传这个同学拿到了我司的60w的offer,一片沸沸扬扬;大家可以向他学习,在一个技术还未在祖国大地流行起来时,迅速写一本书。

至少目前,很少有大公司完全把JavaScript作为前后端通用的技术栈。传统的后端语言和技术并未没有被代替的危险。不能把Node.js简单看做是JavaScript在服务端的延展。

我觉得,Node.js很大程度拓展了JavaScript的使用范围,改变了传统前端的工作流程(后面提)。特别是NPM的产生,意义非常之大,它让JavaScript成为了一个生态系统,CommonJs也在JavaScript模块化未成熟之前,提供了优秀的模块化解决方案。

通过package.json,我们可以依赖已有的NPM项目构建自己的库。前段时间,某个同学应为法律的问题,撤消了发布在NPM上的leftpad包,短短11行代码的包撤消后,造成了React-Native、Babel等项目构建失败的灾难。

module.exports = leftpad;
function leftpad (str, len, ch) {
  str = String(str);
  var i = -1;
  if (!ch && ch !== 0) ch = ' ';
  len = len - str.length;
  while (++i < len) {
    str = ch + str;
  }
  return str;
}

许多人反思,为啥那么简单的代码要依赖,不自己写?是不是Jser忘记了怎么写代码了?我觉得,盲目解除对其他库的依赖会失去NPM社区的初衷,除非你想做超级轮子哥。NPM生态圈制定一下撤包的规则,这种灾难或许以后就不会发生。自给自足的同学们,如果lodash撤包了,或者是tj holowaychuk大神激情起来删了所有包,你们怎么办?

Node.js也让服务端渲染成为可能(universal),代码段技能在服务端运行也可以在客户端运行(universal)。从这点看,代码更容易维护和编写了,部分解耦了前端和服务端。对于SEO这个令人头大的商业问题,服务端渲染可以较好地解决。感兴趣的同学,可以去了解一下搜索引擎的爬虫是如何干活的。

总结一下,Node.js的产生完善了JavaScript生态圈,让大家看到了JavaScript的潜力,让构建更为庞大的JavaScript项目成为可能,同时前端可以使用更为工程化的流程和工具进行开发。

推荐大家一定去了解和使用Node.js,使用NPM构建自己的项目。

JSON

JSON变成了事实上Web数据的传输标准,这个是JavaScript另一大贡献。

对终端的开发,使用JSON数据后使得服务端的开发更加专注和统一。

 data.each do |key, value|
   puts "Key #{key.inspect} has value #{value.inspect}"
 end

这种代码展示了服务端渲染的能力。但是对于对于iOS、Android等原生应用,除了在WebView中,无法消费这些渲染好的HTML。

JSON和App的流行,让后端语言在渲染方面逐渐让道,向纯粹的Service发展。比如beta版本的 Rails 5 大大增强了 Rails 作为 API Service的能力。

从Ruby 或者 PHP转换到JSON需要相应的映射函数,Node.js来得更为直接,因为JSON就是一个普通的JavaScript对象。

大家可以去学习成熟的JSON风格指南。同时通过实践逐步加强对JSON的认识,设计更为规范的JSON(这个会森森影响到Mongo的存储,查询效率,React的性能)。

二进制处理后的JSON,是MongoDB存储的内容,这个基于文档的数据库被越来越多的公司使用,使得编写嵌套数据,存储流数据更为容易。传统的关系数据库,将查询结果表示为JSON,需要经过查询、封装、Transform等多个步骤,而MongoDB的查询结果就是JSON,直接查询直接使用。当然我们看到,在处理事务型问题上,关系型数据库还是首选,比如电商。我们去褒贬关系型数据库是否落后没有意义——使用场景不同。

如果大家有兴趣,可以去尝试MongoDB,感受一下冲击。

前端工程化

接下来提一下前端工程化。脚本语言并不一定需要编译再执行。传统的工程中,通过<script>标签引入依赖的JavaScript库、样式表后直接开发,写样式表。

当工程规模增大后,传统实践维护成本直线上升——于是模块化开发成为工程上的最佳实践。我们可以使用特定的技术对JavaScript、CSS进行压缩,同时合并JS与CSS解决脚本间的依赖问题,以此减少线上环境中HTTP请求的数量。

Sass Less Stylus

CSS在前端领域,进步最慢。声明式的样式表缺乏逻辑性,层层嵌套,维护成本高且不易团队合作,样式覆盖这个问题也相当恼人。

Sass等技术,输出编译后的CSS样式表,把开发过程和实际样式表分开。.scss文件结构清晰,通过变量定义、mixin等的使用让样式表的开发更加正规化。

Less与Sass类似,Stylus是TJ大神的作品,实在非常简约抽象,个人感觉不易维护。最新的Bootstrap 4使用Sass,放弃了Bootstrap 3中使用的Less。

个人觉得,这些技术深入掌握一门就可以了。实际开发还要看公司的技术栈。

CoffeeScript 与 TypeScript

与Sass等技术思想类似,Coffee 与 TypeScript 也是一个编译技术——把.coffee.ts文件编译成javascript。编译这个思想在前端领域很重要:不改变现有的语言环境(JS、CSS)同时进行最佳的工程实践。

使用过一段TypeScript,真心觉得是神器,在无类型语言中写类型不是倒退吗?请摒弃这些激进思想(世界上最好的语言第7代不是也支持类型了吗),尝试在项目中使用TypeScript,你就会感知到它的神器之处。首先,多人协作更为容易了,结合IntelliSense,IDE更为智能,开发快感直线提升。

TypeScript是微软的开源项目,Angular 2 放弃 Dart完全拥抱了TypeScript,TypeScript与Angular 2 强强合作,加入了许多构建 Angular 2的新特性。

我们整理一下这些工程化的实践:预编译、合并、压缩、打包,引入下一个概念Package工具:

Webpack Gulp Grunt Browerify

Package工具,是任务驱动型的工程化工具。通过打包构建,线上代码更为精简,循环引用等问题迎刃而解。

上述这些工具变化极快,Webpack 2快接近稳定了,JSPM这个新的工具也得到了使用:但是没有最好的,只有最合适的工具。都说Grunt已经过时,jQuery等库还一度使用Grunt进行构建。对于新的工具,我们可以了解它们的思想,不要被它们压得喘不过气:比如Gulp更像是一个PipeLine,对代码流一步步通过Loader进行加工。

在淘宝无线时,有些H5前端用Grunt构建工程,使用Less写样式表(还有些人什么都不用,直接在JsBin里面写样式、写JS)。

任务工具结合CommonJS后,可以只引用需要的模块、样式表。这样打包后,文件更小:当然如果结合sourcemap,调试和定位问题会更为容易。

JavaScript Libraries

工程化、模块化解决了code如何生产,模块、结构如何组织等问题。大家也在不断思考在前端与数据的关系。传统前端开发并不是数据导向的,多数页面由服务端渲染,前端的重心不在数据而是聚焦在用户行为的响应上,这时前端仅仅是产品的视觉末梢。

单页应用开发技术越来越多地被实践后,前端逐步变得更为数据导向(JSON API),由末梢变为大脑——业务逻辑前移;对浏览器历史的管理也是也是单页应用的另外一个中心,前端也逐步变得更为历史导向。

为了更为数据,JavaScript 库借鉴了很多服务端的思想比如MVC。MVC将数据抽象为模型,在模型中定义操作数据的基本方法;在控制器中定义商业逻辑,并控制模型的渲染。

这个阶段的代表是Backbone.js。Backbone有一个可以自定义的依赖于jQuery的前端模版引擎,是MVC模式在前端开发中的实践。同时Backbone.js 依赖于同一个作者创建的 Underscore 库,以函数式编程思想操作数据或者集合(这个Jser创造了CoffeScript、Backbone、UnderScore,Lodash是从Underscore项目派生出来的,大家可以膜拜大神Jeremy Ashkenas)。Backbone影响深远。比如facebook的 parse(中国克隆版叫LeanCloud)这种无后端服务就从Backbone借鉴了很多。

Angular.js的产生是跨时代的,这个大而全的框架让单页应用的开发更为容易。
最开始Jser们并不是很适应Angular,反倒Java程序员可以很快的上手,因为Angular 借助了很多 Spring的思想比如依赖注入。

Angular 仍然深受 jQuery 的影响,内部实现了极简版本的选择器。Angular进行双向绑定,这个牛逼的特性也有时因为性能问题被诟病。

JavaScript 库也借鉴了很多客户端开发的思想,比如React、Vue。

个人觉得,拿Angular、React、Vue这些库比较,论其优劣意义不大。他们产生的时代不同,解决的问题不同。React、Vue离开Router和生态圈的其他组件也无法构建单页应用。

React.js

React并不是一个大而全的框架,主要专注在UI层,React以Component的方式组织应用:一个Component包含这个组件的模版(样式)和行为逻辑。多个Component可以组合,产生兄弟、父子的Component结构关系。

以往开发强调结构、样式、行为的分离。但从组件角度看来,所有这些都是构成组件不可分割的整体。JSX使得HTML与JS的混编更为容易,同时React组件也可以使用内联方式来组织样式。

React默认单向绑定,在State发生变换后重新渲染DOM。从Component的生命周期中,我们可以看到客户端开发的影子(特别有iOS开发的经验的话)。譬如componentWillMount、componentDidMount、componentWillReceiveProps、componentWillUnmount这些生命周期的钩子就是像客户端开发学习的例子。一方面,生命周期的增加细化了开发的粒度,另一方,也为前端的再一次拓展做了充分地准备。

父组件通过props向子组件传值,子组件调用porps所传递的父组件的方法来执行业务逻辑。这点,非常类似iOS开发中的委托代理模式,同样是向客户端开发技术学习的例子。

React调用render方法对Component进行渲染,其中涉及了Virtual DOM机制和只能的diff算法——只更新发生变化的DOM,以此提高渲染的效率。

React并没有提供完成的数据管理方案,Flux也仅仅是一个实践的建议。去年,各种Flux解决方案百花齐放,Redux获得了最大的关注度。使用Redux是一个从入门到懵逼的过程,而且往往不知所以然,然后“这厮”有引入了Store、Action、这些概念。建议大家不要为了Flux而Flux,多做一些实践,了解函数式编程,了解Map、Reduce的概念,再深入Redux:

在这里分享些自己的认识:每一个组件都有自己的State,有层次关系的State共同组成一颗状态树来记录整个应用的状态。当Aciton被触发后,State随之更新,React以此部分地更新应用的状态和视图(State ---Action---> State')。贴一个自己总结的白板图:

Redux

这里第一次提到Immultable这个概念:总是不改变原来的对象而生成新的对象。Immultable,让时光大法得以实现。我们如果把DOM看做是State在UI层的映射,难么从State'到State后,自然能把UI层还原到原来的状态:Redux黑魔法

Angular 2

Angular 2 已经到了Realease Candidate阶段,从alpha阶段开始,每一次release都有一大堆BREAKING CHANGES(MD API说变就变)。

不过好在基本每个版本我都在跟进,还是做了些实践。

如果大家抱着学习的心态,一定不要去看国外或者知乎大神们对各种框架的褒贬,也不要去搜“我究竟是学习(使用)React、Angular 2、Ember还是Vue这种问题”,了解它们的思想和解决问题的方式。

Angular 一直使用依赖注入的单例模式解决数据管理的问题。Angular 2使用zone.js实现了一个新的脏值检查机制,并在浏览器端使用System.js管理code的依赖。

使用 TypeScript 进行开发,意味着从生产到上线过程必须经过编译和转换,特别装饰符的使用,让代码表意性更强,同时装饰符作为元数据,指导TypeScript的编译过程。

举个例子,我们看看Angular 2如何解决表单的验证问题:
Angular 2将每一个表单项(比如 input、textArea、radio)抽象为一个Control,将整个表单抽象为一个ControlGroup。每一个Control都有自己的验证规则,ControlGroup的合法性又由其包含的所有Control共同决定。这个实践使得验证逻辑与表单DOM实现了分离,更为清晰,同时原本操蛋的表单测试变得简单易行,因为测试ControlGroup时并不需要依赖于DOM。

分享一篇我翻译的文章:Angular 2核心概念,阅读后可以对Angular 2 有一个大致的了解。

Angular 2是对WebComponent的渐进,通过WebComponent,我们可以导入和使用各种成熟的组件,而不用关心它的具体实现,就像黑盒一样进行使用:例如我们想嵌入一个百度地图的WebComponent,可以这么写:

<BaiduMap [width]=300 [height]=300 [user-scalable]=false [latitude]=... [longitude]=... />

于是就I生成一个指定大小和位置的地图组件(百度地图没有这东西,我YY的,股沟有)。

Vue

这个框架我不是很了解,开发者是尤雨溪大神,在github上stars超过10,000。
手机淘宝的勾股大神一直在布道、推广、实践。个人觉得Vue的核心贡献者太少了,拉上阿里巴巴是一个明智的选择,毕竟很多前端大神可以共同完善它。

脱离浏览器的 JavaScript Libraries

再看Virtual DOM这个概念,就像是薛定谔的猫,在渲染前什么都是、什么都不是。Virtual DOM是一个抽象的概念,也组成了另一个抽象概念—— Component。(这个堪称是FB的野心,以后估计很多人被这个东西搞失业)。

一个视觉元素,一个用户事件,可以做如下抽象:

/*****
          <div></div>
    /(html)
View
    \(iOS)
           UIView

          click
    /(html)
Click
    \(iOS)
          TouchUpInside

*****/

我们发现,如果在编译时View、Click才与运行环境相关,那么app、web app的开发其实是可以共享大部分代码的,这个时候JavaScript变为了中间语言。

这个想法早在cocos2d中就已经实现,进行游戏开发的同学使用c++进行游戏开发,编译后产生安卓和 iOS 的版本平台相关的游戏。

React-Native就是这个思路的实践者,通过js绑定了OC或者安卓的运行环境,开发出性能接近于原生的原生应用。React-Native大法延展了JS的广度,也填补了iOS和安卓开发间的技术沟壑。

另一个黑魔法是热更新,以往大幅度更新App只能在App Store、安卓各大渠道发布更新,然后从应用商店提示用户升级,每一次提交都需要被审核,更何况并不是每一个用户都知道或者想去安装每一个更新版本(比如我妈)。通过React-Native可以直接下载新的bundle,实现热更新。

论坛里,有人讨论React-Native热度骤减,你们去官网看看,人家才v0.25。很多公司用不好驾驭不了React-Native原因出在缺少既了解客户端开发有了解前端开发的程序猿(媛):不能否认,Reactive-Natvie的产生是大势所趋。

说道阿里在搞的Weex,也是在向这个方向探索,如果特别成功并且使用广泛的话一定会把Vue搞得更大:尤雨溪大神在这个问题上相当的明智的。

再回到Angular 2,DomRenderer.getNativeElementSync(elementRef)也不是在做同样的事情吗?相关项目详见:NativeScript

除了JavaScript,CSS 也在尝试成为与平台无关的样式语言。React-Native就实现了CSS 的部分子集与iOS、Android的绑定。

大前端

未来的前端是一个包含但不仅限于app和web,从事多端构建任务的程序员工种。团队中单一技能的人员会越来越少,客户端与web端的界限也会越来越模糊。

同时新的技术大多会在不同领域交叉点产生。网络提速,设别处理能力提高后,应用大小、性能可能退居第二,用户体验和开发效率提升到第一。

比如很多公司,由于担心js、css打包后过大,放弃使用框架。这点我持保留意见,快速迭代的产品必须有敏捷的技术栈和稳定的框架。

目前新版本的Chrome、Node.js对ES6标准的支持已经超过90%,Babel这一类工具的生命周期不会很长,TypeScript可能会越走越远。

给大家的建议

  1. 道阻且长,冰冻三尺非一日之寒;

  2. 广泛地学习,有条件和能力的同学尽早地接触客户端开发,更多地了解服务端开发;

  3. 前端大有可为,新技术的发明者大多不是老东西,老东西经验足但是历史包袱重;

  4. 只在浏览器中思考必死无疑;

  5. 像一位同学提到的,打好基础,offer就有。学校里倒腾几年真的很难搞出什么大新闻,面试官也不会刻意为难你;

  6. 测试测试测试,前端测试值得学习掌握,比如e2e,这是一个机会:我能告诉你很多公司的前端测试都瞎JB点吗?

  7. 学习一些函数式编程的思想,例如:lodash、Redux、RxJS;

  8. 拿到offer只是开始不要嘚瑟。

番外篇——理性看待前端紧缺的现象

刚入职淘宝时,团队里有许多前端外包同学,后来很大一部分转正了,有经验的前端工程师一直稀缺。

2012年,PC购物还是主流,我们见证了无线成交额(GMV)逐渐赶超PC的时刻:端的重心越来越向mobile(App)移动。

Hybrid App很流行,部分是因为Native开发更为复杂,同时审核、更新机制缓慢。每一次手淘release都要考虑与老版本的兼容性,几百号人同时开发二个(安卓、iOS)App想想多复杂。

H5在webview中运行,随时可以更新、快速迭代,新产品或者是活动页面大多数会采用H5的形式进行发布;甚至很多小公司由于财力和技术储备有限,直接用App做壳,里面全是用H5来开发:因此市场上产生了很大的H5程序员的需求。

且慢,没有任何人说过H5比Native更好,也没有什么H5的春天,一切的一切都是因为Native 开发、更新不够成熟。但也仅限在一个时间段内。

如果一切问题都不是问题了,干嘛不全做Native?目前看来类似于React-Native、JsPatch这样的技术逐渐会让很多前端失业或是是被动转岗到纯PC业务。

番外篇——推荐学习资源

  1. You don't know JS,clone下来以后用markdone阅读器阅读;

  2. ES6 教程,阮一峰大神的ES6教程,纸质书可以在京东啥的买到;

  3. LeanPub,自出版书籍网站,每次更新都会发布新版本。支持Visa支付;

  4. Manning,特别是MEAP系列的图书,按章节更新,最新一手资料,支持Visa、PayPal支付;

  5. CodeSchool,在线学习网站,覆盖前端、iOS、Ruby等,可以先试试免费课程,支持Visa、PayPal支付;

  6. EggHead,在线学习网站,先试试免费课程,授课人大神极多,基本涵盖了最新的前端技术,支持Visa支付,200刀一年略贵;

  7. 要是觉得贵,想想火麒麟。

番外篇——F&Q

Q:楼主现在在干嘛?
A:五道口,创业狗。负责公司的技术和产品。

Q:还有什么想说的?
A:帖子还会更新维护。

Q:联系方式?
A:微信请加mumuzhenzhen,告诉我你是谁。

查看原文

赞 61 收藏 258 评论 13

mumuzhenzhen 发布了文章 · 2016-05-06

RxJS 核心概念之Subject

什么是Subject? 在RxJS中,Subject是一类特殊的Observable,它可以向多个Observer多路推送数值。普通的Observable并不具备多路推送的能力(每一个Observer都有自己独立的执行环境),而Subject可以共享一个执行环境。

Subject是一种可以多路推送的可观察对象。与EventEmitter类似,Subject维护着自己的Observer。

每一个Subject都是一个Observable(可观察对象) 对于一个Subject,你可以订阅(subscribe)它,Observer会和往常一样接收到数据。从Observer的视角看,它并不能区分自己的执行环境是普通Observable的单路推送还是基于Subject的多路推送。

Subject的内部实现中,并不会在被订阅(subscribe)后创建新的执行环境。它仅仅会把新的Observer注册在由它本身维护的Observer列表中,这和其他语言、库中的addListener机制类似。

每一个Subject也可以作为Observer(观察者) Subject同样也是一个由next(v)error(e),和 complete()这些方法组成的对象。调用next(theValue)方法后,Subject会向所有已经在其上注册的Observer多路推送theValue

下面的例子中,我们在Subject上注册了两个Observer,并且多路推送了一些数值:

var subject = new Rx.Subject();

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});
subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

subject.next(1);
subject.next(2);

控制台输出结果如下:


observerA: 1
observerB: 1
observerA: 2
observerB: 2

既然Subject是一个Observer,你可以把它作为subscribe(订阅)普通Observable时的参数,如下面例子所示:

var subject = new Rx.Subject();

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});
subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

var observable = Rx.Observable.from([1, 2, 3]);

observable.subscribe(subject); // 你可以传递Subject来订阅observable

执行后结果如下:


observerA: 1
observerB: 1
observerA: 2
observerB: 2
observerA: 3
observerB: 3

通过上面的实现:我们发现可以通过Subject将普通的Observable单路推送转换为多路推送。这说明了Subject的作用——作为单路Observable转变为多路Observable的桥梁。

还有几种特殊的Subject 类型,分别是BehaviorSubjectReplaySubject,和 AsyncSubject

多路推送的Observable

在以后的语境中,每当提到“多路推送的Observable”,我们特指通过Subject构建的Observable执行环境。否则“普通的Observable”只是一个不会共享执行环境并且被订阅后才生效的一系列值。

通过使用Subject可以创建拥有相同执行环境的多路的Observable。

下面展示了多路的运作方式:Subject从普通的Observable订阅了数据,然后其他Observer又订阅了这个Subject,示例如下:

var source = Rx.Observable.from([1, 2, 3]);
var subject = new Rx.Subject();
var multicasted = source.multicast(subject);

// 通过`subject.subscribe({...})`订阅Subject的Observer:
multicasted.subscribe({
  next: (v) => console.log('observerA: ' + v)
});
multicasted.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

// 让Subject从数据源订阅开始生效:
multicasted.connect();

multicast方法返回一个类似于Observable的可观察对象,但是在其被订阅后,它会表现Subject的特性。 multicast 返回的对象同时是ConnectableObservable类型的,拥有connect() 方法。

connect()方法非常的重要,它决定Observable何时开始执行。由于调用connect()后,Observable开始执行,因此,connect()会返回一个Subscription供调用者来终止执行。

引用计数

通过手动调用connect()返回的Subscription控制执行十分繁杂。通常,我们希望在有第一个Observer订阅Subject后自动connnect,当所有Observer都取消订阅后终止这个Subject。

我们来分析一下下面例子中subscription的过程:

  1. 第一个Observer 订阅了多路推送的 Observable

  2. 多路Observable被连接

  3. 向第一个Observer发送 值为0next通知

  4. 第二个Observer订阅了多路推送的 Observable

  5. 向第一个Observer发送 值为1next通知

  6. 向第二个Observer发送 值为1next通知

  7. 第一个Observer取消了对多路推送的Observable的订阅

  8. 向第二个Observer发送 值为2next通知

  9. 第二个Observer取消了对多路推送的Observable的订阅

  10. 取消对多路推送的Observable的连接

通过显式地调用connect(),代码如下:

var source = Rx.Observable.interval(500);
var subject = new Rx.Subject();
var multicasted = source.multicast(subject);
var subscription1, subscription2, subscriptionConnect;

subscription1 = multicasted.subscribe({
  next: (v) => console.log('observerA: ' + v)
});
subscriptionConnect = multicasted.connect();

setTimeout(() => {
  subscription2 = multicasted.subscribe({
    next: (v) => console.log('observerB: ' + v)
  });
}, 600);

setTimeout(() => {
  subscription1.unsubscribe();
}, 1200);

setTimeout(() => {
  subscription2.unsubscribe();
  subscriptionConnect.unsubscribe(); 
}, 2000);

如果你不想显式地调用connect()方法,可以在ConnectableObservable类型的Observable上调用refCount()方法。方法会进行引用计数:记录Observable被订阅的行为。当订阅数从 01refCount() 会调用connect() 方法。到订阅数从10,他会终止整个执行过程。

refCount 使得多路推送的Observable在被订阅后自动执行,在所有观察者取消订阅后,停止执行。

下面是示例:

var source = Rx.Observable.interval(500);
var subject = new Rx.Subject();
var refCounted = source.multicast(subject).refCount();
var subscription1, subscription2, subscriptionConnect;

console.log('observerA subscribed');
subscription1 = refCounted.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

setTimeout(() => {
  console.log('observerB subscribed');
  subscription2 = refCounted.subscribe({
    next: (v) => console.log('observerB: ' + v)
  });
}, 600);

setTimeout(() => {
  console.log('observerA unsubscribed');
  subscription1.unsubscribe();
}, 1200);

setTimeout(() => {
  console.log('observerB unsubscribed');
  subscription2.unsubscribe();
}, 2000);

执行输出结果如下:


observerA subscribed
observerA: 0
observerB subscribed
observerA: 1
observerB: 1
observerA unsubscribed
observerB: 2
observerB unsubscribed

只有ConnectableObservables拥有refCount()方法,调用后会返回一个Observable而不是新的ConnectableObservable。

BehaviorSubject

BehaviorSubject是Subject的一个衍生类,具有“最新的值”的概念。它总是保存最近向数据消费者发送的值,当一个Observer订阅后,它会即刻从BehaviorSubject收到“最新的值”。

BehaviorSubjects非常适于表示“随时间推移的值”。举一个形象的例子,Subject表示一个人的生日,而Behavior则表示一个人的岁数。(生日只是一天,一个人的岁数会保持到下一次生日之前。)

下面例子中,展示了如何用 0初始化BehaviorSubject,当Observer订阅它时,0是第一个被推送的值。紧接着,在第二个Observer订阅BehaviorSubject之前,它推送了2,虽然订阅在推送2之后,但是第二个Observer仍然能接受到2

var subject = new Rx.BehaviorSubject(0 /* 初始值 */);

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

subject.next(1);
subject.next(2);

subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

subject.next(3);

输出结果如下:


observerA: 0
observerA: 1
observerA: 2
observerB: 2
observerA: 3
observerB: 3

ReplaySubject

ReplaySubject 如同于BehaviorSubjectSubject 的子类。通过 ReplaySubject可以向新的订阅者推送旧数值,就像一个录像机ReplaySubject可以记录Observable的一部分状态(过去时间内推送的值)。

.一个ReplaySubject可以记录Observable执行过程中推送的多个值,并向新的订阅者回放它们。

你可以指定回放值的数量:

var subject = new Rx.ReplaySubject(3 /* 回放数量 */);

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);

subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

subject.next(5);

输出如下:


observerA: 1
observerA: 2
observerA: 3
observerA: 4
observerB: 2
observerB: 3
observerB: 4
observerA: 5
observerB: 5

除了回放数量,你也可以以毫秒为单位去指定“窗口时间”,决定ReplaySubject记录多久以前Observable推送的数值。下面的例子中,我们把回放数量设置为100,把窗口时间设置为500毫秒:

var subject = new Rx.ReplaySubject(100, 500 /* windowTime */);

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

var i = 1;
setInterval(() => subject.next(i++), 200);

setTimeout(() => {
  subject.subscribe({
    next: (v) => console.log('observerB: ' + v)
  });
}, 1000);

第二个Observer接受到3(600ms), 4(800ms) 和 5(1000ms),这些值均在订阅之前的500毫秒内推送(窗口长度 1000ms - 600ms = 400ms < 500ms):


observerA: 1
observerA: 2
observerA: 3
observerA: 4
observerA: 5
observerB: 3
observerB: 4
observerB: 5
observerA: 6
observerB: 6
...

AsyncSubject

AsyncSubject是Subject的另外一个衍生类,Observable仅会在执行完成后,推送执行环境中的最后一个值。

var subject = new Rx.AsyncSubject();

subject.subscribe({
  next: (v) => console.log('observerA: ' + v)
});

subject.next(1);
subject.next(2);
subject.next(3);
subject.next(4);

subject.subscribe({
  next: (v) => console.log('observerB: ' + v)
});

subject.next(5);
subject.complete();

输出结果如下:


observerA: 5
observerB: 5

AsyncSubject 与 last() 操作符相似,等待完成通知后推送执行过程的最后一个值。

查看原文

赞 18 收藏 56 评论 4

mumuzhenzhen 发布了文章 · 2016-05-04

RxJS 核心概念Observer & Subscription

Observer(观察者)

什么是Observer? Observer(观察者)是Observable(可观察对象)推送数据的消费者。在RxJS中,Observer是一个由回调函数组成的对象,键名分别为nexterrorcomplete,以此接受Observable推送的不同类型的通知,下面的代码段是Observer的一个示例:

var observer = {
  next: x => console.log('Observer got a next value: ' + x),
  error: err => console.error('Observer got an error: ' + err),
  complete: () => console.log('Observer got a complete notification'),
};

调用Observer逻辑,只需在subscribe(订阅)Observable后将Observer传入:

observable.subscribe(observer);

在RxJS中,Observer是可选的。在nexterrorcomplete处理逻辑部分缺失的情况下,Observable仍然能正常运行,为包含的特定通知类型的处理逻辑会被自动忽略。

下面例子中Observer并不包含complete类型通知的处理逻辑:

var observer = {
  next: x => console.log('Observer got a next value: ' + x),
  error: err => console.error('Observer got an error: ' + err),
};

在订阅Observable时,你甚至可以把回调函数作为参数传入,而不是传入完整的Observer对象:

observable.subscribe(x => console.log('Observer got a next value: ' + x));

在RxJS内部,调用observable.subscribe时,它会创建一个只有next处理逻辑的Observer。当然你也可以将nexterrorcomplete的回调函数分别传入:

observable.subscribe(
  x => console.log('Observer got a next value: ' + x),
  err => console.error('Observer got an error: ' + err),
  () => console.log('Observer got a complete notification')
);

Subscription

什么是Subscription? Subscription是一个代表可以终止资源的对象,表示一个Observable的执行过程。Subscription有一个重要的方法:unsubscribe。这个方法不需要传入参数,调用后便会终止相应的资源。在RxJS以前的版本中,Subscription被称为"Disposable"

var observable = Rx.Observable.interval(1000);
var subscription = observable.subscribe(x => console.log(x));

subscription.unsubscribe(); 

Subscription能够通过unsubscribe() 函数终止Observable的执行过程并释放相应资源。

Subscription可以嵌套使用:你可以调用一个Subscription的unsubscribe() 方法来取消一系列嵌套的Subscription。通过add方法,便可以实现Subscription的嵌套:

var observable1 = Rx.Observable.interval(400);
var observable2 = Rx.Observable.interval(300);

var subscription = observable1.subscribe(x => console.log('first: ' + x));
var childSubscription = observable2.subscribe(x => console.log('second: ' + x));

subscription.add(childSubscription);

setTimeout(() => {
  // 终止所有嵌套的Subscription
  subscription.unsubscribe();
}, 1000);

执行后,控制台会输出一下结果:


second: 0
first: 0
second: 1
first: 1
second: 2

此外,Subscription提供了remove(otherSubscription)方法,可以删除一个Subscription嵌套的子Subscription。

查看原文

赞 9 收藏 36 评论 4

mumuzhenzhen 发布了文章 · 2016-05-03

RxJs 核心概念之Observable

Observable(可观察对象)是基于推送(Push)运行时执行(lazy)的多值集合。下方表格对Observable进行了定位(为解决基于推送的多值问题):

MagicQ单值多值
拉取(Pull)函数遍历器
推送(Push)PromiseObservable

:当observable被订阅后,会立即(同步地)推送123 三个值;1秒之后,继续推送4这个值,最后结束(推送结束通知):

var observable = Rx.Observable.create(function (observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  setTimeout(() => {
    observer.next(4);
    observer.complete();
  }, 1000);
});

为得到observable推送的值,我们需要订阅(subscribe)这个Observable:

var observable = Rx.Observable.create(function (observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  setTimeout(() => {
    observer.next(4);
    observer.complete();
  }, 1000);
});

console.log('just before subscribe');
observable.subscribe({
  next: x => console.log('got value ' + x),
  error: err => console.error('something wrong occurred: ' + err),
  complete: () => console.log('done'),
});
console.log('just after subscribe');

程序执行后,将在控制台输出如下结果:


just before subscribe
got value 1
got value 2
got value 3
just after subscribe
got value 4
done

拉取(Pull) V.S. 推送(Push)

拉取推送是数据生产者和数据消费者之间通信的两种不同机制。

何为拉取? 在拉取系统中,总是由消费者决定何时从生产者那里获得数据。生产者对数据传递给消费者的时间毫无感知(被动的生产者,主动的消费者)。

JavaScript函数是典型的拉取系统:函数是数据的生产者,对函数进行调用的代码(消费者)从函数调用后的返回值中拉取单值进行消费。

// 函数是数据的生产者
let getLuckyNumber = function() {
    return 7;
};

/* let代码段是数据的消费者,
 * getLuckyNumber对调用时间毫无感知。 
 */
let luckNumber = getLuckyNumber();

ES2015 引入了的 生成器函数 | 遍历器 (function*)同样是基于拉取的系统: 调用 iterator.next()的代码段是消费者,它可以从生成器函数中拉取多个值。

function* getLessThanTen() {
  var i = 0;
  while(i < 11) {
    yield i++;
  }
}

// 生产者
let iterator = getLessThanTen();

// 消费者
iterator.next(); // Object {value: 0, done: false}
iterator.next(); // Object {value: 1, done: false}
MagicQ生产者消费者
拉取被动: 在被请求时产生数据主动: 决定何时请求数据
推送主动: 控制数据的产生逻辑被动: 获得数据后进行响应

何为推送? 在推送系统中生产者决定何时向消费者传递数据,消费者对何时收到数据毫无感知(被动的消费者)。

现代JavaScript中Promise是典型的推送系统。作为数据生产者的Promise通过resolve()向数据消费者——回调函数传递数据:与函数不同,Promise决定向回调函数推送值的时间。

RxJS在 JavaScript 中引入了Observable(可观察对象)这个新的推送系统。Observable是多数据值的生产者,向Observer(被动的消费者)推送数据。

  • 函数 调用后同步计算并返回单一值

  • 生成器函数 | 遍历器 遍历过程中同步计算并返回0个到无穷多个值

  • Promise 异步执行中返回或者不返回单一值

  • Observable 同步或者异步计算并返回0个到无穷多个值

Observable 是函数概念的拓展

Observable既不像EventEmitter,也不像是Promise。Observable 中的 Subject 进行多路推送时与 EventEmitter 行为上有些类似,但是实际上Observable与EventEmitter并不相同。

Observable 更像是一个不需要传入参数的函数,它拓展了函数的概念使得它可以返回多个值。

看看下面的例子:

function foo() {
  console.log('Hello');
  return 42;
}

var x = foo.call(); // same as foo()
console.log(x);
var y = foo.call(); // same as foo()
console.log(y);

输出结果如下:


"Hello"
42
"Hello"
42

通过Observable可以实现同样的行为:

var foo = Rx.Observable.create(function (observer) {
  console.log('Hello');
  observer.next(42);
});

foo.subscribe(function (x) {
  console.log(x);
});
foo.subscribe(function (y) {
  console.log(y);
});

输出结果相同:


"Hello"
42
"Hello"
42

不论Observable还是函数都是在运行时进行求值计算的。如果不调用函数,console.log('Hello')就不会执行;如果如果不subscribe(订阅)Observable,console.log('Hello')也不会执行。此外,调用或者订阅都是独立的:两次调用产生两个独立的作用域,两次订阅同样会产生两个独立的作用域。EventEmitter总是在同一个作用域中,发射前也不会在意自己是否已经被订阅;Observable不会被共享而产生副作用,并且总是在被订阅时才执行。

订阅Observable与调用函数类似。

一些人认为Observable总是是异步的,这个观点并不正确,如果在控制台log函数中调用函数:

console.log('before');
console.log(foo.call());
console.log('after');

显然可以看到以下输出:


"before"
"Hello"
42
"after"

Observable的行为完全一样:

console.log('before');
foo.subscribe(function (x) {
  console.log(x);
});
console.log('after');

输出结果为:


"before"
"Hello"
42
"after"

订阅 foo完全是同步的,与函数的调用一样。

Observable可以异步或者同步地产生数据。

那Observable 与函数的不同之处在哪里? Observable可以在一个时间过程中‘返回’多个值,而函数却不能。在函数中你不可以这么做:

function foo() {
  console.log('Hello');
  return 42;
  return 100; // 这个语句永远不会被执行。
}

虽然函数只能有一个返回值,但是在Observable中你完全可以这么做:

var foo = Rx.Observable.create(function (observer) {
  console.log('Hello');
  observer.next(42);
  observer.next(100); // 返回另一个值
  observer.next(200); // 返回另一个值
});

console.log('before');
foo.subscribe(function (x) {
  console.log(x);
});
console.log('after');

输出结果如下:


"before"
"Hello"
42
100
200
"after"

你甚至可以异步地返回值:

var foo = Rx.Observable.create(function (observer) {
  console.log('Hello');
  observer.next(42);
  observer.next(100);
  observer.next(200);
  setTimeout(() => {
    observer.next(300); // happens asynchronously
  }, 1000);
});

console.log('before');
foo.subscribe(function (x) {
  console.log(x);
});
console.log('after');

输出结果:


"before"
"Hello"
42
100
200
"after"
300

结论:

  • func.call() 意味着“同步地给我一个值”

  • observable.subscribe() 意味着“不管是同步或者异步,给我一些值”

Observable 剖析

通过使用 Rx.Observable.create 或者是创建操作符创建一个Observable; Observable 被 Observer(观察者) 订阅; 在执行时 向观察者发送next / error / complete 通知;同时执行过程可以被 终止
Observable 类型的实例具备了以上四个方面的特性,与其他类型如:Observer 和 Subscription 紧密相关。

我们重点关注以下四个方面:

  • 创建

  • 订阅

  • 执行

  • 终止

创建

Rx.Observable.createObservable 构造函数的别名,接受一个参数: subscribe函数。

以下例子会创建一个Observable,每一秒钟向其订阅者发射一个'hi' 字符串。

var observable = Rx.Observable.create(function subscribe(observer) {
  var id = setInterval(() => {
    observer.next('hi')
  }, 1000);
});

除了使用create创建Observable,我们通常还使用创建操作符, 如 offrominterval, 等来创建Observable。

上面例子中,subscribe函数是定义Observable最重要的部分。我们接下来了解订阅的含义。

订阅

上面例子中的observable 可以以如下方式 订阅

observable.subscribe(x => console.log(x));

observable.subscribeObservable.create(function subscribe(observer) {...})中的subscribe 同名并非巧合。虽然在Rx中它们不是同一个对象,但是在工程中,我们可以在概念上视两者为等价物。

调用subscribe的观察者并不会共享同一个Observable。观察者调用observable.subscribe 时,Observable.create(function subscribe(observer) {...})中的subscribe会在调用它的观察者作用域中执行。每一次observable.subscribe的调用,都是彼此独立的。

订阅Observable如同调用函数,需要提供相应的回调方法。

订阅机制与处理事件的addEventListener / removeEventListenerAPI完全不同。通过observable.subscribe,观察者并不需要在Observable中进行注册,Observable也不需要维护订阅者的列表。

订阅后便进入了Observable的执行阶段,在执行阶段值和事件将会被传递给观察者供其消费。

执行

只有在被订阅之后Observable才会执行,执行的逻辑在Observable.create(function subscribe(observer) {...})中描述,执行后将会在特定时间段内,同步或者异步地成产多个数据值。

Observable在执行过程中,可以推送三种类型的值:

  • "Next" 通知: 实际产生的数据,包括数字、字符串、对象等

  • "Error" 通知:一个JavaScript错误或者异常

  • "Complete" 通知:一个不带有值的事件

“Next” 通知是最重要和常用的类型:表示事件传递给观察者的数据。错误和完成通知仅会在执行阶段推送其一,并不会同时推送错误和完成通知。

通过所谓的“Observable语法”或者“契约”可以最好地表达这个规则,“Observable语法”借助于正则表达式:

next*(error|complete)?

在Observable的执行过程中,0个或者多个“Next”通知会被推送。在错误或者完成通知被推送后,Observable不会再推送任何其他通知。

下面代码展示了Observable 在执行过程中推送3个“Next” 通知然后结束:

var observable = Rx.Observable.create(function subscribe(observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();
});

Observable 严格遵守 Observable 契约,后面值为4的“Next” 通知永远不会被推送:

var observable = Rx.Observable.create(function subscribe(observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();
  observer.next(4); // 由于违法契约,4不会被推送
});

使用try/catch块包裹 subscribe 代码是一个很赞的想法,如果捕获了异常,可以推送错误通知:

var observable = Rx.Observable.create(function subscribe(observer) {
  try {
    observer.next(1);
    observer.next(2);
    observer.next(3);
    observer.complete();
  } catch (err) {
    observer.error(err); // 捕获异常后推送错误通知
  }
});

终止

Observable的执行可能是无限的,作为观察者需要主动中断执行:我们需要特定的API去终止执行过程。因为特定的观察者都有特定的执行过程,一旦观察者获得想要的数据后就需要终止执行过程以免带来计算时对内存资源的浪费。

observable.subscribe被调用时,观察者会与其执行作用域绑定,同时返回一个Subscription类型的对象:

var subscription = observable.subscribe(x => console.log(x));

Subscription对象表示执行过程,通过极简的API,你可以终止执行过程。详情请阅读Subscription 相关文档。通过调用subscription.unsubscribe() 你可以终止执行过程:

var observable = Rx.Observable.from([10, 20, 30]);
var subscription = observable.subscribe(x => console.log(x));
// Later:
subscription.unsubscribe();

在Observable被订阅后,代表执行过程的Subscription 对象将被返回。对其调用unsubscribe()就可以终止执行。

每一个Observable都需要在 create()的创建过程中定义终止的逻辑。在function subscribe()中返回自定义的unsubscribe就可以实现。

下面的例子说明了如何在终止后释放setInterval的句柄:

var observable = Rx.Observable.create(function subscribe(observer) {
  // 获得定时函数的句柄
  var intervalID = setInterval(() => {
    observer.next('hi');
  }, 1000);
  
  // 提供终止方法释放定时函数的句柄
  return function unsubscribe() {
    clearInterval(intervalID);
  };
});

类似于observable.subscribeObservable.create(function subscribe() {...})的关系,我们在subscribe中返回的 unsubscribe 也与subscription.unsubscribe在概念上等价。事实上,如果我们除去Rx的包装,纯粹的JavaScript代码简单清晰:

function subscribe(observer) {
  var intervalID = setInterval(() => {
    observer.next('hi');
  }, 1000);
  
  return function unsubscribe() {
    clearInterval(intervalID);
  };
}

var unsubscribe = subscribe({next: (x) => console.log(x)});

// 一段时间后:
unsubscribe(); // 终止

使用Observable、 Observer 和 Subscription这些概念的原因是,我们可以在Observable 契约之下安全、兼容地调用操作符。

查看原文

赞 23 收藏 64 评论 3

mumuzhenzhen 赞了文章 · 2016-02-02

GitHub Archive分析 - 2015最受瞩目的项目们

你应该见过不少对GitHub上等等开源项目进行的分析文章。据说国外甚至有人靠分析Github上的项目解决了毕业论文……(要是我的毕业论文也能这么解决就好了XD) 借助于Google Big Query和来自于GitHub Archive的数据归档,对GitHub上的项目进行简单的数据分析并不困难。下文我将试图分析2015年GitHub上被收藏(starred)最多的5000个项目,进而求出2015年最受瞩目的编程语言排行。

GitHub Archive这个网站通过GitHub的API,定期抓取GitHub的事件数据,并上传到Google Big Query,供热心群众分析。它在官网上介绍了如何用Google Big Query来分析数据

Google Big Query允许用户创建项目,上传数据归档,并通过SQL来查询这些数据。下图就是GitHub Archive在Big Query上,存储着2016-02-01这一天数据的项目

github-star-20160201

我们可以看到它的schema定义,基本上类似于GitHub事件API返回的数据格式。其中一些重要的字段如下:

  • type 事件类型。比如jeresig创建了项目processing-js,那么这个事件的类型就是CreateEvent。你可以上GitHub事件相关的文档里查到各种事件对应的类型。

  • repo.name 项目名,在上面例子中,是jeresig/processing-js

  • actor.login 该事件的主人公,在上面例子中,是jeresig

于是我们小试牛刀,运行下面的Query,查询jeresig去年一年push的次数:(这里用TABLE_DATE_RANGE函数用于匹配从githubarchive:day.events_20150101githubarchive:day.evnets_20151231所有的表)

SELECT COUNT(*) FROM 
    TABLE_DATE_RANGE([githubarchive:day.events_], 
        TIMESTAMP('2015-01-01'), 
        TIMESTAMP('2015-12-31'))
    WHERE type = 'PushEvent' and actor.login = 'jeresig'

得出的结果为

github-jeresig-push

稍微复杂点,运行下面的Query,查询jeresig去年一年内提了Pull Request的项目和各自提的次数:

SELECT COUNT(*) AS num, repo.name FROM 
    TABLE_DATE_RANGE([githubarchive:day.events_], 
        TIMESTAMP('2015-01-01'), 
        TIMESTAMP('2015-12-31'))
    WHERE type = 'PullRequestEvent' and actor.login = 'jeresig'
    GROUP BY repo.name ORDER BY num DESC

把关注点从人转向项目,让我们回归主题,查询去年一年间最受瞩目的那些项目们,并粗略地分析下它们。通过查GitHub的API文档,我们知道用户star一个项目时会触发一个WatchEvent(对的,就是WatchEvent)。所以我们可以遍历下去年所有的WatchEvent事件,按repo_name进行分组,计算每组的数目,并截取前5000名。写出来的Query如下:

SELECT COUNT(*) AS star, repo.name FROM 
      TABLE_DATE_RANGE([githubarchive:day.events_], 
        TIMESTAMP('2015-01-01'), 
        TIMESTAMP('2015-12-31'))
      WHERE type = 'WatchEvent' 
      GROUP BY repo.name ORDER BY star DESC LIMIT 5000

我把Big Query查询到的数据保存成github-star-2015.csv,分享到百度网盘上,有需要的人可以下载:http://pan.baidu.com/s/1dElWKHr

现在,我宣布,2015年最受瞩目的项目前十的名单新鲜出炉啦!(请脑补最应景的BGM)

~/doc head -11 github-star-2015.csv 
star,repo_name
38318,FreeCodeCamp/FreeCodeCamp
25861,facebook/react-native
25479,apple/swift
24344,sindresorhus/awesome
22917,facebook/react
22093,jlevy/the-art-of-command-line
20401,NARKOZ/hacker-scripts
19736,twbs/bootstrap
17885,google/material-design-lite
17568,airbnb/javascript

初看这份名单,你会发现去年是React年。前十的名单里,React就占了两。你也许会想起,swift在这一年里开源了(果粉的力量真强大,同样也是去年搬到GitHub的golang就挤不进前十名~)。仔细分析下各个项目,你会发现,涨star最快的项目有不少是代码无关的项目。比如第一名FreeCodeCamp,第四名awesome,第六名the-art-of-command-life,第十名airbnb/javascript(airbnb内部的javascript编程规范)等等,都是如此。另外,一个显著的发现是,前十名中,前端的项目占了三个,这还不计算半个前端项目的react-native和前端编码规范的airbnb/javascript。前端项目三分天下有其一,准确来讲,已经接近撑起半边天了。如果说前几年的GitHub是Ruby开发者的GitHub,那么如今的GitHub无疑是前端的GitHub。

借助GitHub的API,我们来看看前5000名项目的编程语言使用情况。题外话,如果GitHub提供了项目所有者可以给自己的项目打标签,那么我们除了分析下编程语言,还可以分析下更多方面的内容,比如去年哪一方面的项目最受瞩目。要是有机会给GitHub产品部门提意见,我一定会写上这一点。不过目前就只能分析分析下编程语言了。

由于GitHub设置了API调用限制,我们需要先注册应用,获取对应的client_idclient_secret,才能有足够的调用数量。注册地址见 https://github.com/settings/applications/new,里面的数据不需要审核,我当时是乱填一通的=_=

GitHub提供了查询某个项目的编程语言使用情况的API,借此写出了下面的脚本,统计前5000个项目中编程语言的占比:

#!/usr/bin/env ruby
# encoding: UTF-8

require 'json'
require 'net/http'
require 'set'

def get_language_ingredient(repo)
  url = "https://api.github.com/repos/#{repo}/languages"
  # 请改成你自己的 client_id 和 client_secret
  client_id = '05500dd030f3a5690d8e'
  client_secret = 'b8ba63550e07dd3bf7b5b467824ee9ced1c61192'
  url += "?client_id=#{client_id}&client_secret=#{client_secret}"
  res = Net::HTTP.get_response(URI(url))
  if res.code == '200'
    JSON.parse(res.body)
  else
    puts res.msg
    {}
  end
end

def sum_star_number_per_language(result, repo, star)
  ingredient = get_language_ingredient(repo)
  puts "The language ingredient of #{repo}: #{ingredient}"
  return if ingredient.length == 0
  sum = ingredient.reduce(0){|total, pair| total += pair[1]}
  # 去掉占比不到1%的语言
  ingredient.reject!{|_, bytes| bytes < sum * 0.01}
  # 如果剩下的语言正好是 JavaScript/CSS/HTML,
  # 则表示它很可能是代码无关的项目,直接忽略掉
  if Set.new(ingredient.keys) == Set.new(['JavaScript', 'CSS', 'HTML'])
    # CSS框架除外。考虑到有些静态网站也是CSS比JS多,这里要求CSS比JS和HTML多得多。
    # 下面的公式随手写的,没有什么特殊意义,只是强调CSS一定要占大多数。
    unless ingredient['CSS'] > 2 * ingredient['JavaScript'] + ingredient['HTML']
      return
    end
  end
  # 剩下的按比例分了star数
  sum = ingredient.reduce(0){|total, pair| total += pair[1]}
  ingredient.each_pair do |language, bytes|
    result[language] = result.fetch(language, 0) + (bytes.fdiv(sum) * star).round
  end
end

def output_star_number_per_language(result)
  sum = result.reduce(0){|total, pair| total += pair[1]}
  output = ''
  result.sort {|a, b| b[1] <=> a[1]}.each_with_index do |e, idx|
    output += format("%-4d %-40s %.2f%\n", idx+1, e[0], e[1].fdiv(sum).round(4) * 100)
  end
  output + "\n"
end

result = {}
output = {}
checkpoints = [50, 100, 200, 500, 1000, 2000, 5000]
f = File.new('github-star-2015.csv').each
f.next
f.each_with_index do |line, idx|
  step = idx + 1
  star, repo = line[0...-1].split(',')
  star = star.to_i
  puts format("%-4d %-40s %d", step, repo, star)
  sum_star_number_per_language(result, repo, star)
  puts "The result after #{repo}: #{result}\n\n"
  if checkpoints.include?(step)
    output[step] = output_star_number_per_language(result)
    puts "first #{step}"
    puts output[step]
  end
end
puts ''

output.each_pair do |step, rank|
    puts "first #{step}"
    puts rank
end

注意两点:

  1. 获取了每个项目的语言成分后,去掉占比不到1%的语言,剩下的语言按比例分掉star数。之所以不直接把star分到占比最大的语言,是因为有些项目用到多种语言且比例相当,如facebook/react-native.

  2. 去掉1%之后,如果剩下的语言正好是JavaScript,CSS和HTML,那么该项目很可能是代码无关的(比如一个收集各类资料的静态网站)。显然大家关注它的缘故跟任何一门编程语言无关,所以不列入统计之中。但是考虑到CSS框架也正好会有这三门语言,所以当CSS占比较高时可以豁免。

下面是最终的结果:

...
first 5000
1    JavaScript                               26.38%
2    Java                                     13.33%
3    Objective-C                              8.21%
4    Python                                   8.09%
5    Go                                       5.44%
6    Swift                                    4.63%
7    C                                        3.88%
8    HTML                                     3.84%
9    C++                                      3.82%
10   Ruby                                     3.60%
11   CSS                                      3.28%
12   PHP                                      2.99%
13   Shell                                    2.67%
14   CoffeeScript                             1.51%
15   C#                                       1.19%
16   VimL                                     0.90%
17   TypeScript                               0.63%
18   Scala                                    0.59%
19   Lua                                      0.46%
20   Clojure                                  0.44%
21   Rust                                     0.39%
22   Haskell                                  0.28%
23   Makefile                                 0.22%
24   Objective-C++                            0.21%
25   Emacs Lisp                               0.21%
26   Jupyter Notebook                         0.21%
27   Perl                                     0.20%
28   TeX                                      0.17%
29   Elixir                                   0.16%
30   Groff                                    0.16%
31   Groovy                                   0.14%
32   R                                        0.12%
33   OCaml                                    0.11%
34   PowerShell                               0.10%
35   Batchfile                                0.10%
36   ApacheConf                               0.08%
37   Erlang                                   0.08%
38   Cucumber                                 0.08%
39   Assembly                                 0.07%
40   Crystal                                  0.06%
41   PureBasic                                0.05%
42   QML                                      0.05%
43   Visual Basic                             0.04%
44   PLpgSQL                                  0.04%
45   Tcl                                      0.04%
46   Dart                                     0.04%
47   Vue                                      0.04%
48   CMake                                    0.03%
49   PLSQL                                    0.03%
50   XSLT                                     0.03%
...

github-star-cake

一个显而易见的结论:GitHub上不小一部分的热门项目,是由JavaScript写的。JavaScript一门语言的占比,比第二名和第三名加起来还多出个第六名。这还不包括第十四名的CoffeeScript和第十七名的TypeScript(它们可以编译成JavaScript,严格来说也是JavaScript大家族的一员)。
另外从每个checkpoint时输出的数据可见,排名靠前的项目中,JavaScript占的比例要比全部项目中的高。如果我们选择的样本变小,JavaScript的占比还会升高(都稳拿第一名,排名就不可能升高了)。

github-star-js-trending

另一个结论是,Go(第五名)和Swift(第六名)这两门语言正处于快速发展的时期。虽然实际应用的情况不如前十名中其它语言广泛,但是从star数中可见,开发者们非常看好这两门语言,关注了许多这方面的项目,同时用这两门语言编写的高质量项目也越来越多。

前十名中其它语言的排名倒是一点也不出乎意料。Java和Objective-C分居榜眼和探花。剩下几位自然包括了C/C++/Python等等。令人意外的是,C#(第十五名)居然没能排进前十名。按理说,C#的使用量肯定能排在前十。也许C#生态圈里面主要使用的都是微软的商业产品?

最后,我想感谢GitHub Archieve提供的数据归档,没有这些数据就没有本篇分析。

查看原文

赞 4 收藏 26 评论 2

mumuzhenzhen 发布了文章 · 2016-01-22

RxJS API解析(四)

Rx*(Observable.combineLatest)方法

方法定义

Rx.Observable.combineLatest(...args, [resultSelector])

作用

通过处理函数总是将指定的可观察对象序列最新发射的值合并为一个可观察对象

参数

  1. args(arguments | Array): 一系列可观察对象或可观察对象的数组。

  2. [resultSelector](Function): 在所有可观察对象都发射值后调用的处理函数

返回值

(Observable): 由传入的可观察序列经过处理函数合并后的结果组成的可观察序列。

宝珠图

combineLatest

Observable.combineLastest()函数,总是合并序列中最新发射的值。宝珠图中的颜色球发射颜色,空白的图形发射待染色图形,处理函数对待染色对象进行染色:总是用户最新发射的颜色或者对最新发射的待染色对象

假设颜色序列仅发射了第一个宝珠浅紫色且后续不再发射,那么结果街将会是一个由浅紫色组成的染色后对象的序列。

使用官方可拖动宝珠图,可以帮助理解,拖动序列中的宝珠,观察输出序列的变化。

实例

var colors = ["紫色","黄色","蓝色","黑色"];
var shapes = ["小星星","圆形","三角形","正方形","心形","五边形"];
var source1 = Rx.Observable.interval(3000)
  .map(()=>colors.pop());
var source2 = Rx.Observable.interval(2000)
  .map(()=>shapes.pop());

var combined = Rx.Observable.combineLatest(source1, source2, function(x, y){
  return x + "的" + y;
}).take(8);

combined.subscribe((shaped)=>console.log(shaped));

实例模拟第一个宝珠图点击进入可运行实例。其中列1发射颜色值,序列2发射形状。结果输出染色后的形状:

"黑色的五边形"
"黑色的心形"
"蓝色的心形"
"蓝色的正方形"
"蓝色的三角形"
"黄色的三角形"
"黄色的圆形"
"紫色的圆形"

还有一个非常好的实例在前面的文章中,是combineLatest()在缓存数据方面的应用,如果你想深入理解combineLatest()不妨看一下。

题外话

写这个专题的时候,对Rx的抽象能力赞叹不已。

大家通常把编写一个框架的工作称作“造轮子”。
“轮子”是一个针对某一类问题的解决方案,通常是由于反复解决某一个工程问题而产生的。某种程度上,轮子可以一劳永逸,同时轮子的使用可以大大地提高生产的效率(试想想你在使用如Rails这类有 ORM特性框架时的感受)。

Rx似乎从另一个方面而不是实际问题进行抽象——数学,是一个函数式编程模式。从数学而不是工程作为起点,创造的工具的威力_可能_更强大,但是学习成本(使用成本)_可能_会更高。

任何程序设计语言在讲解递归特性时,基本都会举汉诺塔斐波拉契数列的例子。没错,请你对比一下斐波拉契数列combineLatest()定义的相似之处:

def fibo(i):  
   if i==0 or i==1:  
       return 1  
   else:  
       return fibo(i-1)+fibo(i-2) 

Oops!递归完成后产生值的过程就是combineLatest()的过程。

在学习Rx的操作符时,请反复地理解操作符的作用、限制。最好的理解方法是构建一个场景。

combineLatest()中,我们不妨将场景限定为拥有两个可观察对象的可观察序列,并且对象A总是较低频率地发射新值,而对象B比较频繁地发射:

A ----*----------------*---------->
B -----@---@---@---@---@----@----->

那么对象A在实际中可能是什么?缓存后的http请求后的数据、异步获取的配置文件...
对象B自然可以是,与服务器的实时同步、用户上传图片的实时上传、用户在列表中执行的翻页操作...

前面的文章中缓存Github用户的就是上面提到的场景。

剧终

查看原文

赞 6 收藏 21 评论 9

mumuzhenzhen 赞了文章 · 2016-01-21

前端模块化

前端模块化系列(一):网站需要模块化的原因

由于我最近在研究前端各种各样的模块化系统,所以就翻译了一篇来自webpack官网的文章,总的来说作者写的还是相当不错的。这样在自己学习的同时也可以与大家共同学习~~~

在今天的网站正在逐步的向web apps转变。

  • 单个页面中越来越多的Javascript。

  • 在现代浏览器中你可以做越来越多的功能。

  • 少量的全页面刷新,以至于单个页面中有更多的代码。

正因为这些原因造成越来越多的代码镶嵌在浏览器端中。

这样一个大的代码仓库(code base)急需做出相应的管理。正好,模块化系统提供了这些功能分割你的代码仓库,把它们分割为一个个的模块。

各个模块系统的风格

眼下对于如何定义依赖项和暴露接口有很多的标准:

  • <script>标签风格(ps:不使用模块系统)。

  • CommonJs

  • AMD和它的一些衍生物

  • ES6模块

  • 更多。。。

在下面,我们会一次简介这些模块化系统之间的好处以及坏处。

<script>标签风格

如果你没有使用模块化系统,那么你只能用这种方式来处理你的模块化代码了。

<script data-original="module1.js"></script>
<script data-original="module2.js"></script>
<script data-original="libraryA.js"></script>
<script data-original="module3.js"></script>

每个模块向外暴露一个接口给全局对象,即window对象。模块就可以通过全局对象访问依赖项向外暴露的接口。

通常存在的问题

  • 全局对象中的变量冲突。

  • 按需加载的问题。

  • 开发者需要手动解析模块或者库的依赖项。

  • 在特别大的项目中,这个现象会变得越来越严重,越来越难以管理。

CmmonJs:同步require

这种方式使用了一个同步的require方法去加载依赖项并且返回一个向外暴露的接口。一个模块可以通过给exports添加属性或者给module.exports设置固定值来指定向外暴露的值。

require("module");
require("../file.js");
exports.doStuff = function() {};
module.exports = someValue;

这个只是被node.js使用在server端。

优点:

  • server端的模块可以被复用。

  • 有许多现成的模块以供使用(npm)。

  • 非常的简单易用。

缺点:

  • 因为网络请求都是异步的。所以阻塞式的调用在网路中支持的不是很好。

  • 多个模块之间并能同时并行的加载进来。

实现

  1. node.js - server端。

  2. browserify

  3. modules-webmake-编译到一个bundle里

  4. wreq-客户端

AMD:异步的require

Asynchronous Module Definition:其他的模块化系统(对于浏览器来说)对于同步require(CommonJs)都有或多或少的问题。接下来我们介绍一个异步require的模块化系统(定义模块和暴露值的另外一种实现方式)。

require(["module", "../file"], function(module, file) {
    /* ... */ 
});
define("mymodule", ["dep1", "dep2"], function(d1, d2) {
  return someExportedValue;
});

优点:

  • 十分适合在现下网络的异步请求。

  • 支持多个模块的同时并行加载。

缺点:

  • 写码开销。读写十分的困难。

  • 看上去像是一种解决方案。

实现

  1. require.js - client端。

  2. curl - client端。

获取更多相关CommonJSAMD的知识。

ES6模块化

EcmaScript6针对JavaScript添加了一些新的语言结构,其中就包括模块化系统。

import "jquery";
export function doStuff() {}
module "localModule" {}

优点:

  • 很容易的静态模块解析。

  • 未来不久将要作为ES标准来推行。

缺点:

  • 让大部分的浏览器支持这个功能还需要一段时间。

  • 这种风格的模块太少了,让人不适应。

咱谁也不偏向谁的解决方案

给开发者关于模块化风格的选择权。在现有代码可以正常运行的前提下,可以很容易地添加自定义模块。关键还要看使用某个模块系统对于现在的系统影响大不大。

模块的传输方式

模块是应该在client端被执行的,所以这就需要它们通过http协议让server端向浏览器端传输。

现在有两种方式来处理如何传输模块

  • 一个请求一个模块。

  • 所有的模块都在一个请求里。

这两种方式都有人在用,不过这两种都是次优的:

  • 一个请求一个模块

    • 优点:仅仅传输被请求的那个模块。

    • 缺点:

      • 许多的请求意为着网络开销也很大。

      • 程序启动变慢,因为请求会延迟。

  • 所有的模块都在一个请求里

    • 优点:很小的请求开销,少量的延迟。

    • 缺点:不需要(还没有)被请求的模块也被传输过来了。

分块传输方式

相对于上面两种方式都太死板了,所以灵活点的模块传输会不会更好哪?因为向两个极端之间的折中妥协通常都是最好的。

虽然我们需要把所有的模块都编辑,把模块安装功能和是否公用拆分为多个较小的代码块。

我们有很多的小量请求。把那些不需要一开始就请求,或者是需要按需加载的模块来进行分块传输。浏览器最开始的访问请求并不用包含你的所有代码库(code base)。这样的话,server返回的数据大小也就会变的很小。有效解决了上面两种方式所出现的问题。

至于如何分隔模块应该是开发者根据功能格式加载顺序继承关系分割为一个一个单独的部分。

这样的话。就算再多的代码也可以解决掉了。

注意:拆分的粒度问题,可复用问题,效率问题。如何这些问题处理的不好,就有可能出现不想要的后果。

获取更多相关代码块如何分割的知识。

为什么仅仅只是JavaScript?

不知道大家有没有发现,为什么一个模块化系统仅仅只帮助开发者处理JavaScript哪?除此之外还需要很多的静态资源需要我们去处理呀!

  • stylesheets(样式表)

  • images(图片)

  • webfonts(web字体)

  • html for templating(html模版)

  • 等等...

当然,还有其他的资源:

  • coffeeScript -> javascript

  • less -> stylesheets

  • jade -> 经过javascript生成的html

  • 等等...

它们也应该向JavaScript一样可以被很容易的require到:

require("./style.css");
require("./style.less");
require("./template.jade");
require("./image.png");

模块静态解析

当编译所有模块的时候,为了协调异步环境下模块开发与性能间的矛盾,我们必须在工程阶段就具备依赖分析的能力,把具备依赖关系的资源进行打包。就算不打包,也希望像 F.I.S 那样有个记录依赖关系的map.json,可以照单抓药,一次性地把需要的依赖项加载下来。

策略

一个聪明的解析器将允许大多数现有的代码可以有效运行,不管开发者有没有使用模块化系统。即使开发人员做了一些奇怪的东西,它也会尝试找到最适合的解决方案。实在不行,也就只能抱歉咯!!!

翻译自:http://webpack.github.io/docs/motivation.html
翻译人:张亚涛

查看原文

赞 7 收藏 47 评论 0

mumuzhenzhen 发布了文章 · 2016-01-19

Angular2核心概念

原文地址: THE CORE CONCEPTS OF ANGULAR 2
作者:Victor Savkin是Angular 2项目最多的提交者。

amazing

这篇博客中,我们将介绍Angular 2的三个核心概念:组件化依赖注入绑定

最后更新:2016-01-04

免责声明

Angular 2仍然在开发之中,核心概念不会随之变化,但是相关API可能会随着项目的开发进程发生改变:如果发现博文中某些代码片段无法运行了,请告知我,我会进行更新。


开始构建APP吧

现在我们描述一下将要构建的应用:它包含了一个科技讲座的列表,你可以通过讲师speaker进行筛选,观看讲座或对讲座进行评分,应用Demo如下:

appWeWillBuild


组件

你需要定义UI、路由等一系列组件去构建一个Augular 2应用。一个应用中总是存在一个根(主)组件,根组件中包含了其他组件。简而言之,每一个Angular 2应用都有一棵对应的组件树。我们应用的组件树看起来是这样的:

ourApp'sComponentsTree

Application根组件Filters组件包含一个演讲者speaker姓名输入框和筛选按钮;TalkList是你在Demo中看到的讲座列表;TalkCmp是讲座列表中的一个元素(一个讲座)。

为了理解Angular 2中组件的构成,我们先研究一下TalkCmp

TalkCmp.ts:

@Component({
  selector: 'talk-cmp',
  directives: [FormattedRating, WatchButton, RateButton],
  templateUrl: 'talk_cmp.html'
})
class TalkCmp {
  @Input() talk: Talk;
  @Output() rate: EventEmitter;
  //...
}

talk_cmp.html

{{talk.title}}
{{talk.speaker}}
<formatted-rating [rating]="talk.rating"></formatted-rating>
<watch-button [talk]="talk"></watch-button>
<rate-button [talk]="talk"></rate-button>

INPUT & OUTPUT 属性

每一个组件都拥有inputoutput 属性,我们可以在组件中通过属性装饰符语法定义这些属性。

...
class TalkCmp {
  @Input() talk: Talk;
  @Output() rate: EventEmitter;
  //...
}
...

通过input属性,数据可以流入到组件中;通过output属性,数据可以从组件中流出。

flows in and out

Inputoutput 是组件提供的公共API,在应用中初始化组件时你可以使用它们。

<talk-cmp [talk]="someExp" (rate)="eventHandler($event.rating)"></talk-cmp>

通过属性绑定(使用方括号语法),你可以设置input属性的值;通过事件绑定,(使用圆括号语法),你可以绑定output属性。

每一个组件总是有一个模板与之对应,模板定义了一个组件在页面中的渲染方式。

@Component({
  selector: 'talk-cmp',
  directives: [FormattedRating, WatchButton, RateButton],
  templateUrl: 'talk_cmp.html'
})

talk_cmp.html

{{talk.title}}
{{talk.speaker}}
<formatted-rating [rating]="talk.rating"></formatted-rating>
<watch-button [talk]="talk"></watch-button>
<rate-button [talk]="talk"></rate-button>

为进行渲染,Angular需要事先知道:渲染中可以使用哪些directives?使用什么样的模板?你可以用templateUrl把模板文件定义在外部文件中,或者使用内联的方式像下面这样进行定义:

...
@Component({
  selector: 'talk-cmp',
  directives: [FormattedRating, WatchButton, RateButton],
  template: `
    {{talk.title}}
    {{talk.speaker}}
    <formatted-rating [rating]="talk.rating"></formatted-rating>
    <watch-button [talk]="talk"></watch-button>
    <rate-button [talk]="talk"></rate-button>
  `
})
...

生命周期

Angular 2为组件定义了完整的生命周期,你可以在组件各个生命周期中进行介入。在TalkCmp组件中,我们没有订阅其生命周期中的事件,这并不代表其他组件不能。下面例子中的组件会在input属性变化时收到通知。

@Component({selector: 'cares-about-changes'})
class CareAboutChanges {
  @Input() field1;
  @Input() field2;
  onChange(changes) {
    //..
  }
}

服务提供者

一个组件可以包含一系列服务提供者,其子组件也可以使用这些服务。

@Component({
  selector: 'conf-app',
  providers: [ConfAppBackend, Logger]
})
class TalksApp {
  //...
}

class TalksCmp {
  constructor(backend:ConfAppBackend) {
    //...
  }
}

上面的例子中,我们在根组件中声明了后端服务(即服务器通信)和日志服务,这样在应用中我们都可以使用这些服务。接下来talksCmp组件注入后端服务(因为ConfAppBackend根组件中做过声明)。
我们会在本文的第二部分详细地介绍依赖注入,这里我们只用了解:通过组件进行依赖注入的设置。

宿主元素

为了将Angular组件渲染成DOM树,需要将Angular组件与一个DOM元素相关联,我们把这样的DOM元素称为:宿主元素

组件可以与宿主元素进行如下方式的交互:

  • 监听宿主元素事件

  • 更改宿主元素属性

  • 调用宿主元素方法

下面这个组件中,通过HostListener监听宿主元素的输入事件,然后去除输入值两端的空格,并将其存储。Angular 会时时保持DOM元素与存储值的一致性。

@Component({selector: 'trimmed-input'})
class TrimmedInput {
  @HostBinding() value: string;

  @HostListener("input", "$event.target.value")
  onChange(updatedValue: string) {
    this.value = updatedValue.trim();
  }
}

请注意,上面的例子中我对DOM元素进行操作。Angular 2致力于提供更高层面的抽象接口:我们可以将Angular 2应用映射原生平台(比如iOS、Android)或者浏览器中

这个理念极为重要,因为:

  • 我们可以更为方便地重构应用(与DOM解耦)

  • 可以脱离DOM进行大多数单元测试。测试脚本会变得更加利于理解和编写,测试效率也会显著提升

  • 可以在web worker中运行Angular 2应用

  • 可以脱离浏览器环境,例如使用NativeScript可以在iOSAndroid平台运行Angular 2应用

但是有些时候,我们还是需要和DOM直接打交道。Angular 2 提供了这类接口,不过我们希望你能尽可能少地使用它们。

组件是定义完备的

组件由下面这些部分构成:

  • 组件知道如何与宿主元素进行交互

  • 组件知道如何对自身进行渲染

  • 组件可以进行依赖注入的设置

  • 组件有定义inputsoutputs属性的接口

这些部分让Angular 2元素具备了自我完备定义的能力,我们可以独立地初始化一个组件,因为组件是定义完备的

任意组件都可以bootstrap一个应用;任意组件都可以绑定在特定路由之上并渲染。任意组件可以被其他组件直接使用。虽然我们定义的接口更少了,的但是带来了高可复用性。

那么DIRECTIVES呢?

如果你熟悉Angular 1,你一定会问:“directives去哪里了?”

其实,directives一直都在。组件是最为重要的directives,但并不是唯一的directives。组件是具有模板的directive,你可以使用装饰器语法来定义没有模板的directive

componentsAndDirectives

小结

组件是构建 Angular 2应用的基础:

  • 它们有定义inputsoutputs属性的接口

  • 有完整的生命周期

  • 是定义完备的


依赖注入

现在讨论 Angular 的另一个重要基石——依赖注入。

依赖注入背后的思想很简单:如果一个组件依赖一项服务,那么组件不应该去直接生成这个服务实例。通过在构造方法constructor中注入,框架(指 Angular 2的 DI 框架)会把服务提供给你。面向接口编程而非实现进行编程,可以降低代码的耦合度,提高可测试性(比如mocking数据),带来诸多其他好处。

DI

Angular 2与生俱来拥有一个依赖注入模块(当然该模块可以脱离Angular 2与其他库结合使用)。我们试着从下面的组件了解如何进行依赖注入。这个组件会渲染讲座列表。

@Component({
  selector: 'talk-list',
  templateUrl: 'talks.html'
})
class TalkList {
  constructor() {
    //..获取数据
  }
}

talks.html

<h2>Talks:</h2>
<div *ngFor="#t of talks">
  {{t.name}}
</div>

让我们构造一个服务来提供模拟数据:

class TalksAppBackend {
  fetchTalks() {
    return [
      { name: 'Are we there yet?' },
      { name: 'The value of values' }
    ];
  }
}

我们如何调用这个服务?一种实现是:在我们的组件中创建一个服务对象的实例,并调用实例方法:

class TalkList {
  constructor() {
    var backend = new TalksAppBackend();
    this.talks = backend.fetchTalks();
  }
}

作为应用Demo来说,这么做没有问题。但是真实应用中这的确不是一个很好的解决方案。TalksAppBackend的作用不单单是返回一个讲座对象的数组,它同样需要通过http请求获得数据:在单元测试中,我们理应需要发起http请求。
问题在于:我们在TalkList中创建TalksAppBackend的实例造成了代码的耦合(从面向对象单一职责的原则看,TalkList不应该关心TalksAppBackend的具体实现)。

通过在构造方法中注入TalksAppBackend可以解决这个问题,注入的服务可以在测试中简单地替换,比如下面这样:

class TalkList {
  constructor(backend:TalksAppBackend) {
    this.talks = backend.fetchTalks();
  }
}

代码告知 AngularTalksList 依赖于 TalksAppBackend。我们同样需要告知Angular 如何创建 TalksAppBackend服务。通过在组件中加入providers属性可以完成这个工作。

@Component({
  selector: 'talk-list',
  templateUrl: 'talks.html',
  providers: [TalksAppBackend]
})
class TalkList {
  constructor(backend:TalksAppBackend) {
   this.talks = backend.fetchTalks();
  }
}

TalksAppBackend服务需要在TalkList或者它的祖先组件中进行声明。如果你习惯于 Angular 1 的编程风格,你可以在根组件中设置所有的providers。这样,所有的组件都可以直接使用这些服务了。

@Component({
  selector: 'talk-app',
  providers: [TalksAppBackend] // 在根组件中注册,之后所有应组件都可以直接注入这些服务。
})
class Application {
}

@Component({
  selector: 'talk-list'
})
class TalkList {
  constructor(backend:TalksAppBackend) {
   this.talks = backend.fetchTalks();
  }
}

统一的依赖注入接口

Angular 1Angular 2都有各自的依赖注入模块。在Angular 1中,框架提供了好几种依赖注入接口,有按照位置注入的(如 element),有按照名称注入的,有一点让人困惑。Angular 2提供了单一的依赖注入接口。所有依赖注入都在组件的构造方法中完成。

比如,下面的组件注入了TalksAppBackend(一般都是单例的),ElementRef(对于每一个组件都不同)。

class TalksList {
  constructor(elRef:ElementRef, backend:TalksAppBackend) {
  }
}

我们对于全局或者本地的依赖注入都使用同样的接口,即便是在一个组件中注入其他的组件。

class Component {
  constructor(ancestor:AncestorCmp) {
  }
}

小结

我们并不会在使用依赖注入后马上受益。但然后随着应用复杂度的增加,使用依赖注入会越来越重要。

依赖注入使得我们面向接口而不是实现进行编程,大大降低了代码的耦合性,提高了可测试性。同时Angular 2提供了统一的依赖注入接口。

属性绑定

Angular通过属性绑定自动同步组件树、模型和组件树对应的DOM结构,为了理解其重要性,我们再回顾一下第一节中的应用。

application

我们知道,用户在输入演讲者speaker后,会产生一颗组件树,以及对应的模型。假设模型是简单的Javascript对象,并且看起来是这个样子:

{
  filters: {
    speaker: "Rich Hickey",
  }
  talks: [
    {
      title: "Are we there yet?",
      speaker: "Rich Hickey",
      yourRating: null,
      avgRating: 9.0
    }
  ]
}

我们试着来改变模型。假设我看了这个演讲Are we there yet?并且觉得很赞,给了9.9的评分,模型会变为下面的结构。

{
  filters: {
    speaker: "Rich Hickey",
  }
  talks: [
    {
      title: "Are we there yet?",
      speaker: "Rich Hickey",
      yourRating: 9.9,
      avgRating: 9.0?
    }
  ]
}

如果我需要找到依赖于这个值(我的评分)的所有部分并且更新它们,过程繁琐且易出错。我们希望应用能够自动地映射值的改变,而属性绑定正好能够赋予我们。

虚拟机每一循环周期的末尾,Angular会检查组件树中的每一个组件,更确切地说,它将会检查每一个绑定的属性(所有[]{}),并且更新这些组件,在更新组件的同时,Angular也会依照组件树更新对应的DOM结构。

仅仅使用了属性绑定的input属性可以被更新。

ZONES

Angular 1中我们需要通过scope.$apply告知框架进行脏值检查。Angular 2 通过使用 Zone.js 进行脏值检查,这意味着在使用第三方库时,你再也不需要使用scope.$apply进行脏值检查了,全部交给框架就好了。

小结

Angular 2 通过属性绑定,同步组件树、模型和组件树对应的DOM结构。
Angular 2 使用 Zone.js 来获知进行同步的时间点。

总结

Directives特别是组件,是 Angular 2 中最重要的部分:它们是构建 Angular 2 应用的基础。组件时定义完备的,可以通过接口定义组件的inputsoutputs属性。组件通过私有API,来在组件各个生命周期产生钩子。组件可以与宿主元素进行交互。当组件需要依赖其他服务或组件时,Angular 提供了依赖注入。组件树,是 Angular 2的核心,使用属性绑定和Zone.js使得Angular 2的开发更加便捷。

在开始 Angular 2 之前请先充分地理解上面这些概念。当然实际工程中需要的知识远不止这些,这也是为什么我们在 Angular 2 核心的基础上又构建了一系列模块的原因,它们使得开发体验更加愉悦,这些模块包括:

  • 处理表单和输入的模块

  • http模块

  • 强大的路由模块

  • 对动画的支持的模块

  • 基于material设计风格的UI组件

  • 用以进行单元测试、端对端和性能测试的工具集

还有许多特性尚在开发之中,目前为止进展顺利。

拓展资料

查看原文

赞 3 收藏 64 评论 3

mumuzhenzhen 发布了文章 · 2016-01-17

RxJS API解析(三)

Rx* (Observable.catch)方法

方法定义

Rx.Observable.catch(...args)

作用

序列中可观察对象因为异常而被终止后,继续订阅序列中的其他可观察对象。

参数

args(Array | arguments): 可观察对象序列。

返回值

(Observable): 可观察对象序列中能够正确终止,不抛出异常的第一个可观察对象。

宝珠图

catch

实例

var obs1 = Rx.Observable.throw(new Error('error'));
var obs2 = Rx.Observable.return(42);

var source = Rx.Observable.catch(obs1, obs2);

var subscription = source.subscribe(
  x => console.log(`onNext: ${x}`),
  e => console.log(`onError: ${e}`),
  () => console.log('onCompleted'));

// onNext: 42

在订阅时, obs1抛出错误后,程序继续执行,转而输出没有异常的obs2,并输出obs2发射的值42。点击进入在线演示

题外话

服务可用性是指,服务提供者需要保证服务在任何时间、情况下正确地提供。比如联网的银行系统,用户在各个ATM终端进行提取现金等操作后,数据都会被及时同步和备份。当不可抗因素发生时,数据可以被尽快的通过备份恢复。通常这些解决方案被称为灾备处理

使用云服务,例如UcloudRedis服务,可以在同一个服务上看到两个不同地址访问地址,文档描述如下:

每个云内存存储实例都会提供两个IP进行访问。

这两个IP都可以对云内存存储实例进行访问,分布在不同的接入服务上,其作用在于,当其中一个IP无法正常访问时,仍有另一个IP可用,不会完全中止服务。

因此,应用程序可以增加一个容灾切换的逻辑处理:将访问的IP列表设置好,默认访问其中的一个IP,当该IP无法访问时,自动切换到另一个IP继续业务。

文档中提到了增强服务可用性的线索:总是提供一组相同的服务而不是一个服务,或者至少是相似的服务,服务调用后可以完成相同的业务逻辑。
这个策略也是负载均衡的基础,可以缓解单个服务提供者的压力,从用户角度看,又感知不到服务的差异性:比如 多个HTTP服务 、_读写分离的数据库_。

文末,举一个实例:假设你需要做一个APP,APP中用户在通过手机验证码验证后,才能登录账户。

verifyMobile

许多第三方服务提供商,都提供手机验证服务,比如_LeanCloud_,调用者像服务提供方发送POST请求,请求的body为用户手机号码。然后服务提供者,会将验证码发送到用户手机。用户在收到验证码后,通过表单,输入验证码,提交后,调用者再次向服务提供商发起POST请求,请求的body为用户输入的验证码然后等待服务提供商响应。

当然,某些情况下,服务提供商可能自己挂了,或者是不支持向某个号码所属的运营商提供服务;还有些情况下,用户的号码可能在某个服务提供商的黑名单中。比如:你的一个用户是 经常写竞品分析的产品经理 ,可能也许大概你的号码就在某个服务提供商的黑名单中。

我们往往要同时接入多个服务提供商的短信验证服务,保证用户能够正常通过我们的注册(登录)流程:

回到catch()函数,结合定义我们可以把一个提供商作为主要服务提供者,如果其不能提供服务(调用失败),我们可以选择第二家作为候选:

var service1 = Observable.create("服务提供商#1");
var service2 = Observable.create("服务提供商#1");

Observable.catch(service1, service2).subscribe({
    ()=>console.log('succeed'),
    ()=>console.log('所有验证服务均不可用')
    ()=>console.log('completed')
})

这样,用户能够收到验证码并成功验证的几率大大增加。

剧终

查看原文

赞 6 收藏 20 评论 2

mumuzhenzhen 赞了文章 · 2016-01-17

Angular $q 完全指南

如果想使用 $http 或者其他异步操作, 那 $q 是必须要掌握的概念啦. Let's get started!

如何理解$q, deferred object ?

形象的讲解angular中的$q与promise

假设有一个家具厂,而它有一个VIP客户张先生。

有一天张先生需要一个豪华衣柜,于是,他打电话给家具厂说我需要一个衣柜,回头做好了给我送来,这个操作就叫$q.defer,也就是延期,因为这个衣柜不是现在要的,所以张先生这是在发起一个可延期的请求。

同时,家具厂给他留下了一个回执号,并对他说:我们做好了会给您送过去,放心吧。这叫做promise,也就是承诺。

这样,这个defer算是正式创建了,于是他把这件事记录在自己的日记上,并且同时记录了回执号,这叫做deferred,也就是已延期事件。

现在,张先生就不用再去想着这件事了,该做什么做什么,这就是“异步”的含义。

假设家具厂在一周后做完了这个衣柜,并如约送到了张先生家(包邮哦,亲),这就叫做deferred.resolve(衣柜),也就是“已解决”。而这时候张先生只要签收一下这个(衣柜)参数就行了,当然,这个“邮包”中也不一定只有衣柜,还可以包含别的东西,比如厂家宣传资料、产品名录等。整个过程中轻松愉快,谁也没等谁,没有浪费任何时间。

假设家具厂在评估后发现这个规格的衣柜我们做不了,那么它就需要deferred.reject(理由),也就是“拒绝”。拒绝没有时间限制,可以发生在给出承诺之后的任何时候,甚至可能发生在快做完的时候。而且拒绝时候的参数也不仅仅限于理由,还可以包含一个道歉信,违约金之类的,总之,你想给他什么就给他什么,如果你觉得不会惹恼客户,那么不给也没关系。

假设家具厂发现,自己正好有一个符合张先生要求的存货,它就可以用$q.when(现有衣柜)来把这个承诺给张先生,这件事就立即被解决了,皆大欢喜,张先生可不在乎你是从头做的还是现有的成品,只会惊叹于你们的效率之高。

假设这个家具厂对客户格外的细心,它还可能通过deferred.notify(进展情况)给张先生发送进展情况的“通知”。

这样,整个异步流程就圆满完成,无论成功或者失败,张先生都没有往里面投入任何额外的时间成本。

好,我们再扩展一下这个故事:

张先生这次需要做一个桌子,三把椅子,一张席梦思,但是他不希望今天收到个桌子,明天收到个椅子,后天又得签收一次席梦思,而是希望家具厂做好了之后一次性送过来,但是他下单的时候又是分别下单的,那么他就可以重新跟家具厂要一个包含上述三个承诺的新承诺,这就是$q.all(桌子承诺,椅子承诺,席梦思承诺),

这样,他就不用再关注以前的三个承诺了,直接等待这个新的承诺完成,到时候只要一次性签收了前面的这些承诺就行了。

如何创建 promise -1

$q 支持两种写法, 第一种是类似于ES6标准构造函数写法

$q(function resolver (resolve, reject) {})

注意:

+ ES6 写法并不支持 progress/notify 的回调函数
+ 在构造函数中抛异常也并不会显式的reject the promise
// var iWantResolve = false;
var iWantResolve = true;

function es6promise() {
    return $q(function (resolve, reject) {
        $timeout(function () {
            if (iWantResolve) {
                resolve("es6promise resolved");
            } else {
                reject("es6promise reject");
            }
        }, 1000)
    })
}

promise 的方法

  • promise.then(successCb, errCb, notifyCb)

  • 其中successCb 将在 promise resolve 后被调用, errCb 将在 promise reject 后被调

  • notifyCb 将在 deferred.notify 后被调用, 可以多次调用

  • promise.catch == promise.then(null, errCb), 用于处理之前没有被处理的 rejected promise

  • promise.finally 将最后被调用, 一般用于资源释放的清理操作

es6promise()
    .then(function (data) {
        console.log(data);
    })
    .catch(function (err) {
        console.log(err);
    });

// if(iWantResolve == true) output: es6promise resolved
// if(iWantResolve = false) output: es6promise reject

如何创建 promise -2

第二种是类似于 commonJS 的写法 $q.deferred()

function commonJsPromise() {
    var deferred = $q.defer();
    $timeout(function () {
        deferred.notify("commonJS notify");
        if (iWantResolve) {
            deferred.resolve("commonJS resolved");
        } else {
            deferred.reject("commonJS reject");
        }

    }, 500);

    return deferred.promise;
}

commonJsPromise()
    .then(function /** success callback**/(data) {
        console.log(data);

    }, function /** error callback **/ (err) {
        console.log(err);
    }, function /** progress callback **/ (update) {
        console.log(update);
    });
 
// if(iWantResolve == true) output: commonJS notify commonJS resolved
// if(iWantResolve = false) output: commonJS notify commonJS reject

$q.all

  • $q.all([promise1, promise1]) 接受一个包含若干个 promise 的数组,

  • 等所有的 promise resolve 后, 其本身 resolve 包含上述结果的数组 [data1, data2]

  • 如果上述 promise 有一个 reject, 那么$q.all() 会把这个 rejected promise 作为其 rejected promise (只有一个哦)

  • progress/notify 的 callback 并没有用

$q.all([es6promise(), commonJsPromise()])
    .then(function (dataArr) {
        console.log("$q.all: ", dataArr);
    }, function (err) {
        console.log("$q.all: ", err)
    }, function /** unnecessary **/ (update) {
        console.log("$q.all", update);
    });
// if(iWantResolve == true) output: $q.all:  ["es6promise resolved", "commonJS resolved"]
// if(iWantResolve = false) output: $q.all:  es6promise reject

$q.reject, $q.when, $q.resolve

  • $q.reject() 立即返回一个rejected 的 promise, 在链式调用的时候很有用

  • $q.resolve == $q.when(value, successCb, errorCb, progressCb)

  • value 可能是一个 then-able 的 obj(即可以是 $q.defer() 返回的, 也可以是其他库产生的), 也可能是任意数据, 但是 $q.when 最终都会返回一个 promise

  • $q.when 既可以写成上述的构造函数形式, 也可以写成 $q.when(value).then(fn, fn, fn) 的形式

$q.reject("instant reject")
    .catch(function (err) {
        console.log(err);
    });
// output: instant reject

$q.when(commonJsPromise(),
    function /** success callback **/(data) {
        console.log("$q.when success callback function: " + data);
        return "$q.when success callback return another value";
    })
    .then(function (data) {
        console.log("$q.when then function:" + data);
    });

// if(iWantResolve == true) output: 
// $q.when success callback functionL: commonJS resolved
// $q.when then function:$q.when success callback return another value

// if(iWantResolve = false) output: 
// $q.when err callback function: commonJS reject
// $q.when then function:undefined

$q.when("some value", function (data){
    console.log(data);
})

// output: some value

promise chains 链式调用

任何在 successCb, errCb 中返回的非 $q.reject()对象, 都将成为一个 resolve 的 promise.
所以可以出现如下语法 promise.then().then().then()

$q.when("1")
    .then(function (data) {
        console.log(data);
        return $q.reject(2);
    })
    .catch(function (err) {
        console.log(err);
        return 3;
    })
    .then(function (data) {
        console.log(data);
    })
    
// output: 1 2 3 

参考资料

查看原文

赞 6 收藏 39 评论 2

认证与成就

  • 获得 185 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-01-12
个人主页被 2.7k 人浏览