Cauil's Blog

陌上花开,可缓缓归矣


  • 首页

  • 归档

  • 标签

JS模式之工厂模式和迭代器模式

发表于 2018-01-13 | 分类于 设计模式

工厂模式

设计工厂模式的目的是为了创建对象。它通常在类或者类的静态方法中实现,具有下列目标:

  • 当创建相识对象时执行重复操作
  • 在编译时不知道具体类型的情况下,为工厂客户提供一种创建对象的接口

通过工厂方法创建的对象在设计上都继承了相同的父对象这个思想,它们都是实现专门功能的特定子类。有时候公共父类是一个包含了工厂方法的同一个类。

看一个汽车工厂实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function CarMaker() {}
CarMaker.prototype.drive = function() {
return "Vroom, I have " + this.doors + " doors";
}
CarMaker.factory = function(type) {
var constr = type, newcar;
if(typeof CarMaker[constr] !== "function") {
throw {
name: "Error",
message: constr + " doesn't exist"
}
}
if(typeof CarMaker[constr].prototype.drive !== "function") {
CarMaker[constr].prototype = new CarMaker();
}
newcar = new CarMaker[constr]();
return newcar;
}
CarMaker.Compact = function() {this.doors = 4}
CarMaker.Convertible = function() {this.doors = 2}
CarMaker.SUV = function() {this.doors = 24;}

再看看使用:

1
2
3
4
5
6
var corolla = CarMaker.factory('Compact');
var solstice = CarMaker.factory('Convertible');
var cherokee = CarMaker.factory('SUV');
corolla.drive()
solstice.drive()
chrokee.drive()

实现该工厂模式并没有特别的困难,所有需要做的就是寻找能够创建所需类型对象的构造函数。

迭代器模式

提起迭代器就会联想起ES6新支持的迭代协议,迭代器,可迭代对象;其实迭代器模式就是跟ES6的迭代器类似;

通常的迭代器模式中,有一个包含各种数据集合的对象。该数据可能存储在一个复杂数据结构内部,而要提供一种简单的方法能够访问数据结构中每个元素。对象的消费者并不需要知道如何组织数据,所有需要做的就是取出单个数据进行工作。

在迭代器模式中,需要提供一个next方法,还可以提供一个较为方便的hasNext方法,看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var agg = (functiono() {
var index = 0,
data = [1, 2, 3, 4, 5],
length = data.length;
return {
next: function() {
var element;
if(!this.hasNext()) {
return null;
}
element = data[index];
index = index +2;
return element;
},
hasNext: function() {
return index < length;
}
}
})();

除了上面的两个方法,还可以提供更简单的访问方式及多次迭代数据的能力,你的对象可以提供额外的便利方法:

rewind():重置指针到初始位置。
current(): 返回当前元素,因为不可能在不前进指针的情况下使用next()执行该操作。

1
2
3
4
5
6
7
8
9
10
11
12
var agg = (function() {
// ...
return {
// ...
rewind: function() {
index = 0;
},
current: function() {
return data[index]
}
}
})()

JS模式之策略模式和外观模式

发表于 2018-01-13 | 分类于 设计模式

策略模式

在开发中,表单验证经常会碰到,而且会需要验证不同的值,不同的匹配条件;这时候可以采用策略模式来统一起来处理,策略模式支持在运行时选择算法。

我们可以创建一个具有validate方法的验证器对象。无论表单的具体类型是什么,该方法都将会被调用,并且总是返回相同的结果;

验证器实现:

要验证的数据:

1
2
3
4
5
6
var data = {
first_name = "Super",
last_name = "Man",
age: "unknown",
username: "o_O"
}

配置不同数据采用不同的验证方法:

1
2
3
4
5
validator.config = {
first_name: 'isNonEmpty',
age: 'isNumber',
username: 'isAlphaNum'
};

使用方法:

1
2
3
4
validator.validate(data);
if(validator.hasErrors()) {
console.log(validator.messages.join("\n"))
})

验证器算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
validator.types = {
isNonEmpty: {
validate(value) {
return value !== '';
},
instructions: 'the value cannot be empty'
},
isNumber: {
validate(value) {
return !isNaN(value);
},
instructions: 'the value can only be a valid number, e.g. 1, 3.24, or 2010'
},
isAlphaNum: {
validate(value) {
return !/[^a-z0-9]/i.test(value);
},
instructions: 'the value can only contain characters and numbers, no special symbols'
}
}

验证器本身

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
var validator = {
types: {},
messages: [],
config: {},
validate: function(data) {
var i, msg, type, checker, result_ok;
this.messages = [];
for(i in data) {
if(data.hasOwnProperty(i)) {
type = this.config[i];
checker = this.types[type];
if(!type) {
continue;
}
if(!checker) {
throw {
name: 'VAlidationError',
message: 'No handler to validate type' + type
}
}
result_ok = checker.validate(data[i])
if(!result_ok) {
msg = 'invalid value for *' + i + '*, ' + checker.instructions;
this.message.push(msg)
}
}
}
return this.hasErrors()
},
hasErrors: function() {
return this.message.length !== 0;
}
}

