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

从零组装新工具 - Koa2

整个项目已开源于Github,项目地址:koa2-easy在线Demo:

起因

作为一个前端,Node.js算是必备知识之一。同时因为自己需要做一些后台性的工作,或者完成一个小型应用。所以学习了Node的Express框架,用于辅助和加速开发。

不过当初自己对Express的学习和了解,并不是很深入。要求也仅仅是停留在能发送静态文件,构建后台API,与数据库完成简单交互而已。所以当初自己选用Express时,靠的是Express 应用生成器,相当于Express的最佳实践。
在使用了一段时间之后,被Express的“回调地狱”,“自定义程度不高”等问题所困扰,于是决定更换至新的框架。

在选择框架时,遵循了自己学习新技术的原则:

要么找值得学习的,深入学习并理解。要么找适合当前业务,能快速解决问题的。不要在具体某某某个技术上纠结太久。

这句话也是自己看余果大大的《Web全栈工程师的自我修养》这本书的体会。

选择Koa

在上面原则的指导下,很容易的就找到了一款符合自己需求的框架:Koa。
Koa因为应用了ES6的生成器语法,所以非常优雅的解决了Node.js的回调地狱问题。
比如说这样的Ajax代码,看起来就比回调函数的写法优雅很多。

1
2
3
4
5
6
7
8
9
10
11
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}

例子来源: Generator 函数

虽然yield的写法有点奇怪,但还是可以接受的。

选择Koa2

同时在Koa的github首页中,看到了Koa2。
Koa2应用了ES7的Async/Await来替代Koa1中的生成器函数与yield。
所以上一段代码的main函数,在Koa2里长这样:

1
2
3
4
5
async function main() {
var result = await request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}

使用了Async/Await后,整段代码是变的更加好看的。

理解Koa的中间件

在一开始学习Koa时,是不太理解Koa的中间件级联这个概念的。
就是下图这玩意。
中间件级联

这个算是Koa的核心概念了,不理解这个,只能安安心心继续用Express。

还好自己平时爱去看各种开发大会的视频,来提升自己的眼界。所以昨晚正好在慕课网看到了《阿里D2前端技术论坛——2015融合》的大会视频,便开心的点开学习。
而第一篇《用 Node.js 构建海量页面渲染服务——by 不四》讲的就有Koa框架,还梳理了Koa的中间件级联这个概念。
在不四前辈介绍完Koa的中间件级联后,我发现自己好像理解了。
配合着自己之前学习的ES6知识,才发现原来是这样。
在这儿我贴一段代码和自己的理解,有兴趣的同学可以看一看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var koa = require('koa');
var app = koa();
// x-response-time
app.use(function *(next){
// 首先启动第一个中间件,记录下时间
var start = new Date;
// 进入中间件,并等待返回。
yield next;
// 返回后,代表操作已完成,记录结束时间并输出。
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
});
// response
app.use(function *(){
// 最后一个中间件,将body写成'Hello World'
this.body = 'Hello World';
});
app.listen(3000);

整个的流程,会是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
.middleware1 {
// (1) do some stuff
.middleware2 {
// (2) do some other stuff
.middleware3 {
// (3) NO next yield !
// this.body = 'hello world'
}
// (4) do some other stuff later
}
// (5) do some stuff lastest and return
}

至此,学习Koa的最后一个难关,也被攻克了。

从零组装Koa

因为对Express的学习和使用,知道了自己对于后台框架的真实需求。
所以这回决定不用Koa generator之内的工具,而是自己从零开始,组装一个适合自己的Koa框架。
基于Koa2,使用Async/Await,符合自己需求……
想想就是很美好的事情呀。

梳理需求

首先要做的,自然就是梳理自己的需求。看看到底需要什么东西。
于是翻出自己前两个月在使用的Express框架,确定了以下要点。

  1. 路由,创建Rest Api
  2. 发送静态HTML文件
  3. 设置静态文件目录
  4. 发送和读取JSON数据
  5. 渲染模板
  6. 使用ES6语法完成工作

实现需求

具体的实现部分,这儿就不再赘述了。就是去github和npm上,寻找一个一个的包并组装在一起了而已。
整个项目的亮点就在于:完全符合个人需求,并且使用ES6来完成工作。对我个人而言,用ES6不仅看起来爽,也能提升我的工作效率。

