Event之构造自定义事件

起因

之所以写这篇博客,要追溯到16年寒假时,学习前端时产生的疑惑。
众所周知,在移动端点击事件是有300ms的延迟的。
而为了解决这个问题,各种方法层出不穷。
比较有名的有zeptotap事件。
它可以向下面这样调用:

1
$(element).on('tap', handler)

这种方式我当然还能理解,用zeptoon方法而已。
然后直到我看到了下面这种调用方式:

1
2
const element = document.querySelector(selector)
element.addEventListener('tap', handler, capture)

当时虽然年少,却也知道原生事件中,是不存在tap的。
于是兴趣在一瞬间被调用,开始了探寻之旅。
还记得当时大概折腾了有好几天,至于探寻和折腾的结果,就和下面图片说的一样。

2016-08-23_13:29:12.jpg

自定义事件这个问题,从寒假开始,一直困扰到今天。
基本每个月,我都会想起这个问题,然后尝试去解决。
然后重复得到“是在下输了”的结果。
现在想想,只是因为当时自己找的资料不对,然后一直看别人的源代码,但是源代码里加了很多兼容处理的东西。于是添加tap事件的核心代码就这样被淹没在里面。

意外之喜

今天在MDN找资料时,意外的看到了Event,本来只是想看看自己还有啥没写,或者遗漏的。
结果意外的发现了自定义事件的写法。有种本来只是瞎逛逛,却捡到了宝藏的感觉。

自定义事件

这儿借用MDN给的例子。来作为实例。

1
2
3
4
5
6
7
var event = new Event('build')
// Listen for the event.
elem.addEventListener('build', function (e) { ... }, false)
// Dispatch the event.
elem.dispatchEvent(event)

这样看起来,确实简单。
一个自定义事件,只要做三件事情即可。

构造事件 -> 监听事件 -> 触发事件

结语

没啥好说了。具体的方法下面的参考资料里有。
之所以写这篇文章,也只是纪念下烦恼自己半年有余的BUG罢了。

参考资料:

MDN - Event

MDN- 创建和触发 events

前端路漫漫,且行且歌

Koa源码阅读笔记(4) -- ctx对象

本笔记共四篇
Koa源码阅读笔记(1) – co
Koa源码阅读笔记(2) – compose
Koa源码阅读笔记(3) – 服务器の启动与请求处理
Koa源码阅读笔记(4) – ctx对象

起因

前两天终于把自己一直想读的Koa源代码读了一遍。
今天就要来分析Koa的ctx对象,也就是在写中间件和处理请求和响应时的那个this对象。
而这个this对象,也是和Express的重要区别之一。不用再区分req,res(虽然还是得知道),一个this对象就能调用所有方法。
在实际开发中,是非常便利的。

Koa1和Koa2的区别

在这儿则需要谈一谈Koa1和Koa2调用this对象的区别。
Koa1在调用时,使用的是this,而Koa2则是ctx。

1
2
3
4
5
// Koa1
app.use(function * (next) {
this.body = 'hello world'
yield next
})
1
2
3
4
5
// Koa2
app.use(async (ctx, next) => {
ctx.body = 'hello world'
await next()
})

使用方式,只是把this换成了ctx。
具体为什么出现ctx和next,之前的文章koa-compose的分析有写。

ctx对象的作用

这儿继续以Koa1为例,因为看得懂Koa1源代码的,看Koa2的源码自然也不难。
首先放上关键的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
app.callback = function(){
var fn = co.wrap(compose(this.middleware));
var self = this;
return function(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).then(function () {
respond.call(ctx);
}).catch(ctx.onerror);
}
};

在上一篇Koa源码阅读笔记(3) – 服务器の启动与请求处理中,我们已经分析了fn的作用。
而onFinished则会在请求完成时调用,剩下的则是调用中间件去处理响应。
同时var ctx = self.createContext(req, res);这一句,不看createContext这个函数,应该也能猜出它的作用。
之后的fn.call(ctx)则说明了中间件中this的来源。
在这儿不得不感叹一句,JavaScript的this真的是太灵活了,配合闭包,call,apply等,简直拥有无限魔力。

ctx对象的创建

贴出相关的源代码:

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
var response = require('./response');
var context = require('./context');
var request = require('./request');
/**
* Initialize a new context.
*
* @api private
*/
app.createContext = function(req, res){
var context = Object.create(this.context);
var request = context.request = Object.create(this.request);
var response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.onerror = context.onerror.bind(context);
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
context.accept = request.accept = accepts(req);
context.state = {};
return context;
};

虽然看上去有点绕,但是仔细看看,还是不难的。
之前说过,Koa的源码简洁,一共就4个文件。
除了主要的Application.js, 剩下就都是与请求和响应相关的了。

有趣的地方