这样,我们就实现了策略模式的关于表单验证的一个实例。

外观模式

外观模式是一种简单的模式,它为对象提供了一个可供选择的接口。这是一种非常好的实践,可保持方法的简洁性并且不会使他们处理过多的工作。如果原来有许多接受多个参数的uber方法,相比而言,按照本实现方法,最终将会创建更多数量的方法。有时候两个或更多的方法可能普遍的被一起调用。在这种情况下,创建另一个方法包装重复的方法调用是非常有意义的。

看个实践场景:

当处理浏览器事件时,你有以下方法:

  • stopPropagation 中止事件以避免其冒泡上升到父节点
  • preventDefault 阻止浏览器执行默认动作

并不需要在程序中到处复制者两个方法,可以创建一个外观方法从而同时调用者两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var myevent = {
// ...
stop: function(e) {
// 其他
if(typeof e.preventDefault === 'function') {
e.preventDefault()
}
if(typeof e.stopPropagation === 'function') {
e.stopPropagation();
}
if(typeof e.returnValue === 'boolean') {
e.returnValue = false;
}
if(typeof e.cancelBubble === 'boolean') {
e.cancelBubble = true;
}
}
}

JS模式之装饰器模式

发表于 2018-01-13 | 分类于 设计模式

装饰器模式

在装饰器模式中,可以在运行时动态添加附加功能到对象中。当处理静态类,这可能是一个挑战。在JS中,由于对象是可变的,因此,添加功能到对象中的过程本身并不是问题。

装饰者模式的一个比较方便的特征在于其预期行为的可定制和可配置特性。可以从仅具有一些基本功能的普通对象开始,然后从可用装饰资源池中选择需要用于增加普通对象的那些功能,并且按照顺序进行装饰;

一个例子

如果实现一个销售商品的价格,有原始价格、国税、地税以及币种的转换;

1
2
3
4
5
var sale = new Sale(100);
sale.decorate('fedax');
sale.decorate('quebec');
sale.decorate('money');
sale.getPrice()

实现:

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
function Sale(price) {
this.price = (price > 0) || 100;
this.decorators_list = [];
}
Sale.decorators = {};
Sale.decorators.fedtax = {
getPrice: function(price) {
return price + price*5/100;
}
};
Sale.decorators.quebec = {
getPrice: function(price) {
return price + price*7.5/100;
}
}
Sale.decorators.money = {
getPrice: function(price) {
return "$" + price.toFixed(2);
}
}
Sale.prototype.decorate = function(decorator) {
this.decorators_list.push(decorator);
}
Sale.prototype.getPrice = function() {
var price = this.price,
i,
max = this.decorators_list.length,
name;
for(i = 0; i < max; i += 1) {
name = this.decorators_list[i];
price = Sale.decorators[name].getPrice(price);
}
return price;
}

ECMASCRIPT装饰器

在ES8中,将会添加新的语言功能-装饰器;

新的功能目前已经处于stage2阶段,目前只可作用于class和class属性;

class member decorator

装饰器函数接受三个参数:

  • target - 所属的class
  • name - member的名字
  • descriptor - member的描述对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
class Example {
a() {}
@readonly
b() {}
}
const e = new Example();
e.a = 1;
e.b = 2; // TypeError: Cannot assign to read only property 'b' of object '#

目前这个装饰器使用需要用babel插件才可以使用,浏览器都没有支持;

class decorator

直接看使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function log(Class) {
return (...args) => {
console.log(args);
return new Class(...args);
};
}
@log
class Example {
constructor(name, age) {
}
}
const e = new Example('Graham', 34); // [ 'Graham', 34 ]
console.log(e); // Example {}

其实新的装饰器跟python有点类似,有点区别;类装饰器跟python装饰器思路是一致,只有类属性装饰器是是通过重新定义属性描述器中的值来装饰的。

JS模式之代理模式和中介者模式

发表于 2018-01-13 | 分类于 设计模式

代理模式

在代理模式中,一个对象充当另一个对象的借口。它与外观模式的区别之处在于,在外观模式中你所拥有的是多个方法调用的便利方法。代理则介于对象的客户端和对象本身之间,并且对该对象的访问进行保护。

这种模式可能看起来像是额外的开销,但是出于性能因素的考虑它却非常有用。代理充当了某个对象的守护对象,并且试图使本体对象做尽可能少的工作。

范例

在有视频服务器与客户端的通信之间,可以设置一个代理,进行合并多个请求,或者进行缓存,这样可以省去有些额外的开销;

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
var proxy = {
ids: [],
delay: 50,
timeout: null,
callback: null,
context: null,
makeRequest: function(id, callback, context) {
this.ids.push(id);
this.callback = callback;
this.context = context;
if(!this.timeout) {
this.timeout = setTimeout(function() {
proxy.flush();
}, this.delay)
}
},
flush: function() {
http.makeRequest(this.ids, 'proxy.handler');
this.timeout = null;
this.ids = [];
},
handler: function(data) {
var i, max;
if(parseInt(data.query.count, 10) === 1) {
proxy.callback.call(proxy.context, data.query.results.Video);
return;
}
for(i = 0, max = data.query.results.Video.length; i < max; i += 1) {
proxy.callback.call(proxy.context, data.query.results.Video[i]);
}
}
}

