《数据库原理及应用开发》与我的学习

起因

打小开始,自己就比较喜欢计算机。
上大学之后又自学了C,Java,HTML/CSS/JavaScript等编程语言。也加入了校内的家园工作室,成为了一名前端工程师。
学习过程中,愈发觉得计算机基础的重要性,遂开始选课+蹭课+Mooc自学的路。
上学期末,非常幸运的选中了《数据库原理及应用开发》这门课。

对个人

对于个人而言,选择《数据库原理及应用开发》并非机缘巧合也非无奈之举。
自己之前因为业务原因,一直使用的是MongoDB等NoSQL数据库,做一些简单的CRUD的应用。
因此选修这门课,一方面为了补足计算机基础,另一方面则是业务需要,需要了解SQL数据库的使用。

学习这门课程,对我的帮助如下:

  1. 对数据库的基础概念有一定的了解,能够大致知道什么是数据库,能够用来干什么
  2. 能够对结构化查询语言(SQL)有一定的了解,能够写一些“增、删、改、查”的SQL语句或存储过程
  3. 对某一数据库产品(例如mysql、oracle获取SQL Server)有一些了解,知道其在标准SQL上的一些扩展,知道它的一些特性,熟悉其简单的安装、部署、使用

但自己在学习过程中,发现也有很多不足。

  1. 尽管知道数据库的基础概念,但很多概念了解的还不是很透彻
  2. 能够写出基本的SQL语句,但很多时候写出的SQL语句并不是很好
  3. 由于所从事职业的问题,与数据库接触较少,因此所学的大部分知识并不能投入于实战中

对所学专业的帮助

自己所学专业是人力资源管理,对于下面这句话,感觉深有体会……

关系型数据库遵循ACID规则

翻译成人力资源的话语就是。
人力资源政策与人力资源战略需要遵循AC原则。

1. A (Atomicity) 原子性

原子性在计算机领域很容易理解,也就是说事务里的所有操作要么全部做完,要么都不做,事务成功的条件是事务里的所有操作都成功,只要有一个操作失败,整个事务就失败,需要回滚。

而在人力资源领域,则是说人力资源战略在实行的过程中需要有“原子性”,在颁布人力资源战略和实行过程中,不可突然废弃,否则容易造成公司制度紊乱,人心不齐的现象。所以如果制订了人力资源战略,就需要实施下去,而不能半途而废。

2. C (Consistency) 一致性

一致性也比较容易理解,也就是说数据库要一直处于一致的状态,事务的运行不会改变数据库原本的一致性约束。

而在人力资源领域,可以理解为人力资源政策需要向一致性看起。公司的目的是获取利润,而目标则各不相同,因此在更改政策时,不能破坏这种最基本的目的与约束,需要向公司的目标不断靠近。

独立性与持续性

由于人力资源战略会随着组织的发展而不断调整,因此独立性与持续性并不适用与人力资源管理。

NoSQL与NewSQL的发展

自己注意到,最近业界有在提一个新词,NewSQL。

NewSQL是OldSQL和NoSQL的改进。具体的功能点可以看这个:

而联想到人力资源领域。则想到了人力资源管理的发展。

从一开始最固化的人事管理,再到后期的人力资源管理,然后再到现在最新的人力资源三支柱模型。同样也是在变化且发展着的。

这里如果只提发展的话当然不够。
NoSQL与NewSQL的出现,都是为了解决切实的业务问题而产生的,并非空穴来风。而人力资源管理模式的变更,也是顺应实际业务需求与组织发展而产生的变革。

而这是大部分事物发展的本质。并非突然出现的“破坏式创新”,而是在原有事物基础上的改进与发展,从而更好的顺应现实生活的需要。

本文同步发表于我的博客,在线阅读请扫码。

2016-编程元年

时光

时光荏苒,如白驹过隙。不知不觉,已是2016年的末尾。
一年来,发生了很多很多事情,足矣让我一人细细的怀念上一年。
值得庆幸的,就是这一年来所做的事情,懊恼、开心、遗憾等情绪都有。

