0%

深入理解margin

margin负值的应用

负margin可以增加元素宽度,典型应用为一列元素,每个元素都有一个margin-right,但是又不想最后那个元素的margin-right显示出来,在父元素和子元素中间增加一层元素来为它设置margin-right为负值,以此来增加父元素宽度,即可实现这种效果。

Read more »

性能优化

资源合并与压缩

主要是css和js文件的资源压缩合并,压缩可以减小文件体积,而合并则可减少HTTP请求,可通过webpack和一些现有的线上工具进行。

非核心脚本延迟/异步加载

异步加载包括三种方式:async和defer以及动态脚本创建。

  • async加载,是html5引入的机制,async加载规定脚本一旦加载好就立刻执行,并且各脚本加载的顺序不定。
  • defer加载规定脚本是否延迟加载,在HTML解析之后进行脚本加载,并且加载顺序和声明顺序一致
  • 动态脚本加载是在async和defer之前,动态创造script标签进行脚本加载,并在onload事件触发后执行。
Read more »

发布/订阅模式(观察者模式)

观察者模式中存在两种类型的实体,一种是观察者(observer或者subscriber),一种是订阅者发布者或者主体(publisher或subject)。当某事件触发时发布者通知(调用)所有的订阅者并且可以向订阅者传递消息。

报纸订阅例子

paper对象作为publisher对象,拥有subscribers数组属性,subscribe方法,unsubscribe方法以及publish方法。

我们在subscribers中维护我们的订阅者,subscribe方法中添加订阅者到数组中,而unsubscribe方法从数组中移除订阅者,而publish方法则遍历数组并调用它们注册时提供的方法。

所有这些方法还需要有个type参数,因为paper对象可以发射多种类型事件,而我们的订阅者可以只订阅其中一种事件。对于任何的发布者对象这些成员都是一致的,所以我们可以使用mixin模式将他们copy到任何对象中使之成为一个publisher。

简单说明mixin模式,就是将多个对象上的属性混合到一个对象上,如这里简单的实现,只适用于浅拷贝场景:

1
2
3
4
5
6
7
8
9
10
11
function mix() {
var arg, prop, child = {};
for (arg = 0; arg < arguments.length; arg += 1) {
for (prop in arguments[arg]) {
if (arguments[arg].hasOwnProperty(prop)) {
child[prop] = arguments[arg][prop];
}
}
}
return child;
}

koa-compose

要深入理解koa中的中间件的运行机制,必须要明白的是koa-compose这个包究竟做了些什么。koa-compose包接收一个数组,返回一个compose之后的函数,并且返回的函数接收一个ctx对象和next函数参数,并最终返回一个promise。理解koa-compose需要首先掌握递归概念以及ES6的promise和generator函数以及async/await等特性。

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
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!')
}

/**
* @param {Object} context
* @return {Promise}
* @api public
*/

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, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

compose这个函数在koa中是在handleRequest函数调用时传入的,在handleRequest的函数体中,调用了compose生成的结果函数fnMiddleware并且传入了合成的ctx对象,至此compose返回的函数的生命周期开始。我们进入到compose的函数体中,也就是下面这一段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}

首先context为application中createContext函数返回的context对象,而next最初为undefined。

中间件序号从0开始,将index首先设置为-1,表明上次调用的中间件的序号为-1,也就是代表程序初始状态。此时返回dispatch(0)。于是我们开始调用dispatch函数并且该函数最终返回一个promise。

这里假设我们有两个中间件函数,我们首先取到第一个中间件函数并将其赋值给fn变量,判断条件不成立,继续执行,返回一个promise,但是这个resolve方法的操作数是一个fn函数调用的结果,因此我们需要调用fn函数,并且将fn函数的返回结果传递到resolve方法中。而fn函数就是我们在app.use中传入的中间件函数,于是开始调用第一个中间件函数,执行中间件函数中的代码,如果遇到了await next(),那么我们就调用传入的next方法,而中间件函数中的next参数就是我们这里传入的dispatch.bind(null, i + 1)函数,于是下一轮的dispatch函数就开始了它的生命周期。

此时dispatch函数中的参数index和i均为1,而相应的fn函数也变成了第二个中间件函数。类似于上面的过程,我们依然返回了一个promise对象,且由于resolve方法的参数为fn函数调用的结果,我们则首先需要调用fn函数,也就是第二个中间件函数的函数体。在此过程中,我们既可以写await next(),也可以不写。