这儿,因为每次都要创建并调用ctx对象。为了避免影响原有的context,request,response对象。
这儿采用了Object.create()来克隆对象。

2016-08-02_14:52:55.jpg

context.js

首先就来分析,最开始的context.js。
context的实现很简单,但有意思的地方在于delegate这个地方。
就如下图所示:
2016-08-02_14:45:30.jpg

我看了delegate这个源代码,功能是把context中相应的方法调用和属性读取,委托至某个对象中。
而不用自己一个一个的写apply,call等。

request, response

关于request和response,我这儿就不详细写了。
在这儿放一张图足以。

2016-08-02_14:56:29.jpg

实际上,request和response是通过getter和setter,来实现存取不同属性的功能。
另外,通过刚才说的delegate方法,则使用ctx对象时,便能自动通过getter和setter获取想要的内容。

结语

这一篇很简单,其实也没啥可以说的。
因为Koa除了中间件部分看起来复杂,其它地方还是很简洁明了的。
学习源代码的过程中,也发现了很多优雅的写法,算是开拓了自己的眼界。
从会写到写好,看来还要挺长一段时间的。


前端路漫漫,且行且歌。

Koa源码阅读笔记(3) -- 服务器の启动与请求处理

本笔记共四篇
Koa源码阅读笔记(1) – co
Koa源码阅读笔记(2) – compose
Koa源码阅读笔记(3) – 服务器の启动与请求处理
Koa源码阅读笔记(4) – ctx对象

起因

前两天阅读了Koa的基础co,和Koa中间件的基础compose
然后这两天走在路上也在思考一些Koa运行机制的问题,感觉总算有点理通了。
今天就来解读一下Koa启动时,发生的一系列事情。

启动

如果只是单纯用Koa,那么启动服务器是很方便的。
下面就是一个最简单的Hello World的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
var koa = require('koa')
var app = new koa()
app.use(function * (next) {
this.set('Powered by', 'Koa2-Easy')
yield next
})
app.use(function * (next) {
this.body = 'Hello World!'
})
app.listen(3000)

在上一节对koa-compose的分析中,解决了我一个问题,那就是使用中间件时,那个next参数是如何来的。
这一节也会解决一个问题,那就是中间件中的this是如何来的。

有意思的地方

无new也可使用的构造函数

首先看Koa构造函数的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Expose `Application`.
*/
module.exports = Application;
/**
* Initialize a new `Application`.
*
* @api public
*/
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development';
this.subdomainOffset = 2;
this.middleware = [];
this.proxy = false;
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}

Application函数内部的第一句很有意思。

1
if (!(this instanceof Application)) return new Application;

因为是构造函数,但很多人会忘记使用new来初始化。但是在Koa,则做了一点小措施,从而达到了是否调用new都能初始化的效果。

原型的写法

关于原型的写法,很多人肯定不陌生。以Koa的Application为例,平时如果要写原型的属性,那么会是这样写的。

1
2
3
function Application() {}
Application.prototype.listen = function () {}
Application.prototype.callback = function () {}

这样写的话,每次都需要写冗长的Application.prototype
而在Koa中,则使用一个变量,指向了prototype

1
2
3
var app = Application.prototype;
app.listen = function () {}
app.callback = function () {}

写起来简洁,看起来也简洁。

服务器の启动流程

在Koa中,或者说一切Node.js的Web框架中,其底层都是Node.js HTTP模块来构建的服务器。
那么我就对这点产生了好奇,到底是什么,能让发送给服务器的相应,被Koa等框架截获,并进行相应处理。
同时在Koa框架中,调用listen方法才能启动服务。
那么服务器的启动流程就从listen方法开始。

启动服务器

首先是listen方法的源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Shorthand for:
*
* http.createServer(app.callback()).listen(...)
*
* @param {Mixed} ...
* @return {Server}
* @api public
*/
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};

不难看出,只有使用了listen方法,http服务才会被真正的创建并启动。
而查阅文档,则看到在http.createServer(this.callback())中传入的参数的作用。
2016-07-29_10:07:26.jpg
在这里,server 每次接收到请求,就会将其传入回调函数处理。
同时listen方法执行完毕时,server便开始监听指定端口。
所以在这里,callback便成为一个新的重点。

处理响应

继续放上callback的源代码(删除部分无用部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Return a request handler callback
* for node's native http server.
*
* @return {Function}
* @api public
*/
app.callback = function(){
var fn = co.wrap(compose(this.middleware));
var self = this;
if (!this.listeners('error').length) this.on('error', this.onerror);
return function(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).then(function () {
respond.call(ctx);
}).catch(ctx.onerror);
}
};

在这儿,Koa的注释对这个函数的作用解释的很清楚。

Return a request handler callback for node’s native http server.