但是,没有后悔啊……

魔幻

16年给我的最大感受,大概就是魔幻。
一切显得那么的不真实,但一切又是我所亲身经历的现实……
想过会实现很多事情,只是没想到自己真的一一达成,然后实现了。

编程元年

之前一直在想博客的标题,后面想了想,取了“编程元年”这个名字。
大概是这一年,都和编程紧密相关,自己也从一只前端刚入门的菜鸡,进化到了一个初级前端工程师。

做的事情和数据

这一年都做了什么,让数据来说话:

博客


开源项目

Commit


某个项目的:

技术栈

前端:

1. Vue + Vuex + Vue-Router
2. ES6/7
3. Gulp, Webpack

后端:

1. Node.js
2. Koa
3. TypeScript
4. Docker
5. MongoDB

App:

1. Android
2. React Native

读的书

读的书……没有细算,也基本无法算了。
一个月两三本的阅读量还是有的。
林林总总下来,几十本书吧。
交互、设计、前端、数学、人力资源管理,都会去看一看。

想说却不能说的那些

很多事情,想说。
但是不能说。
静静等待,积蓄能量。
借用三国演义动漫的一句歌词:

不鸣则已,一鸣动九天。

结语

很久没写博客了,很诧异每个月还有1.5k的访问量。写的文章现在也有人去挑错。很开心啊。
有想说的,但是想想其实也没什么好说的。
就这样吧~恢复写博客的好习惯~

2016-12-09 23:56 记于家园工作室机房

Vue nextTick 源码解读

起因

自己第一次用Vue做项目时,经常遇到操作DOM的问题,但是很多时候因为Vue数据更新的特性,是不能在第一时间拿到更新后的DOM。
后面才观察到,Vue有一个nextTick方法。
nextTick的Api如下:

对于这句话:

将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。

自己感到十分好奇,因为之前我解决此类问题,使用的是setTimeout(fn, 0)的方式来的。
所以就继续打开Vue的源代码,细细研读。

异步更新队列

在Vue的文档中,异步更新队列部分有这么一段:

Vue.js 默认异步更新 DOM。每当观察到数据变化时,Vue 就开始一个队列,将同一事件循环内所有的数据变化缓存起来。如果一个 watcher 被多次触发,只会推入一次到队列中。等到下一次事件循环,Vue 将清空队列,只进行必要的 DOM 更新。在内部异步队列优先使用 MutationObserver,如果不支持则使用 setTimeout(fn, 0)。
例如,设置了 vm.someData = ‘new value’,DOM 不会立即更新,而是在下一次事件循环清空队列时更新。
为了在数据变化之后等待 Vue.js 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback) 。回调在 DOM 更新完成后调用。

那么由文档可知,异步更新队列的奥妙则在于MutationObserver
在MDN中,对MutationObserver的介绍如下:

MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时作出适当反应的能力.

而在Vue的源代码中,则是:

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
/**
* Defer a task to execute it asynchronously. Ideally this
* should be executed as a microtask, so we leverage
* MutationObserver if it's available, and fallback to
* setTimeout(0).
*
* @param {Function} cb
* @param {Object} ctx
*/
export const nextTick = (function () {
var callbacks = []
var pending = false
var timerFunc
function nextTickHandler () {
pending = false
var copies = callbacks.slice(0)
callbacks = []
for (var i = 0; i < copies.length; i++) {
copies[i]()
}
}
/* istanbul ignore if */
if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) {
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(counter)
observer.observe(textNode, {
characterData: true
})
timerFunc = function () {
counter = (counter + 1) % 2
textNode.data = counter
}
} else {
// webpack attempts to inject a shim for setImmediate
// if it is used as a global, so we have to work around that to
// avoid bundling unnecessary code.
const context = inBrowser
? window
: typeof global !== 'undefined' ? global : {}
timerFunc = context.setImmediate || setTimeout
}
return function (cb, ctx) {
var func = ctx
? function () { cb.call(ctx) }
: cb
callbacks.push(func)
if (pending) return
pending = true
timerFunc(nextTickHandler, 0)
}
})()