在代理中还可以设置缓存,这样当相同的请求来临时,可以进一步保护对本体对象的http的访问。那么,如果videos对象恰好再一次请求同一个视频,proxy可以从缓存中取出该信息,从而节省了该网络的往返信息。

中介者模式

应用程序中,无论大小都是由一些单个的对象所组成。所有这些对象需要一种方式来实现相互通信,而这种通信方式在一定程度上不降低可维护性,也不损害那种安全的改变部分应用程序儿不会破坏其余部分的能力。随着应用程序的增长,将添加越来越多的对象。然后在代码重构阶段,对象将被删除或重新整理。对对象互相知道太多信息并且直接通信(调用对方的方法并改变属性),这将导致产生不良的紧耦合问题。党对象间紧密耦合时,很难在改变单个对象的同时不影响其他多个对象。因而,即使对应用程序进行最简单的修改也变的不再容易,而且几乎无法估计修改可能话费的时间。

中介者模式缓解了该问题并促进形成松耦合,而且还有助于提高可维护性。在这种模式中,独立的对象之间并不直接通信,而是通过mediator对象。当其中的一个colleague对象改变状态以后,它将会通知该mediator,而mediator将会把该变化传达到任意其他应该知道此变化的colleague对象。

看一个按键游戏,两个玩家看谁按键得分多;

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
function Player(name){
this.points = 0;
this.name = name;
}
Player.prototype.play = function() {
this.points += 1;
mediator.played();
}
var coreboard = {
update(score) {
var i, msg = '';
for(i in score) {
msg += `<p><strong>${i}</strong>: ${score[i]}</p>`
}
}
}
var mediator = {
palyers: {},
setup: function() {
var players = this.players;
players.home = new Player('Home');
players.guest = new Player('Guest');
},
played: function(e) {
var players = this.players,
score = {
Home: players.home.points,
Guest: players.guest.points
};
scoreboard.update(score);
},
keypress: function(e) {
e = e || window.event;
if(e.which === 49) {
mediator.players.home.play();
return
}
if(e.which === 48) {
mediator.players.guest.play();
return
}
}
}
// 最后建立以及拆除该游戏
mediator.setup();;
window.onkeypress = mediator.keypress;
setTimeout(function() {
window.onkeypress = null;
alert('Game Over')
}, 30000)

JS设计模式之单体模式

发表于 2018-01-08 | 分类于 设计模式

单体模式的思想在于保证一个特定类仅有一个实例。这意味着当您第二次使用同一个类床创建新对象的时候,应该得到与第一次创建对象完全相同对象。

ES5之前

在ES5之前是没有类这种概念,只有对象。当创建一个新对象时,实际上没有其他对象与其类似,因此新对象已经是单体了。使用对象字面量创建一个简单的对象也是一个单体的例子:

1
2
3
var obj = {
myprop: 'my value'
}

在JS中,对象之间永远不会完全相等,除非它们是同一个对象,因此即使创建一个具有完全相同成员的同类对象,它也不会与第一个对象完全相等。

1
2
3
4
5
var obj2 = {
myprop: 'my value'
}
obj1 === obj2; // false
obj1 == obj2; // false

因此可以认为每次在使用对象字面量创建对象的时候,实际上就正在创建一个单体,并且并不涉及任何特殊语法。

new操作符

使用new可以创建对象,有时可能需要使用此方法的单体实现。这种思想在于当使用同一个构造函数以new操作符来创建多个对象时,应该仅获得指向完全相同的对象的新指针。

1
2
3
var uni = new Universe()
var uni2 = new Universe()
uni1 === uni2 // true

怎么实现上面的单体模式呢?

可能会想到使用缓存该对象实例this,以便当第二次调用该构造函数时能够创建并返回同一个对象。实现方式有多种:

  • 使用全局变量来缓存;但是不推荐,一般原则下,全局变量是有缺点的。
  • 可以在构造函数的静态属性中缓存该变量。唯一缺点是函数属性是公开访问的属性,在外部代码中可能会修改该属性,以至于让您丢失了该实例。
  • 将实例包装在闭包中。这样可以保证该实例的私有性并且保证该实例不会被构造函数之外的代码所修改。代价是带来了额外的闭包开销。

第二种实现

1
2
3
4
5
6
7
8
9
10
11
12
function Universe() {
if(typeof Universe.instance === 'object') {
return Universe.instance
}
this.start_time = 0;
this.bang = "big";
Universe.instance = this;
//return this;
}

第三种实现