而这儿,对于闭包的应用则让我眼前一亮。
由于服务器启动后,中间件是固定的,所以像初始化中间件,保持this引用,注册事件这种无需多次触发或者高耗能事件,便放入闭包中好了。
一次创建,多次使用。

说到这儿想起一个问题,上次NodeParty, Koa演讲结束后,有人询问Koa能否根据请求做到动态加载中间件,当时他没回答出来。
就源代码来看,是不能做到动态加载的。最多也只是在中间件内部做一些判断,从而决定是否跳过。

往下继续读,则可以看到这一行:

1
var ctx = self.createContext(req, res);

在context中,是把一些常用方法挂载至ctx这个对象中。
比如在koa中,直接调用this.body = 'Hello World'这种response的方法,或者通过this.path获得request的路径都是可行的。
而不用像Express一般,requestresponse方法泾渭分明。同时在使用过程中,是明显有感觉到KoaExpress要便利的。而不仅仅是解决回调地狱那么简单。

中间件的处理

在第一节Koa源码阅读笔记(1) – co中,已经解释了co.wrap的作用。
这儿可以再看一次compose函数的源代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function compose(middleware){
return function *(next){
// next不存在时,调用一个空的generator函数
if (!next) next = noop();
var i = middleware.length;
// 倒序处理中间件,给每个中间件传入next参数
// 而next则是下一个中间件
while (i--) {
next = middleware[i].call(this, next);
}
return yield *next;
}
}
function *noop(){}

在这里,中间件被倒序处理,保证第一个中间件的next参数为第二个中间件函数,第二个的next参数则为第三个中间件函数。以此类推。
而最后一个则以一个空的generator函数结尾。

在这儿,有想了很久才想通的点,那就是next = middleware[i].call(this, next);时,middleware没有返回值,为什么next参数等于下一个函数。
到后来才想通,中间件都是generator函数。generaotr会返回一个指向内部状态的指针对象。
这一点我在co的阅读笔记用提及, 也在阮一峰的《ECMAScript 6入门》看到了。

不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。需要手动调用它的next()方法。

但当时就是想不起来,结果睡了一觉就突然领悟了。= =
最近也在上一门课,名称就叫《学习如何学习》,里面也有提到睡眠能帮自己整理记忆,遇到问题也不需要死钻牛角尖,说不定过一会儿答案会自己浮现的。
2016-07-29_10:36:39.jpg
目前来看,确实是说的很对。

同时在compose函数最后的部分,返回了一个yield *next;

通过翻阅 《ECMAScript 6入门》– 可知。

如果在Generater函数内部,调用另一个Generator函数,默认情况下是没有效果的。这个就需要用到yield*语句,用来在一个Generator函数里面执行另一个Generator函数。

也就是说,其实每次执行时,是这样的:

1
2
3
4
5
6
7
8
9
10
11
co(function* (next) {
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
return yield *next;
})

return yield *next, next作为第一个中间件,会被执行。
如果碰到中间件中的next,则会被co继续调用和执行。
因为在co中,碰到generator函数是这样的:

1
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);

当然,如果在某个中间件中,碰到了以yield形式调用的函数,则会按co的规则,一路调用下去。
当中间件调用时,会返回一个Promise,而Promise在co中,会通过onFulfilled函数,实现自动调用。
从而就形成了独特的Koa风格。

有点迷糊的话,举个具体的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var koa = require('koa')
var app = new koa()
app.use(function * (next) {
console.log('middleware 1 start')
yield next
console.log('middleware 1 finished')
})
app.use(function * (next) {
console.log('middleware 2 finished')
})
app.listen(3000)

当接收到响应时,首先输出middleware 1 start,然后碰到了 yield next, next是下一个中间件,会被co处理为Promise函数。
而当第二个中间件执行完毕时,Promise自动调用then函数,而then却又是第一个中间件的onFulfilled函数。
那么第一个中间件就会继续向下执行。直到执行完成。

所以最后Koa的接收响应并处理的图,是这样的:
2016-07-29_11:28:35.jpg

中间件中的this

到这一步,这些东西就好解释了。

1
2
3
4
5
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).then(function () {
respond.call(ctx);
}).catch(ctx.onerror);

fn是处理过的中间件函数,使用call将创建好的ctx对象作为this传入,就可以实现在中间件中使用this来处理请求/响应。

其他

在整个处理过程中,心细的小伙伴还注意到了onFinished函数和respond函数。
onFinished函数是一个Node的模块。地址
作用则是在请求结束或错误是自动调用。所以这儿把ctx.onerror这个错误处理函数传入,防止请求就直接是错的。

而respond则是koa内部的函数,用于处理在中间件内部经过处理的ctx对象,并发送响应。
至此,Koa的启动和响应流程便完整的走了一遍。

结语