总结

这周因为胃肠炎,好像也没做啥事情……最大的事儿也只是组装了个Koa框架。
因为养病的原因,只能每天看看开发者大会的视频。因为肚子时不时的抽一下,真的很影响工作啊……

今天感觉好了一点,希望病情早日康复~
就酱~

Vuex源码阅读笔记

笔记中的Vue与Vuex版本为1.0.21和0.6.2,需要阅读者有使用Vue,Vuex,ES6的经验。

起因

俗话说得好,没有无缘无故的爱,也没有无缘无故的恨,更不会无缘无故的去阅读别人的源代码。
之所以会去阅读Vuex的源代码,是因为在刚开始接触Vuex时,就在官方文档的Actions部分,看到这么一句:

1
2
3
4
5
6
7
8
9
10
// the simplest action
function increment (store) {
store.dispatch('INCREMENT')
}
// a action with additional arguments
// with ES2015 argument destructuring
function incrementBy ({ dispatch }, amount) {
dispatch('INCREMENT', amount)
}

上面的Action还好说,能看懂,但是下面使用ES6写法的Action是什么鬼呀喂(摔!)
虽然知道有解构赋值,但是那个{ dispatch }又是从哪儿冒出来的呀喂!明明我在调用时,没有传这个参数呀!
之前因为赶项目进度,所以抱着能用就行的态度,也就没管那么多。如今有了空闲时间,必须好好钻研一下呀。
而钻研最好的方式,就是阅读Vuex的源代码。这样就能弄清楚,那个{ dispatch }到底从哪儿冒出来的。

Vuex源代码简介

Vuex的源代码量挺少的,加起来也才600行不到,但是其中大量使用了ES6的语法,且部分功能(如Vuex初始化)使用到了Vue。所以读起来还是有些费劲的。
整个Vuex的源代码,核心内容包括两部分。一部分是Store的构造函数,另一部分则是Vuex的初始化函数。
而刚才问题的答案,就在第二部分。

问题场景还原

首先要介绍的,就是Vuex在Vue项目中的初始化。这儿贴一段代码:
首先是Vuex中,我写的Actions源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// global/Vuex/action.js
export const getMe = ({ dispatch }) => {
/**
* 异步操作,获取用户信息,并存入Vuex的state中
*/
res.user.get_me()
.then(data => {
dispatch('GET_ME', data)
})
.catch(err => {
console.log(err)
})
}

这个则是顶层组件,调用store的地方。由于Vuex的特点,store只需要在最顶层的组件声明一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div id="wrapper">
<router-view></router-view>
</div>
</template>
<script type="text/javascript">
import store from './Vuex/store.js'
export default {
store
}
</script>

接下来则是组件中,则是实际调用Vuex的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// index.vue
import { getMe } from './../global/Vuex/action'
export default {
vuex: {
actions: {
getMe
},
getters: {
// 从state中获取信息
user: state => state.user
}
},
ready() {
// 开始获取用户信息
this.getMe()
}
}

在这儿,可以很明显的看出,我在使用this.getMe()时,是没有任何参数的。但是在getMe函数的定义中,是需要解构赋值出{dispatch}的。
就好比说这个:

1
2
3
4
5
6
function getX({ x }) {
console.log(x)
}
getX({ x: 3, y: 5 })
// 3

你得传入相应的参数,才能进行解构赋值。
同时,我注意到在Vuex的Actions调用,需要在Vue的options的Vuex.actions中先声明,之后才能使用。
那么,一定是Vuex对这个Action动了手脚。(逃)
而动手脚的代码,就存在于Vuex源代码的override.js中。这个文件,是用于初始化Vuex的。

Vuex的初始化

override.js中,有个vuexInit的函数。看名字就知道,这是拿来初始化Vuex的。
在代码开头,有这么一句:

1
2
3
4
5
6
const options = this.$options
const { store, vuex } = options
// 感觉解构赋值真的很棒,这样写能省很多时间。
// 下面的是老写法
// const store = options.store
// const vuex = options.vuex

在这儿,用于是在Vue中调用,所以this指向Vue,而this.$options则是Vue的配置项。
也就是写Vue组件时的:
export default {……一些配置}
这里,就把Vue配置项的store和vuex抽离出来了。