1
2
3
4
5
6
7
8
9
10
function Universe() {
var instance = this;
this.start_time = 0;
this.bang = "big";
Universe = function() {
return instance;
}
}

这种实现有一些缺点,在于重写函数时会丢失所有在初始定义和重定义时刻之间添加到它里面的属性;在这里的特定情况下,任何添加到Universe()的原型中的对象都不会存在指向由原始实现所创建实例的活动链接。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
Universe.prototype.nothing = true;
var uni = new Universe()
Universe.prototype.everything = true;
var uni2 = new Universe();
uni.nothing // true
uni2.nothing // true
uni.everything // undefined
uni2.everything // undefined
uni.constructor.name // Universe
uni.constructor === Universe; // false

改进1

可以把第一次就返回改写Universe函数调用得到的实例,并链接原型链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Universe() {
var instance;
Universe = function() {
return instance;
}
Universe.prototype = this;
instance = new Universe();
instance.constuctor = Universe;
instance.start_time = 0;
instance.bang = "big";
return instance;
}

测试得到预期结果

1
2
3
4
5
6
7
8
9
10
11
12
13
Universe.prototype.nothing = true;
var uni = new Universe()
Universe.prototype.everything = true;
var uni2 = new Universe();
uni.nothing // true
uni2.nothing // true
uni.everything //true
uni2.everything // true
uni.constructor.name // Universe
uni.constructor === Universe; // true

改进2-立即执行函数

利用立即执行函数进行改写Universe,并在闭包里面封装一个实例,每次调用构造函数都返回这个实例;

1
2
3
4
5
6
7
8
9
10
11
12
13
var Universe;
(function() {
var instance;
Universe = function() {
if(instance) {
return instance
}
instance = this;
this.start_time = 0;
this.bang = 'Big';
}
}())

ES6实现方法

ES6实现原理其实跟ES5一样,只不过语法不一样而与;

1
2
3
4
5
6
7
8
9
10
class Universe {
constructor() {
if(!Universe.instance) {
this.start_time = 0;
this.bang = "Big";
Universe.instance = this;
}
return Universe.instance;
}
}

koa2探秘

发表于 2017-12-27 | 分类于 node

阅读背景:

  • 已经读过上一篇koa1的文章
  • async await
  • class

还是跟前面koa1文章类似,先看看koa2的使用方法;

koa2使用方法