有些感慨,也有些唏嘘。
有很多想说的,但也感觉没什么可说的。
就这样吧。


前端路漫漫,且行且歌。

Koa源码阅读笔记(2) -- compose

本笔记共四篇
Koa源码阅读笔记(1) – co
Koa源码阅读笔记(2) – compose
Koa源码阅读笔记(3) – 服务器の启动与请求处理
Koa源码阅读笔记(4) – ctx对象

2016-07-27_19:11:39.jpg

起因

自从写了个Koa的脚手架koa2-easy,愈发觉得Koa的精妙。
于是抱着知其然也要知其所以然的想法,开始阅读Koa的源代码。

问题

读Koa源代码时,自然是带着诸多问题的。无论是上一篇所写的generator函数如何自动执行,还是对于Koa中间件如何加载,next参数如何来的。都充满了好奇。
今天写文章,并不是介绍整个koa-compose如何如何(涉及太宽,准备放在下面几篇统一介绍)。而是从自身需求出发,找到问题的答案。
而问题就是Koa中间件的加载,和next参数的来源

源码解读

初始化与中间件加载

首先的是Koa加载初始化时的函数(删除部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Koa类
function Application() {
this.middleware = [];
}
// Koa原型
var app = Application.prototype;
// Koa中间件加载函数
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are not allowed,
// so we have to make sure that `fn` is a generator function
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
}
this.middleware.push(fn);
return this;
};

在这儿不难看出,Koa对象内部有个中间件的数组,其中所有中间件都会存在其中。
而在服务器启动时,则会调用并处理该数组。
源代码如下:

1
2
3
4
var co = require('co');
var compose = require('koa-compose');
var fn = co.wrap(compose(this.middleware))

在fn被处理完后,每当有新请求,便会调用fn,去处理请求。
而在这里,co.wrap的作用是返回一个Promise函数,用于后续自动执行generator函数。

koa-compose

于是不难看出,中间件这儿的重点,是compose函数。
而compose函数的源代码虽然很简洁,但是也很烧脑。(对我而言)

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
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
// 传入中间件作为参数
function compose(middleware){
return function *(next){
// next不存在时,调用一个空的generator函数
if (!next) next = noop();
var i = middleware.length;
// 倒序处理中间件,给每个中间件传入next参数
// 而next则是下一个中间件
while (i--) {
next = middleware[i].call(this, next);
}
return yield *next;
}
}
function *noop(){}

在这里,得提一提Koa中间件的调用方式。

1
2
3
4
5
6
7
app.use(function * (next) {
this.set('Koa', 'Example');
yield next;
})
app.use(function * (next) {
this.body = 'Hello World'
})

在中间件中的next,则是在koa-compose中传入的。
而这儿, yield nextyield *next也是有区别的。
yield next, next 会作为next()的value返回。
yield *next则是在generator函数内执行这个generator函数。

结语

这两天一直在读Koa的源代码,细细看来不是很难,但是被作者的奇思妙想给打动了。
接下来会继续写一些阅读笔记,因为看Koa的源代码确实是获益匪浅。


前端路漫漫,且行且歌

Koa源码阅读笔记(1) -- co

本笔记共四篇
Koa源码阅读笔记(1) – co
Koa源码阅读笔记(2) – compose
Koa源码阅读笔记(3) – 服务器の启动与请求处理
Koa源码阅读笔记(4) – ctx对象

起因

在7月23号时,我参加了北京的NodeParty。其中第一场演讲就是深入讲解Koa。
由于演讲只有一个小时,讲不完Koa的原理。于是在听的时候觉得并不是很满足,遂开始自己翻看源代码。
而Koa1是基于ES6的generator的。其在Koa1中的运行依赖于co。
正好自己之前也想看co的源代码,所以趁着这个机会,一口气将其读完。

co

关于co,其作者的介绍很是简单。

The ultimate generator based flow-control goodness for nodejs (supports thunks, promises, etc)

而co的意义,则在于使用generator函数,解决了JavaScript的回调地狱问题

源码解读