假如我们写了await next(),那么dispatch函数会再一次调用,并且传入的i参数又加1,此时变为2。于是我们调用dispatch(2)。但是此时判定条件成立,于是返回了一个空的promise对象,返回之后,我们继续上一轮fn函数next调用下面的函数内容,直至执行完毕。

此时第二个中间件函数体已然执行完毕,则第一次调用的resolve方法中的参数fn函数中的next部分就执行完毕,于是开始执行next调用下面的内容了,直至函数执行完毕,resolve中的操作数计算完成,此时就可以执行application.js中then方法中的handleResponse了。

总的来说,就是通过promise控制了中间件的调用流程,形成了一种类似栈方式的中间件调用,而本质还是通过递归实现的。

delegates

因为Koa.js在将request对象和response对象的属性委托到context对象时,依赖了delegates包,我们就来分析一下这个包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module.exports = Delegator;

/**
* Initialize a delegator.
*
* @param {Object} proto
* @param {String} target
* @api public
*/

function Delegator(proto, target) {
// 只有在new Delefator的时候this才会指向新创建的对象
// 若不是以new方式创建对象,则返回一个新的delegator实例
// 接着就继续执行构造函数
if (!(this instanceof Delegator)) return new Delegator(proto, target);
this.proto = proto;
this.target = target;
this.methods = [];
this.getters = [];
this.setters = [];
this.fluents = [];
}

delegates包对外暴露一个函数,其本质为一个构造函数,作用也是作为类存在的,我们通过暴露的构造函数可以实例化一个delegator。Koa.js中在context.js文件中利用了delegates将context对象上的某些getter,setter和方法委托给了context.request以及context.response对象。

Read more »

阅读Koa.js源码之application

application.js是整个koajs的入口文件,对外暴露的是一个Application类,下面以一段代码作为演示分析koa执行过程。

1
2
3
4
5
6
7
8
9
10
// 首先引入koa,这个对应的Koa就是application对外暴露的Application类。
const Koa = require('koa');
const app = new Koa();

// app作为Application类的实例,拥有use,listen方法
app.use(async ctx => {
ctx.body = 'Hello World';
});

app.listen(3000);

use实现

Read more »

Generator函数

生成器函数作为ES6的新特性之一,已经并不是什么新鲜的概念了。在此之前,Python,Php等均有生成器的概念。其本质就是一个返回iterator的函数,通过yield关键字可以暂停函数的执行,并在下一次调用相关方法时继续函数的执行。

基本用法

1
2
3
4
5
6
7
8
9
10
11
function* letterGenerator() {
yield 'a';
yield 'b';
yield 'c';
}

const letterIterator = letterGenerator();
letterIterator.next(); // {value: 'a', done: false}
letterIterator.next(); // {value: 'b', done: false}
letterIterator.next(); // {value: 'c', done: false}
letterIterator.next(); // {value: undefined, done: true}

可以看到通过调用生成器函数,我们得到了一个iterator对象。iterator对象包含next方法,可用于迭代iterable类型的数据。通过调用iterator的next方法,我们得到一个包含两个属性value和done的对象,用于标识我们的迭代过程,这也是generator函数最基本的用法。

Read more »

NoSQL

NoSQL(Not Only SQL)意指不仅仅是SQL(Structured Query Language)。是为了应对传统关系型数据库所不能解决的挑战而生的。可以分为以下四类:

  • 文档数据库(mongodb)
  • 键值对数据库(redis)
  • 列族(column-family)数据库(Cassandra)
  • 图数据库(Neo4J)

通用情况下,文档数据库能提供良好的性能以及可伸缩性。在不需要复杂的查询需求时,键值对数据库可以提供最佳性能。

文档数据库(document database)

文档(document)也就是自包含的一块信息,用于描述单一实体(entity)。如下面的JSON文档:

1
2
3
4
5
6
7
8
9
{
"firstName": "John",
"lastName": "Dahn",
"age": 25,
"address": {
"streetAddress": "21 2nd Street",
"city": "Shanghai"
}
}

当然也可以使用XML数据格式甚至是二进制格式表示这个document,在这里我们采用了JSON。关系型数据库中,这样的document会存放在两个不同的表中,一个persons表,一个addresses表。文档数据库中就仅仅是一个文档。