1
2
3
4
5
6
7
8
9
10
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});`
app.listen(8888);

koa2原理

从上面代码可以看出,koa2中间件函数使用了异步函数、关键字async await;

使用async与await带来了什么呢?

async await class

熟悉async与await的童鞋一定会知道这是JS最新处理异步的关键字;

async会把正常函数转化为async函数,执行后会返回promise;async 函数中可能会有 await 表达式,这将会使 async 函数暂停执行,等待 Promise 正常解决后继续执行 async 函数并返回解决结果。

async/await的目的是简化同步使用 promises 的做法,并对一组Promises执行一些操作。正如Promises类似于结构化回调,async/await类似于组合生成器和 promises。

先看看koa现在主体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Application extends Emitter {
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
...
}

从上面看出现在koa主体代码使用了最新es6中的class、解构与展开. 这样带来一个限制,只能使用new关键字来生成实例,不然报错.

app.callback

下面是callback源码以及req来临的处理代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

上面代码与koa1最大不同的是中间件函数的处理,之前是co.wrap(compose(this.middleware)), 现在是compose(this.middleware); 这个到最后执行的关键函数是什么,怎么调用参数来执行;

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
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}

中间件处理之后生成函数其实就是

1
2
3
4
5
function(context, next) {
return Promise resolve(fn(context, function next() {
return dispatch(i+1)
}))
}

koa2执行流程

  1. 请求到来
  2. 执行fnMiddleware(ctx), 也就是上面的compose处理中间件得到的函数
  3. 执行dispatch(0)
  4. 其实就是执行中间件数组第一个中间件,直到await next()停止;
  5. 而next实际就是传递给第一个函数的第二个参数function next() {return dispatch(i+1)}
  6. 执行next函数,也就是执行dispatch(1), 从而进入到第二个中间件;
  7. 依次往复,直到执行到最后一个中间件next
  8. 当执行dispatch(n+1)时,n+1取到fn为空,则return Promise.resolve(), 接着执行最后一个中间件await后面的内容, 并返回一个resolve的Promise
  9. 然后返回到上一个中间件,执行await后面的内容,依次返回, 直到第一个中间件返回resolve了的promise
  10. 最后执行return fnMiddleware(ctx).then(handleResponse).catch(onerror);语句then后面的handleResponse
  11. 至此整个请求执行完所有中间件,并返回结果;

dispatch注意点

dispatch函数中有个需要注意的地方:

第一句if (i <= index) return Promise.reject(new Error('next() called multiple times'))

这个异常什么时候会执行呢, 当一个中间件中有多次await next()的时候,由于执行某一个中间件的时候根据闭包原理会存储一个i值,而多个await意味着多次执行next函数

1
function next() {return dispatch(i+1)}

第一次执行上面的函数返回后index就会等于i+1,当第二次执行到dispatch(i+1)里面,i值没变,就会得到i+1 === index, 报错。这也是koa1与koa2的不同点,也是解决在上一篇koa1文章结尾的问题.

总结

至此,就完成了koa2原理的核心部分,其余部分都是小变化。

也可以看出await与async简化了koa的实现,并且代码比koa1中的多次封装中间件、co函数的协助、委托生成器与迭代器好理解多了.

koa1探秘

发表于 2017-12-25 | 分类于 node

背景知识

掌握这些背景知识对理解koa1源码有帮助:

  • 原型链查找 闭包 高阶函数(返回函数的函数) getter/setter call/apply
  • 代理委托
  • promise 异步函数 迭代器 委托生成器(yield *)
  • co模块处理
  • node http模块

koa使用

先从koa1开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const Koa = require('koa');
const app = new Koa();
// response
app.use(function *() {
console.log('1-1');
yield next;
console.log('1-2');
});
app.use(function *() {
console.log('2-1');
yield next;
console.log('2-2');
});
app.listen(3000);
// 1-1
// 2-1
// 2-2
// 1-2

上面代码加了两个中间件,执行过程是:

每一个请求到达服务器后初始化请求上下文对象,然后按照中间件添加的顺序,一个一个执行:

  • 执行中间件1,next之前的逻辑
  • next

    • 执行中间2的逻辑
  • 执行中间件1,next之后的逻辑

其他的过程都是模拟上面的流程。

koa的方法

koa有公共方法use/listen,私有方法callback等;

其中use是添加中间件,listen是创建服务、监听端口,callback初始化中间件形成闭包并返回网络请求到达时的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
app.use = function(fn){
this.middleware.push(fn);
return this;
};
app.listen = function() {
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
app.callback = function(){
var fn = co.wrap(compose(this.middleware))
var self = this;
return function handleRequest(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).then(function handleResponse() {
respond.call(ctx);
}).catch(ctx.onerror);
}
};

koa实现原理

其实koa中间件调用过程实现原理就是根据之前写过一篇文章JS之异步1中讲述的异步生成器。

但是koa实现有所不同,koa采用的是根据promise来实现;一步一步看看上面关键callback做了什么工作;

co.wrap

上面关键代码var fn = co.wrap(compose(this.middleware))实现中间件的封装,其中co.warp其实就是返回一个函数,这个返回函数的函数体是把fn执行得到的结果作为参数传入co并执行:

1
2
3
4
5
6
co.wrap = function (fn) {
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};

compose

其中compose(this.middleware)实现的是一个迭代器的嵌套调用(这里有个背景知识,生成器函数执行是生成一个迭代器,也就是就是middleware[i].call的结果):

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

执行compose实际生成的可以看作返回了一个异步函数:

1
2
3
*function() {
return yield * itor1(itor2(itor3))
}

这个异步函数其实就是传入到co.wrap中的参数fn

再看最后一个关键函数co

co其实就是实现koa’剥洋葱’执行流程的执行体;

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
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
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(res);
} catch (e) {
return reject(e);
}
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);
}
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执行的时候,迭代器gen被传入,gen的值为上面compose生成的异步函数执行得到的迭代器,为啥是迭代器,因为co执行的时候,传入的是fn.call(…);

再回顾一下compose生成的异步函数:

1
2
3
\*function() {
return yield * itor1(itor2(itor3))
}

co执行过程:

  1. 返回一个promise,promise中包括的执行流程是:
  2. 如果gen是普通函数,直接执行;如果gen是不为空的值并且gen.next不为function也就是说gen不是迭代器,直接返回resolve(), 这个实际是co返回promise的resolve,也就是总的出口;
  3. gen是迭代器,执行gen.next(),也就是会执行yield * itor1(itor2(noop_itor)),yield *实际上是委托生成器; 会直接执行到itor1第一个yield next处;并且此时gen.next()得到ret;并且ret.value就是next的值,也就是itor1的传入的实参itor2(itor3);
  4. 我们快要接近剥洋葱的流程了,只要itor2重复前面的流程,怎么实现呢,实际就是toPromise的工作了,且看next(ret);
  5. next函数中:如果ret.done === true, 直接退出, 这也是一个出口;否则把ret.value转换为promise对象,当ret.value是一个迭代器的时候,toPromise主要工作是重复co.call(this, ret.value); 这样就重复的执行每个中间件的yield next前面的代码,直到执行到noop并层层返回;这实际有点类似二叉树的先序遍历过程;
  6. 最后会回到第一次返回promise的函数中,并且resolve,这样就可以执行fn.call(ctx).then(function handleResponse() {respond.call(ctx);}).catch(ctx.onerror);的then处理了,否则catch处理;
  7. 自此一个网络请求后的中间件处理完毕;

总执行过程

  • 请求到来
  • …
  • 执行fn.call(ctx),实际上就是执行co.wrap(compose(this.middleware)).call(ctx), 而co.wrap(...).call(ctx)实际就是上面的co函数的执行过程;
  • 中间件处理完毕, 然后执行respond.call(ctx);

koa的上下文

koa服务每当一个新的网络请求来临的时候会生成一个新的上下文,也就是代码中的多次出现的ctx;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var ctx = self.createContext(req, res);
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;
};

这个context是以koa的实例服务app.context为原型,并且对ctx(即context)的属性request、response也设计了原型,这有什么用处呢?

实际上我们获取ctx.header、ctx.method或者设置ctx.method等的值,就是通过这里来的;举一个例子,获取ctx.header

  • 1 ctx中没有,从原型app.context中寻找
  • 2 app.context从静态类context中寻找

    1
    2
    3
    4
    5
    6
    7
    function Application() {
    if (!(this instanceof Application)) return new Application;
    ...
    **this.context = Object.create(context);**
    this.request = Object.create(request);
    this.response = Object.create(response);
    }
  • 3 context中的header代理了request静态类的属性header;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    delegate(proto, 'request')
    .method('acceptsLanguages')
    ... // 省略
    .getter('header')
    ...
    .getter('ip');
    Delegator.prototype.getter = function(name){
    var proto = this.proto;
    var target = this.target;
    this.getters.push(name);
    proto.__defineGetter__(name, function(){
    return this[target][name];
    });
    return this;
    };
  • 4 也就是ctx[‘request’][‘header’]

  • 5 寻找ctx[‘request’][‘header’]也就是从ctx[‘request’]的原型app.request寻找,接着到静态类request中寻找

    1
    2
    3
    get header() {
    return this.req.headers;
    },
  • 6 也就是return ctx.request.req.headers, 其中ctx.request.req实际就是网络请求传入的req对象

  • 7 获取header成功

koa利用原型链查找、委托代理等手段,设置了很多方面快捷的属性获取、属性设置和方法调用;

总结

本文讲述了下面几方面内容:

  • koa简单使用
  • koa中间件执行过程
  • koa中间件执行原理
  • koa上下文ctx
  • koa快捷过程

不过koa也有不足之处,如果上层中间件设置了多次yield next,虽然后面的迭代器已经执行到done:true,但每次重新到下层中间件也是一种不必要的开销;koa1的代码也比较繁杂,理解起来比较困难;不过这些koa2都有所改善,koa2见。

Vue之mergeOptions

发表于 2017-12-01 | 分类于 Vue

new Vue(options)中首先处理的是下面一段代码, 这次就剖析这一段代码;

1
2
3
4
5
6
7
8
9
10
11
12
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}

初始化vm.$option

上面代码主要目的就是初始化vm.$option. 其中逻辑很简单,如果传入的option是一个组件,执行initInternalComponent, 否则执行mergeOptions;

initInternalComponent

initInternalComponent非常简单,就是以Vue.options为原型创建vm.$options, 并把options中的属性复制到vm.$options;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
opts.parent = options.parent
opts.propsData = options.propsData
opts._parentVnode = options._parentVnode
opts._parentListeners = options._parentListeners
opts._renderChildren = options._renderChildren
opts._componentTag = options._componentTag
opts._parentElm = options._parentElm
opts._refElm = options._refElm
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}

mergeOptions

mergeOptions接受三个参数,这个方法比较复杂; 首先接受三个参数

[parent, child, vm] = [resolveConstructorOptions(vm.constructor), options || {}, vm]

其中parent为resolveConstructorOptions(vm.constructor), 得到结果为Vue.options;child跟vm没什么说的;我们来看看mergeOptions做了什么;

具体代码在github这里; 主要逻辑是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
normalizeProps(child)
normalizeInject(child)
normalizeDirectives(child)
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}

其中normalizeProps/normalizeInject/normalizeDirectives主要是格式化对象;

normalizeProps主要是把options中的props格式化为如下对象 :

1
2
3
4
5
6
7
['bad-name', 'goodName'] // Array
// =>
{badName: {type: null}, goodName: {type: null}}
{badName: {type: 'String', default: 'hi'}, 'good-name': 'String'} // Object
// =>
{badName: {type: "String", default: "hi"}, goodName: {type: "String"}}

normalizeProps主要是把options中的inject格式化为如下对象:

1
2
3
4
5
6
7
['test', 'test1']
// =>
{test: {from: 'test'}, test1: {from: 'test1'}}
{test: 'hello', test1: {name: 'hi'}}
// =>
{test: {from: "hello"}, test1: {from: "test1", name: "hi"}}

normalizeDirectives主要是把options中的directives格式化为如下对象:

1
2
3
4
5
6
7
8
9
directives: {
hello: function() {console.log('hi')}, // function
hi: 'test'
}
// =>
directives: {
hello: {bind: function() {console.log('hi')}, update: function() {console.log('hi')} },
hi: 'test'
}

extend与mixins

接下来的代码其实是vue的混合用法,这里有官方文档-混合描述;
由于mixins与extend用法一致,只对mixins讲述:

1
2
3
4
5
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}

上面代码其实是递归调用mergeOptions, 并把mixins的内容递归处理到parent对象中;

接着初始化一个空对象{}这里我们记做obj, 并遍历parent里的key,mergeField到options中;

1
2
3
4
function mergeField (key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}

mergeField是一个多态函数,根据传递的key值不同,parent与child合并的方式不同:

  • 如果key是el或者propsData时: obj[key] = child[key] === undefined ? parent[key] : child[key]
  • 如果key是data, 则递归的一层一层把所有的parent不存在的数据复制到child
  • 如果key是vue实例的生命周期, 则obj[key] = parent[key].concat(child[key])

    beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed, activated, deactivated

  • 如果key是components, directives, filters这些key值,则

    1
    2
    const res = Object.create(parent[ke] || null)
    return child[key] ? extend(res, child[ke]) : res
  • 如果key是watch,与生命周期类似

  • 如果key是prosp/methods/inject/computed时,与componets类似;

也即:

  1. key值为生命周期或者watch时,同名钩子函数将混合为一个数组,因此都将被调用。另外,混合对象的钩子将在组件自身钩子之前调用.
  2. 当为prosp/methods/inject/computed/components/directives/filters时,将被混合为同一个对象。两个对象键名冲突时,取组件对象的键值对。

如下面一个例子:

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
var mixin = {
created: function () {
console.log('混合对象的钩子被调用')
},
methods: {
foo: function () {
console.log('foo')
},
conflicting: function () {
console.log('from mixin')
}
}
}
var vm = new Vue({
mixins: [mixin],
created: function () {
console.log('组件钩子被调用')
},
methods: {
bar: function () {
console.log('bar')
},
conflicting: function () {
console.log('from self')
}
}
})
// => "混合对象的钩子被调用"
// => "组件钩子被调用"
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"

总结

上面的所有步骤就是初始化传入的options中的一些对象, 并把混合的mixins和extend合并起来,最后赋值给vue实例的$options.

Vue实例化过程

发表于 2017-11-30 | 分类于 Vue

vue是一个灵活渐进的mvvm框架,其实际内容可以用 UI=f(State)这个公式来表示。

今天第一篇关于vue的剖析,从其实例化的过程来展开。

vue实例化

vue核心是声明式渲染,其初始化的实例为后面的过程准备了各种条件,看下面一段简单的vue例子:

1
2
3
4
5
6
7
8
9
10
<div id="app">
{{ message }}
</div>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})

第一段代码是要初始化的template,第二段是实例的真正初始化内容,最终template会被渲染成Hello Vue!;

那当执行new Vue(options)时里面到底执行了什么?

实例化流程

实际new Vue(options)是调用了Vue.prototype._init方法:

主要流程有:

merge options

第一个流程先简单剖析下,流程过程主要是初始化实例vm的$options, 把构造函数Vue.options({components:..., directives:..., filters:..., _base:...})和options合并赋值给vm.$options;

1
2
3
4
5
6
7
8
9
10
11
12
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}

设置代理initProxy(vm)

初始化声明周期initLifecycle(vm)

主要是初始化与vm声明周期有关的值

1
2
3
4
5
6
7
8
9
10
11
12
vm.$parent = parent;
vm.$root = parent ? parent.$root : vm;
vm.$children = [];
vm.$refs = {};
vm._watcher = null;
vm._inactive = null;
vm._directInactive = false;
vm._isMounted = false;
vm._isDestroyed = false;
vm._isBeingDestroyed = false;

initEvents(vm)

初始化事件属性:

1
2
vm._events = Object.create(null);
vm._hasHookEvent = false;

initRender(vm)

初始化render的时候需要的一些属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vm._vnode = null; // the root of the child tree
vm._staticTrees = null; // v-once cached trees
var options = vm.$options;
var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree
var renderContext = parentVnode && parentVnode.context;
vm.$slots = resolveSlots(options._renderChildren, renderContext);
vm.$scopedSlots = emptyObject;
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };

callHook(vm, ‘beforeCreate’)

调用‘beforeCreate’的hook函数var handlers = vm.$options[hook];

initInjections(vm)

对vm.$options中的injections处理

initState(vm)

这个过程非常重要,里面吹了props/methods/data/computed等数据并初始化了依赖、watchers、deps等,是整个vm实例能够渲染并且监听数据变化的关键;

1
2
3
4
5
6
7
8
9
10
11
12
13
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}

initProvide(vm)

var provide = vm.$options.provide;

callHook(vm, ‘created’)

调用‘created’的hook函数var handlers = vm.$options[hook];

vm.$mount(vm.$options.el)

这个是初始化vue实例的最后一步,把虚拟dom挂载到option.el(即’#app’)中;

以上就是vue实例化的所有流程,本文并没有介绍各个流程的详细处理过程,在后续文章中会一一展开。

vue实例生命周期

从上面流程中,其实可以看出一些实例生命周期的过程:

声明周期

ES6之Promise

发表于 2017-11-27 | 分类于 JS

Promise基础知识

Promise相当于异步操作结果的占位符,它不会去订阅一个事件,也不会传递一个回调函数给目标函数,而是让函数返回一个Promise:

let promise = readFile('example.txt')

上例子中,readFile不会立即开始读取文件,函数会返回一个表示异步读取操作的Promise对象,未来对这个对象的操作完全取决于Promise的声明周期。

Promise生命周期

生命周期有三个:pending、fullfilled、rejected

所有promise都有then方法,它接受两个参数:第一个当Promise的状态变为fullfilled时要调用的函数,与异步操作相关的附加数据都会传递给这个完成函数;第二个是当Promise的状态变为rejected时要调用的函数,其与完成时调用的函数类似,所有与失败相关的附加数据都会传递这个拒绝函数;

1
2
3
4
5
6
7
8
let promise = readFile("example.txt");
promise.then(function(contents) {
// fulfillment
console.log(contents);
}, function(err) {
// rejection
console.error(err.message);
});

如果一个对象实现了上面的then方法,那这个对象我们称之为thenable对象。所有的Promise对象都是thenable对象,但并非所有thenable对象都是Promise。

Promise被解决(resolved)时执行,这些任务最终会被加入到一个为Promise量身定制的独立队列中,而不会立即执行,当前面的任务完成后其才被调用;

1
2
3
4
5
var p = new Promise(function(resolve, reject){resolve()});
p.then(function(){console.log('do')});
console.log('do1');
// do1
// do

创建未完成的Promise

Promise的执行器会立即执行,然后执行后续流程中的代码;当Promise resolved之后会把任务加入到任务队列尾部,即调用resolve()后出触发一个异步操作,传入then和catch方法的函数会被添加到任务队列中并异步执行;

1
2
3
4
5
6
7
8
9
10
11
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log("Resolved.");
});
console.log("HI!");
// 'Promise'
// HI!
// Resolved

创建已处理的Promise

Promise.resolve() / Promise.reject()

除了上面的方法直接创建已处理的Promise;Promise的resolve和reject方法还可以接受非Promise的Thenable对象作为参数。如果传入非Promise的Thenable对象,则这些方法会创建一个新的Promise,并在then函数中被调用;

1
2
3
4
5
6
7
8
9
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});

全局的Promise拒绝处理

为了防止有些没有拒绝处理的promise的错误被忽略掉,可以用全局的Promise拒绝处理。

浏览器环境的拒绝处理

浏览器有两个事件来识别未处理的拒绝的;

  • unhandledrejection 在一个事件循环中,当Promise被拒绝,并且没有提供拒绝处理程序时,触发该事件
  • rejecttionhandled 在一个事件循环后,当Promise被拒绝时,若拒绝处理程序被调用,触发该事件;
1
2
3
4
5
6
7
8
9
10
11
12
let rejected;
window.onunhandledrejection = function(event) {
console.log(event.type); // "unhandledrejection"
console.log(event.reason.message); // "Explosion!"
console.log(rejected === event.promise); // true
};
window.onrejectionhandled = function(event) {
console.log(event.type); // "rejectionhandled"
console.log(event.reason.message); // "Explosion!"
console.log(rejected === event.promise); // true
};
rejected = Promise.reject(new Error("Explosion!"));

串联Promise

1
2
3
4
5
6
7
8
9
10
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
console.log(value);
}).then(function() {
console.log("Finished");
});
// 42
// Finished

务必在Promise链的末尾留一个有拒绝处理程序保证能够正确处理所有可能发生的错误。

Promise链的返回值

1
2
3
4
5
6
7
8
9
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value) {
console.log(value); // 42
return value + 1;
}).then(function(value) {
console.log(value); // 43
});

在Promise链中返回Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
p1.then(function(value) {
// first fulfillment handler
console.log(value); // 42
return p2;
}).then(function(value) {
// second fulfillment handler
console.log(value); // 43
});

响应多个Promise

Promise.all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.then(function(value) {
console.log(Array.isArray(value)); // true
console.log(value[0]); // 42
console.log(value[1]); // 43
console.log(value[2]); // 44
});

拒绝处理程序总是接受一个值而非数组,改值来自被拒绝Promise的拒绝值;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
reject(43);
});
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.catch(function(value) {
console.log(Array.isArray(value)) // false
console.log(value); // 43
});

Promise.race

race方法监听多个Promise,只要有一个Promise被解决,整个race就被解决;如果选解决的是已完成Promise,则返回已完成Promise,如果被解决的是已拒绝Promise,返回的是已拒绝Promise:

1
2
3
4
5
6
7
8
9
10
11
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = Promise.reject(43);
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.race([p1, p2, p3]);
p4.catch(function(value) {
console.log(value); // 43
});
12…4
cauil

cauil

37 日志
12 分类
23 标签
GitHub Weibo DouBan
© 2018 cauil
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.3