co的源代码十分简洁,一共才两百余行。而且里面注释到位,所以阅读起来的难度还是不大的。
co的核心代码如下(已加上自己的注释):

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
/**
* Execute the generator function or a generator
* and return a promise.
*
* @param {Function} fn
* @return {Promise}
* @api public
*/
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
// 启动generator函数。
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// 如果gen不存在或者gen.next不是函数(非generator函数)则返回空值
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
function onFulfilled(res) {
var ret;
try {
// ret = gen.next return的对象
// gen.next(res),则是向generator函数传参数,作为yield的返回值
/**
* yield句本身没有返回值,或者说总是返回undefined。
* next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。
* [next方法的参数](http://es6.ruanyifeng.com/#docs/generator#next方法的参数)
*/
ret = gen.next(res);
} catch (e) {
return reject(e);
}
// 在这儿,每完成一次yield,便交给next()处理
next(ret);
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
// 如果这个generator函数完成了,返回最终的值
// 在所有yield完成后,调用next()会返回{value: undefined, done: true}
// 所以需要手动return一个值。这样最后的value才不是undefined
if (ret.done) return resolve(ret.value);
// 未完成则统一交给toPromise函数去处理
// 这里的ret.value实际是 yield 后面的那个(对象|函数|值) 比如 yield 'hello', 此时的value则是 'hello'
var value = toPromise.call(ctx, ret.value);
// 这里value.then(onFulfilled, onRejected),实际上已经调用并传入了 onFulfilled, onRejected 两个参数。
// 因为非这些对象,无法调用then方法。也就无法使用onFulfilled
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
/**
* Convert a `yield`ed value into a promise.
*
* @param {Mixed} obj
* @return {Promise}
* @api private
*/
function toPromise(obj) {
if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}

co的运行机制

看完了源代码,对generator函数有更深的理解,也理解了co的运行机制。

自动执行generator

首先解决的问题则是自动执行generator函数是如何实现的。
这儿的核心部分则在于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function co(gen) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
}

这儿,在给co传入一个generator函数后,co会将其自动启动。然后调用onFulfilled函数。
onFulfilled函数内部,首先则是获取next的返回值。交由next函数处理。
next函数则首先判断是否完成,如果这个generator函数完成了,返回最终的值。
否则则将yield后的值,转换为Promise
最后,通过Promise的then,并将onFulfilled函数作为参数传入。

1
2
3
if (value && isPromise(value)) {
return value.then(onFulfilled, onRejected);
}

而在generator中,yield句本身没有返回值,或者说总是返回undefined
而next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。
同时通过onFulfilled函数,则可以实现自动调用。
这也就能解释为什么co基于Promise。且能自动执行了。

co.wrap的运行机制

首先,先放上co.wrap的源代码:

1
2
3
4
5
6
7
8
co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
// arguments是createPromise()这个函数传入的。
return co.call(this, fn.apply(this, arguments));
}
};

使用方法也很简单:

1
2
3
4
5
6
7
8
var fn = co.wrap(function* (val) {
console.log('this is fn')
return yield Promise.resolve(val);
});
fn(true).then(function (val) {
});

然而在这里,我差点想破了脑袋。一直不理解,但执行co.call(this, fn.apply(this, arguments));这一句时,为什么fn没有实际运行,控制台也没有输出'this is fn'的提示信息。百思不得其解。
然后在苦思冥想,写了好几个demo后,才发现了问题所在。
因为co.wrap()需要传入一个generator函数。而generator函数在运行时时不会自动执行的。
这一点,阮一峰的《ECMAScript 6入门》中有提及。

不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。需要手动调用它的next()方法。

而剩下的步骤,就是把这个对象传入co,开始自动执行。

结语

co的源代码读取来不难,但其处理方式却令人赞叹。
而且generator函数的使用,对ES7中的Async/Await的产生,起了关键作用。
正如其作者TJ在co的说明文档中所说的那样:

Co is a stepping stone towards ES7 async/await.

虽然说我没用过co,只使用过Async/Await
但如今的Async/Await,使用babel,启用transform-async-to-generator插件,转译后,也是编译为generator函数。
所以了解一下,还是有好处的。而且阅读co的源代码,是阅读koa1源码的必经之路。


前端路漫漫,且行且歌。

Sass和Atom与CSS学习

起因

在五月初的时候,我停止了博客的更新。当时还发了篇博文《备战期末考试,暑期再见~》
停止更新的原因在里面也说的很清楚,一是因为要备战期末考试(结果自然是全部通过),二则是因为可写的话题越来越窄。基础知识大抵了解,中高级知识了解不多。处于很尴尬的位置。
但背后真正的原因,却是因为自己前端的学习处于一个迷茫期。让我出现了无文章可写,没有动力的情况。
因为当时HTML/CSS/JavaScript的基础知识大抵掌握的还行,同时在JavaScript的学习和应用上也越来越得心应手。Node.js与ES6的出现更是拯救了自己之前那混乱不堪的代码。而CSS的学习,却出现了毫无寸进的现象。

CSS学习的停滞

CSS学习的停滞,大概出现在三月底四月初那时候。
对于这个我有很深的印象。因为当时JavaScript还有大量可学习的资源,也能找到前进的方向。
但当时对于CSS,我当时完全就是懵的。学习时,纷繁复杂的细节与浏览器兼容性让我无暇顾及其他,很有可能谷歌浏览器上是正常的,火狐浏览器就出了问题。甚至于可能Linux下开发时是正常的,但是Windows下就出了问题。
同时,由于自己在写的一个内网项目,规模不断的在膨胀,从而在CSS中出现了不可控制的状况。这个时候全局样式冲突,复用性差,维护难度增加等问题纷至沓来。自己也对如何更好的写CSS,CSS命名规范等问题的解决方案产生了需求。