键值数据库

键值数据库就是保留了必要功能的精简版的文档数据库。键值数据库的键是一个特殊的ID,用于标识某一特定文档,而值则是该键对应的文档。不同的是,键值数据库只允许通过键查询,而文档数据库可以根据文档的内容进行查询,这就使得键值数据库可针对基于键的查询做性能上的优化,并且它们也可以对值进行压缩。

Redis就是很优秀的键值数据库,实际上它将整个数据库保存在RAM中,而在硬盘中做备份,具有闪电般的性能。

MongoDB

MongoDB名字来源于humongous,下载的MongoDB包含的mongod.exe是MongoDB的守护程序,也就是主服务器二进制文件。为了开启数据库服务器,可以执行它。而mongo.exe则是MongoDB提供的repl工具,用于管理数据库以及进行一些测试性的实验。

MongoDB的运行需要一个数据文件夹(在MongoDB的语境中叫做dbpath)来存储数据库数据。默认的dbpathdata/db(取决于当前工作的磁盘),最好总是指定一个明确的dbpath

JS中的继承

原型链

提到继承,不得不说的是JS中的原型链概念。JS不像传统的面向对象语言,在ES6出现以前,JS标准中并没有类的概念,所以往往是通过原型链的特性实现面向对象的继承。看过一些资料以后,发现其实原型链的概念十分简单,一句话总结:JS中的每个对象都有一个__proto__属性,而这个属性指向它的构造函数的prototype属性,而因为函数在JS中也是对象,所以函数本身也有__proto__属性,而在对象上进行属性查找就会沿着这条链一直向上进行,直到原型链的终点Object.prototypeObject.prototype.__proto__属性为null)。还有一个重要的点是构造函数的prototype对象有一个constructor属性,这个属性指向构造函数本身,我们可以利用这个特性建立对象和其构造函数之间的联系,因为我们知道,对于bar.constructor属性的查找,如果bar本身没有constructor属性,就会在其__proto__属性上查找,而__proto__又指向其构造器的prototype,所以依据这个特性,就能找到某对象的构造函数。另外一个便捷的判断实例构造器的方法是利用instanceof操作符,这个操作符会沿着对象的原型链一直向上查找直到对象的__proto____proto__…等于函数的prototype属性,或是一直到Object.prototype都没有相等,返回false

Read more »

Binary Tree Zigzag Level Order Traversal

Given a binary tree, return the zigzag level order traversal of its nodes’ values. (ie, from left to right, then right to left for the next level and alternate between).

For example:
Given binary tree [3,9,20,null,null,15,7],

1
2
3
4
5
  3
/ \
9 20
/ \
15 7

return its zigzag level order traversal as:

1
2
3
4
5
[
[3],
[20,9],
[15,7]
]

题意分析

以螺旋形层序遍历我们的二叉树。和普通的层序遍历相比,螺旋形只是在原来的基础上增加一个ltr变量,而在我们每次遍历完一层之后,只需要将这个变量取反即可。

代码实现

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
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
/**
* @param {TreeNode} root
* @return {number[][]}
*/
var zigzagLevelOrder = function(root) {
if(root == null) return [];
let res = [];
let ltr = true;
let levelResult = [];
for(let i = 1; i <= height(root); i++) {
traverseGivenLevel(root, i, ltr, levelResult);
res.push(levelResult);
ltr = !ltr;
levelResult = [];
}
return res;
};


// 参数:给定树根,层,以及遍历方向
// 返回一个遍历后该层的数组
function traverseGivenLevel(root, i, ltr, levelResult) {
if(root == null) return;
if(i == 1) {
levelResult.push(root.val);
} else if(i > 1) {
if(ltr) {
traverseGivenLevel(root.left, i - 1, ltr, levelResult);
traverseGivenLevel(root.right, i - 1, ltr, levelResult);
} else {
traverseGivenLevel(root.right, i - 1, ltr, levelResult);
traverseGivenLevel(root.left, i - 1, ltr, levelResult);
}
}
}

function height(root) {
if(root == null) return 0;
let lheight = height(root.left);
let rheight = height(root.right);
return lheight > rheight ? lheight + 1 : rheight + 1;
}

参考https://www.geeksforgeeks.org/level-order-traversal-in-spiral-form/