解读一下,这是个自执行函数。在MutationObserver存在的情况下,则是这样的:

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
var callbacks = []
var pending = false
var timerFunc
function nextTickHandler () {
pending = false
var copies = callbacks.slice(0)
callbacks = []
for (var i = 0; i < copies.length; i++) {
copies[i]()
}
}
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(counter)
observer.observe(textNode, {
characterData: true
})
timerFunc = function () {
counter = (counter + 1) % 2
textNode.data = counter
}
const nextTick = function (cb, ctx) {
// 如果ctx参数存在,则为回调函数绑定this
var func = ctx
? function () { cb.call(ctx) }
: cb
callbacks.push(func)
if (pending) return
pending = true
timerFunc(nextTickHandler, 0)
}

核心的部分为:

1
2
3
4
5
6
7
8
9
10
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(counter)
observer.observe(textNode, {
characterData: true
})
timerFunc = function () {
counter = (counter + 1) % 2
textNode.data = counter
}

在调用observe时,传入的参数有:

因为Mutation Observer则是异步触发,DOM发生变动以后,并不会马上触发,而是要等到当前所有DOM操作都结束后才触发。
调用timerFunc时,因为DOM操作已经结束,此刻触发注册的回调,就能获取到更新后的回调。

队列更新

在看文档时,也有注意这句话:

Vue.js 默认异步更新 DOM。每当观察到数据变化时,Vue 就开始一个队列,将同一事件循环内所有的数据变化缓存起来。如果一个 watcher 被多次触发,只会推入一次到队列中。等到下一次事件循环,Vue 将清空队列,只进行必要的 DOM 更新。

队列更新的实现则在于Mutation Observerpending状态的配合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var pending = false
function nextTickHandler () {
pending = false
var copies = callbacks.slice(0)
callbacks = []
for (var i = 0; i < copies.length; i++) {
copies[i]()
}
}
const nextTick = function (cb, ctx) {
// 如果ctx参数存在,则为回调函数绑定this
var func = ctx
? function () { cb.call(ctx) }
: cb
callbacks.push(func)
if (pending) return
pending = true
timerFunc(nextTickHandler, 0)
}

在这里,pending = true时代表正在等待所有的DOM操作结束,等待操作结束时调用nextTick传入的回调,将会被推入队列。从而实现DOM更新后,才触发某个队列的回调。
回调触发时,pending将被设为false, 队列也将被清空,从而继续实现队列功能。

setTimeout

既然看到了Mutation Observer,源代码中又有setTimeout(fn, 0)
就必须解释下setTimeout(fn, 0)的作用。这个涉及到了JavaScript的EventLoop,还是挺有意思的。
继续看MDN的解释:

零延迟 (Zero delay) 并不是意味着回调会立即执行。在零延迟调用 setTimeout 时,其并不是过了给定的时间间隔后就马上执行回调函数。其等待的时间基于队列里正在等待的消息数量。

因此,每次调用setTimeout(fn, 0)时,DOM的操作已经完成。确保获取的是更新后的DOM。

setImmediate

在Node.js中,有个setImmediate的Api。

在Node.js的Api中,解释如下:

1
2
3
4
setImmediate(callback[, ...arg])#
callback <Function> The function to call at the end of this turn of the Node.js Event Loop
[, ...arg] Optional arguments to pass when the callback is called.

作用也和setTimeout(fn, 0)类似。

结语

很早之前就想写这篇文章,但是因为各种事情,一直拖到了现在。
今天抽空,一口气写了出来。也算是自己对之前知识的总结。

参考资料:

Vue - 异步更新队列
HTML5新特性之Mutation Observer
MDN - 事件循环
Node.js - setImmediate