迷茫期

从三月底四月初,一直到这七月下半旬,CSS学习的停滞期足足持续了四个月。
中间看过大牛的文章,读过《CSS权威指南》《CSS揭秘》等图书,也尝试过Sass, Stylus等预处理工具。却发现自己写CSS代码时愈发吃力。
从一开始能用各种方式布局,到后期则成为了只知道使用flex的“废人”。然后使用flex多了,发了这种布局方式的局限性也很强。遂又开始了苦苦的思考。中途也求助过大牛,当然人家也没理我。于是依然只能自己思考。
还有非常重要的一点,则是写代码时所用的编辑器Sublime Text,对CSS3,Sass等新特性的支持太差。没有snnipets,代码高亮来写CSS,很大一部分时间都得浪费在思考写的东西是不是对的。
这种东西,看起来是成为大牛的“必经之路”,实际上只是自己在一边烧脑,还一边安慰自己这是学习的行为。毕竟这年头用记事本写代码的,十之八九是情非得已。

项目重新开始

七月中旬,之前写的办公项目决定推倒重来。重新开发3.0版。
当然,这次重构并不是简单的儿戏。而是基于项目情况,自身能力,各方要求。从而做出的选择。同时项目仓库也从开源中国转移至Coding.net。
当决定推倒重来的时候,其实心里很矛盾,一方面是由于参与人员能力的成长,项目会做的更加好。另一方面则是对自己几千行代码,瞬间废弃,所带来的不舍。
下图就是开发这个项目的commit次数。
2016-07-26_14:50:57.jpg
但最后,还是决定重新开始了。七月十六,一切推倒重来。
2016-07-26_14:54:14.jpg

曙光

在项目一开始,我就一直追着设计师,和他谈。希望他的风格能统一化。从而方便前端的代码复用。
同时尝试采取快速迭代的方式,在完成办公系统的核心部分之前,不对其进行大的更改。

Sass

2016-07-26_14:43:21.jpg
项目中的预处理器采取的是Sass。至于他的好处我就不说了,虽然之前也用,但是只是用来简单的嵌套元素。现在回想,觉得当时的行为也算是暴殄天物。
新项目中,模仿了mint-ui的写法。采用了mixin与变量的方式来构造CSS。
2016-07-26_15:02:45.jpg
而在实际的使用中,由于Vue的scoped属性,保证了每个组件的的CSS不会影响其它组件。同时得益于设计风格的统一,大大加快了开发速度。可维护性则提高了,之前的问题也迎刃而解。

Atom

2016-07-26_15:06:14.jpg
另一方面,不得不提的这是Atom编辑器。其对CSS3,Sass等语言的支持,要远远超过Sublime Text。
以下是Atom与Sublime的比较。选择的都是Sass语言,但是对于flex等新属性,Sublime几乎没有支持。
2016-07-26_15:11:24.jpg
2016-07-26_15:12:02.jpg
虽然也能自己写snnipets,但是考虑到后期会加入无限多的新属性,自己也无暇顾及,所有稍微思考了一下,就放弃了这个选项。

结语

Sass与Atom双管齐下的情况下,不仅解决了之前自己的问题,也大大的提高了开发速度。
而对于CSS学习的问题,很多时候觉得更是一种心理的症结。我不担心自己学不会,也不怕自己写得多。但确实害怕自己像个人形打字机一般,无意义的堆积,复制,粘贴代码。最后还因为混乱,从而让写的代码整个崩塌,无可维护。
所以到后期,自己会看一些编写CSS的方法论,但是一直没有找到什么应对当前项目需求的解决方法,所以对于CSS的心情也日渐厌烦,毕竟一些重复的代码写上十几遍,确实能把人写吐了。而如果其中某个颜色一旦更改,大把大把的时间都得浪费在这种本就可以避免的重构上。
不过吃一堑长一智,这个坑估计我现在不踩,后期也会踩到。


收拾收拾,前端的小船重新扬帆起航~
前端路漫漫,且行且歌。

备战期末考试,暑期再见~

起因

距离宣布更新博客已经两个月了,一共写了6篇博客。
然后就进入考试月,得准备期末考试的复习了。
至于写博客,说实话,可写的话题越来越窄。基础知识大抵了解,中高级知识了解不多。处于很尴尬的位置。

暑期

博客预计恢复更新的时间,应该在暑期。那时候时间多,且自己暑期会去北京闪银奇异实习,能学习到大量的前端知识,相信对于我是一个很大的提高。
那时候再来更新,相信对自己和对各位都会有帮助,而非单纯的凑凑字数啥的。