搜寻store

接下来,则看到了Vuex源代码的精妙之处:

1
2
3
4
5
6
// store injection
if (store) {
this.$store = store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}

解构赋值并不是一定成功的,如果store在options中不存在,那么store就会是undefined。但是我们需要找store。
于是Vuex提供了向父级(Vue中的功能)寻找store的功能。不难看出,这儿父级的$store如果不存在,那么其实他也会到自己的父级去寻找。直到找到为止。
就想一条锁链一样,一层一层的连到最顶部store。所以在没有找到时,Vuex会给你报个错误。

1
2
3
4
5
6
7
8
// 声明了Vuex但没有找到store时的状况
if (vuex) {
if (!this.$store) {
console.warn(
'[vuex] store not injected. make sure to ' +
'provide the store option in your root component.'
)
}

对Vuex声明的内容,进行改造

接下来,则是对Vuex声明的内容,进行改造。
首先的是获取Vuex对象的内容:

1
let { state, getters, actions } = vuex

同时,在这儿还看到了对过时API的处理。感觉算是意料之外的惊喜。

1
2
3
4
5
6
7
8
9
10
// handle deprecated state option
// 如果使用state而不是getters来获取Store的数据,则会提示你state已经过时的,你需要使用新的api。
// 但是,这儿也做了兼容,确保升级时服务不会挂掉。
if (state && !getters) {
console.warn(
'[vuex] vuex.state option will been deprecated in 1.0. ' +
'Use vuex.getters instead.'
)
getters = state
}

接下来,则是对getters和actions的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// getters
if (getters) {
options.computed = options.computed || {}
for (let key in getters) {
defineVuexGetter(this, key, getters[key])
}
}
// actions
if (actions) {
options.methods = options.methods || {}
for (let key in actions) {
options.methods[key] = makeBoundAction(this.$store, actions[key], key)
}
}

可以看出,在这儿对getters和actions都进行了额外处理。
在这儿,我们讲述actions的额外处理,至于getters,涉及了过多的Vue,而我不是很熟悉。等我多钻研后,再写吧。

Actions的改造

对整个Actions的改造,首先是Vuex的检测:

1
2
3
4
5
6
7
8
// actions
if (actions) {
// options.methods是Vue的methods选项
options.methods = options.methods || {}
for (let key in actions) {
options.methods[key] = makeBoundAction(this.$store, actions[key], key)
}
}

在这儿,我们一点一点的剖析。可以看出,所有的actions,都会被makeBoundAction函数处理,并加入Vue的methods选项中。
那么看来,makeBoundAction函数就是我要找的答案了。
接下来贴出makeBoundAction函数的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Make a bound-to-store version of a raw action function.
*
* @param {Store} store
* @param {Function} action
* @param {String} key
*/
function makeBoundAction(store, action, key) {
if (typeof action !== 'function') {
console.warn(`[vuex] Action bound to key 'vuex.actions.${key}' is not a function.`)
}
return function vuexBoundAction(...args) {
return action.call(this, store, ...args)
}
}

事情到这儿,其实已经豁然明朗了。
我在Vuex中传入的actions,实际会被处理为vuexBoundAction,并加入options.methods中。
在调用这个函数时,实际上的action会使用call,来改变this指向并传入store作为第一个参数。而store是有dispatch这个函数的。
那么,在我传入{dispatch}时,自然而然就会解构赋值。
这样的话,也形成了闭包,确保action能访问到store。

结语

今天应该算是解决了心中的一个大疑惑,还是那句话:

没有无缘无故的爱,也没有无缘无故的恨,更没有无缘无故冒出来的代码。

整个源代码读下来一遍,虽然有些部分不太理解,但是对ES6和一些代码的使用的理解又加深了一步。比如这回就巩固了我关于ES6解构赋值的知识。而且还收获了很多别的东西。总而言之,收获颇丰~
最后的,依然是那句话:前端路漫漫,且行且歌。

ES6学习之解构赋值

本文选自我在SegmentFault的#21天阅读分享#中,所记录的两篇笔记。
因为对自己帮助较大,所以分享在此。

起因

前两天在项目中,需要应用到vuex(类似redux的状态管理工具)。而vuex中,关于变量的赋值是ES6中的解构赋值。
恰巧今天在看犀牛书时,也看到了关于解构赋值的介绍,所以今天准备专门学习解构赋值。

解构赋值

之前我们声明变量,是这样的:

1
2
var one = 1;
var two = 2;

这种变量声明的方式,写的少了还好说。写多了,却会感觉繁琐。也容易出错。
而ES6中,关于解构赋值的写法,是这样的:

1
2
3
4
5
var [one, two] = [1, 2]
console.log(one)
// 1
console.log(two)
// 2

这样的话,一次性就命名了两个变量。
但只是这样的话,功能其实是不够用的。
结构赋值还支持如下的形式:

1
2
3
var [one,,three,] = [1,2,3,4]
console.log(three)
// 3

这种方式,可以留空位,从而是变量赋值达到精准的要求。
在阮一峰老师的ES6文档中,关于解构赋值有这么一句:

解构赋值可以方便地将一组参数与变量名对应起来。

1
2
3
4
5
6
7
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3])
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1})