前端路漫漫,且行且歌

Ning - JSConf

起因:

在知晓JSConf2016举办消息的那一刻起,心中便是无限期待。
期待着自己有一个也能参加JSConf这种JS开发者的盛会。
而当时则是受限于自己的财力,票价虽然只要500,但是对于学生而言,还是较难承担的。
很幸运的是在JSConf门票开售时,自己抢到了早鸟票。同时因为拿到的暑期实习Offer,资金问题也解决了。

大会两日

大会两日,各种分享不断。然后几乎云集前端大牛。平时只能在书里看到的,今天也是在现场看见了。
至于分享的质量,作为一个初学者,自然是不好评论的。
但是可以说的是,有些地方广告还是有点多。
第一天的主持人也是非常有趣。
领取到最有意思的奖品,应该是WiredCraft的一份贴纸,不是firefox而是waterfox

遇见贺老 && fix 人生 BUG

在大会第二天的下午,看到了贺老一个人坐在会场边上。便壮着胆上去交流和感谢了一番。
感谢是因为在我16年寒假准备深入学习前端一些知识时,看到了贺老在知乎上的回答:

才大二,少做(react/angular/php)hello world级别的事情(除非你能作出点真的有点实际价值的产品),先打基础。

当时觉得十分受用,后续也是按照他所说的去做了。后续则受益颇多。
此次在会场看到了真人,自然是要表达一番感谢的。

后续则是向贺老询问了学习和学习方向上的一些问题。
而贺老的一番回答,对于此时的我则有醍醐灌顶之感。
下面是问题与回答的实录(加了些语气助词):

Q: 贺老您好,就我现在一半时间写Node.js,一半时间写前端。这样感觉自己没有一个侧重的点,也不知道自己往哪里发展比较好。感觉时间和精力分配上出现了矛盾。

A: 没事,你还是学生,两个都学就可以了。你现在的功利心不要那么强,不要总想着学什么是有用的,学什么又是没用的。我做工程师那么多年,很多之前学过的东西都已经不用了,比如我当年花了一个暑假,才学会DOS系统的那一串指令。想想对现在的自己有用吗?很显然是没有的。但是编程就是这样,你学的很多东西都是会被淘汰的,所以功利心不要那么强

Q: 贺老,我现在只是学一门JS语言,因为前端啥都能做,所以也没打算换。但是我担心自己只学习一门语言还不够,需要再去学另外一门吗?

A: 当然可以学习啊。但是还是和我之前给的建议一样,既然你希望学习一门新的语言。就不要停留在Hello World的层面,而是要深入的去学习和使用它。这样你才能得到真正的收获。

Q: 贺老,我是自学前端的,现在也在补习计算机专业的基础课。您看有什么我在大学期间需要特别侧重的课程吗?

A: 如果可以的话,我建议你去把计算机专业的基础课都去上一遍。因为接触的越多,你的视角就越宽泛。你学一个东西,并不一定你一定就要去写出它。比如说算法,你知道常用的算法与时间复杂度,空间复杂度的计算。在遇到问题的时候,就知道选用何种算法去解决它。如果是在需要自己写的时候,也可以翻书再看看。但是这些如果你没有去学习,遇到问题时可能就没有这种思路。

Q: 谢谢贺老,我能和您拍张照,合影留念吗?

A: 当然可以,对于我来说,能帮你们这些年轻的工程师就非常好了。

总结

贺老的回答,真真切切的解决了我的疑惑,由于功利心太重,做什么事情都要权衡一二。
但是回望这一年多编程的学习,写代码时也只有快乐。
所谓功利心,可能就是自己想用更短的时间去写出更好的代码吧

与贺老的这一番面对面交流,是本次JSConf最大的收获了。

既然喜欢,去学就行,何必纠结那么多。

时夜,于回校列车上记录。


前端路漫漫,且行且歌。

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源码的必经之路。


前端路漫漫,且行且歌。