结语

就酱~

山不在高,有仙则灵。
水不在深,有龙则灵。
文不在多,有助则灵。

深入理解JavaScript类数组

起因

写这篇博客的起因,是我在知乎上回答一个问题时,说自己在学前端时把《JavaScript高级程序设计》看了好几遍。
于是在评论区中,出现了如下的对话:
对话

天啦噜,这话说的,宝宝感觉到的,是满满的恶意啊。还好自己的JavaScript基础还算不错,没被打脸。(吐槽一句:知乎少部分人真的是恶意度爆表,整天想着打别人的脸。都是搞技术的,和善一点不行吗…………)

不过这个话题也引起了我的注意,问了问身边很多前端同学关于数组与类数组的区别。他们都表示不太熟悉,所以决定写一篇博客,来分享我对数组与类数组的理解。

什么是类数组

类数组的定义,只有一条:
有length属性。

这儿有三个典型的JavaScript类数组例子。

  1. DOM方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取所有div
let arrayLike = document.querySelectorAll('div')
console.log(Object.prototype.toString.call(arrayLike)) // [object NodeList]
console.log(arrayLike.length) // 127
console.log(arrayLike[0])
// <div id="js-pjax-loader-bar" class="pjax-loader-bar"></div>
console.log(Array.isArray(arrayLike)) // false
arrayLike.push('push')
// Uncaught TypeError: arrayLike.push is not a function(…)

是的,这个arrayLike的 NodeList,有length,也能用数组下标访问,但是使用Array.isArray测试时,却告诉我们它不是数组。直接使用push方法时,当然也会报错。
但是,我们可以借用类数组方法:

1
2
3
4
5
6
let arr = Array.prototype.slice.call(arrayLike, 0)
console.log(Array.isArray(arr)) // true
arr.push('push something to arr')
console.log(arr[arr.length - 1]) // push something to arr

不难看出,此时的arrayLike在调用数组原型方法时,返回值已经转化成数组了。也能正常使用数组的方法。

  1. 类数组对象
1
2
3
4
5
6
7
8
9
10
11
12
let arrayLikeObj = {
length: 2,
0: 'This is Array Like Object',
1: true
}
console.log(arrayLikeObj.length) // 2
console.log(arrayLikeObj[0]) // This is Array Like Object
console.log(Array.isArray(arrayLikeObj)) // false
let arrObj = Array.prototype.slice.call(arrayLikeObj, 0)
console.log(Array.isArray(arrObj)) // true

这个例子也很好理解。一个对象,加入了length属性,再用Array的原型方法处理一下,摇身一变成为了真的数组。

  1. 类数组函数

这个应该算是最好玩,也是最迷惑人的类数组对象了。

1
2
3
4
5
6
7
8
9
let arrayLikeFunc1 = function () {}
console.log(arrayLikeFunc1.length) // 0
let arrFunc1 = Array.prototype.slice.call(arrayLikeFunc1, 0)
console.log(arrFunc1, arrFunc1.length) // ([], 0)
let arrayLikeFunc2 = function (a, b) {}
console.log(arrayLikeFunc2.length) // 2
let arrFunc2 = Array.prototype.slice.call(arrayLikeFunc2, 0)
console.log(arrFunc2, arrFunc2.length) // ([undefined × 2], 2)

可以看出,函数也有length属性,其值等于函数要接收的参数。

注:不适用于ES6的rest参数。具体原因和表现这儿就不再阐述了,不属于本文讨论范围。可参见 《rest参数 - ECMAScript 6 入门》。另外arguments在ES6中,被rest参数代替了,所以这儿不作为例子。

而length属性大于0时,如果转为数组,则数组里的值会是undefined。个数等于函数length的长度。

类数组的实现原理

类数组的实现原理,主要有以下两点:
第一点是JavaScript的“万物皆对象”概念。
第二点则是JavaScript支持的“鸭子类型”。

首先,从第一点开始解释。

万物皆对象

万物皆对象具体解释如下:

在JavaScript中,“一切皆对象”,数组和函数本质上都是对象,就连三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”。

而另外一个要点则是,所有对象都继承于Object。所以都能调用对象的方法,比如使用点和方括号访问属性。
比如说,这样的:

1
2
3
4
let func = function() {}
console.log(func instanceof Object) // true
func[0] = 'I\'m a func'
console.log(func[0]) // 'I\'m a func'

鸭子类型

万物皆对象具体解释如下:

如果它走起来像鸭子,而且叫起来像鸭子,那么它就是鸭子。

比如说上面举的类数组例子,虽然他们是对象/函数,但是只要有length属性,能当数组用,那么他们就是数组。
是什么,不是什么对鸭子类型来说,一点也不重要。能做什么,才是鸭子类型的核心。(谢谢nightre大大的指正)