现在看来,只是把传入的dispatch参数,给解构赋值了。

对象的解构赋值

先写一个demo。

1
2
3
4
5
6
7
8
var { foo, bar } = {
foo: "Hi i'm foo",
bar: "Hi i'm bar"
}
console.log(foo)
// "Hi i'm foo"
console.log(bar)
// "Hi i'm bar"

对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
在这儿,我设置了foo和bar,自然就取到了相应的变量。
但是如果要名字不一样呢?

1
2
3
4
5
6
7
8
var { foo: Anotherfoo, bar: Anotherbar } = {
foo: "Hi i'm foo",
bar: "Hi i'm bar"
}
console.log(Anotherfoo)
"Hi i'm foo"
console.log(Anotherbar)
"Hi i'm bar"

这儿相当于把获取到的foo值,赋值给Anotherfoo。从而达到变量名不同也能变量赋值的效果。
这部分的机制,就借用阮一峰老师的话语:

这实际上说明,对象的解构赋值是下面形式的简写

1
var { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

1
2
3
var { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined

上面代码中,真正被赋值的是变量baz,而不是模式foo

同时之前在使用vuex中,对这一句话很不理解:

1
2
3
4
5
6
7
8
const vm = new Vue({
vuex: {
getters: { ... },
actions: {
plus: ({ dispatch }) => dispatch('INCREMENT')
}
}
})

2016.04.16更新

在看到Vuex源代码时,发现有这么一部分:

1
2
3
4
5
6
7
constructor ({
state = {},
mutations = {},
modules = {},
middlewares = [],
strict = false
} = {})

感兴趣的,是函数中,指定了变量的默认参数并进行了变量解构赋值,但给整个参数又指定了默认值。
于是手写了一个demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
function getInfo({
a = '我是默认参数A',
b = '我是默认参数B'
} = {}) {
console.log(a, b)
}
getInfo({ a: 'A被覆盖了', b: 'B被覆盖了' })
// A被覆盖了 B被覆盖了
getInfo({})
// 我是默认参数A 我是默认参数B
getInfo()
// 我是默认参数A 我是默认参数B

也就是说,这种写法,当函数未传入覆盖默认值的参数,则默认参数将被解构赋值。从而保证默认参数100%得到使用。
而不会出现下面,没有传入参数时报错的现象。

1
2
3
4
function getInfo({……一些默认参数} = {}) {}
getAnotherInfo()
// Uncaught TypeError: Cannot match against 'undefined' or 'null'.

嵌套对象的解构

感觉,这应该是解构赋值中最实用的部分了(个人认为)。
因为经常套数据,所以也经常需要把变量的数据取出,转成变量。写多了的话,也是感觉很繁琐的。
而ES6,提供了一种全新的解决方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var json = {
'name': 'Lxxyx',
'info': {
'age': 19,
'subject': 'HRM'
}
}
var {
name,
info: {
age,
subject
}
} = json
name // "Lxxyx"
age //19
subject //"HRM"

如果直接写变量,代表把相应变量赋值。
如果加个:号,则表示操作符。表示要去这里面找变量。
如果不理解的话,自己写一遍demo也就理解了。

结尾

前端路漫漫,且行且歌~