但是,在这儿,还是有些迷糊的。为什么使用call/apply借用数组方法就能处理这些类数组呢?

探秘V8

一开始,我也对这个犯迷糊啊。直到我去Github上,看到了谷歌V8引擎处理数组的源代码。
地址在这儿:v8/array.js
作为讲述,我们在这里引用push的源代码(方便讲述,删除部分。slice的比较长,但是原理一致):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Appends the arguments to the end of the array and returns the new
// length of the array. See ECMA-262, section 15.4.4.7.
function ArrayPush() {
// 获取要处理的数组
var array = TO_OBJECT(this);
// 获取数组长度
var n = TO_LENGTH(array.length);
// 获取函数参数长度
var m = arguments.length;
for (var i = 0; i < m; i++) {
// 将函数参数push进数组
array[i+n] = arguments[i];
}
// 修正数组长度
var new_length = n + m;
array.length = new_length;
// 返回值是数组的长度
return new_length;
}

是的,整个push函数,并没有涉及是否是数组的问题。只关心了length。而因为其对象的特性,所以可以使用方括号来设置属性。

这也是万物皆类型和鸭子类型最生动的体现。

总结

JavaScript中的类数组的特殊性,是由其“万物皆类型”和“鸭子类型”决定的,而浏览器引擎底层的实现,更是佐证了这一点。
而先前说我的那位同学,因为只是知道类数组的几种表现和用法,并且想通过apply来打我脸,证明我根本没有仔细看书。这种行为不仅不友善,而且学习效率也不高。
因为,知其然而不知其所以然是不可取的。特别是发现很多这种例子,就得学会归纳总结。(感谢winter老师的演讲:一个前端的自我修养,教会我很多东西。)。
很多时候,深入看看源代码也会让你对这个理解的更透彻。将来就算是蹦出一百种类数组,也能知道是怎么回事儿。

最后,还是开头那句话:“都是搞技术的,和善一点不行吗?有问题就好好交流,不要总想着打别人脸啊…………”

Always bet on F2E

这篇文章应该五一就发出来的,但是写到一半,和室友出去浪了。还剩下最后一点,在五月七号给补完了。

起因

学习编程和前端的路上,有过欢笑有过迷茫。
庆幸的是,我从15年3月份开始学编程,到15年10月份确定以编程作为我大学的方向,只花了6个月。因为编程使我感到快乐。
更庆幸的是,我从15年10月份到16年5月份,花了7个月。确定把前端作为编程的方向。因为这能让我的优势得到最大的发挥,而且,我喜欢前端啊。

我所理解的前端

昨天和一位前端的前辈聊天,他看了看我的博客,和我说:“你只要坚持下去,毕业之后一定会在前端有所建树的。”
当然,他指的前端可能是HTML,CSS,JavaScript。跑在浏览器的前端,基于JavaScript的前端。但是,不是我所理解的前端。

我所理解的前端,正如维基百科给出的定义一般:

在软件架构和程序设计领域,前端是软件系统中直接和用户交互的部分。

也正如我现在干的事情一般,一半时间写Node.js,一半时间写前端。

没有常青树

作为一名公共管理学院的文科生,阅读过许多历史学与管理学的书籍。对这些书籍的理解,就有一条:“没有常青树”
在软件开发的领域,时局瞬息万变。树立起没有常青树的概念是很重要的,君且看万古时空,多少王朝沉浮。也看硅谷,曾经的王者雅虎,如今也不得挂牌出售。

因为没有常青树,所以不愿把自己局限于仅仅只是跑在浏览器的前端,基于JavaScript的前端。
所以我更加认同:“前端是软件系统中直接和用户交互的部分”

虽然目前JavaScript是王者地位,自己也很喜欢它。但WebAssembly的出现,VR/AR的发展,总有一天,情势会变化的。
无论那一天依然是JavaScript登顶,或者是别的替代了它。

我都喜欢做前端啊,直接与用户交互的,决定用户体验好坏的前端,需要懂得计算机基础,也要理解设计、交互、产品、后台知识的前端啊。

Always bet on F2E

曾经看过一个slides,最后有这么一些话:
Always bet on js
这些话是Brendan Eich,也就是JavaScript之父说的。

这也是我想说的话:Always bet on F2E.
可能有人会说,这样会不会太过偏激,知识面狭窄?

我喜欢编程,因为写代码就是一件很开心的事情。只是更加偏向于前端。
于是我决定了,这就是我编程学习的方向,不再迷茫Java好还是C#的语法优雅又或是XXX的发展前途广。
因为有用户交互的地方,就有前端。而语言只是其实现方式而已。

So,always bet on F2E