Coder Social home page Coder Social logo

biaochenxuying / blog Goto Github PK

View Code? Open in Web Editor NEW
4.6K 117.0 719.0 29.3 MB

大前端技术为主,读书笔记、随笔、理财为辅,做个终身学习者。

HTML 69.75% JavaScript 8.72% TypeScript 8.44% CSS 12.49% Vue 0.39% Shell 0.09% Stylus 0.09% Pug 0.04%
javascript vue react nodejs express html typescript webpack vue-typescript-element element-ui

blog's Introduction

芳华正茂始少年,时光正好,未来可期 !

技术为主,读书笔记、随笔、理财为辅,做个终身学习者。

My Github Status 🦸

More Repositories ... biaochenxuying biaochenxuying biaochenxuying biaochenxuying biaochenxuying biaochenxuying

Thank you for the visit.

Please give a Star if you like.

blog's People

Contributors

biaochenxuying avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

那些必会用到的 ES6 精粹

前言

最新的 ECMAScript 都已经到发布到 2018 版了。

我们应该有的态度是: Stay hungry ! Stay young !

从接触 vue 到工作中用到 vue 将近 2 年了,在开发 vue 项目中用到了很多 es6 的 api ,es6 给我的开发带来了很大便利。

本文只总结小汪在工作和面试中经常遇到的 ES6 及之后的新 api 。

有空就得多总结,一边总结,一边重温学习!!!

正文

1 let 和 const

let 的作用域与 const 命令相同:只在声明所在的块级作用域内有效。且不存在变量提升 。

1.1 let

let 所声明的变量,可以改变。

let a = 123
a = 456 // 正确,可以改变

let b = [123]
b = [456] // 正确,可以改变

1.2 const

const 声明一个只读的常量。一旦声明,常量的值就不能改变。

简单类型的数据(数值、字符串、布尔值),不可以变动

const a = 123
a = 456 // 报错,不可改变

const b = [123]
b = [456] // 报错,不可以重新赋值,不可改变

复合类型的数据(主要是对象和数组),可以这样子变动

const a = [123]
a.push(456) // 成功

const b = {}
b.name = 'demo'  // 成功

1.3 不存在变量提升

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

所以 for循环的计数器,就很合适使用 let 命令。

let a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

1.4 推荐

对于 数值、字符串、布尔值 经常会变的,用 let 声明。

对象、数组和函数用 const 来声明。

// 如经常用到的导出 函数
export const funA = function(){
    // ....
}

2 解构(Destructuring)

2.1 数组

一次性声明多个变量:

let [a, b, c] = [1, 2, 3];
console.log(a) // 1
console.log(b) // 2
console.log(c) // 3

结合扩展运算符:

let [head, ...tail] = [1, 2, 3, 4];
console.log(head) // 1
console.log(tail) // [2, 3, 4]

解构赋值允许指定默认值:

let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a'];
// x='a', y='b'

2.2 对象

解构不仅可以用于数组,还可以用于对象。

let { a, b } = { a: "aaa", b: "bbb" };
a // "aaa"
b // "bbb"

数组中,变量的取值由它 排列的位置 决定;而对象中,变量必须与 属性 同名,才能取到正确的值。

对象的解构也可以指定默认值。

let {x = 3} = {};
x // 3

let {x, y = 5} = {x: 1};
x // 1
y // 5

2.3 字符串

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

2.4 用途

  1. 交换变量的值
let x = 1;
let y = 2;

[x, y] = [y, x];
  1. 从函数返回多个值
// 返回一个数组

function example() {
  let [a, b, c] = [1, 2, 3]
  return  [a, b, c] 
}
let [a, b, c] = example();

// 返回一个对象
function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();
  1. 函数参数的默认值
function funA (a = 1, b = 2){
      return a + b;
}

funA(3) // 5 因为 a 是 3, b 是 2
funA(3,3) // 6 因为 a 是 3, b 是 3

  1. 输入模块的指定方法

加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

const { SourceMapConsumer, SourceNode } = require("source-map");

在 utils.js 中:

export const function A (){
    console.log('A')
}

export const function B (){
   console.log('B')
}

export const function C (){
     console.log('C')
}

在 组件中引用时:

import { A, B, C } from "./utils.js" 

//调用
A() // 输出 A 

3. 模板字符串(template string)

模板字符串(template string)用反引号(`)标识。

3.1 纯字符串

所有模板字符串的空格和换行,都是被保留的.

console.log(`输出值为 N, 

换行`)
// "输出值为 N

换行"

3.2 字符串中加变量

模板字符串中嵌入变量,需要将变量名写在 ${ } 之中

let x = 1;
let y = 2;

console.log(`输出值为:${x}`) // "输出值为:1"
console.log(`输出值为:${x + y}`) // "输出值为:3"

3.3 模板字符串之中还能调用函数。

function fn() {
  return "Hello World";
}

console.log(`输出值为:${fn()}`) // "输出值为:Hello World"

4. 字符串函数扩展

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = 'Hello world!';

s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

这三个方法都支持第二个参数,表示开始搜索的位置。

let s = 'Hello world!';

s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false

5. 数值扩展

5.1 指数运算符

ES2016 新增了一个指数运算符(**)。

2 ** 2 // 4
2 ** 3 // 8

这个运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。

// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512

上面代码中,首先计算的是第二个指数运算符,而不是第一个。

指数运算符可以与等号结合,形成一个新的赋值运算符(**=)。

let a = 1.5;
a **= 2;
// 等同于 a = a * a;

let b = 4;
b **= 3;
// 等同于 b = b * b * b;

6. 函数的扩展

除了在解构中说到的函数参数的默认值,还有不少经常会用到的方法。

6. 1 rest 参数

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用 arguments 对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

function add(...values) {
  let sum = 0;

  for (let val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

上面代码的 add 函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。

注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

// 报错
function f(a, ...b, c) {
  // ...
}

6.2 箭头函数

ES6 允许使用“箭头”(=>)定义函数。

const f = v => v;
console.log('输出值:', f(3)) // 输出值: 3
// 等同于
const f = function (v) {
  return v;
};

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

// 等同于
const f = function () { return 5 };

const sum = (num1, num2) => num1 + num2;
// 等同于
const sum = function(num1, num2) {
  return num1 + num2;
};

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用 return 语句返回。

const sum = (num1, num2) => { return num1 + num2; }

箭头函数的一个用处是简化回调函数。

const square = n => n * n;

// 正常函数写法
[1,2,3].map(function (x) {
  return x * x;
});

// 箭头函数写法
[1,2,3].map(x => x * x);

注意: 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。

this 对象的指向是可变的,但是在箭头函数中,它是固定的。

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

let id = 21;

foo.call({ id: 42 });
// id: 42

上面代码中,setTimeout 的参数是一个箭头函数,这个箭头函数的定义生效是在 foo 函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时 this 应该指向全局对象window,这时应该输出 21。但是,箭头函数导致 this 总是指向函数定义生效时所在的对象(本例是{ id: 42}),所以输出的是 42。

7. 数组的扩展

扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

7.1 数组合并的新写法。

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

7.2 函数调用。

function add(x, y) {
  return x + y;
}

const numbers = [4, 4];
add(...numbers) // 8

7.3 复制数组的简便写法。

const a1 = [1, 2];
// 写法一
const a2 = [...a1];
a2[0] = 2;
a1 // [1, 2]
// 写法二
const [...a2] = a1;
a2[0] = 2;
a1 // [1, 2]

上面的两种写法,a2 都是 a1 的克隆,且不会修改原来的数组。

7.4 将字符串转为真正的数组。

[...'hello']
// [ "h", "e", "l", "l", "o" ]

7.5 数组实例的 entries(),keys() 和 values()

用 for...of 循环进行遍历,唯一的区别是 keys() 是对键名的遍历、values() 是对键值的遍历,entries() 是对键值对的遍历。

for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"

7.6 includes()

Array.prototype.includes 方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的 includes 方法类似。ES2016 引入了该方法。

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

该方法的第二个参数表示搜索的起始位置,默认为 0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为 -4,但数组长度为 3 ),则会重置为从 0 开始。

[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true

8. 对象的扩展

8.1 属性和方法 的简洁表示法

let birth = '2000/01/01';

const Person = {

  name: '张三',

  //等同于birth: birth
  birth,

  // 等同于hello: function ()...
  hello() { console.log('我的名字是', this.name); }

};

8.2 Object.assign()

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

const target = { a: 1 };

const source1 = { b: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。

注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

const target = { a: 1, b: 1 };

const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

Object.assign 方法实行的是浅拷贝,而不是深拷贝。

const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);

obj1.a.b = 2;
obj2.a.b // 2

上面代码中,源对象 obj1 的 a 属性的值是一个对象,Object.assign 拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

9. Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set 本身是一个构造函数,用来生成 Set 数据结构。

// 基本用法
const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);
}
// 2 3 5 4


// 去除数组的重复成员
const array = [1, 1, 2, 3, 4, 4]
[...new Set(array)]
// [1, 2, 3, 4]

10. Promise 对象

Promise 是异步编程的一种解决方案。

Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为
rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)

const someAsyncThing = function(flag) {
  return new Promise(function(resolve, reject) {
    if(flag){
        resolve('ok');
    }else{
        reject('error')
    }
  });
};

someAsyncThing(true).then((data)=> {
  console.log('data:',data); // 输出 'ok'
}).catch((error)=>{
  console.log('error:', error); // 不执行
})

someAsyncThing(false).then((data)=> {
  console.log('data:',data); // 不执行
}).catch((error)=>{
  console.log('error:', error); // 输出 'error'
})

上面代码中,someAsyncThing 函数成功返回 ‘OK’, 失败返回 ‘error’, 只有失败时才会被 catch 捕捉到。

最简单实现:

// 发起异步请求
    fetch('/api/todos')
      .then(res => res.json())
      .then(data => ({ data }))
      .catch(err => ({ err }));

来看一道有意思的面试题:

setTimeout(function() {
  console.log(1)
}, 0);
new Promise(function executor(resolve) {
  console.log(2);
  for( var i=0 ; i<10000 ; i++ ) {
    i == 9999 && resolve();
  }
  console.log(3);
}).then(function() {
  console.log(4);
});
console.log(5);

这道题应该考察 JavaScript 的运行机制的。
首先先碰到一个 setTimeout,于是会先设置一个定时,在定时结束后将传递这个函数放到任务队列里面,因此开始肯定不会输出 1 。
然后是一个 Promise,里面的函数是直接执行的,因此应该直接输出 2 3 。
然后,Promise 的 then 应当会放到当前 tick 的最后,但是还是在当前 tick 中。
因此,应当先输出 5,然后再输出 4 。
最后在到下一个 tick,就是 1 。
答案:“2 3 5 4 1”

11. async 函数

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

async 函数的使用方式,直接在普通函数前面加上 async,表示这是一个异步函数,在要异步执行的语句前面加上 await,表示后面的表达式需要等待。async 是 Generator 的语法糖

    1. async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。
async function f() {
  return 'hello world';
}

f().then(v => console.log(v))
// "hello world"

上面代码中,函数 f 内部 return 命令返回的值,会被 then 方法回调函数接收到。

    1. async 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态。抛出的错误对象会被 catch 方法回调函数接收到。
async function f() {
  throw new Error('出错了');
}

f().then(
  result => console.log(result),
  error => console.log(error)
)
// Error: 出错了
    1. async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。也就是说,只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数。
      下面是一个例子:
async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log('完成'))
// "ECMAScript 2017 Language Specification"

上面代码中,函数 getTitle 内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行 then 方法里面的 console.log。

    1. 在 vue 中,我们可能要先获取 token ,之后再用 token 来请求用户数据什么的,可以这样子用:
methods:{
        getToken() {
            return new Promise((resolve, reject) => {
                this.$http.post('/token')
                    .then(res => {
                        if (res.data.code === 200) {
                           resolve(res.data.data)
                        } else {
                            reject()
                        }
                    })
                    .catch(error => {
                        console.error(error);
                    });
            })
       },
       getUserInfo(token) {
            return new Promise((resolve, reject) => {
                this.$http.post('/userInfo',{
                        token: token
                    })
                    .then(res => {
                        if (res.data.code === 200) {
                           resolve(res.data.data)
                        } else {
                            reject()
                        }
                    })
                    .catch(error => {
                        console.error(error);
                    });
            })
       },
       async initData() {
            let token = await this.getToken()
            this.userInfo = this.getUserInfo(token)
       },
}

##12. import 和 export

import 导入模块、export 导出模块

// example2.js  // 导出默认, 有且只有一个默认
export default const example2 = {
  name : 'my name',
  age : 'my age',
  getName  = function(){  return 'my name' }
}
//全部导入 // 名字可以修改
import people from './example2.js'

-------------------我是一条华丽的分界线---------------------------

// example1.js // 部分导出
export let name  = 'my name'
export let age  = 'my age'
export let getName  = function(){ return 'my name'}

// 导入部分 // 名字必须和 定义的名字一样。
import  {name, age} from './example1.js'

//有一种特殊情况,即允许你将整个模块当作单一对象进行导入
//该模块的所有导出都会作为对象的属性存在
import * as example from "./example1.js"
console.log(example.name)
console.log(example.age)
console.log(example.getName())

-------------------我是一条华丽的分界线---------------------------

// example3.js  // 有导出默认, 有且只有一个默认,// 又有部分导出
export default const example3 = {
  birthday : '2018 09 20'
}
export let name  = 'my name'
export let age  = 'my age'
export let getName  = function(){ return 'my name'}

// 导入默认与部分
import example3, {name, age} from './example1.js'

总结:

1.当用 export default people 导出时,就用 import people 导入(不带大括号)

2.一个文件里,有且只能有一个 export default。但可以有多个 export。

3.当用 export name 时,就用 import { name }导入(记得带上大括号)

4.当一个文件里,既有一个 export default people, 又有多个 export name 或者 export age 时,导入就用 import people, { name, age } 

5.当一个文件里出现 n 多个 export 导出很多模块,导入时除了一个一个导入,也可以用 import * as example

##13. Class

对于 Class ,小汪用在 react 中较多。

13.1基本用法:

//定义类
class FunSum {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  sum() {
    console.log( this.x +this.y')
  }
}

// 使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。
let f = new FunSum(10, 20);
f.sum() // 30

13.2 继承

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

上面代码中,constructor 方法和 toString 方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的 this 对象。

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor() {
  }
}

let cp = new ColorPoint(); // ReferenceError

上面代码中,ColorPoint 继承了父类 Point,但是它的构造函数没有调用 super 方法,导致新建实例时报错。

最后

总结和写博客的过程就是学习的过程,是一个享受的过程 !!!

好了,面试和工作中用到 ES6 精粹几乎都在这了。

如果你觉得该文章对你有帮助,欢迎到我的 github star 一下,谢谢。
github 地址

文章很多内容参考了:ECMAScript 6 标准入门

如果你是 JavaScript 语言的初学者,建议先看 《JavaScript 语言入门教程》

js 实现上下改变父 div 的高度,左右上下动态分割孩子的宽高

1. 需求

实现父 div 里面 左右,上下动态分割 div,并上下改变父 div 的高度,并且宽和高都是按百分比(如图) 。

2. 实现原理

2.1 父布局

<div class='hj-wrap'>
        <div class="arrow"></div>
    </div>
  • 首先一个父 div 为 hj-wrap,相对定位 。
  • 一个改变父 div 高度的 arrow,用于上下拖动 , 不能占有位置,所以要绝对定位,并定位到最右下角。
  • 上下拖动的 arrow,当上拖动时,arrow 的父 div 的高度变小,当下拖动时,arrow 的父 div 的高度变大。

2.2 横向布局

<div class='hj-wrap'>
        <div class="hj-transverse-split-div">
            横 向
            <label class="hj-transverse-split-label"></label>
        </div>
        <div class="hj-transverse-split-div">横 向 2
            <label class="hj-transverse-split-label"></label>
        </div>
        <div class="hj-transverse-split-div">横 向 3
            <label class="hj-transverse-split-label"></label>
        </div>
        <div class="hj-transverse-split-div">横 向 4
            <label class="hj-transverse-split-label"></label>
        </div>
        <div class="hj-transverse-split-div">横 向 5
        </div>
        <div class="arrow"></div>
    </div>
  • 每一个横向的 div 为 hj-transverse-split-div 并相对定位,里面有一个拖动改变左右的 label 为 hj-transverse-split-label ,不能占有位置,所以要绝对定位,并定位到最右边并高为 100%,最后一个横向的 div 不用 hj-transverse-split-label 。
  • 拖动改变左右的 label 时,向左时,label 的父 div 的宽变小,label 的父 div 相邻的 右边的 div 宽度变大。

2.3 竖向布局

<div class='hj-wrap verticals'>
        <div class="hj-vertical-split-div">上
            <label class="hj-vertical-split-label"></label>
        </div>
        <div class="hj-vertical-split-div">中
            <label class="hj-vertical-split-label"></label>
        </div>
        <div class="hj-vertical-split-div">下</div>
        <div class="arrow"></div>
    </div>
  • 每一个横向的 div 为 hj-vertical-split-div 并相对定位,里面有一个拖动改变左右的 label 为 hj-vertical-split-label ,不能占有位置,所以要绝对定位,并定位到最下边并宽为 100%,最后一个竖向的 div 不用再放 hj-vertical-split-label 的 label 。
  • 拖动改变上下的 label 时,向上时,label 的父 div 的高度变小,label 的父 div 相邻的下边的 div 高度变大。

3. js 实现

代码:

/**
 * name:   split.js
 * author:  biaochen
 * date:    2018-12-26
 *
 */
$(function() {
    //鼠标横向、竖向、和改变父高度的上下 操作对象
    var thisTransverseObject, thisVerticalObject, thisArrowObject;
    //文档对象
    var doc = document;
    //横向分割栏
    var transverseLabels = $(".hj-wrap").find(".hj-transverse-split-label");
    //竖向分割栏
    var verticalLabels = $(".hj-wrap").find(".hj-vertical-split-label");
    // 改变父高度的 箭头 div
    var parentArrow = $(".hj-wrap").find(".arrow");

    // 设置宽
    function setWidth(type) {
        if (type === "init") {
            var length = $(".hj-wrap").length;
            if (length > 0) {
                for (var i = 0; i < length; i++) {
                    var width = $($(".hj-wrap")[i])[0].offsetWidth;
                    var hjDivNums = $($(".hj-wrap")[i]).children(".hj-transverse-split-div");
                    // var defaultWidth = Math.floor(100 / hjDivNums.length);
                    var defaultWidth = Math.floor(width / hjDivNums.length);
                    $($(".hj-wrap")[i])
                        .children(".hj-transverse-split-div")
                        .width(defaultWidth + "px");
                    // .width(defaultWidth + "%");
                }
            }
        } else {
            // 设置百分比
            var transverseDivs = $(".hj-transverse-split-div")
            var widthLength = transverseDivs.length
            for (var i = 0; i < widthLength; i++) {
                var width = $(transverseDivs[i]).width();
                var parentWidth = $(transverseDivs[i])
                    .parent()
                    .width();
                var rate = (width / parentWidth) * 100 + "%";
                $(transverseDivs[i]).css({ width: rate });
            }
        }
    }

    // 设置高
    function setHeight(type) {
        if (type === "init") {
            var verticalsParentDivs = $(".verticals");
            var parentLengths = verticalsParentDivs.length;
            for (var i = 0; i < parentLengths; i++) {
                var parentHeight = $(verticalsParentDivs[i]).height();
                var childrenNum = $(verticalsParentDivs[i]).children(
                    ".hj-vertical-split-div"
                ).length;
                var defaultHeight = Math.floor(parentHeight / childrenNum);
                // var rate = Math.floor((height / parentHeight)* 100)  + '%'
                var defaultHeight = Math.floor(100 / childrenNum);
                $(verticalsParentDivs[i])
                    .children(".hj-vertical-split-div")
                    .height(defaultHeight + "%");
                // .height(defaultHeight + "px");
            }
        } else {
            // 设置百分比
            var verticalsDivs = $(".hj-vertical-split-div");
            var heightLength = verticalsDivs.length;
            for (var i = 0; i < heightLength; i++) {
                var height = $(verticalsDivs[i]).height();
                var parentHeight = $(verticalsDivs[i])
                    .parent()
                    .height();
                var rate = (height / parentHeight) * 100 + "%";
                $(verticalsDivs[i]).css({ height: rate });
            }
        }
    }

    setWidth('init')
    setHeight("init");

    //定义一个对象
    function PointerObject() {
        this.el = null; //当前鼠标选择的对象
        this.clickX = 0; //鼠标横向初始位置
        this.clickY = 0; //鼠标竖向初始位置
        this.transverseDragging = false; //判断鼠标可否横向拖动
        this.verticalDragging = false; //判断鼠标可否竖向拖动
    }
    //横向分隔栏绑定事件
    transverseLabels.bind("mousedown", function(e) {
        thisTransverseObject = new PointerObject();
        thisTransverseObject.transverseDragging = true; //鼠标可横向拖动
        thisTransverseObject.el = this;
        thisTransverseObject.clickX = e.pageX; //记录鼠标横向初始位置
    });

    //竖向分隔栏绑定事件
    verticalLabels.bind("mousedown", function(e) {
        //console.log("mousedown");
        thisVerticalObject = new PointerObject();
        thisVerticalObject.verticalDragging = true; //鼠标可竖向拖动
        thisVerticalObject.el = this;
        thisVerticalObject.clickY = e.pageY; //记录鼠标竖向初始位置
    });
    //上下绑定事件
    parentArrow.bind("mousedown", function(e) {
        //console.log("mousedown");
        thisArrowObject = new PointerObject();
        // thisArrowObject.transverseDragging = true; //鼠标可横向拖动
        thisArrowObject.verticalDragging = true; //鼠标可竖向拖动
        thisArrowObject.el = this;
        thisArrowObject.clickY = e.pageY; //记录鼠标竖向初始位置
    });

    doc.onmousemove = function(e) {
        //鼠标横向拖动
        if (thisTransverseObject != null) {
            if (thisTransverseObject.transverseDragging) {
                var changeDistance = 0;
                if (thisTransverseObject.clickX >= e.pageX) {
                    //鼠标向左移动
                    changeDistance =
                        Number(thisTransverseObject.clickX) - Number(e.pageX);
                    if (
                        $(thisTransverseObject.el)
                        .parent()
                        .width() -
                        changeDistance <
                        20
                    ) {} else {
                        $(thisTransverseObject.el)
                            .parent()
                            .width(
                                $(thisTransverseObject.el)
                                .parent()
                                .width() - changeDistance
                            );
                        $(thisTransverseObject.el)
                            .parent()
                            .next()
                            .width(
                                $(thisTransverseObject.el)
                                .parent()
                                .next()
                                .width() + changeDistance
                            );
                        thisTransverseObject.clickX = e.pageX;
                        $(thisTransverseObject.el).offset({ left: e.pageX });
                    }
                } else {
                    //鼠标向右移动
                    changeDistance =
                        Number(e.pageX) - Number(thisTransverseObject.clickX);
                    if (
                        $(thisTransverseObject.el)
                        .parent()
                        .next()
                        .width() -
                        changeDistance <
                        20
                    ) {} else {
                        $(thisTransverseObject.el)
                            .parent()
                            .width(
                                $(thisTransverseObject.el)
                                .parent()
                                .width() + changeDistance
                            );
                        $(thisTransverseObject.el)
                            .parent()
                            .next()
                            .width(
                                $(thisTransverseObject.el)
                                .parent()
                                .next()
                                .width() - changeDistance
                            );
                        thisTransverseObject.clickX = e.pageX;
                        $(thisTransverseObject.el).offset({ left: e.pageX });
                    }
                }
                $(thisTransverseObject.el).width(2);
            }
        }
        //鼠标竖向拖动
        if (thisVerticalObject != null) {
            if (thisVerticalObject.verticalDragging) {
                var changeDistance = 0;
                if (thisVerticalObject.clickY >= e.pageY) {
                    //鼠标向上移动
                    changeDistance = Number(thisVerticalObject.clickY) - Number(e.pageY);
                    if (
                        $(thisVerticalObject.el)
                        .parent()
                        .height() -
                        changeDistance <
                        20
                    ) {} else {
                        $(thisVerticalObject.el)
                            .parent()
                            .height(
                                $(thisVerticalObject.el)
                                .parent()
                                .height() - changeDistance
                            );
                        $(thisVerticalObject.el)
                            .parent()
                            .next()
                            .height(
                                $(thisVerticalObject.el)
                                .parent()
                                .next()
                                .height() + changeDistance
                            );
                        thisVerticalObject.clickY = e.pageY;
                        $(thisVerticalObject.el).offset({ top: e.pageY });
                    }
                } else {
                    //鼠标向下移动
                    changeDistance = Number(e.pageY) - Number(thisVerticalObject.clickY);
                    if (
                        $(thisVerticalObject.el)
                        .parent()
                        .next()
                        .height() -
                        changeDistance <
                        20
                    ) {} else {
                        $(thisVerticalObject.el)
                            .parent()
                            .height(
                                $(thisVerticalObject.el)
                                .parent()
                                .height() + changeDistance
                            );
                        $(thisVerticalObject.el)
                            .parent()
                            .next()
                            .height(
                                $(thisVerticalObject.el)
                                .parent()
                                .next()
                                .height() - changeDistance
                            );
                        thisVerticalObject.clickY = e.pageY;
                        $(thisVerticalObject.el).offset({ top: e.pageY });
                    }
                }
                $(thisVerticalObject.el).height(2);
            }
        }
        // 改变父的 高度
        if (thisArrowObject != null) {
            //鼠标竖向拖动
            if (thisArrowObject.verticalDragging) {
                var changeDistance = 0;
                if (thisArrowObject.clickY >= e.pageY) {
                    //鼠标向上移动
                    changeDistance = Number(thisArrowObject.clickY) - Number(e.pageY);
                    if (
                        $(thisArrowObject.el)
                        .parent()
                        .height() -
                        changeDistance <
                        50
                    ) {} else {
                        $(thisArrowObject.el)
                            .parent()
                            .height(
                                $(thisArrowObject.el)
                                .parent()
                                .height() - changeDistance
                            );
                        thisArrowObject.clickY = e.pageY;
                        $(thisArrowObject.el).offset({ bottom: e.pageY });
                    }
                } else {
                    //鼠标向下移动
                    changeDistance = Number(e.pageY) - Number(thisArrowObject.clickY);
                    $(thisArrowObject.el)
                        .parent()
                        .height(
                            $(thisArrowObject.el)
                            .parent()
                            .height() + changeDistance
                        );
                    thisArrowObject.clickY = e.pageY;
                    $(thisArrowObject.el).offset({ bottom: e.pageY });
                }
                $(thisArrowObject.el).height(10);
            }
        }
    };

    $(doc).mouseup(function(e) {
        setHeight("setHeight");
        setWidth("setWidth");
        // 鼠标弹起时设置不能拖动
        if (thisTransverseObject != null) {
            thisTransverseObject.transverseDragging = false;
            thisTransverseObject = null;
        }
        if (thisVerticalObject != null) {
            thisVerticalObject.verticalDragging = false;
            thisVerticalObject = null;
        }
        if (thisArrowObject != null) {
            thisArrowObject.verticalDragging = false;
            thisArrowObject = null;
        }

        e.cancelBubble = true;
    });
});

4. 完整代码与效果

效果图:

split.gif

项目地址:https://github.com/biaochenxuying/split
效果体验地址: https://biaochenxuying.github.io/split/index.html

初始代码是从网上来的,不过网上的并不完整,父 div 的高也不能改变,并且孩子的宽高并不是百分比的,布局也并不合理,所以修改成这样子。

5. 最后

一次网站的性能优化之路 -- 天下武功,唯快不破

首屏作为直面用户的第一屏,其重要性不言而喻,如何加快加载的速度是非常重要的一课。

本文讲解的是:笔者对自己搭建的个人博客网站的速度优化的经历。

效果体验地址: http://biaochenxuying.cn

1. 用户期待的速度体验

2018 年 8 月,百度搜索资源平台发布的《百度移动搜索落地页体验白皮书 4.0 》中提到:页面的首屏内容应在 1.5 秒内加载完成

也许有人有疑惑:为什么是 1.5 秒内?哪些方式可加快加载速度?以下将为您解答这些疑问!

移动互联网时代,用户对于网页的打开速度要求越来越高。百度用户体验部研究表明,页面放弃率和页面的打开时间关系如下图所示:

页面放弃率和页面的打开时间关系

根据百度用户体验部的研究结果来看,普通用户期望且能够接受的页面加载时间在 3 秒以内。若页面的加载时间过慢,用户就会失去耐心而选择离开,这对用户和站长来说都是一大损失。

百度搜索资源平台有 “闪电算法” 的支持,为了能够保障用户体验,给予优秀站点更多面向用户的机会,“闪电算法”在 2017 年 10 月初上线。

闪电算法 的具体内容如下:

移动网页首屏在 2 秒之内完成打开的,在移动搜索下将获得提升页面评价优待,获得流量倾斜;同时,在移动搜索页面首屏加载非常慢(3 秒及以上)的网页将会被打压。

2. 分析问题

未优化之前,首屏时间居然大概要 7 - 10 秒,简直不要太闹心。

开始分析问题,先来看下 network :

主要问题:

  • 第一个文章列表接口用了 4.42 秒
  • 其他的后端接口速度也不快
  • 另外 js css 等静态的文件也很大,请求的时间也很长

我还用了 Lighthouse 来测试和分析我的网站。

Lighthouse 是一个开源的自动化工具,用于改进网络应用的质量。 你可以将其作为一个 Chrome 扩展程序运行,或从命令行运行。 为 Lighthouse 提供一个需要审查的网址,它将针对此页面运行一连串的测试,然后生成一个有关页面性能的报告。

未优化之前:

image.png

上栏内容分别是页面性能、PWA(渐进式 Web 应用)、可访问性(无障碍)、最佳实践、SEO 五项指标的跑分。

下栏是每一个指标的细化性能评估。

再看下 Lighthouse 对性能问题给出了可行的建议、以及每一项优化操作预期会帮我们节省的时间:

image.png

从上面可以看出,主要问题:

  • 图片太大
  • 一开始图片就加载了太多

知道问题所在就已经成功了一半了,接下来便开始优化之路。

2. 优化之路

网页速度优化的方法实在太多,本文只说本次优化用到的方法。

2.1 前端优化

本项目前端部分是用了 react 和 antd,但是 webpack 用的还是 3.8.X 。

2.1.1 webpack 打包优化

因为 webpack4 对打包做了很多优化,比如 Tree-Shaking ,所以我用最新的 react-create-app 重构了一次项目,把项目升级了一遍,所有的依赖包都是目前最新的稳定版了,webpack 也升级到了 4.28.3 。

用最新 react-create-app 创建的项目,很多配置已经是很好了的,笔者只修改了两处地方。

    1. 打包配置修改了 webpack.config.js 的这一行代码:
// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';

// 把上面的代码修改为: 
const shouldUseSourceMap = process.env.NODE_ENV === 'production' ? false : true;

生产环境下,打包去掉 SourceMap,静态文件就很小了,从 13M 变成了 3M 。

    1. 还修改了图片打包大小的限制,这样子小于 40K 的图片都会变成 base64 的图片格式。
{
      test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/,/\.jpg$/,/\.svg$/],
      loader: require.resolve('url-loader'),
      options: {
            limit: 40000, // 把默认的 10000 修改为 40000
            name: 'static/media/[name].[hash:8].[ext]',
      },
 }

2.1.2 去掉没用的文件

比如之前可能觉得会有用的文件,后面发现用不到了,注释或者删除,比如 reducers 里面的 home 模块。

import { combineReducers } from 'redux'
import { connectRouter } from 'connected-react-router'
// import { home } from './module/home'
import { user } from './module/user'
import { articles } from './module/articles'

const rootReducer = (history) => combineReducers({
 // home, 
  user,
  articles,
  router: connectRouter(history)
})

2.1.3 图片处理

  • 把一些静态文件再用 photoshop 换一种格式或者压缩了一下, 比如 logo 图片,原本 111k,压缩后是 23K。

  • 首页的文章列表图片,修改为懒加载的方式加载。

之前因为不想为了个懒加载功能而引用一个插件,所以想自己实现,看了网上关于图片懒加载的一些代码,再结合本项目,实现了一个图片懒加载功能,加入了 事件的节流(throttle)与防抖(debounce)

代码如下:

// fn 是事件回调, delay 是时间间隔的阈值
function throttle(fn, delay) {
  // last 为上一次触发回调的时间, timer 是定时器
  let last = 0,
    timer = null;
  // 将throttle处理结果当作函数返回
  return function() {
    // 保留调用时的 this 上下文
    let context = this;
    // 保留调用时传入的参数
    let args = arguments;
    // 记录本次触发回调的时间
    let now = +new Date();

    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
    if (now - last < delay) {
      // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
      clearTimeout(timer);
      timer = setTimeout(function() {
        last = now;
        fn.apply(context, args);
      }, delay);
    } else {
      // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
      last = now;
      fn.apply(context, args);
    }
  };
}

// 获取可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
// 用新的 throttle 包装 scroll 的回调
const lazyload = throttle(() => {
  // 获取所有的图片标签
  const imgs = document.querySelectorAll('#list .wrap-img img');
  // num 用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
  let num = 0;
  for (let i = num; i < imgs.length; i++) {
    // 用可视区域高度减去元素顶部距离可视区域顶部的高度
    let distance = viewHeight - imgs[i].getBoundingClientRect().top;
    // 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
    if (distance >= 100) {
      // 给元素写入真实的 src,展示图片
      let hasLaySrc = imgs[i].getAttribute('data-has-lazy-src');
      if (hasLaySrc === 'false') {
        imgs[i].src = imgs[i].getAttribute('data-src');
        imgs[i].setAttribute('data-has-lazy-src', true); // 
      }
      // 前 i 张图片已经加载完毕,下次从第 i+1 张开始检查是否露出
      num = i + 1;
    }
  }
}, 1000);

注意:给元素写入真实的 src 了之后,把 data-has-lazy-src 设置为 true ,是为了避免回滚的时候再设置真实的 src 时,浏览器会再请求这个图片一次,白白浪费服务器带宽。

具体细节请看文件 文章列表

2.2 后端优化

后端用到的技术是 node、express 和 mongodb。

后端主要问题是接口速度很慢,特别是文章列表的接口,已经是分页请求数据了,为什么还那么慢呢 ?

所以查看了接口返回内容之后,发现返回了很多列表不展示的字段内容,特别是文章内容都返回了,而文章内容是很大的,占用了很多资源与带宽,从而使接口消耗的时间加长

列表

从上图可以看出文章列表接口只要返回文章的 标题、描述、封面、查看数,评论数、点赞数和时间即可。

所以把不需要给前端展示的字段注释掉或者删除。

// 待返回的字段
      let fields = {
        title: 1,
        // author: 1,
        // keyword: 1,
        // content: 1,
        desc: 1,
        img_url: 1,
        tags: 1,
        category: 1,
        // state: 1,
        // type: 1,
        // origin: 1,
        // comments: 1,
        // like_User_id: 1,
        meta: 1,
        create_time: 1,
        // update_time: 1,
      };

同样对其他的接口都做了这个处理。

后端做了处理之后,所有的接口速度都加快了,特别是文章列表接口,只用了 0.04 - 0.05 秒左右,相比之前的 4.3 秒,速度提高了 100 倍,简直不要太爽, 效果如下:

image.png

此刻心情如下:

2.3 服务器优化

你以为前后端都优化一下,本文就完了 ?小兄弟,你太天真了,重头戏在后头 !

笔者服务器用了 nginx 代理。

做的优化如下:

  • 隐藏 nginx 版本号

一般来说,软件的漏洞都和版本相关,所以我们要隐藏或消除 web 服务对访问用户显示的各种敏感信息。

如何查看 nginx 版本号? 直接看 network 的接口或者静态文件请求的 Response Headers �即可。

没有设置之前,可以看到版本号,比如我网站的版本号如下:

Server: nginx/1.6.2

设置之后,直接显示 nginx 了,没有了版本号,如下:

Server: nginx
  • 开启 gzip 压缩

nginx 对于处理静态文件的效率要远高于 Web 框架,因为可以使用 gzip 压缩协议,减小静态文件的体积加快静态文件的加载速度、开启缓存和超时时间减少请求静态文件次数。

笔者开启 gzip 压缩之后,请求的静态文件大小大约减少了 2 / 3 呢。

gzip on;
#该指令用于开启或关闭gzip模块(on/off)

gzip_buffers 16 8k;
#设置系统获取几个单位的缓存用于存储gzip的压缩结果数据流。16 8k代表以8k为单位,安装原始数据大小以8k为单位的16倍申请内存

gzip_comp_level 6;
#gzip压缩比,数值范围是1-9,1压缩比最小但处理速度最快,9压缩比最大但处理速度最慢

gzip_http_version 1.1;
#识别http的协议版本

gzip_min_length 256;
#设置允许压缩的页面最小字节数,页面字节数从header头得content-length中进行获取。默认值是0,不管页面多大都压缩。这里我设置了为256

gzip_proxied any;
#这里设置无论header头是怎么样,都是无条件启用压缩

gzip_vary on;
#在http header中添加Vary: Accept-Encoding ,给代理服务器用的

gzip_types
    text/xml application/xml application/atom+xml application/rss+xml application/xhtml+xml image/svg+xml
    text/javascript application/javascript application/x-javascript
    text/x-json application/json application/x-web-app-manifest+json
    text/css text/plain text/x-component
    font/opentype font/ttf application/x-font-ttf application/vnd.ms-fontobject
    image/x-icon;
#进行压缩的文件类型,这里特别添加了对字体的文件类型

gzip_disable "MSIE [1-6]\.(?!.*SV1)";
#禁用IE 6 gzip

把上面的内容加在 nginx 的配置文件 ngixn.conf 里面的 http 模块里面即可。

是否设置成功,看文件请求的 Content-Encoding 是不是 gzip 即可。

  • 设置 expires,设置缓存
 server {
        listen       80;
        server_name  localhost;
        location  / {
            root   /home/blog/blog-react/build/;
            index  index.html;
            try_files $uri $uri/ @router;
            autoindex on;
            expires 7d; # 缓存 7 天
        }
    }

我重新刷新请求的时候是 2019 年 3 月 16 号,是否设置成功看如下几个字段就知道了:

  1. Staus Code 里面的 form memory cache 看出,文件是直接从本地浏览器本地请求到的,没有请求服务器。
  2. Cache-Control 的 max-age= 604800 看出,过期时间为 7 天。
  3. Express 是 2019 年 3 月 23 号过期,也是 7 天过期。

注意:上面最上面的用红色圈中的 Disable cache 是否是打上了勾,打了勾表示:浏览器每次的请求都是请求服务器,无论本地的文件是否过期。所以要把这个勾去掉才能看到缓存的效果。

3 4. 优化结果

3.1 测试场景

一切优化测试的结果脱离了实际的场景都是在耍流氓,而且不同时间的网速对测试结果的影响也是很大的。

所以笔者的测试场景如下:

  • a. 笔者的服务器是阿里的,配置是入门级的学生套餐配置,如下:

服务器配置

  • b. 测试网络为 10 M 光纤宽带。

3.2 优化结果

优化之后的首屏速度是 2.07 秒。

最后加了缓存的结果为 0.388 秒

image.png

再来看下 Lighthouse 的测试结果:

image.png

比起优化之前,各项指标都提升了很大的空间。

4. 最后

优化之路漫漫,永无止境,天下武功,唯快不破。

本次优化的前端与后端项目,都已经开源在 github 上了,欢迎围观。

前端:https://github.com/biaochenxuying/blog-react

后端:https://github.com/biaochenxuying/blog-node

github 博客地址:https://github.com/biaochenxuying/blog

细数 JavaScript 实用黑科技(一)

JavaScript

前言

只有深入学精一门语言,学其他语言才能更好地举一反三,触类旁听。

从接触前端开发到现在已经将近 2 年了,最近又看了阮一锋写的: 《JavaScript 语言入门教程》 一书,重温 JavaScript 。

小汪将工作和面试遇到过的,没多少人知道的 JavaScript 技巧,却十分实用的技巧都总结在这里面,分享给大家 。

温故而知新,我们对技术应该有的态度是: Stay hungry ! Stay young !

1. 标签(label)

JavaScript 语言允许,语句的前面有标签(label),相当于定位符,用于跳转到程序的任意位置,标签的格式如下。

label:
  语句

标签可以是任意的标识符,但不能是保留字,语句部分可以是任意语句。

标签通常与 break 语句和 continue 语句配合使用,跳出特定的循环。

top:
  for (var i = 0; i < 3; i++){
    for (var j = 0; j < 3; j++){
      if (i === 1 && j === 1) break top;
      console.log('i=' + i + ', j=' + j);
    }
  }
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0

上面代码为一个双重循环区块,break 命令后面加上了 top 标签(注意,top 不用加引号),满足条件时,直接跳出双层循环。如果 break 语句后面不使用标签,则只能跳出内层循环,进入下一次的外层循环。

标签也可以用于跳出代码块。

foo: {
  console.log(1);
  break foo;  // 注意要加 break 才能退出
  console.log('本行不会输出');
}
console.log(2);
// 1
// 2

上面代码执行到 break foo,就会跳出区块。

continue 语句也可以与标签配合使用。

top:
  for (var i = 0; i < 3; i++){
    for (var j = 0; j < 3; j++){
      if (i === 1 && j === 1) continue top;
      console.log('i=' + i + ', j=' + j);
    }
  }
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
// i=2, j=0
// i=2, j=1
// i=2, j=2

上面代码中,continue 命令后面有一个标签名,满足条件时,会跳过当前循环,直接进入下一轮 外层循环。 如果 continue 语句后面不使用标签,则只能进入下一轮的 内层循环。

小汪经过实践得出以下用途。

用途:

  • 可以跳出循环。
  • 对于多层循环也同样适用。
  • 特别是两层或者多层循环,只是为了找到想要的某个值时,而循环的数据是大量的,用标签就非常高效。

2. 区分数组和对象

先来道面试题:

console.log(typeof window)
console.log(typeof {}) 
console.log(typeof [])
console.log(typeof null)

答案:

"object"
"object"
"object"
"object"

上面代码中,null 返回 object 。这是由于历史原因造成的,且一切原型链的终点都是 null。
空数组( [] )的类型也是 object,这表示在 JavaScript 内部,数组本质上只是一种特殊的对象。而 instanceof 运算符可以区分数组和对象。

var o = {};
var a = [];

o instanceof Array // false
a instanceof Array // true

3. null, undefined 和布尔值

经常会有面试官问:null 与 undefined 的区别 ?

区别:

  • null 是一个表示“空”的对象,转为数值时为 0 。
  • undefined 是一个表示"此处无定义"的原始值,转为数值时为 NaN。
Number(null) // 0
5 + null // 5

Number(undefined) // NaN
5 + undefined // NaN

3.1 用法和含义

对于 null 和 undefined,大致可以像下面这样理解。

null 表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入 null,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出错,那么这个参数就会传入 null ,表示未发生错误。

undefined 表示“未定义”,下面是返回 undefined 的典型场景。

// 变量声明了,但没有赋值
var i;
i // undefined

// 调用函数时,应该提供的参数没有提供,该参数等于 undefined
function f(x) {
  return x;
}
f() // undefined

// 对象没有赋值的属性
var  o = new Object();
o.p // undefined

// 函数没有返回值时,默认返回 undefined
function f() {}
f() // undefined

注意,布尔值转换的时候,空数组([])和空对象({})对应的布尔值,都是true。

if ([]) {
  console.log('true');
}
// true

if ({}) {
  console.log('true');
}
// true

4. 数值

JavaScript 内部,所有数字都是以 64 位浮点数形式储存,即使整数也是如此。所以,1 与 1.0 是相同的,是同一个数。

1 === 1.0 // true

JavaScript 语言的底层根本没有整数,所有数字都是小数( 64 位浮点数)。容易造成混淆的是,某些运算只有整数才能完成,此时 JavaScript 会自动把 64 位浮点数,转成 32 位整数,然后再进行运算。

由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。

例如:

0.1 + 0.2 === 0.3
// false

0.3 / 0.1
// 2.9999999999999996

(0.3 - 0.2) === (0.2 - 0.1)
// false

2.22 + 2.21
// 4.43 

3.45 + 1.11
// 4.5600000000000005

2.22 + 2.24
// 4.460000000000001

但是商品计算金额的时候,金额的结果一般都是保留两倍小数点的,那怎么办呢?

可以用 toFixed 解决:

var a = 2.22 + 2.24
// 4.460000000000001
var result = (a).toFixed(2)
// 4.46

5. Object 属性的遍历

for...in 循环用来遍历一个对象的全部属性(包括可遍历的继承的属性)。但是,一般情况下,都是只想遍历对象自身的属性,所以使用 for...in 的时候,应该结合使用 hasOwnProperty 方法,在循环内部判断一下,某个属性是否为对象自身的属性。

var person = { name: '老张' };

for (var key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key);
  }
}
// name

最后

重大事件:2017年11月,所有主流浏览器全部支持 WebAssembly,这意味着任何语言都可以编译成 JavaScript,在浏览器运行。

前端还是很有未来的 !!!

下节内容:细数 JavaScript 实用黑科技(二)

如果你觉得该文章对你有帮助,欢迎到我的 github star 一下,谢谢。

github 地址

参考教程: 《JavaScript 语言入门教程》

你以为本文就这么结束了 ? 精彩在后面 !!!

原生 js 实现一个前端路由 router

效果图:

route-origin.gif

项目地址:https://github.com/biaochenxuying/route

效果体验地址:

1. 滑动效果: https://biaochenxuying.github.io/route/index.html

2. 淡入淡出效果: https://biaochenxuying.github.io/route/index2.html

1. 需求

因为我司的 H 5 的项目是用原生 js 写的,要用到路由,但是现在好用的路由都是和某些框架绑定在一起的,比如 vue-router ,framework7 的路由;但是又没必要为了一个路由功能而加入一套框架,现在自己写一个轻量级的路由。

2. 实现原理

现在前端的路由实现一般有两种,一种是 Hash 路由,另外一种是 History 路由。

2.1 History 路由

History 接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。

属性

  • History.length 是一个只读属性,返回当前 session 中的 history 个数,包含当前页面在内。举个例子,对于新开一个 tab 加载的页面当前属性返回值 1 。
  • History.state 返回一个表示历史堆栈顶部的状态的值。这是一种可以不必等待 popstate 事件而查看状态而的方式。

方法

  • History.back()

前往上一页, 用户可点击浏览器左上角的返回按钮模拟此方法. 等价于 history.go(-1).

Note: 当浏览器会话历史记录处于第一页时调用此方法没有效果,而且也不会报错。

  • History.forward()

在浏览器历史记录里前往下一页,用户可点击浏览器左上角的前进按钮模拟此方法. 等价于 history.go(1).

Note: 当浏览器历史栈处于最顶端时( 当前页面处于最后一页时 )调用此方法没有效果也不报错。

  • History.go(n)

通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面。比如:参数为 -1的时候为上一页,参数为 1 的时候为下一页. 当整数参数超出界限时 ( 译者注:原文为 When integerDelta is out of bounds ),例如: 如果当前页为第一页,前面已经没有页面了,我传参的值为 -1,那么这个方法没有任何效果也不会报错。调用没有参数的 go() 方法或者不是整数的参数时也没有效果。( 这点与支持字符串作为 url 参数的 IE 有点不同)。

  • history.pushState() 和 history.replaceState()

这两个 API 都接收三个参数,分别是

a. 状态对象(state object) — 一个JavaScript对象,与用 pushState() 方法创建的新历史记录条目关联。无论何时用户导航到新创建的状态,popstate 事件都会被触发,并且事件对象的state 属性都包含历史记录条目的状态对象的拷贝。

b. 标题(title) — FireFox 浏览器目前会忽略该参数,虽然以后可能会用上。考虑到未来可能会对该方法进行修改,传一个空字符串会比较安全。或者,你也可以传入一个简短的标题,标明将要进入的状态。

c. 地址(URL) — 新的历史记录条目的地址。浏览器不会在调用 pushState() 方法后加载该地址,但之后,可能会试图加载,例如用户重启浏览器。新的 URL 不一定是绝对路径;如果是相对路径,它将以当前 URL 为基准;传入的 URL 与当前 URL 应该是同源的,否则,pushState() 会抛出异常。该参数是可选的;不指定的话则为文档当前 URL。

相同之处: 是两个 API 都会操作浏览器的历史记录,而不会引起页面的刷新。

不同之处在于: pushState 会增加一条新的历史记录,而 replaceState 则会替换当前的历史记录。

例子:

本来的路由

 http://biaochenxuying.cn/

执行:

window.history.pushState(null, null, "http://biaochenxuying.cn/home");

路由变成了:

 http://biaochenxuying.cn/home

详情介绍请看:MDN

2.2 Hash 路由

我们经常在 url 中看到 #,这个 # 有两种情况,一个是我们所谓的锚点,比如典型的回到顶部按钮原理、Github 上各个标题之间的跳转等,但是路由里的 # 不叫锚点,我们称之为 hash。

现在的前端主流框架的路由实现方式都会采用 Hash 路由,本项目采用的也是。

当 hash 值发生改变的时候,我们可以通过 hashchange 事件监听到,从而在回调函数里面触发某些方法。

3. 代码实现

3.1 简单版 - 单页面路由

先看个简单版的 原生 js 模拟 Vue 路由切换。

route-vue.gif

原理

  • 监听 hashchange ,hash 改变的时候,根据当前的 hash 匹配相应的 html 内容,然后用 innerHTML 把 html 内容放进 router-view 里面。

这个代码是网上的:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta name="author" content="">
    <title>原生模拟 Vue 路由切换</title>
    <style type="text/css">
        .router_box,
        #router-view {
            max-width: 1000px;
            margin: 50px auto;
            padding: 0 20px;
        }
        
        .router_box>a {
            padding: 0 10px;
            color: #42b983;
        }
    </style>
</head>

<body>
    <div class="router_box">
        <a href="/home" class="router">主页</a>
        <a href="/news" class="router">新闻</a>
        <a href="/team" class="router">团队</a>
        <a href="/about" class="router">关于</a>
    </div>
    <div id="router-view"></div>
    <script type="text/javascript">
        function Vue(parameters) {
            let vue = {};
            vue.routes = parameters.routes || [];
            vue.init = function() {
                document.querySelectorAll(".router").forEach((item, index) => {
                    item.addEventListener("click", function(e) {
                        let event = e || window.event;
                        event.preventDefault();
                        window.location.hash = this.getAttribute("href");
                    }, false);
                });

                window.addEventListener("hashchange", () => {
                    vue.routerChange();
                });

                vue.routerChange();
            };
            vue.routerChange = () => {
                let nowHash = window.location.hash;
                let index = vue.routes.findIndex((item, index) => {
                    return nowHash == ('#' + item.path);
                });
                if (index >= 0) {
                    document.querySelector("#router-view").innerHTML = vue.routes[index].component;
                } else {
                    let defaultIndex = vue.routes.findIndex((item, index) => {
                        return item.path == '*';
                    });
                    if (defaultIndex >= 0) {
                        window.location.hash = vue.routes[defaultIndex].redirect;
                    }
                }
            };

            vue.init();
        }

        new Vue({
            routes: [{
                path: '/home',
                component: "<h1>主页</h1><a href='https://github.com/biaochenxuying'>https://github.com/biaochenxuying</a>"
            }, {
                path: '/news',
                component: "<h1>新闻</h1><a href='http://biaochenxuying.cn/main.html'>http://biaochenxuying.cn/main.html</a>"
            }, {
                path: '/team',
                component: '<h1>团队</h1><h4>全栈修炼</h4>'
            }, {
                path: '/about',
                component: '<h1>关于</h1><h4>关注公众号:BiaoChenXuYing</h4><p>分享 WEB 全栈开发等相关的技术文章,热点资源,全栈程序员的成长之路。</p>'
            }, {
                path: '*',
                redirect: '/home'
            }]
        });
    </script>
</body>

</html>

3.2 复杂版 - 内联页面版,带缓存功能

首先前端用 js 实现路由的缓存功能是很难的,但像 vue-router 那种还好,因为有 vue 框架和虚拟 dom 的技术,可以保存当前页面的数据。

要做缓存功能,首先要知道浏览器的 前进、刷新、回退 这三个操作。

但是浏览器中主要有这几个限制:

  • 没有提供监听前进后退的事件
  • 不允许开发者读取浏览记录
  • 用户可以手动输入地址,或使用浏览器提供的前进后退来改变 url

所以要自定义路由,解决方案是自己维护一份路由历史的记录,存在一个数组里面,从而区分 前进、刷新、回退。

  • url 存在于浏览记录中即为后退,后退时,把当前路由后面的浏览记录删除。
  • url 不存在于浏览记录中即为前进,前进时,往数组里面 push 当前的路由。
  • url 在浏览记录的末端即为刷新,刷新时,不对路由数组做任何操作。

另外,应用的路由路径中可能允许相同的路由出现多次(例如 A -> B -> A),所以给每个路由添加一个 key 值来区分相同路由的不同实例。

这个浏览记录需要存储在 sessionStorage 中,这样用户刷新后浏览记录也可以恢复。

3.2.1 route.js
3.2.1.1 跳转方法 linkTo

像 vue-router 那样,提供了一个 router-link 组件来导航,而我这个框架也提供了一个 linkTo 的方法。

        // 生成不同的 key 
        function genKey() {
            var t = 'xxxxxxxx'
            return t.replace(/[xy]/g, function(c) {
                var r = Math.random() * 16 | 0
                var v = c === 'x' ? r : (r & 0x3 | 0x8)
                return v.toString(16)
            })
        }

        // 初始化跳转方法
        window.linkTo = function(path) {
                if (path.indexOf("?") !== -1) {
                    window.location.hash = path + '&key=' + genKey()
                } else {
                    window.location.hash = path + '?key=' + genKey()
                }
        }

用法:

//1. 直接用 a 标签
<a href='#/list' >列表1</a>

//2. 标签加 js 调用方法
<div onclick='linkTo(\"#/home\")'>首页</div>

// 3. js 调用触发
linkTo("#/list")
3.2.1.2 构造函数 Router

定义好要用到的变量

function Router() {
        this.routes = {}; //保存注册的所有路由
        this.beforeFun = null; //切换前
        this.afterFun = null; // 切换后
        this.routerViewId = "#routerView"; // 路由挂载点 
        this.redirectRoute = null; // 路由重定向的 hash
        this.stackPages = true; // 多级页面缓存
        this.routerMap = []; // 路由遍历
        this.historyFlag = '' // 路由状态,前进,回退,刷新
        this.history = []; // 路由历史
        this.animationName = "slide" // 页面切换时的动画
    }
3.2.1.3 实现路由功能

包括:初始化、注册路由、历史记录、切换页面、切换页面的动画、切换之前的钩子、切换之后的钩子、滚动位置的处理,缓存。

Router.prototype = {
        init: function(config) {
            var self = this;
            this.routerMap = config ? config.routes : this.routerMap
            this.routerViewId = config ? config.routerViewId : this.routerViewId
            this.stackPages = config ? config.stackPages : this.stackPages
            var name = document.querySelector('#routerView').getAttribute('data-animationName')
            if (name) {
                this.animationName = name
            }
            this.animationName = config ? config.animationName : this.animationName

            if (!this.routerMap.length) {
                var selector = this.routerViewId + " .page"
                var pages = document.querySelectorAll(selector)
                for (var i = 0; i < pages.length; i++) {
                    var page = pages[i];
                    var hash = page.getAttribute('data-hash')
                    var name = hash.substr(1)
                    var item = {
                        path: hash,
                        name: name,
                        callback: util.closure(name)
                    }
                    this.routerMap.push(item)
                }
            }

            this.map()

            // 初始化跳转方法
            window.linkTo = function(path) {
                console.log('path :', path)
                if (path.indexOf("?") !== -1) {
                    window.location.hash = path + '&key=' + util.genKey()
                } else {
                    window.location.hash = path + '?key=' + util.genKey()
                }
            }

            //页面首次加载 匹配路由
            window.addEventListener('load', function(event) {
                // console.log('load', event);
                self.historyChange(event)
            }, false)

            //路由切换
            window.addEventListener('hashchange', function(event) {
                // console.log('hashchange', event);
                self.historyChange(event)
            }, false)

        },
        // 路由历史纪录变化
        historyChange: function(event) {
            var currentHash = util.getParamsUrl();
            var nameStr = "router-" + (this.routerViewId) + "-history"
            this.history = window.sessionStorage[nameStr] ? JSON.parse(window.sessionStorage[nameStr]) : []

            var back = false,
                refresh = false,
                forward = false,
                index = 0,
                len = this.history.length;

            for (var i = 0; i < len; i++) {
                var h = this.history[i];
                if (h.hash === currentHash.path && h.key === currentHash.query.key) {
                    index = i
                    if (i === len - 1) {
                        refresh = true
                    } else {
                        back = true
                    }
                    break;
                } else {
                    forward = true
                }
            }
            if (back) {
                this.historyFlag = 'back'
                this.history.length = index + 1
            } else if (refresh) {
                this.historyFlag = 'refresh'
            } else {
                this.historyFlag = 'forward'
                var item = {
                    key: currentHash.query.key,
                    hash: currentHash.path,
                    query: currentHash.query
                }
                this.history.push(item)
            }
            console.log('historyFlag :', this.historyFlag)
                // console.log('history :', this.history)
            if (!this.stackPages) {
                this.historyFlag = 'forward'
            }
            window.sessionStorage[nameStr] = JSON.stringify(this.history)
            this.urlChange()
        },
        // 切换页面
        changeView: function(currentHash) {
            var pages = document.getElementsByClassName('page')
            var previousPage = document.getElementsByClassName('current')[0]
            var currentPage = null
            var currHash = null
            for (var i = 0; i < pages.length; i++) {
                var page = pages[i];
                var hash = page.getAttribute('data-hash')
                page.setAttribute('class', "page")
                if (hash === currentHash.path) {
                    currHash = hash
                    currentPage = page
                }
            }
            var enterName = 'enter-' + this.animationName
            var leaveName = 'leave-' + this.animationName
            if (this.historyFlag === 'back') {
                util.addClass(currentPage, 'current')
                if (previousPage) {
                    util.addClass(previousPage, leaveName)
                }
                setTimeout(function() {
                    if (previousPage) {
                        util.removeClass(previousPage, leaveName)
                    }
                }, 250);
            } else if (this.historyFlag === 'forward' || this.historyFlag === 'refresh') {
                if (previousPage) {
                    util.addClass(previousPage, "current")
                }
                util.addClass(currentPage, enterName)
                setTimeout(function() {
                    if (previousPage) {
                        util.removeClass(previousPage, "current")
                    }
                    util.removeClass(currentPage, enterName)
                    util.addClass(currentPage, 'current')
                }, 350);
                // 前进和刷新都执行回调 与 初始滚动位置为 0
                currentPage.scrollTop = 0
                this.routes[currHash].callback ? this.routes[currHash].callback(currentHash) : null
            }
            this.afterFun ? this.afterFun(currentHash) : null
        },
        //路由处理
        urlChange: function() {
            var currentHash = util.getParamsUrl();
            if (this.routes[currentHash.path]) {
                var self = this;
                if (this.beforeFun) {
                    this.beforeFun({
                        to: {
                            path: currentHash.path,
                            query: currentHash.query
                        },
                        next: function() {
                            self.changeView(currentHash)
                        }
                    })
                } else {
                    this.changeView(currentHash)
                }
            } else {
                //不存在的地址,重定向到默认页面
                location.hash = this.redirectRoute
            }
        },
        //路由注册
        map: function() {
            for (var i = 0; i < this.routerMap.length; i++) {
                var route = this.routerMap[i]
                if (route.name === "redirect") {
                    this.redirectRoute = route.path
                } else {
                    this.redirectRoute = this.routerMap[0].path
                }
                var newPath = route.path
                var path = newPath.replace(/\s*/g, ""); //过滤空格
                this.routes[path] = {
                    callback: route.callback, //回调
                }
            }
        },
        //切换之前的钩子
        beforeEach: function(callback) {
            if (Object.prototype.toString.call(callback) === '[object Function]') {
                this.beforeFun = callback;
            } else {
                console.trace('路由切换前钩子函数不正确')
            }
        },
        //切换成功之后的钩子
        afterEach: function(callback) {
            if (Object.prototype.toString.call(callback) === '[object Function]') {
                this.afterFun = callback;
            } else {
                console.trace('路由切换后回调函数不正确')
            }
        }
    }
3.2.1.4 注册到 Router 到 window 全局
    window.Router = Router;
    window.router = new Router();

完整代码:https://github.com/biaochenxuying/route/blob/master/js/route.js

3.2.2 使用方法
3.2.2.1 js 定义法
  • callback 是切换页面后,执行的回调
<script type="text/javascript">
        var config = {
            routerViewId: 'routerView', // 路由切换的挂载点 id
            stackPages: true, // 多级页面缓存
            animationName: "slide", // 切换页面时的动画
            routes: [{
                path: "/home",
                name: "home",
                callback: function(route) {
                    console.log('home:', route)
                    var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>首页</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/list\")'>列表</a></div><div class='height'>内容占位</div>"
                    document.querySelector("#home").innerHTML = str
                }
            }, {
                path: "/list",
                name: "list",
                callback: function(route) {
                    console.log('list:', route)
                    var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>列表</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/detail\")'>详情</a></div>"
                    document.querySelector("#list").innerHTML = str
                }
            }, {
                path: "/detail",
                name: "detail",
                callback: function(route) {
                    console.log('detail:', route)
                    var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>详情</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/detail2\")'>详情 2</a></div><div class='height'>内容占位</div>"
                    document.querySelector("#detail").innerHTML = str
                }
            }, {
                path: "/detail2",
                name: "detail2",
                callback: function(route) {
                    console.log('detail2:', route)
                    var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>详情 2</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/home\")'>首页</a></div>"
                    document.querySelector("#detail2").innerHTML = str
                }
            }]
        }

        //初始化路由
        router.init(config)
        router.beforeEach(function(transition) {
            console.log('切换之 前 dosomething', transition)
            setTimeout(function() {
                //模拟切换之前延迟,比如说做个异步登录信息验证
                transition.next()
            }, 100)
        })
        router.afterEach(function(transition) {
            console.log("切换之 后 dosomething", transition)
        })
    </script>
3.2.2.2 html 加 script 定义法
  • id="routerView" :路由切换时,页面的视图窗口
  • data-animationName="slide":切换时的动画,目前有 slide 和 fade。
  • class="page": 切换的页面
  • data-hash="/home":home 是切换路由时执行的回调方法
  • window.home : 回调方法,名字要与 data-hash 的名字相同
<div id="routerView" data-animationName="slide">
        <div class="page" data-hash="/home">
            <div class="page-content">
                <div id="home"></div>
                <script type="text/javascript">
                    window.home = function(route) {
                        console.log('home:', route)
                            // var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>首页</h2> <input type='text'> <div><a href='#/list' >列表1</div></div><div class='height'>内容占位</div>"
                        var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>首页</h2> <input type='text'> <div><div href='javascript:void(0);' onclick='linkTo(\"#/list\")'>列表</div></div><div class='height'>内容占位</div>"
                        document.querySelector("#home").innerHTML = str
                    }
                </script>
            </div>
        </div>
        <div class="page" data-hash="/list">
            <div class="page-content">
                <div id="list"></div>
                <div style="height: 700px;border: solid 1px red;background-color: #eee;margin-top: 20px;">内容占位</div>

                <script type="text/javascript">
                    window.list = function(route) {
                        console.log('list:', route)
                        var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>列表</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/detail\")'>详情</a></div>"
                        document.querySelector("#list").innerHTML = str
                    }
                </script>
            </div>
        </div>
        <div class="page" data-hash="/detail">
            <div class="page-content">
                <div id="detail"></div>
                <script type="text/javascript">
                    window.detail = function(route) {
                        console.log('detail:', route)
                        var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>详情</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/detail2\")'>详情 2</a></div><div class='height'>内容占位</div>"
                        document.querySelector("#detail").innerHTML = str
                    }
                </script>
            </div>
        </div>
        <div class="page" data-hash="/detail2">
            <div class="page-content">
                <div id="detail2"></div>
                <div style="height: 700px;border: solid 1px red;background-color: pink;margin-top: 20px;">内容占位</div>

                <script type="text/javascript">
                    window.detail2 = function(route) {
                        console.log('detail2:', route)
                        var str = "<div><a class='back' onclick='window.history.go(-1)'>返回</a></div> <h2>详情 2</h2> <input type='text'> <div><a href='javascript:void(0);' onclick='linkTo(\"#/home\")'>首页</a></div>"
                        document.querySelector("#detail2").innerHTML = str
                    }
                </script>
            </div>
        </div>
    </div>
    <script type="text/javascript" src="./js/route.js"></script>
    <script type="text/javascript">
        router.init()
        router.beforeEach(function(transition) {
            console.log('切换之 前 dosomething', transition)
            setTimeout(function() {
                //模拟切换之前延迟,比如说做个异步登录信息验证
                transition.next()
            }, 100)
        })
        router.afterEach(function(transition) {
            console.log("切换之 后 dosomething", transition)
        })
    </script>

参考项目:https://github.com/kliuj/spa-routers

5. 最后

项目地址:https://github.com/biaochenxuying/route

博客常更地址1 :https://github.com/biaochenxuying/blog

博客常更地址2 :http://biaochenxuying.cn/main.html

足足一个多月没有更新文章了,因为项目太紧,加班加班啊,趁着在家有空,赶紧写下这篇干货,免得忘记了,希望对大家有所帮助。

如果您觉得这篇文章不错或者对你有所帮助,请点个赞,谢谢。

前端架构师亲述:前端工程师成长之路的 N 问 及 回答

问题回答者:黄轶,目前就职于 Zoom 公司担任前端架构师,曾就职于滴滴和百度,毕业于北京科技大学。

1. 前端开发

问题

大佬,能分享下学习路径么,感觉天天忙着开发业务,但是能力好像没有太大提升,不知道该怎么充实自己 ?

解答

  • 业务开发有没有痛点,能不能通过技术的手段解决 ?�
  • 平时开发业务用到了哪些技术栈和周边的生态链,我是否对他们熟练掌握了,对他们的实现原理呢 ?
  • 平时开发遇到了 bug,调试了很久,能不能提升自己快速定位 bug,解决问题的能力 ?
  • 如果上面分配了一个需求,没有现成的轮子可以用,我是否可以快速造一个出来 ?
  • 如果使用第三方轮子出现问题,我能否能找到合适的解决方案,甚至参与共建 ?
  • 以上提到了这些问题,不妨问问自己,如果没有做的足够好,都是你可以提升的方向。

问题

我想知道你为什么对前端这个职业(行业),总是保持一颗好奇心,每天都不停探索,每天保持学习进步,你是怎样坚持下来的呢 ?

就像医院里的医生(教授/专家),在这个行业刻苦钻研了大半辈子,怎样保持每天学习的这种精神 ?探索精神 ?并且长久坚持下去 ?为社会做出了非常多的贡献。

我知道你是以怎样的决心和毅力保持每天学习,不停探索前进 ?

解答

  • 主要是兴趣驱动吧,对技术保持热情和好奇。
  • 另外就是成就感,当我 get 到某个新技能,解决了某个复杂的问题的时候会非常有成就感。
  • 工作前几年的时间是非常关键的,是成长空间大且精力最旺盛的阶段,一定要在这个阶段多学知识。
  • 学习是无止境的,尤大说过一句话我印象非常深刻 ”做脑力工作的人,往往钻研得越深,越发现自己的渺小和无知“ ,与君共勉。

问题

最近拿到了滴滴出行的实习生 offer,我想问一下您对实习生 (或者说初步踏入 IT 行业的学生) 在融入部门和提升上有什么建议 ?

解答

  • 实习生一定要多做业务,工作要积极主动,争取转正机会。
  • 另外,非常推荐去我之前的团队,现在是苗老板负责,你可以私下联系他喔~

问题

感觉自己的 js 基础很薄弱啊,我想问如果想进大厂你指的基础具体一点到底指啥啊,我是一个非科班出身的求解呀 ?

解答

  • 如果是应届生,大厂关注的是你的基础和潜力。
  • 如果是社招,大厂会关注你的经验和能力,以及潜力。
  • 如果你有心仪的大厂,不妨去看一下他们的招聘要求,以及关注一下他们对外输出的东西。
  • 非科班是一个劣势,那么你就务必要花时间去补一些计算机相关的理论知识,简历有需要亮点,最好能有一些技术输出,比如很多人会做博客、写系列文章、做有趣的项目等等。
  • 另外,最好的时机是等大厂缺人,招人名额多的时候去投简历,也可以多认识一些找内推机会。
  • 最后,一切的一切,都离不开硬实力,所以优先提升自己的硬实力,多花时间学习。

问题

黄轶老师,你对于在项目中推行 BFF 模式有什么见解吗 ?
希望你可以回答的略广一点,非常期待您的回答。

解答

  • BFF 在服务聚合上还是很有优势的吧,特别是微服务特别火的今天,前端只需要关注所需要的数据,不用关注底层提供这些数据的服务。我在滴滴和 Zoom 的时候都是这么玩的~

问题

请问一下,你做兼职的话,一般是关注那些方面呢 ? 还有比较建议在哪些渠道寻找兼职做呢 ?

解答

  • 主要关注的是性价比,因为牺牲了自己的业余时间,要么是多挣钱(很难),要么是提升能力。
  • 最好是熟人介绍,没有的话可以去水木论坛找找看(我曾经找到过),其它渠道没有经验,我就不推荐了。

问题

黄老师,想问下你对于前后端数据交互的最佳实践的看法,ajax ?axios ?等等,有没有系统学习的推荐。

解答

  • 前后端交互通常有 HTTP 和 WebSocket 2 种通讯方式,建议你首先系统的学习一下 HTTP 相关知识,推荐看 《HTTP 权威指南》或者是 《图解 HTTP》。
  • 另外你提到的 axios 只是对 Ajax 的封装,如果你想了解它的实现原理,正好前阵子我在慕课网做了一门课程《基于TypeScript从零重构axios》,学一遍后你会对 axios 的实现细节会了如执掌,同时也可以巩固不少 HTTP 相关的知识。

问题

黄老师,我现在刚入门前端,能力有很大的欠缺,想找一些视频学习,现在主要用到的是 Vue 框架,有什么视频可以推荐给我吗 ?

解答

  • 这里不打广告都不行了,可以关注我在慕课网的实战课程喔~ Vue 三步曲,入门 - 进阶的都有。
  • 不过除了看视频学习,我也建议你务必多去读官方文档,敲一敲上面的示例。
    最后附上链接 http://www.imooc.com/t/3017249

问题

我目前是一名后端工程师,工作快五年了。刚工作时,认为前端只是写页面,写交互,技能项要求少。
我自己也是比较喜欢前端,因为做后端的也会接触到前端东西,所以暂时没有转到前端。
工作之中也在加强前端基础技术技能。慢慢发现前端并不是刚开始认为的那样,现在的前端能应用于各个客户端,服务端,以及组件化,模块化,激发了我更想学习前端的欲望,于是我利用空闲时间学习更多前端相关技术。
现在流行前后端分离,后端只做接口,完全不会接触我喜欢的前端部分,我喜欢技术,不喜欢业务,但是业务也很重要,在我的经历中一个项目完成上线后,基本就是后端解决运营或用户使用中出现的问题,因此正在考虑转专职前端。
这几年工作中写过接口,写过 h5 嵌入 App,写过前后不分离的项目,也写过前后分离的项目,想问问黄大仙站在前端的角度有什么看法 ?

解答

  • 其实你有丰富的后端经验,不妨就直接转全栈,并不一定要做专职的前端。
  • 即使是前后端分离的开发方式,也可以一个人完成 2 端的开发。
  • 而且越往上走,如果前后端都精通的人,可以走的更远。
  • 所以你往前端方向走是好事,不过后端也不要扔下。
  • 后端也不仅仅是 curd,当业务复杂,用户规模大的时候,面临的挑战比前端要大的多,如果你所在的公司没这方面的挑战,建议去大厂感受一下。

问题

关于前端开发,如何完善自己的工作流呢,目前的工作流十分原始,流程是明确需求-项目开发(开发环境/正式环境)-测试-上线。
如果在大厂面试,工作流这块比较吃亏。像黄老这种包括了项目初始化、本地开发、联调、测试、上线等各个环节,是如何探索出来的呢 ?

解答

  • 这些都是在大厂训练出来的,其实程序员更多的应该去思考一个需求从产生 - 落地的各个环节。
  • 现在大部分人能做到从一个项目的开发-上线各个流程的属性,其实在 Zoom 我们会从产品的设计开始,自己会去设计产品应该如何实现,用户需要什么样的功能,从 owner 的角度去设计和开发,并负责产品的测试和上线,这才是工程师应该有的素质。
  • 所以即使在一个有很多产品经理的公司,工程师也可以多参与产品的设计和讨论喔。

问题

大佬,对于 flutter 持什么看法,以后会成为全端的解决方案吗 ?特别是 flutter 转 web 之后 。

解答

  • 肯定不会替代 Web 开发的,至于双端的应用,可以关注一些主做移动端的公司,比如滴滴、阿里、腾讯、头条等大厂在这方面的应用实践吧。

问题

中级怎么突破到高级前端工程师呢? 自己尝试写框架和库吗 ?

解答

  • 中级前端基本上就是能够独立开发,满足基本功能需求,质量一般,对于复杂业务需求实现吃力,需要指导,对标阿里 p5。
  • 高级前具备独挡一面的能力,能够高质量完成工作,胜任复杂业务需求开发,能把握一个系统/团队的整体实现,在推行过程中能提炼新的方法或方案,或对现有方案提出改进建议并被证明有效,对标阿里 p6。
  • 其实级别的突破,侧面反馈就是能力的成长,那怎么提升能力呢?既要会偷懒,又要勤奋。
    这里说的“偷懒”,并不是说少做需求,而是从业务开发中多去思考和总结,学会抽象,学会复用代码,减少重复的劳动。学会使用工具来帮我们解决人肉的问题。
  • 举个例子,前端最近非常火热的编程**就是模块化、组件化,本质上都是为了复用代码,提升代码的可维护性,比如我们是不是需要开发通用组件库、JS库等等,来辅助我们的业务代码。还有几乎现在所有项目都会用构建化工具帮助我们开发,最有代表性的就是 webpack,它能帮我初始化代码,调试,编译打包等等,极大的帮助我们节约了开发时间,我们是不是多花点时间去研究它的配置,甚至是它的源码。
  • 所以,去花时间学习这些编程**,掌握这些工具,都能够很好的帮助我们提升技术。除了会“偷懒”之外,我们也要勤奋,虽然业务忙,但往往也不会忙到8小时工作时间都在写代码的地步吧。
  • 如果我们把每天在群里斗图、刷微信朋友圈等等的时间节约下来学习技术的话,相信只要坚持,技术一定会提升比别人快,特别是初级的同学,你们的进步空间还很大,一定要多花时间在学习,而不是浪费时间问 “我工作 1-3 年,出去要多少钱合适” 这类的问题,薪资一定是和能力匹配的。
  • 至于学习的方向,只要和你工作相关,你没有掌握透彻的技术,都是可以的。

问题

学习到了一个进度学不进去了,静不下心看书。想问问大佬有没有遇到类似的情况,有什么调解方法吗 ?谢谢~

解答

  • 学习学不下去的时候,不妨做一些放松自己的事情,然后在状态好的时候再回过头去看喔~

问题

Node.js 对于前端是必备的么,但目前公司并没有应用场景, 学了用不上,就忘了。

解答

  • Node.js 至少在工具方面的表现还是很不错的,比如一些构建工具、脚手架工具都是通过 Node.js 写的,可以通过学习他们的源码去了解 Node.js。
  • 另外一些不错的 Node.js 后端框架,比如 express,也可以去了解一下,因为通常使用 webpack 起的内置 server 就是使用了 express。

问题

作为一名初级前端工程师,前路很迷茫,不知道要怎么学习提升,老师,您可以给点建议吗 ?

解答

  • 首先是基础,这里不仅仅是前端基础,还有计算机相关的基础知识(数据结构、网络),基础务必要打牢。可以通过反复看书、coding 练习的方式。
  • 其次是项目开发,你工作中用到技术栈,一定要熟练掌握,可以通过官方文档入门,通过工作中的 coding 巩固,并可以去看一些高质量的进阶教学视频课程做提升(这里有广告嫌疑)。
  • 以上是入门-中级的阶段主要做的事情,其实就是不断花时间学习 +coding,想办法让自己先成为熟练工,初级可提升的空间还是很大的。
  • 中级-高级,下面有一个类似的问题喔。

问题

黄老师,请问一下中级前端开发和高级前端开发分别需要具备什么样的能力素质 ?

解答

  • 中级前端基本上就是能够独立开发,满足基本功能需求,质量一般,对于复杂业务需求实现吃力,需要指导,对标阿里 p5。
  • 高级前具备独挡一面的能力,能够高质量完成工作,胜任复杂业务需求开发,能把握一个系统/团队的整体实现,在推行过程中能提炼新的方法或方案,或对现有方案提出改进建议并被证明有效,对标阿里 p6。

问题

node ts 确实自己都在玩,ts 是跟你课程面学的,但有的面试官就反问我,node ts 并没有在真正生产环境玩过还敢拿出来说会, 就把我拒绝了,这些东西确实会,只是之前公司没有机会发挥,但我不知该如何应付这样的面试官,希望你解答。很多东西也需要遇到一个好团队才能发挥,但现在没有这个机会。

解答

  • 其实不妨把你自己玩的东西形成一些作品,发布到 GitHub 上,可以是文章,也可以是项目。
  • 我之前面试一个携程的小朋友,他们的技术栈是 React,但是他自己研究过 Vue.js 实现并写了一个 Mini 版本的 Vue 实现,这样给我的感觉就很好。
  • 所以虽然有些技术没有在生产环境中用过,但是你通过自学掌握了并且折腾出一些成果,我会认为你是一个喜欢技术,爱折腾的人。
  • 如果说你这么做了面试官依然不认可你,那说明你们的价值观不符,那么面试不通过也没什么好遗憾的,面试本来也是一个双向选择的过程~

问题

对于一个中大型的前端项目来说,各种组件如何分类更合理呢 ?比如基础组件、业务组件。

解答

  • 通常分为基础组件、业务组件、视图组件,基础组件通常都会在组件库里解决了。

问题

大佬,我现在就职一家比较大的公司,做前端,外包。每天平均 9 10 点下班,平常学习的时间感觉太少了,想补基础知识。
只能通勤时间看看电子书,回家了再敲一会代码。有时候,11 点,12 点。有点干下去了,有点迷茫,求大佬指导。

解答

  • 你属于人力外包还是项目外包,如果是人力外包到大公司,就想办法转正。
  • 另外你需要评估一下你每天工作这么长时间是否有提升,还是一味地重复劳动。
  • 如果有提升的话,那还是可以继续做,如果没什么提升,并且也没什么好机会提升的话,那可以考虑出去看看机会了~

问题

毕业三年,第一年在一个小公司,第二年在一个大公司的外包到现在。
现在的状态是这样的,公司有老项目(13 年一直用到现在的)需要维护,也有新项目( vue + 微服务),总之我的工作上主要在维护那些老项目上,实际上工作上用的时间不多,每天有一定量的空闲时间,对应的工资也上不来,一年了也没有调薪,由于老项目的重要性自然是日益下降的,未来也不像是多能期待。
新项目实际上我也接触过,还做过一些脚手架整体升级一类的工作,所以现在工作中能带来的学习方向和提升实在不多,于是我现在在学习一些基础性的东西( http 协议,数据结构与算法,网络硬件,甚至计算机组成)。
由于我是转行来的这些东西没有系统的学过,现在捡起来说有用也有用,但短期看来带不来什么明显的改变,我的问题就是,如果从现实出发,之后的岁月应该怎么规划合适,短期来说有什么能带来收益(比如方便面试 ?)的学习方向 ?

解答

  • 短期如果是面向面试学习的话,我认为一方面你需要准备面试,可以看一下掘金那本面试相关的小册,查漏补缺。
  • 一方面需要好好地对你现有的工作总结,即使看上去技术含量不太高的工作,是不是也会有一些亮点,让面试官看到你对工作的思考。
  • 长期的话,如果想让自己的天花板变高,还是需要学习计算机的一些基础知识的。
  • 工作中如果遇到了相关知识不明白的,就认真去学习,直到弄明白为止。

问题

我大学也是 .net 方向,现在大三,已经转向前端,基础知识已了解,准备学习 vue。我现在有些焦虑,即将秋招,可是我还没有拿的出手的项目,怎么办啊 ?求解。

解答

  • 校招主要看重的是候选人的基础和潜力,如果有实习经验更佳。
  • 建议你这段时间就认真备战秋招吧,先把基础好好学习,除了前端知识外,数据结构算法也是考点,刷刷题吧。
  • 至于项目的话,如果你有时间能高仿个 xx,并在掘金上发布文章 ,也可能是一个亮点吧。
  • 去年有个同学就通过这个方式获得滴滴的面试,不过可惜基础略薄弱。所以重点还是先搞基础吧~

问题

我参加了滴滴的校招,但遗憾面试没有通过,后来我去了一个创业公司到现在也快一年了,负责开发公司的 webapp( Cordova + vue )和官网,node 和 mysql 对于正常的开发都能熟练使用,后台接口和前后端联调也要我负责,但是我非常想去大厂和优秀的人在一起,提升自己,不过我投过几家大厂要求 1 到 3 年经验却没有任何消息,所以我现在对于未来有点迷茫了,因为我真的真的想去大厂,现在的我该怎么办,是不是我太急躁了 ?

解答

  • 不用太急,你也只工作一年而已,这个过程你可以提升的东西有很多,先多做业务,提升技术。
  • 等 2-3 年后,再尝试去投大厂,但你的简历一定要有亮点,并且基础足够扎实,相信以后机会还是会有很多的,加油~

问题

现在多端统一开发框架这么多,有没有学习的建议 ?

解答

  • 首先思考一下你的业务是否需要多端框架,比如 App、h5 和小程序需要一样的设计吗,答案是很多情况下是不一样的,从产品设计上来说,App 通常是最完善的功能,h5 保留主要功能,而小程序多半是一个快速入口。
  • 其次目前多端框架多半不成熟,如果是核心业务,务必谨慎使用,很多情况下,大公司也会在一些偏运营展示的简单业务中使用这类多端框架。
  • 最后,一定要做技术选型,那么就从技术栈、框架维护力度,以及社区的活跃度几个方向权衡吧。

问题

黄老师,请问应该怎么培养自己的架构思维呢 ?

解答

  • 这个需要长期的经验积累的,说几个关键词吧,借鉴、思考、总结,交流。
  • 借鉴是站在巨人的肩膀上,比如可以学习张云龙大佬的博客: https://github.com/fouber/blog
  • 思考是多去想我怎么设计才是最合理,能否解决当前业务的痛点,如何做到开发时对开发者友好,上线时对用户友好。
  • 总结就是每次经验用文字记录下来,积累和沉淀。这个时候也可以多思考思考,有没有哪些地方是不合理的,有没有更好的方案。
  • 交流就是把一些案例分享出去,和大家一起探讨和交流,碰撞一些不一样的思维火花。

问题

前端的职业发展,怎么建立良好的知识体系呢 ?

解答

  • 深度优先,不忘发展广度,前端相关的知识体系可参考朴灵大佬这幅图:https://github.com/JacksonTian/fks
  • 当然,这幅图只是一个参考,你的知识体系还是要通过工作建立起来的,所以要以你的工作为准,深入学习周边的工具链技术,学习过程中要多思考,勤总结,对于每个知识点,最好是能学精学透,切忌囫囵吞枣。
  • 对于些热门的技术,不要盲目追新,如果工作用不到,除非很感兴趣,否则了解关注即可。

问题

老师好,我是一名工作一年的前端菜鸟,目前,有点迷茫,不知道以后自己的生涯规划朝那个方向发展,横行还是纵向,求教 ?

解答

  • 对于初入行我的建议都是多做业务,多提升技术,等到 2-3 年,再考虑之后的发展方向。
  • 关于职业生涯规划,我下面有回答喔。

问题

黄老,这几年的前端趋势是 mvvm、组件化、工程化以及 typescript,您觉得接下来的近几年前端会向哪个方向重点发展呢 ?目前前端部分还有什么痛点需要解决呢 ?期待您的解答,感谢!

解答

  • 前端往深入做通常是几个方向,复杂应用(比如 web doc 这种规模的)、数据可视化(2D、3D)、前端工程化、架构。
  • 痛点如目前非常火的多端框架,本身是值得探索的一个方向,但是由于各个端的标准和实现不一致,导致目前的状态是调试困难,坑多。
  • 另外,感兴趣的话也可以把视野放更广一些,比如可以关注 AI,入门机器学习等等。

问题

你以前刚接触 web 前端时,每天看几本书 ?通过啥方式提升 ?

解答

  • 我看的前端书并不多,也远达不到一天几本,经典的红宝书和犀牛书我有反复的看过,前期基本就是一个编码 - 看书 - 编码 - 看书的节奏。
  • 另外,我也比较推荐看一下广度相关的书籍,比如《http 权威指南》、《精通正则表达式》、《Head First 设计模式》《代码整洁之道》 等。

问题

大佬,请问下公司就我一个前端,没有经验比较丰富的人可以交流学习,依靠自己学习可以从初级进阶到中级前端工程师吗 ?

解答

  • 现在学习资源比之前丰富太多了,除了看一些经典书籍,还有一些很不错的教学视频可以去学习,讲师很多也是一线互联网公司的大佬,投资自己总不会错的。
  • 也可以适当参加一些技术会议,认识一些人。
  • 另外,对于在小公司如何做技术提升,可以参考我下面的一些回答。
  • 当然,当你能力提升到一定水平后,能进大公司就去大公司。大公司相对来说,业务挑战更大,也更加规范,是一个非常不错的镀金机会。

问题

老师怎么看待未来桌面应用,例如 electron 的发展前景 ?

解答

  • electron 让前端工程师开发桌面应用更加容易,但它本身还是桌面应用,需要安装,大部分企业还是会更倾向于 web 的方式吧。
  • 另外,我觉得 PWA 可以多关注喔。

问题

大佬,天天加班严重,忙于业务,还是基于老的框架,如何能提升自身 ?感觉很困惑 。

解答

  • 首先需要提升自己的开发效率,思考一下能不能抽象一些通用的模块和组件等,开发过程中有没有痛点,有没有能通过工具而不用人解决的问题,如果你能发现一些问题并通过技术手段解决,那么已经是一个提升了。
  • 其次,老的框架是否需要升级,如需要,如何平滑升级,如何做到升级对现有业务影响最小,如果影响很大,思考一下现有项目的组织方式是否合理。如果把上面这些问题都想清楚,并解决,也是一个提升了。
  • 再次,我每天做的业务,接触到的一些工具链和技术栈,我是否已经对它们的原理深入掌握了,出现 Bug 和坑我能否快速定位和解决,如果现有轮子不能满足需求的时候,我能不能快速造一个出来,做了这些又是一个提升。
  • 最后,如果觉得公司对技术重视程度不够,也可以考虑换一个坑。

问题

前端的架构师一般都做些什么呢 ?

解答

  • 主要是分析当前业务的痛点和目标,结合场景去提出一套合理的解决方案。
  • 其中涉及到编码的部分包括不限于编写一些工具插件、脚手架、甚至是框架。
  • 前端架构是没有银弹的,不同场景的架构方案也往往是不同的。

2. 性能优化

问题

性能优化有什么推荐的书吗?

解答

  • 《高性能网站建设进阶指南》

3. Vue.js

问题

你好,老师,前端使用 vue 技术栈的,有哪些提升开发效率的经验 ?

解答

  • 对 Vue.js 熟练掌握,可以方便你快速开发。
  • 深入了解 Vue.js 的原理,对于快速定位 bug,了解它的职责边界有很大帮助。
  • 另外,尽量使用第三方成熟库,避免重复造轮子。

问题

培训结束一年,三大框架只了解 vue,目前准备跳槽,但是觉得自己 js 基础也不扎实,vue 也没有特别的熟练,react 更是完全都不了解,想问下接下来的学习路径大概是什么 ?

解答

  • 查漏补缺吧,知道自己什么不足,就花时间去学习。
  • Vue.js 技术栈方向的可以考虑去学习我的课程,不过会有一定难度,也是需要你花时间和耐心去学的。
  • 另外就是在工作中学习和成长了,如果是一年经验,还是多做业务,在业务中多思考和总结,使用 Vue 就先把 Vue 用熟,接下来研究其原理实现,学透。
  • 之后有需要再学 React、Angular 都比较容易了。
  • 总之前期还是先让自己成为一名熟练工,之后再去做一些有挑战的事情。

问题

黄轶老师,vue.js 源码都跟着您学完了,另外还学过 ts,网络,基础的构建,接下来如果像往前端继续深造应该学什么呢,深入算法还是可以看看 webpack 源码 ?谢谢。

解答

  • 通常都是结合你工作中使用到的一些工具链,做深入的学习和研究。
  • 另外,你已经学习了这么多东西,可以尝试一下学以致用,比如用 ts 重构一些项目,给一些基础库和组件编写测试等等。
  • 学习了 Vue 源码,可以尝试去编写一些自定义指令、插件等,或者是去研究社区 Vue 相关生态工具,做进一步研究和学习。

问题

Vue 应该如何进阶和提升呢 ? 总感觉自己处在一个业务仔的技术水平。

解答

    1. 做复杂的应用,思考不同场景在 Vue 下如何解决,并搞清楚 Vue 的边界职责(即 Vue 能做什么,不能做什么)。
    1. 了解一下周边生态工具如 vue-router、vuex 的实现原理,尝试去写一些简单的轮子,比如自定义指令、插件等。
    1. 阅读源码,了解 Vue 的核心原理实现。
    1. 参与 Vue 或者周边生态工具源码的共建。
  • 推荐学习工作中使用的工具链的源码,比如用了 webpack 就看研究一下 webpack,如果能顺手写一两个 webpack 插件就更好了。
  • 另外也可以多看一些经典的书籍和一些进阶的视频课程学习。

4. 个人成长

问题

在对未来规划的方面老师可否给一些参考性的建议 ?

解答

  • 一般建议只做 3 年内的规划,你作为一个应届生,前几年的目标就是多做业务,提升技术。
  • 关于职业规划,下面有个类似的问题喔。

问题

黄老师,你在滴滴的时候你是你们团队的第一个人 ,团队都是你组建起来的吗 ?可以分享一下组建团队的时候,你是怎样考虑自己要选择的队员 ,以及如何对他们的工作进行分配和评估的 ?

解答

  • 是第一人,不过后来没有做 leader,有些事情就不便这里说啦。
  • 我可以分享一下我后来做 WebApp 前端架构团队负责人是如何招人的,主要看候选人是不是符合团队的整体气质,比如我们团队是一个爱折腾技术,喜欢分享的团队,如果候选人在这方面突出,肯定是大大的加分项。
  • 工作分配主要是看他们每个人的情况,通常一个人会负责一块业务,同时也有相关的技术方向。
  • 会把一些基础的开发工作分配给应届生,因为他们是需要大量做业务的阶段,会把一些有挑战的工作分配给一些有潜力的同学,让他们快速成长。
  • 另外我们团队还有很优秀的同学,会主动承担和负责一些技术方向,这些我都非常鼓励的。

问题

小公司没有什么大公司背景,没有牛逼的项目,怎么走上前端架构之路 ?

解答

  • 首先,你需要能快速响应公司的业务需求,成为一名熟练工。
  • 然后可以思考开发过程中有没有什么痛点,能不能通过技术的手段,比如开发一些工具和插件来提升开发效率,在这个过程中,你可以去调研业内有没有成熟的轮子,轮子能不能满不满足你的需求,可以对轮子做研究甚至去做贡献,这个过程你会接触到学习到很多不曾接触到的知识,积累沉淀下来。
  • 另外,你也需要多花业余时间去学习,学习的方向是你工作相关的技术栈,学精学通。
  • 等自己有一定能力了,不妨去投简历到心仪的大公司,刚提到的这些经历可以成为简历的亮点。
  • 进入大公司后,你会遇到更多的挑战,业务规模、开发效率、性能、稳定性等等都会有更高的要求,在你不断去面对挑战,解决问题的过程中,你自然就会慢慢成长了。
  • 当然,进入大公司后你可能一开始也可能是一颗螺丝钉,但是你自己是可以多花时间,对自己接触到的工具链做研究,主动承担一些有挑战的任务,如果你的能力得到了认可,你就会有机会接触到更多有挑战的任务。能够分析出问题的痛点,提出一些适合场景且合理的解决方案,就是前端架构师通常做的事情。
  • 我以上说的,会有很多时候都需要跳出自己的舒适圈,并且需要付出更多的时间和努力,勤思考,多总结。所以,想成为前端架构师并不容易,加油吧~

5. 源码

问题

想请教一下大神在最初学习源码,组织开源时如何入门的,同时学习源码对于工作变现是不是有必然的联系,如何把控 ?

解答

  • 拿 Vue.js 为例吧,最初是兴趣驱动,好奇,后来是写文章,需要深入研究,再之后就是工作中陆陆续续地看,然后录源码课程前系统地看了好几遍。
  • 看源码的好处在于可以提升自己的内功,工作中遇到 bug 能快速定位和解决,充分了解它的职责边界等,另外现在面试似乎都喜欢问原理实现,熟悉源码肯定是一个加分项咯。

问题

框架熟悉哪几个比较合适呢 ?

解答

  • 通常优先精通工作中使用的框架,其它的了解即可。因为框架很多设计**都是相通的,一旦精通一门,之后想学习其他框架非常容易。

问题

人到 30 ,该如何规划未来 5 年的时间 ?

解答

  • 职业规划我下面有回答,我个人认为这个时间还是应该做技术、写代码的时间。

问题

你认为如何做职业生涯规划呢 ?从前前端几年了,感觉处于瓶颈期,目前比较迷茫,目标不明确。

解答

  • 通常做技术往上 2 条路。
  • 1 :纯技术路线:架构师技术专家
  • 2 :技术+管理路线。
  • 先找准你的方向,如果对技术感兴趣,建议走 1 路线,否则就走 2。
  • 对于管理,我不是很擅长,我的经验就是首先你自己的技术要过硬,让底下人认可你,其次就是思考怎么发挥团队的最大价值,为团队小伙伴谋福利,关注他们的成长等。
  • 但是无论哪条路线,你的技术一定要好,而且我是不太建议工作个 5,6 年就转纯管理,时间太短,即使做了也不要落下技术。
  • 所以你目前还是需要精进技术,突破瓶颈通过就需要跳出舒适圈,解决一些需要你跳一下才能搞定的问题,最好是能找到你目前工作中的一些痛点,通过技术的手段去解决。
  • 我看你在字节跳动,也可以关注一下其他团队做的事情。
  • 我知道头条有一个非常牛逼的大佬-张袁炜,他是我在百度时期的导师,你也可以找他交流下。

6. 最后

以上问题及回答全部来自: 我是开源库 better-scroll 的作者 -- 黄轶,你有什么问题要问我 ?

以上问题及回答,对笔者很有帮助,相信以上回答对前端开发者也会有很大的启发,能解决很多人的迷茫,所以整理成了这篇文章。

2018 年,我的本命年 - 前端工作师的年终总结

1. 前言

时间过得很快,2018 年已经接近尾声了。离开大学校园已经一年半,正式工作也一年半了。

2018 年,我的本命年,今年 24 岁,离 “而立之年” —— 30 岁, 又近了一步。今年对我而言,是人生的一个重要节点。今年是我觉得过得最快的一年,也是成长最多的一年。

2. 技术

作为一名代码搬运工,技术做为安身立命的本钱,今年技术上有了挺大的见长。

技术上,前端和后端都接触到当前流行的技术栈,前端方面有: vue.js 、react.js ;后端方面有:python 3 、node、express、mongodb、mysql。

但是这些应用层的知识都是次要的,学到的编程能力和编程思维才是最重要的,毕竟一门通,门门通。况且对于程序员来说,编程能力和编程思维占了 80%,其他 api 的运用只占了 20%。

2.1 前端

对于 vue 的相关技术栈,虽然之前也有在用,但今年是技术上达到熟练的一年,做过 公众号、pc 端管理后台、H 5 应用。经过几个的项目的锤炼,应用上应该达到了熟练程度,也学到了不少好用的技巧。

而 react 相关技术栈 ,是今年后半年学的。学而不用,等于没学。 所以要实战一下才行,所以做了个博客网站的项目,也就是本人现在的个人网站,并把项目源码开源在 github 上。这个过程中,也学到了一些常用的、基本的 api ,对一般的 react 项目,也能自行搭建和开发了。

今年还看完了一本书:【WebKit 技术内幕】。看的不是纸质版的,是 pdf 的电子版,对浏览器和 WebKit 也有了一丢丢深入的了解,随着时间的久远,忘得差不多了 😅。

2.2 后端

python 3 和 mysql 是前半年学的,最初想着边做前端边能用 python 的,不过没找到相应的工作,最后还是做前端,现在很久没用,也忘得差不多了啦 😅。

对于 node、express、mongodb 是今年后半年学的,主要是为了快速搭建博客网站后端的。虽然还有很多要优化的地方,特别是数据的查询方面,但是最终还是搭建出来了。过程中,发现 node 比 python 好学,毕竟是用的是 javaScript 语言。

对于编程也有了一丢丢的理解。之前看到阮一峰老师的一篇文章内容,说得好有道理。他的原文是这样说的:

在此引用一个开发者对年轻程序员的告诫:在软件开发中,技术变化如此之快,你花费了大量时间学习技术和工具,一旦这些技术被取代,你的知识将变得毫无价值,因为它们大部分都是实施的细节。

我最近总是在想这段话,软件开发算不算是真正的知识 ?
如果它是一种真正的知识,那么理论上,我们学到的东西大部分应该不会过时,就好像微积分不会过时一样。可是实际上,我们都知道,软件开发技能有时效性,十年前学习的编程知识,十年后几乎肯定不能用于生产。那样的话,软件开发就不能算真正的知识,只是一种实施的细节。

公司旁边有一家税务所,每天都有很多人排队交税。如果你是第一次来交税,肯定搞不清楚怎么交,交税是一门学问,必须有人教你,要带哪些证件,要填哪些表,去哪些窗口排队等等。

我现在认为,学习编程跟学习交税是一样的,都是学习实施的细节。一旦外部环境变了,原来的实施细节就没用了。 当代编程由于层层的抽象和封装,我们已经不必接触底层真正具有通用性的知识了。大部分时候,所谓编程就是在写某个抽象层的配置。比如,网页样式就是在写 CSS 配置,你很难说这到底是真正的知识,还是像《办税指南》那样的实施细节。

实施细节并不是知识,而是操作步骤。如果技术栈发生变更,实施细节就会毫无用处。但是,你又不能不学习它,不知道实施细节,就没法做出项目。我觉得,程序员应该要警惕,不要落入实施细节的陷阱,不要把全部精力花在实施细节上面,然后以为自己学到了真正的知识。对待各种语言和工具,正确的态度应该是“进得去,出得来”,既要了解足够的细节,也要能够站在宏观的角度看待它,探寻底层到底是怎么实现的。

3. 工作

今年 5 月份的时候,换了东家。

在上一家东家那里学到了很多东西,毕竟是刚毕业后工作的第一年。上一家东家的两位前端老大和另外二位后端开发,无论是技术还是做事上,对我都产生了比较大的影响,感谢。上一家东家的工作氛围还是很好的,特别怀念的是每周五一次的运动啊。

在现在的公司,也不错,也有不少学习的榜样,就少了活动与运动节目。

我一直认为一个合格的程序员,正常的工作安排,应该都是在上班时间高效的做完的,下班了就准时下班的。所以很多时候,我都是下午 5 点半 准时下班的,毕竟回去之后,想做的事还有一堆呢。

但是非正常的工作安排就不一定了,比如项目很紧。因为项目时间紧,今年试过那么几次加班修 bugger 到凌晨 3 点的,然后早上七点多起来继续的。还试过一次项目中的数据被同事误删了,要配合后端开发抢救的,抢救到接近凌晨 4 点,第二天早上 8 点多照常起来正常上班的。只能感叹一声:修仙真棒,年轻真好!!!

4. 运动

身体是一,金钱、地位、荣誉则是零,只有有了前面的一,后面的零才会有用;反之,则都是做了无用功。

这一年来,还是和往年一样,时不时会进行各种运动,运动的项目一般有:跑步,健身,羽毛球,骑行等。

跑步的频率大概每月平均有 3 次吧,每次一般都是 5 公里;健身大概每周 2 次;羽毛球就得看有没有合适的时机了;而骑行呢,现在是只要天气许可,下班都是骑车回去,因为比搭公交车实在是快太多了,时间宝贵啊。不间断的运动也慢慢成为了一种习惯。正因为一直有不间断的运动,所以这一年来又没有感冒过,身体还算健康。

图一

  • 图一是 2018-07-17 到 2018-12-18 期间,所有运动的数据,以骑行为主。

图二

  • 图二是 2017-03-20 到 2018-07-15 期间,所有运动的数据,以跑步为主。

两图的总路程加起来,够回家两趟了 😇。

这不间断的运动,也不算什么坚持,只是觉得应该做的,又刚好是喜欢做的事情而已。一直做着就成了习惯,能做自己喜欢的事情是一件幸福的事情。当然,现在正值冬季,户外运动的频率要相应减少好一点。

运动带来的益处真的是没法估量,大学四年在校期间都没有生病过,只在大一寒假在家的时候,感冒了一次。近 5 年来,还有一次感冒是一年多前,刚毕业找工作的时候,被两同学轮流感冒传染,最终没能顶住。还有的益处就是保持着一个健康的身形,腹肌,胸肌都还在,只是这一年感觉肚皮比之前厚了一点了 😂。一天坐十几个小时,来程序员来说真的很伤。

当运动成为一种习惯,终将会是受益一生的事情。

5. 额外技能

在 21 世纪, 写作、英语和编程 估计是最有前途的技能。

5.1 写作

今年掌握的最有用的技能应该就是 写作 了,估计这个是受用一生的技能。

逐渐地,写作又成了一个爱好与习惯。当一项技能变成爱好的时候,就能产生很巨大的能量(就像很多人喜欢玩的王者农药)。自从写作以来,利用在学习上的时间比之前多了,学习知识的时候有了一定的深入,毕竟要写给别人看的,自己如果都不理解,别人又怎会能懂呢。

这半年时间陆续写了 30 多篇文章,其中包含读书的笔记、随笔、技术文章,有写得不怎么样的,有写的挺好的,获得多人点赞的。虽然有时会参考一下别人的文章,但还是一直鼓励原创与坚持原创。大概只有作者才能懂原创的不易。

写博客半年以来,也见到了不一样的风景。文章写的好,会有编辑找你出书;会有猎头顾问找上你,给你介绍大厂的好工作;会有好公司的团队负责人找你,想你加入他们的团队等等。这些都有遇到过,但自知现在能力还欠缺,还要不断积累与沉淀。写作是展现自己才华与能力很好的方式,当积累到一定的程度,好机会自会找上门来。

如果 30 岁之后,不想写代码了或者写不动了,还能有一个额外技能可以谋生,且这个技能还是自己的一个兴趣来的,是多么快乐的一件事。

努力成为一个斜杠青年才是正途。(斜杠青年:不仅指那些有着多重身份,多重收入的人。它代表的是一种全新的人生价值,核心在于多元化的人生。)

花一样的年纪,该奋斗的年纪不要安逸,实现梦想的同时顺便赚点钱,何乐而不为?

5.2 做一顿好吃的

今年后半年里,还尝试自己做饭、煲粥和煲汤。虽然这些都会,但是刚出来工作后的一年里,还没做过呢。不过都很少做饭做菜,因为实在是太耗费时间了啊,还要在旁边看着。大多都是煲粥煲汤,放好各种材料就可以了,接着就是:一边玩电脑,一边等着吃就可以了,非常方便啊。

做一顿好吃的饭菜,也是一个必备的技能,毕竟 自己动手,丰衣足食

5.3 理财

理财 -- 人生必会技能。今年开始尝试用其他方式理财了,比如买股票,买基金,买活期产品等,虽然还一直在亏,但是都尝试一下,才知道这些东西好不好嘛。当然理财的水也很深,不是一朝一日就可学会的,需要长期研究才行。

出来社会之后,对钱的理解越来越深刻了,长大后 99% 的烦恼是因为没钱。

6. 娱乐

今年大多数空闲的时间都是和电脑度过了。看电影,看电视剧,看动漫还有运动。今年看了很多动漫,热门的 3D 国漫都看过了,或者在追着看。国漫真的强势掘起了。

今年是外出游玩最少的一年了,好像没有主动去过哪里游玩,仅有的几次都是身边的朋友叫去的。其实想去的地方真的还很多。只是还没有找到合适的那个人,和其一起去。

祖国山河那么秀丽,还是得努力挣钱,去看一看。

7. 期望 2019

2018 年完成的事:

  • 上手 react 技术栈
  • 上手 node 技术栈
  • 完成了自己的个人博客网站
  • 在 github 上开源了博客网站的源码
  • 把写作培养成了一种习惯
  • 运营个人公众号 【 全栈修炼 】
  • 不间断的运动,保持健康的体魄
  • 看完了一本书【WebKit 技术内幕】
  • 初尝其他理财方式
  • 做一顿好吃的

2019 的目标:

  • 把个人博客网站接入到公众号里面
  • 深入 vue 技术栈的原理与内在实现
  • 熟练 react 和 node 技术栈,可能还要学 java。
  • 学习算法与数据结构
  • 英语词汇量达到 7000
  • 加大运动量,增重 5 斤
  • 坚持写作,运营好公众号
  • 多看书与文章(书到用时方恨少)
  • 逐渐深入其他理财方式
  • 培养其他技能

当一个目标需要很长远的时间来实现时,那就将每天要做的事培养成习惯,就会变得很容易了(比如:英语)。

现在正值冬天,天气冷,什么都不想干。最近在学英语,跟着水滴阅读看英语原著,总是想看就看,不想看就不看了,觉得每天的任务只是个任务,还没习惯。得把每天花 20 分钟看英语原著培养成习惯才行。

最终目标是 尽早实现个人财富自由,做自己喜欢的事情。

我比较赞成的财务自由的解释是:所谓的财务自由,指的是某人再也不用为了满足生活必需而出售自己的时间了。

不是生活所迫,谁特么想努力!目标还是要有的,不然和咸鱼有什么区别 ?虽然 努力了不一定有结果,但是不努力一定很舒服。

没被生活折磨过只有两种可能,其一是有人替你扛了,其二是别着急,还没轮到你。

8. 总结

致敬将要过去的 2018 ,期望 2019。

要么不努力,让生活选择你,随波逐流;要么自己选择生活,做自己喜欢的事。

无论在哪里工作,无论做什么工作,握住能掌控的生活,遇见更好的自己,便是一件特别幸运又足够幸福的事。

往后余生,愿你我都能,从前生活是工作,今后工作是生活。

时光正好,未来还有无限可能,加油!

9. 最后

一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构(文字版)

vue

前言

本文内容讲解的内容:一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构

项目地址:https://github.com/biaochenxuying/vue-family-mindmap

markdown 文字版

pdf 版

先来张 Vue 全家桶 总图:

1. 项目目录 

scripts: 构建相关的文件,一般情况下我们不需要动。

  • git-hooks:存放git钩子的目录
  • alias.js:别名配置
  • config.js:生成rollup配置的文件
  • build.js:对 config.js 中所有的rollup配置进行构建
  • ci.sh:持续集成运行的脚本
  • release.sh: 用于自动发布新版本的脚本

dist: 构建后文件的输出目录

examples: 存放一些使用Vue开发的应用案例

flow: 类型声明,使用开源项目 [Flow]

packages: 存放独立发布的包的目录

test: 包含所有测试文件

src: 源码,重点

  • compiler: 编译器代码的存放目录,将 template 编译为 render 函数

  • core: 核心代码 ,与平台无关的代码

    • observer: 响应系统,包含数据观测的核心代码
    • vdom:包含虚拟DOM创建(creation)和打补丁(patching)的代码
    • instance:包含Vue构造函数设计相关的代码
    • global-api:包含给Vue构造函数挂载全局方法(静态方法)或属性的代码
    • components:包含抽象出来的通用组件
  • platforms: 不同平台的支持,包含平台特有的相关代码,不同平台的不同构建的入口文件也在这里

    • web:web平台

      • entry-runtime.js:运行时构建的入口,不包含模板(template)到render函数的编译器,所以不支持 template 选项,我们使用vue默认导出的就是这个运行时的版本。大家使用的时候要注意
      • entry-runtime-with-compiler.js:独立构建版本的入口,它在 entry-runtime 的基础上添加了模板(template)到render函数的编译器
      • entry-compiler.js:vue-template-compiler 包的入口文件
      • entry-server-renderer.js:vue-server-renderer 包的入口文件
      • entry-server-basic-renderer.js:输出 packages/vue-server-renderer/basic.js 文件
    • weex:混合应用

  • serve: 服务端渲染,包含(server-side rendering)的相关代码

  • sfc: 包含单文件组件( .vue 文件)的解析逻辑,用于vue-template-compiler包

  • shared: 共享代码,包含整个代码库通用的代码

package.json:对项目的描述文件,包含了依赖包等信息

yarn.lock :yarn 锁定文件

.editorconfig:针对编辑器的编码风格配置文件

.flowconfig:flow 的配置文件

.babelrc:babel 配置文件

.eslintrc:eslint 配置文件

.eslintignore:eslint 忽略配置

.gitignore:git 忽略配置

2. 源码构建,基于 Rollup 

1. 根据 format 构建格式可分为三个版(再根据有无 compiler ,每个版本中又可以再分出二个版本)

  • cjs:表示构建出来的文件遵循 CommonJS 规范

    • Runtime Only 
    • Runtime + Compiler
  • es:构建出来的文件遵循 ES Module 规范

    • Runtime Only 
    • Runtime + Compiler
  • umd:构建出来的文件遵循 UMD 规范

    • Runtime Only 
    • Runtime + Compiler

2. 总结

  • Runtime Only:通常需要借助如 webpack 的 vue-loader 工具把 .vue 文件编译成JavaScript,因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,因此代码体积也会更轻量。
    Runtime + Compiler:我们如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,则需要在客户端编译模板。Vue.js 2.0 中,最终渲染都是通过 render 函数,如果写 template 属性,则需要编译成 render 函数,那么这个编译过程会发生运行时,所以需要带有编译器的版本。

3. vue 本质:构造函数

function Vue (options) {
  if (process.env.NODE_ENV !== production' && !(this instanceof Vue) ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

总结

  • vue 本质上就是一个用 Function 实现的 Class,然后在它的原型 prototype 以及它本身上扩展了一系列的方法和属性。
  • Vue 不用 ES6 的 Class 去实现的原因:按功能区分,把功能扩展分散到多个模块中去实现,然后挂载中 vue 的原型 prototype 上,也有在 Vue 这个对象本身上。
  • 而不是在一个模块里实现所有,这种方式是用 Class 难以实现的。这么做的好处是非常方便代码的维护和管理。

4. 数据驱动

1. new Vue

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

2. init

  • 调用 this._init(options) 进行初始化

    • mergeOptions 合并配置
    • initLifecycle(vm) 初始化生命周期,调用生命周期钩子函数 callHook(vm, 'beforeCreate')
    • initEvents(vm) 初始化事件中心
    • initRender(vm) 初始化渲染
    • 初始化 data、props、computed、watcher 等等

3. Vue 实例挂载 $mount

  • $mount 这个方法的实现是和平台、构建方式都相关的。我们分析带 compiler 版本的 $mount 实现。在 Vue 2.0 版本中,所有 Vue 的组件最终都会转换成 render 方法。

      1. 它对 el 做了限制,Vue 不能挂载在 body、html 这样的根节点上。
      1. 如果没有定义 render 方法,则会调用 compileToFunctions 方法把 el 或者 template 字符串转换成 render 方法。
      1. mountComponent:核心就是先实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。
      1. 将 vm._isMounted 设置为 true,表示已经挂载
      1. 执行 mounted 钩子函数:callHook(vm, 'mounted')

4. compile

  • 在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个“在线编译”的过程,它是调用 compileToFunctions 方法实现的。

5. render: Vue 的 _render 方法是实例的一个私有方法,最终会把实例渲染成一个虚拟 Node。

  • vm._render 最终是通过执行 createElement 方法并返回的是 vnode,它是一个虚拟 Node

6. Virtual DOM(虚拟 dom): 本质上是一个原生的 JS 对象,用 class 来定义。

    1. 核心定义:几个关键属性,标签名、数据、子节点、键值等,其它属性都是都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。
    1. 映射到真实的 DOM ,实际上要经历 VNode 的 create、diff、patch 等过程。
    1. createElement: 创建 VNode
      1. children 的规范化:由于 Virtual DOM 实际上是一个树状结构,每一个 VNode 可能会有若干个子节点,这些子节点应该也是 VNode 的类型。因为子节点 children 是任意类型的,因此需要把它们规范成 VNode 类型。
        1. simpleNormalizeChildren:调用场景是 render 函数是编译生成的。
        1. normalizeChildren
          1. 一个场景是 render 函数是用户手写的,当 children 只有一个节点的时候,Vue.js 从接口层面允许用户把 children 写成基础类型用来创建单个简单的文本节点,这种情况会调用 createTextVNode 创建一个文本节点的 VNode。
          1. 另一个场景是当编译 slot、v-for 的时候会产生嵌套数组的情况,会调用 normalizeArrayChildren 方法,遍历 children (可能会递归调用 normalizeArrayChildren )。
        1. 总结
        • 经过对 children 的规范化,children 变成了一个类型为 VNode 的 Array
      1. VNode 的创建
      • 规范化 children 后,会去创建一个 VNode 的实例。

          1. 直接创建一个普通 VNode。
          1. 或者通过 createComponent 创建一个组件类型的 VNode,本质上它还是返回了一个 VNode。
          1. 总结
          • 每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个 VNode Tree,它很好的描述了我们的 DOM Tree。
      1. update:通过 Vue 的 _update 方法,_update 方法的作用是把 VNode 渲染成真实的 DOM。_update 的核心就是调用 vm.patch 方法,__patch__在不同的平台,比如 web 和 weex 上的定义是不一样的。

7. update 的核心:调用 vm.patch 方法

  • update:通过 Vue 的 _update 方法,_update 方法的作用是把 VNode 渲染成真实的 DOM。_update 的核心就是调用 vm.patch 方法,__patch__在不同的平台,比如 web 和 weex 上的定义是不一样的。

      1. 首次渲染
        1. 通过 createElm 方法,把虚拟节点创建真实的 DOM 并插入到它的父节点中。
        1. 然后调用 createChildren 方法去创建子元素,实际上是遍历子虚拟节点,递归调用 createElm。
        1. 接着再调用 invokeCreateHooks 方法执行所有的 create 的钩子并把 vnode push 到 insertedVnodeQueue
        1. 最后调用 insert 方法把 DOM 插入到父节点中,因为是递归调用,子元素会优先调用 insert,所以整个 vnode 树节点的插入顺序是先子后父。
        1. 总结
        • 其实就是调用原生 DOM 的 API 进行 DOM 操作,Vue 就是这样动态创建的 DOM。
      1. 数据更新

8. DOM:Vue 最终创建的 DOM。

9. 总结

  • 初始化 Vue 到最终渲染的整个过程:

new Vue => init => $mounted => compile => render => vnode => patch => DOM

5. 组件化

1. introduction

  • 组件化,就是把页面拆分成多个组件 (component),每个组件依赖的 CSS、JavaScript、模板、图片等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。

2. createComponent

  • 在 createElement 的实现的时候,如果不是一个普通的 html 标签,就是通过 createComponent 方法创建一个组件 VNode。

      1. 构造子类构造函数
      • Vue.extend 函数

        • Vue.extend 的作用就是构造一个 Vue 的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本身扩展了一些属性,如扩展 options、添加全局 API 等;并且对配置中的 props 和 computed 做了初始化工作;最后对于这个 Sub 构造函数做了缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造。

          • 当我们去实例化 Sub 的时候,就会执行 this._init 逻辑再次走到了 Vue 实例的初始化逻辑。
          • 代码如下:
const Sub = function VueComponent (options) {
  this._init(options)
}
    1. 安装组件钩子函数:installComponentHooks(data)

      • installComponentHooks 的过程就是把 componentVNodeHooks 的钩子函数合并到 data.hook 中,在 VNode 执行 patch 的过程中执行相关的钩子函数
      • 这里要注意的是合并策略,在合并过程中,如果某个时机的钩子已经存在 data.hook 中,那么通过执行 mergeHook 函数做合并,这个逻辑很简单,就是在最终执行的时候,依次执行这两个钩子函数即可。
- 3. 实例化 vnode

	- 通过 new VNode 实例化一个 vnode 并返回。需要注意的是和普通元素节点的 vnode 不同,组件的 vnode 是没有 children 的,这点很关键

- 4. 总结

	- createComponent 后返回的是组件 vnode,它也一样走到 vm._update 方法,进而执行了 patch 函数。

3. path

  • 一个组件的 VNode 是如何创建、初始化、渲染的过程

4. 合并配置

    1. 外部调用场景
    • 外部我们的代码主动调用 new Vue(options) 的方式实例化一个 Vue 对象。
    1. 组件场景
    • 上一节分析的组件过程中内部通过 new Vue(options) 实例化子组件。
    1. 总结
    • 子组件初始化过程通过 initInternalComponent 方式要比外部初始化 Vue 通过 mergeOptions 的过程要快,合并完的结果保留在 vm.$options 中。

5. 生命周期

注意:activated 和 deactivated 钩子函数是专门为 keep-alive 组件定制的钩子。

6. 组件注册

    1. 全局注册:Vue.component(tagName, options)
    1. 局部注册
    1. 总结
    • 注意,局部注册和全局注册不同的是,只有该类型的组件才可以访问局部注册的子组件,而全局注册是扩展到 Vue.options 下,所以在所有组件创建的过程中,都会从全局的 Vue.options.components 扩展到当前组件的 vm.$options.components 下,这就是全局注册的组件能被任意使用的原因。

7. 异步组件

    1. 普通函数异步组件

代码

Vue.component('async-example', function (resolve, reject) {
   // 这个特殊的 require 语法告诉 webpack
   // 自动将编译后的代码分割成不同的块,
   // 这些块将通过 Ajax 请求自动下载。
   require(['./my-async-component'], resolve)
})

    1. Promise 异步组件

代码

Vue.component(
  'async-webpack-example',
  // 该 `import` 函数返回一个 `Promise` 对象。
  () => import('./my-async-component')
)
    1. 高级异步组件

代码

const AsyncComp = () => ({
  // 需要加载的组件。应当是一个 Promise
  component: import('./MyComp.vue'),
  // 加载中应当渲染的组件
  loading: LoadingComp,
  // 出错时渲染的组件
  error: ErrorComp,
  // 渲染加载中组件前的等待时间。默认:200ms。
  delay: 200,
  // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  timeout: 3000
})
Vue.component('async-example', AsyncComp)
    1. 总结

异步组件实现的本质是 2 次渲染,除了 0 delay 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 forceRender 强制重新渲染,这样就能正确渲染出我们异步加载的组件了。

6. 深入响应式原理

1. 响应式对象:Vue.js 实现响应式的核心是利用了 ES5 的 Object.defineProperty。

    1. Object.defineProperty

直接在一个对象上定义一个新属性,或者修改一个对象的现有属性

    1. initState:在 Vue 的初始化阶段,_init 方法执行的时候,会执行 initState(vm) 方法
    • 主要是对 props、methods、data、computed 和 wathcer 等属性做了初始化操作

        1. initProps:props 的初始化主要过程,就是遍历定义的 props 配置
          1. 一个是调用 defineReactive 方法把每个 prop 对应的值变成响应式,可以通过 vm._props.xxx 访问到定义 props 中对应的属性。
          1. 通过 proxy 把 vm._props.xxx 的访问代理到 vm.xxx 上
        1. initData
          1. 一个是对定义 data 函数返回对象的遍历,通过 proxy 把每一个值 vm._data.xxx 都代理到 vm.xxx 上;
          1. 另一个是调用 observe 方法观测整个 data 的变化,把 data 也变成响应式,可以通过 vm._data.xxx 访问到定义 data 返回函数中对应的属性
        1. proxy:代理的作用是把 props 和 data 上的属性代理到 vm 实例上
        • proxy 方法的实现很简单,通过 Object.defineProperty 把 target[sourceKey][key] 的读写变成了对 target[key] 的读写。

          • 比如 data ,对 vm._data.xxxx 的读写变成了对 vm.xxxx 的读写。
        1. 总结
        • 无论是 props 或是 data 的初始化都是把它们变成响应式对象
    1. observe :功能就是用来监测数据的变化
    • observe 方法的作用就是给非 VNode 的对象类型数据添加一个 Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例
    1. Observer 是一个类,它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新
    1. defineReactive: 功能就是定义一个响应式对象,给对象动态添加 getter 和 setter。
    1. 总结
    • 响应式对象,核心就是利用 Object.defineProperty 给数据添加了 getter 和 setter,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集,setter 做的事情是派发更新

2. 依赖收集:响应式对象 getter 相关的逻辑就是做依赖收集

    1. Dep:整个 getter 依赖收集的核心
    • Dep 实际上就是对 Watcher 的一种管理。而且在同一时间只能有一个全局的 Watcher 被计算
    1. Watcher
    • Watcher 是一个 Class,定义了一些和 Dep 相关的属性, 还定义了一些原型的方法,和依赖收集相关的有 get、addDep 和 cleanupDeps 方法。

    • 总结

      • 在添加 deps 的订阅过程,可以通过 id 去重避免重复订阅。在每次添加完新的订阅,会移除掉旧的订阅
    1. 总结
    • 收集依赖就是订阅数据变化的 watcher 的收集。收集依赖的目的是为了当这些响应式数据发生变化,触发它们的 setter 的时候,能知道应该通知哪些订阅者去做相应的逻辑处理,我们把这个过程叫派发更新,其实 Watcher 和 Dep 就是一个非常经典的观察者设计模式的实现

3. 派发更新

  • 修改值的时候,会触发 setter ,会对新设置的值变成一个响应式对象,并通过 dep.notify() 通知所有的订阅者

    • 做派发更新的时候的一个优化的点,它并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue

    • 总结

      • 当数据发生变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher,都触发它们的 update 过程,这个过程又利用了队列做了进一步优化,在 nextTick 后执行所有 watcher 的 run,最后执行它们的回调函数。

4. nextTick

    1. JS 运行机制
    • JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤

        1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
        1. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
        1. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
        1. 主线程不断重复上面的第三步。
        1. 代码演示 macro task 和 micro task 执行顺序
for (macroTask of macroTaskQueue) {
    // 1. Handle current MACRO-TASK
    handleMacroTask();
      
    // 2. Handle all MICRO-TASK
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}

// 在浏览器环境中,常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate;
// 常见的 micro task 有 MutationObsever 和 Promise.then。
    1. 总结

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。

  • vue 中 nextTick 实现

      1. 申明了 microTimerFunc 和 macroTimerFunc 2 个变量,它们分别对应的是 micro task 的函数和 macro task 的函数。
      1. 对于 macro task 的实现,优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的 MessageChannel,如果也不支持的话就会降级为 setTimeout 0;
      1. 而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现。
      1. nextTick 把传入的回调函数 cb 压入 callbacks 数组,最后一次性地根据 useMacroTask 条件执行 macroTimerFunc 或者是 microTimerFunc,而它们都会在下一个 tick 执行 flushCallbacks,flushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数。

这里使用 callbacks 而不是直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。

- 5. next-tick.js 还对外暴露了 withMacroTask 函数,它是对函数做一层包装,确保函数执行过程中对数据任意的修改,触发变化执行 nextTick 的时候强制走 macroTimerFunc。比如对于一些 DOM 交互事件,如 v-on 绑定的事件回调函数的处理,会强制走 macro task。
- 6. 总结

对 nextTick 的分析,并结合上一节的 setter 分析,我们了解到数据的变化到 DOM 的重新渲染是一个异步过程,发生在下一个 tick。这就是我们平时在开发的过程中,比如从服务端接口去获取数据的时候,数据做了修改,如果我们的某些方法去依赖了数据修改后的 DOM 变化,我们就必须在 nextTick 后执行。

Vue.js 提供了 2 种调用 nextTick 的方式,一种是全局 API Vue.nextTick,一种是实例上的方法 vm.$nextTick,无论我们使用哪一种,最后都是调用 next-tick.js 中实现的 nextTick 方法。

5. 检测变化的注意事项

    1. 对象添加属性

对于使用 Object.defineProperty 实现响应式的对象,当我们去给这个对象添加一个新的属性的时候,是不能够触发它的 setter 的

var vm = new Vue({
  data:{
    a:1
  }
})
// vm.b 是非响应的
vm.b = 2

要用 Vue.set 方法

set 方法是在对象上设置属性。添加新属性和如果属性不存在,通过 defineReactive(ob.value, key, val) 把新添加的属性变成响应式对象,然后再通过 ob.dep.notify() 手动的触发依赖通知。

    1. 数组
    • Vue 也是不能检测到以下变动的数组

        1. 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
        • 可以使用:Vue.set(example1.items, indexOfItem, newValue)
        1. 当你修改数组的长度时,例如:vm.items.length = newLength
        • 可以使用 vm.items.splice(newLength)
    • 总结

      • vue 通过 arrayMethods 继承了 Array,然后对数组中所有能改变数组自身的方法,如 push、pop 等这些方法进行重写,重写后的方法会先执行它们本身原有的逻辑,并对能增加数组长度的 3 个方法 push、unshift、splice 方法做了判断,获取到插入的值,然后把新添加的值变成一个响应式对象,并且再调用 ob.dep.notify() 手动触发依赖通知,这就很好地解释了之前的示例中调用 vm.items.splice(newLength) 方法可以检测到变化。

6. 计算属性 VS 侦听属性

    1. computd
    • 计算属性本质上就是一个 computed watcher,确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染。

      • computed watcher
    1. watch
    • 本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher

      • deep watcher

      • user watcher

        • 通过 vm.$watch 创建的 watcher 是一个 user watcher,其实它的功能很简单,在对 watcher 求值以及在执行回调函数的时候,会处理一下错误
      • computed watcher

      • sync watcher

    1. 总结
    • 计算属性本质上是 computed watcher,而侦听属性本质上是 user watcher。就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。

7. 组件更新:过程的核心就是新旧 vnode diff,对新旧节点相同以及不同的情况分别做不同的处理。

    1. 新旧节点不同
    • 新旧 vnode 不同,本质上是要替换已存在的节点。

        1. 创建新节点
        • 以当前旧节点为参考节点,创建新的节点,并插入到 DOM 中
        1. 更新父的占位符节点
        1. 删除旧节点
        • 删除节点就是遍历待删除的 vnodes 做删除
    1. 新旧节点相同
    1. updateChildren

7. 编译

1. introduction

  • 模板到真实 DOM 渲染的过程,中间有一个环节是把模板编译成 render 函数,这个过程我们把它称作编译。

2. 编译入口

  • mount 的时候,通过 compileToFunctions 方法就是把模板 template 编译生成 render 以及 staticRenderFns

      1. 解析模板字符串生成 AST
      • const ast = parse(template.trim(), options)
      1. 优化语法树
      • optimize(ast, options)
      1. 生成代码
      • const code = generate(ast, options)
      1. 总结
      • 编译入口逻辑之所以这么绕,是因为 Vue.js 在不同的平台下都会有编译的过程,因此编译过程中的依赖的配置 baseOptions 会有所不同。而编译过程会多次执行,但这同一个平台下每一次的编译过程配置又是相同的,为了不让这些配置在每次编译过程都通过参数传入,Vue.js 利用了函数柯里化的技巧很好的实现了 baseOptions 的参数保留。同样,Vue.js 也是利用函数柯里化技巧把基础的编译过程函数抽出来,通过 createCompilerCreator(baseCompile) 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的。

3. parse

  • 编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。

    • 整体流程

        1. 从 options 中获取方法和配置, 如伪代码 getFnsAndConfigFromOptions(options)
        • 这些属性和方法之所以放到 platforms 目录下是因为它们在不同的平台(web 和 weex)的实现是不同的。
        1. 解析 HTML 模板, 对应伪代码 parseHTML(template, options)
        • 整体来说它的逻辑就是循环解析 template ,用正则做各种匹配,对于不同情况分别进行不同的处理,直到整个 template 被解析完毕。 在匹配的过程中会利用 advance 函数不断前进整个模板字符串,直到字符串末尾。

          • 匹配的过程中主要利用了正则表达式,通过一系列正则表达式,可以匹配注释节点、文档类型节点、文本、开始标签、闭合标签等。
        1. 处理开始标签
          1. 创建 AST 元素
          1. 处理 AST 元素
          • 这过程会判断 element 是否包含各种指令通过 processXXX 做相应的处理,处理的结果就是扩展 AST 元素的属性。比如 v-for、v-if 指令。
          1. AST 树管理
          • 在处理开始标签的时候为每一个标签创建了一个 AST 元素,在不断解析模板创建 AST 元素的时候,我们也要为它们建立父子关系,就像 DOM 元素的父子关系那样。

            • AST 树管理的目标是构建一颗 AST 树,本质上它要维护 root 根节点和当前父节点 currentParent。为了保证元素可以正确闭合,这里也利用了 stack 栈的数据结构,和我们之前解析模板时用到的 stack 类似。
        1. 处理闭合标签
        • 对应伪代码:
          end () {
          treeManagement()
          closeElement()
          }
        1. 处理文本内容
        • 对应伪代码:
          chars (text: string) {
          handleText()
          createChildrenASTOfText()
          }
        1. 总结
        • parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。 AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。其实这里我觉得源码写的不够友好,这种是典型的魔术数字,如果转换成用常量表达会更利于源码阅读。
        • 当 AST 树构造完毕,下一步就是 optimize 优化这颗树。

4. optimize

  • 当我们的模板 template 经过 parse 过程后,会输出生成 AST 树,那么接下来我们需要对这颗树做优化,Vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的比对。

      1. 标记静态节点 markStatic(root)
      1. 标记静态根 markStaticRoots(root, false)
  • 总结

    • optimize 的过程,就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成 DOM 永远不需要改变,这对运行时对模板的更新起到极大的优化作用。

5. codegen

  • 编译的最后一步就是把优化后的 AST 树转换成可执行的代码

例子:

<ul :class="bindCls" class="list" v-if="isShow">
    <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>
  • 它经过编译,执行 const code = generate(ast, options),生成的 render 代码串如下:
with(this){
  return (isShow) ?
    _c('ul', {
        staticClass: "list",
        class: bindCls
      },
      _l((data), function(item, index) {
        return _c('li', {
          on: {
            "click": function($event) {
              clickItem(index)
            }
          }
        },
        [_v(_s(item) + ":" + _s(index))])
      })
    ) : _e()
}
  • codegen 的目标是把 AST 树转换成代码字符串,整个 codegen 过程就是深度遍历 AST 树,根据不同条件生成不同代码的过程。

8. 扩展

1. event

    1. 编译
      1. 先从编译阶段开始看起,在 parse 阶段,会执行 processAttrs 方法。
      1. processAttrs 方法在对标签属性的处理过程中,判断如果是指令,首先通过 parseModifiers 解析出修饰符,然后判断如果事件的指令,则执行 addHandler(el, name, value, modifiers, false, warn) 方法。
      1. addHandler 函数实际上就做了 3 件事情,首先根据 modifier 修饰符对事件名 name 做处理,接着根据 modifier.native 判断是一个纯原生事件还是普通事件,分别对应 el.nativeEvents 和 el.events,最后按照 name 对事件做归类,并把回调函数的字符串保留到对应的事件中。
      1. 然后在 codegen 的阶段,会在 genData 函数中根据 AST 元素节点上的 events 和 nativeEvents 生成 data 数据,也即是事件的代码字符串。
    1. DOM 事件
      1. 原生 DOM 事件
    1. 自定义事件
    • Vue 还支持了自定义事件,并且自定义事件只能作用在组件上,如果在组件上使用原生事件,需要加 .native 修饰符,普通元素上使用 .native 修饰符无效

    • 总结

      • Vue 支持 2 种事件类型,原生 DOM 事件和自定义事件,它们主要的区别在于添加和删除事件的方式不一样,并且自定义事件的派发是往当前实例上派发,但是可以利用在父组件环境定义回调函数来实现父子组件的通讯。另外要注意一点,只有组件节点才可以添加自定义事件,并且添加原生 DOM 事件需要使用 native 修饰符;而普通元素使用 .native 修饰符是没有作用的,也只能添加原生 DOM 事件。

2. v-model

实现

在理解 Vue 的时候都把 Vue 的数据响应原理理解为双向绑定,但实际上这是不准确的,我们之前提到的数据响应,都是通过数据的改变去驱动 DOM 视图的变化,而双向绑定除了数据驱动 DOM 外, DOM 的变化反过来影响数据,是一个双向关系,在 Vue 中,我们可以通过 v-model 来实现双向绑定。

    1. 表单元素

结合示例来分析:

let vm = new Vue({
  el: '#app',
  template: '<div>'
  + '<input v-model="message" placeholder="edit me">' +
  '<p>Message is: {{ message }}</p>' +
  '</div>',
  data() {
    return {
      message: ''
    }
  }
})
  1. 首先在 parse 阶段, v-model 被当做普通的指令解析到 el.directives 中,然后在 codegen 阶段,执行 genData ,最终的生成的 code
if($event.target.composing)return; message=$event.target.value
  1. code 生成完后,又执行了 2 句非常关键的代码:
addProp(el, 'value', `(${value})`)
addHandler(el, event, code, null, true)  

这实际上就是 input 实现 v-model 的精髓,通过修改 AST 元素,给 el 添加一个 prop,相当于我们在 input 上动态绑定了 value,又给 el 添加了事件处理,相当于在 input 上绑定了 input 事件,其实转换成模板如下:

<input
  v-bind:value="message"
  v-on:input="message=$event.target.value">

其实就是动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message 设置为目标值,这样实际上就完成了数据双向绑定了,所以说 v-model 实际上就是语法糖。

  1. 最终生成的 render 代码如下:
with(this) {
  return _c('div',[_c('input',{
    directives:[{
      name:"model",
      rawName:"v-model",
      value:(message),
      expression:"message"
    }],
    attrs:{"placeholder":"edit me"},
    domProps:{"value":(message)},
    on:{"input":function($event){
      if($event.target.composing)
        return;
      message=$event.target.value
    }}}),_c('p',[_v("Message is: "+_s(message))])
    ])
}
    1. 组件

通过一个例子分析:

let Child = {
  template: '<div>'
  + '<input :value="value" @input="updateValue" placeholder="edit me">' +
  '</div>',
  props: ['value'],
  methods: {
    updateValue(e) {
      this.$emit('input', e.target.value)
    }
  }
}

let vm = new Vue({
  el: '#app',
  template: '<div>' +
  '<child v-model="message"></child>' +
  '<p>Message is: {{ message }}</p>' +
  '</div>',
  data() {
    return {
      message: ''
    }
  },
  components: {
    Child
  }
})

可以看到,父组件引用 child 子组件的地方使用了 v-model 关联了数据 message;而子组件定义了一个 value 的 prop,并且在 input 事件的回调函数中,通过 this.$emit('input', e.target.value) 派发了一个事件,为了让 v-model 生效,这两点是必须的。

其实就相当于我们在这样编写父组件:

let vm = new Vue({
  el: '#app',
  template: '<div>' +
  '<child :value="message" @input="message=arguments[0]"></child>' +
  '<p>Message is: {{ message }}</p>' +
  '</div>',
  data() {
    return {
      message: ''
    }
  },
  components: {
    Child
  }
})

子组件传递的 value 绑定到当前父组件的 message,同时监听自定义 input 事件,当子组件派发 input 事件的时候,父组件会在事件回调函数中修改 message 的值,同时 value 也会发生变化,子组件的 input 值被更新。

  • 总结

    • v-model 实现双向绑定的本质上就是一种语法糖,它即可以支持原生表单元素,也可以支持自定义组件。在组件的实现中,我们是可以配置子组件接收的 prop 名称,以及派发的事件名称。

3. slot

    1. 编译
    • 编译是发生在调用 vm.$mount 的时候,所以编译的顺序是先编译父组件,再编译子组件。
    1. 普通插槽
      1. 有定义对应 name 的是具名插槽
      1. 没有定义 name 的是默认插槽
    1. 作用域插槽
  • 总结

    • 它们有一个很大的差别是数据作用域,普通插槽是在父组件编译和渲染阶段生成 vnodes,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的 vnodes。而对于作用域插槽,父组件在编译和渲染阶段并不会直接生成 vnodes,而是在父节点 vnode 的 data 中保留一个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在编译和渲染子组件阶段才会执行这个渲染函数生成 vnodes,由于是在子组件环境执行的,所以对应的数据作用域是子组件实例。

4. keep-alive

    1. 内置组件
    • 是 Vue 源码中实现的一个组件,是 Vue 的内置组件,是下抽象组件,形式 “有点像” 平时写的 Vue 的组件,但是做了缓存的处理。
    1. 组件渲染
    1. 生命周期
    1. 总结
      1. 组件是一个抽象组件,它的实现通过自定义 render 函数并且利用了插槽,并且 缓存 vnode,组件包裹的子元素——也就是插槽是如何做更新的
      1. 且在 patch 过程中对于已缓存的组件不会执行 mounted,所以不会有一般的组件的生命周期函数但是又提供了 activated 和 deactivated 钩子函数。
      1. 的 props 除了 include 和 exclude 还有文档中没有提到的 max,它能控制我们缓存的个数。

5. transition

    1. 内置组件
    • 组件和 组件一样,都是 Vue 的内置组件,同样是抽象组件,同样直接实现 render 函数,同样利用了默认插槽。而且 组件是 web 平台独有的
    1. transition module
    • 动画相关的逻辑,过渡动画提供了 2 个时机,一个是 create 和 activate 的时候提供了 entering 进入动画,一个是 remove 的时候提供了 leaving 离开动画
    1. entering
    • 主要发生在组件插入后
    1. leaving
    • 主要发生在组件销毁前
    1. 总结
      1. 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。
      1. 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。
      1. 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。
    • 总结

      • 所以真正执行动画的是我们写的 CSS 或者是 JavaScript 钩子函数,而 Vue 的 只是帮我们很好地管理了这些 CSS 的添加/删除,以及钩子函数的执行时机。

6. transition-group

    1. render 函数
    • 组件也是由 render 函数渲染生成 vnode,不同于 组件, 组件非抽象组件,它会渲染成一个真实元素,默认 tag 是 span。
    1. move 过渡实现
    1. 总结
    • 和 组件相比,实现了列表的过渡,以及它会渲染成真实的元素。当我们去修改列表的数据的时候,如果是添加或者删除数据,则会触发相应元素本身的过渡动画,这点和 组件实现效果一样,除此之外 还实现了 move 的过渡效果,让我们的列表过渡动画更加丰富。

9. Vue-Router

1. introduction

  • Vue-Router 的能力十分强大,它支持 hash、history、abstract 3 种路由方式,提供了 和 2 种组件,还提供了简单的路由配置和一系列好用的 API。注意:本思维导图主要讲的是 hash 模式下的。

2. 路由注册

    1. Vue.use
    • Vue 提供了 Vue.use 的全局 API 来注册这些插件,比如注册 VueRouter。
    1. 路由安装
      1. VueRouter 本质上是一个类,实现了 install 的静态方法:VueRouter.install = install,当执行 Vue.use(VueRouter) 的时候,实际上就是在执行 install 函数
      1. Vue-Router 安装最重要的一步就是利用 Vue.mixin 去把 beforeCreate 和 destroyed 钩子函数注入到每一个组件中。
      1. 通过 Vue.component 方法定义了全局的 和 2 个组件,这也是为什么我们在写模板的时候可以使用这两个标签

3. VueRouter 对象

    1. VueRouter 的实现是一个类,定义了一些属性和方法。
    1. 当我们执行 new VueRouter 的时候
      1. 在浏览器不支持 history.pushState 的情况下,根据传入的 fallback 配置参数,决定是否回退到 hash 模式。
      1. 实例化 VueRouter 后会返回它的实例 router
    1. 组件在执行 beforeCreate 钩子函数的时候,如果传入了 router 实例,都会执行 router.init 进行初始化。
    1. 然后又会执行 history.transitionTo 方法做路由过渡,进而引出了 matcher 的概念。

4. matcher

    1. createMatcher
    • createMatcher 的初始化就是根据路由的配置描述建立映射表,包括路径、名称到路由 record 的映射关系。
    1. addRoutes
    • addRoutes 方法的作用是动态添加路由配置,因为在实际开发中有些场景是不能提前把路由写死的,需要根据一些条件动态添加路由

      • function addRoutes (routes) {
        createRouteMap(routes, pathList, pathMap, nameMap)
        }

        • addRoutes 的方法十分简单,再次调用 createRouteMap 即可,传入新的 routes 配置,由于 pathList、pathMap、nameMap 都是引用类型,执行 addRoutes 后会修改它们的值。
    1. match
    • match 会根据传入的位置和路径计算出新的位置并匹配到相应的路由 record ,然后根据新的位置 和 record 创建新的路径并返回。

      • 通过 matcher 的 match 方法,我们会找到匹配的路径 Route,这个对 Route 的切换,组件的渲染都有非常重要的指导意义。

5. 路径切换

    1. history.transitionTo
      1. 前一节我们分析了 matcher 的相关实现,知道它是如何找到匹配的新线路,那么匹配到新线路后,当我们切换路由线路的时候,就会执行到方法 transitionTo。
      1. 拿到新的路径后,那么接下来就会执行 confirmTransition 方法去做真正的切换,由于这个过程可能有一些异步的操作(如异步组件),所以整个 confirmTransition API 设计成带有成功回调函数和失败回调函数。
      1. 拿到 updated、activated、deactivated 3 个 ReouteRecord 数组后,接下来就是路径变换后的一个重要部分,执行一系列的钩子函数,也就是导航守卫。
    1. 导航守卫
    • 实际上就是发生在路由路径切换的时候,执行的一系列钩子函数。

      • 完整的导航解析流程

          1. 导航被触发。
          1. 在失活的组件里调用离开守卫。
          1. 调用全局的 beforeEach 守卫。
          1. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
          1. 在路由配置里调用 beforeEnter。
          1. 解析异步路由组件。
          1. 在被激活的组件里调用 beforeRouteEnter。
          1. 调用全局的 beforeResolve 守卫 (2.5+)。
          1. 导航被确认。
          1. 调用全局的 afterEach 钩子。
          1. 触发 DOM 更新。
          1. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
    1. url ( hash 模式 )
      1. 当我们点击 router-link 的时候,实际上最终会执行 router.push。
      1. push 函数会先执行 this.transitionTo 做路径切换,在切换完成的回调函数中,执行 pushHash 函数
      1. pushState 会调用浏览器原生的 history 的 pushState 接口或者 replaceState 接口,更新浏览器的 url 地址,并把当前 url 压入历史栈中。
      1. ensureSlash
    1. 组件

路由最终的渲染离不开组件,Vue-Router 内置了 组件。 是一个 functional 组件,它的渲染也是依赖 render 函数。

  1. 是支持嵌套的,回到 render 函数,其中定义了 depth 的概念,它表示 嵌套的深度。
  2. 每个 在渲染的时候,会进行一个循环,就是从当前的 的父节点向上找,一直找到根 Vue 实例,在这个过程,如果碰到了父节点也是 的时候,说明 有嵌套的情况,depth++。遍历完成后,根据当前线路匹配的路径和 depth 找到对应的 RouteRecord,进而找到该渲染的组件。
  3. 注册路由实例
const registerInstance = (vm, callVal) => {
  let i = vm.$options._parentVnode
  if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
    i(vm, callVal)
  }
}

Vue.mixin({
  beforeCreate () {
    // ...
    registerInstance(this, this)
  },
  destroyed () {
    registerInstance(this)
  }
})

在混入的 beforeCreate 钩子函数中,会执行 registerInstance 方法,进而执行 render 函数中定义的 registerRouteInstance 方法,从而给 matched.instances[name] 赋值当前组件的 vm 实例。

  1. render 函数的最后根据 component 渲染出对应的组件 vonde:

return h(component, data, children)

  1. 当我们执行 transitionTo 来更改路由线路后,组件是如何重新渲染 ?

  2. 在 Vue 混入的 beforeCreate 钩子函数中,我们把根 Vue 实例的 _route 属性定义成响应式的了。

 if (isDef(this.$options.router)) {
      Vue.util.defineReactive(this, '_route', this._router.history.current)
    }
  1. 访问 this._routerRoot._route,触发了它的 getter,相当于 对它有依赖,然后再执行完 transitionTo 后,修改 app._route 的时候,又触发了setter,因此会通知 的渲染 watcher 更新,重新渲染组件。

Vue-Router 还内置了另一个组件 ,它支持用户在具有路由功能的应用中(点击)导航。 通过 to 属性指定目标地址,默认渲染成带有正确链接的 标签,可以通过配置 tag 属性生成别的标签。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的 CSS 类名。

  1. 首先做了路由解析
  2. router.resolve 计算出最终跳转的 href
  3. 对 exactActiveClass 和 activeClass 做处理
  4. 创建了一个守卫函数 handler,最终会监听点击事件或者其它可以通过 prop 传入的事件类型,执行 hanlder 函数,最终执行 router.push 或者 router.replace 函数

实际上就是执行了 history 的 push 和 replace 方法做路由跳转。

  1. 最后判断当前 tag 是否是 标签, 默认会渲染成 标签,当然我们也可以修改 tag 的 prop 渲染成其他节点,这种情况下会尝试找它子元素的 标签,如果有则把事件绑定到 标签上并添加 href 属性,否则绑定到外层元素本身。
    1. 总结
    • 路径变化是路由中最重要的功能,我们要记住以下内容:路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据。

6. 总结

  • 路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据。

10. Vuex

1. introduction

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

  • 什么是“状态管理模式”?

      1. state,驱动应用的数据源;
      1. view,以声明方式将 state 映射到视图;
      1. actions,响应在 view 上的用户输入导致的状态变化。
  • Vuex 核心**

      1. Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)
      1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
      1. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

2. Vuex 初始化

    1. 安装
  1. 当我们在代码中通过 import Vuex from 'vuex' 的时候,实际上引用的是一个对象,它的定义在 src/index.js 中:
export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}
  1. 和 Vue-Router 一样,Vuex 也同样存在一个静态的 install 方法,它的定义在 src/store.js 中:
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
  1. install 的逻辑很简单,把传入的 _Vue 赋值给 Vue 并执行了 applyMixin(Vue) 方法,执行 Vue.mixin({ beforeCreate: vuexInit })。

它其实给 Vue 全局混入了一个 beforeCreate 钩子函数,它的实现非常简单,就是把 options.store 保存在所有组件的 this.$store 中,这个 options.store 就是我们在实例化 Store 对象的实例。

    1. Store 实例化
  1. 用法
const store = new Vuex.Store({
    strict: process.env.NODE_ENV !== "production",
    modules: {
        moduleA
    },
    state: initPageState(),
    mutations: {},
    actions: {}
});

export default store;

Store 对象的构造函数也是一个 class,接收一个对象参数,它包含 actions、getters、state、mutations、modules 等 Vuex 的核心概念

  1. 初始化模块

  2. Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter,甚至是嵌套子模块——从上至下进行同样方式的分割

- const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... },
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

从数据结构上来看,模块的设计就是一个树型结构,store 本身可以理解为一个 root module,它下面的 modules 就是子模块,Vuex 需要完成这颗树的构建。

    1. 构建过程的入口
this._modules = new ModuleCollection(options)
  1. 调用 register 方法,通过 const newModule = new Module(rawModule, runtime) 创建了一个 Module 的实例,Module 是用来描述单个模块的类。

  2. register 首先根据路径获取到父模块,然后再调用父模块的 addChild 方法建立父子关系。

  3. register 的最后一步,就是遍历当前模块定义中的所有 modules,根据 key 作为 path,递归调用 register 方法,这样就建立父子关系。

    1. 安装模块

对模块中的 state、getters、mutations、actions 做初始化工作
它的入口代码是:

const state = this._modules.root.state;
installModule(this, state, [], this._modules.root);
  1. 默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。如果我们希望模块具有更高的封装度和复用性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

  2. 构造了一个本地上下文环境:

const local = module.context = makeLocalContext(store, namespace, path);
  1. registerMutation
  2. registerAction
  3. registerGetter

总结: 所以 installModule 实际上就是完成了模块下的 state、getters、actions、mutations 的初始化工作,并且通过递归遍历的方式,就完成了所有子模块的安装工作。

    1. 初始化 store._vm

Store 实例化的最后一步,就是执行初始化 store._vm 的逻辑,它的入口代码是:

resetStoreVM(this, state);

resetStoreVM 的作用实际上是想建立 getters 和 state 的联系,因为从设计上 getters 的获取就依赖了 state ,并且希望它的依赖能被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。因此这里利用了 Vue 中用 computed 计算属性来实现。

strict mode

当严格模式下,store._vm 会添加一个 wathcer 来观测 this._data.$$state 的变化,也就是当 store.state 被修改的时候, store._committing 必须为 true,否则在开发阶段会报警告。

if (store.strict) {
  enableStrictMode(store)
}

function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}
    1. 总结

我们要把 store 想象成一个数据仓库,为了更方便的管理仓库,我们把一个大的 store 拆成一些 modules,整个 modules 是一个树型结构。每个 module 又分别定义了 state,getters,mutations、actions,我们也通过递归遍历模块的方式都完成了它们的初始化。为了 module 具有更高的封装度和复用性,还定义了 namespace 的概念。最后我们还定义了一个内部的 Vue 实例,用来建立 state 到 getters 的联系,并且可以在严格模式下监测 state 的变化是不是来自外部,确保改变 state 的唯一途径就是显式地提交 mutation。

3. API

    1. 数据获取
    • Vuex 最终存储的数据是在 state 上的,我们之前分析过在 store.state 存储的是 root state,那么对于模块上的 state,假设我们有 2 个嵌套的 modules,它们的 key 分别为 a 和 b,我们可以通过 store.state.a.b.xxx 的方式去获取。

      • 在递归执行 installModule 的过程中,就完成了整个 state 的建设,这样我们就可以通过 module 名的 path 去访问到一个深层 module 的 state。
    1. 数据存储
      1. Vuex 对数据存储的存储本质上就是对 state 做修改,并且只允许我们通过提交 mutaion 的形式去修改 state。
      1. mutation 必须是同步函数
      1. action
      • action 类似于 mutation,不同在于 action 提交的是 mutation,而不是直接操作 state,并且它可以包含任意异步操作。

        • action 比我们自己写一个函数执行异步操作然后提交 muataion 的好处是在于它可以在参数中获取到当前模块的一些方法和状态,Vuex 帮我们做好了这些。
    1. 语法糖
  1. mapState

mapState 支持传入 namespace, 因此我们可以这么写:

computed: {
  mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
},

在 mapState 的实现中,如果有 namespace,则尝试去通过 getModuleByNamespace(this.$store, 'mapState', namespace) 对应的 module,然后把 state 和 getters 修改为 module 对应的 state 和 getters

主要原因是在 Vuex 初始化执行 installModule 的过程中,初始化了这个映射表:

function installModule (store, rootState, path, module, hot) {
  // ...
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // ...
}
  1. mapGetters

mapGetters 的用法:

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
    // 使用对象展开运算符将 getter 混入 computed 对象中
    mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

和 mapState 类似,mapGetters 是将 store 中的 getter 映射到局部计算属性

mapGetters 也同样支持 namespace,如果不写 namespace ,访问一个子 module 的属性需要写很长的 key,一旦我们使用了 namespace,就可以方便我们的书写,每个 mappedGetter 的实现实际上就是取 this.$store.getters[val]。

    1. mapMutations

我们可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 的调用。mapMutations 支持传入一个数组或者一个对象,目标都是组件中对应的 methods 映射为 store.commit 的调用。

mapMutations 的用法:

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
}

mappedMutation 同样支持了 namespace,并且支持了传入额外的参数 args,作为提交 mutation 的 payload,最终就是执行了 store.commit 方法,并且这个 commit 会根据传入的 namespace 映射到对应 module 的 commit 上。

    1. mapActions

在组件中使用 this.$store.dispatch('xxx') 提交 action,或者使用 mapActions 辅助函数将组件中的 methods 映射为 store.dispatch 的调用。

mapActions 在用法上和 mapMutations 几乎一样,实现也很类似,和 mapMutations 的实现几乎一样,不同的是把 commit 方法换成了 dispatch。

    1. 动态更新模块
  1. 模块动态注册 registerModule

在有一些场景下,我们需要动态去注入一些新的模块,Vuex 提供了模块动态注册功能,在 store 上提供了一个 registerModule 的 API。

registerModule 支持传入一个 path 模块路径 和 rawModule 模块定义,首先执行 register 方法扩展我们的模块树,接着执行 installModule 去安装模块,最后执行 resetStoreVM 重新实例化 store._vm,并销毁旧的 store._vm。

  1. 动态卸载模块 unregisterModule

相对的,有动态注册模块的需求就有动态卸载模块的需求,Vuex 提供了模块动态卸载功能,在 store 上提供了一个 unregisterModule 的 API。

  1. unregisterModule 支持传入一个 path 模块路径,首先执行 unregister 方法去修剪我们的模块树。 注意,这里只会移除我们运行时动态创建的模块。
  2. 接着会删除 state 在该路径下的引用,最后执行 resetStore 方法。
  3. 该方法就是把 store 下的对应存储的 _actions、_mutations、_wrappedGetters 和 _modulesNamespaceMap 都清空,然后重新执行 installModule 安装所有模块以及 resetStoreVM 重置 store._vm。

4. 插件

Vuex 除了提供的存取能力,还提供了一种插件能力,让我们可以监控 store 的变化过程来做一些事情。

    1. Vuex 的 store 接受 plugins 选项,我们在实例化 Store 的时候可以传入插件,它是一个数组,然后在执行 Store 构造函数的时候,会执行这些插件:
const {
  plugins = [],
  strict = false
} = options
// apply plugins
plugins.forEach(plugin => plugin(this));
    1. Logger 插件
  1. Logger 函数,它相当于订阅了 mutation 的提交,它的 prevState 表示之前的 state,nextState 表示提交 mutation 后的 state,这两个 state 都需要执行 deepCopy 方法拷贝一份对象的副本,这样对他们的修改就不会影响原始 store.state。

  2. 接下来就构造一些格式化的消息,打印出一些时间消息 message, 之前的状态 prevState,对应的 mutation 操作 formattedMutation 以及下一个状态 nextState。

  3. 最后更新 prevState = nextState,为下一次提交 mutation 输出日志做准备。

  4. 总结

  • Vuex 从设计上支持了插件,让我们很好地从外部追踪 store 内部的变化,Logger 插件在我们的开发阶段也提供了很好地指引作用。

11. 已完成与待完成

已完成

  • 思维导图

待完成

  • 继续完善 思维导图
  • 添加 流程图

因为该项目都是业余时间做的,笔者能力与时间也有限,很多细节还没有完善。

如果你是大神,或者对 vue 源码有更好的见解,欢迎提交 issue ,大家一起交流学习,一起打造一个像样的 讲解 Vue 全家桶源码架构 的开源项目

12. 总结

以上内容是笔者最近学习 Vue 源码时的收获与所做的笔记,本文内容大多是开源项目 Vue.js 技术揭秘 的内容,只不过是以思维导图的形式来展现,内容有省略,还加入了笔者的一点理解。

笔者之所以采用思维导图的形式来记录所学内容,是因为思维导图更能反映知识体系与结构,更能使人形成完整的知识架构,知识一旦形成一个体系,就会容易理解和不易忘记。

注意:文章的图片可能上传时会经过压缩,可能有点模糊,不过本文用到的 所有 超清图片 都已经放在 github 上,而且还有 pdf 格式、markdown 语法、思维导图 的原文件,自己可以根据 思维导图原文件 导出相应的超清图片。

13. 最后

传承至善

如果你觉得本文章或者项目对你有启发,请给个赞或者 star 吧,点赞是一种美德,谢谢。

参考开源项目:

  1. https://github.com/ustbhuangyi/vue-analysis
  2. https://github.com/HcySunYang/vue-design

JavaScript 数据结构与算法之美 - 时间和空间复杂度

复杂度分析是整个算法学习的精髓,只要掌握了它,数据结构和算法的内容基本上就掌握了一半了。

1. 什么是复杂度分析 ?

  1. 数据结构和算法解决是 “如何让计算机更快时间、更省空间的解决问题”。

  2. 因此需从执行时间和占用空间两个维度来评估数据结构和算法的性能。

  3. 分别用时间复杂度和空间复杂度两个概念来描述性能问题,二者统称为复杂度。

  4. 复杂度描述的是算法执行时间(或占用空间)与数据规模的增长关系。

2. 为什么要进行复杂度分析 ?

  1. 和性能测试相比,复杂度分析有不依赖执行环境、成本低、效率高、易操作、指导性强的特点。

  2. 掌握复杂度分析,将能编写出性能更优的代码,有利于降低系统开发和维护成本。

3. 如何进行复杂度分析 ?

3.1 大 O 表示法

算法的执行时间与每行代码的执行次数成正比,用 T(n) = O(f(n)) 表示,其中 T(n) 表示算法执行总时间,f(n) 表示每行代码执行总次数,而 n 往往表示数据的规模。这就是大 O 时间复杂度表示法。

3.2 时间复杂度

1)定义

算法的时间复杂度,也就是算法的时间量度。

大 O 时间复杂度表示法 实际上并不具体表示代码真正的执行时间,而是表示 代码执行时间随数据规模增长的变化趋势,所以也叫 渐进时间复杂度,简称 时间复杂度(asymptotic time complexity)。

例子1:

function aFun() {
    console.log("Hello, World!");      //  需要执行 1 次
    return 0;       // 需要执行 1 次
}

那么这个方法需要执行 2 次运算。

例子 2:

function bFun(n) {
    for(let i = 0; i < n; i++) {         // 需要执行 (n + 1) 次
        console.log("Hello, World!");      // 需要执行 n 次
    }
    return 0;       // 需要执行 1 次
}

那么这个方法需要执行 ( n + 1 + n + 1 ) = 2n +2 次运算。

例子 3:

 function cal(n) {
   let sum = 0; // 1 次
   let i = 1; // 1 次
   let j = 1; // 1 次
   for (; i <= n; ++i) {  // n 次
     j = 1;  // n 次
     for (; j <= n; ++j) {  // n * n ,也即是  n平方次
       sum = sum +  i * j;  // n * n ,也即是  n平方次
     }
   }
 }

注意,这里是二层 for 循环,所以第二层执行的是 n * n = n2 次,而且这里的循环是 ++i,和例子 2 的是 i++,是不同的,是先加与后加的区别。

那么这个方法需要执行 ( n2 + n2 + n + n + 1 + 1 +1 ) = 2n2 +2n + 3 。

2)特点

以时间复杂度为例,由于 时间复杂度 描述的是算法执行时间与数据规模的 增长变化趋势,所以 常量、低阶、系数 实际上对这种增长趋势不产生决定性影响,所以在做时间复杂度分析时 忽略 这些项。

所以,上面例子1 的时间复杂度为 T(n) = O(1),例子2 的时间复杂度为 T(n) = O(n),例子3 的时间复杂度为 T(n) = O(n2)。

3.3 时间复杂度分析

    1. 只关注循环执行次数最多的一段代码

单段代码看高频:比如循环。

function cal(n) { 
   let sum = 0;
   let i = 1;
   for (; i <= n; ++i) {
     sum = sum + i;
   }
   return sum;
 }

执行次数最多的是 for 循环及里面的代码,执行了 n 次,所以时间复杂度为 O(n)。

    1. 加法法则:总复杂度等于量级最大的那段代码的复杂度

多段代码取最大:比如一段代码中有单循环和多重循环,那么取多重循环的复杂度。

function cal(n) {
   let sum_1 = 0;
   let p = 1;
   for (; p < 100; ++p) {
     sum_1 = sum_1 + p;
   }

   let sum_2 = 0;
   let q = 1;
   for (; q < n; ++q) {
     sum_2 = sum_2 + q;
   }
 
   let sum_3 = 0;
   let i = 1;
   let j = 1;
   for (; i <= n; ++i) {
     j = 1; 
     for (; j <= n; ++j) {
       sum_3 = sum_3 +  i * j;
     }
   }
 
   return sum_1 + sum_2 + sum_3;
 }

上面代码分为三部分,分别求 sum_1、sum_2、sum_3 ,主要看循环部分。

第一部分,求 sum_1 ,明确知道执行了 100 次,而和 n 的规模无关,是个常量的执行时间,不能反映增长变化趋势,所以时间复杂度为 O(1)。

第二和第三部分,求 sum_2 和 sum_3 ,时间复杂度是和 n 的规模有关的,为别为 O(n) 和 O(n2)。

所以,取三段代码的最大量级,上面例子的最终的时间复杂度为 O(n2)。

同理类推,如果有 3 层 for 循环,那么时间复杂度为 O(n3),4 层就是 O(n4)。

所以,总的时间复杂度就等于量级最大的那段代码的时间复杂度

    1. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

嵌套代码求乘积:比如递归、多重循环等。

function cal(n) {
   let ret = 0; 
   let i = 1;
   for (; i < n; ++i) {
     ret = ret + f(i); // 重点为  f(i)
   } 
 } 
 
function f(n) {
  let sum = 0;
  let i = 1;
  for (; i < n; ++i) {
    sum = sum + i;
  } 
  return sum;
 }

方法 cal 循环里面调用 f 方法,而 f 方法里面也有循环。

所以,整个 cal() 函数的时间复杂度就是,T(n) = T1(n) * T2(n) = O(n*n) = O(n2) 。

    1. 多个规模求加法:比如方法有两个参数控制两个循环的次数,那么这时就取二者复杂度相加
function cal(m, n) {
  let sum_1 = 0;
  let i = 1;
  for (; i < m; ++i) {
    sum_1 = sum_1 + i;
  }

  let sum_2 = 0;
  let j = 1;
  for (; j < n; ++j) {
    sum_2 = sum_2 + j;
  }

  return sum_1 + sum_2;
}

以上代码也是求和 ,求 sum_1 的数据规模为 m、求 sum_2 的数据规模为 n,所以时间复杂度为 O(m+n)。

公式:T1(m) + T2(n) = O(f(m) + g(n)) 。

    1. 多个规模求乘法:比如方法有两个参数控制两个循环的次数,那么这时就取二者复杂度相乘
function cal(m, n) {
  let sum_3 = 0;
   let i = 1;
   let j = 1;
   for (; i <= m; ++i) {
     j = 1; 
     for (; j <= n; ++j) {
       sum_3 = sum_3 +  i * j;
     }
   }
}

以上代码也是求和,两层 for 循环 ,求 sum_3 的数据规模为 m 和 n,所以时间复杂度为 O(m*n)。

公式:T1(m) * T2(n) = O(f(m) * g(n)) 。

3.4 常用的时间复杂度分析

    1. 多项式阶:随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增长。

包括 O(1)(常数阶)、O(logn)(对数阶)、O(n)(线性阶)、O(nlogn)(线性对数阶)、O(n2) (平方阶)、O(n3)(立方阶)。

除了 O(logn)、O(nlogn) ,其他的都可从上面的几个例子中看到。

下面举例说明 O(logn)(对数阶)

let i=1;
while (i <= n)  {
   i = i * 2;
}

代码是从 1 开始,每次循环就乘以 2,当大于 n 时,循环结束。

其实就是高中学过的等比数列,i 的取值就是一个等比数列。在数学里面是这样子的:

20 21 22 ... 2k ... 2x = n

所以,我们只要知道 x 值是多少,就知道这行代码执行的次数了,通过 2x = n 求解 x,数学中求解得 x = log2n 。所以上面代码的时间复杂度为 O(log2n)。

实际上,不管是以 2 为底、以 3 为底,还是以 10 为底,我们可以把所有对数阶的时间复杂度都记为 O(logn)。为什么呢?

因为对数之间是可以互相转换的,log3n = log32 * log2n,所以 O(log3n) = O(C * log2n),其中 C=log32 是一个常量。

由于 时间复杂度 描述的是算法执行时间与数据规模的 增长变化趋势,所以 常量、低阶、系数 实际上对这种增长趋势不产生决定性影响,所以在做时间复杂度分析时 忽略 这些项。

因此,在对数阶时间复杂度的表示方法里,我们忽略对数的 “底”,统一表示为 O(logn)

下面举例说明 O(nlogn)(对数阶)

function aFun(n){
  let i = 1;
  while (i <= n)  {
     i = i * 2;
  }
  return i
}

function cal(n) { 
   let sum = 0;
   for (let i = 1; i <= n; ++i) {
     sum = sum + aFun(n);
   }
   return sum;
 }

aFun 的时间复杂度为 O(logn),而 cal 的时间复杂度为 O(n),所以上面代码的时间复杂度为 T(n) = T1(logn) * T2(n) = O(logn*n) = O(nlogn) 。

    1. 非多项式阶:随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法性能极差。

包括 O(2n)(指数阶)、O(n!)(阶乘阶)。

O(2n)(指数阶)例子:

aFunc( n ) {
    if (n <= 1) {
        return 1;
    } else {
        return aFunc(n - 1) + aFunc(n - 2);
    }
}

参考答案:
显然运行次数,T(0) = T(1) = 1,同时 T(n) = T(n - 1) + T(n - 2) + 1,这里的 1 是其中的加法算一次执行。
显然 T(n) = T(n - 1) + T(n - 2) 是一个斐波那契数列,通过归纳证明法可以证明,当 n >= 1 时 T(n) < (5/3)n,同时当 n > 4 时 T(n) >= (3/2)n
所以该方法的时间复杂度可以表示为 O((5/3)n),简化后为 O(2n)。
可见这个方法所需的运行时间是以指数的速度增长的。
如果大家感兴趣,可以试下分别用 1,10,100 的输入大小来测试下算法的运行时间,相信大家会感受到时间复杂度的无穷魅力。

3.5 时间复杂度分类

时间复杂度可以分为:

  • 最好情况时间复杂度(best case time complexity):在最理想的情况下,执行这段代码的时间复杂度。
  • 最坏情况时间复杂度(worst case time complexity):在最糟糕的情况下,执行这段代码的时间复杂度。
  • 平均情况时间复杂度(average case time complexity),用代码在所有情况下执行的次数的加权平均值表示。也叫 加权平均时间复杂度 或者 期望时间复杂度
  • 均摊时间复杂度(amortized time complexity): 在代码执行的所有复杂度情况中绝大部分是低级别的复杂度,个别情况是高级别复杂度且发生具有时序关系时,可以将个别高级别复杂度均摊到低级别复杂度上。基本上均摊结果就等于低级别复杂度。

举例说明:

// n 表示数组 array 的长度
function find(array, n, x) {
  let i = 0;
  let pos = -1;
  for (; i < n; ++i) {
    if (array[i] == x) {
      pos = i; 
      break;
    }
  }
  return pos;
}

find 函数实现的功能是在一个数组中找到值等于 x 的项,并返回索引值,如果没找到就返回 -1 。

最好情况时间复杂度,最坏情况时间复杂度

如果数组中第一个值就等于 x,那么时间复杂度为 O(1),如果数组中不存在变量 x,那我们就需要把整个数组都遍历一遍,时间复杂度就成了 O(n)。所以,不同的情况下,这段代码的时间复杂度是不一样的。

所以上面代码的 最好情况时间复杂度为 O(1),最坏情况时间复杂度为 O(n)。

平均情况时间复杂度

如何分析平均时间复杂度 ?代码在不同情况下复杂度出现量级差别,则用代码所有可能情况下执行次数的加权平均值表示。

要查找的变量 x 在数组中的位置,有 n+1 种情况:在数组的 0~n-1 位置中和不在数组中。我们把每种情况下,查找需要遍历的元素个数累加起来,然后再除以 n+1,就可以得到需要遍历的元素个数的平均值,即:

省略掉系数、低阶、常量,所以,这个公式简化之后,得到的平均时间复杂度就是 O(n)。

我们知道,要查找的变量 x,要么在数组里,要么就不在数组里。这两种情况对应的概率统计起来很麻烦,我们假设在数组中与不在数组中的概率都为 1/2。另外,要查找的数据出现在 0~n-1 这 n 个位置的概率也是一样的,为 1/n。所以,根据概率乘法法则,要查找的数据出现在 0~n-1 中任意位置的概率就是 1/(2n)。

因此,前面的推导过程中存在的最大问题就是,没有将各种情况发生的概率考虑进去。如果我们把每种情况发生的概率也考虑进去,那平均时间复杂度的计算过程就变成了这样:

这个值就是概率论中的 加权平均值,也叫 期望值,所以平均时间复杂度的全称应该叫 加权平均时间复杂度 或者 期望时间复杂度

所以,根据上面结论推导出,得到的 平均时间复杂度 仍然是 O(n)。

均摊时间复杂度

均摊时间复杂度就是一种特殊的平均时间复杂度 (应用场景非常特殊,非常有限,这里不说)。

3.6 时间复杂度总结

常用的时间复杂度所耗费的时间从小到大依次是:

O(1) < O(logn) < (n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)

常见的时间复杂度:

3.7 空间复杂度分析

时间复杂度的全称是 渐进时间复杂度,表示 算法的执行时间与数据规模之间的增长关系

类比一下,空间复杂度全称就是 渐进空间复杂度(asymptotic space complexity),表示 算法的存储空间与数据规模之间的增长关系

定义:算法的空间复杂度通过计算算法所需的存储空间实现,算法的空间复杂度的计算公式记作:S(n) = O(f(n)),其中,n 为问题的规模,f(n) 为语句关于 n 所占存储空间的函数。

function print(n) {
 const newArr = []; // 第 2 行
 newArr.length = n; // 第 3 行
  for (let i = 0; i <n; ++i) {
    newArr[i] = i * i;
  }

  for (let j = n-1; j >= 0; --j) {
    console.log(newArr[i])
  }
}

跟时间复杂度分析一样,我们可以看到,第 2 行代码中,我们申请了一个空间存储变量 newArr ,是个空数组。第 3 行把 newArr 的长度修改为 n 的长度的数组,每项的值为 undefined ,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是 O(n)。

我们常见的空间复杂度就是 O(1)、O(n)、O(n2),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。

4. 如何掌握好复杂度分析方法 ?

复杂度分析关键在于多练,所谓孰能生巧。

平时我们在写代码时,是用 空间换时间 还是 时间换空间,可以根据算法的时间复杂度和空间复杂度来衡量。

5. 最后

如果你觉得本文章或者项目对你有启发,请给个赞或者 star 吧,点赞是一种美德,谢谢。

参考文章:

复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?
(数据结构)十分钟搞定算法时间复杂度

一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构

vue

1.前言

本文内容讲解的内容:一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构

项目地址:https://github.com/biaochenxuying/vue-family-mindmap

文章的图文结合版

Vue-family.md

Vue-family.pdf

2. Vue 全家桶

先来张 Vue 全家桶 总图:

3. Vue

细分如下

源码目录

源码构建,基于 Rollup 

Vue 本质:构造函数

数据驱动

组件化

深入响应式原理

编译

扩展

4. Vue-Router

introduction

路由注册

VueRouter 对象

matcher

路径切换

5. Vuex

introduction

Vuex 初始化

API

插件

6. 已完成与待完成

已完成

  • 思维导图

待完成

  • 继续完善 思维导图
  • 添加 流程图

因为该项目都是业余时间做的,笔者能力与时间也有限,很多细节还没有完善。

如果你是大神,或者对 vue 源码有更好的见解,欢迎提交 issue ,大家一起交流学习,一起打造一个像样的 讲解 Vue 全家桶源码架构 的开源项目

7. 总结

以上内容是笔者最近学习 Vue 源码时的收获与所做的笔记,本文内容大多是开源项目 Vue.js 技术揭秘 的内容,只不过是以思维导图的形式来展现,内容有省略,还加入了笔者的一点理解。

笔者之所以采用思维导图的形式来记录所学内容,是因为思维导图更能反映知识体系与结构,更能使人形成完整的知识架构,知识一旦形成一个体系,就会容易理解和不易忘记。

注意:文章的图片可能上传时会经过压缩,可能有点模糊,不过本文用到的 所有 超清图片 都已经放在 github 上,而且还有 pdf 格式、markdown 语法、思维导图 的原文件,自己可以根据 思维导图原文件 导出相应的超清图片。

8. 最后

传承至善

如果你觉得本文章或者项目对你有启发,请给个赞或者 star 吧,点赞是一种美德,谢谢。

参考开源项目:

  1. https://github.com/ustbhuangyi/vue-analysis
  2. https://github.com/HcySunYang/vue-design

github 授权登录教程与如何设计第三方授权登录的用户表

效果图

需求:在网站上想评论一篇文章,而评论文章是要用户注册与登录的,那么怎么免去这麻烦的步骤呢?答案是通过第三方授权登录。本文讲解的就是 github 授权登录的教程。

效果体验地址: http://biaochenxuying.cn

1. github 第三方授权登录教程

先来看下 github 授权的完整流程图 1:

github 1

或者看下 github 授权的完整流程图 2:

github 2

1.1 申请一个 OAuth App

首先我们必须登录上 github 申请一个 OAuth App,步骤如下:

  1. 登录 github
  2. 点击头像下的 Settings -> Developer settings 右侧 New OAuth App
  3. 填写申请 app 的相关配置,重点配置项有2个
  4. Homepage URL 这是后续需要使用授权的 URL ,你可以理解为就是你的项目根目录地址
  5. Authorization callback URL 授权成功后的回调地址,这个至关重要,这是拿到授权 code 时给你的回调地址。

具体实践如下:

    1. 首先登录你的 GitHub 账号,然后点击进入Settings。

    1. 点击 OAuth Apps , Register a new application 或者 New OAuth App 。

    1. 输入信息。

image.png

    1. 应用信息说明。

流程也可看 GitHub 设置的官方文档-Registering OAuth Apps

1.2 授权登录

github 文档:building-oauth-apps/authorizing-oauth-apps

授权登录的主要 3 个步骤:

笔者这次实践中,项目是采用前后端分离的,所以第 1 步在前端实现,而第 2 步和第 3 步是在后端实现的,因为第 2 个接口里面需要Client_secret 这个参数,而且第 3 步获取的用户信息在后端保存到数据库。

1.3. 代码实现

1.3.1 前端

笔者项目的技术是 react。

// config.js

// ***** 处请填写你申请的 OAuth App 的真实内容
 const config = {
  'oauth_uri': 'https://github.com/login/oauth/authorize',
  'redirect_uri': 'http://biaochenxuying.cn/',
  'client_id': '*****',
  'client_secret': '*******',
};

// 本地开发环境下
if (process.env.NODE_ENV === 'development') {
  config.redirect_uri = "http://localhost:3001/"
  config.client_id = "******"
  config.client_secret = "*****"
}
export default config; 

代码参考 config.js

redirect_uri 回调地址是分环境的,所以我是新建了两个 OAuth App 的,一个用于线上生产环境,一个用于本地开发环境。

一般来说,登录的页面应该是独立的,对应相应的路由 /login , 但是本项目的登录 login 组件是 nav 组件的子组件,nav 是个全局用的组件, 所以回调地址就写了 http://biaochenxuying.cn/。

  • 所以点击跳转是写在 login.js 里面;
  • 授权完拿到 code 后,是写在 nav.js 里面
  • nav.js 拿到 code 值后去请求后端接口,后端接口返回用户信息。
  • 其中后端拿到 code 还要去 github 取 access_token ,再根据 access_token 去取 github 取用户的信息。
// login.js

// html
<Button
    style={{ width: '100%' }}
    onClick={this.handleOAuth} >
      github 授权登录
</Button>

// js
handleOAuth(){
    // 保存授权前的页面链接
    window.localStorage.preventHref = window.location.href
    // window.location.href = 'https://github.com/login/oauth/authorize?client_id=***&redirect_uri=http://biaochenxuying.cn/'
    window.location.href = `${config.oauth_uri}?client_id=${config.client_id}&redirect_uri=${config.redirect_uri}`
}

代码参考 login.js

// nav.js

componentDidMount() {
    // console.log('code :', getQueryStringByName('code'));
    const code = getQueryStringByName('code')
    if (code) {
      this.setState(
        {
          code
        },
        () => {
          if (!this.state.code) {
            return;
          }
          this.getUser(this.state.code);
        },
      );
    }
  }

componentWillReceiveProps(nextProps) {
    const code = getQueryStringByName('code')
    if (code) {
      this.setState(
        {
          code
        },
        () => {
          if (!this.state.code) {
            return;
          }
          this.getUser(this.state.code);
        },
      );
    }
  }
  getUser(code) {
    https
      .post(
        urls.getUser,
        {
          code,
        },
        { withCredentials: true },
      )
      .then(res => {
        // console.log('res :', res.data);
        if (res.status === 200 && res.data.code === 0) {
          this.props.loginSuccess(res.data);
          let userInfo = {
            _id: res.data.data._id,
            name: res.data.data.name,
          };
          window.sessionStorage.userInfo = JSON.stringify(userInfo);
          message.success(res.data.message, 1);
          this.handleLoginCancel();
          // 跳转到之前授权前的页面
          const href = window.localStorage.preventHref
          if(href){
            window.location.href = href 
          }
        } else {
          this.props.loginFailure(res.data.message);
          message.error(res.data.message, 1);
        }
      })
      .catch(err => {
        console.log(err);
      });
  }

参考 nav.js

1.3.2 后端

笔者项目的后端采用的技术是 node.js 和 express。

  • 后端拿到前端传来的 code 后,还要去 github 取 access_token ,再根据 access_token 去取 github 取用户的信息。
  • 然后把要用到的用户信息通过 注册 的方式保存到数据库,然后返回用户信息给前端。
// app.config.js

exports.GITHUB = {
	oauth_uri: 'https://github.com/login/oauth/authorize',
	access_token_url: 'https://github.com/login/oauth/access_token',
	// 获取 github 用户信息 url // eg: https://api.github.com/user?access_token=******&scope=&token_type=bearer
	user_url: 'https://api.github.com/user',

	// 生产环境
    redirect_uri: 'http://biaochenxuying.cn/',
    client_id: '*****',
    client_secret: '*****',

	// // 开发环境
	// redirect_uri: "http://localhost:3001/",
    // client_id: "*****",
	// client_secret: "*****",
};

代码参考 app.config.js

// 路由文件  user.js

const fetch = require('node-fetch');
const CONFIG = require('../app.config.js');
const User = require('../models/user');

// 第三方授权登录的用户信息
exports.getUser = (req, res) => {
  let { code } = req.body;
  if (!code) {
    responseClient(res, 400, 2, 'code 缺失');
    return;
  }
  let path = CONFIG.GITHUB.access_token_url;
  const params = {
    client_id: CONFIG.GITHUB.client_id,
    client_secret: CONFIG.GITHUB.client_secret,
    code: code,
  };
  // console.log(code);
  fetch(path, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json', 
    },
    body: JSON.stringify(params),
  })
    .then(res1 => {
      return res1.text();
    })
    .then(body => {
      const args = body.split('&');
      let arg = args[0].split('=');
      const access_token = arg[1];
      // console.log("body:",body);
      console.log('access_token:', access_token);
      return access_token;
    })
    .then(async token => {
      const url = CONFIG.GITHUB.user_url + '?access_token=' + token;
      console.log('url:', url);
      await fetch(url)
        .then(res2 => {
          console.log('res2 :', res2);
          return res2.json();
        })
        .then(response => {
          console.log('response ', response);
          if (response.id) {
            //验证用户是否已经在数据库中
            User.findOne({ github_id: response.id })
              .then(userInfo => {
                // console.log('userInfo :', userInfo);
                if (userInfo) {
                  //登录成功后设置session
                  req.session.userInfo = userInfo;
                  responseClient(res, 200, 0, '授权登录成功', userInfo);
                } else {
                  let obj = {
                    github_id: response.id,
                    email: response.email,
                    password: response.login,
                    type: 2,
                    avatar: response.avatar_url,
                    name: response.login,
                    location: response.location,
                  };
                  //注册到数据库
                  let user = new User(obj);
                  user.save().then(data => {
                    // console.log('data :', data);
                    req.session.userInfo = data;
                    responseClient(res, 200, 0, '授权登录成功', data);
                  });
                }
              })
              .catch(err => {
                responseClient(res);
                return;
              });
          } else {
            responseClient(res, 400, 1, '授权登录失败', response);
          }
        });
    })
    .catch(e => {
      console.log('e:', e);
    });
};

代码参考 user.js

至于拿到 github 的用户信息后,是注册到 user 表,还是保存到另外一张 oauth 映射表,这个得看自己项目的情况。

从 github 拿到的用户信息如下图:

github-login.png

最终效果:

github-logining.gif

参与文章:

  1. https://www.jianshu.com/p/a9c0b277a3b3

  2. https://blog.csdn.net/zhuming3834/article/details/77649960

2. 如何设计第三方授权登录的用户表

第三方授权登录的时候,第三方的用户信息是存数据库原有的 user 表还是新建一张表呢 ?

答案:这得看具体项目了,做法多种,请看下文。

第三方授权登录之后,第三方用户信息一般都会返回用户唯一的标志 openid 或者 unionid 或者 id,具体是什么得看第三方,比如 github 的是 id

  • 1. 直接通过 注册 的方式保存到数据库

第一种:如果网站 没有 注册功能的,直接通过第三方授权登录,授权成功之后,可以直接把第三的用户信息 注册 保存到自己数据库的 user 表里面。典型的例子就是 微信公众号的授权登录。

第二种:如果网站 注册功能的,也可以通过第三方授权登录,授权成功之后,也可以直接把第三的用户信息 注册 保存到自己数据库的 user 表里面(但是密码是后端自动生成的,用户也不知道,只能用第三方授权登录),这样子的第三方的用户和原生注册的用户信息都在同一张表了,这种情况得看自己项目的具体情况。笔者的博客网站暂时就采用了这种方式。

  • 2. 增加映射表

现实中很多网站都有多种账户登录方式,比如可以用网站的注册 id 登录,还可以用手机号登录,可以用 QQ 登录等等。数据库中都是有映射关系,QQ、手机号等都是映射在网站的注册 id 上。保证不管用什么方式登录,只要去查映射关系,发现是映射在网站注册的哪个 id 上,就让哪个 id 登录成功。

  • 3. 建立一个 oauth 表,一个 id 列,记录对应的用户注册表的 id

建立一个 oauth 表,一个 id 列,记录对应的用户注册表的 id,然后你有多少个第三方登陆功能,你就建立多少列,记录第三方登陆接口返回的 openid;第三方登陆的时候,通过这个表的记录的 openid 获取 id 信息,如果存在通过 id 读取注册表然后用 session 记录相关信息。不存在就转向用户登陆/注册界面要用户输入本站注册的账户进行 openid 绑定或者新注册账户信息进行绑定。

具体代码实践请参考文章:

1. 第三方登录用户信息表设计

2. 浅谈数据库用户表结构设计,第三方登录

4. 最后

笔者的 github 博客地址:https://github.com/biaochenxuying/blog

GitHub 上能挖矿的神仙技巧 - 如何发现优秀开源项目

1. 前言

本文介绍如何在 GitHub 上发现优秀的开源项目,找到你想要的矿。

GitHub 作为全球最大的同性交友网站,也是矿资源非常丰富的矿场。

GitHub 有时比 Google 还有用,如果你不懂如何使用它来挖矿,那你不算一名合格的程序员。

GitHub 是一个宝藏库,可没有藏宝图,GitHub 这个亿计的优秀的开源项目也和你没有关系。

一般人没事的时候刷刷朋友圈、微博、电视剧、知乎,而有些人是没事的时候刷刷 GitHub ,看看最近有哪些流行的项目,久而久之,这差距就越来越大,那么如何发现优秀的开源项目呢 ?

笔者做前端开发这些年,几乎每天都会刷 GitHub,也算是 GitHub 的重度使用者了,其中也掌握了一定的技巧,由此写一下我是如何使用它来挖矿的 !

笔者博客地址:GitHub

2. Follow

关注 GitHub 上活跃的大牛。

GitHub 主页有一个类似微信的朋友圈,所有你关注的人(相当于微信的好友)的动作,比如 create、star、fork 了某个项目都会出现在你的时间线上,这种方式适合我这种比较懒的人,不用主动去找项目,而这种基本是我每天获取信息的一个很重要的方式。

一些大牛 create、star、fork 了某个项目,很大程度是因为该项目做的好,或者对他有用的。

比如:github 上的 actions 功能刚出不是很久,很多人还不会用,然后阮玉峰老师今天就创建了一个 github-actions-demo 的仓库。

再比如:还有过几天就是中秋了,所以很多人抢票回家,所以不少人 star 了 12306 的智能刷票,订票的项目。

比如下图就是我关注的一些大牛在今天点了 Star 的项目。

不知道怎么关注这些人?那么很简单,关注我 biaochenxuying ,以及我 GitHub 上关注的一些大牛,基本就差不多了,因为我关注的很多在 GitHub 上活跃的大牛,平时看到活跃的大牛也会继续关注。

可能很多人不想 Follow 别人,因为不想被别人看到,不想承认别人比自己优秀。

但我想说:承认别人比自己优秀不丢脸

Vue.js 的作者尤雨溪够牛 B 吧,都关注了不少大牛呢,都虚心向别人学习呢,更何况我们呢。

活跃是指:经常在 GitHub 上做开源项目、 Star 别人优秀项目、Fork 别人优秀项目、Flow 别人、或者写博客。

但是你关注太多比你的 level 高太多的大牛用处不是很大的,往往对你现在的帮助不是很大,所以关注顶级大牛的目的应该是更好的知道行业的动态或者方向。

多关注一些 level 高一两级的大牛,比如你是初级前端,那你应该关注多一些中级或者高级的前端, 只比你的 level 高一两级的前端现在关注的内容或者知识往往是你即将要学到或者用到的。

至于为什么只关注活跃的大牛呢,因为自己能从他那里有所收获,如果某个技术大牛确实很厉害,但是对你没什么帮助,关注 TA 有个毛用嘛!

3. Explore Repositories

github 也会推一些你可能感兴趣的仓库给你的,只要你一打开 github.com 网站,就出现了。比如下图是今天推送给我的仓库。

4. Explore

4.1 Trending

Trending:趋势的意思。

在 Trending 页面,你可以看到最近一些热门的开源项目或者开发者,这个页面可以算是很多人主动获取一些开源项目和活跃开发者最好的途径。

首先点击 Explore => Trending。

  • 可以选择看开源项目还是开发者,切换 Repositories 和 Developers 即可。
  • 可以选择「当天热门」、「一周之内热门」和「一月之内热门」来查看。
  • 可以选择语言类来查看,比如你想查看最近热门的 Vue 项目,那么右边就可以选择 Vue 语言。

这个页面推荐大家每隔几天就去看下,主动发掘一些优秀的开源项目。

4.2 Topics

Topics 里面也可以看某个话题或者领域内最优秀的项目。

比如前端领域: Front end

5. Star

因为笔者也做过几个开源项目,所以知道 star 数会给作者动力的,越多人点 star ,维护这个开源项目的驱动力就越足。

笔者经常看到不错的、有趣的、有用的,或者现在没用,以后会用到的优秀开源项目,都会 star 一下,当是给这个开源的作者一份鼓励,希望 TA 更好的维护这个开源项目,以后用到的时候可以在 star 过的项目里面找出来。

笔者不想 fork 别人的项目,除非想深入研究该项目的源码才会 fork。

别人是把 fork 当收藏,而我把是 star 当收藏,把 fork 当研究

所以你也可以在某些大牛的 star 列表里面找优秀开源项目,比如笔者就 star 了不少优秀的开源项目,如下图。

如果你在笔者的 star 列表 里面找的话,你应该会有惊喜,你会发现很多有趣实用的项目的。

因为笔者 star 过前端学习、教程、免费电子书、工具、资源、面试、Git 的奇技淫巧、有趣实用的项目等等。

比如:

6. Search

除了平时主动发现优秀开源项目之外,主动搜索又是非常重要的技能,很多百度或者 google 不到的东西,在 github 上都能找到。

输入搜索关键字,可以选择排序的方式、语言、仓库。

7. 总结

GitHub 上优秀开源项目真的是一大堆,授人以鱼不如授人以渔,请大家自行主动发掘自己需要的开源项目吧,不管是应用在实际项目上,还是对源码的学习,都是提升自己工作效率与技能的很重要的一个渠道,总有一天,你会突然意识到,原来不知不觉你已经走了这么远!

笔者博客地址:GitHub

觉得不错,不妨随手转发、点赞,都是对我这个良心笔者莫大的鼓励!

参考文章:从 0 开始学习 GitHub 系列之「如何发现优秀的开源项目?」

基于 vue+mint-ui 的 mobile-h5 的项目说明

Vue作为前端三大框架之一,其已经悄然成为主流,学会用vue相关技术来开发项目会相当轻松。

对于还没学习或者还没用过vue的初学者,基础知识这里不作详解,推荐先去相关官网,学习一下vue相关的基础知识。

a. vue.js 官网 参考:https://cn.vuejs.org/

b. Vue Router  官网 参考:https://router.vuejs.org/zh/

c. Vuex  官网 参考:https://vuex.vuejs.org/zh/

d. ECMAScript 6 入门 参考:http://es6.ruanyifeng.com/  

或者 es6 精简篇 https://www.jianshu.com/p/287e0bb867ae

e. webpack  官网 参考:https://www.webpackjs.com/

f. less  官网 参考:https://less.bootcss.com/

g. mint-ui  官网   参考: http://mint-ui.github.io/#!/zh-cn

一. 搭建vue的相关环境与脚手架的说明

首先,要开发vue相关的项目,要会搭建vue的相关环境,要搭建的目录如下:

1.安装node.js和npm

2. webpack

3. vue-cli脚手架构建工具 

具体这里不作详解,站在前人的肩膀上学习即可
安装node.js和npm
webpack和vue-cli脚手架构建工具 

二.  vue-cli脚手架创建的代码详解

利用脚手架(vue-cli)构建一个vue项目,接下来学习分析下代码。

具体这里不作详解,站在前人的肩膀上学习即可
教程参考: https://www.jianshu.com/p/2b661d01eaf8

三. 本项目 vue+mint-ui 的h5项目说明及详解

1. 运行项目

因为项目配置和需要的模块都已经配好了的,所以运行只需要:

1.在svn上把 mobile-h5 项目代码下载下来。
2.直接进入mobile-h5目录中,即是和 package.json 的文件同级目录,或者直接用vsCode编辑器打开 mobile-h5 项目,在命令终端那里。
3. 安装依赖,执行命令:

npm  install 

或者简写:

npm i

没有报错时,安装结果如图:

安装报错时,会出现 error 的,或者直接中止了,window系统一般是因为npm 的环境没有配置好。
4. 安装好模块后,再执行如下命令来运行

npm  run dev

没有报错时,结果会如下图:
npm  run dev.png
5. 直接打开本地链接 http://localhost:8080 就可访问了,如下图:
本地运行效果

2. 项目目录说明

   都写在 README.md 里面了,具体的也可查看 README.md 的内容。

目录结构

├── mobile-h5 # 总项目目录

├── build # webpack 配置文件目录

├── config # webpack 配置文件引用的目录

├── kpi # webpack 打包正式生成的目录

├── src # 主开发文件的目录

│ ├── assets # 图片资源

│ ├── components # 组件模块

| │ ├── app # 组件模块

| | | ├── add # 本项目指标新增的组件的模块

| | | ├── common # 组件通用的模块

| | | ├── index # 本项目首页组件通用的模块

| | | ├── index # 本项目套餐组件通用的模块

│ ├── filters # 过滤器模块

│ ├── filters # 过滤器模块

│ ├── less # less 的公共样式模块

│ ├── libs # 封装的公共 js 文件模块

│ ├── mixins # mixins 文件模块

│ ├── router # 路由模块

│ ├── store # vuex 的 store 模块

│ ├── views # 主开发的 vue 模块

│ ├──

├── static # webpack 打包依赖的目录

├── index.html # 主页面入口,也是生成打包生产环境代码要依赖的文件

3. 修改webpack的配置,使其符合本项目的要示

a. 因为本地开发时,请求接口会跨域的问题,所以要用代理。

具体说明 参考 https://my.oschina.net/lixiaoyan/blog/1797724

提示:要在手机上开发测试,npm run dev 运行的localhost是不行的,要配置nginx来做代理服务才行。

image.png

b. 把打包的目录修改成生产环境需要的目录 kpi。

image

image

c. 添加别名,在其他地方引入文件时,可以省略部分路径的书写

image

4. 开发目录规范说明

a. 开发要根据 业务模块 来划分,进而进行 文件模块 的划分。

比如:

组件components 里面 

components/common是全局公共的组件,

components/app 是mobile-h5的开发组件

components/app/add mobile-h5的指标新增的组件

components/app/index 是mobile-h5的首页的组件

components/app/setMeal 是mobile-h5的套餐的组件

mixins 里面

mixins/add 是mobile-h5的指标新增的 mixins

mixins/common 是mobile-h5的公共的 mixins

modules 里面

modules/add 是mobile-h5的指标新增的状态数据保存

modules/setMeal 是mobile-h5的套餐的状态数据保存

image

b. 开发链接的书写。

domain.js 是域名的配置, 只要统一配置一项即可,方便。

image.png

urls.js 是请求的链接 

比如: 添加收藏  https://baidu.com:8443/emm/favorite/save

因为引入了 domain.js 了

所以我们只需要在urls.js里面写:  save(即别名):  'emm/favorite/save' 即可

image

c. 引入mock.js ,用来模拟请求接口数据,当后端接口还没开发出来时,就大有用场了。

image

用法如下:

只要打开 总开关,再打开你要用mock数据的 具体接口的开关,接口就不会请求后台的接口了,只用mock返回的数据。

image

d. css样式书写

用法:

image

比如上图的,全省的宽 280px ,高 58px , 正常开发下,程序的css上需要写 

{

width: 280px;

height: 58px;

}

但是我们只需要:

{

width: 280 / 100 rem;

height: 58 / 100 rem;

}

也即是:

{

width: 2.8rem;

height: 0.58rem;

}

换算公式就是具体: 像素/100 = rem, 还能指定7.5rem 宽就是屏幕的总宽

而且无论手机大小是多少,因为设计了 config_width = 750 ,所以满宽都是 7.5rem。

字体大小等也同理。

注意: 因为 phone的分辨率高,所以 0.01rem 在 iphone真机上会不显示,在 border设置的时候犹为明显,至少要0.02rem 才行。

  1. 是因为引入了下面这个文件,要了解具体的内容,请看 rem_config.js。

image

  1. 还有一个方法就是非常简单的,在 js 里面加入这句代码即可,写css时,也可像上面一样写,效果和上面介绍的一样。
 // 屏幕适配(windowWidth/设计稿宽*100) ——*100为了方便计算。即font-size值是手机deviceWidth与设计稿比值的100倍
    document.getElementsByTagName('html')[0].style.fontSize=window.screen.width/7.5+'px';
    // 如上:通过查询屏幕宽度,动态的设置html的font-size值,设计稿大多以750*1334 设置的,通过上述换算后,在设计图上一张150*150的图,在css中对应的rem值则为:1.5*1.5rem
e. 其他

libs里的文件内容都是 内有乾坤的,开发人员都有必要了解一下,这里就不多介绍了。

image

5. 推荐开发方式

vue和react一样,可以组件化,所以推荐组件化开发的方式。

组件系统是 Vue 的一个重要概念,允许我们使用小型、独立和通常可复用的组件构建大型应用。

参考vue官网,组件化 :https://cn.vuejs.org/v2/guide/components.html

项目举例:比如一个h5的首页,可以分为标题组件,业务实时组件,关键指标组件,tab切换组件。这几个组件的几乎没有联系,可以相互独立。

image

image

当然你也可以再划分成小组件,相同的模块抽成公共的小组件,这样子达到代码重用的目的更好。比如首页里面大模块的 title 。

image

6. 项目用vuex 的讲解

本项目vuex的用法 和官方的有点不一样,所以这里有必要做一下讲解。

1. 保存值 mutations

1. 要用store来存储值,都要先定义

比如:新增里面要存储关于 日 月 类型的切换:tabActiveType: '',
要先在store模块的add里面的initPageState 先定义,不然找不到,为取值会为undefined。

add模块的 initPageState.png

2. 定义type,至于为什么要大写?因为当作常量来用,当然不大写也可以,不过本项目要统一成大写。

type.png

3. mutatuons 写一个方法来保存值

mutatuons.png

4. 调用保存值,这里的 SAVE_TAB_ACTIVE_TYPE 要与定义在第2中 type 里面的对应,而且 对象里的 key 要与initPageState里面定义的对应,如 tabActiveType。

调用保存值.png

5. 当然怎么取值?只需要在组件的computed勾子像下面这样子写即可。

image.png

按照这5步,一个关于vuex的取值与偘保存值就ok了!

2. 那怎么异步action呢?

1. 定义type,和上面的第二步一样
2. 在相应模块的 actions 里定义一个方法,如下图:

actions.png
这样子可以获取异步请求数据,并保存在store里面了。

3. 当然调用?只需要在组件的方法或者勾子里面,像下面这样子调用即可。

调用.png

四、开发规范

1. 代码规范

结合团队日常业务需求以及团队在日常开发过程中总结提炼出的经验而制定。

旨在增强团队开发协作、提高代码质量和打造开发基石的编码规范,

以下规范是团队基本约定的内容,必须严格遵循。

规范链接: 

参考 腾讯和京东的前端代码规范 

腾讯的 http://tgideas.qq.com/doc/index.html

京东的 https://guide.aotu.io/index.html

2. 统一编辑器--vsCode 全称 Visual Studio Code

Visual Studio Code 是微软推出的跨平台编辑器。它采用经典的VS的UI布局,功能强大,扩展性很强。

这个编辑器流畅简洁,自从用了这个编辑器,其他的都不想了,只想静静地敲代码了。

Microsoft Visual Studio Code 中文手册  https://jeasonstudio.gitbooks.io/vscode-cn-doc/content/

统一格式化代码插件-- Vetur  一个关于vue代码格式化 

image

vue代码统一格式化可以减少代码风格差异

而且这个插件配置一下更好用:
"vetur.format.defaultFormatter.js": "vscode-typescript",

image

不然有些地方会出现换行,代码难懂了。像下面这样:

image

3. 推荐皮肤插件--Boxxy Theme Kit 

其中的代码颜色:Boxy Ocean 是很好看的代码风格

用上之后不满意 ?哼,那本汪就不高兴了,本汪不负责!

image

5. 总 结

最后:

团队开发要规范!!!
还想学到完整的牛逼技术?去看相关技术的官网!!!

复杂表格设计数据格式

1. 表头设计

原理:
和多叉树的原理类似,参考了它的展示形式。
多叉树.png

表头说明:
如果没有孩子节点就只返回如下一个字段:

  • name :名字

如果有孩子节点,就把数据加在children里面,层层嵌套,返回字段如下:

  • name :名字
  • children : 孩子节点

数据结构格式,参考如下代码:

headerData:[
            {
                name: '地区',
            },
            {
                name: '总数据',
                children: [
                    {
                        name: '数据1',
                        children: [
                            {
                                name: '数据11',
                                children: [
	                                {
	                                    name: '数据111',
	                                },
	                                {
	                                    name: '数据112',
	                                }
                                ]
                            },
                            {
                                name: '数据12',
                                children: [
	                                {
	                                    name: '数据121',
	                                },
	                                {
	                                    name: '数据122',
	                                }
                                ]
                            },
                            {
                                name: '数据13',
                                children: [
	                                {
	                                    name: '数据131',
	                                },
	                                {
	                                    name: '数据132',
	                                }
                                ]
                            },
                            {
                                name: '数据14',
                            },

                        ]
                    }
                ]
            }
        ];

表头的宽高方面,前端计算,后端不用管,按照如下格式返回数据即可。

#2. 表格数据格式
每一项按照表头展示的顺序返回,通过数组的形式
返回一个参数:

  • bodyData:总数据

数据结构格式参考代码如下:

bodyData:[
        ["地区最先","数据111","数据112","数据121","数据122","数据131","数据132","数据14"],
        ["地区","数据111","数据112","数据121","数据122","数据131","数据132","数据14"],
        ["地区","数据111","数据112","数据121","数据122","数据131","数据132","数据14"],
        ["地区","数据111","数据112","数据121","数据122","数据131","数据132","数据14"],
        ["地区","数据111","数据112","数据121","数据122","数据131","数据132","数据14"],
        ["地区","数据111","数据112","数据121","数据122","数据131","数据132","数据14"],
        ["地区","数据111","数据112","数据121","数据122","数据131","数据132","数据14"],
        ["地区","数据111","数据112","数据121","数据122","数据131","数据132","数据14"], 
        ["地区","数据111","数据112","数据121","数据122","数据131","数据132","数据14"], 
        ["地区最后","数据111","数据112","数据121","数据122","数据131","数据132","数据14"], 
    ]

#3. 效果
如上表头与表格数据代码生成的效果如图:
效果.png

4. 代码

语法高亮用到 codemirror 插件

/**
 * 递归遍历 格式化数组
 * @param { Array } paramArr 目标数组
 * @param { Number } level 层级
 */
export function formatArray(paramArr, level) {
  let levelFirst = Number(level)
  const arr = []
  let childArr = []
  for (let i = 0; i < paramArr.length; i++) {
      let obj = {}
      for (let j in paramArr[i]) {
          if (j != 'children') {
              obj[j] = paramArr[i][j]
          }
          obj['level'] = levelFirst
          obj['width'] = getLeafCountTree(paramArr[i])
          if (!paramArr[i].children) {
              obj['childrenNumber'] = 0
              // LeafNode: 叶子节点就是树中最底段的节点
              // obj['isLeafNode'] = true
          } else {
              // obj['isLeafNode'] = false
              obj['childrenNumber'] = paramArr[i].children.length
          }
      }
      arr.push(obj)
      if (paramArr[i].children) {
          let lev = Number(levelFirst) + 1
          childArr = childArr.concat(formatArray(paramArr[i].children, lev));
      }
  }
  let endArr = arr.concat(childArr)
  return endArr
}
/**
 * 获取 节点的所有叶子节点个数
 * @param {Object} json Object对象
 */
export function getLeafCountTree(json) {
  if(!json.children){
      return 1;
  }else{
      var leafCount = 0;
      for(var i = 0 ; i < json.children.length ; i++){
          leafCount = leafCount + getLeafCountTree(json.children[i]);
      }
      return leafCount;
  }
}

// json对对象字符串的格式化,美化
export function  jsonFromat (text_value){
    if(text_value == ""){
       alert("不能为空");  
       return false;
    } else {
          var json=eval('(' + text_value + ')');
          text_value=JSON.stringify(json);
          var res="";
          for(var i=0,j=0,k=0,ii,ele;i<text_value.length;i++)
          {//k:缩进,j:""个数
              ele=text_value.charAt(i);
              if(j%2==0&&ele=="}")
              {
                  k--;                
                  for(ii=0;ii<k;ii++) ele="    "+ele;
                  ele="\n"+ele;
              }
              else if(j%2==0&&ele=="{")
              {
                  ele+="\n";
                  k++;     
                  for(ii=0;ii<k;ii++) ele+="    ";
              }
              else if(j%2==0&&ele==",")
              {
                  ele+="\n";
                  for(ii=0;ii<k;ii++) ele+="    ";
              }
              else if(ele=="\"") j++;
              res+=ele;        
          }
          return res
    }
  }

<template>
    <div class="pages-tables " id="pages-tables">
        <div class="textarea">
            <h1 class="title">复杂表头 json 数据格式验证:</h1>
            <p class="message">表头展示效果如下:</p>
            <div class="rolling-table auto-table" ref="tableBox" :style="{height: maxHeight + 'px'}">
                <table class="table" id="table" cellpadding="0" cellspacing="0" ref="rollingTable">
                    <thead ref="thead">
                        <tr v-for="(x,i) in headerList" :key="i">
                            <th class="rows " :class="{'cross': index == 0 && i == 0}" v-for="(l,index) in x" :key="index" :colspan="l.width" :rowspan="l.height">{{l.name}}</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr v-for="(b,i) in bodyList" :key="i + 'a'">
                            <template v-for="(x, index) in b">
                                <td :class="{'cols':  index == 0 }" :key="index + 'b'">
                                    {{ x | valueFromt }}
                                </td>
                            </template>
                        </tr>
                        <tr></tr>
                    </tbody>
                </table>
            </div>
            <p class="message">提示:输入 json 覆盖原来的即可,且有验证 json 格式是否正确的功能</p>
            <div class="control">
                <!-- <div class="content fl">
                    <p>select a theme:
                        <select @change="selectTheme" v-model="selected">
                            <option>default</option>
                            <option>night</option>
                            <option>monokai</option>
                            <option>neat</option>
                            <option>elegant</option>
                            <option>cobalt</option>
                            <option>eclipse</option>
                            <option>rubyblue</option>
                            <option>lesser-dark</option>
                            <option>xq-dark</option>
                        </select>
                    </p>
                </div>
                <div class="content fl ml20">
                    <p>select the editor language:
                        <select @change="selectMode" v-model="mode">
                            <option>javascript</option>
                            <option>php</option>
                            <option>python</option>
                            <option>vue</option>
                            <option>xml</option>
                            <option>sql</option>
                            <option>http</option>
                            <option>css</option>
                            <option>sass</option>
                            <option>jsx</option>
                            <option>django</option>
                        </select>
                    </p>
                </div>
                <div class="content fl ml20">
                    <p>select keyMap:
                        <select @change="selectKeyMap" v-model="keyMap">
                            <option>default</option>
                            <option>emacs</option>
                            <option>sublime</option>
                            <option>vim</option>
                        </select>
                    </p>
                </div> -->
                <div class="fl ml20 mt20 mb10 submit">
                    <mt-button type="primary"  size="small" @click.native="setInputValue">提交</mt-button>
                </div>
                <div class="clearfix"></div>
            </div>
            <textarea id="code" name="code">
                [
                    {
                        name: '地区',
                    },
                    {
                        name: '总数据',
                        children: [
                            {
                                name: '数据1',
                                children: [
                                    {
                                        name: '数据11',
                                        children: [{
                                            name: '数据111',
                                        },
                                        {
                                            name: '数据112',
                                        }
                                        ]
                                    },
                                    {
                                        name: '数据12',
                                        children: [{
                                            name: '数据121',
                                        },
                                        {
                                            name: '数据122',
                                        }
                                        ]
                                    },
                                    {
                                        name: '数据13',
                                        children: [{
                                            name: '数据131',
                                        },
                                        {
                                            name: '数据132',
                                        }
                                        ]
                                    },
                                    {
                                        name: '数据14',
                                    },
                                    {
                                        name: '数据15',
                                    },
                                    {
                                        name: '数据16数据16数据16数据16',
                                    },
                                    {
                                        name: '数据17',
                                    },
                                ]
                            }
                        ]
                    }
                ];
            </textarea>
        </div>

    </div>
</template>
<script>

// 说明这个 demo 是给 pc 端用的,单位要为 px
import { formatArray, getLeafCountTree, jsonFromat } from "libs/common/common";
import { Button, MessageBox } from 'mint-ui';
import * as CodeMirror from 'codemirror/lib/codemirror'
// 根据设置的主题,引入相应的主题包,主题包存储在theme下,使用其他主题包时设置option中theme为对应主题
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/monokai.css'
import 'codemirror/theme/neat.css'
import 'codemirror/theme/elegant.css'
import 'codemirror/theme/night.css'
import 'codemirror/theme/cobalt.css'
import 'codemirror/theme/eclipse.css'
import 'codemirror/theme/rubyblue.css'
import 'codemirror/theme/xq-dark.css'
// styleActiveLine: 设置光标所在行高亮true/false,需引入工具包:
import 'codemirror/addon/selection/active-line'
// 根据设置的编辑器语言,引入相应工具包,以下为常用语言包
import 'codemirror/mode/javascript/javascript'
import 'codemirror/mode/go/go'
import 'codemirror/mode/php/php'
import 'codemirror/mode/python/python'
import 'codemirror/mode/http/http'
import 'codemirror/mode/sql/sql'
import 'codemirror/mode/vue/vue'
import 'codemirror/mode/xml/xml'
import 'codemirror/mode/css/css'
import 'codemirror/mode/sass/sass'
import 'codemirror/mode/jsx/jsx'
import 'codemirror/mode/django/django'
// keyMap:快捷键,default使用默认快捷键,除此之外包括emacs,sublime,vim快捷键,使用需引入工具
import 'codemirror/keymap/sublime.js'
import 'codemirror/keymap/emacs.js'
import 'codemirror/keymap/vim.js'
// extraKeys 快捷键,例如 {“Ctrl-Q”: “autocomplete”}:自动补全使用需要引入工具
import 'codemirror/addon/hint/show-hint'
import 'codemirror/addon/hint/javascript-hint'
import 'codemirror/addon/hint/sql-hint'
import 'codemirror/addon/hint/html-hint'
import 'codemirror/addon/hint/xml-hint'
import 'codemirror/addon/hint/anyword-hint'
import 'codemirror/addon/hint/css-hint'
import 'codemirror/addon/hint/show-hint'

export default {
    data() {
        return {
            mapArray: [],
            keyMap: 'default',
            mode: 'javascript',
            editor: '',
            selected: 'monokai',
            header: '',
            maxHeight: '100%',
            theadHeight: '100%',
            offsetHeight: 0,
            scroll: {
                scroller: null
            },
            headerList: [],
            bodyList: [],

        }
    },
    filters: {
        valueFromt: function (value) {
            let realValue = ''
            if (!value) return ''
            value = value.toString()
            if (value.length > 20) {
                realValue = value.slice(0, 15) + '...'
            } else {
                realValue = value
            }
            return realValue
        },
    },
    methods: {
        selectKeyMap(){
            this.editor.addKeyMap(this.keyMap)  
        },
        selectMode(){
            this.editor.setOption("mode",this.mode)   
        },
        selectTheme() {
            this.editor.setOption("theme", this.selected);
        },
        setInputValue() {
            this.header = this.editor.getValue();
            if(this.header){
                this.change()
            }
        },
        change() {
            try {
                const newData = formatArray(eval(this.header), 0)
                let maxLevel = newData[newData.length - 1].level
                this.setHeight(newData, maxLevel + 1)
                this.arayLayered(newData, maxLevel)
                this.headerList = this.arayLayered(newData, maxLevel)
            } catch (e) {
                console.log('e:', e)
                MessageBox('提示', '请检查 json 格式是否正确!!!');
            }
        },
        setHeight(arr, maxLevel) {
            // console.log("setHeight maxLevel", maxLevel)
            for (let i = maxLevel; i >= 0; i--) {
                for (let j = 0; j < arr.length; j++) {
                    // 设置高
                    if (arr[j].childrenNumber) {
                        arr[j].height = 1
                    } else {
                        arr[j].height = maxLevel - arr[j].level
                    }
                }
            }
            return arr
        },
        arayLayered(arr, maxLevel) {
            let returnArr = []
            for (let i = 0; i <= maxLevel; i++) {
                let arrLevel = []
                for (let j = 0; j < arr.length; j++) {
                    if (arr[j].level == i) {
                        arrLevel.push(arr[j])
                    }
                }
                returnArr[i] = arrLevel
            }
            return returnArr
        }
    },
    mounted() {
        let bodyListA = [
            ["地区最先", "数据111", "数据112", "数据121", "数据122", "数据131", "数据132", "数据14"],
            ["地区", "数据111", "数据112", "数据121", "数据122", "数据131", "数据132", "数据14"],
            ["地区", "数据111", "数据112", "数据121", "数据122", "数据131", "数据132", "数据14"],
            ["地区", "数据111", "数据112", "数据121", "数据122", "数据131", "数据132", "数据14"],
            ["地区", "数据111", "数据112", "数据121", "数据122", "数据131", "数据132", "数据14"],
            ["地区", "数据111", "数据112", "数据121", "数据122", "数据131", "数据132", "数据14"],
            ["地区", "数据111", "数据112", "数据121", "数据122", "数据131", "数据132", "数据14"],
            ["地区", "数据111", "数据112", "数据121", "数据122", "数据131", "数据132", "数据14"],
            ["地区", "数据111", "数据112", "数据121", "数据122", "数据131", "数据132", "数据14"],
            ["地区最后", "数据111", "数据112", "数据121", "数据122", "数据131", "数据132", "数据14"],
        ]

        const data = [
            {
                name: '地区',
            },
            {
                name: '总数据',
                children: [
                    {
                        name: '数据1',
                        children: [
                            {
                                name: '数据11',
                                children: [{
                                    name: '数据111',
                                },
                                {
                                    name: '数据112',
                                }
                                ]
                            },
                            {
                                name: '数据12',
                                children: [{
                                    name: '数据121',
                                },
                                {
                                    name: '数据122',
                                }
                                ]
                            },
                            {
                                name: '数据13',
                                children: [{
                                    name: '数据131',
                                },
                                {
                                    name: '数据132',
                                }
                                ]
                            },
                            {
                                name: '数据14',
                            },
                            {
                                name: '数据15',
                            },
                            {
                                name: '数据16数据16数据16数据16',
                            },
                            {
                                name: '数据17',
                            },
                        ]
                    }
                ]
            }
        ];
        this.header = jsonFromat(JSON.stringify(data))
        const newData = formatArray(data, 0)
        let maxLevel = newData[newData.length - 1].level
        this.setHeight(newData, maxLevel + 1)
        this.arayLayered(newData, maxLevel)
        this.headerList = this.arayLayered(newData, maxLevel)

        this.editor = CodeMirror.fromTextArea(document.getElementById("code"), {
            // value : data,  // 文本域默认显示的文本
            lineNumbers: true, /* 定义是否显示行号 */
            mode: "javascript",  /* 定义语法的类型,如果是html则为:text/html */
            theme: "monokai", /* 定义主题 */
            smartIndent: true,  // 是否智能缩进
            styleActiveLine: true,
            keymap:"defaule"

        });
        // this.editor.on("changes",() =>{
        //     //编译器内容更改事件
        //     this.setInputValue();
        // });
        this.editor.setSize(1200,500)
    }
}

</script>
<style lang="less" >
.CodeMirror {
    border: 1px solid black;
    line-height: 16px;
    font-size: 16px;
    text-align: left;
}
</style>
<style lang="less" scoped>
.submit{
    margin-left: 580px;
    margin-top: 20px;
}
textarea{
	width: 1300px;
	height: 1300px;
}
.content{
    font-size: 16px;
}
.textarea{
    text-align: center;
    font-size: 20px;
    .title{
        margin-top: 20px;
        font-size: 30px;
        color: #333;
    }
    textarea{
        border: 1px solid #eee;
        font-size: 16px;
        resize: both;
        width: 800px;
        min-height: 900px;
    }
}
.message{
    color: red;
    font-size: 16px;
}
.waterMask {
    position: absolute;
    width: 100%;
    height: 100%;
    z-index: 4;
    pointer-events: none;
}
.pages-tables {
  -webkit-overflow-scrolling: touch; // ios滑动顺畅
  position: relative;
  margin-left: 5%;
  padding-bottom: 160px;
  margin: 0 auto;
  width: 1200px;
}
.rolling-table {
    height: 100%;
    font-size: 0.28rem;
    color: #86939a;
    background-color: #fff;
    width: 100%;
    -webkit-overflow-scrolling: touch;
    position: relative;
    top: 0;
    // overflow: hidden;
  }
.rows {
    position: relative;
    z-index: 3;
}
.cross {
    position: relative;
    z-index: 5;
}
table td {
  border: 0px solid #000;
  font-size: 25px;
  background: #fff;
}
::-webkit-scrollbar {
    display: none;
}
.table {
  border-collapse: collapse; //去掉重复的border
  color: #86939e;
  font-size: 25px;
  border: 0px solid #000;
  min-height: 100%;
  text-align: center;
  td {
    border-bottom: 1px solid #eee;
    height: 30px;
    line-height: 30px;
    padding: 0 0.2rem;
    // white-space: nowrap;
    white-space: inherit;
    max-width: 500px;
    min-width: 50px;
    // overflow:hidden; 
    // text-overflow:ellipsis;
    // -webkit-line-clamp:2; 
  }
  th {
    color: #43484d;
    white-space: pre-wrap;
    height: 36px;
    line-height: 36px;
    padding: 5px 6px;
    background-color: #f3f4f6;
    font-weight: normal;
    padding-bottom: 0;
    padding-top: 0;
    max-width: 200px;
    border: 1px solid red;
    &:last-child{
        // border-right: 0rem solid #e4e8f5;
    }
  }
}
tr{
    position: relative;
    background-color: #fff;
    &:nth-of-type(odd){
        td{
            background-color: #ebf9fc;
        }
    }
}
</style>

5. 效果链接:

效果链接如下:

复杂表格设计数据格式

动态效果:
动态效果.gif

JavaScript 数据结构与算法之美 - 冒泡排序、插入排序、选择排序

JavaScript 数据结构与算法之美

1. 前言

算法为王。

想学好前端,先练好内功,只有内功深厚者,前端之路才会走得更远

笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。

之所以把冒泡排序、选择排序、插入排序放在一起比较,是因为它们的平均时间复杂度都为 O(n2)。

请大家带着问题:为什么插入排序比冒泡排序更受欢迎 ?来阅读下文。

2. 如何分析一个排序算法

复杂度分析是整个算法学习的精髓。

  • 时间复杂度: 一个算法执行所耗费的时间。
  • 空间复杂度: 运行完一个程序所需内存的大小。

时间和空间复杂度的详解,请看 JavaScript 数据结构与算法之美 - 时间和空间复杂度

学习排序算法,我们除了学习它的算法原理、代码实现之外,更重要的是要学会如何评价、分析一个排序算法。

分析一个排序算法,要从 执行效率内存消耗稳定性 三方面入手。

2.1 执行效率

1. 最好情况、最坏情况、平均情况时间复杂度

我们在分析排序算法的时间复杂度时,要分别给出最好情况、最坏情况、平均情况下的时间复杂度。
除此之外,你还要说出最好、最坏时间复杂度对应的要排序的原始数据是什么样的。

2. 时间复杂度的系数、常数 、低阶

我们知道,时间复杂度反应的是数据规模 n 很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。

但是实际的软件开发中,我们排序的可能是 10 个、100 个、1000 个这样规模很小的数据,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。

3. 比较次数和交换(或移动)次数

这一节和下一节讲的都是基于比较的排序算法。基于比较的排序算法的执行过程,会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。

所以,如果我们在分析排序算法的执行效率的时候,应该把比较次数和交换(或移动)次数也考虑进去。

2.2 内存消耗

也就是看空间复杂度。

还需要知道如下术语:

  • 内排序:所有排序操作都在内存中完成;
  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
  • 原地排序:原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。
    其中,冒泡排序就是原地排序算法。

2.3 稳定性

  • 稳定:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变
    比如: a 原本在 b 前面,而 a = b,排序之后,a 仍然在 b 的前面;
  • 不稳定:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序改变
    比如:a 原本在 b 的前面,而 a = b,排序之后, a 在 b 的后面;

3. 冒泡排序

冒泡

**

  • 冒泡排序只会操作相邻的两个数据。
  • 每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。
  • 一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。

特点

  • 优点:排序算法的基础,简单实用易于理解。
  • 缺点:比较次数多,效率较低。

实现

// 冒泡排序(未优化)
const bubbleSort = arr => {
	console.time('改进前冒泡排序耗时');
	const length = arr.length;
	if (length <= 1) return;
	// i < length - 1 是因为外层只需要 length-1 次就排好了,第 length 次比较是多余的。
	for (let i = 0; i < length - 1; i++) {
		// j < length - i - 1 是因为内层的 length-i-1 到 length-1 的位置已经排好了,不需要再比较一次。
		for (let j = 0; j < length - i - 1; j++) {
			if (arr[j] > arr[j + 1]) {
				const temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
	console.log('改进前 arr :', arr);
	console.timeEnd('改进前冒泡排序耗时');
};

优化:当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。

// 冒泡排序(已优化)
const bubbleSort2 = arr => {
	console.time('改进后冒泡排序耗时');
	const length = arr.length;
	if (length <= 1) return;
	// i < length - 1 是因为外层只需要 length-1 次就排好了,第 length 次比较是多余的。
	for (let i = 0; i < length - 1; i++) {
		let hasChange = false; // 提前退出冒泡循环的标志位
		// j < length - i - 1 是因为内层的 length-i-1 到 length-1 的位置已经排好了,不需要再比较一次。
		for (let j = 0; j < length - i - 1; j++) {
			if (arr[j] > arr[j + 1]) {
				const temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
				hasChange = true; // 表示有数据交换
			}
		}

		if (!hasChange) break; // 如果 false 说明所有元素已经到位,没有数据交换,提前退出
	}
	console.log('改进后 arr :', arr);
	console.timeEnd('改进后冒泡排序耗时');
};

测试

// 测试
const arr = [7, 8, 4, 5, 6, 3, 2, 1];
bubbleSort(arr);
// 改进前 arr : [1, 2, 3, 4, 5, 6, 7, 8]
// 改进前冒泡排序耗时: 0.43798828125ms

const arr2 = [7, 8, 4, 5, 6, 3, 2, 1];
bubbleSort2(arr2);
// 改进后 arr : [1, 2, 3, 4, 5, 6, 7, 8]
// 改进后冒泡排序耗时: 0.318115234375ms

分析

  • 第一,冒泡排序是原地排序算法吗 ?
    冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个原地排序算法。
  • 第二,冒泡排序是稳定的排序算法吗 ?
    在冒泡排序中,只有交换才可以改变两个元素的前后顺序。
    为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序。
    所以冒泡排序是稳定的排序算法。
  • 第三,冒泡排序的时间复杂度是多少 ?
    最佳情况:T(n) = O(n),当数据已经是正序时。
    最差情况:T(n) = O(n2),当数据是反序时。
    平均情况:T(n) = O(n2)。

动画

冒泡排序动画

冒泡排序动画

4. 插入排序

插入排序又为分为 直接插入排序 和优化后的 拆半插入排序希尔排序,我们通常说的插入排序是指直接插入排序。

一、直接插入

**

一般人打扑克牌,整理牌的时候,都是按牌的大小(从小到大或者从大到小)整理牌的,那每摸一张新牌,就扫描自己的牌,把新牌插入到相应的位置。

插入排序的工作原理:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

步骤

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤 2~5。

实现

// 插入排序
const insertionSort = array => {
	const len = array.length;
	if (len <= 1) return

	let preIndex, current;
	for (let i = 1; i < len; i++) {
		preIndex = i - 1; //待比较元素的下标
		current = array[i]; //当前元素
		while (preIndex >= 0 && array[preIndex] > current) {
			//前置条件之一: 待比较元素比当前元素大
			array[preIndex + 1] = array[preIndex]; //将待比较元素后移一位
			preIndex--; //游标前移一位
		}
		if (preIndex + 1 != i) {
			//避免同一个元素赋值给自身
			array[preIndex + 1] = current; //将当前元素插入预留空位
			console.log('array :', array);
		}
	}
	return array;
};

测试

// 测试
const array = [5, 4, 3, 2, 1];
console.log("原始 array :", array);
insertionSort(array);
// 原始 array:    [5, 4, 3, 2, 1]
// array:  		 [4, 5, 3, 2, 1]
// array:  		 [3, 4, 5, 2, 1]
// array: 		 [2, 3, 4, 5, 1]
// array:  		 [1, 2, 3, 4, 5]

分析

  • 第一,插入排序是原地排序算法吗 ?
    插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1),所以,这是一个原地排序算法。
  • 第二,插入排序是稳定的排序算法吗 ?
    在插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法。
  • 第三,插入排序的时间复杂度是多少 ?
    最佳情况:T(n) = O(n),当数据已经是正序时。
    最差情况:T(n) = O(n2),当数据是反序时。
    平均情况:T(n) = O(n2)。

动画

insertion-sort.gif

二、拆半插入

插入排序也有一种优化算法,叫做拆半插入

**

折半插入排序是直接插入排序的升级版,鉴于插入排序第一部分为已排好序的数组, 我们不必按顺序依次寻找插入点, 只需比较它们的中间值与待插入元素的大小即可。

步骤

  • 取 0 ~ i-1 的中间点 ( m = (i-1)>>1 ),array[i] 与 array[m] 进行比较,若 array[i] < array[m],则说明待插入的元素 array[i] 应该处于数组的 0 ~ m 索引之间;反之,则说明它应该处于数组的 m ~ i-1 索引之间。
  • 重复步骤 1,每次缩小一半的查找范围,直至找到插入的位置。
  • 将数组中插入位置之后的元素全部后移一位。
  • 在指定位置插入第 i 个元素。

注:x>>1 是位运算中的右移运算,表示右移一位,等同于 x 除以 2 再取整,即 x>>1 == Math.floor(x/2) 。

// 折半插入排序
const binaryInsertionSort = array => {
	const len = array.length;
	if (len <= 1) return;

	let current, i, j, low, high, m;
	for (i = 1; i < len; i++) {
		low = 0;
		high = i - 1;
		current = array[i];

		while (low <= high) {
			//步骤 1 & 2 : 折半查找
			m = (low + high) >> 1; // 注: x>>1 是位运算中的右移运算, 表示右移一位, 等同于 x 除以 2 再取整, 即 x>>1 == Math.floor(x/2) .
			if (array[i] >= array[m]) {
				//值相同时, 切换到高半区,保证稳定性
				low = m + 1; //插入点在高半区
			} else {
				high = m - 1; //插入点在低半区
			}
		}
		for (j = i; j > low; j--) {
			//步骤 3: 插入位置之后的元素全部后移一位
			array[j] = array[j - 1];
			console.log('array2 :', JSON.parse(JSON.stringify(array)));
		}
		array[low] = current; //步骤 4: 插入该元素
	}
	console.log('array2 :', JSON.parse(JSON.stringify(array)));
	return array;
};

测试

const array2 = [5, 4, 3, 2, 1];
console.log('原始 array2:', array2);
binaryInsertionSort(array2);
// 原始 array2:  [5, 4, 3, 2, 1]
// array2 :     [5, 5, 3, 2, 1]
// array2 :     [4, 5, 5, 2, 1]
// array2 :     [4, 4, 5, 2, 1]
// array2 :     [3, 4, 5, 5, 1]
// array2 :     [3, 4, 4, 5, 1]
// array2 :     [3, 3, 4, 5, 1]
// array2 :     [2, 3, 4, 5, 5]
// array2 :     [2, 3, 4, 4, 5]
// array2 :     [2, 3, 3, 4, 5]
// array2 :     [2, 2, 3, 4, 5]
// array2 :     [1, 2, 3, 4, 5]

注意:和直接插入排序类似,折半插入排序每次交换的是相邻的且值为不同的元素,它并不会改变值相同的元素之间的顺序,因此它是稳定的。

三、希尔排序

希尔排序是一个平均时间复杂度为 O(nlogn) 的算法,会在下一个章节和 归并排序、快速排序、堆排序 一起讲,本文就不展开了。

5. 选择排序

思路

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

步骤

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

实现

const selectionSort = array => {
	const len = array.length;
	let minIndex, temp;
	for (let i = 0; i < len - 1; i++) {
		minIndex = i;
		for (let j = i + 1; j < len; j++) {
			if (array[j] < array[minIndex]) {
				// 寻找最小的数
				minIndex = j; // 将最小数的索引保存
			}
		}
		temp = array[i];
		array[i] = array[minIndex];
		array[minIndex] = temp;
		console.log('array: ', array);
	}
	return array;
};

测试

// 测试
const array = [5, 4, 3, 2, 1];
console.log('原始array:', array);
selectionSort(array);
// 原始 array:  [5, 4, 3, 2, 1]
// array:  		 [1, 4, 3, 2, 5]
// array:  		 [1, 2, 3, 4, 5]
// array: 		 [1, 2, 3, 4, 5]
// array:  		 [1, 2, 3, 4, 5]

分析

  • 第一,选择排序是原地排序算法吗 ?
    选择排序空间复杂度为 O(1),是一种原地排序算法。
  • 第二,选择排序是稳定的排序算法吗 ?
    选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,这样破坏了稳定性。所以,选择排序是一种不稳定的排序算法。
  • 第三,选择排序的时间复杂度是多少 ?
    无论是正序还是逆序,选择排序都会遍历 n2 / 2 次来排序,所以,最佳、最差和平均的复杂度是一样的。
    最佳情况:T(n) = O(n2)。
    最差情况:T(n) = O(n2)。
    平均情况:T(n) = O(n2)。

动画

selection-sort.gif

5. 选择排序

思路

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

步骤

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

实现

const selectionSort = array => {
	const len = array.length;
	let minIndex, temp;
	for (let i = 0; i < len - 1; i++) {
		minIndex = i;
		for (let j = i + 1; j < len; j++) {
			if (array[j] < array[minIndex]) {
				// 寻找最小的数
				minIndex = j; // 将最小数的索引保存
			}
		}
		temp = array[i];
		array[i] = array[minIndex];
		array[minIndex] = temp;
		console.log('array: ', array);
	}
	return array;
};

测试

// 测试
const array = [5, 4, 3, 2, 1];
console.log('原始array:', array);
selectionSort(array);
// 原始 array:  [5, 4, 3, 2, 1]
// array:  		 [1, 4, 3, 2, 5]
// array:  		 [1, 2, 3, 4, 5]
// array: 		 [1, 2, 3, 4, 5]
// array:  		 [1, 2, 3, 4, 5]

分析

  • 第一,选择排序是原地排序算法吗 ?
    选择排序空间复杂度为 O(1),是一种原地排序算法。
  • 第二,选择排序是稳定的排序算法吗 ?
    选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,这样破坏了稳定性。所以,选择排序是一种不稳定的排序算法。
  • 第三,选择排序的时间复杂度是多少 ?
    无论是正序还是逆序,选择排序都会遍历 n2 / 2 次来排序,所以,最佳、最差和平均的复杂度是一样的。
    最佳情况:T(n) = O(n2)。
    最差情况:T(n) = O(n2)。
    平均情况:T(n) = O(n2)。

动画

selection-sort.gif

6. 解答开篇

为什么插入排序比冒泡排序更受欢迎 ?

冒泡排序和插入排序的时间复杂度都是 O(n2),都是原地排序算法,为什么插入排序要比冒泡排序更受欢迎呢 ?

这里关乎到 逆序度、满有序度、有序度。

  • 有序度:是数组中具有有序关系的元素对的个数。
    有序元素对用数学表达式表示就是这样:
有序元素对:a[i] <= a[j], 如果 i < j。
  • 满有序度:把完全有序的数组的有序度叫作 满有序度

  • 逆序度:正好跟有序度相反(默认从小到大为有序)。

逆序元素对:a[i] > a[j], 如果 i < j。

同理,对于一个倒序排列的数组,比如 6,5,4,3,2,1,有序度是 0;
对于一个完全有序的数组,比如 1,2,3,4,5,6,有序度就是 n(n-1)/2* ,也就是满有序度为 15。

原因

  • 冒泡排序不管怎么优化,元素交换的次数是一个固定值,是原始数据的逆序度。
  • 插入排序是同样的,不管怎么优化,元素移动的次数也等于原始数据的逆序度。
  • 但是,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个,数据量一旦大了,这差别就非常明显了。

7. 排序算法的复杂性对比

复杂性对比

名称 平均 最好 最坏 空间 稳定性 排序方式
冒泡排序 O(n2) O(n) O(n2) O(1) Yes In-place
插入排序 O(n2) O(n) O(n2) O(1) Yes In-place
选择排序 O(n2) O(n2) O(n2) O(1) No In-place

算法可视化工具

这里推荐一个算法可视化工具。

算法可视化工具 algorithm-visualizer 是一个交互式的在线平台,可以从代码中可视化算法,还可以看到代码执行的过程。

效果如下图。

算法可视化工具

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。

8. 最后

喜欢就点个赞吧。

文中所有的代码及测试事例都已经放到我的 GitHub 上了。

参考文章:

js 日期对象 setMonth 的锅

BiaoChenXuYing

前言

需求:获取当前日期的前一个月份

当月有 31 天时,JS 日期对象 setMonth 问题

1. 一般做法

当前日期如果不是 31 号, 是没问题的,是 31 号就会有问题:

// 比如今天是 2018-09-30 号,前一个月应该是 2018-08-30 
let now = new Date(new Date("2018-09-30").setMonth(new Date("2018-09-30").getMonth() - 1))
console.log('now :', now.toLocaleString())
// now : 2018/8/30 上午8:00:00

// 比如今天是 2018-10-31 号,前一个月没有 31 号,所以结果 2018-10-01:
let now = new Date(new Date("2018-10-31").setMonth(new Date("2018-10-31").getMonth() - 1))
console.log('now :', now.toLocaleString())
// now : 2018/10/1 上午8:00:00

2. 正确的方法:

2.1 方法一

原理: 当前时间减去当前时间的天数


function initLastMonth(date) {
            let monthDate = new Date(date);
            let newDate = new Date(monthDate.getTime() - 24 * 60 * 60 * 1000 * monthDate.getDate())
            console.log('newDate :', newDate.toLocaleString())
          return newDate
}
initLastMonth("2018-10-31")
//  newDate : 2018/9/30 上午8:00:00

2.2 方法二

原理: setMonth 之前先 setDate(1)

function initLastMonth(date) {
            const now = new Date(date);
            now.setDate(1)
            now.setMonth(now.getMonth() - 1)
            console.log(now.toLocaleString()) 
            return now
        }
initLastMonth("2018-10-31")
// 2018/9/1 上午8:00:00

最后

技术文章更新地址:github

Vue + TypeScript + Element 搭建简洁时尚的博客网站及踩坑记

前言

本文讲解如何在 Vue 项目中使用 TypeScript 来搭建并开发项目,并在此过程中踩过的坑 。

TypeScript 具有类型系统,且是 JavaScript 的超集,TypeScript 在 2018年 势头迅猛,可谓遍地开花。

Vue3.0 将使用 TS 重写,重写后的 Vue3.0 将更好的支持 TS。2019 年 TypeScript 将会更加普及,能够熟练掌握 TS,并使用 TS 开发过项目,将更加成为前端开发者的优势。

所以笔者就当然也要学这个必备技能,就以 边学边实践 的方式,做个博客项目来玩玩。

此项目是基于 Vue 全家桶 + TypeScript + Element-UI 的技术栈,且已经开源,github 地址 blog-vue-typescript

因为之前写了篇纯 Vue 项目搭建的相关文章 基于vue+mint-ui的mobile-h5的项目说明 ,有不少人加我微信,要源码来学习,但是这个是我司的项目,不能提供原码。

所以做一个不是我司的项目,且又是 vue 相关的项目来练手并开源吧。

1. 效果

效果图:

  • pc 端

  • 移动端

完整效果请看:https://biaochenxuying.cn

2. 功能

已经完成功能

  • 登录
  • 注册
  • 文章列表
  • 文章归档
  • 标签
  • 关于
  • 点赞与评论
  • 留言
  • 历程
  • 文章详情(支持代码语法高亮)
  • 文章详情目录
  • 移动端适配
  • github 授权登录

待优化或者实现

  • 使用 vuex-class
  • 更多 TypeScript 的优化技巧
  • 服务器渲染 SSR

3. 前端主要技术

所有技术都是当前最新的。

  • vue: ^2.6.6
  • typescript : ^3.2.1
  • element-ui: 2.6.3
  • vue-router : ^3.0.1
  • webpack: 4.28.4
  • vuex: ^3.0.1
  • axios:0.18.0
  • redux: 4.0.0
  • highlight.js: 9.15.6
  • marked:0.6.1

4. 5 分钟上手 TypeScript

如果没有一点点基础,可能没学过 TypeScript 的读者会看不懂往下的内容,所以先学点基础。

TypeScript 的静态类型检查是个好东西,可以避免很多不必要的错误, 不用在调试或者项目上线的时候才发现问题 。

  • 类型注解

TypeScript 里的类型注解是一种轻量级的为函数或变量添加约束的方式。变量定义时也要定义他的类型,比如常见的 :

// 布尔值
let isDone: boolean = false; // 相当于 js 的 let isDone = false;
// 变量定义之后不可以随便变更它的类型
isDone = true // 不报错
isDone = "我要变为字符串" // 报错
// 数字
let decLiteral: number = 6; // 相当于 js 的 let decLiteral = 6;
// 字符串
let name: string = "bob";  // 相当于 js 的 let name = "bob";
// 数组
 // 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:
let list: number[] = [1, 2, 3]; // 相当于 js 的let list = [1, 2, 3];
// 第二种方式是使用数组泛型,Array<元素类型>:
let list: Array<number> = [1, 2, 3]; // 相当于 js 的let list = [1, 2, 3];
// 在 TypeScript 中,我们使用接口(Interfaces)来定义 对象 的类型。
interface Person {
    name: string;
    age: number;
}
let tom: Person = {
    name: 'Tom',
    age: 25
};
// 以上 对象 的代码相当于 
let tom = {
    name: 'Tom',
    age: 25
};
// Any 可以随便变更类型 (当这个值可能来自于动态的内容,比如来自用户输入或第三方代码库)
let notSure: any = 4;
notSure = "我可以随便变更类型" // 不报错
notSure = false;  // 不报错
// Void 当一个函数没有返回值时,你通常会见到其返回值类型是 void
function warnUser(): void {
    console.log("This is my warning message");
}
// 方法的参数也要定义类型,不知道就定义为 any
function fetch(url: string, id : number, params: any): void {
    console.log("fetch");
}

以上是最简单的一些知识点,更多知识请看 TypeScript 中文官网

5. 5 分钟上手 Vue +TypeScript

  • vue-class-component 
    vue-class-component 对 Vue 组件进行了一层封装,让 Vue 组件语法在结合了 TypeScript 语法之后更加扁平化:
<template>
  <div>
    <input v-model="msg">
    <p>prop: {{propMessage}}</p>
    <p>msg: {{msg}}</p>
    <p>helloMsg: {{helloMsg}}</p>
    <p>computed msg: {{computedMsg}}</p>
    <button @click="greet">Greet</button>
  </div>
</template>

<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component({
  props: {
    propMessage: String
  }
})
export default class App extends Vue {
  // initial data
  msg = 123

  // use prop values for initial data
  helloMsg = 'Hello, ' + this.propMessage

  // lifecycle hook
  mounted () {
    this.greet()
  }

  // computed
  get computedMsg () {
    return 'computed ' + this.msg
  }

  // method
  greet () {
    alert('greeting: ' + this.msg)
  }
}
</script>

上面的代码跟下面的代码作用是一样的:

<template>
  <div>
    <input v-model="msg">
    <p>prop: {{propMessage}}</p>
    <p>msg: {{msg}}</p>
    <p>helloMsg: {{helloMsg}}</p>
    <p>computed msg: {{computedMsg}}</p>
    <button @click="greet">Greet</button>
  </div>
</template>

<script>
export default {
  // 属性
  props: {
    propMessage: {
      type: String
    }
  },
  data () {
    return {
      msg: 123,
      helloMsg: 'Hello, ' + this.propMessage
    }
  },
  // 声明周期钩子
  mounted () {
    this.greet()
  },
  // 计算属性
  computed: {
    computedMsg () {
      return 'computed ' + this.msg
    }
  },
  // 方法
  methods: {
    greet () {
      alert('greeting: ' + this.msg)
    }
  },
}
</script>

vue-property-decorator 是在 vue-class-component 上增强了更多的结合 Vue 特性的装饰器,新增了这 7 个装饰器:

  • @Emit
  • @Inject
  • @Model
  • @Prop
  • @Provide
  • @Watch
  • @Component (从 vue-class-component 继承)

在这里列举几个常用的@Prop/@Watch/@Component, 更多信息,详见官方文档

import { Component, Emit, Inject, Model, Prop, Provide, Vue, Watch } from 'vue-property-decorator'

@Component
export class MyComponent extends Vue {
  
  @Prop()
  propA: number = 1

  @Prop({ default: 'default value' })
  propB: string

  @Prop([String, Boolean])
  propC: string | boolean

  @Prop({ type: null })
  propD: any

  @Watch('child')
  onChildChanged(val: string, oldVal: string) { }
}

上面的代码相当于:

export default {
  props: {
    checked: Boolean,
    propA: Number,
    propB: {
      type: String,
      default: 'default value'
    },
    propC: [String, Boolean],
    propD: { type: null }
  }
  methods: {
    onChildChanged(val, oldVal) { }
  },
  watch: {
    'child': {
      handler: 'onChildChanged',
      immediate: false,
      deep: false
    }
  }
}
  • vuex-class
    vuex-class :在 vue-class-component 写法中 绑定 vuex
import Vue from 'vue'
import Component from 'vue-class-component'
import {
  State,
  Getter,
  Action,
  Mutation,
  namespace
} from 'vuex-class'

const someModule = namespace('path/to/module')

@Component
export class MyComp extends Vue {
  @State('foo') stateFoo
  @State(state => state.bar) stateBar
  @Getter('foo') getterFoo
  @Action('foo') actionFoo
  @Mutation('foo') mutationFoo
  @someModule.Getter('foo') moduleGetterFoo

  // If the argument is omitted, use the property name
  // for each state/getter/action/mutation type
  @State foo
  @Getter bar
  @Action baz
  @Mutation qux

  created () {
    this.stateFoo // -> store.state.foo
    this.stateBar // -> store.state.bar
    this.getterFoo // -> store.getters.foo
    this.actionFoo({ value: true }) // -> store.dispatch('foo', { value: true })
    this.mutationFoo({ value: true }) // -> store.commit('foo', { value: true })
    this.moduleGetterFoo // -> store.getters['path/to/module/foo']
  }
}

6. 用 vue-cli 搭建 项目

笔者使用最新的 vue-cli 3 搭建项目,详细的教程,请看我之前写的 vue-cli3.x 新特性及踩坑记,里面已经有详细讲解 ,但文章里面的配置和此项目不同的是,我加入了 TypeScript ,其他的配置都是 vue-cli 本来配好的了。详情请看 vue-cli 官网

6.1 安装及构建项目目录

安装的依赖:

安装过程选择的一些配置:

搭建好之后,初始项目结构长这样:

├── public                          // 静态页面

├── src                             // 主目录

    ├── assets                      // 静态资源

    ├── components                  // 组件

    ├── views                       // 页面

    ├── App.vue                     // 页面主入口

    ├── main.ts                     // 脚本主入口

    ├── router.ts                   // 路由

    ├── shims-tsx.d.ts              // 相关 tsx 模块注入

    ├── shims-vue.d.ts              // Vue 模块注入

    └── store.ts                    // vuex 配置

├── tests                           // 测试用例

├── .eslintrc.js                    // eslint 相关配置

├── .gitignore                      // git 忽略文件配置

├── babel.config.js                 // babel 配置

├── postcss.config.js               // postcss 配置

├── package.json                    // 依赖

└── tsconfig.json                   // ts 配置

奔着 大型项目的结构 来改造项目结构,改造后 :


├── public                          // 静态页面

├── src                             // 主目录

    ├── assets                      // 静态资源

    ├── filters                     // 过滤

    ├── store                       // vuex 配置

    ├── less                        // 样式

    ├── utils                       // 工具方法(axios封装,全局方法等)

    ├── views                       // 页面

    ├── App.vue                     // 页面主入口

    ├── main.ts                     // 脚本主入口

    ├── router.ts                   // 路由

    ├── shime-global.d.ts           // 相关 全局或者插件 模块注入

    ├── shims-tsx.d.ts              // 相关 tsx 模块注入

    ├── shims-vue.d.ts              // Vue 模块注入, 使 TypeScript 支持 *.vue 后缀的文件

├── tests                           // 测试用例

├── .eslintrc.js                    // eslint 相关配置

├── postcss.config.js               // postcss 配置

├── .gitignore                      // git 忽略文件配置

├── babel.config.js                 // preset 记录

├── package.json                    // 依赖

├── README.md                       // 项目 readme

├── tsconfig.json                   // ts 配置

└── vue.config.js                   // webpack 配置

tsconfig.json 文件中指定了用来编译这个项目的根文件和编译选项。
本项目的 tsconfig.json 配置如下 :

{
    // 编译选项
  "compilerOptions": {
    // 编译输出目标 ES 版本
    "target": "esnext",
    // 采用的模块系统
    "module": "esnext",
    // 以严格模式解析
    "strict": true,
    "jsx": "preserve",
    // 从 tslib 导入外部帮助库: 比如__extends,__rest等
    "importHelpers": true,
    // 如何处理模块
    "moduleResolution": "node",
    // 启用装饰器
    "experimentalDecorators": true,
    "esModuleInterop": true,
    // 允许从没有设置默认导出的模块中默认导入
    "allowSyntheticDefaultImports": true,
    // 定义一个变量就必须给它一个初始值
    "strictPropertyInitialization" : false,
    // 允许编译javascript文件
    "allowJs": true,
    // 是否包含可以用于 debug 的 sourceMap
    "sourceMap": true,
    // 忽略 this 的类型检查, Raise error on this expressions with an implied any type.
    "noImplicitThis": false,
    // 解析非相对模块名的基准目录 
    "baseUrl": ".",
    // 给错误和消息设置样式,使用颜色和上下文。
    "pretty": true,
    // 设置引入的定义文件
    "types": ["webpack-env", "mocha", "chai"],
    // 指定特殊模块的路径
    "paths": {
      "@/*": ["src/*"]
    },
    // 编译过程中需要引入的库文件的列表
    "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
  },
  // ts 管理的文件
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
  ],
  // ts 排除的文件
  "exclude": ["node_modules"]
}

更多配置请看官网的 tsconfig.json 的 编译选项

本项目的 vue.config.js:

const path = require("path");
const sourceMap = process.env.NODE_ENV === "development";

module.exports = {
  // 基本路径
  publicPath: "./",
  // 输出文件目录
  outputDir: "dist",
  // eslint-loader 是否在保存的时候检查
  lintOnSave: false,
  // webpack配置
  // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
  chainWebpack: () => {},
  configureWebpack: config => {
    if (process.env.NODE_ENV === "production") {
      // 为生产环境修改配置...
      config.mode = "production";
    } else {
      // 为开发环境修改配置...
      config.mode = "development";
    }

    Object.assign(config, {
      // 开发生产共同配置
      resolve: {
        extensions: [".js", ".vue", ".json", ".ts", ".tsx"],
        alias: {
          vue$: "vue/dist/vue.js",
          "@": path.resolve(__dirname, "./src")
        }
      }
    });
  },
  // 生产环境是否生成 sourceMap 文件
  productionSourceMap: sourceMap,
  // css相关配置
  css: {
    // 是否使用css分离插件 ExtractTextPlugin
    extract: true,
    // 开启 CSS source maps?
    sourceMap: false,
    // css预设器配置项
    loaderOptions: {},
    // 启用 CSS modules for all css / pre-processor files.
    modules: false
  },
  // use thread-loader for babel & TS in production build
  // enabled by default if the machine has more than 1 cores
  parallel: require("os").cpus().length > 1,
  // PWA 插件相关配置
  // see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
  pwa: {},
  // webpack-dev-server 相关配置
  devServer: {
    open: process.platform === "darwin",
    host: "localhost",
    port: 3001, //8080,
    https: false,
    hotOnly: false,
    proxy: {
      // 设置代理
      // proxy all requests starting with /api to jsonplaceholder
      "/api": {
        // target: "https://emm.cmccbigdata.com:8443/",
        target: "http://localhost:3000/",
        // target: "http://47.106.136.114/",
        changeOrigin: true,
        ws: true,
        pathRewrite: {
          "^/api": ""
        }
      }
    },
    before: app => {}
  },
  // 第三方插件配置
  pluginOptions: {
    // ...
  }
};

6.2 安装 element-ui

本来想搭配 iview-ui 来用的,但后续还想把这个项目搞成 ssr 的,而 vue + typescript + iview + Nuxt.js 的服务端渲染还有不少坑, 而 vue + typescript + element + Nuxt.js 对 ssr 的支持已经不错了,所以选择了 element-ui 。

安装:

npm i element-ui -S

按需引入, 借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。

npm install babel-plugin-component -D

然后,将 babel.config.js 修改为:

module.exports = {
  presets: ["@vue/app"],
  plugins: [
    [
      "component",
      {
        libraryName: "element-ui",
        styleLibraryName: "theme-chalk"
      }
    ]
  ]
};

接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:

import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';

Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或写为
 * Vue.use(Button)
 * Vue.use(Select)
 */

new Vue({
  el: '#app',
  render: h => h(App)
});

6.3 完善项目目录与文件

route

使用路由懒加载功能。

export default new Router({
  mode: "history",
  routes: [
    {
      path: "/",
      name: "home",
      component: () => import(/* webpackChunkName: "home" */ "./views/home.vue")
    },
    {
      path: "/articles",
      name: "articles",
      // route level code-splitting
      // this generates a separate chunk (articles.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () =>
        import(/* webpackChunkName: "articles" */ "./views/articles.vue")
    },
  ]
});

utils

  • utils/utils.ts 常用函数的封装, 比如 事件的节流(throttle)与防抖(debounce)方法:
// fn是我们需要包装的事件回调, delay是时间间隔的阈值
export function throttle(fn: Function, delay: number) {
  // last为上一次触发回调的时间, timer是定时器
  let last = 0,
    timer: any = null;
  // 将throttle处理结果当作函数返回
  return function() {
    // 保留调用时的this上下文
    let context = this;
    // 保留调用时传入的参数
    let args = arguments;
    // 记录本次触发回调的时间
    let now = +new Date();
    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
    if (now - last < delay) {
      // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
      clearTimeout(timer);
      timer = setTimeout(function() {
        last = now;
        fn.apply(context, args);
      }, delay);
    } else {
      // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
      last = now;
      fn.apply(context, args);
    }
  };
}
  • utils/config.ts 配置文件,比如 github 授权登录的回调地址、client_id、client_secret 等。
const config = {
  'oauth_uri': 'https://github.com/login/oauth/authorize',
  'redirect_uri': 'https://biaochenxuying.cn/login',
  'client_id': 'XXXXXXXXXX',
  'client_secret': 'XXXXXXXXXX',
};

// 本地开发环境下
if (process.env.NODE_ENV === 'development') {
  config.redirect_uri = "http://localhost:3001/login"
  config.client_id = "502176cec65773057a9e"
  config.client_secret = "65d444de381a026301a2c7cffb6952b9a86ac235"
}
export default config;

如果你的生产环境也要 github 登录授权的话,请在 github 上申请一个 Oauth App ,把你的 redirect_uri,client_id,client_secret 的信息填在 config 里面即可。具体详情请看我写的这篇文章 github 授权登录教程与如何设计第三方授权登录的用户表

  • utils/urls.ts 请求接口地址,统一管理。
// url的链接
export const urls: object = {
  login: "login",
  register: "register",
  getArticleList: "getArticleList",
};
export default urls;
  • utils/https.ts axios 请求的封装。
import axios from "axios";

// 创建axios实例
let service: any = {};
service = axios.create({
    baseURL: "/api", // api的base_url
    timeout: 50000 // 请求超时时间
  });

// request拦截器 axios的一些配置
service.interceptors.request.use(
  (config: any) => {
    return config;
  },
  (error: any) => {
    // Do something with request error
    console.error("error:", error); // for debug
    Promise.reject(error);
  }
);

// respone拦截器 axios的一些配置
service.interceptors.response.use(
  (response: any) => {
    return response;
  },
  (error: any) => {
    console.error("error:" + error); // for debug
    return Promise.reject(error);
  }
);

export default service;

把 urls 和 https 挂载到 main.ts 里面的 Vue 的 prototype 上面。

import service from "./utils/https";
import urls from "./utils/urls";

Vue.prototype.$https = service; // 其他页面在使用 axios 的时候直接  this.$http 就可以了
Vue.prototype.$urls = urls; // 其他页面在使用 urls 的时候直接  this.$urls 就可以了

然后就可以统一管理接口,而且调用起来也很方便啦。比如下面 文章列表的请求。

async handleSearch() {
    this.isLoading = true;
    const res: any = await this.$https.get(this.$urls.getArticleList, {
      params: this.params
    });
    this.isLoading = false;
    if (res.status === 200) {
      if (res.data.code === 0) {
        const data: any = res.data.data;
        this.articlesList = [...this.articlesList, ...data.list];
        this.total = data.count;
        this.params.pageNum++;
        if (this.total === this.articlesList.length) {
          this.isLoadEnd = true;
        }
      } else {
        this.$message({
          message: res.data.message,
          type: "error"
        });
      }
    } else {
      this.$message({
        message: "网络错误!",
        type: "error"
      });
    }
  }

store ( Vuex )

一般大型的项目都有很多模块的,比如本项目中有公共信息(比如 token )、 用户模块、文章模块。

├── modules                         // 模块

    ├── user.ts                     // 用户模块 
    
    ├── article.ts                 // 文章模块 

├── types.ts                        // 类型

└── index.ts                        // vuex 主入口
  • store/index.ts 存放公共的信息,并导入其他模块
import Vue from "vue";
import Vuex from "vuex";
import * as types from "./types";
import user from "./modules/user";
import article from "./modules/article";

Vue.use(Vuex);
const initPageState = () => {
  return {
    token: ""
  };
};
const store = new Vuex.Store({
  strict: process.env.NODE_ENV !== "production",
  // 具体模块
  modules: {
    user,
    article
  },
  state: initPageState(),
  mutations: {
    [types.SAVE_TOKEN](state: any, pageState: any) {
      for (const prop in pageState) {
        state[prop] = pageState[prop];
      }
    }
  },
  actions: {}
});

export default store;
  • types.ts
// 公共 token
export const SAVE_TOKEN = "SAVE_TOKEN";

// 用户
export const SAVE_USER = "SAVE_USER";
  • user.ts
import * as types from "../types";

const initPageState = () => {
  return {
    userInfo: {
      _id: "",
      name: "",
      avator: ""
    }
  };
};
const user = {
  state: initPageState(),
  mutations: {
    [types.SAVE_USER](state: any, pageState: any) {
      for (const prop in pageState) {
        state[prop] = pageState[prop];
      }
    }
  },
  actions: {}
};

export default user;

7. markdown 渲染

markdown 渲染效果图:

markdown 渲染效果图

markdown 渲染 采用了开源的 marked, 代码高亮用了 highlight.js 。

用法:

第一步:npm i marked highlight.js --save

npm i marked highlight.js --save

第二步: 导入封装成 markdown.js,将文章详情由字符串转成 html, 并抽离出文章目录。

marked 的封装 得感谢这位老哥。

const highlight = require("highlight.js");
const marked = require("marked");
const tocObj = {
  add: function(text, level) {
    var anchor = `#toc${level}${++this.index}`;
    this.toc.push({ anchor: anchor, level: level, text: text });
    return anchor;
  },
  // 使用堆栈的方式处理嵌套的ul,li,level即ul的嵌套层次,1是最外层
  // <ul>
  //   <li></li>
  //   <ul>
  //     <li></li>
  //   </ul>
  //   <li></li>
  // </ul>
  toHTML: function() {
    let levelStack = [];
    let result = "";
    const addStartUL = () => {
      result += '<ul class="anchor-ul" id="anchor-fix">';
    };
    const addEndUL = () => {
      result += "</ul>\n";
    };
    const addLI = (anchor, text) => {
      result +=
        '<li><a class="toc-link" href="#' + anchor + '">' + text + "<a></li>\n";
    };

    this.toc.forEach(function(item) {
      let levelIndex = levelStack.indexOf(item.level);
      // 没有找到相应level的ul标签,则将li放入新增的ul中
      if (levelIndex === -1) {
        levelStack.unshift(item.level);
        addStartUL();
        addLI(item.anchor, item.text);
      } // 找到了相应level的ul标签,并且在栈顶的位置则直接将li放在此ul下
      else if (levelIndex === 0) {
        addLI(item.anchor, item.text);
      } // 找到了相应level的ul标签,但是不在栈顶位置,需要将之前的所有level出栈并且打上闭合标签,最后新增li
      else {
        while (levelIndex--) {
          levelStack.shift();
          addEndUL();
        }
        addLI(item.anchor, item.text);
      }
    });
    // 如果栈中还有level,全部出栈打上闭合标签
    while (levelStack.length) {
      levelStack.shift();
      addEndUL();
    }
    // 清理先前数据供下次使用
    this.toc = [];
    this.index = 0;
    return result;
  },
  toc: [],
  index: 0
};

class MarkUtils {
  constructor() {
    this.rendererMD = new marked.Renderer();
    this.rendererMD.heading = function(text, level, raw) {
      var anchor = tocObj.add(text, level);
      return `<h${level} id=${anchor}>${text}</h${level}>\n`;
    };
    highlight.configure({ useBR: true });
    marked.setOptions({
      renderer: this.rendererMD,
      headerIds: false,
      gfm: true,
      tables: true,
      breaks: false,
      pedantic: false,
      sanitize: false,
      smartLists: true,
      smartypants: false,
      highlight: function(code) {
        return highlight.highlightAuto(code).value;
      }
    });
  }

  async marked(data) {
    if (data) {
      let content = await marked(data); // 文章内容
      let toc = tocObj.toHTML(); // 文章目录
      return { content: content, toc: toc };
    } else {
      return null;
    }
  }
}

const markdown = new MarkUtils();

export default markdown;

第三步: 使用

import markdown from "@/utils/markdown";

// 获取文章详情
async handleSearch() {
    const res: any = await this.$https.post(
      this.$urls.getArticleDetail,
      this.params
    );
    if (res.status === 200) {
      if (res.data.code === 0) {
        this.articleDetail = res.data.data;
       // 使用 marked 转换
        const article = markdown.marked(res.data.data.content);
        article.then((response: any) => {
          this.articleDetail.content = response.content;
          this.articleDetail.toc = response.toc;
        });
      } else {
        // ...
    } else {
     // ... 
    }
  }

// 渲染
<div id="content"
       class="article-detail"
       v-html="articleDetail.content">
</div>

第四步:引入 monokai_sublime 的 css 样式

<link href="http://cdn.bootcss.com/highlight.js/8.0/styles/monokai_sublime.min.css" rel="stylesheet">

第五步:对 markdown 样式的补充

如果不补充样式,是没有黑色背景的,字体大小等也会比较小,图片也不会居中显示

/*对 markdown 样式的补充*/
pre {
    display: block;
    padding: 10px;
    margin: 0 0 10px;
    font-size: 14px;
    line-height: 1.42857143;
    color: #abb2bf;
    background: #282c34;
    word-break: break-all;
    word-wrap: break-word;
    overflow: auto;
}
h1,h2,h3,h4,h5,h6{
    margin-top: 1em;
    /* margin-bottom: 1em; */
}
strong {
    font-weight: bold;
}

p > code:not([class]) {
    padding: 2px 4px;
    font-size: 90%;
    color: #c7254e;
    background-color: #f9f2f4;
    border-radius: 4px;
}
p img{
    /* 图片居中 */
    margin: 0 auto;
    display: flex;
}

#content {
    font-family: "Microsoft YaHei",  'sans-serif';
    font-size: 16px;
    line-height: 30px;
}

#content .desc ul,#content .desc ol {
    color: #333333;
    margin: 1.5em 0 0 25px;
}

#content .desc h1, #content .desc h2 {
    border-bottom: 1px solid #eee;
    padding-bottom: 10px;
}

#content .desc a {
    color: #009a61;
}

8. 注意点

  • 关于 页面

对于 关于 的页面,其实是一篇文章来的,根据文章类型 type 来决定的,数据库里面 type 为 3
的文章,只能有一篇就是 博主介绍 ;达到了想什么时候修改内容都可以。

所以当 当前路由 === '/about' 时就是请求类型为 博主介绍 的文章。

type: 3,  // 文章类型: 1:普通文章;2:是博主简历;3 :是博主简介;
  • 移动端适配
    移动端使用 rem 单位适配。
// 屏幕适配( window.screen.width / 移动端设计稿宽 * 100)也即是 (window.screen.width / 750 * 100)  ——*100 为了方便计算。即 font-size 值是手机 deviceWidth 与设计稿比值的 100 倍
document.getElementsByTagName('html')[0].style.fontSize = window.screen.width / 7.5 + 'px';

如上:通过查询屏幕宽度,动态的设置 html 的 font-size 值,移动端的设计稿大多以宽为 750 px 来设置的。

比如在设计图上一个 150 * 250 的盒子(单位 px):

原本在 css 中的写法:

width: 150px;
heigth: 250px;

通过上述换算后,在 css 中对应的 rem 值只需要写:

width: 1.5rem; // 150 / 100 rem
heigth: 2.5rem; // 250 / 100 rem

如果你的移动端的设计稿是以宽为 1080 px 来设置的话,就用 window.screen.width / 10.8 吧。

9. 踩坑记

  • 1. 让 vue 识别全局方法/变量
  1. 我们经常在 main.ts 中给 vue.prototype 挂载实例或者内容,以方便在组件里面使用。
import service from "./utils/https";
import urls from "./utils/urls";

Vue.prototype.$https = service; // 其他页面在使用 axios 的时候直接  this.$http 就可以了
Vue.prototype.$urls = urls; // 其他页面在使用 urls 的时候直接  this.$urls 就可以了

然而当你在组件中直接 this.$http 或者 this.$urls 时会报错的,那是因为 $http 和 $urls 属性,并没有在 vue 实例中声明。

  1. 再比如使用 Element-uI 的 meesage。
import { Message } from "element-ui";

Vue.prototype.$message = Message;

之前用法如下图:

  this.$message({
    message: '恭喜你,这是一条成功消息',
    type: 'success'
  })

然而还是会报错的。

再比如 监听路由的变化:

import { Vue, Watch } from "vue-property-decorator";
import Component from "vue-class-component";
import { Route } from "vue-router";

@Component
export default class App extends Vue {

  @Watch("$route")
  routeChange(val: Route, oldVal: Route) {
      //  do something
  }
}

只是这样写的话,监听 $route 还是会报错的。

想要以上三种做法都正常执行,就还要补充如下内容:

在 src 下的 shims-vue.d.ts 中加入要挂载的内容。 表示 vue 里面的 this 下有这些东西。

import VueRouter, { Route } from "vue-router";

declare module "vue/types/vue" {
  interface Vue {
    $router: VueRouter; // 这表示this下有这个东西
    $route: Route;
    $https: any; // 不知道类型就定为 any 吧(偷懒)
    $urls: any;
    $Message: any;
  }
}
  • 2. 引入的模块要声明

比如 在组件里面使用 window.document 或者 document.querySelector 的时候会报错的,npm run build 不给通过。

再比如:按需引用 element 的组件与动画组件:

import { Button } from "element-ui";
import CollapseTransition from "element-ui/lib/transitions/collapse-transition";

npm run serve 时可以执行,但是在 npm run build 的时候,会直接报错的,因为没有声明。

正确做法:

我在 src 下新建一个文件 shime-global.d.ts ,加入内容如下:

// 声明全局的 window ,不然使用 window.XX 时会报错
declare var window: Window;
declare var document: Document;

declare module "element-ui/lib/transitions/collapse-transition";
declare module "element-ui";

当然,这个文件你加在其他地方也可以,起其他名字都 OK。

但是即使配置了以上方法之后,有些地方使用 document.XXX ,比如 document.title 的时候,npm run build 还是通过不了,所以只能这样了:

<script lang="ts">
// 在用到 document.XXX  的文件中声明一下即可
declare var document: any;
// 此处省略 XXXX 多的代码
</script>
  • 3. this 的类型检查

比如之前的 事件的节流(throttle)与防抖(debounce)方法:

export function throttle(fn: Function, delay: number) {
  return function() {
    // 保留调用时的 this 上下文
    let context = this;
}

function 里面的 this 在 npm run serve 时会报错的,因为 tyescript 检测到它不是在类(class)里面。

正确做法:

在根目录的 tsconfig.json 里面加上 "noImplicitThis": false ,忽略 this 的类型检查。

// 忽略 this 的类型检查, Raise error on this expressions with an implied any type.
"noImplicitThis": false,
  • 4. import 的 .vue 文件

import .vue 的文件的时候,要补全 .vue 的后缀,不然 npm run build 会报错的。

比如:

import Nav from "@/components/nav"; // @ is an alias to /src
import Footer from "@/components/footer"; // @ is an alias to /src

要修改为:

import Nav from "@/components/nav.vue"; // @ is an alias to /src
import Footer from "@/components/footer.vue"; // @ is an alias to /src

报错。

<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
export default class LoadingCustom extends Vue {}
</script>

以下才是正确,因为这里的 Vue 是从 vue-property-decorator import 来的。

<script lang="ts">
import { Vue, Component } from "vue-property-decorator";

@Component
export default class LoadingCustom extends Vue {}
</script>
  • 6. 路由的组件导航守卫失效

vue-class-component 官网里面的路由的导航钩子的用法是没有效果的 Adding Custom Hooks

路由的导航钩子不属于 Vue 本身,这会导致 class 组件转义到配置对象时导航钩子无效,因此如果要使用导航钩子需要在 router 的配置里声明(网上别人说的,还没实践,不确定是否可行)。

  • 7. tsconfig.json 的 strictPropertyInitialization 设为 false,不然你定义一个变量就必须给它一个初始值。

  • position: sticky;

本项目中的文章详情的目录就是用了 sticky。

.anchor {
  position: sticky;
  top: 213px;
  margin-top: 213px;
}

position:sticky 是 css 定位新增属性;可以说是相对定位 relative 和固定定位 fixed 的结合;它主要用在对 scroll 事件的监听上;简单来说,在滑动过程中,某个元素距离其父元素的距离达到 sticky 粘性定位的要求时(比如 top:100px );position:sticky 这时的效果相当于 fixed 定位,固定到适当位置。

用法像上面那样用即可,但是有使用条件:

1、父元素不能 overflow:hidden 或者 overflow:auto 属性。
2、必须指定 top、bottom、left、right 4 个值之一,否则只会处于相对定位
3、父元素的高度不能低于 sticky 元素的高度
4、sticky 元素仅在其父元素内生效

  • 8. eslint 报找不到文件和装饰器的错

App.vue 中只是写了引用文件而已,而且 webpack 和 tsconfig.josn 里面已经配置了别名了的。

import Nav from "@/components/nav.vue"; // @ is an alias to /src
import Slider from "@/components/slider.vue"; // @ is an alias to /src
import Footer from "@/components/footer.vue"; // @ is an alias to /src
import ArrowUp from "@/components/arrowUp.vue"; // @ is an alias to /src
import { isMobileOrPc } from "@/utils/utils";

但是,还是会报如下的错:

只是代码不影响文件的打包,而且本地与生产环境的代码也正常,没报错而已。

这个 eslint 的检测目前还没找到相关的配置可以把这些错误去掉。

  • 9. 路由模式修改为 history

因为文章详情页面有目录,点击目录时定位定相应的内容,但是这个目录定位内容是根据锚点来做的,如果路由模式为 hash 模式的话,本来文章详情页面的路由就是 #articleDetail 了,再点击目录的话(比如 #title2 ),会在 #articleDetail 后面再加上 #title2,一刷新会找不到这个页面的。

10. Build Setup

 # clone
git clone https://github.com/biaochenxuying/blog-vue-typescript.git
# cd
cd  blog-vue-typescript
# install dependencies
npm install
# Compiles and hot-reloads for development
npm run serve
# Compiles and minifies for production
npm run build
### Run your tests
npm run test
### Lints and fixes files
npm run lint
### Run your unit tests
npm run test:unit

如果要看有后台数据完整的效果,是要和后台项目 blog-node 一起运行才行的,不然接口请求会失败。

虽然引入了 mock 了,但是还没有时间做模拟数据,想看具体效果,请稳步到我的网站上查看 https://biaochenxuying.cn

11. 项目地址与系列相关文章

基于 Vue + TypeScript + Element 的 blog-vue-typescript 前台展示: https://github.com/biaochenxuying/blog-vue-typescript

基于 react + node + express + ant + mongodb 的博客前台,这个是笔者之前做的,效果和这个类似,地址如下:
blog-react 前台展示: https://github.com/biaochenxuying/blog-react

推荐阅读 :

本博客系统的系列文章:

12. 最后

笔者也是初学 TS ,如果文章有错的地方,请指出,感谢。

一开始用 Vue + TS 来搭建时,我也是挺抵触的,因为踩了好多坑,而且很多类型检查方面也挺烦人。后面解决了,明白原理之后,是越用越爽,哈哈。

权衡

如何更好的利用 JS 的动态性和 TS 的静态特质,我们需要结合项目的实际情况来进行综合判断。一些建议:

  • 如果是中小型项目,且生命周期不是很长,那就直接用 JS 吧,不要被 TS 束缚住了手脚。
  • 如果是大型应用,且生命周期比较长,那建议试试 TS。
  • 如果是框架、库之类的公共模块,那更建议用 TS 了。

至于到底用不用TS,还是要看实际项目规模、项目生命周期、团队规模、团队成员情况等实际情况综合考虑。

其实本项目也是小项目来的,其实并不太适合加入 TypeScript ,不过这个项目是个人的项目,是为了练手用的,所以就无伤大大雅。

未来,class-compoent 也将成为主流,现在写 TypeScript 以后进行 3.0 的迁移会更加方便。

每天下班后,用几个晚上的时间来写这篇文章,码字不易,如果您觉得这篇文章不错或者对你有所帮助,请给个赞或者星吧,你的点赞就是我继续创作的最大动力。

参考文章:

  1. vue + typescript 项目起手式

  2. TypeScript + 大型项目实战

js 递归调用

程序员不止眼前的逻辑和代码,还有底层的框架与架构。

  1. 前言

最近在做一个复杂表格设计数据格式设置,其中用到了多叉树的原理,所以要用到递归来实现数据格式化。

  1. 递归的概念
    在程序中函数直接或间接调用自己

**注意:**使用递归函数一定要注意,处理不当就会进入死循环。递归函数只有在特定的情况下使用 ,比如阶乘问题。

  1. 例子

1. 一个阶乘的例子:

function fact(num) {
       if (num <= 1) {
                return 1;
       } else {
                return num * fact(num - 1);
       }
}
fact(3) // 结果为 6

以下代码可导致出错:

var anotherFact = fact; 
fact = null; 
alert(antherFact(4)); //出错 

由于fact已经不是函数了,所以出错。

使用arguments.callee
arguments.callee 是一个指向正在执行的函数的指针,arguments.callee 返回正在被执行的对现象。
新的函数为:

function fact(num){ 
    if (num<=1){ 
        return 1; 
    }else{ 
        return num*arguments.callee(num-1); //此处更改了。 
    } 
} 
var anotherFact = fact; 
fact = null; 
alert(antherFact(4)); //结果为24. 

2.再看一个多叉树的例子:

先看图
多叉树.png
数据结构格式,参考如下代码:

headerData: {
                name: '总数据',
                children: [
                    {
                        name: '数据1',
                        children: [
                            {
                                name: '数据11',
                                children: [
	                                {
	                                    name: '数据111',
	                                },
	                                {
	                                    name: '数据112',
	                                }
                                ]
                            },
                            {
                                name: '数据12',
                                children: [
	                                {
	                                    name: '数据121',
	                                },
	                                {
	                                    name: '数据122',
	                                }
                                ]
                            },
                            {
                                name: '数据13',
                                children: [
	                                {
	                                    name: '数据131',
	                                },
	                                {
	                                    name: '数据132',
	                                }
                                ]
                            },
                            {
                                name: '数据14',
                            },

                        ]
                    }
                ]
            }

叶子结点 就是度为0的结点 就是没有孩子结点的结点
简单的说就是一个二叉树任意一个分支上的终端节点
我们如何获取节点的所有叶子节点个数呢? 递归代码如下:

/**
 * 获取 节点的所有 叶子节点 个数
 * @param {Object} json Object对象
 */
function getLeafCountTree(json) {
  if(!json.children){
      return 1;
  }else{
      var leafCount = 0;
      for(var i = 0 ; i < json.children.length ; i++){
          leafCount = leafCount + getLeafCountTree(json.children[i]);
      }
      return leafCount;
  }
}

#最后

递归遍历是比较常用的方法,比如:省市区遍历成树、多叉树、阶乘等。
希望本文对你有点帮助。

JavaScript 数据结构与算法之美 - 线性表(数组、栈、队列、链表)

JavaScript 数据结构与算法之美

前言

  1. 基础知识就像是一座大楼的地基,它决定了我们的技术高度。
  2. 我们应该多掌握一些可移值的技术或者再过十几年应该都不会过时的技术,数据结构与算法就是其中之一。

栈、队列、链表、堆 是数据结构与算法中的基础知识,是程序员的地基。

笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。

1. 线性表与非线性表

线性表(Linear List):就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。数组、链表、队列、栈 等就是线性表结构。

线性表

非线性表:数据之间并不是简单的前后关系。二叉树、堆、图 就是非线性表。

非线性表

本文主要讲线性表,非线性表会在后面章节讲。

2. 数组

数组

定义

  • 数组 (Array) 是一个有序的数据集合,我们可以通过数组名称 (name) 和索引 (index) 进行访问。
  • 数组的索引是从 0 开始的。

特点

  • 数组是用一组连续的内存空间来存储的
    所以数组支持 随机访问,根据下标随机访问的时间复杂度为 O(1)。

  • 低效的插入和删除
    数组为了保持内存数据的连续性,会导致插入、删除这两个操作比较低效,因为底层通常是要进行大量的数据搬移来保持数据的连续性。
    插入与删除的时间复杂度如下:
    插入:从最好 O(1) ,最坏 O(n) ,平均 O(n)
    删除:从最好 O(1) ,最坏 O(n) ,平均 O(n)

注意

但是因为 JavaScript 是弱类型的语言,弱类型则允许隐式类型转换。

隐式:是指源码中没有明显的类型转换代码。也就是说,一个变量,可以赋值字符串,也可以赋值数值。

let str = "string"
str = 123 
console.log(str)  //   123

你还可以直接让字符串类型的变量和数值类型的变量相加,虽然得出的最终结果未必是你想象的那样,但一定不会报错。

let a = 123
let b = "456"
let c = a + b
// 数值加字符串,结果是字符串
console.log(c)  //   "123456"

数组的每一项可以是不同的类型,比如:

// 数组的类型有 数值、字符串,还可以随意变更类型
const arr = [ 12, 34, "abc" ]
arr[2] = { "key": "value" }  // 把数组的第二项变成对象
console.log(arr) //  [ 12, 34,  { "key": "value"} ]

定义的数组的大小是可变的,不像强类型语言,定义某个数组变量的时候就要定义该变量的大小。

const arr = [ 12, 34, "abc"] 
arr.push({ "key": "value" }) // 添加一项 对象
consolelog(arr) //  [ 12, 34, "abc", { "key": "value" } ]

实现

JavaScript 原生支持数组,而且提供了很多操作方法,这里不展开讲。

3. 栈

栈

定义

  1. 后进者先出,先进者后出,简称 后进先出(LIFO),这就是典型的结构。
  2. 新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端就叫栈底
  3. 在栈里,新元素都靠近栈顶,旧元素都接近栈底。
  4. 从栈的操作特性来看,是一种 操作受限的线性表,只允许在一端插入和删除数据。
  5. 不包含任何元素的栈称为空栈

栈也被用在编程语言的编译器和内存中保存变量、方法调用等,比如函数的调用栈。

实现

栈的方法:

  • push(element):添加一个(或几个)新元素到栈顶。
  • pop():移除栈顶的元素,同时返回被移除的元素。
  • peek():返回栈顶的元素,不对栈做任何修改。
  • isEmpty():如果栈里没有任何元素就返回 true,否则返回 false。
  • clear():移除栈里的所有元素。
  • size():返回栈里的元素个数。
// Stack类
function Stack() {
  this.items = [];

  // 添加新元素到栈顶
  this.push = function(element) {
    this.items.push(element);
  };
  // 移除栈顶元素,同时返回被移除的元素
  this.pop = function() {
    return this.items.pop();
  };
  // 查看栈顶元素
  this.peek = function() {
    return this.items[this.items.length - 1];
  };
  // 判断是否为空栈
  this.isEmpty = function() {
    return this.items.length === 0;
  };
  // 清空栈
  this.clear = function() {
    this.items = [];
  };
  // 查询栈的长度
  this.size = function() {
    return this.items.length;
  };
  // 打印栈里的元素
  this.print = function() {
    console.log(this.items.toString());
  };
}

测试:

// 创建Stack实例
var stack = new Stack();
console.log(stack.isEmpty()); // true
stack.push(5); // undefined
stack.push(8); // undefined
console.log(stack.peek()); // 8
stack.push(11); // undefined
console.log(stack.size()); // 3
console.log(stack.isEmpty()); // false
stack.push(15); // undefined
stack.pop(); // 15
console.log(stack.size()); // 3
stack.print(); // 5,8,11
stack.clear(); // undefined
console.log(stack.size()); // 0

栈的应用实例:JavaScript 数据结构与算法之美 - 实现一个前端路由,如何实现浏览器的前进与后退 ?

4. 队列

队列

普通队列

定义

  • 队列是遵循 FIFO(First In First Out,先进先出)原则的一组有序的项。
  • 队列在尾部添加新元素,并从顶部移除元素。
  • 最新添加的元素必须排在队列的末尾。
  • 队列只有 入队 push() 和出队 pop()。

实现

队列里面有一些声明的辅助方法:

  • enqueue(element):向队列尾部添加新项。
  • dequeue():移除队列的第一项,并返回被移除的元素。
  • front():返回队列中第一个元素,队列不做任何变动。
  • isEmpty():如果队列中不包含任何元素,返回 true,否则返回 false。
  • size():返回队列包含的元素个数,与数组的 length 属性类似。
  • print():打印队列中的元素。
  • clear():清空整个队列。

代码:

// Queue类
function Queue() {
	this.items = [];

	// 向队列尾部添加元素
	this.enqueue = function(element) {
		this.items.push(element);
	};

	// 移除队列的第一个元素,并返回被移除的元素
	this.dequeue = function() {
		return this.items.shift();
	};

	// 返回队列的第一个元素
	this.front = function() {
		return this.items[0];
	};

	// 判断是否为空队列
	this.isEmpty = function() {
		return this.items.length === 0;
	};

	// 获取队列的长度
	this.size = function() {
		return this.items.length;
	};

	// 清空队列
	this.clear = function() {
		this.items = [];
	};

	// 打印队列里的元素
	this.print = function() {
		console.log(this.items.toString());
	};
}

测试:

// 创建Queue实例
var queue = new Queue();
console.log(queue.isEmpty()); // true
queue.enqueue('John'); // undefined
queue.enqueue('Jack'); // undefined
queue.enqueue('Camila'); // undefined
queue.print(); // "John,Jack,Camila"
console.log(queue.size()); // 3
console.log(queue.isEmpty()); // false
queue.dequeue(); // "John"
queue.dequeue(); // "Jack"
queue.print(); // "Camila"
queue.clear(); // undefined
console.log(queue.size()); // 0

优先队列

定义

优先队列中元素的添加和移除是依赖优先级的。

应用

  • 一个现实的例子就是机场登机的顺序。头等舱和商务舱乘客的优先级要高于经济舱乘客。
  • 再比如:火车,老年人、孕妇和带小孩的乘客是享有优先检票权的。

优先队列分为两类

  • 最小优先队列
  • 最大优先队列

最小优先队列是把优先级的值最小的元素被放置到队列的最前面(代表最高的优先级)。
比如:有四个元素:"John", "Jack", "Camila", "Tom",他们的优先级值分别为 4,3,2,1。
那么最小优先队列排序应该为:"Tom","Camila","Jack","John"。

最大优先队列正好相反,把优先级值最大的元素放置在队列的最前面。
以上面的为例,最大优先队列排序应该为:"John", "Jack", "Camila", "Tom"。

实现

实现一个优先队列,有两种选项:

    1. 设置优先级,根据优先级正确添加元素,然后和普通队列一样正常移除
    1. 设置优先级,和普通队列一样正常按顺序添加,然后根据优先级移除

这里最小优先队列和最大优先队列我都采用第一种方式实现,大家可以尝试一下第二种。

下面只重写 enqueue() 方法和 print() 方法,其他方法和上面的普通队列完全相同。

实现最小优先队列

// 定义最小优先队列
function MinPriorityQueue () {
  this.items = [];

  this.enqueue = enqueue;
  this.dequeue = dequeue;
  this.front = front;
  this.isEmpty = isEmpty;
  this.size = size;
  this.clear = clear;
  this.print = print;
}

实现最小优先队列 enqueue() 方法和 print() 方法:

// 优先队列添加元素,要根据优先级判断在队列中的插入顺序
function enqueue (element, priority) {
  var queueElement = {
    element: element,
    priority: priority
  };

  if (this.isEmpty()) {
    this.items.push(queueElement);
  } else {
    var added = false;
    for (var i = 0; i < this.size(); i++) {
      if (queueElement.priority < this.items[i].priority) {
        this.items.splice(i, 0, queueElement);
        added = true;
        break ;
      }
    }

    if (!added) {
      this.items.push(queueElement);
    }
  }
}

// 打印队列里的元素
function print () {
  var strArr = [];

  strArr = this.items.map(function (item) {
    return `${item.element}->${item.priority}`;
  });

  console.log(strArr.toString());
}

最小优先队列测试:

// 创建最小优先队列minPriorityQueue实例
var minPriorityQueue = new MinPriorityQueue();

console.log(minPriorityQueue.isEmpty());     // true
minPriorityQueue.enqueue("John", 1);         // undefined
minPriorityQueue.enqueue("Jack", 3);         // undefined
minPriorityQueue.enqueue("Camila", 2);       // undefined
minPriorityQueue.enqueue("Tom", 3);          // undefined
minPriorityQueue.print();                    // "John->1,Camila->2,Jack->3,Tom->3"
console.log(minPriorityQueue.size());        // 4
console.log(minPriorityQueue.isEmpty());     // false
minPriorityQueue.dequeue();                  // {element: "John", priority: 1}
minPriorityQueue.dequeue();                  // {element: "Camila", priority: 2}
minPriorityQueue.print();                    // "Jack->3,Tom->3"
minPriorityQueue.clear();                    // undefined
console.log(minPriorityQueue.size());        // 0

实现最大优先队列

// 最大优先队列 MaxPriorityQueue 类
function MaxPriorityQueue () {
  this.items = [];

  this.enqueue = enqueue;
  this.dequeue = dequeue;
  this.front = front;
  this.isEmpty = isEmpty;
  this.size = size;
  this.clear = clear;
  this.print = print;
}

// 优先队列添加元素,要根据优先级判断在队列中的插入顺序
function enqueue (element, priority) {
  var queueElement = {
    element: element,
    priority: priority
  };

  if (this.isEmpty()) {
    this.items.push(queueElement);
  } else {
    var added = false;

    for (var i = 0; i < this.items.length; i++) {
      // 注意,只需要将这里改为大于号就可以了
      if (queueElement.priority > this.items[i].priority) {
        this.items.splice(i, 0, queueElement);
        added = true;
        break ;
      }
    }

    if (!added) {
      this.items.push(queueElement);
    }
  }
}

最大优先队列测试:

// 创建最大优先队列maxPriorityQueue实例
var maxPriorityQueue = new MaxPriorityQueue();

console.log(maxPriorityQueue.isEmpty());     // true
maxPriorityQueue.enqueue("John", 1);         // undefined
maxPriorityQueue.enqueue("Jack", 3);         // undefined
maxPriorityQueue.enqueue("Camila", 2);       // undefined
maxPriorityQueue.enqueue("Tom", 3);          // undefined
maxPriorityQueue.print();                    // "Jack->3,Tom->3,Camila->2,John->1"
console.log(maxPriorityQueue.size());        // 4
console.log(maxPriorityQueue.isEmpty());     // false
maxPriorityQueue.dequeue();                  // {element: "Jack", priority: 3}
maxPriorityQueue.dequeue();                  // {element: "Tom", priority: 3}
maxPriorityQueue.print();                    // "Camila->2,John->1"
maxPriorityQueue.clear();                    // undefined
console.log(maxPriorityQueue.size());        // 0

循环队列

定义

循环队列,顾名思义,它长得像一个环。把它想像成一个圆的钟就对了。

关键是:确定好队空和队满的判定条件。

循环队列的一个例子就是击鼓传花游戏(Hot Potato)。在这个游戏中,孩子们围城一个圆圈,击鼓的时候把花尽快的传递给旁边的人。某一时刻击鼓停止,这时花在谁的手里,谁就退出圆圈直到游戏结束。重复这个过程,直到只剩一个孩子(胜者)。

下面我们在普通队列的基础上,实现一个模拟的击鼓传花游戏,下面只写击鼓传花的代码片段:

// 实现击鼓传花
function hotPotato (nameList, num) {
  var queue = new Queue();

  for (var i = 0; i < nameList.length; i++) {
    queue.enqueue(nameList[i]);
  }

  var eliminated = '';

  while (queue.size() > 1) {
    // 循环 num 次,队首出来去到队尾
    for (var i = 0; i < num; i++) {
      queue.enqueue(queue.dequeue());
    }
    // 循环 num 次过后,移除当前队首的元素
    eliminated = queue.dequeue();
    console.log(`${eliminated} 在击鼓传花中被淘汰!`);
  }

  // 最后只剩一个元素
  return queue.dequeue();
}

// 测试
var nameList = ["John", "Jack", "Camila", "Ingrid", "Carl"];
var winner = hotPotato(nameList, 10);
console.log(`最后的胜利者是:${winner}`);

执行结果为:

// John 在击鼓传花中被淘汰!
// Ingrid 在击鼓传花中被淘汰! 
// Jack 在击鼓传花中被淘汰!
// Camila 在击鼓传花中被淘汰!
// 最后的胜利者是:Carl

队列小结

一些具有某些额外特性的队列,比如:循环队列、阻塞队列、并发队列。它们在很多偏底层系统、框架、中间件的开发中,起着关键性的作用。

以上队列的代码要感谢 leocoder351

5. 链表

定义

  • 链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的,它是通过 指针零散的内存块 串连起来的。
  • 每个元素由一个存储元素本身的 节点 和一个指向下一个元素的 引用(也称指针或链接)组成。

简单的链接结构图:

单链表结构图

其中,data 中保存着数据,next 保存着下一个链表的引用。
上图中,我们说 data2 跟在 data1 后面,而不是说 data2 是链表中的第二个元素。值得注意的是,我们将链表的尾元素指向了 null 节点,表示链接结束的位置。

特点

  • 链表是通过指针将零散的内存块串连起来的
    所以链表不支持 随机访问,如果要找特定的项,只能从头开始遍历,直到找到某个项。
    所以访问的时间复杂度为 O(n)。

  • 高效的插入和删除
    链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而搬移结点,因为链表的存储空间本身就不是连续的,只需要考虑相邻结点的指针改变。
    所以,在链表中插入和删除一个数据是非常快速的,时间复杂度为 O(1)。

三种最常见的链表结构,它们分别是:

  • 单链表
  • 双向链表
  • 循环链表

单链表

定义

单链表结构图

由于链表的起始点的确定比较麻烦,因此很多链表的实现都会在链表的最前面添加一个特殊的节点,称为 头节点,表示链表的头部。

经过改造,链表就成了如下的样子:

有头节点的链表

针对链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以插入与删除的时间复杂度为 O(1)。

在 d2 节点后面插入 d4 节点:

插入节点

删除 d4 节点:

删除节点

实现

  • Node 类用来表示节点。
  • LinkedList 类提供插入节点、删除节点等一些操作。

单向链表的八种常用操作:

  • append(element):尾部添加元素。
  • insert(position, element):特定位置插入一个新的项。
  • removeAt(position):特定位置移除一项。
  • remove(element):移除一项。
  • indexOf(element):返回元素在链表中的索引。如果链表中没有该元素则返回 -1。
  • isEmpty():如果链表中不包含任何元素,返回 true,如果链表长度大于 0,返回 false。
  • size():返回链表包含的元素个数,与数组的 length 属性类似。
  • getHead():返回链表的第一个元素。
  • toString():由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值。
  • print():打印链表的所有元素。

具体代码:

// 单链表
function SinglyLinkedList() {
	// 节点
	function Node(element) {
		this.element = element; // 当前节点的元素
		this.next = null; // 下一个节点指针
	}

	var length = 0; // 链表的长度
	var head = null; // 链表的头部节点

	// 向链表尾部添加一个新的节点
	this.append = function(element) {
		var node = new Node(element);
		var currentNode = head;

		// 判断是否为空链表
		if (head === null) {
			// 是空链表,就把当前节点作为头部节点
			head = node;
		} else {
			// 从 head 开始一直找到最后一个 node
			while (currentNode.next) {
				// 后面还有 node
				currentNode = currentNode.next;
			}
			// 把当前节点的 next 指针 指向 新的节点
			currentNode.next = node;
		}
		// 链表的长度加 1
		length++;
	};

	// 向链表特定位置插入一个新节点
	this.insert = function(position, element) {
		if (position < 0 || position > length) {
			// 越界
			return false;
		} else {
			var node = new Node(element);
			var index = 0;
			var currentNode = head;
			var previousNode;

			// 在最前插入节点
			if (position === 0) {
				node.next = currentNode;
				head = node;
			} else {
				// 循环找到位置
				while (index < position) {
					index++;
					previousNode = currentNode;
					currentNode = currentNode.next;
				}
				// 把前一个节点的指针指向新节点,新节点的指针指向当前节点,保持连接性
				previousNode.next = node;
				node.next = currentNode;
			}

			length++;

			return true;
		}
	};

	// 从链表的特定位置移除一项
	this.removeAt = function(position) {
		if ((position < 0 && position >= length) || length === 0) {
			// 越界
			return false;
		} else {
			var currentNode = head;
			var index = 0;
			var previousNode;

			if (position === 0) {
				head = currentNode.next;
			} else {
				// 循环找到位置
				while (index < position) {
					index++;
					previousNode = currentNode;
					currentNode = currentNode.next;
				}
				// 把当前节点的 next 指针 指向 当前节点的 next 指针,即是 删除了当前节点
				previousNode.next = currentNode.next;
			}

			length--;

			return true;
		}
	};

	// 从链表中移除指定项
	this.remove = function(element) {
		var index = this.indexOf(element);
		return this.removeAt(index);
	};

	// 返回元素在链表的索引,如果链表中没有该元素则返回 -1
	this.indexOf = function(element) {
		var currentNode = head;
		var index = 0;

		while (currentNode) {
			if (currentNode.element === element) {
				return index;
			}

			index++;
			currentNode = currentNode.next;
		}

		return -1;
	};

	// 如果链表中不包含任何元素,返回 true,如果链表长度大于 0,返回 false
	this.isEmpty = function() {
		return length === 0;
	};

	// 返回链表包含的元素个数,与数组的 length 属性类似
	this.size = function() {
		return length;
	};

	// 获取链表头部元素
	this.getHead = function() {
		return head.element;
	};

	// 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
	this.toString = function() {
		var currentNode = head;
		var string = '';

		while (currentNode) {
			string += ',' + currentNode.element;
			currentNode = currentNode.next;
		}

		return string.slice(1);
	};

	// 打印链表数据
	this.print = function() {
		console.log(this.toString());
	};

	// 获取整个链表
	this.list = function() {
		console.log('head: ', head);
		return head;
	};
}

测试:

// 创建单向链表实例
var singlyLinked = new SinglyLinkedList();
console.log(singlyLinked.removeAt(0)); // false
console.log(singlyLinked.isEmpty()); // true
singlyLinked.append('Tom');
singlyLinked.append('Peter');
singlyLinked.append('Paul');
singlyLinked.print(); // "Tom,Peter,Paul"
singlyLinked.insert(0, 'Susan');
singlyLinked.print(); // "Susan,Tom,Peter,Paul"
singlyLinked.insert(1, 'Jack');
singlyLinked.print(); // "Susan,Jack,Tom,Peter,Paul"
console.log(singlyLinked.getHead()); // "Susan"
console.log(singlyLinked.isEmpty()); // false
console.log(singlyLinked.indexOf('Peter')); // 3
console.log(singlyLinked.indexOf('Cris')); // -1
singlyLinked.remove('Tom');
singlyLinked.removeAt(2);
singlyLinked.print(); // "Susan,Jack,Paul"
singlyLinked.list(); // 具体控制台

整个链表数据在 JavaScript 里是怎样的呢 ?

为了看这个数据,特意写了个 list 函数:

// 获取整个链表
	this.list = function() {
		console.log('head: ', head);
		return head;
	};

重点上上面的最后一行代码: singlyLinked.list() ,打印的数据如下:

所以,在 JavaScript 中,单链表的真实数据有点类似于对象,实际上是 Node 类生成的实例。

双向链表

单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。
而双向链表,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。

双向链表

插入

删除

单向链表与又向链表比较

  • 双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。
    所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。
    虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。
  • 双向链表提供了两种迭代列表的方法:从头到尾,或者从尾到头
    我们可以访问一个特定节点的下一个或前一个元素。
  • 在单向链表中,如果迭代链表时错过了要找的元素,就需要回到链表起点,重新开始迭代。
  • 在双向链表中,可以从任一节点,向前或向后迭代,这是双向链表的一个优点。
  • 所以,双向链表可以支持 O(1) 时间复杂度的情况下找到前驱结点,正是这样的特点,也使双向链表在某些情况下的插入、删除等操作都要比单链表简单、高效。

实现

具体代码:

// 创建双向链表 DoublyLinkedList 类
function DoublyLinkedList() {
  function Node(element) {
    this.element = element; //当前节点的元素
    this.next = null; //下一个节点指针
    this.previous = null; //上一个节点指针
  }

  var length = 0; // 链表长度
  var head = null; // 链表头部
  var tail = null; // 链表尾部

  // 向链表尾部添加一个新的项
  this.append = function(element) {
    var node = new Node(element);
    var currentNode = tail;

    // 判断是否为空链表
    if (currentNode === null) {
      // 空链表
      head = node;
      tail = node;
    } else {
      currentNode.next = node;
      node.prev = currentNode;
      tail = node;
    }

    length++;
  };

  // 向链表特定位置插入一个新的项
  this.insert = function(position, element) {
    if (position < 0 || position > length) {
      // 越界
      return false;
    } else {
      var node = new Node(element);
      var index = 0;
      var currentNode = head;
      var previousNode;

      if (position === 0) {
        if (!head) {
          head = node;
          tail = node;
        } else {
          node.next = currentNode;
          currentNode.prev = node;
          head = node;
        }
      } else if (position === length) {
        this.append(element);
      } else {
        while (index < position) {
          index++;
          previousNode = currentNode;
          currentNode = currentNode.next;
        }

        previousNode.next = node;
        node.next = currentNode;

        node.prev = previousNode;
        currentNode.prev = node;
      }

      length++;

      return true;
    }
  };

  // 从链表的特定位置移除一项
  this.removeAt = function(position) {
    if ((position < 0 && position >= length) || length === 0) {
      // 越界
      return false;
    } else {
      var currentNode = head;
      var index = 0;
      var previousNode;

      if (position === 0) {
        // 移除第一项
        if (length === 1) {
          head = null;
          tail = null;
        } else {
          head = currentNode.next;
          head.prev = null;
        }
      } else if (position === length - 1) {
        // 移除最后一项
        if (length === 1) {
          head = null;
          tail = null;
        } else {
          currentNode = tail;
          tail = currentNode.prev;
          tail.next = null;
        }
      } else {
        while (index < position) {
          index++;
          previousNode = currentNode;
          currentNode = currentNode.next;
        }
        previousNode.next = currentNode.next;
        previousNode = currentNode.next.prev;
      }

      length--;

      return true;
    }
  };

  // 从链表中移除指定项
  this.remove = function(element) {
    var index = this.indexOf(element);
    return this.removeAt(index);
  };

  // 返回元素在链表的索引,如果链表中没有该元素则返回 -1
  this.indexOf = function(element) {
    var currentNode = head;
    var index = 0;

    while (currentNode) {
      if (currentNode.element === element) {
        return index;
      }

      index++;
      currentNode = currentNode.next;
    }

    return -1;
  };

  // 如果链表中不包含任何元素,返回 true ,如果链表长度大于 0 ,返回 false
  this.isEmpty = function() {
    return length == 0;
  };

  // 返回链表包含的元素个数,与数组的 length 属性类似
  this.size = function() {
    return length;
  };

  // 获取链表头部元素
  this.getHead = function() {
    return head.element;
  };

  // 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
  this.toString = function() {
    var currentNode = head;
    var string = '';

    while (currentNode) {
      string += ',' + currentNode.element;
      currentNode = currentNode.next;
    }

    return string.slice(1);
  };

  this.print = function() {
    console.log(this.toString());
  };

  // 获取整个链表
  this.list = function() {
    console.log('head: ', head);
    return head;
  };
}

测试:

// 创建双向链表
var doublyLinked = new DoublyLinkedList();
console.log(doublyLinked.isEmpty()); // true
doublyLinked.append('Tom');
doublyLinked.append('Peter');
doublyLinked.append('Paul');
doublyLinked.print(); // "Tom,Peter,Paul"
doublyLinked.insert(0, 'Susan');
doublyLinked.print(); // "Susan,Tom,Peter,Paul"
doublyLinked.insert(1, 'Jack');
doublyLinked.print(); // "Susan,Jack,Tom,Peter,Paul"
console.log(doublyLinked.getHead()); // "Susan"
console.log(doublyLinked.isEmpty()); // false
console.log(doublyLinked.indexOf('Peter')); // 3
console.log(doublyLinked.indexOf('Cris')); // -1
doublyLinked.remove('Tom');
doublyLinked.removeAt(2);
doublyLinked.print(); // "Susan,Jack,Paul"
doublyLinked.list(); // 请看控制台输出

整个链表数据在 JavaScript 里是怎样的呢 ?

// 获取整个链表
  this.list = function() {
    console.log('head: ', head);
    return head;
  };

调用 doublyLinked.list(); .

控制台输出如下:

链表代码实现的关键是弄清楚:前节点与后节点与边界。

循环链表

循环链表是一种特殊的单链表。
循环链表和单链表相似,节点类型都是一样。
唯一的区别是,在创建循环链表的时候,让其头节点的 next 属性指向它本身
即:

head.next = head;

这种行为会导致链表中每个节点的 next 属性都指向链表的头节点,换句话说,也就是链表的尾节点指向了头节点,形成了一个循环链表。如下图所示:

循环链表

循环链表:在单链表的基础上,将尾节点的指针指向头结点,就构成了一个循环链表。环形链表从任意一个节点开始,都可以遍历整个链表。

代码:

// 循环链表
function CircularLinkedList() {
	// 节点
	function Node(element) {
		this.element = element; // 当前节点的元素
		this.next = null; // 下一个节点指针
	}

	var length = 0,
		head = null;

	this.append = function(element) {
		var node = new Node(element),
			current;

		if (!head) {
			head = node;
			// 头的指针指向自己
			node.next = head;
		} else {
			current = head;

			while (current.next !== head) {
				current = current.next;
			}

			current.next = node;
			// 最后一个节点指向头节点
			node.next = head;
		}

		length++;
		return true;
	};

	this.insert = function(position, element) {
		if (position > -1 && position < length) {
			var node = new Node(element),
				index = 0,
				current = head,
				previous;

			if (position === 0) {
				// 头节点指向自己
				node.next = head;
				head = node;
			} else {
				while (index++ < position) {
					previous = current;
					current = current.next;
				}
				previous.next = node;
				node.next = current;
			}
			length++;
			return true;
		} else {
			return false;
		}
	};
	this.removeAt = function(position) {
		if (position > -1 && position < length) {
			var current = head,
				previous,
				index = 0;
			if (position === 0) {
				head = current.next;
			} else {
				while (index++ < position) {
					previous = current;
					current = current.next;
				}
				previous.next = current.next;
			}
			length--;
			return current.element;
		} else {
			return false;
		}
	};
	this.remove = function(element) {
		var current = head,
			previous,
			indexCheck = 0;
		while (current && indexCheck < length) {
			if (current.element === element) {
				if (indexCheck == 0) {
					head = current.next;
					length--;
					return true;
				} else {
					previous.next = current.next;
					length--;
					return true;
				}
			} else {
				previous = current;
				current = current.next;
				indexCheck++;
			}
		}
		return false;
	};
	this.remove = function() {
		if (length === 0) {
			return false;
		}
		var current = head,
			previous,
			indexCheck = 0;
		if (length === 1) {
			head = null;
			length--;
			return current.element;
		}
		while (indexCheck++ < length) {
			previous = current;
			current = current.next;
		}
		previous.next = head;
		length--;
		return current.element;
	};
	this.indexOf = function(element) {
		var current = head,
			index = 0;
		while (current && index < length) {
			if (current.element === element) {
				return index;
			} else {
				index++;
				current = current.next;
			}
		}
		return -1;
	};
	this.isEmpty = function() {
		return length === 0;
	};
	this.size = function() {
		return length;
	};

	// 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
	this.toString = function() {
		var current = head,
			string = '',
			indexCheck = 0;
		while (current && indexCheck < length) {
			string += ',' + current.element;
			current = current.next;
			indexCheck++;
		}
		return string.slice(1);
	};

	// 获取链表头部元素
	this.getHead = function() {
		return head.element;
	};

	// 打印链表数据
	this.print = function() {
		console.log(this.toString());
	};

	// 获取整个链表
	this.list = function() {
		console.log('head: ', head);
		return head;
	};
}

测试:

// 创建单向链表实例
var circularLinked = new CircularLinkedList();
console.log(circularLinked.removeAt(0)); // false
console.log(circularLinked.isEmpty()); // true
circularLinked.append('Tom');
circularLinked.append('Peter');
circularLinked.append('Paul');
circularLinked.print(); // "Tom,Peter,Paul"
circularLinked.insert(0, 'Susan');
circularLinked.print(); // "Susan,Tom,Peter,Paul"
circularLinked.insert(1, 'Jack');
circularLinked.print(); // "Susan,Jack,Tom,Peter,Paul"
console.log(circularLinked.getHead()); // "Susan"
console.log(circularLinked.isEmpty()); // false
console.log(circularLinked.indexOf('Peter')); // 3
console.log(circularLinked.indexOf('Cris')); // -1
circularLinked.remove('Tom');
circularLinked.removeAt(2);
circularLinked.print(); // "Susan,Jack,Paul"
circularLinked.list(); // 具体控制台

整个链表数据在 JavaScript 里是怎样的呢 ?

// 获取整个链表
  this.list = function() {
    console.log('head: ', head);
    return head;
  };

调用 circularLinked.list() 。

控制台输出如下:

你知道大家发现没有,为什么从 1 - 4 - 1 了,还有 next 节点,而且是还可以一直点 next ,重复的展开下去,这正是 循环 的原因。

链表总结

  • 写链表代码是最考验逻辑思维能力的,要熟练链表,只有 多写多练,没有捷径
  • 因为,链表代码到处都是指针的操作、边界条件的处理,稍有不慎就容易产生 Bug。
  • 链表代码写得好坏,可以看出一个人写代码是否够细心,考虑问题是否全面,思维是否缜密。
  • 所以,这也是很多面试官喜欢让人手写链表代码的原因。
  • 一定要自己写代码实现一下,才有效果。

6. 文章输出计划

JavaScript 数据结构与算法之美 的系列文章,坚持 3 - 7 天左右更新一篇,暂定计划如下表。

标题 链接
时间和空间复杂度 #29
线性表(数组、链表、栈、队列) #34
实现一个前端路由,如何实现浏览器的前进与后退 ? #30
栈内存与堆内存 、浅拷贝与深拷贝 精彩待续
非线性表(树、堆) 精彩待续
递归 精彩待续
冒泡排序 精彩待续
插入排序 精彩待续
选择排序 精彩待续
归并排序 精彩待续
快速排序 精彩待续
计数排序 精彩待续
基数排序 精彩待续
桶排序 精彩待续
希尔排序 精彩待续
堆排序 精彩待续
十大经典排序汇总 精彩待续

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。

7. 最后

文章中的代码已经全部放在了我的 github 上,如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

关注我的公众号,第一时间接收最新的精彩博文。

文章可以转载,但须注明作者及出处,需要转载到公众号的,喊我加下白名单就行了。

参考文章:

数组:为什么很多编程语言中数组都从 0 开始编号?
JS中的算法与数据结构——链表(Linked-list)
JavaScript数据结构 03 - 队列
链表(上):如何实现 LRU 缓存淘汰算法?
JavaScript数据结构——队列

笔芯

react + node + express + ant + mongodb 的简洁兼时尚的博客网站

首页

前言

此项目是用于构建博客网站的,由三部分组成,包含前台展示、管理后台和后端。

此项目是基于 react + node + express + ant + mongodb 的,项目已经开源,项目地址在 github 上,喜欢的,欢迎给个 star 。

项目地址:

前台展示: https://github.com/biaochenxuying/blog-react
管理后台:https://github.com/biaochenxuying/blog-react-admin
后端:https://github.com/biaochenxuying/blog-node

1. 效果图

1.1 前台展示

首页

标签分类文章

留言

时间轴

登录

注册

文章详情-1

文章详情-2

文章详情-3

点赞与评论

前台展示目前只支持 pc 端。

1.2 管理后台

管理后台是在蚂蚁金服用户开源的 ANT DESIGN PRO 基础上进行开发的。

登录

用户

文章

文章添加

留言

留言回复

链接

时间轴

时间轴添加

标签

分类

2. 体验地址

3. 计划

这次是一个完整的全栈式开发,只要部署了这三个项目的代码,是完全可以搭建好博客网站的。

作为一个后端的小白,在这次开发中,小汪也遇到了很多问题。

往后的时间里,我会就这三个项目,推出相应的三篇文章教程或者说明和踩到的坑,敬请期待。

4. 收获与感触

学而不用,基本等于没学,所以为了有 react 相关的技术栈的实战经验,所以用了 react ,而且后端技术 node.js 和 mongodb 也是这一个多月里现学现用的,所以项目中肯定还有很多我不知道的实用技巧,如果写的不好的地方,请大家指出。

网站前端部分如果用 vue 相关技术栈来完成的话,会更好更快,因为本人专长的是 vue 相关的技术栈。

因为最近一直在做自己的个人博客网站,所以好久没更新技术文章了;而且是利用业余时间做的,所以经过差不多两个月的搬砖,现在网站终于都上线了。

开发网站的这段时间里,每天晚上几乎都搬砖到接近 11 点,周末的时间大多也在搬砖,今晚写完这篇文章,也快 12 点了,搬砖不易啊,喜欢或者觉得不错的,欢迎到 github 上给个 star,谢谢。

5. 文档教程

项目地址:

前台展示: https://github.com/biaochenxuying/blog-react

管理后台:https://github.com/biaochenxuying/blog-react-admin

后端:https://github.com/biaochenxuying/blog-node

blog:https://github.com/biaochenxuying/blog

本博客系统的系列文章:

前端解决第三方图片防盗链的办法 - html referrer 访问图片资源403问题

问题

笔者网站的图片都是上传到第三方网站上的,比如 简书、掘金、七牛云上的,但是最近简书和掘金都开启了 防盗链,防止其他网站访问他们网站上的图片了,导致笔者的网站存在他们网站上的图片全挂了。

具体问题,就是 html 中通过 img 标签引入一个第三方的图片地址,报 403 。但是这个图片地址直接复制出来在地址栏打开,却是看得到的。

原因

官方输出图片的时候,判断了来源 Referer ,就是从哪个网站访问这个图片,如果是你的网站去加载这个图片,那么 Referer 就是:你的网站地址;

如果我们的网站地址不在官方的白名单内,所以就看不到图片了。

我们做这个跳板的关键:不发送 Referer,也就是没有来源。那么官方那边,就认为是从浏览器直接访问的,所以就能加载正常的图片了。

referrer

在某些情况下,出于一些原因,网站想要控制页面发送给 server 的 referrer 信息的情况下,可以使用这一 referer metadata 参数。

参数

referer 的 metedata 属性可设置 content 属性值为以下集合:

  • never
  • always
  • origin

结果

  • 如果 referer-policy 的值为 never:删除 http head 中的 referer;
  • 如果 referer-policy 的值为 default:如果当前页面使用的是 https 协议,而正要加载资源使用的是普通的 http 协议,则将 http header 中额 referer 置为空;
  • 如果 referer-policy 的值 origin:只发送 origin 部分;
  • 如果 referer-policy 的值为 always:不改变 http header 中的 referer 的值;

举例

如果页面中包含了如下 meta 标签,所有从当前页面中发起的请求将不会携带 referer:

<meta name="referrer" content="never">

如果页面中包含了如下 meta 标签,则从当前页面中发起的 http 请求将只携带 origin 部分:

<meta name="referrer" content="origin">

解决方案

初步方案

  • 在 标签里加 meta,referrer 的 content 设置为 nerver
<meta name="referrer" content="never">

这样存在第三方网站上的图片,在你的网站上就可以访问了。

但是还有一个问题,就是如果你的网站需要发送你的网站地址的,那上面的的设置就不行了,比如:用到了百度统计。

那上面的设置会导致百度统计的代码加载不了,因为它需要发送你的网站地址给百度统计。

既要不发送 你的网站地址,又要发送你的网站地址,那么怎么办呢 ?

最终的解决方案

  • 先在 html 上设置 referrer 为 always
<meta id="referrer" name="referrer" content="always" />

这样之后,首屏加载的时候,加载了百度统计的代码了,能正常统计访客数据了。

  • 不需要网站地址的时候,再把 referrer 设置为 nerver

加个延时 setTimeout 再把 referrer 的 content 值设置为 nerver 。
或者 在有图片的地方再把 referrer 的 content 值设置为 nerver 。

const referrer  = document.getElementById("referrer");
referrer.setAttribute("content", "never")

这样就能解决第三方图片防盗链,又能用到百度统计了。

最后

笔者博客首更地址 :https://github.com/biaochenxuying/blog

希望:大家不要恶意盗用、滥用第三方的 CDN 资源行为。

比如:掘金 CDN 本是一件公益性质的社区服务,为便大家在自己的技术博客中使用掘金 CDN 的图片,并没有开启防盗链。

但是就是因为某些人恶意盗用、滥用第三方的 CDN 资源,所以掘金社区不得不开启防盗链来减少损失和规避风险 https://juejin.im/post/5cefb6a3f265da1b95703b9d

参考文章:

  1. 微信图片防盗链解决办法
  2. Meta referrer标签的简要介绍

重磅:GitHub 上 100K+ Star 的前端面试开源项目汇总(进大厂必备)

复习前端面试的知识,是为了巩固前端的基础知识,最重要的还是平时的积累!

开源项目

最后

笔者 GitHub

觉得有用 ?那就收藏,顺便点个赞吧,你的支持是我最大的鼓励 !

基于 node express mongodb 的 blog-node 项目文档说明

项目结构图

前言

blog-node 是采用了主流的前后端分离**的,主里只讲 后端。

blog-node 项目是 node + express + mongodb 的进行开发的,项目已经开源,项目地址在 github 上。

效果请看 http://biaochenxuying.cn/main.html

1. 后端

1.1 已经实现功能

  • 登录
  • 文章管理
  • 标签管理
  • 评论
  • 留言管理
  • 用户管理
  • 友情链接管理
  • 时间轴管理
  • 身份验证

1.2 待实现功能

  • 点赞、留言和评论 的通知管理
  • 个人中心(用来设置博主的各种信息)
  • 工作台( 接入百度统计接口,查看网站浏览量和用户访问等数据 )

2. 技术

  • node
  • cookie-parser : "~1.4.3"
  • crypto : "^1.0.1"
  • express: "~4.16.0"
  • express-session : "^1.15.6",
  • http-errors : "~1.6.2",
  • mongodb : "^3.1.8",
  • mongoose : "^5.3.7",
  • mongoose-auto-increment : "^5.0.1",
  • yargs : "^12.0.2"

3. 主文件 app.js

// modules
const createError = require('http-errors');
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const session = require('express-session');

// import 等语法要用到 babel 支持
require('babel-register');

const app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));
app.use(cookieParser('blog_node_cookie'));
app.use(
	session({
		secret: 'blog_node_cookie',
		name: 'session_id', //# 在浏览器中生成cookie的名称key,默认是connect.sid
		resave: true,
		saveUninitialized: true,
		cookie: { maxAge: 60 * 1000 * 30, httpOnly: true }, //过期时间
	}),
);

const mongodb = require('./core/mongodb');

// data server
mongodb.connect();

//将路由文件引入
const route = require('./routes/index');

//初始化所有路由
route(app);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
	next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
	// set locals, only providing error in development
	res.locals.message = err.message;
	res.locals.error = req.app.get('env') === 'development' ? err : {};

	// render the error page
	res.status(err.status || 500);
	res.render('error');
});

module.exports = app;

4. 数据库 core/mongodb.js

/**
 * Mongoose module.
 * @file 数据库模块
 * @module core/mongoose
 * @author  biaochenxuying <https://github.com/biaochenxuying>
 */

const consola = require('consola')
const CONFIG = require('../app.config.js')
const mongoose = require('mongoose')
const autoIncrement = require('mongoose-auto-increment')

// remove DeprecationWarning
mongoose.set('useFindAndModify', false)


// mongoose Promise
mongoose.Promise = global.Promise

// mongoose
exports.mongoose = mongoose

// connect
exports.connect = () => {

	// 连接数据库
	mongoose.connect(CONFIG.MONGODB.uri, {
		useCreateIndex: true,
		useNewUrlParser: true,
		promiseLibrary: global.Promise
	})

	// 连接错误
	mongoose.connection.on('error', error => {
		consola.warn('数据库连接失败!', error)
	})

	// 连接成功
	mongoose.connection.once('open', () => {
		consola.ready('数据库连接成功!')
	})

	// 自增 ID 初始化
	autoIncrement.initialize(mongoose.connection)
	
	// 返回实例
	return mongoose
}

5. 数据模型 Model

这里只介绍 用户、文章和评论 的模型。

5.1 用户

用户的字段都有设置类型 type,大多都设置了默认值 default ,邮箱设置了验证规则 validate,密码保存用了 crypto 来加密。

用了中间件自增 ID 插件 mongoose-auto-increment。

/**
 * User model module.
 * @file 权限和用户数据模型
 * @module model/user
 * @author biaochenxuying <https://github.com/biaochenxuying>
 */

const crypto = require('crypto');
const { argv } = require('yargs');
const { mongoose } = require('../core/mongodb.js');
const autoIncrement = require('mongoose-auto-increment');

const adminSchema = new mongoose.Schema({
	// 名字
	name: { type: String, required: true, default: '' },

	// 用户类型 0:博主 1:其他用户
	type: { type: Number, default: 1 },

	// 手机
	phone: { type: String, default: '' },

	//封面
	img_url: { type: String, default: '' },

	// 邮箱
	email: { type: String, required: true, validate: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/ },

	// 个人介绍
	introduce: { type: String, default: '' },

	// 头像
	avatar: { type: String, default: 'user' },

	// 密码
	password: {
		type: String,
		required: true,
		default: crypto
			.createHash('md5')
			.update(argv.auth_default_password || 'root')
			.digest('hex'),
	},

	// 创建日期
	create_time: { type: Date, default: Date.now },

	// 最后修改日期
	update_time: { type: Date, default: Date.now },
});

// 自增 ID 插件配置
adminSchema.plugin(autoIncrement.plugin, {
	model: 'User',
	field: 'id',
	startAt: 1,
	incrementBy: 1,
});

module.exports = mongoose.model('User', adminSchema);

5.2 文章

文章是分类型的:文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍
而且简历和管理员介绍的文章只能是各自一篇(因为前台展示那里有个导航 关于我 ,就是请求管理员介绍这篇文章的,简历也是打算这样子用的),普通文章可以是无数篇。

点赞的用户 like_users 那里应该只保存用户 id 的,这个后面修改一下。

/**
 * Article model module.
 * @file 文章数据模型
 * @module model/article
 * @author biaochenxuying <https://github.com/biaochenxuying>
 */

const { mongoose } = require('../core/mongodb.js');
const autoIncrement = require('mongoose-auto-increment');

// 文章模型
const articleSchema = new mongoose.Schema({
	// 文章标题
	title: { type: String, required: true, validate: /\S+/ },

	// 文章关键字(SEO)
	keyword: [{ type: String, default: '' }],

	// 作者
	author: { type: String, required: true, validate: /\S+/ },

	// 文章描述
	desc: { type: String, default: '' },

	// 文章内容
	content: { type: String, required: true, validate: /\S+/ },

	// 字数
	numbers: { type: String, default: 0 },

	// 封面图
	img_url: { type: String, default: 'https://upload-images.jianshu.io/upload_images/12890819-80fa7517ab3f2783.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240' },

	// 文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍
	type: { type: Number, default: 1 },

	// 文章发布状态 => 0 草稿,1 已发布
	state: { type: Number, default: 1 },

	// 文章转载状态 => 0 原创,1 转载,2 混合
	origin: { type: Number, default: 0 },

	// 文章标签
	tags: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Tag', required: true }],

	comments: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Comment', required: true }],

	// 文章分类
	category: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Category', required: true }],

	// 点赞的用户
	like_users: [
		{
			// 用户id
			id: { type: mongoose.Schema.Types.ObjectId },

			// 名字
			name: { type: String, required: true, default: '' },

			// 用户类型 0:博主 1:其他用户
			type: { type: Number, default: 1 },

			// 个人介绍
			introduce: { type: String, default: '' },

			// 头像
			avatar: { type: String, default: 'user' },

			// 创建日期
			create_time: { type: Date, default: Date.now },
		},
	],

	// 其他元信息
	meta: {
		views: { type: Number, default: 0 },
		likes: { type: Number, default: 0 },
		comments: { type: Number, default: 0 },
	},

	// 创建日期
	create_time: { type: Date, default: Date.now },

	// 最后修改日期
	update_time: { type: Date, default: Date.now },
});

// 自增 ID 插件配置
articleSchema.plugin(autoIncrement.plugin, {
	model: 'Article',
	field: 'id',
	startAt: 1,
	incrementBy: 1,
});

// 文章模型
module.exports = mongoose.model('Article', articleSchema);

5.3 评论

评论功能是实现了简单的三级评论的,第三者的评论(就是别人对一级评论进行再评论)放在 other_comments 里面。

/**
 * Comment model module.
 * @file 评论数据模型
 * @module model/comment
 * @author biaochenxuying <https://github.com/biaochenxuying>
 */

const { mongoose } = require('../core/mongodb.js');
const autoIncrement = require('mongoose-auto-increment');

// 评论模型
const commentSchema = new mongoose.Schema({
	// 评论所在的文章 id
	article_id: { type: mongoose.Schema.Types.ObjectId, required: true },

	// content
	content: { type: String, required: true, validate: /\S+/ },

	// 是否置顶
	is_top: { type: Boolean, default: false },

	// 被赞数
	likes: { type: Number, default: 0 },

	user_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },

	// 父评论的用户信息
	user: {
		// 用户id
		user_id: { type: mongoose.Schema.Types.ObjectId },

		// 名字
		name: { type: String, required: true, default: '' },

		// 用户类型 0:博主 1:其他用户
		type: { type: Number, default: 1 },

		// 头像
		avatar: { type: String, default: 'user' },
	},

	// 第三者评论
	other_comments: [
		{
			user: {
				id: { type: mongoose.Schema.Types.ObjectId },

				// 名字
				name: { type: String, required: true, default: '' },

				// 用户类型 0:博主 1:其他用户
				type: { type: Number, default: 1 },
			},

			// content
			content: { type: String, required: true, validate: /\S+/ },

			// 状态 => 0 待审核 / 1 通过正常 / -1 已删除 / -2 垃圾评论
			state: { type: Number, default: 1 },

			// 创建日期
			create_time: { type: Date, default: Date.now },
		},
	],

	// 状态 => 0 待审核 / 1 通过正常 / -1 已删除 / -2 垃圾评论
	state: { type: Number, default: 1 },

	// 创建日期
	create_time: { type: Date, default: Date.now },

	// 最后修改日期
	update_time: { type: Date, default: Date.now },
});

// 自增 ID 插件配置
commentSchema.plugin(autoIncrement.plugin, {
	model: 'Comment',
	field: 'id',
	startAt: 1,
	incrementBy: 1,
});

// 标签模型
module.exports = mongoose.model('Comment', commentSchema);

其他模块的具体需求,都是些常用的逻辑可以实现的,也很简单,这里就不展开讲了。

6. 路由接口 routes

6.1 主文件

/*
*所有的路由接口
*/
const user = require('./user');
const article = require('./article');
const comment = require('./comment');
const message = require('./message');
const tag = require('./tag');
const link = require('./link');
const category = require('./category');
const timeAxis = require('./timeAxis');

module.exports = app => {
	app.post('/login', user.login);
	app.post('/logout', user.logout);
	app.post('/loginAdmin', user.loginAdmin);
	app.post('/register', user.register);
	app.post('/delUser', user.delUser);
	app.get('/currentUser', user.currentUser);
	app.get('/getUserList', user.getUserList);

	app.post('/addComment', comment.addComment);
	app.post('/addThirdComment', comment.addThirdComment);
	app.post('/changeComment', comment.changeComment);
	app.post('/changeThirdComment', comment.changeThirdComment);
	app.get('/getCommentList', comment.getCommentList);

	app.post('/addArticle', article.addArticle);
	app.post('/updateArticle', article.updateArticle);
	app.post('/delArticle', article.delArticle);
	app.get('/getArticleList', article.getArticleList);
	app.get('/getArticleListAdmin', article.getArticleListAdmin);
	app.post('/getArticleDetail', article.getArticleDetail);
	app.post('/likeArticle', article.likeArticle);

	app.post('/addTag', tag.addTag);
	app.post('/delTag', tag.delTag);
	app.get('/getTagList', tag.getTagList);

	app.post('/addMessage', message.addMessage);
	app.post('/addReplyMessage', message.addReplyMessage);
	app.post('/delMessage', message.delMessage);
	app.post('/getMessageDetail', message.getMessageDetail);
	app.get('/getMessageList', message.getMessageList);

	app.post('/addLink', link.addLink);
	app.post('/updateLink', link.updateLink);
	app.post('/delLink', link.delLink);
	app.get('/getLinkList', link.getLinkList);

	app.post('/addCategory', category.addCategory);
	app.post('/delCategory', category.delCategory);
	app.get('/getCategoryList', category.getCategoryList);

	app.post('/addTimeAxis', timeAxis.addTimeAxis);
	app.post('/updateTimeAxis', timeAxis.updateTimeAxis);
	app.post('/delTimeAxis', timeAxis.delTimeAxis);
	app.get('/getTimeAxisList', timeAxis.getTimeAxisList);
	app.post('/getTimeAxisDetail', timeAxis.getTimeAxisDetail);
};

6.2 文章

各模块的列表都是用了分页的形式的。

import Article from '../models/article';
import User from '../models/user';
import { responseClient, timestampToTime } from '../util/util';

exports.addArticle = (req, res) => {
	// if (!req.session.userInfo) {
	// 	responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
	// 	return;
	// }
	const { title, author, keyword, content, desc, img_url, tags, category, state, type, origin } = req.body;
	let tempArticle = null
	if(img_url){
		tempArticle = new Article({
			title,
			author,
			keyword: keyword ? keyword.split(',') : [],
			content,
			numbers: content.length,
			desc,
			img_url,
			tags: tags ? tags.split(',') : [],
			category: category ? category.split(',') : [],
			state,
			type,
			origin,
		});
	}else{
		tempArticle = new Article({
			title,
			author,
			keyword: keyword ? keyword.split(',') : [],
			content,
			numbers: content.length,
			desc,
			tags: tags ? tags.split(',') : [],
			category: category ? category.split(',') : [],
			state,
			type,
			origin,
		});
	}
	
	tempArticle
		.save()
		.then(data => {
			responseClient(res, 200, 0, '保存成功', data);
		})
		.catch(err => {
			console.log(err);
			responseClient(res);
		});
};

exports.updateArticle = (req, res) => {
	// if (!req.session.userInfo) {
	// 	responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
	// 	return;
	// }
	const { title, author, keyword, content, desc, img_url, tags, category, state, type, origin, id } = req.body;
	Article.update(
		{ _id: id },
		{
			title,
			author,
			keyword: keyword ? keyword.split(','): [],
			content,
			desc,
			img_url,
			tags: tags ? tags.split(',') : [],
			category:category ? category.split(',') : [],
			state,
			type,
			origin,
		},
	)
		.then(result => {
			responseClient(res, 200, 0, '操作成功', result);
		})
		.catch(err => {
			console.error(err);
			responseClient(res);
		});
};

exports.delArticle = (req, res) => {
	let { id } = req.body;
	Article.deleteMany({ _id: id })
		.then(result => {
			if (result.n === 1) {
				responseClient(res, 200, 0, '删除成功!');
			} else {
				responseClient(res, 200, 1, '文章不存在');
			}
		})
		.catch(err => {
			console.error('err :', err);
			responseClient(res);
		});
};

// 前台文章列表
exports.getArticleList = (req, res) => {
	let keyword = req.query.keyword || null;
	let state = req.query.state || '';
	let likes = req.query.likes || '';
	let tag_id = req.query.tag_id || '';
	let category_id = req.query.category_id || '';
	let pageNum = parseInt(req.query.pageNum) || 1;
	let pageSize = parseInt(req.query.pageSize) || 10;
	let conditions = {};
	if (!state) {
		if (keyword) {
			const reg = new RegExp(keyword, 'i'); //不区分大小写
			conditions = {
				$or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }],
			};
		}
	} else if (state) {
		state = parseInt(state);
		if (keyword) {
			const reg = new RegExp(keyword, 'i');
			conditions = {
				$and: [
					{ $or: [{ state: state }] },
					{ $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }, { keyword: { $regex: reg } }] },
				],
			};
		} else {
			conditions = { state };
		}
	}

	let skip = pageNum - 1 < 0 ? 0 : (pageNum - 1) * pageSize;
	let responseData = {
		count: 0,
		list: [],
	};
	Article.countDocuments(conditions, (err, count) => {
		if (err) {
			console.log('Error:' + err);
		} else {
			responseData.count = count;
			// 待返回的字段
			let fields = {
				title: 1,
				author: 1,
				keyword: 1,
				content: 1,
				desc: 1,
				img_url: 1,
				tags: 1,
				category: 1,
				state: 1,
				type: 1,
				origin: 1,
				comments: 1,
				like_User_id: 1,
				meta: 1,
				create_time: 1,
				update_time: 1,
			};
			let options = {
				skip: skip,
				limit: pageSize,
				sort: { create_time: -1 },
			};
			Article.find(conditions, fields, options, (error, result) => {
				if (err) {
					console.error('Error:' + error);
					// throw error;
				} else {
					let newList = [];
					if (likes) {
						// 根据热度 likes 返回数据
						result.sort((a, b) => {
							return b.meta.likes - a.meta.likes;
						});
						responseData.list = result;
					} else if (category_id) {
						// 根据 分类 id 返回数据
						result.forEach(item => {
							if (item.category.indexOf(category_id) > -1) {
								newList.push(item);
							}
						});
						let len = newList.length;
						responseData.count = len;
						responseData.list = newList;
					} else if (tag_id) {
						// 根据标签 id 返回数据
						result.forEach(item => {
							if (item.tags.indexOf(tag_id) > -1) {
								newList.push(item);
							}
						});
						let len = newList.length;
						responseData.count = len;
						responseData.list = newList;
					} else {
						responseData.list = result;
					}
					responseClient(res, 200, 0, '操作成功!', responseData);
				}
			});
		}
	});
};

// 后台文章列表
exports.getArticleListAdmin = (req, res) => {
	let keyword = req.query.keyword || null;
	let state = req.query.state || '';
	let likes = req.query.likes || '';
	let pageNum = parseInt(req.query.pageNum) || 1;
	let pageSize = parseInt(req.query.pageSize) || 10;
	let conditions = {};
	if (!state) {
		if (keyword) {
			const reg = new RegExp(keyword, 'i'); //不区分大小写
			conditions = {
				$or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }],
			};
		}
	} else if (state) {
		state = parseInt(state);
		if (keyword) {
			const reg = new RegExp(keyword, 'i');
			conditions = {
				$and: [
					{ $or: [{ state: state }] },
					{ $or: [{ title: { $regex: reg } }, { desc: { $regex: reg } }, { keyword: { $regex: reg } }] },
				],
			};
		} else {
			conditions = { state };
		}
	}

	let skip = pageNum - 1 < 0 ? 0 : (pageNum - 1) * pageSize;
	let responseData = {
		count: 0,
		list: [],
	};
	Article.countDocuments(conditions, (err, count) => {
		if (err) {
			console.log('Error:' + err);
		} else {
			responseData.count = count;
			// 待返回的字段
			let fields = {
				title: 1,
				author: 1,
				keyword: 1,
				content: 1,
				desc: 1,
				img_url: 1,
				tags: 1,
				category: 1,
				state: 1,
				type: 1,
				origin: 1,
				comments: 1,
				like_User_id: 1,
				meta: 1,
				create_time: 1,
				update_time: 1,
			};
			let options = {
				skip: skip,
				limit: pageSize,
				sort: { create_time: -1 },
			};
			Article.find(conditions, fields, options, (error, result) => {
				if (err) {
					console.error('Error:' + error);
					// throw error;
				} else {
					if (likes) {
						result.sort((a, b) => {
							return b.meta.likes - a.meta.likes;
						});
					}
					responseData.list = result;
					responseClient(res, 200, 0, '操作成功!', responseData);
				}
			})
				.populate([
					{ path: 'tags', },
					{ path: 'comments',  },
					{ path: 'category',  },
				])
				.exec((err, doc) => {});
		}
	});
};

// 文章点赞
exports.likeArticle = (req, res) => {
	if (!req.session.userInfo) {
		responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
		return;
	}
	let { id, user_id } = req.body;
	Article.findOne({ _id: id })
		.then(data => {
			let fields = {};
			data.meta.likes = data.meta.likes + 1;
			fields.meta = data.meta;
			let like_users_arr = data.like_users.length ? data.like_users : [];
			User.findOne({ _id: user_id })
				.then(user => {
					let new_like_user = {};
					new_like_user.id = user._id;
					new_like_user.name = user.name;
					new_like_user.avatar = user.avatar;
					new_like_user.create_time = user.create_time;
					new_like_user.type = user.type;
					new_like_user.introduce = user.introduce;
					like_users_arr.push(new_like_user);
					fields.like_users = like_users_arr;
					Article.update({ _id: id }, fields)
						.then(result => {
							responseClient(res, 200, 0, '操作成功!', result);
						})
						.catch(err => {
							console.error('err :', err);
							throw err;
						});
				})
				.catch(err => {
					responseClient(res);
					console.error('err 1:', err);
				});
		})
		.catch(err => {
			responseClient(res);
			console.error('err 2:', err);
		});
};

// 文章详情
exports.getArticleDetailByType = (req, res) => {
	let { type } = req.body;
	if (!type) {
		responseClient(res, 200, 1, '文章不存在 !');
		return;
	}
	Article.findOne({ type: type }, (Error, data) => {
		if (Error) {
			console.error('Error:' + Error);
			// throw error;
		} else {
			data.meta.views = data.meta.views + 1;
			Article.updateOne({ type: type }, { meta: data.meta })
				.then(result => {
					responseClient(res, 200, 0, '操作成功 !', data);
				})
				.catch(err => {
					console.error('err :', err);
					throw err;
				});
		}
	})
		.populate([
			{ path: 'tags', select: '-_id' },
			{ path: 'category', select: '-_id' },
			{ path: 'comments', select: '-_id' },
		])
		.exec((err, doc) => {
			// console.log("doc:");          // aikin
			// console.log("doc.tags:",doc.tags);          // aikin
			// console.log("doc.category:",doc.category);           // undefined
		});
};

// 文章详情
exports.getArticleDetail = (req, res) => {
	let { id } = req.body;
	let type = Number(req.body.type) || 1; //文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍
	console.log('type:', type);
	if (type === 1) {
		if (!id) {
			responseClient(res, 200, 1, '文章不存在 !');
			return;
		}
		Article.findOne({ _id: id }, (Error, data) => {
			if (Error) {
				console.error('Error:' + Error);
				// throw error;
			} else {
				data.meta.views = data.meta.views + 1;
				Article.updateOne({ _id: id }, { meta: data.meta })
					.then(result => {
						responseClient(res, 200, 0, '操作成功 !', data);
					})
					.catch(err => {
						console.error('err :', err);
						throw err;
					});
			}
		})
			.populate([
				{ path: 'tags',  },
				{ path: 'category',  },
				{ path: 'comments',  },
			])
			.exec((err, doc) => {
				// console.log("doc:");          // aikin
				// console.log("doc.tags:",doc.tags);          // aikin
				// console.log("doc.category:",doc.category);           // undefined
			});
	} else {
		Article.findOne({ type: type }, (Error, data) => {
			if (Error) {
				console.log('Error:' + Error);
				// throw error;
			} else {
				if (data) {
					data.meta.views = data.meta.views + 1;
					Article.updateOne({ type: type }, { meta: data.meta })
						.then(result => {
							responseClient(res, 200, 0, '操作成功 !', data);
						})
						.catch(err => {
							console.error('err :', err);
							throw err;
						});
				} else {
					responseClient(res, 200, 1, '文章不存在 !');
					return;
				}
			}
		})
			.populate([
				{ path: 'tags',  },
				{ path: 'category',  },
				{ path: 'comments',  },
			])
			.exec((err, doc) => {});
	}
};

6.3 评论

评论是有状态的:状态 => 0 待审核 / 1 通过正常 / -1 已删除 / -2 垃圾评论。
管理一级和三级评论是设置前台能不能展示的,默认是展示,如果管理员看了,是条垃圾评论就 设置为 -1 或者 -2 ,进行隐藏,前台就不会展现了。

import { responseClient } from '../util/util';
import Comment from '../models/comment';
import User from '../models/user';
import Article from '../models/article';

//获取全部评论
exports.getCommentList = (req, res) => {
	let keyword = req.query.keyword || null;
	let comment_id = req.query.comment_id || null;
	let pageNum = parseInt(req.query.pageNum) || 1;
	let pageSize = parseInt(req.query.pageSize) || 10;
	let conditions = {};
	if (comment_id) {
		if (keyword) {
			const reg = new RegExp(keyword, 'i'); //不区分大小写
			conditions = {
				_id: comment_id,
				content: { $regex: reg },
			};
		} else {
			conditions = {
				_id: comment_id,
			};
		}
	} else {
		if (keyword) {
			const reg = new RegExp(keyword, 'i'); //不区分大小写
			conditions = {
				content: { $regex: reg },
			};
		}
	}

	let skip = pageNum - 1 < 0 ? 0 : (pageNum - 1) * pageSize;
	let responseData = {
		count: 0,
		list: [],
	};
	Comment.countDocuments(conditions, (err, count) => {
		if (err) {
			console.error('Error:' + err);
		} else {
			responseData.count = count;
			// 待返回的字段
			let fields = {
				article_id: 1,
				content: 1,
				is_top: 1,
				likes: 1,
				user_id: 1,
				user: 1,
				other_comments: 1,
				state: 1,
				create_time: 1,
				update_time: 1,
			};
			let options = {
				skip: skip,
				limit: pageSize,
				sort: { create_time: -1 },
			};
			Comment.find(conditions, fields, options, (error, result) => {
				if (err) {
					console.error('Error:' + error);
					// throw error;
				} else {
					responseData.list = result;
					responseClient(res, 200, 0, '操作成功!', responseData);
				}
			});
		}
	});
};

// 添加一级评论
exports.addComment = (req, res) => {
	if (!req.session.userInfo) {
		responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
		return;
	}
	let { article_id, user_id, content } = req.body;
	User.findById({
		_id: user_id,
	})
		.then(result => {
			// console.log('result :', result);
			if (result) {
				let userInfo = {
					user_id: result._id,
					name: result.name,
					type: result.type,
					avatar: result.avatar,
				};
				let comment = new Comment({
					article_id: article_id,
					content: content,
					user_id: user_id,
					user: userInfo,
				});
				comment
					.save()
					.then(commentResult => {
						Article.findOne({ _id: article_id }, (errors, data) => {
							if (errors) {
								console.error('Error:' + errors);
								// throw errors;
							} else {
								data.comments.push(commentResult._id);
								data.meta.comments = data.meta.comments + 1;
								Article.updateOne({ _id: article_id }, { comments: data.comments, meta: data.meta })
									.then(result => {
										responseClient(res, 200, 0, '操作成功 !', commentResult);
									})
									.catch(err => {
										console.error('err :', err);
										throw err;
									});
							}
						});
					})
					.catch(err2 => {
						console.error('err :', err2);
						throw err2;
					});
			} else {
				responseClient(res, 200, 1, '用户不存在');
			}
		})
		.catch(error => {
			console.error('error :', error);
			responseClient(res);
		});
};

// 添加第三者评论
exports.addThirdComment = (req, res) => {
	if (!req.session.userInfo) {
		responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
		return;
	}
	let { article_id, comment_id, user_id, content } = req.body;

	Comment.findById({
		_id: comment_id,
	})
		.then(commentResult => {
			User.findById({
				_id: user_id,
			})
				.then(userResult => {
					if (userResult) {
						let userInfo = {
							user_id: userResult._id,
							name: userResult.name,
							type: userResult.type,
							avatar: userResult.avatar,
						};
						let item = {
							user: userInfo,
							content: content,
						};
						commentResult.other_comments.push(item);
						Comment.updateOne(
							{ _id: comment_id },
							{
								other_comments: commentResult,
							},
						)
							.then(result => {
								responseClient(res, 200, 0, '操作成功', result);
								Article.findOne({ _id: article_id }, (errors, data) => {
									if (errors) {
										console.error('Error:' + errors);
										// throw errors;
									} else {
										data.meta.comments = data.meta.comments + 1;
										Article.updateOne({ _id: article_id }, { meta: data.meta })
											.then(result => {
												// console.log('result :', result);
												responseClient(res, 200, 0, '操作成功 !', result);
											})
											.catch(err => {
												console.log('err :', err);
												throw err;
											});
									}
								});
							})
							.catch(err1 => {
								console.error('err1:', err1);
								responseClient(res);
							});
					} else {
						responseClient(res, 200, 1, '用户不存在');
					}
				})
				.catch(error => {
					console.error('error :', error);
					responseClient(res);
				});
		})
		.catch(error2 => {
			console.error('error2 :', error2);
			responseClient(res);
		});
};

// 管理一级评论
exports.changeComment = (req, res) => {
	if (!req.session.userInfo) {
		responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
		return;
	}
	let { id, state } = req.body;
	Comment.updateOne(
		{ _id: id },
		{
			state: Number(state),
		},
	)
		.then(result => {
			responseClient(res, 200, 0, '操作成功', result);
		})
		.catch(err => {
			console.error('err:', err);
			responseClient(res);
		});
};

// 管理第三者评论
exports.changeThirdComment = (req, res) => {
	if (!req.session.userInfo) {
		responseClient(res, 200, 1, '您还没登录,或者登录信息已过期,请重新登录!');
		return;
	}
	let { comment_id, state, index } = req.body;
	Comment.findById({
		_id: comment_id,
	})
		.then(commentResult => {
			let i = index ? Number(index) : 0;
			if (commentResult.other_comments.length) {
				commentResult.other_comments[i].state = Number(state);
				Comment.updateOne(
					{ _id: comment_id },
					{
						other_comments: commentResult,
					},
				)
					.then(result => {
						responseClient(res, 200, 0, '操作成功', result);
					})
					.catch(err1 => {
						console.error('err1:', err1);
						responseClient(res);
					});
			} else {
				responseClient(res, 200, 1, '第三方评论不存在!', result);
			}
		})
		.catch(error2 => {
			console.log('error2 :', error2);
			responseClient(res);
		});
};

其他模块的具体需求,都是些常用的逻辑可以实现的,也很简单,这里就不展开讲了。

7. Build Setup ( 构建安装 )

# install dependencies
npm install 

# serve with hot reload at localhost: 3000
npm start 

# build for production with minification
请使用 pm2 ,可以永久运行在服务器上,且不会一报错 node 程序就挂了。

8. 项目地址

如果觉得该项目不错或者对你有所帮助,欢迎到 github 上给个 star,谢谢。

项目地址:

前台展示: https://github.com/biaochenxuying/blog-react

管理后台:https://github.com/biaochenxuying/blog-react-admin

后端:https://github.com/biaochenxuying/blog-node

blog:https://github.com/biaochenxuying/blog

本博客系统的系列文章:

9. 最后

小汪也是第一次搭建 node 后端项目,也参考了其他项目。

参考项目:
1. nodepress
2. React-Express-Blog-Demo

2019 前端秋季社招面试经历总结(二年多经验)

1. 前言

本文内容讲笔者在 2019 的秋季社招时期,去大厂面试的问题和经验总结。

居安思危,安逸久了,都要试着知道自己目前的市场价,而最好的方法就是去外面面试几场,受受打击,知道自己的不足,以后加强。

笔者入坑前端 3 年多了,有 2 年多的前端工作经验,面试坐标:广州。

本文的面试问题只写了些开放性的问题,其公司要求保密的试题就不写出来了。

2. 字节跳 X

一面,45分钟

  • 根据自己简历和做过的项目,问一系列相关问题。
  • 闭包的输出值,考查闭包(看试题给结果,分析过程)。
  • 状态码 304 是什么意思,有什么用 ?
  • 浏览器缓存的方法有哪些,它们的优先级是怎样的 ?
  • 都说要减少 https 的请求,https 为什么慢 ?
  • http2 与 http1 有什么区别 ?
  • click DOM 节点的 inner 与 outer 的执行机制,考查事件冒泡与事件捕获 (看试题给结果,分析过程)。
  • for 循环中的 var 、let 与 const 区别,比如 for( const i = 0; i< 3; i++ ){ console.log(i); } 会输出什么结果 ?(看试题给结果,分析过程)。
  • 有没有系统学习过 es6 或者看过 es6 的书 ?
  • js 单线程、宏任务与微任务的执行顺序 (看试题给结果,分析过程)。
  • 考查箭头函数的 this 与 普通函数的区别,this 的指向 (看试题给结果,分析过程)。
  • vue 中 computed 与 watch 的内在是如何实现的 ?
  • 接下来前端要深入的方向 ?
  • 写一个方法输出 ABCDEFG 的值(看试题、现场写程序)。
  • 从排好序的两个链表中,找到相同的节点,并输出链表(看试题、现场写程序)。
  • 最后面试官问了句,你没刷过什么面试题吗 😪。

总结

最后没过面试,总结一下原因。

  • 因为这次面试是字节的猎头找的我,帮我内推的,但我还没准备好就去了,没多大信心;
  • 很久没面试了,第一次面试还是比较紧张,很多技术问题的回答也不是很好,现在回想下,当时连情商都不在线 😪。

3. CVT X

一面是电话面试,半小时

  • 问了简历中的项目的一系列问题(此处省略)。
  • vue 中 next-tick 的作用与大概实现原理 ?
  • vue 组件的双向绑定通信是如何实现的 ?
  • vue 按需加载的方式有几种,是哪几种 ?
  • 浏览器缓存的类别与优先级 ?
  • react 中数据请求为什么要在 某个生命周期里面执行,在哪个生命周期 ?
  • react 高级组件的作用 ?本质是什么来的 ?

总结

  • CVT X 的面试是找熟人内推的。
  • 回答的都挺好,过了一面。

二面是视频 HR 面试,20 分钟

  • 问了一系列个人以及在现在公司的问题。

总结

后面就没有下文了,所以没有第三轮现场技术面,我猜原因如下:

  • 谈期望薪资时,我说了该公司的招聘信息上的最低薪资,应该还是太高了,因为那是 3年+ 经验 的薪资范围。
  • 再加上我是以前面的字节跳 X 的薪资范围做为了参考标准 😂,字节跳 X 的薪资范围的是很高的,我有点漂了 😰。

4. X 教育

一面,现场面试,大概 1 小时

  • var 和 let 的作用域,匿名表达式的运用(看试题给结果,分析过程)。
  • --proto-- 指向 (看试题给结果,分析过程)
  • 闭包,及修正方法 (看试题给结果,分析过程)
  • 一个从小到大排好的数字型数组,找到数值为 target 值,并返回 index 值(现场写程序)
    function findIndex(arr, target){ ... }
  • 给两个 input 框,实现双向绑定功能的思路或者代码(现场写程序)。【JavaScript学习笔记】自己实现双向绑定
  • nextTick 的使用场景 ?
  • vue 路由中,有 post/:id 的路由,当路由切换 post/1 => post/2 时,组件会更新吗,如何修改能更新 ?vue更新路由router-view复用组件内容不刷新
  • vue-router 的导航钩子有哪些 ?组件里面的导航钩子又有哪些 ?
  • header 头部固定,剩下 body 占满全部高,超出就上下滚动,如何实现 ?
  • 平时的开发流程 ?

二面,大概一个钟

  • tcp 和 udp 的区别,各自的优势是什么 ?
  • web-socket 用过吗 ?
  • 对计算机的基础知识熟悉吗,比如网络层什么的 ?
  • 给 10 亿的数据的 url 去重,思路(现场写部分代码)

三面,HR 面

  • 问了一系列个人以及在现在公司的问题。

总结

  • 技术面试的过程表现的都不错,最终拿到了 offer ,开出的薪资涨了不少。
  • 虽然总收入涨了,但是就时薪来说,还是亏了的。
  • 跳槽的成本是很高的,总薪资或者时薪没有涨 30% ,都是亏的。
  • 因为现在的公司上班时间是 965 或者说是 955,平时有比较多的时间写博客,做些有意思的开源项目什么的,入职这家公司估计就不能了。
  • 所以没去这家公司。

5. 酷 X

一面

线上笔试,20 分钟

  • 线上笔试的内容应该是有试题库的,笔试前签了不能泄露试题的协议之类,就不写出来了。
  • for(var i = 0,j=0; i<10,j<6; i++,j++){ console.log(i+j)}
  • typeof 与 instanceof 的区别与使用
  • html 中基本的三大结构元素是什么
  • 行内元素与块级元素的区分与使用
  • 闭包的理解与使用
  • var 作用域的理解
  • “+” 的类型转换

现场逻辑题面试,30 分钟

  • 行测逻辑题面试,试题声明了不能泄露试题之类,就不写出来了。

现场技术面试,大概 10 分钟,就问 10 个问题左右

  • 模块化开发方式有哪些,你用过哪些 ?
  • 闭包的理解和其应用场景有哪些 ?对JS闭包的理解及常见应用场景
  • 对 mvc、mvp、mvvm 的理解 ?
  • vue 双向绑定的理解 ?
  • 对设计模式的理解,有用过哪些 ?
  • http 的理解,tcp 包有什么内容 ?
  • https 为什么比 http 安全 ?
  • https 请求,你通常要注意的是什么 ?
  • post 和 get 请求的区别 ?
  • 前端要考虑什么安全问题,比如:你知道 ssr 跨站脚本攻击吗 ?
  • 一般工作中你是如何排查前端性能问题的 ?

总结

  • 结果是没过,技术面试问的太宽了,以上写出来的问题还是我细化之后的了。
  • 我感觉我的技术范围和面试公司的不是很一致。

6. 总结

情况

  • 投了十来份简历,都是离我住的地方不是很远的公司,都是广州的大厂,收到的面试邀请才 4 个而已,有些投了简历都没人看。
  • 笔者 GitHub 上的开源项目总收获的 Star 数超过了 1.6K 好像也没多大作用,面试过程都没怎么问。
  • 广州的互联网大厂其实挺少的,目前大厂没有什么坑位,有的估计也是候补的,要求更加严格,而且社招的几乎都是 3 年及以上的高级前端工程师,不到 3 年经验的没多少机会。
  • 2 年多的社招,面试的内容绝大部分还是基础知识,实际工作的内容问得少。
  • 笔者在广州的时薪应该算是中偏上的水平。

经过最近的面试,笔者感觉如下:

  • 经过 2018 年的寒冬,现在 2019 年下半年了,寒冬依然严峻。
  • 想通过社招进大厂的程序员,最好有 3 年及以上经验了再去面试大厂。
  • 回答开放性问题时,要把面试官往自己熟悉的方向引。
  • 很久没面试了,要准备好再去面试,不然信心不足,成功率不高。
  • 面试成功往往是后面面试的公司,先去面试几个不是最想去的公司,再去面试最想去的公司。
  • 不要裸辞,特别是才工作一两年的程序员,不然压力非常大,最后可能找到的也是不是非常理想的公司。
  • 如果非要跳槽,那建议: 骑驴找马

7. 最后

推荐阅读:GitHub 上能挖矿的神仙技巧 - 如何发现优秀开源项目

JavaScript 数据结构与算法之美 - 栈内存与堆内存 、浅拷贝与深拷贝

JavaScript 数据结构与算法之美

前言

想写好前端,先练好内功。

栈内存与堆内存 、浅拷贝与深拷贝,可以说是前端程序员的内功,要知其然,知其所以然。

笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。

栈

定义

  1. 后进者先出,先进者后出,简称 后进先出(LIFO),这就是典型的结构。
  2. 新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端就叫栈底
  3. 在栈里,新元素都靠近栈顶,旧元素都接近栈底。
  4. 从栈的操作特性来看,是一种 操作受限的线性表,只允许在一端插入和删除数据。
  5. 不包含任何元素的栈称为空栈

栈也被用在编程语言的编译器和内存中保存变量、方法调用等,比如函数的调用栈。

定义

  • 堆数据结构是一种树状结构。
    它的存取数据的方式,与书架与书非常相似。我们不关心书的放置顺序是怎样的,只需知道书的名字就可以取出我们想要的书了。
    好比在 JSON 格式的数据中,我们存储的 key-value 是可以无序的,只要知道 key,就能取出这个 key 对应的 value。

堆与栈比较

  • 堆是动态分配内存,内存大小不一,也不会自动释放。
  • 栈是自动分配相对固定大小的内存空间,并由系统自动释放。
  • 栈,线性结构,后进先出,便于管理。
  • 堆,一个混沌,杂乱无章,方便存储和开辟内存空间。

栈内存与堆内存

JavaScript 中的变量分为基本类型和引用类型。

  • 基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问,并由系统自动分配和自动释放。
    这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。
    JavaScript 中的 Boolean、Null、Undefined、Number、String、Symbol 都是基本类型。

  • 引用类型(如对象、数组、函数等)是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript 不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用。
    JavaScript 中的 Object、Array、Function、RegExp、Date 是引用类型。

结合实例说明

let a1 = 0; // 栈内存
let a2 = "this is string" // 栈内存
let a3 = null; // 栈内存
let b = { x: 10 }; // 变量 b 存在于栈中,{ x: 10 } 作为对象存在于堆中
let c = [1, 2, 3]; // 变量 c 存在于栈中,[1, 2, 3] 作为对象存在于堆中

栈/堆内存空间

当我们要访问堆内存中的引用数据类型时

    1. 从栈中获取该对象的地址引用
    1. 再从堆内存中取得我们需要的数据

基本类型发生复制

let a = 20;
let b = a;
b = 30;
console.log(a); // 20

基本类型发生复制过程

在栈内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值,最后这些变量都是 相互独立,互不影响的

引用类型发生复制

let a = { x: 10, y: 20 }
let b = a;
b.x = 5;
console.log(a.x); // 5
  • 引用类型的复制,同样为新的变量 b 分配一个新的值,保存在栈内存中,不同的是,这个值仅仅是引用类型的一个地址指针。
  • 他们两个指向同一个值,也就是地址指针相同,在堆内存中访问到的具体对象实际上是同一个。
  • 因此改变 b.x 时,a.x 也发生了变化,这就是引用类型的特性。

结合下图理解

引用类型(浅拷贝)的复制过程

总结

栈内存 堆内存
存储基础数据类型 存储引用数据类型
按值访问 按引用访问
存储的值大小固定 存储的值大小不定,可动态调整
由系统自动分配内存空间 由代码进行指定分配
空间小,运行效率高 空间大,运行效率相对较低
先进后出,后进先出 无序存储,可根据引用直接获取

浅拷贝与深拷贝

上面讲的引用类型的复制就是浅拷贝,复制得到的访问地址都指向同一个内存空间。所以修改了其中一个的值,另外一个也跟着改变了。

深拷贝:复制得到的访问地址指向不同的内存空间,互不相干。所以修改其中一个值,另外一个不会改变。

平时使用数组复制时,我们大多数会使用 =,这只是浅拷贝,存在很多问题。比如:

let arr = [1,2,3,4,5];
let arr2 = arr;
console.log(arr) //[1, 2, 3, 4, 5]
console.log(arr2) //[1, 2, 3, 4, 5]
arr[0] = 6;
console.log(arr) //[6, 2, 3, 4, 5]
console.log(arr2) //[6, 2, 3, 4, 5]
arr2[4] = 7;
console.log(arr) //[6, 2, 3, 4, 7]
console.log(arr2) //[6, 2, 3, 4, 7]

很明显,浅拷贝下,拷贝和被拷贝的数组会相互受到影响。

所以,必须要有一种不受影响的方法,那就是深拷贝。

深拷贝的的复制过程

let a = { x: 10, y: 20 }
let b = JSON.parse(JSON.stringify(a));
b.x = 5;
console.log(a.x); // 10
console.log(b.x); // 5

复制前

复制后

b.x 修改为 5 后

数组

一、for 循环

//for 循环 copy
function copy(arr) {
    let cArr = []
    for(let i = 0; i < arr.length; i++){
      cArr.push(arr[i])
    }
    return cArr;
}
let arr3 = [1,2,3,4];
let arr4 = copy(arr3) //[1,2,3,4]
console.log(arr4) //[1,2,3,4]
arr3[0] = 5;
console.log(arr3) //[5,2,3,4]
console.log(arr4) //[1,2,3,4]

二、slice 方法

//slice实现深拷贝
let arr5 = [1,2,3,4];
let arr6 = arr5.slice(0);
arr5[0] = 5;
console.log(arr5); //[5,2,3,4]
console.log(arr6); //[1,2,3,4]

三、concat 方法

//concat实现深拷贝
let arr7 = [1,2,3,4];
let arr8 = arr7.concat();
arr7[0] = 5;
console.log(arr7); //[5,2,3,4]
console.log(arr8); //[1,2,3,4]

四、es6 扩展运算

//es6 扩展运算实现深拷贝
let arr9 = [1,2,3,4];
let [...arr10] = arr9;
arr9[0] = 5;
console.log(arr9) //[5,2,3,4]
console.log(arr10) //[1,2,3,4]

五、JSON.parse 与 JSON.stringify

let arr9 = [1,2,3,4];
let arr10 = JSON.parse(JSON.stringify(arr9))
arr9[0] = 5;
console.log(arr9) //[5,2,3,4]
console.log(arr10) //[1,2,3,4]

注意:该方法在数据量比较大时,会有性能问题。

对象

一、对象的循环

//  循环 copy 对象
let obj = {
    id:'0',
    name:'king',
    sex:'man'
}
let obj2 = copy2(obj)
function copy2(obj) {
    let cObj = {};
    for(var key in obj){
      cObj[key] = obj[key]
    }
    return cObj
}
obj2.name = "king2"
console.log(obj) // {id: "0", name: "king", sex: "man"}
console.log(obj2) // {id: "0", name: "king2", sex: "man"}

二、JSON.parse 与 JSON.stringify

var obj1 = {
    x: 1, 
    y: {
        m: 1
    },
    a:undefined,
    b:function(a,b){
      return a+b
    },
    c:Symbol("foo")
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}, a: undefined, b: ƒ, c: Symbol(foo)}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}, a: undefined, b: ƒ, c: Symbol(foo)}
console.log(obj2) //{x: 2, y: {m: 2}}

可实现多维对象的深拷贝。

注意:进行JSON.stringify() 序列化的过程中,undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。

三、es6 扩展运算

let obj = {
    id:'0',
    name:'king',
    sex:'man'
}
let {...obj4} = obj
obj4.name = "king4"
console.log(obj) //{id: "0", name: "king", sex: "man"}
console.log(obj4) //{id: "0", name: "king4", sex: "man"}

四、Object.assign()

Object.assign() 只能实现一维对象的深拷贝。

var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1);
console.log(obj1) // {x: 1, y: 2}
console.log(obj2) // {x: 1, y: 2}

obj2.x = 2; // 修改 obj2.x
console.log(obj1) // {x: 1, y: 2}
console.log(obj2) // {x: 2, y: 2}

var obj1 = {
    x: 1, 
    y: {
        m: 1
    }
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) // {x: 1, y: {m: 1}}
console.log(obj2) // {x: 1, y: {m: 1}}

obj2.y.m = 2; // 修改 obj2.y.m
console.log(obj1) // {x: 1, y: {m: 2}}
console.log(obj2) // {x: 1, y: {m: 2}}

通用深拷贝方法

简单版

let clone = function (v) {
    let o = v.constructor === Array ? [] : {};
    for(var i in v){
      o[i] = typeof v[i] === "object" ? clone(v[i]) : v[i];
    }
    return o;
}
// 测试
let obj = {
    id:'0',
    name:'king',
    sex:'man'
}
let obj2 = clone(obj)
obj2.name = "king2"
console.log(obj) // {id: "0", name: "king", sex: "man"}
console.log(obj2) // {id: "0", name: "king2", sex: "man"}

let arr3 = [1,2,3,4];
let arr4 = clone(arr3) // [1,2,3,4]
arr3[0] = 5;
console.log(arr3) // [5,2,3,4]
console.log(arr4) // [1,2,3,4]

但上面的深拷贝方法遇到循环引用,会陷入一个循环的递归过程,从而导致爆栈,所以要避免。

let obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;
let obj2 = clone(obj1);
console.log(obj2) 

结果如下:

爆栈

总结:深刻理解 javascript 的深浅拷贝,可以灵活的运用数组与对象,并且可以避免很多 bug。

文章输出计划

JavaScript 数据结构与算法之美 的系列文章,坚持 3 - 7 天左右更新一篇,暂定计划如下表。

标题 链接
时间和空间复杂度 #29
线性表(数组、链表、栈、队列) #34
实现一个前端路由,如何实现浏览器的前进与后退 ? #30
栈内存与堆内存 、浅拷贝与深拷贝 #35
非线性表(树、堆) 精彩待续
递归 精彩待续
冒泡排序 精彩待续
插入排序 精彩待续
选择排序 精彩待续
归并排序 精彩待续
快速排序 精彩待续
计数排序 精彩待续
基数排序 精彩待续
桶排序 精彩待续
希尔排序 精彩待续
堆排序 精彩待续
十大经典排序汇总 精彩待续

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。

7. 最后

文章中的代码已经全部放在了我的 github 上,如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

关注我的公众号,第一时间接收最新的精彩博文。

文章可以转载,但须注明作者及出处,需要转载到公众号的,喊我加下白名单就行了。

参考文章:

JavaScript栈内存和堆内存
JavaScript实现浅拷贝与深拷贝的方法分析
浅拷贝与深拷贝(JavaScript)

笔芯

JavaScript 数据结构与算法之美 - 非线性表(树、堆)

JavaScript 数据结构与算法之美

全栈修炼

1. 前言

想学好前端,先练好内功,内功不行,就算招式练的再花哨,终究成不了高手。

非线性表(树、堆),可以说是前端程序员的内功,要知其然,知其所以然。

笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。

非线性表中的树、堆是干嘛用的 ?其数据结构是怎样的 ?

希望大家带着这两个问题阅读下文。

2. 树

树

的数据结构就像我们生活中的真实的树,只不过是倒过来的形状。

术语定义

  • 节点:树中的每个元素称为节点,如 A、B、C、D、E、F、G、H、I、J。
  • 父节点:指向子节点的节点,如 A。
  • 子节点:被父节点指向的节点,如 A 的孩子 B、C、D。
  • 父子关系:相邻两节点的连线,称为父子关系,如 A 与 B,C 与 H,D 与 J。
  • 根节点:没有父节点的节点,如 A。
  • 叶子节点:没有子节点的节点,如 E、F、G、H、I、J。
  • 兄弟节点:具有相同父节点的多个节点称为兄弟节点,如 B、C、D。
  • 节点的高度:节点到叶子节点的最长路径所包含的边数。
  • 节点的深度:根节点到节点的路径所包含的边数。
  • 节点层数:节点的深度 +1(根节点的层数是 1 )。
  • 树的高度:等于根节点的高度。
  • 森林: n 棵互不相交的树的集合。

树的高度、深度、层

高度是从下往上度量,比如一个人的身高 180cm ,起点就是从 0 开始的。
深度是从上往下度量,比如泳池的深度 180cm ,起点也是从 0 开始的。
高度和深度是带有字的,都是从 0 开始计数的。
而层数的计算,是和我们平时的楼层的计算是一样的,最底下那层是第 1 层,是从 1 开始计数的,所以根节点位于第 1 层,其他子节点依次加 1。

二叉树分类

二叉树分类

二叉树

  • 每个节点最多只有 2 个子节点的树,这两个节点分别是左子节点和右子节点。如上图中的 1、 2、3。
    不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。以此类推,自己想四叉树、八叉树的结构图。

满二叉树

  • 一种特殊的二叉树,除了叶子节点外,每个节点都有左右两个子节点,这种二叉树叫做满二叉树。如上图中的 2。

完全二叉树

  • 一种特殊的二叉树,叶子节点都在最底下两层,最后一层叶子节都靠排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫做完全二叉树。如上图的 3。
    完全二叉树与不是完全二叉树的区分比较难,所以对比下图看看。

完全二叉树与不是完全二叉树

之前的文章 栈内存与堆内存 、浅拷贝与深拷贝 中有说到:JavaScript 中的引用类型(如对象、数组、函数等)是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript 不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用。

那么到底是什么呢 ?其数据结构又是怎样的呢 ?

堆其实是一种特殊的树。只要满足这两点,它就是一个堆。

  • 堆是一个完全二叉树。
    完全二叉树:除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。
  • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。
    也可以说:堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。这两种表述是等价的。

对于每个节点的值都大于等于子树中每个节点值的堆,我们叫作大顶堆。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫作小顶堆

区分堆、大顶堆、小顶堆

其中图 1 和 图 2 是大顶堆,图 3 是小顶堆,图 4 不是堆。除此之外,从图中还可以看出来,对于同一组数据,我们可以构建多种不同形态的堆。

二叉查找树(Binary Search Tree)

  • 一种特殊的二叉树,相对较小的值保存在左节点中,较大的值保存在右节点中,叫二叉查找树,也叫二叉搜索树。
    二叉查找树是一种有序的树,所以支持快速查找、快速插入、删除一个数据。
    下图中, 3 个都是二叉查找树,

二叉查找树

平衡二叉查找树

  • 平衡二叉查找树:二叉树中任意一个节点的左右子树的高度相差不能大于 1
    从这个定义来看,完全二叉树、满二叉树其实都是平衡二叉树,但是非完全二叉树也有可能是平衡二叉树。
    平衡二叉查找树中平衡的意思,其实就是让整棵树左右看起来比较对称、比较平衡,不要出现左子树很高、右子树很矮的情况。这样就能让整棵树的高度相对来说低一些,相应的插入、删除、查找等操作的效率高一些。
    平衡二叉查找树其实有很多,比如,Splay Tree(伸展树)、Treap(树堆)等,但是我们提到平衡二叉查找树,听到的基本都是红黑树。

平衡二叉树与非平衡二叉树

红黑树(Red-Black Tree)

红黑树中的节点,一类被标记为黑色,一类被标记为红色。除此之外,一棵红黑树还需要满足这样几个要求:

  • 根节点是黑色的。
  • 每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存储数据。
  • 任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的。
  • 每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点。

下面两个都是红黑树。

红黑树

存储

完全二叉树的存储

  • 链式存储
    每个节点由 3 个字段,其中一个存储数据,另外两个是指向左右子节点的指针。
    我们只要拎住根节点,就可以通过左右子节点的指针,把整棵树都串起来。
    这种存储方式比较常用,大部分二叉树代码都是通过这种方式实现的。

链式存储

  • 顺序存储
    用数组来存储,对于完全二叉树,如果节点 X 存储在数组中的下标为 i ,那么它的左子节点的存储下标为 2 * i ,右子节点的下标为 2 * i + 1,反过来,下标 i / 2 位置存储的就是该节点的父节点。
    注意,根节点存储在下标为 1 的位置。完全二叉树用数组来存储是最省内存的方式。

顺序存储

二叉树的遍历

经典的方法有三种:前序遍历、中序遍历、后序遍历。其中,前、中、后序,表示的是节点与它的左右子树节点遍历访问的先后顺序。

前序遍历(根 => 左 => 右)

  • 对于树中的任意节点来说,先访问这个节点,然后再访问它的左子树,最后访问它的右子树。

中序遍历(左 => 根 => 右)

  • 对于树中的任意节点来说,先访问它的左子树,然后再访问它的本身,最后访问它的右子树。

后序遍历(左 => 右 => 根)

  • 对于树中的任意节点来说,先访问它的左子树,然后再访问它的右子树,最后访问它本身。

实际上,二叉树的前、中、后序遍历就是一个递归的过程。

遍历

时间复杂度:3 种遍历方式中,每个节点最多会被访问 2 次,跟节点的个数 n 成正比,所以时间复杂度是 O(n)。

实现二叉查找树

二叉查找树的特点是:相对较小的值保存在左节点中,较大的值保存在右节点中。

代码实现二叉查找树,方法有以下这些。

方法

  • insert(key):向树中插入一个新的键。
  • search(key):在树中查找一个键,如果节点存在,则返回 true;如果不存在,则返回 false。
  • min:返回树中最小的值/键。
  • max:返回树中最大的值/键。
  • remove(key):从树中移除某个键。

遍历

  • preOrderTraverse:通过先序遍历方式遍历所有节点。
  • inOrderTraverse:通过中序遍历方式遍历所有节点。
  • postOrderTraverse:通过后序遍历方式遍历所有节点。

具体代码

  • 首先实现二叉查找树类的类
// 二叉查找树类
function BinarySearchTree() {
    // 用于实例化节点的类
    var Node = function(key){
        this.key = key; // 节点的健值
        this.left = null; // 指向左节点的指针
        this.right = null; // 指向右节点的指针
    };
    var root = null; // 将根节点置为null
}
  • insert 方法,向树中插入一个新的键。
    遍历树,将插入节点的键值与遍历到的节点键值比较,如果前者大于后者,继续递归遍历右子节点,反之,继续遍历左子节点,直到找到一个空的节点,在该位置插入。
this.insert = function(key){
    var newNode = new Node(key); // 实例化一个节点
    if (root === null){
        root = newNode; // 如果树为空,直接将该节点作为根节点
    } else {
        insertNode(root,newNode); // 插入节点(传入根节点作为参数)
    }
};
// 插入节点的函数
var insertNode = function(node, newNode){
    // 如果插入节点的键值小于当前节点的键值
    // (第一次执行insertNode函数时,当前节点就是根节点)
    if (newNode.key < node.key){
        if (node.left === null){
            // 如果当前节点的左子节点为空,就直接在该左子节点处插入
            node.left = newNode;
        } else {
            // 如果左子节点不为空,需要继续执行insertNode函数,
            // 将要插入的节点与左子节点的后代继续比较,直到找到能够插入的位置
            insertNode(node.left, newNode);
        }
    } else {
        // 如果插入节点的键值大于当前节点的键值
        // 处理过程类似,只是insertNode函数继续比较的是右子节点
        if (node.right === null){
            node.right = newNode;
        } else {
            insertNode(node.right, newNode);
        }
    }
}

在下图的树中插入健值为 6 的节点,过程如下:

  • 搜索最小值
    在二叉搜索树里,不管是整个树还是其子树,最小值一定在树最左侧的最底层。
    因此给定一颗树或其子树,只需要一直向左节点遍历到底就行了。
this.min = function(node) {
    // min方法允许传入子树
    node = node || root;
    // 一直遍历左侧子节点,直到底部
    while (node && node.left !== null) {
        node = node.left;
    }
    return node;
};
  • 搜索最大值
    搜索最大值与搜索最小值类似,只是沿着树的右侧遍历。
this.max = function(node) {
    // min方法允许传入子树
    node = node || root;
    // 一直遍历左侧子节点,直到底部
    while (node && node.right !== null) {
        node = node.right;
    }
    return node;
};
  • 搜索特定值
    搜索特定值的处理与插入值的处理类似。遍历树,将要搜索的值与遍历到的节点比较,如果前者大于后者,则递归遍历右侧子节点,反之,则递归遍历左侧子节点。
this.search = function(key, node){
    // 同样的,search方法允许在子树中查找值
    node = node || root;
    return searchNode(key, node);
};
var searchNode = function(key, node){
    // 如果node是null,说明树中没有要查找的值,返回false
    if (node === null){
        return false;
    }
    if (key < node.key){
        // 如果要查找的值小于该节点,继续递归遍历其左侧节点
        return searchNode( key, node.left);
    } else if (key > node.key){
        // 如果要查找的值大于该节点,继续递归遍历其右侧节点
        return searchNode(key, node.right);
    } else {
        // 如果要查找的值等于该节点,说明查找成功,返回改节点
        return node;
    }
};
  • 移除节点
    移除节点,首先要在树中查找到要移除的节点,再判断该节点是否有子节点、有一个子节点或者有两个子节点,最后分别处理。
this.remove = function(key, node) {
	// 同样的,允许仅在子树中删除节点
	node = node || root;
	return removeNode(key, node);
};
var self = this;
var removeNode = function(key, node) {
	// 如果 node 不存在,直接返回
	if (node === false) {
		return null;
	}

	// 找到要删除的节点
	node = self.search(key, node);

	// 第一种情况,该节点没有子节点
	if (node.left === null && node.right === null) {
		node = null;
		return node;
	}
	// 第二种情况,该节点只有一个子节点的节点
	if (node.left === null) {
		// 只有右节点
		node = node.right;
		return node;
	} else if (node.right === null) {
		// 只有左节点
		node = node.left;
		return node;
	}
	// 第三种情况,有有两个子节点的节点
	// 将右侧子树中的最小值,替换到要删除的位置
	// 找到最小值
	var aux = self.min(node.right);
	// 替换
	node.key = aux.key;
	// 删除最小值
	node.right = removeNode(aux.key, node.right);
	return node;
};

第三种情况的处理过程,如下图所示。
当要删除的节点有两个子节点时,为了不破坏树的结构,删除后要替补上来的节点的键值大小必须在已删除节点的左、右子节点的键值之间,且替补上来的节点不应该有子节点,否则会产生一个节点有多个字节点的情况,因此,找右侧子树的最小值替换上来。
同理,找左侧子树的最大值替换上来也可以。

  • 先序遍历
this.preOrderTraverse = function(callback){
    // 同样的,callback用于对遍历到的节点做操作
    preOrderTraverseNode(root, callback);
};
var preOrderTraverseNode = function (node, callback) {
    // 遍历到node为null为止
    if (node !== null) {
        callback(node.key); // 先处理当前节点
        preOrderTraverseNode(node.left, callback); // 再继续遍历左子节点
        preOrderTraverseNode(node.right, callback); // 最后遍历右子节点
    }
};

用先序遍历遍历下图所示的树,并打印节点键值。
输出结果:11 7 5 3 6 9 8 10 15 13 12 14 20 18 25。
遍历过程如图:

  • 中序遍历
this.inOrderTraverse = function(callback){
    // callback用于对遍历到的节点做操作
    inOrderTraverseNode(root, callback);
};
var inOrderTraverseNode = function (node, callback) {
    // 遍历到node为null为止
    if (node !== null) {
        // 优先遍历左边节点,保证从小到大遍历
        inOrderTraverseNode(node.left, callback);
        // 处理当前的节点
        callback(node.key);
        // 遍历右侧节点
        inOrderTraverseNode(node.right, callback);
    }
};

对下图的树做中序遍历,并输出各个节点的键值。
依次输出:3 5 6 7 8 9 10 11 12 13 14 15 18 20 25。
遍历过程如图:

  • 后序遍历
this.postOrderTraverse = function(callback){
    postOrderTraverseNode(root, callback);
};
var postOrderTraverseNode = function (node, callback) {
    if (node !== null) {
        postOrderTraverseNode(node.left, callback); //{1}
        postOrderTraverseNode(node.right, callback); //{2}
        callback(node.key); //{3}
    }
};

可以看到,中序、先序、后序遍历的实现方式几乎一模一样,只是 {1}、{2}、{3} 行代码的执行顺序不同。
对下图的树进行后序遍历,并打印键值:3 6 5 8 10 9 7 12 14 13 18 25 20 15 11。
遍历过程如图:

  • 添加打印的方法 print。
this.print = function() {
  console.log('root :', root);
  return root;
};

完整代码请看文件 binary-search-tree.html

测试过程:

// 测试
var binarySearchTree = new BinarySearchTree();
var arr = [11, 7, 5, 3, 6, 9, 8, 10, 15, 13, 12, 14, 20, 18, 25];
for (var i = 0; i < arr.length; i++) {
	var value = arr[i];
	binarySearchTree.insert(value);
}

console.log('先序遍历:');
var arr = [];
binarySearchTree.preOrderTraverse(function(value) {
	// console.log(value);
	arr.push(value);
});
console.log('arr :', arr); // [11, 7, 5, 3, 6, 9, 8, 10, 15, 13, 12, 14, 20, 18, 25]

var min = binarySearchTree.min();
console.log('min:', min); // 3
var max = binarySearchTree.max();
console.log('max:', max); // 25
var search = binarySearchTree.search(10);
console.log('search:', search); // 10
var remove = binarySearchTree.remove(13);
console.log('remove:', remove); // 13

console.log('先序遍历:');
var arr1 = [];
binarySearchTree.preOrderTraverse(function(value) {
	// console.log(value);
	arr1.push(value);
});
console.log('arr1 :', arr1); //  [11, 7, 5, 3, 6, 9, 8, 10, 15, 14, 12, 20, 18, 25]

console.log('中序遍历:');
var arr2 = [];
binarySearchTree.inOrderTraverse(function(value) {
	// console.log(value);
	arr2.push(value);
}); 
console.log('arr2 :', arr2); // [3, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 18, 20, 25]

console.log('后序遍历:');
var arr3 = [];
binarySearchTree.postOrderTraverse(function(value) {
	// console.log(value);
	arr3.push(value);
});
console.log('arr3 :', arr3); //  [3, 6, 5, 8, 10, 9, 7, 12, 14, 18, 25, 20, 15, 11]

binarySearchTree.print(); // 看控制台

结果如下:

测试结果

2. 最后

看到这里,你能解答文章的题目 非线性表中的树、堆是干嘛用的 ?其数据结构是怎样的 ?

如果不能,建议再回头仔细看看哦。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。

参考文章:

数据结构与算法之美

学习JavaScript数据结构与算法 — 树

笔芯

喜欢就点个赞吧,听说点在看的都会很有钱。

react + Ant Design + 支持 markdown 的 blog-react

主页

前言

此 blog 项目是基于 react 全家桶 + Ant Design 的,项目已经开源,项目地址在 github 上。

1. 效果

首页

完整效果请看:http://biaochenxuying.cn/main.html

2. 功能描述

2.1 已经实现功能

  • 登录
  • 注册
  • 文章列表
  • 标签分类
  • 个人介绍
  • 点赞与评论
  • 留言
  • 时间轴
  • 发文(支持 MarkDown 语法)
  • 文章详情展示(支持代码语法高亮)

2.2 待实现功能

  • 文章归档
  • 文章分类
  • 文章详情的目录
  • 移动端适配
  • 升级 webpack 版本到 4.X

3. 前端技术

3.1 主要技术

  • react: 16.5.2
  • antd: 3.9.3
  • react-router::4.3.1
  • webpack: 3.8.1
  • axios:0.18.0
  • redux: 4.0.0
  • highlight.js: 9.12.0
  • marked:0.5.1

4. 项目搭建

5. 主要项目结构

- components
  - article 文章详情
  - articles 文章列表
  - comments 评论
  - loadEnd 加载完成
  - loading 加载中
  - login 登录
  - message 留言
  - nav 导航
  - register 注册
  - slider 右边栏(博主 logo 、链接和标签等)
  - timeLine 时间轴
- router 路由
- store redux 的状态管理
- utils 封装的常用的方法
- views 框架页面

6. markdown 渲染

markdown 渲染效果图:

markdown 渲染效果图

react 相关的支持 markdown 语法的有 react-markdown,但不支持表格的渲染,所以没用。

markdown 渲染 采用了开源的 marked, 代码高亮用了 highlight.js 。

用法:

第一步:npm i marked highlight.js --save

npm i marked highlight.js --save

第二步: 导入

import marked from 'marked';
import hljs from 'highlight.js';

第三步: 设置

componentWillMount() {
		// marked相关配置
		marked.setOptions({
			renderer: new marked.Renderer(),
			gfm: true,
			tables: true,
			breaks: true,
			pedantic: false,
			sanitize: true,
			smartLists: true,
			smartypants: false,
			highlight: function(code) {
				return hljs.highlightAuto(code).value;
			},
		});
	}

第四步:

<div className="content">
	<div
		id="content"
		className="article-detail"
		dangerouslySetInnerHTML={{
		      __html: this.state.articleDetail.content ? marked(this.state.articleDetail.content) : null,
			}}
		/>
	</div>

第五步:引入 monokai_sublime 的 css 样式

<link href="http://cdn.bootcss.com/highlight.js/8.0/styles/monokai_sublime.min.css" rel="stylesheet">

第六步:对 markdown 样式的补充

如果不补充样式,是没有黑色背景的,字体大小等也会比较小,图片也不会居中显示

/*对 markdown 样式的补充*/
pre {
    display: block;
    padding: 10px;
    margin: 0 0 10px;
    font-size: 14px;
    line-height: 1.42857143;
    color: #abb2bf;
    background: #282c34;
    word-break: break-all;
    word-wrap: break-word;
    overflow: auto;
}
h1,h2,h3,h4,h5,h6{
    margin-top: 1em;
    /* margin-bottom: 1em; */
}
strong {
    font-weight: bold;
}

p > code:not([class]) {
    padding: 2px 4px;
    font-size: 90%;
    color: #c7254e;
    background-color: #f9f2f4;
    border-radius: 4px;
}
p img{
    /* 图片居中 */
    margin: 0 auto;
    display: flex;
}

#content {
    font-family: "Microsoft YaHei",  'sans-serif';
    font-size: 16px;
    line-height: 30px;
}

#content .desc ul,#content .desc ol {
    color: #333333;
    margin: 1.5em 0 0 25px;
}

#content .desc h1, #content .desc h2 {
    border-bottom: 1px solid #eee;
    padding-bottom: 10px;
}

#content .desc a {
    color: #009a61;
}

6. 主页的满屏 飘花洒落 的效果

主页的 飘花洒落 的效果

大家也看到了,主页的满屏动态 飘花洒落 的效果很棒吧,这效果我也是网上找的,是在单独的一个 main.html 文件上的,代码链接如下:

主页的满屏 飘花洒落 的效果

7. 注意点

7.1 打包的配置

因为项目是用了 react-app-rewired (一个对 create-react-app 进行自定义配置的社区解决方案) 来打包了,所以如果你想修改 webpack.config.dev.js 和 webpack.config.prod.js 的配置,打包后可能看不到想要的效果,因为 react-app-rewired 打包时,是根据根目录的 config-overrides.js 来进行打包,所以要修改 webpack 的配置的话,请修改 config-overrides.js 。

比如:关闭 sourceMap 和 支持装饰器

config.devtool = false; // 关闭 sourceMap 
config = injectBabelPlugin('babel-plugin-transform-decorators-legacy', config); // 支持装饰器

7.2 关于 页面

对于 关于 的页面,其实是一篇文章来的,根据文章类型 type 来决定的,数据库里面 type 为 3
的文章,只能有一篇就是 博主介绍 ;达到了想什么时候修改内容都可以。

所以当 this.props.location.pathname === '/about' 时就是请求类型为 博主介绍 的文章。

type: 3, // 文章类型: 1:普通文章;2:是博主简历;3 :是博主简介;

8. Build Setup ( 建立安装 )

# install dependencies
npm install 

# serve with hot reload at localhost: 3000
npm start 或者 yarn start

# build for production with minification
npm run build 或者 yarn run build

如果要看完整的效果,是要和后台项目 blog-node 一起运行才行的,不然接口请求会失败。

虽然引入了 mock 了,但是还没有时间做模拟数据,想看具体效果,请稳步到我的网站上查看 http://biaochenxuying.cn/main.html

最后

其他具体的业务代码,都是些常会见到的需求,这里就不展开讲了。

如果你觉得该文章不错,欢迎到我的 github,star 一下,谢谢。

项目地址:

前台展示: https://github.com/biaochenxuying/blog-react

管理后台:https://github.com/biaochenxuying/blog-react-admin

后端:https://github.com/biaochenxuying/blog-node

blog:https://github.com/biaochenxuying/blog

本博客系统的系列文章:

你以为本文就这么结束了 ? 精彩在后面 !!!

原生 js 实现一个有动画效果的进度条插件 progress

效果图:

progress.gif

项目地址:https://github.com/biaochenxuying/progress

效果体验地址: https://biaochenxuying.github.io/progress/index.html

1. 原理

  • 一个用于装载进度条内容的 div (且叫做 container)。
  • 然后在 container 里面动态生成三个元素,一个是做为背景的 div (且叫做 progress),一个是做为显示进度的 div (且叫做 bar),还有一个是显示文字的 span (且叫做 text)。
  • progress 的宽为 100%,bar 的宽根据传入数值 target 的值来定( 默认为 0 ,全部占满的值为 100 ),text 展示的文字为 bar 的宽占 progress 宽的百分比。
  • bar 的宽从 0 逐渐增加到的 target 值的过程( 比如: 0 > 80 ),给这个过程添加一个逐渐加快的动画。

2. 代码实现

具体的过程请看代码:

/*
* author: https://github.com/biaochenxuying
*/

(function() {
  function Progress() {
    this.mountedId = null;
    this.target = 100;
    this.step = 1;
    this.color = '#333';
    this.fontSize = '18px';
    this.borderRadius = 0;
    this.backgroundColor = '#eee';
    this.barBackgroundColor = '#26a2ff';
  }

  Progress.prototype = {
    init: function(config) {
      if (!config.mountedId) {
        alert('请输入挂载节点的 id');
        return;
      }

      this.mountedId = config.mountedId;
      this.target = config.target || this.target;
      this.step = config.step || this.step;
      this.fontSize = config.fontSize || this.fontSize;
      this.color = config.color || this.color;
      this.borderRadius = config.borderRadius || this.borderRadius;
      this.backgroundColor = config.backgroundColor || this.backgroundColor;
      this.barBackgroundColor =
        config.barBackgroundColor || this.barBackgroundColor;

      var box = document.querySelector(this.mountedId);
      var width = box.offsetWidth;
      var height = box.offsetHeight;
      var progress = document.createElement('div');
      progress.style.position = 'absolute';
      progress.style.height = height + 'px';
      progress.style.width = width + 'px';
      progress.style.borderRadius = this.borderRadius;
      progress.style.backgroundColor = this.backgroundColor;

      var bar = document.createElement('div');
      bar.style.float = 'left';
      bar.style.height = '100%';
      bar.style.width = '0';
      bar.style.lineHeight = height + 'px';
      bar.style.textAlign = 'center';
      bar.style.borderRadius = this.borderRadius;
      bar.style.backgroundColor = this.barBackgroundColor;

      var text = document.createElement('span');
      text.style.position = 'absolute';
      text.style.top = '0';
      text.style.left = '0';
      text.style.height = height + 'px';
      text.style.lineHeight = height + 'px';
      text.style.fontSize = this.fontSize;
      text.style.color = this.color;

      progress.appendChild(bar);
      progress.appendChild(text);
      box.appendChild(progress);

      this.run(progress, bar, text, this.target, this.step);
    },
    /**
     * @name 执行动画
     * @param progress 底部的 dom 对象
     * @param bar 占比的 dom 对象
     * @param text 文字的 dom 对象
     * @param target 目标值( Number )
     * @param step 动画步长( Number )
     */
    run: function(progress, bar, text, target, step) {
      var self = this;
      ++step;
      var endRate = parseInt(target) - parseInt(bar.style.width);
      if (endRate <= step) {
        step = endRate;
      }
      var width = parseInt(bar.style.width);
      var endWidth = width + step + '%';
      bar.style.width = endWidth;
      text.innerHTML = endWidth;

      if (width >= 94) {
        text.style.left = '94%';
      } else {
        text.style.left = width + 1 + '%';
      }

      if (width === target) {
        clearTimeout(timeout);
        return;
      }
      var timeout = setTimeout(function() {
        self.run(progress, bar, text, target, step);
      }, 30);
    },
  };

  // 注册到 window 全局
  window.Progress = Progress;
})();

3. 使用方法

  var config = {
    mountedId: '#bar',
    target: 8,
    step: 1,
    color: 'green',
    fontSize: "20px",
    borderRadius: "5px",
    backgroundColor: '#eee',
    barBackgroundColor: 'red',
  };
  var p = new Progress();
  p.init(config);

4. 最后

  • 笔者的博客后花圆地址如下:

github :https://github.com/biaochenxuying/blog
个人网站 :http://biaochenxuying.cn/main.html

如果您觉得这篇文章不错或者对你有所帮助,请给个赞呗,你的点赞就是对我最大的鼓励,谢谢。

用钱生钱,从掌握金钱的规律开始

“如果你没找到一个当你睡觉时还能挣钱的方法,你将工作到死。” ——巴菲特

image

上图是北京早上 8 点钟挤地铁上班的人们。不知图上有没有你的缩写。

前言

金钱是有规律的,只要掌握了一定的规律,虽说不能一夜暴富,但是把掌握的规律用于生活中,至少是可以让挣钱变得轻松一些。

不知道他们有没有想过:生活中,单靠增加工时获得的收入永远无法让你摆脱贫穷。用青春来换钱的交易也绝对不可取。

绝大多数的人,都是非常勤奋的,不然也不能坚持每天定时去上班,但又是懒惰的,从来没有想过学习掌握金钱的规律。

要点

最近笔者在看作者博多·令费尔写的一篇小作《小狗钱钱》,这本《小狗钱钱》是根据作者根据他写的《财务自由之路》,把其中的方法与策略编成这么一个小故事的。《小狗钱钱》讲的是一个 11 岁的小女孩捡到一条会说人话的狗(名为 钱钱),然后钱钱教会了这个小女孩与钱打交道的方法,怎样理解钱与一步步轻松地挣钱,最后小姑娘不仅自己掌提了钱的使用方法,而且还帮助自己的父母走出了财务危机的故事。

看完这篇小作,做点笔记与写自己下看完后的一些感悟。

里面讲到一个故事:

“从前有一个农家小伙儿,他每天的愿望就是从鹅笼里拣一个鹅蛋当早饭。有一天,他竟然在鹅笼里发现了一只金蛋。一开始他当然不敢相信自己的眼睛。他想,也许是有人在捉弄他。为了谨慎起见,他把金蛋拿去让金匠辨别,可是金匠向他保证说,这只蛋完完全全是金子铸成的。于是,这个农家小伙儿就卖了这只金蛋,举行了一个盛大的庆祝会。”
“第二天清晨,他起了一个大早,赶到鹅笼里一看,那里果真又放着一个金蛋,这样的情况延续了好几天。可是这个农家小伙儿是一个贪婪的人,他抱怨自己的鹅,因为鹅没法向他解释是怎么下出这个蛋的,否则他也许自己就可以制造金蛋了。他还气乎乎地想,这只懒惰的鹅每天至少应该下两只金蛋。他觉得现在这样的速度太慢了。他的怒火越来越大,最后,他终于怒不可遏地把鹅揪出笼子劈成了两半。自那以后,他再也得不到金蛋了。”

假如我没有了我的“鹅”,我就总是得为了赚钱而工作,但是一旦我有了属于自己的“鹅”,我的钱就会自动为我工作了。

金钱有一些秘密和规律,要想了解这些秘密和规律,前提条件是,你自己必须真的有这个愿望。

必须真的有这个愿望的意义在于,如果足够的动力,没有足够的决心去做某些事情,终究很难成事。

如果你只是带着试试看的心态,那么你最后只会以失败而告终,你会一事无成。尝试纯粹是一种借口,你还没有做,就已经给自己想好退路了。不能试验。你只有两个选择: 做,或者不做

你是否能挣到钱,最关键的因素并不在于你是不是有一个好点子。你有多聪明也不是主要原因,决定因素是你的自信程度。 一个人把精力集中在自己所能做的,知道的和拥有的东西上的那一天起,他的成功就已经拉开了序幕。

你最好想清楚,你喜欢做什么,然后再考虑你怎么用它来挣钱。

你要每天不间断地去做对称的未来意义重大的事情。你为此花费的时间不会超过 10 分钟,但是就是这 10 分钟会让一切变得不同。

当你定下了大目标的时候,就意味着你必须付付出比别人多得多的努力。

公式

72 定理: 用 72 除以你们投资的年收益率的百分比, 得出的数字就是这笔钱翻一倍所要的年数。

72 小时规定: 当你决定做一件事情的时候,你必须在 72 小时之内去做这件事情,否则你很可能 就永远不会再做了。

72 公式 也可以用来帮助我们计算通货膨胀。它可以告诉我们,在一定通货膨胀率下,我们的钱在多长时间后会贬值一半。我上网查了一下,我国目前的通货膨胀率大概是 3% ,用 72 除以 3% ,得到 24,就是说 24 年以后,你的钱只值现在的一半。”

买基金为自己加薪

学习一门技能时,尽量选择积累性很强的技能,随着时间的推移,这个技能越来越厉害,那这个技能就很值得去学习。毫无疑问,投资理财,这个技能积累性就很强,最重要的是,这个技能适用于任何人、任何行业,因为投资理财 的本质,是用钱来帮你赚钱,这是最高级别的赚钱方式,除非你不想赚钱,否则每个想赚钱的人,都该重视投资理财。越是有钱人,越是懂得投资理财。

银行一年期定存利率大概是 1.5% 左右,而这利率是跑不赢通货膨胀的。一般的年轻人都是把钱放到一些收益稍高一点的理财产品里面,比如:余额宝、微信理财通里。但这也只是比银行还是要好一点而已。

定投十年挣十倍。我建议年轻人应该把部分钱(除去这部分钱,不会对你的生活产生任何影响的,比如你每月工资的 5% - 10% )用来买基金比较好,通过 买基金为自己加薪,每月定投。

当然买基金也是有风险的,建议是了解基金之后再买。

最保险的方法是,自己不懂的东西不要碰。

往后的时间里面,笔者会读更多关于挣钱的书箱,对买基金的策略有了更深的了解后,会再分享。

本质

在笔者看来,故事里面的小姑娘,在后期可以轻松获取金钱的本质:改变了自己。

这和《大学》里面说的:“修身、齐家、治国、平天下” 的道理是一样的, 一切从改变自身做起。

最后

如果想获取更多的财富,就从读如何实现财富自由类的书籍开始吧,从书中获取金钱的规律,从而使自己获取财富变得更轻松一些。

最后送上笔者目前的理财收益图,收益中的 99.9% 都是买基金得到的。

愿你我都能如下图:

JavaScript 数据结构与算法之美 - 归并排序、快速排序、希尔排序、堆排序

JavaScript 数据结构与算法之美

1. 前言

算法为王。

想学好前端,先练好内功,只有内功深厚者,前端之路才会走得更远

笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。

之所以把归并排序、快速排序、希尔排序、堆排序放在一起比较,是因为它们的平均时间复杂度都为 O(nlogn)

请大家带着问题:快排和归并用的都是分治**,递推公式和递归代码也非常相似,那它们的区别在哪里呢 ? 来阅读下文。

2. 归并排序(Merge Sort)

**

排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

归并排序采用的是分治**

分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。

merge-sort-example.png

注:x >> 1 是位运算中的右移运算,表示右移一位,等同于 x 除以 2 再取整,即 x >> 1 === Math.floor(x / 2) 。

实现

const mergeSort = arr => {
	//采用自上而下的递归方法
	const len = arr.length;
	if (len < 2) {
		return arr;
	}
	// length >> 1 和 Math.floor(len / 2) 等价
	let middle = Math.floor(len / 2),
		left = arr.slice(0, middle),
		right = arr.slice(middle); // 拆分为两个子数组
	return merge(mergeSort(left), mergeSort(right));
};

const merge = (left, right) => {
	const result = [];

	while (left.length && right.length) {
		// 注意: 判断的条件是小于或等于,如果只是小于,那么排序将不稳定.
		if (left[0] <= right[0]) {
			result.push(left.shift());
		} else {
			result.push(right.shift());
		}
	}

	while (left.length) result.push(left.shift());

	while (right.length) result.push(right.shift());

	return result;
};

测试

// 测试
const arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
console.time('归并排序耗时');
console.log('arr :', mergeSort(arr));
console.timeEnd('归并排序耗时');
// arr : [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
// 归并排序耗时: 0.739990234375ms

分析

  • 第一,归并排序是原地排序算法吗 ?
    这是因为归并排序的合并函数,在合并两个有序数组为一个有序数组时,需要借助额外的存储空间。
    实际上,尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)。
    所以,归并排序不是原地排序算法。

  • 第二,归并排序是稳定的排序算法吗 ?
    merge 方法里面的 left[0] <= right[0] ,保证了值相同的元素,在合并前后的先后顺序不变。归并排序是一种稳定的排序方法。

  • 第三,归并排序的时间复杂度是多少 ?
    从效率上看,归并排序可算是排序算法中的佼佼者。假设数组长度为 n,那么拆分数组共需 logn 步, 又每步都是一个普通的合并子数组的过程,时间复杂度为 O(n),故其综合时间复杂度为 O(nlogn)。
    最佳情况:T(n) = O(nlogn)。
    最差情况:T(n) = O(nlogn)。
    平均情况:T(n) = O(nlogn)。

动画

merge-sort.gif

3. 快速排序 (Quick Sort)

快速排序的特点就是快,而且效率高!它是处理大数据最快的排序算法之一。

**

  • 先找到一个基准点(一般指数组的中部),然后数组被该基准点分为两部分,依次与该基准点数据比较,如果比它小,放左边;反之,放右边。
  • 左右分别用一个空数组去存储比较后的数据。
  • 最后递归执行上述操作,直到数组长度 <= 1;

特点:快速,常用。

缺点:需要另外声明两个数组,浪费了内存空间资源。

实现

方法一:

const quickSort1 = arr => {
	if (arr.length <= 1) {
		return arr;
	}
	//取基准点
	const midIndex = Math.floor(arr.length / 2);
	//取基准点的值,splice(index,1) 则返回的是含有被删除的元素的数组。
	const valArr = arr.splice(midIndex, 1);
	const midIndexVal = valArr[0];
	const left = []; //存放比基准点小的数组
	const right = []; //存放比基准点大的数组
	//遍历数组,进行判断分配
	for (let i = 0; i < arr.length; i++) {
		if (arr[i] < midIndexVal) {
			left.push(arr[i]); //比基准点小的放在左边数组
		} else {
			right.push(arr[i]); //比基准点大的放在右边数组
		}
	}
	//递归执行以上操作,对左右两个数组进行操作,直到数组长度为 <= 1
	return quickSort1(left).concat(midIndexVal, quickSort1(right));
};
const array2 = [5, 4, 3, 2, 1];
console.log('quickSort1 ', quickSort1(array2));
// quickSort1: [1, 2, 3, 4, 5]

方法二:

// 快速排序
const quickSort = (arr, left, right) => {
	let len = arr.length,
		partitionIndex;
	left = typeof left != 'number' ? 0 : left;
	right = typeof right != 'number' ? len - 1 : right;

	if (left < right) {
		partitionIndex = partition(arr, left, right);
		quickSort(arr, left, partitionIndex - 1);
		quickSort(arr, partitionIndex + 1, right);
	}
	return arr;
};

const partition = (arr, left, right) => {
	//分区操作
	let pivot = left, //设定基准值(pivot)
		index = pivot + 1;
	for (let i = index; i <= right; i++) {
		if (arr[i] < arr[pivot]) {
			swap(arr, i, index);
			index++;
		}
	}
	swap(arr, pivot, index - 1);
	return index - 1;
};

const swap = (arr, i, j) => {
	let temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
};

测试

// 测试
const array = [5, 4, 3, 2, 1];
console.log('原始array:', array);
const newArr = quickSort(array);
console.log('newArr:', newArr);
// 原始 array:  [5, 4, 3, 2, 1]
// newArr:     [1, 4, 3, 2, 5]

分析

  • 第一,快速排序是原地排序算法吗 ?
    因为 partition() 函数进行分区时,不需要很多额外的内存空间,所以快排是原地排序算法。

  • 第二,快速排序是稳定的排序算法吗 ?
    和选择排序相似,快速排序每次交换的元素都有可能不是相邻的,因此它有可能打破原来值为相同的元素之间的顺序。因此,快速排序并不稳定。

  • 第三,快速排序的时间复杂度是多少 ?
    极端的例子:如果数组中的数据原来已经是有序的了,比如 1,3,5,6,8。如果我们每次选择最后一个元素作为 pivot,那每次分区得到的两个区间都是不均等的。我们需要进行大约 n 次分区操作,才能完成快排的整个过程。每次分区我们平均要扫描大约 n / 2 个元素,这种情况下,快排的时间复杂度就从 O(nlogn) 退化成了 O(n2)。
    最佳情况:T(n) = O(nlogn)。
    最差情况:T(n) = O(n2)。
    平均情况:T(n) = O(nlogn)。

动画

quick-sort.gif

解答开篇问题

快排和归并用的都是分治**,递推公式和递归代码也非常相似,那它们的区别在哪里呢 ?

快速排序与归并排序

可以发现:

  • 归并排序的处理过程是由下而上的,先处理子问题,然后再合并。
  • 而快排正好相反,它的处理过程是由上而下的,先分区,然后再处理子问题。
  • 归并排序虽然是稳定的、时间复杂度为 O(nlogn) 的排序算法,但是它是非原地排序算法。
  • 归并之所以是非原地排序算法,主要原因是合并函数无法在原地执行。
  • 快速排序通过设计巧妙的原地分区函数,可以实现原地排序,解决了归并排序占用太多内存的问题。

4. 希尔排序(Shell Sort)

**

  • 先将整个待排序的记录序列分割成为若干子序列。
  • 分别进行直接插入排序。
  • 待整个序列中的记录基本有序时,再对全体记录进行依次直接插入排序。

过程

  1. 举个易于理解的例子:[35, 33, 42, 10, 14, 19, 27, 44],我们采取间隔 4。创建一个位于 4 个位置间隔的所有值的虚拟子列表。下面这些值是 { 35, 14 },{ 33, 19 },{ 42, 27 } 和 { 10, 44 }。

栗子

  1. 我们比较每个子列表中的值,并在原始数组中交换它们(如果需要)。完成此步骤后,新数组应如下所示。

栗子

  1. 然后,我们采用 2 的间隔,这个间隙产生两个子列表:{ 14, 27, 35, 42 }, { 19, 10, 33, 44 }。

栗子

  1. 我们比较并交换原始数组中的值(如果需要)。完成此步骤后,数组变成:[14, 10, 27, 19, 35, 33, 42, 44],图如下所示,10 与 19 的位置互换一下。

image.png

  1. 最后,我们使用值间隔 1 对数组的其余部分进行排序,Shell sort 使用插入排序对数组进行排序。

栗子

实现

const shellSort = arr => {
	let len = arr.length,
		temp,
		gap = 1;
	console.time('希尔排序耗时');
	while (gap < len / 3) {
		//动态定义间隔序列
		gap = gap * 3 + 1;
	}
	for (gap; gap > 0; gap = Math.floor(gap / 3)) {
		for (let i = gap; i < len; i++) {
			temp = arr[i];
			let j = i - gap;
			for (; j >= 0 && arr[j] > temp; j -= gap) {
				arr[j + gap] = arr[j];
			}
			arr[j + gap] = temp;
			console.log('arr  :', arr);
		}
	}
	console.timeEnd('希尔排序耗时');
	return arr;
};

测试

// 测试
const array = [35, 33, 42, 10, 14, 19, 27, 44];
console.log('原始array:', array);
const newArr = shellSort(array);
console.log('newArr:', newArr);
// 原始 array:   [35, 33, 42, 10, 14, 19, 27, 44]
// arr      :   [14, 33, 42, 10, 35, 19, 27, 44]
// arr      :   [14, 19, 42, 10, 35, 33, 27, 44]
// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]
// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]
// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]
// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]
// arr      :   [10, 14, 19, 27, 35, 33, 42, 44]
// arr      :   [10, 14, 19, 27, 35, 33, 42, 44]
// arr      :   [10, 14, 19, 27, 33, 35, 42, 44]
// arr      :   [10, 14, 19, 27, 33, 35, 42, 44]
// arr      :   [10, 14, 19, 27, 33, 35, 42, 44]
// 希尔排序耗时: 3.592041015625ms
// newArr:     [10, 14, 19, 27, 33, 35, 42, 44]

分析

  • 第一,希尔排序是原地排序算法吗 ?
    希尔排序过程中,只涉及相邻数据的交换操作,只需要常量级的临时空间,空间复杂度为 O(1) 。所以,希尔排序是原地排序算法。

  • 第二,希尔排序是稳定的排序算法吗 ?
    我们知道,单次直接插入排序是稳定的,它不会改变相同元素之间的相对顺序,但在多次不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,可能导致相同元素相对顺序发生变化。
    因此,希尔排序不稳定

  • 第三,希尔排序的时间复杂度是多少 ?
    最佳情况:T(n) = O(n logn)。
    最差情况:T(n) = O(n (log(n))2)。
    平均情况:T(n) = 取决于间隙序列。

动画

shell-sort.gif

5. 堆排序(Heap Sort)

堆的定义

堆其实是一种特殊的树。只要满足这两点,它就是一个堆。

  • 堆是一个完全二叉树。
    完全二叉树:除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。
  • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。
    也可以说:堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。这两种表述是等价的。

对于每个节点的值都大于等于子树中每个节点值的堆,我们叫作大顶堆
对于每个节点的值都小于等于子树中每个节点值的堆,我们叫作小顶堆

区分堆、大顶堆、小顶堆

其中图 1 和 图 2 是大顶堆,图 3 是小顶堆,图 4 不是堆。除此之外,从图中还可以看出来,对于同一组数据,我们可以构建多种不同形态的堆。

**

  1. 将初始待排序关键字序列 (R1, R2 .... Rn) 构建成大顶堆,此堆为初始的无序区;
  2. 将堆顶元素 R[1] 与最后一个元素 R[n] 交换,此时得到新的无序区 (R1, R2, ..... Rn-1) 和新的有序区 (Rn) ,且满足 R[1, 2 ... n-1] <= R[n]。
  3. 由于交换后新的堆顶 R[1] 可能违反堆的性质,因此需要对当前无序区 (R1, R2 ...... Rn-1) 调整为新堆,然后再次将 R[1] 与无序区最后一个元素交换,得到新的无序区 (R1, R2 .... Rn-2) 和新的有序区 (Rn-1, Rn)。不断重复此过程,直到有序区的元素个数为 n - 1,则整个排序过程完成。

实现

// 堆排序
const heapSort = array => {
	console.time('堆排序耗时');
	// 初始化大顶堆,从第一个非叶子结点开始
	for (let i = Math.floor(array.length / 2 - 1); i >= 0; i--) {
		heapify(array, i, array.length);
	}
	// 排序,每一次 for 循环找出一个当前最大值,数组长度减一
	for (let i = Math.floor(array.length - 1); i > 0; i--) {
		// 根节点与最后一个节点交换
		swap(array, 0, i);
		// 从根节点开始调整,并且最后一个结点已经为当前最大值,不需要再参与比较,所以第三个参数为 i,即比较到最后一个结点前一个即可
		heapify(array, 0, i);
	}
	console.timeEnd('堆排序耗时');
	return array;
};

// 交换两个节点
const swap = (array, i, j) => {
	let temp = array[i];
	array[i] = array[j];
	array[j] = temp;
};

// 将 i 结点以下的堆整理为大顶堆,注意这一步实现的基础实际上是:
// 假设结点 i 以下的子堆已经是一个大顶堆,heapify 函数实现的
// 功能是实际上是:找到 结点 i 在包括结点 i 的堆中的正确位置。
// 后面将写一个 for 循环,从第一个非叶子结点开始,对每一个非叶子结点
// 都执行 heapify 操作,所以就满足了结点 i 以下的子堆已经是一大顶堆
const heapify = (array, i, length) => {
	let temp = array[i]; // 当前父节点
	// j < length 的目的是对结点 i 以下的结点全部做顺序调整
	for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
		temp = array[i]; // 将 array[i] 取出,整个过程相当于找到 array[i] 应处于的位置
		if (j + 1 < length && array[j] < array[j + 1]) {
			j++; // 找到两个孩子中较大的一个,再与父节点比较
		}
		if (temp < array[j]) {
			swap(array, i, j); // 如果父节点小于子节点:交换;否则跳出
			i = j; // 交换后,temp 的下标变为 j
		} else {
			break;
		}
	}
};

测试

const array = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
console.log('原始array:', array);
const newArr = heapSort(array);
console.log('newArr:', newArr);
// 原始 array:  [4, 6, 8, 5, 9, 1, 2, 5, 3, 2]
// 堆排序耗时: 0.15087890625ms
// newArr:     [1, 2, 2, 3, 4, 5, 5, 6, 8, 9]

分析

  • 第一,堆排序是原地排序算法吗 ?
    整个堆排序的过程,都只需要极个别临时存储空间,所以堆排序是原地排序算法。

  • 第二,堆排序是稳定的排序算法吗 ?
    因为在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序。
    所以,堆排序是不稳定的排序算法。

  • 第三,堆排序的时间复杂度是多少 ?
    堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn),所以,堆排序整体的时间复杂度是 O(nlogn)。
    最佳情况:T(n) = O(nlogn)。
    最差情况:T(n) = O(nlogn)。
    平均情况:T(n) = O(nlogn)。

动画

heap-sort.gif

heap-sort2.gif

6. 排序算法的复杂性对比

复杂性对比

名称 最好 平均 最坏 内存 稳定性 备注
归并排序 nlog(n) nlog(n) nlog(n) n Yes ...
快速排序 nlog(n) nlog(n) n2 log(n) No 在 in-place 版本下,内存复杂度通常是 O(log(n))
希尔排序 nlog(n) 取决于差距序列 n(log(n))2 1 No ...
堆排序 nlog(n) nlog(n) nlog(n) 1 No ...
名称 平均 最好 最坏 空间 稳定性 排序方式
归并排序 O(n log n) O(n log n) O(n log n) O(n) Yes Out-place
快速排序 O(n log n) O(n log n) O(n2) O(logn) No In-place
希尔排序 O(n log n) O(n log2 n) O(n log2 n) O(1) No In-place
堆排序 O(n log n) O(n log n) O(n log n) O(1) No In-place

算法可视化工具

  • 算法可视化工具 algorithm-visualizer
    算法可视化工具 algorithm-visualizer 是一个交互式的在线平台,可以从代码中可视化算法,还可以看到代码执行的过程。

效果如下图。

算法可视化工具

旨在通过交互式可视化的执行来揭示算法背后的机制。

insert-sort.gif

变量和操作的可视化表示增强了控制流和实际源代码。您可以快速前进和后退执行,以密切观察算法的工作方式。

binary-search.gif

7. 文章输出计划

JavaScript 数据结构与算法之美 的系列文章,坚持 3 - 7 天左右更新一篇,暂定计划如下表。

标题 链接
时间和空间复杂度 #29
线性表(数组、链表、栈、队列) #34
实现一个前端路由,如何实现浏览器的前进与后退 ? #30
栈内存与堆内存 、浅拷贝与深拷贝 #35
递归 #36
非线性表(树、堆) #37
冒泡排序、选择排序、插入排序 #39
归并排序、快速排序、希尔排序、堆排序 #40
计数排序、桶排序、基数排序 精彩待续
十大经典排序汇总 精彩待续
强烈推荐 GitHub 上值得前端学习的数据结构与算法项目 #43

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。

8. 最后

文中所有的代码及测试事例都已经放到我的 GitHub 上了。

觉得有用 ?喜欢就收藏,顺便点个赞吧。

参考文章:

vue 移动端复杂表格表头,固定表头与固定第一列

复杂表格表头

前言

最近做移动端的h5项目,要做一个可配置表头的复杂表格,网上找了很久也没什么好方法,结合网上的一些例子,在此做一了一个完整的vue版的例子。

效果

无图无真相,先上最终效果图再说 。
table.gif

方法一:iscroll 插件版

第一步:npm install

引入 iscroll

npm i iscroll --save 

第二步:封装

对插件再做一层封装,封装成 iscrollTable.js 方便调用,代码如下:

// 统一使用
const iScollProbe = require('iscroll/build/iscroll-probe');
let scroller = null;
let Selector = "";
export function createIScroller(selector) {
  Selector = selector;
  scroller = new iScollProbe(Selector, {
    preventDefault: false,  // 阻止浏览器滑动默认行为
    probeType: 3, //需要使用 iscroll-probe.js 才能生效 probeType : 1 滚动不繁忙的时候触发 probeType : 2 滚动时每隔一定时间触发 probeType : 3   每滚动一像素触发一次
    mouseWheel: true, //是否监听鼠标滚轮事件。
    scrollX: true,  // 启动x轴滑动
    scrollY: true,  // 启动y轴滑动
    // momentum: false,
    lockDirection: false,
    snap: false, //自动分割容器,用于制作走马灯效果等。Options.snap:true// 根据容器尺寸自动分割
    //snapSpeed: 400,
    scrollbars: false, //是否显示默认滚动条
    freeScroll: true, //主要在上下左右滚动都生效时使用,可以向任意方向滚动。
    deceleration: 0.0001, //滚动动量减速越大越快,建议不大于 0.01,默认:0.0006
    disableMouse: true, //是否关闭鼠标事件探测。如知道运行在哪个平台,可以开启它来加速。
    disablePointer: true, //是否关闭指针事件探测。如知道运行在哪个平台,可以开启它来加速。
    disableTouch: false, //是否关闭触摸事件探测。如知道运行在哪个平台,可以开启它来加速。
    eventPassthrough: false, //使用 IScroll 的横轴滚动时,如想使用系统立轴滚动并在横轴上生效,请开启。
    bounce: false //是否启用弹力动画效果,关掉可以加速
  });
  scroller.on('scroll', updatePosition);
  scroller.on('scrollEnd', updatePosition);
  scroller.on('beforeScrollStart', function () {
    scroller.refresh();
  });

  function updatePosition() {
    let frozenCols = document.querySelectorAll(selector + ' table tr td.cols');
    let frozenRows = document.querySelectorAll(selector + ' table tr th.rows');
    let frozenCrosses = document.querySelectorAll(selector + ' table tr th.cross');
    for (let i = 0; i < frozenCols.length; i++) {
      frozenCols[i].style.transform = 'translate(' + -1 * this.x + 'px, 0px) translateZ(0px)';
    }
    for (let i = 0; i < frozenRows.length; i++) {
      frozenRows[i].style.transform = 'translate(0px, ' + -1 * this.y + 'px) translateZ(0px)';
    }
    for (let i = 0; i < frozenCrosses.length; i++) {
      frozenCrosses[i].style.transform = 'translate(' + -1 * this.x + 'px,' + -1 * this.y + 'px) translateZ(0px)';
    }
  }

  return scroller;
}

export function refreshScroller() {
  if (scroller === null) {
    console.error("先初始化scroller");
    return;
  }
  setTimeout(() => {
    scroller.refresh();
    scroller.scrollTo(0, 0);
    let frozenCols = document.querySelectorAll(Selector + ' table tr td.cols');
    let frozenRows = document.querySelectorAll(Selector + ' table tr th.rows');
    let frozenCrosses = document.querySelectorAll(Selector + ' table tr th.cross');
    for (let i = 0; i < frozenCols.length; i++) {
      frozenCols[i].style.transform = 'translate(0px, 0px) translateZ(0px)';
    }
    for (let i = 0; i < frozenRows.length; i++) {
      frozenRows[i].style.transform = 'translate(0px, 0px) translateZ(0px)';
    }
    for (let i = 0; i < frozenCrosses.length; i++) {
      frozenCrosses[i].style.transform = 'translate(0px, 0px) translateZ(0px)';
    }
  }, 0);
}

###第三步:使用
引用前面的自己封装的iscrollTable.js,用到的table.vue的具体代码如下:

<template>
    <div class="pages-tables " id="pages-tables">
        <div class="waterMask" id="watermark"></div>
        <div class="rolling-table meal-table" ref="tableBox" :style="{height: maxHeight + 'px'}">
            <table class="table" id="table" cellpadding="0" cellspacing="0" ref="rollingTable">
                <tr v-for="(x,i) in xList" :key="i">
                    <th class="rows " :class="{'cross': index == 0 && i == 0}" v-for="(l,index) in x" :key="index" :colspan="l.colspan" :rowspan="l.rowspan">{{l.name}}</th>
                </tr>
                <tr v-for="(l,i) in yList" :key="i + 'a'">
                    <template v-for="(x, xKey) in xField">
                        <td v-for="(ll,yKey) in l" :key="yKey" v-if="x === yKey" :class="{'cols': yKey == xField[0]}">
                            {{ yList[i][yKey]}}
                        </td>
                    </template>
                </tr>
                <tr></tr>
            </table>
        </div>
    </div>
</template>
<script>
import { createIScroller, refreshScroller } from "libs/iscrollTable";
import { addWaterMarker } from "libs/common/common";
export default {
    data() {
        return {
            maxHeight:'100%',
            scroll: {
                scroller: null
            },
            xList: [
                [
                    {
                        field_name: "statis_date",
                        name: "第一行合并3行1列",
                        colspan: 1, //指定单元格 横向 跨越的 列数
                        rowspan: 3, //指定单元格 纵向 跨越的 行数
                    },
                    {
                        field_name: "custom_field",
                        name: "第一行合并2列",
                        colspan: 2,
                        rowspan: 1,
                    },
                    {
                        field_name: "custom_field",
                        name: "第一行合并2列",
                        colspan: 2,
                        rowspan: 1,
                    },
                    {
                        field_name: "custom_field",
                        name: "第一行合并3列",
                        colspan: 3,
                        rowspan: 1,
                    },
                ],
                [
                    {
                        field_name: "custom_field",
                        name: "第二行日期",
                        colspan: 1, //指定单元格 横向 跨越的 列数
                        rowspan: 1, //指定单元格 纵向 跨越的 行数
                    },
                    {
                        field_name: "custom_field",
                        name: "第二行日期合并2列",
                        colspan: 2,
                        rowspan: 1,
                    },
                    {
                        field_name: "custom_field",
                        name: "第二行日期合并2列",
                        colspan: 2,
                        rowspan: 1,
                    },
                    {
                        field_name: "custom_field",
                        name: "第二行日期合并3列",
                        colspan: 3,
                        rowspan: 1,
                    },
                ],
                [
                    {
                        field_name: "area_name",
                        name: "第三行当月新增",
                        colspan: 1,  //指定单元格 横向 跨越的 列数
                        rowspan: 1, //指定单元格 纵向 跨越的 行数
                    },
                    {
                        field_name: "area_name1",
                        name: "第三行当月新增1",
                        colspan: 1,
                        rowspan: 1,
                    },
                    {
                        field_name: "area_name2",
                        name: "第三行当月新增2",
                        colspan: 1,
                        rowspan: 1,
                    },
                    {
                        field_name: "area_name3",
                        name: "第三行当月新增3",
                        colspan: 1,
                        rowspan: 1,
                    },
                    {
                        field_name: "area_name4",
                        name: "第三行当月新增4",
                        colspan: 1,
                        rowspan: 1,
                    },
                    {
                        field_name: "area_name5",
                        name: "第三行当月新增5",
                        colspan: 1,
                        rowspan: 1,
                    },
                    {
                        field_name: "area_name6",
                        name: "第三行当月新增6",
                        colspan: 1,
                        rowspan: 1,
                    },
                ],
            ],
            xField: ['statis_date', 'area_name', "area_name1", "area_name2", "area_name3", "area_name4", "area_name5", "area_name6",],
            yList: [
                {
                    area_name: "新增数据开始",
                    area_name1: "新增数据开始1",
                    area_name2: "新增数据开始2",
                    area_name3: "新增数据开始3",
                    area_name4: "新增数据开始4",
                    area_name5: "新增数据开始5",
                    area_name6: "新增数据开始6",
                    statis_date: 100007,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据最后",
                    area_name1: "新增数据最后1",
                    area_name2: "新增数据最后2",
                    area_name3: "新增数据最后3",
                    area_name4: "新增数据最后4",
                    area_name5: "新增数据最后5",
                    area_name6: "新增数据最后6",
                    statis_date: 222222,
                }
            ]
        }
    },
    mounted() {
        this.maxHeight = window.screen.height
        this.scroll.scroller = createIScroller(".meal-table");
        // addWaterMarker(document.getElementById('watermark'))
    }
}

</script>
<style lang="less" scoped>
.pages-tables {
  -webkit-overflow-scrolling: touch; // ios滑动顺畅
  position: relative;
}
.rolling-table {
    height: 100%;
    font-size: 0.28rem;
    color: #86939a;
    background-color: #fff;
    width: 100%;
    -webkit-overflow-scrolling: touch;
    position: relative;
    top: 0;
    overflow: hidden;
  }
.rows {
    position: relative;
    z-index: 3;
}
.cross {
    position: relative;
    z-index: 5;
}
table td {
  border: 0px solid #000;
  font-size: 0.32rem;
  background: #fff;
}
::-webkit-scrollbar {
    display: none;
}
.table {
//   border-collapse: collapse; //去掉重复的border
  color: #86939e;
  font-size: 0.32rem;
  border: 0px solid #000;
  min-height: 100%;
  text-align: center;
  td {
    border-bottom: 0.02rem solid #eee;
    white-space: nowrap;
    height: 0.86rem;
    line-height: 0.86rem;
    padding: 0 0.2rem;
  }
  th {
    color: #43484d;
    white-space: nowrap;
    height: 0.74rem;
    line-height: 0.74rem;
    padding: 0rem 0.3rem;
    background-color: #f3f4f6;
    font-weight: normal;
    padding-bottom: 0;
    padding-top: 0;
    border: 0.02rem solid red;
  }
}
tr{
    position: relative;
    background-color: #fff;
    &:nth-of-type(odd){
        td{
            // background-color: pink;
        }
    }
}
</style>

注意点:

  1. table 外的盒子 .rolling-table 要设置高度,不然向上滚动失效
    2.固定和行与列,即:rows、cross 的position要设为relative

最终效果就如上图。

方法二: 结合css,自定义封装版

原理

因为除了表头和第一列,其他都可以滚动所以需要:
1.一个展示的table表格
2.一个用来覆盖上表头的 thead,一个用来覆盖左上角的 div,一个固定在第一列的 tbody。
3. 展示的table表格放在最底层,覆盖上表头的 thead固定定位在最上面,固定在第一列的 tbody固定定位在最左边,左上角的 div固定是左上角且z-index最大,在最上层。
4. 固定的表格头部与第一列的宽、高、行高都是通过获取真实的表格的宽高来设定的。
5. 通过展示的table表格的上下滚动从而带动固定在第一列的 tbody向上滚动,向左右滚动带动覆盖上表头的 thead的左右滚动。

完整代码如下:

<template>
    <div class="pages" id="pages" :style="{height: maxHeight + 'px'}">
        <table id="table" class="table" cellpadding="0" cellspacing="0">
            <thead v-show="showFixedHeaderCloFirst" class="fixedThead" id="fixedThead" :style="{left: fixedTheadLeft}">
                <tr v-for="(x,i) in xList"   :key="i+'a'">
                    <th v-for="(l,i) in x" :key="i" :colspan="l.colspan" :rowspan="l.rowspan">{{l.name}}</th>
                </tr>
            </thead>
            <div v-show="showFixedHeaderCloFirst" class="fixedHeaderCenter" id="fixedHeaderCenter" :style="{'line-height': firstCloHeight + 'px',width: firstCloWidth-1 + 'px'}">{{xList[0][0].name}}</div>

            <tbody v-show="showFixedHeaderCloFirst" class="fixedHeaderCloFirst" id="fixedHeaderCloFirst" :style="{'top': fixedHeaderCloFirstTop ,'height': fixedHeaderCloFirstHeight}">
                <tr v-for="(l,i) in yList" :key="i" :style="{width: firstCloWidth + 'px',height: cloFirstLineHeight + 'px'}">
                    <td :style="{width: firstCloWidth + 'px',height: cloFirstLineHeight -1 + 'px' }">{{l.statis_date }} </td>
                </tr>
            </tbody>
            
            <thead >
                <tr v-for="(x,index) in xList"   :key="index+'b'">
                    <th :class="{firstCol: index == 0 && i == 0}" v-for="(l,i) in x" :key="i" :colspan="l.colspan" :rowspan="l.rowspan">{{l.name}}</th>
                </tr>
            </thead>
            <tbody id="tbody">
                <tr  v-for="(l,i) in yList" :key="i">
                    <td v-for="(x,xKey) in xField" :key="xKey">
                        <div v-for="(ll,yKey) in l" v-if="x === yKey" :key="yKey" :class="{'tables-content-item-yellow':yList[i][yKey] > 0 && validateVal(x),'tables-content-item-green':yList[i][yKey] <= 0 && validateVal(x),}">
                            {{ yList[i][yKey]}}
                        </div>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</template>
<script>
export default {
    data() {
        return {
            maxHeight: '100%',
            fixedTheadLeft: 0,
            fixedHeaderCloFirstTop: 0,
            fixedHeaderCloFirstHeight: '100%',
            th: [],
            tl: [],
            temp: [],
            firstCloWidth: 0,
            firstCloHeight: 0,
            showFixedHeaderCloFirst: false,
            cloFirstLineHeight: '0',
            fixedCol: false,
            fixedHeader: false,
            fixedA1: false,
            hLeft: 0,
            hHeight: 0,
            xList: [
                [
                    {
                        field_name: "statis_date",
                        name: "第一行合并3行1列",
                        colspan: 1, //指定单元格 横向 跨越的 列数
                        rowspan: 3, //指定单元格 纵向 跨越的 行数
                    },
                    {
                        field_name: "custom_field",
                        name: "第一行合并2列",
                        colspan: 2,
                        rowspan: 1,
                    },
                    {
                        field_name: "custom_field",
                        name: "第一行合并2列",
                        colspan: 2,
                        rowspan: 1,
                    },
                    {
                        field_name: "custom_field",
                        name: "第一行合并3列",
                        colspan: 3,
                        rowspan: 1,
                    },
                ],
                [
                    {
                        field_name: "custom_field",
                        name: "第二行日期",
                        colspan: 1, //指定单元格 横向 跨越的 列数
                        rowspan: 1, //指定单元格 纵向 跨越的 行数
                    },
                    {
                        field_name: "custom_field",
                        name: "第二行日期合并2列",
                        colspan: 2,
                        rowspan: 1,
                    },
                    {
                        field_name: "custom_field",
                        name: "第二行日期合并2列",
                        colspan: 2,
                        rowspan: 1,
                    },
                    {
                        field_name: "custom_field",
                        name: "第二行日期合并3列",
                        colspan: 3,
                        rowspan: 1,
                    },
                ],
                [
                    {
                        field_name: "area_name",
                        name: "第三行当月新增",
                        colspan: 1,  //指定单元格 横向 跨越的 列数
                        rowspan: 1, //指定单元格 纵向 跨越的 行数
                    },
                    {
                        field_name: "area_name1",
                        name: "第三行当月新增1",
                        colspan: 1,
                        rowspan: 1,
                    },
                    {
                        field_name: "area_name2",
                        name: "第三行当月新增2",
                        colspan: 1,
                        rowspan: 1,
                    },
                    {
                        field_name: "area_name3",
                        name: "第三行当月新增3",
                        colspan: 1,
                        rowspan: 1,
                    },
                    {
                        field_name: "area_name4",
                        name: "第三行当月新增4",
                        colspan: 1,
                        rowspan: 1,
                    },
                    {
                        field_name: "area_name5",
                        name: "第三行当月新增5",
                        colspan: 1,
                        rowspan: 1,
                    },
                    {
                        field_name: "area_name6",
                        name: "第三行当月新增6",
                        colspan: 1,
                        rowspan: 1,
                    },
                ],
            ],
            xField: ['statis_date', 'area_name', "area_name1", "area_name2", "area_name3", "area_name4", "area_name5", "area_name6",],
            yList: [
                {
                    area_name: "新增数据开始",
                    area_name1: "新增数据开始1",
                    area_name2: "新增数据开始2",
                    area_name3: "新增数据开始3",
                    area_name4: "新增数据开始4",
                    area_name5: "新增数据开始5",
                    area_name6: "新增数据开始6",
                    statis_date: 100007,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据",
                    area_name1: "新增数据1",
                    area_name2: "新增数据2",
                    area_name3: "新增数据3",
                    area_name4: "新增数据4",
                    area_name5: "新增数据5",
                    area_name6: "新增数据6",
                    statis_date: 201807,
                },
                {
                    area_name: "新增数据最后",
                    area_name1: "新增数据最后1",
                    area_name2: "新增数据最后2",
                    area_name3: "新增数据最后3",
                    area_name4: "新增数据最后4",
                    area_name5: "新增数据最后5",
                    area_name6: "新增数据最后6",
                    statis_date: 222222,
                }
            ]
        }
    },
    filters: {
        dateFromt: function (value) {
            if (!value) return ''
            value = value.toString()
            return value.slice(4)
        },
    },
    methods: {
        // 返回
        vdVal(val, value) {
            return value
        },
        // 是否包含 % 标志,有就要颜色控制
        validateVal(value) {
            let flag = false
            this.xList.forEach((e, i) => {
                if (e.zd_name == value) {
                    if (e.zx_name.includes('%')) {
                        flag = true
                    }
                }
            })
            return flag
        },
        $$(dom){
            return document.getElementById(dom)
        },
        savePosition(){
            this.maxHeight = window.screen.height
            this.$$("pages").onscroll = (()=> {
                let offsetHeight = this.$$("fixedHeaderCenter").offsetHeight
                let scrollTop = offsetHeight + 1 - this.$$("pages").scrollTop
                this.fixedTheadLeft =  - this.$$("pages").scrollLeft + 'px'
                this.fixedHeaderCloFirstTop = scrollTop + 'px'
                console.log("scrollTop:",scrollTop)
                console.log("this.fixedHeaderCloFirstTop:",this.fixedHeaderCloFirstTop)
            });
            // this.$$("fixedHeaderCloFirst").onscroll = (()=> {
            //     let offsetHeight = this.$$("fixedHeaderCenter").offsetHeight
            //     let scrollTop = - this.$$("fixedHeaderCloFirst").scrollTop
            //     this.$$("pages").style.left =  - this.$$("fixedHeaderCloFirst").scrollLeft + 'px'
            //     this.$$("table").style.top =  scrollTop + 'px'
            // });
        }
    },
    mounted() {
        setTimeout(()=>{
            this.firstCloWidth = document.getElementsByClassName("firstCol")[0].offsetWidth 
            this.firstCloHeight = document.getElementsByClassName("firstCol")[0].offsetHeight-1
            this.fixedHeaderCloFirstHeight = this.$$("tbody").offsetHeight + 'px'
            this.cloFirstLineHeight = this.$$("tbody").children[0].offsetHeight  + 'px'
            this.fixedHeaderCloFirstTop = this.firstCloHeight + 3 + 'px'
            this.showFixedHeaderCloFirst = true
            this.savePosition()
        },1000)
        
    },
}

</script>
<style lang="less" scoped>
::-webkit-scrollbar {
    display: none;
}
.pages{
    overflow: scroll;
    height: 100%;

}
.fixedHeaderCenter{
    border: 1px solid red;
    background-color: #f3f4f6;
    color: #43484d;
    text-align: center;
    // padding: 0 0.3rem;
    position: fixed;
    left: 0;
    top: 0;
    z-index: 9999;
}
.fixedHeaderCloFirst{
    // border-bottom: 1px solid #eee;
    height: 100%;
    line-height: 0.86rem;
    background: #fff;
    white-space: nowrap;
    text-align: center;
    position: fixed;
    left: 0;
    top: 0;
    th,td{
        padding: 0;
    }
    // overflow: scroll;
    div{
        background-color: #fff;
        border-bottom: 1px solid #eee;
    }
}
@main-color-green: #269513;
@main-color-yellow: #fc9d2e;
table {
  position: relative;
  color: #86939e;
  font-size: 0.32rem;
  border: 0px solid #000;
  min-height: 100%;
  text-align: center;
  white-space:nowrap;
  td {
    border-bottom: 1px solid #eee;
    white-space: nowrap;
    height: 0.86rem;
    line-height: 0.86rem;
    padding: 0 0.2rem;
    white-space:nowrap;
  }
  th {
      white-space:nowrap;
    color: #43484d;
    white-space: nowrap;
    height: 0.74rem;
    line-height: 0.74rem;
    padding: 0rem 0.3rem;
    background-color: #f3f4f6;
    font-weight: normal;
    padding-bottom: 0;
    padding-top: 0;
    border: 1px solid red;
  }
}
.tables-content-item-green {
  color: @main-color-green;
}

.tables-content-item-yellow {
  color: @main-color-yellow;
}
table {
    font-size: 0.3rem;
    // margin: 300px;
    border-collapse:collapse
}
.fixedThead{
    background: lightblue;
    position: fixed;
    top: 0 ;
    z-index: 2;
}
/*固定表头的样式*/
.fixedHeader {
    background: lightblue;
    position: fixed;
    top: 0 ;
    z-index: 2;
}

</style>

最终效果图如下:
table.gif

不过这个版本的上下滚动时的精准计算有点误差。

推荐第一种方式。

最后

希望文章内容对你有一点帮助!

程序员不止眼前的逻辑和代码,还应有健康的体魄和精气神

程序员不止眼前的逻辑和代码,还应有健康的体魄和精气神。

程序猿

对大多数程序猿来说,生活没那么多诗和远方,只有加不完的班,写不完的代码和修不完的 bug。

大多数人对程序猿的看法应该是生活随便,只专注于与电脑交流的一个群体吧,程序猿一天的时间大概有 90% 都是与电脑陪伴的,上班和加班时间都对着电脑,下班了学习时间用电脑,下班的娱乐时间也用电脑,睡觉抱着电脑睡觉,睡前还想着没解决的 bugger !!!梦里还梦见自己敲着代码、修着 bugger(言重了、言重了,开个玩笑!)。

说程序猿没有女朋友、老婆!不存在的!!!电脑就是自己的女朋友、自己的老婆,谁要敢破坏程序猿的电脑,他可是会和你拼命的!!!

程序猿就是这个一个特殊的群体,都说程序猿是在拿身体与时间换未来,真是说的一点也没错。除了晚上是躺着的,大多数时间都是坐着的,就连站着和蹲着的时间都是屈指可数,是不是 ?

但本汪想说:程序员,你的身体还健康吗 ?你重视了吗 ?

危害程序猿的几大害

说程序猿苦逼,恐怕没有几个人会反对,相对之下的高薪也是以牺牲健康为代价。由于工作性质的关系,程序猿健康状况整体不高,在社会公认最毁健康的三件事中,程序猿全占了!

本汪昨晚就因为一个浏览器环境的 bugger,修了将近 3 小时!我说我这样子修仙很快乐,你相信吗 ?我呸,我自己都不相信 !
bugger.jpg

1. 熬夜修仙成常态

修仙.jpg

本汪和你说,本汪从不加班的,本汪只修仙的!因为修仙使本汪快乐!!!

如果有哪个程序猿说自己从没加过班的,肯定是假的程序猿!!!

加班文化成了整个行业默认的潜规则。很多公司甚至在面试环节就明确的问到:你对加班怎么看 ?其实就是想看下你愿不愿意加班的,如果不愿意,肯定是不招你了的。

平常工作是朝九晚五,但在 IT 行业却流行着修福报、朝九晚六,996 和 167 工作制:

朝九晚六:早上 9 点上班,晚上 6 点下班,每周工作 5 天,这是极少数才会有的!!!

996:早上 9 点上班,晚上 9 点下班,每周工作 6 天。

716:每天工作 16 小时,每周工作 7 天。

IT 行业的程序员工作的时长远比正常人长!

除此之外,大假期远程支持,随叫随到。前期赶做需求分析,中期赶项目上线,后期抢修复 Bug 等已成常态,更常有的可能是项目经理安排的项目时间不合理导致的开发时间紧迫,最后还是自己加班修仙。在这种情况下,不仅容易失去另一半,更容易失去健康。

昨天就在其他公众号上看到一篇关于程序猿加班的事件:
有一名程序员周末和女友出去玩,没想到所负责的系统出现了 bug ,领导要求其马上修复 bug 。程序员表示周一上班就解决!这时领导就不乐意了,一顿大道理说教,指责该程序员作为企业员工却没有敬业精神!难道客户至上我程序员还成了店小二不成,他整天有问题我就得 24 小时在线不成 ?抱歉,我是程序员,我是来上班的,不是来卖身的,再见!

因持续加班猝死的新闻我们都已经屡见不鲜了,前年就发生过,年经轻轻的居然比本汪还要早就修成仙了。虽然只是极端现象,但是这种情况的出现也向我们敲响了警钟:班是加不完的,但是身体却是自己的,什么都替代不了自己的健康。

更有去年的苏宁关于某个技术负责人 0 点睡觉很会养生 的事件,项目没事,大家早点睡觉是正常的啦!!!这都还要受到领导的批评,谁又可以料到服务器什么时候抽风了,要崩了呢!所以啊,程序员真难做!

有过熬夜修仙修改 bugger 的程序猿应该都知道,如果几个小时都解决不了的 bugger,你再怎么耗下去也不会有什么结果的,一般加班到 00:00 的时候,自己的大脑已经不怎么会转了,只会让自己越来越累而已,而且长时间的熬夜会使人的记忆力和反应降低,持续影响后期工作效率,造成恶性循环。

还不如让自己先睡一觉,第二天早点起来,大脑经过休息,思维转的够快,你晚上几个小时都搞不定的问题,说不定你思维一转,就找到了问题所在,并有了解决方案,本汪也时常会遇到这种情况。而且本汪觉得在早上思考难点更合适,更容易有想法。

当然经常加班的也需要分析下加班的原因:是工作经验不足还是效率太低 ?是项目需求还是公司文化 ?

如果是因为经验能力不足,那你完全可以利用业余时间多修炼学习,增强专业技能,当然初期经验不足时,加班的确会使自己进步更快,这个得自己结合自己的情况。

如果是工作效率太低,那就需要调整工作方式,找到低效率的原因。比如说很多人是因为在反复的修改调试中浪费了很多时间,那么可以用双屏支架,边写代码边预览,减少反复切换、调试和后期修改时间,提升工作效率。

如果是因为公司文化不得不加班,那就要分析这项工作的意义和价值。除了不计报酬的加班,加点公司还有没有给我更多的技能培训?更好的发展机会?还是只是把自己当做一个不停运转的赚钱机器?如果是后者,那么完全可以修炼好内功,寻找更好的发展平台,或者可以效仿前面那个加班程序猿那样:我是程序员,我是来上班的,不是来卖身的,再见!

你的猫来叫你早点睡啦.gif

你的猫来叫你早点睡啦

2. 久坐时间长

IT 行业的,平时上班、加班必然对应的久坐时间也长。久坐是健康的大敌,这几乎已经成为职场共识。

对普通人来说,每天久坐时间可能只有 8 小时,但是对程序员来说可能就要十几小时以上了:上班是坐着!回家敲代码坐着!空余时间学习对着电脑坐着! 玩游戏坐着!娱乐看电影坐着!这的确是在“慢性自杀”。

研究表明,超过 8 小时以上的久坐能提高 18% 患心血管疾病的风险,17% 得癌症的风险。2 型糖尿病的患病概率提高了 91%。有木有狠狠地吓一跳?好吧,也许这些你都还没有感觉,但是有一个问题你一定已经感觉到了,你的老腰越来越不好了,经常容易酸痛。

腰椎受力.png

看到了吗 ?坐姿弯腰时腰椎受力高达 270%,这就不难解释为什么很多坐着办公的人腰都不怎么好。那么,怎么预防 ?

有些大公司(如谷歌)上班的时候是可以选择站着办公的还是坐着的,非常的棒。

没有只能坐着办公的,那怎么办 ?本汪也偷偷告诉你,本汪是这样子做的,有空时会出去到窗口看看风景,大家都知道平时我们要多喝水,特别是下午,所以没空时我就会多喝水,然后会上 WC 的时候就会多了,逼着自己出去走走!!!

特别是没空的时候,一段高强度的工作时间,对腰和腰椎的负担实在太大了,而且上 WC 的路上,可能还能在路上遇到美女帅哥呢,养养眼呢,命都多几年呢,兴许还能搭个讪什么的呢,以本汪的天生丽质,兴许还能被某个帅哥美女看中呢,是也不是 ?

改变久坐不动的状态吧,多走走,身体更健康,工作效率更高。

如果公司有运动的,本汪推荐大家要积极参加,本汪之前的公司就是每周五下班都会有篮球或者羽毛球的运动的,本汪虽然是个菜鸟,但是我都会积极参与的,而且还要玩到累,玩得尽兴!

现在的公司的同事的年纪都比本汪大不少,公司缺少运动的企业文化,平时没有什么运动的活动,这真的让我很怀念之前公司的运动氛围,和之前同事一起玩的时光也真的让本汪很怀念。

在上一家公司和实习的公司,都是有运动文化的公司,本汪深深的体会到:

只有大家一起玩了,同事之间的相互了解才会更高,大家也会更熟悉,相处的更加和谐,从而促进公司的团队活跃和谐氛围。

3. 长期面对电脑

因为工作性质的原因,程序员的工作是和电脑绑定在一起的。前面也说到电脑就像是程序猿的老婆,女朋友!长时间近距离盯着电脑,除了造成眼睛干涩,视力下降之外,颈椎也会早早老化。为什么 ?盗用网上的一张图来说明!

颈椎伤害.jpg

在电脑前俯身前倾的坐姿不仅伤害腰椎,更伤颈椎。回想下你工作时是不是也是经常这个姿势 ?

WechatIMG152.jpeg

要想颈椎舒适,就要换个姿势。调整坐姿,挺腰抬头,让视线与显示器保持平行,手肘和桌面保持垂直。

反正本汪,颈椎累了,会时不时转动一下脖子的,让颈椎舒适才是王道!

对于眼睛干涩,每天下班的时候,本汪的眼睛也非常干涩,平时工作中,眼睛累了,本汪也只是偶尔咪一下眼睛让眼睛休息一会而已,如果你知道更好的方法,也请在下方留言告诉本汪。

怎样保持身体健康 ?--健身与运动

说到健身与运动,不得不说这是本汪的最爱了,除了修仙能使本汪快乐,运动健身也使本汪快乐。

说到运动,就说一下今年世界杯中依旧很火的的 C 罗吧

先上图

对于今年的世界杯,33 岁的 C 罗,依旧扛着自己的球队向冠军进发

曾经站在足坛顶峰的一些巨星,30 岁之后状态急剧下滑

而对 33 岁的 C 罗来说,岁月仿佛在他的身体上没留下任何痕迹

今年 5 月,接受媒体采访的 C 罗说:“如今的我,有着 23 岁的身体。”

所以健康吧!运动吧!少年们!!!说不定自己在 30 岁的时候还能像现在二十几岁一样精力充沛。

无氧运动 ---- 健身

说到健身,本汪可是有一定的健身基础的,毕竟大三的时候跟着我们班的中级健身教练认真健身了一年多,一年多的时间增重了十几斤,胸肌、腹肌、肱二头肌等都有了,效果也比较明显。

没有健身基础的朋友,建议去网上找一些图文教程或者视频来看下,当初我就是这样子健身过来的,前面说的我们班的健身教练,当然一开始他也是不太懂的,健身教练也是他后来考到的。

现在网络那么发达,只要你想学一门技能,大多都在网上找到方法,就看你怎么选择与坚持而已。当然下载app和关注一些健身的公众号也是很好的选择,本汪就关注了几个健身的公众号来学习与坚持锻炼。

还有一个很好的方法是找一个志同道合的道友一起锻炼,相互监督与鼓励,健身之路会走得更远。

在此分享一下本汪的健身时间,从大四再到工作后的现在,已经没有那么多时间用来专门健身了,现在基本就坚持每周锻炼二次左右,一般都在晚上 22:30 左右开始锻炼半个钟到 45 分钟,然后休息一会就洗澡睡觉去,简直完美。

再告诉大家一个高效利用的方法,我一般在晚上如果想放松或者看电影的时候,都是放着电影,站在电脑前举着哑铃来锻炼的,一边锻炼一边看电影,放松与健身两不误!

健身除了塑形之外呢,还可以增强体质。健身不一定要去健身房的,在家里就可以了。锻炼胸肌、背肌、腹肌、手臂、你只要买了哑铃和杠铃,大部分健身动作就都可以完成了,我宿舍就有一个哑铃,真的很方便锻炼。

不过健身一定要注意动作的正确性,这个是极其关键的,动作不对,除了健身效果不明显之外,还会伤到肌肉和骨头。想当初大学第一次去隔壁宿舍做仰卧起坐的时候,做到了极限方法还不太对,最后伤的颈椎,痛了一周,难受。

最重要的是很久没怎么锻炼过的人,在最初的几次锻炼,不要锻炼到极限,因为没有什么基础,一到极限,过后的几天非常的腰酸背痛,会打消自己的积极性的。

如果周围没有熟悉健身的朋友,可以看一些健身视频,慢慢揣摩动作。

有氧运动 ---- 跑步

先上图
程序员进化之路.jpg

这是网上流传的一幅图,说的是等级与身材的对比图,对比一下自己,是不是在自己的身上得到了印证(奸笑)?相信不少人都发现身边的程序员要不就是比较瘦的,要不就是有着肚腩的,而且有着肚腩的正好和这图的规律一致!

平时周末大家还是多跑步吧!跑步是全身的锻炼,内外都能锻炼到。跑步的效果有多好,自己百度一下就知道了。

有好身材的人自信心都提高很多的哦!而且穿什么衣服都好看!

running.png

本汪就是喜欢跑步的汪了,一般周末都会去附近的大学跑道里面跑步,几乎每周都会去二次左右。附近没有大学的朋友们,可以选择附近的公园或者适合跑步的场所去跑步。本汪之所以不想搬到其他地方住,就是因为这附近有公园、有大学,真的非常棒!选择住处的时候,最好选择有运动场所的地方。

跑步真的是让人愉悦的事情,跑步的过程中可挑战自己的极限,还可以发现不一样的自己。

我记得刚毕业实习的时候一般夜跑都是 3 公里,到后来觉得 3 公里没什么意思了,就跑 4 公里,再后来发现 4 公里也没什么感觉了,就到了现在的能跑 7 公里,估计坚持下去还能跑更远,但是本汪是偏瘦的体型,所以不适合跑那么久,跑那么久消耗是很大的,在消耗脂肪的同时也会消耗我的肌肉,所以现在周末跑步就定在 5 公里好了,比较合适本汪的距离。当然自己的情况自己来定。

能有个伴跑步也是不错的选择。

前不久,我大学的二个同学也搬到这边来住了,所以周末跑步都会叫上他们两个,每周末跑完步,在足球场上坐着放松一下,交流一下最近学习的技术与工作情况,也很有 feel 。

身体发肤,受之父母,不敢损伤,孝之始也。
请记住:你的身体不仅仅是你自己的,还是你的父母与爱你的人的。

多掌握一些编程以外的技能

程序员不能简单的做码农,日子也要过的精彩。

平时可以多学习一些技能,最重要就是理财!

作为 21 世纪中新一代的程序员,理财都不会,真的说不过去,大家都知道物价飞涨,钱放在银行已经是跟不上物价飞涨的速度的,前几年之前放余额宝的时候,利息高银行高不少,也还可以啦,但现在余额宝的利息也和银行的差不多了,所以学会买基金等理财更好一点,平时要多关注一些理财的公众号!

本汪还有另外一个喜好的,那写毛笔字,为了生活,现在都没怎么写,有时间还是得抽空练习一下才行,装一下 B,陶冶一下所谓的情操。

当然,像健身、乒乓球、羽毛球、溜冰、唱歌、保龄球、钢琴、吉他、做家常菜等等。

可以帮助你把日子过的精彩些,要往自己身上多投资。

最后

程序员不止眼前的逻辑和代码,还应有健康的体魄和精气神。

希望大家都能做到,做最好的自己。

笔者平时 BB 的地方: GitHub

GitHub 吸星大法 - 一年收获 2000+ Star 的心得

1. 前言

笔者做前端开发这些年,几乎每天都会刷 GitHub,也时不时在上面分享博客和做一些开源项目,也算是 GitHub 的重度使用者了,其中也掌握了一定的技巧,并在一年内收获了 2000+ Star。

因为有读者问过我,想知道我在 GitHub 上做开源项目并获得 2000+ Star 的心得,所以笔者在此分享一下这过程的一些经验与心得,算是给那些关注了我的读者的福利。

2. 为什么要经营好你的 GitHub ?

GitHub 可以说是你的技术名片,你在 GitHub 的贡献可以作为简历的加分项。

据我所知,对于技术岗位,猎头在找候选人的诸多方法中,有一条就是通过 GitHub 来找技术比较好的候选人的,如果你的 GitHub 经营得很好,开源项目收获的 Star 比较多,一般都会为你提供一些好的机会。

为什么笔者知道 ?因为 ta 们找过笔者,所以我知道,哈哈哈。

而且如果某个公司的团队负责人看到你的 GitHub,觉得你的技术不错,也会给你抛来招揽的橄榄枝。这种情况,笔者也遇到过,哈哈哈。

笔者也是最近裸辞并换了工作,最近在找工作过程中,笔者知道了:想通过社招获得好工作或者进大厂,一般都要有如下 4 点中的 1 - 2 个亮点才行。

  • 高学历,名校毕业
  • 工作年限足,经验丰富(但不是 1 年经验当 5 年用那种)
  • 有开源与影响力,GitHub 的贡献或者经常写优质博客
  • 本身就有大厂的工作经历

大多数人都是普通人,平时所做工作几乎都是写业务而已,那么只有你具备 1 - 2 个亮点,HR 或者面试官 在筛选简历时,才会选中你,或者好机会才会自动找上你。

找工作时,我简历中的亮点就是 GitHub 的贡献,在开源与影响力的一栏中,我是这样写的:

开源与影响力

  • GitHub: https://github.com/biaochenxuying
  • 本人有 写技术博客和做开源项目 的习惯,乐于分享,坚持写博客和做开源项目的时间长达 一年半
  • 利用业余时间开源和维护了 10 个个人项目,有 博客文章、Vue 源码的思维导图、Vue 版的博客网站前台、React 管理后台、Express 后台、还有一些 js 轮子。
  • GitHub 上总共收获 2000+ Star,500+ Fork ,570+ Followers;超过 100 star 的项目有 6 个,超过 500 star 的项目有 1 个。

如果没有这个亮点,估计在这互联网寒冬期间,笔者也很难有好公司的面试机会或者找到工作啊。

3. 如何经营好你的 GitHub ?

你能为他人提供什么样的价值。

想收获到很多小星星,那你首先要想的是:你能为他人提供什么样的价值

就笔者来说,笔者在 GitHub 上为他人提供的价值有:

  • 写的博客文章,他人可以从中吸取到 经验、知识点,或者思维得到提升;
  • 把相关知识总结成思维导图,分享出来,他人可以直接学习;
  • 把根据自己的兴趣,做了个博客网站,并把源码分享出来,并做了开源,别人可以直接用;
  • 自己工作中造的一些轮子,也分享出来,他人可以直接用。

总之,原则就是:你能提供的价值越大越多,收获到的小星星就会越多

3.1 写博客文章

至于为什么要写博客,我就不说了,很多大神已经写过了,可以参考一下几个大佬们写的 我为什么要写博客 ?

笔者只想说,只要你开始了写博客之路,那基本就是一条一去不回头的路了。因为笔者就是这样,而且我看到很多写博客的人也是这样。

还有就是最好用 markdown 语法来写作,也可以参考阮一峰写的 中文技术文档的写作规范,这样可以更加关注内容本身,而不是样式,多个平台也可以发布。

而且写作这是非常重要的一环,因为后面介绍的方法,多多少少都依赖于写作。

笔者专门在 GitHub 上创建了一个 blog 仓库来写文章的,也是目前笔者收获最多 Star 的开源项目,而且布局和风格什么的,都是比较正规的。如果你也想创建个仓库专门来写文章的,可以参考我这个 blog 项目。

3.2 做开源项目

可能你觉得自己的代码写的不好,没有什么流弊的功能,不敢开源代码之类的,这想法也没错,但你要知道,大神都是从小白过来的,每个人都有是小白的时候

而且后来者从来都不缺,很多时候,你的分享主要是对那些后来者有用而已;更何况,比你厉害的人可能会指出你分享中的错误或者改进的地方,也是能促进你的进步的。

这个开源项目类型可以是很多种的,有造轮子的、写插件的、高仿某个 app 或者网站的、用某些技术写个通用模版的、总结知识做成思维导图的、提供某个功能的 等等。

虽然类型那么多,最主要的是:要根据自身的兴趣和平时日常工作来选择要做哪种类型的开源项目

笔者因为平时有写博客,所以想做个自己的个人网站,专门来展示自己的文章的,而且当时想学习 react 和 node ,所以做了个网站的项目并开源了,包含 前台展示管理后台后台

还有一些开源项目是笔者在工作中造的轮子或者插件(ps:如果是公司的机密项目的轮子、插件之类,又或者公司声明了不能把代码外传的,不要随意开源哦)。

我是这样想的:既然自己有这样的需求(比如:做个自己的个人网站需求),那么同理,其他人可能也有这个需求的,所以我做好功能并开源,对他人就可能有帮助。

我开源了之后,也的给不少人提供了帮助,因为这个项目,笔者收获了很多的小星星。而且很多人是伸手党来的,你做好了,别人可以直接用,多方便啊。

还有一个项目就是 vue + typescript 版的博客前台展示,当时我已经写了一版 react 版的前台展示了,为什么还写一版 vue 版的呢 ?因为我想学习 typescirpt,所以想在结合 vue 来实践一下,而工作中还没用得上,所以又把我的网站前台展示用 vue + typescript 用了一版。

而且当时 typescript 加 vue 的开源项目还很少的,连相关的博客都少,我想参考一下别人的项目,但是没有啊,所以当时也踩了很多坑。所以我想:我如果开源了的话,肯定很多人会参考我这个项目的,也会带来一定的流量,所以能收获不少的 star 。也的确是这样,这个项目也是我目前的完整项目中最多 star 的一个。

有一点要注意的是:一个人的精力与业余时间是非常有限的。如果是一个人的话,做的开源项目不要太多吧,维护好一个开源项目是很需要时间的,维护多个项目所需要的时间就更多了

你以为开源了就行了吗 ?太天真了。

那要写 README.md 来介绍你开源的项目的,比如一般要有如下内容:

  • 简介:简单说明一下这个项目是干嘛的
  • 结果:这个项目的代码达到了什么效果
  • 步骤:怎么运行你这个项目,或者怎么使用你写的插件。
  • 文章:详细讲解这个项目(可无,最好有)

有了这个 README.md 之后,别人一看到你的项目的 github 就知道这个项目的情况了。

3.3 硬核为王

以做好一个伟大的产品的心态来做开源项目。

做开源项目说白了就是做一个产品,我们要以做好一个产品的心态来做开源项目,这样你的产品质量才会更优,才会够硬核,也就是有料。

我做这个博客网站的时候是有这个意识的,做完第一版之后,也在不断的迭代和完善。

就我做成的成果来看,其实还不够硬核,因为还有一些优化的点和实用的功能的,只是我还没做。

目前,笔者比较遗憾的是:还没有一个达到 1000+ Star、甚至 10000+ Star 的硬核开源项目。以后技术更精进了,或者有好想法了,再开源一个好的开源项目吧。

我知道的一个比较硬核的开源项目是这个:支持自定义样式的 Markdown 编辑器,这个项目就是以一个产品的理念来做的,作者也在不断的迭代和完善。而且更新的速度很快,也很规范。

当然你也可以参考那些做得很出名的开源项目,毕竟做得那么成功,肯定有其原因。

3.4 时间与坚持

做开源项目是很需要时间的。

比如笔者做的博客网站项目就用了 2 个多月的业余时间来做,还好公司的正常的上班时间是 965 的,平时上班只需要 7 个钟,加班的情况比较少,所以业余时间比较多。

但利用业余时间做开源项目时,我的每天真实工作时间可以说是 9117 或者 907,因为晚上下班了,我都会用 2 - 3 个钟来做开源项目,周末的两天也是这样,而且周一到周五的中午吃完饭时,我也会挤出大概 30 - 40 分钟的时候来学习相关的技术,或者做开源项目。

这样习惯了大概两个月之后,终于把网站的第一版撸了出来。

所以时间很重要,没有时间你就做不出好的开源项目。

而且这是一直坚持的结果,如果中途觉得累了,可能就放弃了。

如果你问我难道不觉得累吗,其实我很少觉得累,因为是做自己喜欢的事,兴致比较高,再加上平时有锻炼身体,所以不累。

当然,如果你的工作时间是 996 的,可能没那么多时间了,最好是开源一些工作中开发好的插件或者特定功能的轮子之类的。

3.5 推广自己的项目

有才华很重要,让别人知道你的才华更重要。

酒好也怕巷子深。

当你做好你的开源项目之后,你以为就会有人给你小星星了,那你就太天真了。

想收获小星星,还要自己去技术社区推广的,不然没人知道你的项目,现在这个时代,流量为王,这一点对于开源项目也是一样的,人来了,了解到你的项目,才有可能给你小星星。

而且要推广就要脸皮厚,这叫做自我营销。

所以要写文章介绍你的开源项目,文章的要点主要是突出 效果与功能

然后就是 宣传 了,到各大技术社区(比如:思否、掘金 等)去发布你的文章,达到引流的目的。

如果想知道怎么写推广的文章,可以参考我写的这两篇文章: react + node + express + ant + mongodb 的简洁兼时尚的博客网站Vue + TypeScript + Element 项目实践(简洁时尚博客网站)及踩坑记

4. 总结

笔者觉得想做好开源项目,最重要因素是兴趣,不然你可能中途就放弃了,很难坚持到把项目做完和做好。

有时候,有很强的功利心(比如 为了钱、为了名)也是好事,这可是你的一大助力,是可以推动你做完你想做的事的。

最后,要掌握 GitHub 吸星大法,先从写作开始,从现在开始。

推荐阅读GitHub 上能挖矿的神仙技巧 - 如何发现优秀开源项目,估计很多人都不知道的技巧,甚至很多高级工程师都不知道。

JavaScript 数据结构与算法之美 - 强烈推荐: GitHub 上 170K+ Star 的前端学习的数据结构与算法项目

前言

JavaScript 数据结构与算法之美

前言

算法为王。

想学好前端,先练好内功,内功不行,就算招式练的再花哨,终究成不了高手;只有内功深厚者,前端之路才会走得更远。

强烈推荐 GitHub 上值得前端学习的数据结构与算法项目,包含 gif 图的演示过程与视频讲解。

GitHub 项目

数据结构与算法

关于数据结构与算法的 GitHub 项目,star 数由高到低排序。

leetcode

关于数据结构与算法的 GitHub 项目,star 数由高到低排序。

算法可视化工具

  • 算法可视化工具 algorithm-visualizer
    算法可视化工具 algorithm-visualizer 是一个交互式的在线平台,可以从代码中可视化算法,还可以看到代码执行的过程。

效果如下图。

算法可视化工具

旨在通过交互式可视化的执行来揭示算法背后的机制。

insert-sort.gif

变量和操作的可视化表示增强了控制流和实际源代码。您可以快速前进和后退执行,以密切观察算法的工作方式。

binary-search.gif

JavaScript 数据结构与算法之美

JavaScript 数据结构与算法之美系列是笔者写的, 用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。

最后

觉得有用 ?喜欢就点个赞吧。

WebKit 技术内幕之浏览器与WebKit 内核

WX20180808-221515@2x.png

#前言
此文章是我最近在看的【WebKit 技术内幕】一书的一些理解和做的笔记。
而【WebKit 技术内幕】是基于 WebKit 的 Chromium 项目的讲解。

#第一章 浏览器和浏览器内核
WebKit 内核是苹果2005年先开发并提出开源的,后面 Google 也以此为基础,并独立开发出 Chromium 的,2008年 Google 为 WebKit 为内核创建了一个新项目 chormium ,后来 Google 的 chrom 占领了浏览器的大部分市场。
WebKit
图 1-6 显示的是该项目的大模块。图中“WebKit 嵌入式接口”就是批的狭义 WebKit,它批的是在 WebCore(包含上面提到的 HTML 解释器、CSS 解释器和布局等模块)和 JavaScript 引擎之上的一层绑定和嵌入式编程接口,可以被浏览器调用。

WebKit2.png

Chromium 内核 Blink

2013年4月 gogle宣布从 WebKit中复制一份出来然后独立,并运作为Blink项目。

#第二章 HTML网页与结构

1. 基本组成 html 、css、js。

2. html5新特性 video、canvas、2d、3d等,2012年就推出。

3. 框结构: iframe、frame、frameset,用于嵌入html文档。

iframe.png
image.png

上面的图说的是 iframe 的应用

4. 层次结构

理解层次结构非常重要,因为它可以帮忙你理解 WebKit 如何构建它来渲染,这有助于写高效的 HTML 代码。

网页的层次结构是指网页中的元素可能分布在不周的层次中,也就是说某些元素可以不同于它的父元素所在的层次,因为某些原因, WebKit 需要为该元素和它的子女建立一个新层。

image.png

图中各层的前后关系。“ 根层 ” 在最后面,“ 层 3 ”和 “层 4 ” 在最前面。规律是需要复杂变换和处理的元素,它们需要新层,所以 WebKit 为它们构建新层其实是为了渲染引擎在处理上的方便和高效。对于不同的基于 WebKit 的浏览器,分层策略也有可能不一样,通常是有一些基本原则的,比如 video 、2d、3d 转换、canvas 等。

5. WebKit网页内核的渲染过程

渲染过程.png

从网页 URL 到构建 DOM 树

img.png

从 CSS 和 DOM 树到绘图上下文.png

从绘图上下文到最终的图像.png

绘图过程说明.png

6. 编写高效代码注意点

编写高效代码注意点

#6. 最后

希望本文对你有点帮助。

下期分享 第三章 WebKit 架构与模块 敬请期待。

JavaScript 数据结构与算法之美 - 递归

JavaScript 数据结构与算法之美

前言

  1. 算法为王。
  2. 排序算法博大精深,前辈们用了数年甚至一辈子的心血研究出来的算法,更值得我们学习与推敲。

因为后面要讲有内容和算法的实现都要用到递归,所以,搞懂递归非常重要。

1. 定义

  • 方法或函数调用自身的方式称为递归调用,调用称为递,返回称为归。

现实例子:周末你带着女朋友去电影院看电影,女朋友问你,咱们现在坐在第几排啊 ?电影院里面太黑了,看不清,没法数,现在你怎么办 ?

于是你就问前面一排的人他是第几排,你想只要在他的数字上加一,就知道自己在哪一排了。
但是,前面的人也看不清啊,所以他也问他前面的人。
就这样一排一排往前问,直到问到第一排的人,说我在第一排,然后再这样一排一排再把数字传回来。
直到你前面的人告诉你他在哪一排,于是你就知道答案了。

基本上,所有的递归问题都可以用递推公式来表示,比如:

f(n) = f(n-1) + 1; 
// 其中,f(1) = 1 

f(n) 表示你想知道自己在哪一排,f(n-1) 表示前面一排所在的排数,f(1) = 1 表示第一排的人知道自己在第一排。

有了这个递推公式,我们就可以很轻松地将它改为递归代码,如下:

function f(n) {
  if (n == 1) return 1;
  return f(n-1) + 1;
}

2. 为什么使用递归 ?递归的优缺点 ?

  • 优点:代码的表达力很强,写起来简洁。
  • 缺点:空间复杂度高、有堆栈溢出风险、存在重复计算、过多的函数调用会耗时较多等问题。

3. 什么样的问题可以用递归解决呢 ?

一个问题只要同时满足以下 3 个条件,就可以用递归来解决。

    1. 问题的解可以分解为几个子问题的解。何为子问题 ?就是数据规模更小的问题。
      比如,前面讲的电影院的例子,你要知道,自己在哪一排的问题,可以分解为前一排的人在哪一排这样一个子问题。
    1. 问题与子问题,除了数据规模不同,求解思路完全一样
      比如电影院那个例子,你求解自己在哪一排的思路,和前面一排人求解自己在哪一排的思路,是一模一样的。
    1. 存在递归终止条件
      比如电影院的例子,第一排的人不需要再继续询问任何人,就知道自己在哪一排,也就是 f(1) = 1,这就是递归的终止条件。

4. 递归常见问题及解决方案

    1. 警惕堆栈溢出:可以声明一个全局变量来控制递归的深度,从而避免堆栈溢出。
    1. 警惕重复计算:通过某种数据结构来保存已经求解过的值,从而避免重复计算。

5. 如何实现递归 ?

1. 递归代码编写

写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。

2. 递归代码理解

对于递归代码,若试图想清楚整个递和归的过程,实际上是进入了一个思维误区。

那该如何理解递归代码呢 ?

  • 如果一个问题 A 可以分解为若干个子问题 B、C、D,你可以假设子问题 B、C、D 已经解决。
  • 而且,你只需要思考问题 A 与子问题 B、C、D 两层之间的关系即可,不需要一层层往下思考子问题与子子问题,子子问题与子子子问题之间的关系。
  • 屏蔽掉递归细节,这样子理解起来就简单多了。

因此,理解递归代码,就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。

6. 例子

1. 一个阶乘的例子:

function fact(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * fact(num - 1);
    }
}
fact(3) // 结果为 6

以下代码可导致出错:

var anotherFact = fact; 
fact = null; 
alert(antherFact(4)); //出错 

由于 fact 已经不是函数了,所以出错。

使用 arguments.callee

arguments.callee 是一个指向正在执行的函数的指针,arguments.callee 返回正在被执行的对现象。
新的函数为:

function fact(num){ 
    if (num <= 1){ 
        return 1; 
    }else{ 
        return num * arguments.callee(num - 1); //此处更改了。 
    } 
} 
var anotherFact = fact; 
fact = null; 
alert(antherFact(4)); // 结果为 24

2. 再看一个多叉树的例子

先看图

多叉树

叶子结点:就是深度为 0 的结点,也就是没有孩子结点的结点,简单的说就是一个二叉树任意一个分支上的终端节点。

数据结构格式,参考如下代码:

const json = {
  name: 'A',
  children: [
    {
      name: 'B',
      children: [
        {
          name: 'E',
        },
        {
          name: 'F',
        },
        {
          name: 'G',
        }
      ]
    },
    {
      name: 'C',
      children: [
        {
          name: 'H'
        }
      ]
    },
    {
      name: 'D',
      children: [
        {
          name: 'I',
        },
        {
          name: 'J',
        }
      ]
    }
  ]
}

我们如何获取根节点的所有叶子节点个数呢 ?

递归代码如下:

/**
 * 获取根节点的所有 叶子节点 个数
 * @param {Object} json Object 对象
 */
function getLeafCountTree(json) {
  if(!json.children){
      return 1;
  } else {
      let leafCount = 0;
      for(let i = 0 ; i < json.children.length ; i++){
          // leafCount = leafCount + getLeafCountTree(json.children[i]);
          leafCount = leafCount + arguments.callee(json.children[i]);
      }
      return leafCount;
  }
}

递归遍历是比较常用的方法,比如:省市区遍历成树、多叉树、阶乘等。

vue-cli3.x 新特性及踩坑记

webpack.png

前言

vue-cli 都到 3.0.3 了,所以是时候玩转一下 vue-cli 3 的新特性了。

1. vue-cli 3.0.3

以下的安装都是在 macOS 的环境下进行的,当然在 windows 和 linus 下也同理。

1.1 安装

vue cli 的包名称由 vue-cli 改成了 @vue/cli。 如果你已经全局安装了旧版本的 vue-cli (1.x 或 2.x),你需要先通过 npm uninstall vue-cli -g 或 yarn global remove vue-cli 卸载它。

可以使用下列任一命令安装这个新 vue-cli 3.0.3 的包:

npm install -g @vue/cli
# OR
yarn global add @vue/cli

你还可以用这个命令来检查其版本是否正确 (3.x):

vue --version

或者:

vue -V

1.2使用图形化界面

你也可以通过 vue ui 命令以图形化界面创建和管理项目:

vue ui

上述命令会打开一个浏览器窗口,并以图形化界面将你引导至项目创建的流程。

1.3 创建项目

1.3.1 默认型
  • 新建文件夹,在该文件夹下打开命令窗口,输入以下命令进行新建项目,当然我起的项目名字叫 vue-webpack-demo
vue create vue-webpack-demo
  • 会让你选择默认(default)还是手动(Manually),(注:现在vue-cli3.0默认使用yarn下载)。

  • 先是默认的,一路回车后的项目目录如下:

  • 再来手动的,我起的项目名字叫 vue-webpack-demo2,如下图,让你选择那些选项,按 空格键 是选择单个,a 键 是全选。

  • 我选择了常用的如下选项:

  • vue-router 默认 hash 模式,所以我选择默认的,选择了 n ,而不是 history 模式:

  • 下一步之后问询问你安装哪一种 CSS 预处理语言,我是选择了用的 less。

  • 这个是问你选择哪个自动化代码格式化检测,配合 vscode 编辑器的,Prettier - Code formatter插件,我选的随后一个。

  • 第一个是保存就检测,第二个是 fix 和 commit 的时候检查。

  • 选择单元测试解决方案,Mocha是流行的JavaScript测试框架之一,通过它添加和运行测试,从而保证代码质量,chai 是断言库,我两个都选择了。

  • 上边这俩意思问你像,babel, postcss, eslint 这些配置文件放哪?第一个是:放独立文件放置,第二个是:放package.json里,这里小汪选择放单独配置文件,选第一个

image.png

  • 下面倒数第二行问你是否将以上这些将此保存为未来项目的预配置吗 ?选择是的时候,下次创建项目时,可以选择刚刚配置好的配置,不用再每个都配置一遍。最后一个是选择的名字,你随意选择,点击确定就开始下载模板了。

  • 再创建项目的时候,刚刚配置好的选择的名字 vue-webpack4 会这样子出现:

  • 启动命令
// 1. 进入项目
cd vue-webpack-demo 
// 或者 cd vue-webpack-demo2
// 2. 安装依赖
npm i
// 3. 启动
npm run serve

1.4 项目改变
  1. 相比 vue-cli 2.X 创建的目录,vue-cli 3.0 创建的目录看不见 webpack 的配置

image.png

  1. 启动命令行由:
npm run dev 或者 npm start

改变为:

npm run serve
  1. 安装过程也发生了一些变化,配置可以保存,下次可以再用,像前面的 vue-webpack4。

  2. 手动配置 webpack:在根目录下新建一个 vue.config.js 文件,进行你的配置 :

const path = require('path');

module.exports = {
	// 基本路径
	baseUrl: './',
	// 输出文件目录
	outputDir: 'dist',
	// eslint-loader 是否在保存的时候检查
	lintOnSave: true,
	// webpack配置
	// see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
	chainWebpack: () => {},
	configureWebpack: (config) => {
		if (process.env.NODE_ENV === 'production') {
			// 为生产环境修改配置...
			config.mode = 'production';
		} else {
			// 为开发环境修改配置...
			config.mode = 'development';
		}

		Object.assign(config, {
			// 开发生产共同配置
			resolve: {
				alias: {
					'@': path.resolve(__dirname, './src'),
					'@c': path.resolve(__dirname, './src/components')
				}
			}
		});
	},
	// 生产环境是否生成 sourceMap 文件
	productionSourceMap: true,
	// css相关配置
	css: {
		// 是否使用css分离插件 ExtractTextPlugin
		extract: true,
		// 开启 CSS source maps?
		sourceMap: false,
		// css预设器配置项
		loaderOptions: {},
		// 启用 CSS modules for all css / pre-processor files.
		modules: false
	},
	// use thread-loader for babel & TS in production build
	// enabled by default if the machine has more than 1 cores
	parallel: require('os').cpus().length > 1,
	// PWA 插件相关配置
	// see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
	pwa: {},
	// webpack-dev-server 相关配置
	devServer: {
		open: process.platform === 'darwin',
		host: '0.0.0.0',
		port: 8080,
		https: false,
		hotOnly: false,
		// proxy: {
		// 	// 设置代理
		// 	// proxy all requests starting with /api to jsonplaceholder
		// 	'http://localhost:8080/': {
		// 		target: 'http://baidu.com:8080', //真实请求的目标地址
		// 		changeOrigin: true,
		// 		pathRewrite: {
		// 			'^http://localhost:8080/': ''
		// 		}
		// 	}
		// },
		before: (app) => {}
	},
	// 第三方插件配置
	pluginOptions: {
		// ...
	}
};

  1. 当然如果你不想用3.0的话,还是可以继续使用2.0的, 官方文档是这样说的:

具体配置看官方文档:
vue-cli 3.0
简单的配置方式

踩坑记

1. npm 的全局路径被修改了

我都不记得在装什么包的时候修改了 mac 中 npm 的全局路径了,平时 npm 运行各种命令不报错。

全局卸载 vue-cli 命令行:

npm uninstall vue-cli -g;

但是今天全局卸载 vue-cli 的时候一直不成功,搞了一个小时,结果看了一下 npm 的全局路径,才发现路径不对!!!

如果你的 npm 的全局路径也变了,请按如下步骤修改加默认的。

方法一:

原因:npmr 的配置改变了,导致正确的 npmr 不能用。

  • 打开终端,切换到根路径
cd 
open .npmrc 
  • 文件里面修改为 prefix=/usr/local

方法二:

npm config set prefix /usr/local  //是默认路径 修改了路径会出现错误。

按上面的方法修改完,再全局卸载 vue-cli 果然就成功了。

JavaScript 数据结构与算法之美 - 计数排序、桶排序、基数排序

JavaScript 数据结构与算法之美

1. 前言

算法为王。

想学好前端,先练好内功,只有内功深厚者,前端之路才会走得更远

笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。

之所以把 计数排序、桶排序、基数排序 放在一起比较,是因为它们的平均时间复杂度都为 O(n)

因为这三个排序算法的时间复杂度是线性的,所以我们把这类排序算法叫作 线性排序(Linear sort)。

之所以能做到线性的时间复杂度,主要原因是,这三个算法不是基于比较的排序算法,都不涉及元素之间的比较操作。

另外,请大家带着问题来阅读下文,问题:如何根据年龄给 100 万用户排序 ?

2. 桶排序(Bucket Sort)

桶排序是计数排序的升级版,也采用了分治**

**

  • 将要排序的数据分到有限数量的几个有序的桶里。
  • 每个桶里的数据再单独进行排序(一般用插入排序或者快速排序)。
  • 桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

比如:

桶排序利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。

为了使桶排序更加高效,我们需要做到这两点:

  • 在额外空间充足的情况下,尽量增大桶的数量。
  • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中。

桶排序的核心:就在于怎么把元素平均分配到每个桶里,合理的分配将大大提高排序的效率。

实现

// 桶排序
const bucketSort = (array, bucketSize) => {
  if (array.length === 0) {
    return array;
  }

  console.time('桶排序耗时');
  let i = 0;
  let minValue = array[0];
  let maxValue = array[0];
  for (i = 1; i < array.length; i++) {
    if (array[i] < minValue) {
      minValue = array[i]; //输入数据的最小值
    } else if (array[i] > maxValue) {
      maxValue = array[i]; //输入数据的最大值
    }
  }

  //桶的初始化
  const DEFAULT_BUCKET_SIZE = 5; //设置桶的默认数量为 5
  bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
  const bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
  const buckets = new Array(bucketCount);
  for (i = 0; i < buckets.length; i++) {
    buckets[i] = [];
  }

  //利用映射函数将数据分配到各个桶中
  for (i = 0; i < array.length; i++) {
    buckets[Math.floor((array[i] - minValue) / bucketSize)].push(array[i]);
  }

  array.length = 0;
  for (i = 0; i < buckets.length; i++) {
    quickSort(buckets[i]); //对每个桶进行排序,这里使用了快速排序
    for (var j = 0; j < buckets[i].length; j++) {
      array.push(buckets[i][j]);
    }
  }
  console.timeEnd('桶排序耗时');

  return array;
};

// 快速排序
const quickSort = (arr, left, right) => {
	let len = arr.length,
		partitionIndex;
	left = typeof left != 'number' ? 0 : left;
	right = typeof right != 'number' ? len - 1 : right;

	if (left < right) {
		partitionIndex = partition(arr, left, right);
		quickSort(arr, left, partitionIndex - 1);
		quickSort(arr, partitionIndex + 1, right);
	}
	return arr;
};

const partition = (arr, left, right) => {
	//分区操作
	let pivot = left, //设定基准值(pivot)
		index = pivot + 1;
	for (let i = index; i <= right; i++) {
		if (arr[i] < arr[pivot]) {
			swap(arr, i, index);
			index++;
		}
	}
	swap(arr, pivot, index - 1);
	return index - 1;
};

const swap = (arr, i, j) => {
	let temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
};

测试

const array = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
console.log('原始array:', array);
const newArr = bucketSort(array);
console.log('newArr:', newArr);
// 原始 array:  [4, 6, 8, 5, 9, 1, 2, 5, 3, 2]
// 堆排序耗时:   0.133056640625ms
// newArr:  	 [1, 2, 2, 3, 4, 5, 5, 6, 8, 9]

分析

  • 第一,桶排序是原地排序算法吗 ?

因为桶排序的空间复杂度,也即内存消耗为 O(n),所以不是原地排序算法。

  • 第二,桶排序是稳定的排序算法吗 ?

取决于每个桶的排序方式,比如:快排就不稳定,归并就稳定。

  • 第三,桶排序的时间复杂度是多少 ?

因为桶内部的排序可以有多种方法,是会对桶排序的时间复杂度产生很重大的影响。所以,桶排序的时间复杂度可以是多种情况的。

总的来说
最佳情况:当输入的数据可以均匀的分配到每一个桶中。
最差情况:当输入的数据被分配到了同一个桶中。

以下是桶的内部排序快速排序的情况:

如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k =n / m 个元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)。
m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k = n / m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。
当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。

最佳情况:T(n) = O(n)。当输入的数据可以均匀的分配到每一个桶中。
最差情况:T(n) = O(nlogn)。当输入的数据被分配到了同一个桶中。
平均情况:T(n) = O(n)。

桶排序最好情况下使用线性时间 O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为 O(n)。
很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

适用场景

  • 桶排序比较适合用在外部排序中。
  • 外部排序就是数据存储在外部磁盘且数据量大,但内存有限,无法将整个数据全部加载到内存中。

动画

bocket-sort.gif

3. 计数排序(Counting Sort)

**

  • 找出待排序的数组中最大和最小的元素。
  • 统计数组中每个值为 i 的元素出现的次数,存入新数组 countArr 的第 i 项。
  • 对所有的计数累加(从 countArr 中的第一个元素开始,每一项和前一项相加)。
  • 反向填充目标数组:将每个元素 i 放在新数组的第 countArr[i] 项,每放一个元素就将 countArr[i] 减去 1 。

关键在于理解最后反向填充时的操作。

使用条件

  • 只能用在数据范围不大的场景中,若数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序。
  • 计数排序只能给非负整数排序,其他类型需要在不改变相对大小情况下,转换为非负整数。
  • 比如如果考试成绩精确到小数后一位,就需要将所有分数乘以 10,转换为整数。

实现

方法一:

const countingSort = array => {
	let len = array.length,
		result = [],
		countArr = [],
		min = (max = array[0]);
	console.time('计数排序耗时');
	for (let i = 0; i < len; i++) {
		// 获取最小,最大 值
		min = min <= array[i] ? min : array[i];
		max = max >= array[i] ? max : array[i];
		countArr[array[i]] = countArr[array[i]] ? countArr[array[i]] + 1 : 1;
	}
	console.log('countArr :', countArr);
	// 从最小值 -> 最大值,将计数逐项相加
	for (let j = min; j < max; j++) {
		countArr[j + 1] = (countArr[j + 1] || 0) + (countArr[j] || 0);
	}
	console.log('countArr 2:', countArr);
	// countArr 中,下标为 array 数值,数据为 array 数值出现次数;反向填充数据进入 result 数据
	for (let k = len - 1; k >= 0; k--) {
		// result[位置] = array 数据
		result[countArr[array[k]] - 1] = array[k];
		// 减少 countArr 数组中保存的计数
		countArr[array[k]]--;
		// console.log("array[k]:", array[k], 'countArr[array[k]] :', countArr[array[k]],)
		console.log('result:', result);
	}
	console.timeEnd('计数排序耗时');
	return result;
};

测试

const array = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];
console.log('原始 array: ', array);
const newArr = countingSort(array);
console.log('newArr: ', newArr);
// 原始 array:  [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2]
// 计数排序耗时:   5.6708984375ms
// newArr:  	 [1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9]

测试结果

方法二:

const countingSort2 = (arr, maxValue) => {
	console.time('计数排序耗时');
	maxValue = maxValue || arr.length;
	let bucket = new Array(maxValue + 1),
		sortedIndex = 0;
	(arrLen = arr.length), (bucketLen = maxValue + 1);

	for (let i = 0; i < arrLen; i++) {
		if (!bucket[arr[i]]) {
			bucket[arr[i]] = 0;
		}
		bucket[arr[i]]++;
	}

	for (let j = 0; j < bucketLen; j++) {
		while (bucket[j] > 0) {
			arr[sortedIndex++] = j;
			bucket[j]--;
		}
	}
	console.timeEnd('计数排序耗时');
	return arr;
};

测试

const array2 = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];
console.log('原始 array2: ', array2);
const newArr2 = countingSort2(array2, 21);
console.log('newArr2: ', newArr2);
// 原始 array:  [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2]
// 计数排序耗时:   0.043212890625ms
// newArr:  	 [1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9]

例子

可以认为,计数排序其实是桶排序的一种特殊情况

当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。

我们都经历过高考,高考查分数系统你还记得吗?我们查分数的时候,系统会显示我们的成绩以及所在省的排名。如果你所在的省有 50 万考生,如何通过成绩快速排序得出名次呢?

  • 考生的满分是 900 分,最小是 0 分,这个数据的范围很小,所以我们可以分成 901 个桶,对应分数从 0 分到 900 分。
  • 根据考生的成绩,我们将这 50 万考生划分到这 901 个桶里。桶内的数据都是分数相同的考生,所以并不需要再进行排序。
  • 我们只需要依次扫描每个桶,将桶内的考生依次输出到一个数组中,就实现了 50 万考生的排序。
  • 因为只涉及扫描遍历操作,所以时间复杂度是 O(n)。

分析

  • 第一,计数排序是原地排序算法吗 ?
    因为计数排序的空间复杂度为 O(n + k),k 是待排序列最大值,所以不是原地排序算法。
  • 第二,计数排序是稳定的排序算法吗 ?
    计数排序不改变相同元素之间原本相对的顺序,因此它是稳定的排序算法。
  • 第三,计数排序的时间复杂度是多少 ?
    最佳情况:T(n) = O(n + k)
    最差情况:T(n) = O(n + k)
    平均情况:T(n) = O(k)
    k:桶的个数。

动画

counting-sort.gif

4. 基数排序(Radix Sort)

**

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。

由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

例子

假设我们有 10 万个手机号码,希望将这 10 万个手机号码从小到大排序,你有什么比较快速的排序方法呢 ?

这个问题里有这样的规律:假设要比较两个手机号码 a,b 的大小,如果在前面几位中,a 手机号码已经比 b 手机号码大了,那后面的几位就不用看了。所以是基于来比较的。

桶排序、计数排序能派上用场吗 ?手机号码有 11 位,范围太大,显然不适合用这两种排序算法。针对这个排序问题,有没有时间复杂度是 O(n) 的算法呢 ? 有,就是基数排序。

使用条件

  • 要求数据可以分割独立的来比较;
  • 位之间由递进关系,如果 a 数据的高位比 b 数据大,那么剩下的地位就不用比较了;
  • 每一位的数据范围不能太大,要可以用线性排序,否则基数排序的时间复杂度无法做到 O(n)。

方案

按照优先从高位或低位来排序有两种实现方案:

  • MSD:由高位为基底,先按 k1 排序分组,同一组中记录, 关键码 k1 相等,再对各组按 k2 排序分成子组, 之后,对后面的关键码继续这样的排序分组,直到按最次位关键码 kd 对各子组排序后,再将各组连接起来,便得到一个有序序列。MSD 方式适用于位数多的序列。
  • LSD:由低位为基底,先从 kd 开始排序,再对 kd - 1 进行排序,依次重复,直到对 k1 排序后便得到一个有序序列。LSD 方式适用于位数少的序列。

实现

/**
	* name: 基数排序
	* @param  array 待排序数组
	* @param  max 最大位数
	*/
const radixSort = (array, max) => {
	console.time('计数排序耗时');
	const buckets = [];
	let unit = 10,
		base = 1;
	for (let i = 0; i < max; i++, base *= 10, unit *= 10) {
		for (let j = 0; j < array.length; j++) {
			let index = ~~((array[j] % unit) / base); //依次过滤出个位,十位等等数字
			if (buckets[index] == null) {
				buckets[index] = []; //初始化桶
			}
			buckets[index].push(array[j]); //往不同桶里添加数据
		}
		let pos = 0,
			value;
		for (let j = 0, length = buckets.length; j < length; j++) {
			if (buckets[j] != null) {
				while ((value = buckets[j].shift()) != null) {
					array[pos++] = value; //将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞
				}
			}
		}
	}
	console.timeEnd('计数排序耗时');
	return array;
};

测试

const array = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
console.log('原始array:', array);
const newArr = radixSort(array, 2);
console.log('newArr:', newArr);
// 原始 array:  [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
// 堆排序耗时:   0.064208984375ms
// newArr:  	 [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

分析

  • 第一,基数排序是原地排序算法吗 ?
    因为计数排序的空间复杂度为 O(n + k),所以不是原地排序算法。

  • 第二,基数排序是稳定的排序算法吗 ?
    基数排序不改变相同元素之间的相对顺序,因此它是稳定的排序算法。

  • 第三,基数排序的时间复杂度是多少 ?
    最佳情况:T(n) = O(n * k)
    最差情况:T(n) = O(n * k)
    平均情况:T(n) = O(n * k)
    k 是待排序列最大值。

动画

LSD 基数排序动图演示:

radixSort.gif

5. 解答开篇

回过头来看看开篇的思考题:如何根据年龄给 100 万用户排序 ?

你可能会说,我用上一节讲的归并、快排就可以搞定啊!是的,它们也可以完成功能,但是时间复杂度最低也是 O(nlogn)。

有没有更快的排序方法呢 ?以下是参考答案。

  • 实际上,根据年龄给 100 万用户排序,就类似按照成绩给 50 万考生排序。
  • 我们假设年龄的范围最小 1 岁,最大不超过 120 岁。
  • 我们可以遍历这 100 万用户,根据年龄将其划分到这 120 个桶里,然后依次顺序遍历这 120 个桶中的元素。
  • 这样就得到了按照年龄排序的 100 万用户数据。

6. 复杂性对比

基数排序 vs 计数排序 vs 桶排序

基数排序有两种方法:

  • MSD 从高位开始进行排序
  • LSD 从低位开始进行排序

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶;
  • 计数排序:每个桶只存储单一键值;
  • 桶排序:每个桶存储一定范围的数值;

复杂性对比

名称 平均 最好 最坏 空间 稳定性 排序方式
桶排序 O(n + k) O(n + k) O(n2) O(n + k) Yes Out-place
计数排序 O(n + k) O(n + k) O(n + k) O(k) Yes Out-place
基数排序 O(n * k) O(n * k) O(n * k) O(n + k) Yes Out-place

n: 数据规模

桶排序的时间复杂度可以是多种情况的,取决于桶内的排序。

7. 算法可视化工具

  • 算法可视化工具 algorithm-visualizer
    算法可视化工具 algorithm-visualizer 是一个交互式的在线平台,可以从代码中可视化算法,还可以看到代码执行的过程。
    效果如下图。

算法可视化工具

旨在通过交互式可视化的执行来揭示算法背后的机制。

insert-sort.gif

  • illustrated-algorithms
    变量和操作的可视化表示增强了控制流和实际源代码。您可以快速前进和后退执行,以密切观察算法的工作方式。

binary-search.gif

8. 系列文章

JavaScript 数据结构与算法之美 的系列文章。

标题 链接
时间和空间复杂度 #29
线性表(数组、链表、栈、队列) #34
实现一个前端路由,如何实现浏览器的前进与后退 ? #30
栈内存与堆内存 、浅拷贝与深拷贝 #35
递归 #36
非线性表(树、堆) #37
冒泡排序、选择排序、插入排序 #39
归并排序、快速排序、希尔排序、堆排序 #40
计数排序、桶排序、基数排序 #41
十大经典排序汇总 #42
强烈推荐 GitHub 上值得前端学习的数据结构与算法项目 #43

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。

9. 最后

文中所有的代码及测试事例都已经放到我的 GitHub 上了。

觉得有用 ?喜欢就收藏,顺便点个赞吧,你的支持是我最大的鼓励!

参考文章:

服务器小白的我,是如何将 node+mongodb 项目部署在服务器上并进行性能优化的

BiaoChenXuYing

前言

本文讲解的是:做为前端开发人员,对服务器的了解还是小白的我,是如何一步步将 node+mongodb 项目部署在阿里云 centos 7.3 的服务器上,并进行性能优化,达到页面 1 秒内看到 loading ,3 秒内看到首屏内容的。

搭建的项目是采用了主流的前后端分离**的,这里只讲 服务器环境搭建与性能优化。

效果请看 http://biaochenxuying.cn/main.html

1. 流程

  • 开发好前端与后端程序。
  • 购买服务器与域名
  • 服务器上安装所需环境(本项目是 node 和 mongodb )
  • 服务器上开放端口与设置规则
  • 用 nginx、apache 或者tomcat 来提供HTTP服务或者设置代理
  • 上传项目代码 或者 用码云或者 gihub 来拉取你的代码到服务器上
  • 启动 express 服务器
  • 优化页面加载

2. 内容细节

2.1 开发好前端与后端程序

开发好前端与后端程序,这个没什么好说的,就是开发!开发!开发!再开发!

2.2 购买服务器与域名

本人一直觉得程序员应该有一个自己的个人网站,拥有自己的域名与服务器。学知识或者测试项目的时候可以用来测试。

阿里云有个专供学生的云翼计划 阿里云学生套餐,入门级的云服务器原价1400多,学生认证后只要114一年,非常划算。

还是学生的,直接购买;不是学生了,有弟弟、妹妹的,可以用他们的大学生身份,购买,非常便宜实用(我购买的就是学生优惠套餐)。当然阿里云服务器在每年双 11 时都有很大优惠,也很便宜,选什么配置与价格得看自己的用处。

服务器预装环境可以选择 CentOS 或者 windows server,,为了体验和学习 linux 系统,我选择了CentOS。

学生优惠套餐

再次是购买域名 阿里域名购买,本人也是在阿里云购买的。域名是分 国际域名与国内域名的,国际域名是不用备案的,但是国内的域名是必须 ICP备案的 阿里云ICP代备案管理系统,不然不能用,如果是国内域名,如何备案域名,请自己上网查找教程。

域名

当然如果你的网站只用来自己用的话,可以不用买域名,因为可以通过服务器的公网 ip 来访问网站内容的。

如果购买了域名了,还要设置域名映射到相应的公网 ip ,不然也不能用。

域名解析

3. 服务器上安装所需环境(本项目是 node 和 mongodb )

3.1 登录服务器

因本人用的是 MacBook Pro ,所以直接打开 mac 终端,通过下面的命令行连接到服务器。root 是阿里云服务器默认的账号名,连接时候会叫你输入密码,输入你购买时设置的或者后来设置的密码。

ssh [email protected]   //你的服务器公网 ip,比如 47.106.20.666

如图:

登录成功效果

window 系统的,请用 Putty 或 Xshell 来登录,可以参考一下这篇文章 把 Node.js 项目部署到阿里云服务器(CentOs)

一般在新服务器创建后,建议先升级一下 CentOS:

yum -y update

常用的 Linux 命令

cd 进入目录
cd .. 返回上一个目录
ls -a 查看当前目录
mkdir abc 创建abc文件夹
mv 移动或重命名
rm 删除一个文件或者目录

3.2 安装 node

升级常用库文件, 安装 node.js 需要通过 g++ 进行编译。

yum -y install gcc gcc-c++ autoconf

跳转到目录:/usr/local/src,这个文件夹通常用来存放软件源代码:

cd /usr/local/src

下载 node.js 源码,也可以使用 scp 命令直接上传,因为下载实在太慢了:
下载地址:Downloads,请下载最新的相应版本的源码进行下载,本人下载了 v10.13.0 版本的。

下载 node.js 源码

https://nodejs.org/dist/v10.13.0/node-v10.13.0.tar.gz

下载完成后解压:

tar -xzvf node-v10.13.0.tar.gz

进入解压后的文件夹:

cd node-v10.13.0

执行配置脚本来进行预编译处理:

./configure

编译源代码,这个步骤花的时间会很长,大概需要 5 到 10 分钟:

make

编译完成后,执行安装命令,使之在系统范围内可用:

make install

安装 express 推荐 global 安装

npm -g install express

建立超级链接, 不然 sudo node 时会报 "command not found"

sudo ln -s /usr/local/bin/node /usr/bin/node
sudo ln -s /usr/local/lib/node /usr/lib/node
sudo ln -s /usr/local/bin/npm /usr/bin/npm
sudo ln -s /usr/local/bin/node-waf /usr/bin/node-waf

通过指令查看 node 及 npm 版本:

node -v
npm -v

node.js 到这里就基本安装完成了。

3.2 安装 mongodb

下载地址:mongodb
下载时,请选对相应的环境与版本,因为本人的服务器是 CentOS ,其实本质就是 linux 系统,所以选择了如下图环境与目前最新的版本。

mongodb

mongodb :

软件安装位置:/usr/local/mongodb
数据存放位置:/home/mongodb/data
数据备份位置:/home/mongodb/bak
日志存放位置:/home/mongodb/logs

下载安装包

> cd /usr/local
> wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-4.0.4.tgz

解压安装包,重命名文件夹为 mongodb

tar zxvf mongodb-linux-x86_64-4.0.4.tgz
mv mongodb-linux-x86_64-4.0.4 mongodb

在 var 文件夹里建立 mongodb 文件夹,并分别建立文件夹 data 用于存放数据,logs 用于存放日志

mkdir /var/mongodb
mkdir /var/mongodb/data
mkdir /var/mongodb/logs

打开 rc.local 文件,添加 CentOS 开机启动项:

vim /etc/rc.d/rc.local
// 不懂 vim 操作的请自行查看相应的文档教程,比如: vim 模式下,要 按了 i 才能插入内容,输入完之后,要按 shift 加 :wq 才能保存退出。

将 mongodb 启动命令追加到本文件中,让 mongodb 开机自启动:

/usr/local/mongodb/bin/mongod --dbpath=/var/mongodb/data --logpath /var/mongodb/logs/log.log -fork

启动 mongodb

/usr/local/mongodb/bin/mongod --dbpath=/var/mongodb/data --logpath /var/mongodb/logs/log.log -fork

看到如下信息说明已经安装完成并成功启动:

forked process: 18394
all output going to: /var/mongodb/logs/log.log

mongodb 默认的端口号是 27017。

如果你数据库的连接要账号和密码的,要创建数据库管理员,不然直接连接即可。
在 mongo shell 中创建管理员及数据库。

切换到 admin 数据库,创建超级管理员帐号

use admin
db.createUser({ user: "用户名", pwd:"登陆密码", roles:[{ role: "userAdminAnyDatabase", db: "admin" }] })

切换到要使用的数据库,如 taodb 数据库,创建这个数据库的管理员帐号

use taodb
db.createUser({ user: "用户名", pwd:"登陆密码", roles:[ { role: "readWrite", db: "taodb" }] //读写权限 })

重复按两下 control+c ,退出 mongo shell。
到这里 mongodb 基本已经安装设置完成了。

备份与恢复 请看这篇文章:MongoDB 备份(mongodump)与恢复(mongorestore)
安装 node 与 mongodb 也可以参考这篇文章:CentOs搭建NodeJs服务器—Mongodb安装

3.3 服务器上开放端口与设置安全组规则

如果你只放静态的网页,可以参考这个篇文章 通过云虚拟主机控制台设置默认首页

但是我们是要部署后台程序的,所以要看以下的内容:

安全组规则是什么鬼

授权安全组规则可以允许或者禁止与安全组相关联的 ECS 实例的公网和内网的入方向和出方向的访问。 
阿里云安全组应用案例文档

80 端口是为 HTTP(HyperText Transport Protocol) 即超文本传输协议开放的,浏览器 HTTP 访问 IP 或域名的 80 端口时,可以省略 80 端口号

如果我们没有开放相应的端口,

比如我们的服务要用到 3000 ,就要开放 3000 的端口,不然是访问不了的;其他端口同理。

配置安全组规则 1

配置安全组规则 2

配置安全组规则 3

端口都配置对了,以为能用公网 IP 进行访问了么 ? 小兄弟你太天真了 ...
 太天真了

还有 防火墙 这一关呢,如果防火墙没有关闭或者相关的端口没有开放,也是不能用公网 IP 进行访问网站内容的。

和安全组端口同理,比如我们的服务要用到的是 3000 端口,就要开放 3000 的端口,不然是访问不了的;其他端口同理。

出于安全考虑还是把防火墙开上,只开放相应的端口最好。

怎么开放相应的端口 ? 看下面两篇文章足矣,这里就不展开了。

1. 将nodejs项目部署到阿里云ESC服务器,linux系统配置80端口,实现公网IP访问

2. centos出现“FirewallD is not running”怎么办

3.4 用 nginx、apache 或者 tomcat 来提供 HTTP 服务或者设置代理

我是用了 nginx 的,所以这里只介绍 nginx 。
安装 nginx 请看这两篇文章:

1. Centos7安装Nginx实战

2. 阿里云Centos7安装Nginx服务器实现反向代理

开启 ngnx 代理

  • 进入到目录位置
cd /usr/local/nginx
  • 在 nginx 目录下有一个 sbin 目录,sbin 目录下有一个 nginx 可执行程序。
./nginx
  • 关闭 nginx
./nginx -s stop
  • 重启
./nginx -s reload

基本的使用就是这样子了。

如下给出我的 nginx 代理的设置:

我的两个项目是放在 /home/blog/blog-react/build/; 和 /home/blog/blog-react-admin/dist/; 下的,如果你们的路径不是这个,请修改成你们的路径。

#user  nobody;
worker_processes  1;
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;
#pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    # 如果port_in_redirect为off时,那么始终按照默认的80端口;如果该指令打开,那么将会返回当前正在监听的端口。
    port_in_redirect off;

    # 前台展示打开的服务代理
    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;
        #root /home/blog;

        location  / {
            root   /home/blog/blog-react/build/;
            index  index.html;
            try_files $uri $uri/ @router;
            autoindex on;
        }

        location @router{
            rewrite ^.*$ /index.html last;
        }

        location /api/ {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_pass http://47.106.136.114:3000/ ;
        }
        gzip on;

        gzip_buffers 32 4k;

        gzip_comp_level 6;

        gzip_min_length 200;

        gzip_types text/css text/xml application/javascript;

        gzip_vary on;

        #error_page  404              /404.html;
        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }


    # HTTPS server
    # 管理后台打开的服务代理
    server {
        listen       4444;
        server_name  localhost;
        #   charset koi8-r;
        #   ssl_certificate      cert.pem;
        #   ssl_certificate_key  cert.key;

        #   ssl_session_cache    shared:SSL:1m;
        #    ssl_session_timeout  5m;

        #    ssl_ciphers  HIGH:!aNULL:!MD5;
        #    ssl_prefer_server_ciphers  on;

        location / {
            root   /home/blog/blog-react-admin/dist/;
            index  index.html index.htm;
            try_files $uri $uri/ @router;
            autoindex on;
        }
        location @router{
            rewrite ^.*$ /index.html last;
        }

        location /api/ {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_pass http://47.106.136.114:3000/ ;
        }
        gzip on;

        gzip_buffers 32 4k;

        gzip_comp_level 6;

        gzip_min_length 200;

        gzip_types text/css text/xml application/javascript;

        gzip_vary on;

        error_page   500 502 503 504  /50x.html;
    }
}

我是开了两个代理的:前台展示打开的服务代理和管理后台打开的服务代理,这个项目是分开端口访问的。
比如:我的公网 ip 是 47.106.20.666,那么可以通过 http://47.106.20.666 即可访问前台展示,http://47.106.20.666:4444 即可访问管理后台的登录界面。

至于为什么要写这样的配置:

try_files $uri $uri/ @router;

location @router{
        rewrite ^.*$ /index.html last;
    }

因为进入到文章详情时或者前端路由变化了,再刷新浏览器,发现浏览器出现 404 。刷新页面时访问的资源在服务端找不到,因为 react-router 设置的路径不是真实存在的路径。
所以那样设置是为了可以刷新还可以打到对应的路径的。

刷新出现 404 问题,可以看下这篇文章 react,vue等部署单页面项目时,访问刷新出现404问题

3.5 上传项目代码,或者用码云、 gihub 来拉取你的代码到服务器上

我是创建了码云的账号来管理项目代码的,因为码云上可以创建免费的私有仓库,我在本地把码上传到 Gitee.com 上,再进入服务器用 git 把代码拉取下来就可以了,非常方便。

具体请看:码云(Gitee.com)帮助文档 V1.2

git 的安装请看: CentOS 7.4 系统安装 git

如果不想用 git 进行代码管理,请用其他可以连接服务器上传文件的软件,比如 FileZilla。

3.6 启动 express 服务

启动 express 服务,我用了 pm2, 可以永久运行在服务器上,且不会一报错 express 服务就挂了,而且运行中还可以进行其他操作。

安装:

npm install -g pm2

切换当前工作目录到 express 应用文件夹下,执行 pm2 命令启动 express 服务:

pm2 start ./bin/www

比如我操作项目时的基本操作:

cd /home/blog/blog-node
pm2 start ./bin/www // 开启
pm2 stop ./bin/www // 关闭
pm2 list //查看所用已启动项目:

3.7 页面加载优化

再看刚刚的 nginx 的一些配置:

server {
        gzip on;
        gzip_buffers 32 4k;
        gzip_comp_level 6;
        gzip_min_length 200;
        gzip_types text/css text/xml application/javascript;
        gzip_vary on;
    }

这个就是利用 ngonx 开启 gzip,亲测开启之后,压缩了接近 2/3 的文件大小,本来要 1M 多的文件,开启压缩之后,变成了 300k 左右。

还有其他的优化请看这篇文章 React 16 加载性能优化指南,写的很不错,我的一些优化都是参考了这个篇文章的。

做完一系列的优化处理之后,在网络正常的情况下,页面首屏渲染由本来是接近 5 秒,变成了 3 秒内,首屏渲染之前的 loading 在 1 秒内可见了。

4. 项目地址

本人的个人博客项目地址:

前台展示: https://github.com/biaochenxuying/blog-react

管理后台:https://github.com/biaochenxuying/blog-react-admin

后端:https://github.com/biaochenxuying/blog-node

blog:https://github.com/biaochenxuying/blog

本博客系统的系列文章:

项目文档说明:react + Ant Design 的 blog-react-admin

效果图

前言

此 blog-react-admin 项目是基于 蚂蚁金服开源的 ant design pro 之上,用 react 全家桶 + Ant Design 的进行再次开发的,项目已经开源,项目地址在 github 上。

效果预览 https://preview.pro.ant.design/user/login

1. 后台管理

1.1 已经实现功能

  • 登录
  • 文章管理(支持 MarkDown 语法)
  • 标签管理
  • 留言管理
  • 用户管理
  • 友情链接管理
  • 时间轴管理

1.2 待实现功能

  • 点赞、留言和评论 的通知管理
  • 评论管理
  • 个人中心(用来设置博主的各种信息)
  • 工作台( 接入百度统计接口,查看网站浏览量和用户访问等数据 )

2. 主要项目结构

- pages
  - Account 博主个人中心
  - article 文章管理
  - Category 分类
  - Dashboard 工作台
  - Exection 403 404 500 等页面
  - Link 链接管理
  - Message 留言管理
  - OtherUser 用户管理
  - Tag 标签管理
  - TimeAsix 时间轴
  - User 登录注册管理

文章管理、用户管理、留言等 具体业务需求,都是些常用的逻辑可以实现的,也很简单,这里就不展开讲了。

3. 使用

使用详情请查看 Ant Design Pro ,因为本项目也是在这个基础之上,按这个规范来构建的。

4. 缺点

开发时,程序出错后,修改正确后,webpack 有时不会及时查觉到内容已经更改,从而不能及时编译,要重新运行命令打包。

5. 项目地址

开源不易,如果觉得该项目不错或者对你有所帮助,欢迎到 github 上给个 star,谢谢。

项目地址:

前台展示: https://github.com/biaochenxuying/blog-react

管理后台:https://github.com/biaochenxuying/blog-react-admin

后端:https://github.com/biaochenxuying/blog-node

blog:https://github.com/biaochenxuying/blog

本博客系统的系列文章:

6. Build Setup ( 构建安装 )

# install dependencies
npm install 

# serve with hot reload at localhost: 3000
npm start 

# build for production with minification
npm run build 

如果要看完整的效果,是要和后台项目 blog-node 一起运行才行的,不然接口请求会失败。

细数 JavaScript 实用黑科技(二)

JavaScript

前言

书接上文:细数 JavaScript 实用黑科技(一)

本文介绍 独孤九剑和两篇最高内功心法。

第一式. !!

!! 操作符:!!variable 。
!! 可以将变量转换为布尔值。
!! 可以把任何类型的值转换为布尔值,并且只有当这个变量的值为 0 / null / "" / NaN / undefined 的时候才会返回 false,其他情况都返回 true。

!!'' 
// false
!!' '
// true
!!0
// false
!!null
// false
!!undefined
// false
!!NaN
// false
!!123
// true
!![]
// true

第二式. +

它只能作用于字符串数值,否则就会返回 NaN(不是数字)。
例子:

function toNumber(strNumber) {
    return +strNumber;
}
console.log(toNumber("1234")); 
// 1234
console.log(toNumber("abc"));
 // NaN

并且此方法也可作用于 Date 函数,这是它将返回时间戳:

console.log(+new Date()) 
// 1461288164385

第三式. if (条件)

if (token) {
    getUser();
}

可以通过使用 && 操作符组合两个变量来缩短它。

比如前面这段代码可以缩短为:

token && getUser();

第四式. 短路表达式 ||

如果第一个参数返回 false,第二个值将被作为默认值。用来设置默认参数。

function getUser(token) {
    var token = token || "XXXXXXXXXX";
    console.log('token',token)
    // 用 token 来异步请求数据
    // .......
}
getUser(666666);
// 666666
getUser();
// XXXXXXXXXX

当然,ES6 已经支持默认值参数设置了。
如果你想学到更多工作中会用到的 ES6 的新特性,请看小汪写过的:那些必会用到的 ES6 精粹

第五式. 获取数组中最后的元素

大多数人的做法:

var arr = [123, 456, 789];
var len = arr.length;
var end = arr[len-1]
console.log('end:', end)
// 'end:' 789

优化方法:

var array = [1, 2, 3, 4, 5, 6];
console.log( array.slice(-1) ); // [6]
console.log( array.slice(-1)[0] ); // 6
console.log( array.slice(-2) ); // [5,6]
console.log( array.slice(-3) ); // [4,5,6]

第六式. 打乱数组元素的顺序

不适用 Lodash 等这些库打乱数组元素顺序,你可以使用这个技巧:

var list = [1,2,3];
console.log( list.sort(function() { Math.random() - 0.5 }) ); // [2,1,3]

第七式. 伪数组转换为真数组

var elements = document.querySelectorAll("p"); 
 // NodeList 节点列表对象。但这个对象并不具有数组的全部方法,如 sort(), reduce(), map(), 
 filter()
var arrayElements = [].slice.call( elements ); 
// 现在 NodeList 是一个数组
var arrayElements = Array.from( elements ); 
// 这是另一种转换 NodeList 到 Array  的方法

第八式. 截断数组

比如,当数组中有 10 个元素,而你只想获取其中前 5 个的话,你可以截断数组,通过设置 array.length = 5 使其更小。

var array = [1,2,3,4,5,6];
console.log( array.length ); 
// 6
array.length = 3;
console.log( array.length );
 // 3
console.log( array ); 
// [1,2,3]

第九式. 合并数组

一般人合并两个数组的话,通常会使用 Array.concat()。

var array1 = [1,2,3];
var array2 = [4,5,6];
console.log(array1.concat(array2)); // [1,2,3,4,5,6];

然而,这个函数并不适用于合并大的数组,因为它需要创建一个新的数组,而这会消耗很多内存

这时,你可以使用 Array.push.apply( arr1, arr2 ) 来代替创建新的数组,它可以把第二个数组合并到第一个中,从而较少内存消耗:

var array1 = [1,2,3];
var array2 = [4,5,6];
console.log( array1.push.apply(array1, array2) );  // [1,2,3,4,5,6];

内功心法. 易混淆点

函数本身的作用域

函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。

// 先来一道题,看看输出什么
var a = 1;
var x = function () {
  console.log(a);
};

function f() {
  var a = 2;
  x();
}

f() // 1

上面代码中,函数 x 是在函数 f 的外部声明的,所以它的作用域绑定外层,内部变量 a 不会到函数 f 体内取值,所以输出 1,而不是 2。

总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。

很容易犯错的一点是,如果函数 A 调用函数 B,却没考虑到函数 B 不会引用函数 A 的内部变量。

// 再来一道题,看看输出什么
var x = function () {
  console.log(a);
};

function y(f) {
  var a = 2;
  f();
}

y(x)
// ReferenceError: a is not defined

上面代码将函数 x 作为参数,传入函数 y。但是,函数 x 是在函数 y 体外声明的,作用域绑定外层,因此找不到函数 y 的内部变量 a,导致报错。

同样的,函数体内部声明的函数,作用域绑定函数体内部。

function foo() {
  var x = 1;
  function bar() {
    console.log(x);
  }
  return bar;
}

var x = 2;
var f = foo();
f() // 1

上面代码中,函数 foo 内部声明了一个函数 bar,bar 的作用域绑定 foo。当我们在 foo 外部取出 bar 执行时,变量 x 指向的是 foo 内部的 x,而不是 foo 外部的 x。正是这种机制,构成了 “闭包” 现象。

闭包简单理解,请看我的笔记: 闭包

立即调用的函数表达式

立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。

通常写法:

(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错。

// 报错
(function(){ /* code */ }())
(function(){ /* code */ }())

上面代码的两行之间没有分号,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数。

IIFE 的目的有两个:

  • 一是不必为函数命名,避免了污染全局变量;
  • 二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

例子:

// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);

// 写法二
(function () {
  var tmp = newData;
  processData(tmp);
  storeData(tmp);
}());

上面代码中,写法二比写法一更好,因为完全避免了污染全局变量。

最后

独孤九剑共九式和两篇最高内功心法都在这里面了,大侠学会后,除恶惩奸,遨游江湖吧!!!

如果你觉得该文章对你有帮助,欢迎到我的 github,star 一下,谢谢。

github 地址

参考教程: 《JavaScript 语言入门教程》
参考文章:12 个非常有用的 JavaScript Hacks

你以为本文就这么结束了 ? 精彩在后面 !!!

JavaScript 数据结构与算法之美 - 十大经典排序算法汇总

JavaScript 数据结构与算法之美

1. 前言

算法为王。

想学好前端,先练好内功,内功不行,就算招式练的再花哨,终究成不了高手;只有内功深厚者,前端之路才会走得更远。

笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便以后复习。

文中包含了 十大经典排序算法 的**、代码实现、一些例子、复杂度分析、动画、还有算法可视化工具。

这应该是目前最全的 JavaScript 十大经典排序算法 的讲解了吧。

2. 如何分析一个排序算法

复杂度分析是整个算法学习的精髓。

  • 时间复杂度: 一个算法执行所耗费的时间。
  • 空间复杂度: 运行完一个程序所需内存的大小。

时间和空间复杂度的详解,请看 JavaScript 数据结构与算法之美 - 时间和空间复杂度

学习排序算法,我们除了学习它的算法原理、代码实现之外,更重要的是要学会如何评价、分析一个排序算法。

分析一个排序算法,要从 执行效率内存消耗稳定性 三方面入手。

2.1 执行效率

1. 最好情况、最坏情况、平均情况时间复杂度

我们在分析排序算法的时间复杂度时,要分别给出最好情况、最坏情况、平均情况下的时间复杂度。
除此之外,你还要说出最好、最坏时间复杂度对应的要排序的原始数据是什么样的。

2. 时间复杂度的系数、常数 、低阶

我们知道,时间复杂度反应的是数据规模 n 很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。

但是实际的软件开发中,我们排序的可能是 10 个、100 个、1000 个这样规模很小的数据,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。

3. 比较次数和交换(或移动)次数

这一节和下一节讲的都是基于比较的排序算法。基于比较的排序算法的执行过程,会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。

所以,如果我们在分析排序算法的执行效率的时候,应该把比较次数和交换(或移动)次数也考虑进去。

2.2 内存消耗

也就是看空间复杂度。

还需要知道如下术语:

  • 内排序:所有排序操作都在内存中完成;
  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
  • 原地排序:原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。

2.3 稳定性

  • 稳定:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变
    比如: a 原本在 b 前面,而 a = b,排序之后,a 仍然在 b 的前面;
  • 不稳定:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序改变
    比如:a 原本在 b 的前面,而 a = b,排序之后, a 在 b 的后面;

3. 十大经典排序算法

3.1 冒泡排序(Bubble Sort)

冒泡

**

  • 冒泡排序只会操作相邻的两个数据。
  • 每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。
  • 一次冒泡会让至少一个元素移动到它应该在的位置,重复 n 次,就完成了 n 个数据的排序工作。

特点

  • 优点:排序算法的基础,简单实用易于理解。
  • 缺点:比较次数多,效率较低。

实现

// 冒泡排序(未优化)
const bubbleSort = arr => {
	console.time('改进前冒泡排序耗时');
	const length = arr.length;
	if (length <= 1) return;
	// i < length - 1 是因为外层只需要 length-1 次就排好了,第 length 次比较是多余的。
	for (let i = 0; i < length - 1; i++) {
		// j < length - i - 1 是因为内层的 length-i-1 到 length-1 的位置已经排好了,不需要再比较一次。
		for (let j = 0; j < length - i - 1; j++) {
			if (arr[j] > arr[j + 1]) {
				const temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
	console.log('改进前 arr :', arr);
	console.timeEnd('改进前冒泡排序耗时');
};

优化:当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。

// 冒泡排序(已优化)
const bubbleSort2 = arr => {
	console.time('改进后冒泡排序耗时');
	const length = arr.length;
	if (length <= 1) return;
	// i < length - 1 是因为外层只需要 length-1 次就排好了,第 length 次比较是多余的。
	for (let i = 0; i < length - 1; i++) {
		let hasChange = false; // 提前退出冒泡循环的标志位
		// j < length - i - 1 是因为内层的 length-i-1 到 length-1 的位置已经排好了,不需要再比较一次。
		for (let j = 0; j < length - i - 1; j++) {
			if (arr[j] > arr[j + 1]) {
				const temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
				hasChange = true; // 表示有数据交换
			}
		}

		if (!hasChange) break; // 如果 false 说明所有元素已经到位,没有数据交换,提前退出
	}
	console.log('改进后 arr :', arr);
	console.timeEnd('改进后冒泡排序耗时');
};

测试

// 测试
const arr = [7, 8, 4, 5, 6, 3, 2, 1];
bubbleSort(arr);
// 改进前 arr : [1, 2, 3, 4, 5, 6, 7, 8]
// 改进前冒泡排序耗时: 0.43798828125ms

const arr2 = [7, 8, 4, 5, 6, 3, 2, 1];
bubbleSort2(arr2);
// 改进后 arr : [1, 2, 3, 4, 5, 6, 7, 8]
// 改进后冒泡排序耗时: 0.318115234375ms

分析

  • 第一,冒泡排序是原地排序算法吗 ?
    冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个原地排序算法。
  • 第二,冒泡排序是稳定的排序算法吗 ?
    在冒泡排序中,只有交换才可以改变两个元素的前后顺序。
    为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序。
    所以冒泡排序是稳定的排序算法。
  • 第三,冒泡排序的时间复杂度是多少 ?
    最佳情况:T(n) = O(n),当数据已经是正序时。
    最差情况:T(n) = O(n2),当数据是反序时。
    平均情况:T(n) = O(n2)。

动画

冒泡排序动画

冒泡排序动画

3.2 插入排序(Insertion Sort)

插入排序又为分为 直接插入排序 和优化后的 拆半插入排序希尔排序,我们通常说的插入排序是指直接插入排序。

一、直接插入

**

一般人打扑克牌,整理牌的时候,都是按牌的大小(从小到大或者从大到小)整理牌的,那每摸一张新牌,就扫描自己的牌,把新牌插入到相应的位置。

插入排序的工作原理:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

步骤

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤 2 ~ 5。

实现

// 插入排序
const insertionSort = array => {
	const len = array.length;
	if (len <= 1) return

	let preIndex, current;
	for (let i = 1; i < len; i++) {
		preIndex = i - 1; //待比较元素的下标
		current = array[i]; //当前元素
		while (preIndex >= 0 && array[preIndex] > current) {
			//前置条件之一: 待比较元素比当前元素大
			array[preIndex + 1] = array[preIndex]; //将待比较元素后移一位
			preIndex--; //游标前移一位
		}
		if (preIndex + 1 != i) {
			//避免同一个元素赋值给自身
			array[preIndex + 1] = current; //将当前元素插入预留空位
			console.log('array :', array);
		}
	}
	return array;
};

测试

// 测试
const array = [5, 4, 3, 2, 1];
console.log("原始 array :", array);
insertionSort(array);
// 原始 array:    [5, 4, 3, 2, 1]
// array:  		 [4, 5, 3, 2, 1]
// array:  		 [3, 4, 5, 2, 1]
// array: 		 [2, 3, 4, 5, 1]
// array:  		 [1, 2, 3, 4, 5]

分析

  • 第一,插入排序是原地排序算法吗 ?
    插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1),所以,这是一个原地排序算法。
  • 第二,插入排序是稳定的排序算法吗 ?
    在插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法。
  • 第三,插入排序的时间复杂度是多少 ?
    最佳情况:T(n) = O(n),当数据已经是正序时。
    最差情况:T(n) = O(n2),当数据是反序时。
    平均情况:T(n) = O(n2)。

动画

insertion-sort.gif

二、拆半插入

插入排序也有一种优化算法,叫做拆半插入

**

折半插入排序是直接插入排序的升级版,鉴于插入排序第一部分为已排好序的数组,我们不必按顺序依次寻找插入点,只需比较它们的中间值与待插入元素的大小即可。

步骤

  • 取 0 ~ i-1 的中间点 ( m = (i-1) >> 1 ),array[i] 与 array[m] 进行比较,若 array[i] < array[m],则说明待插入的元素 array[i] 应该处于数组的 0 ~ m 索引之间;反之,则说明它应该处于数组的 m ~ i-1 索引之间。
  • 重复步骤 1,每次缩小一半的查找范围,直至找到插入的位置。
  • 将数组中插入位置之后的元素全部后移一位。
  • 在指定位置插入第 i 个元素。

注:x >> 1 是位运算中的右移运算,表示右移一位,等同于 x 除以 2 再取整,即 x >> 1 == Math.floor(x/2) 。

// 折半插入排序
const binaryInsertionSort = array => {
	const len = array.length;
	if (len <= 1) return;

	let current, i, j, low, high, m;
	for (i = 1; i < len; i++) {
		low = 0;
		high = i - 1;
		current = array[i];

		while (low <= high) {
			//步骤 1 & 2 : 折半查找
			m = (low + high) >> 1; // 注: x>>1 是位运算中的右移运算, 表示右移一位, 等同于 x 除以 2 再取整, 即 x>>1 == Math.floor(x/2) .
			if (array[i] >= array[m]) {
				//值相同时, 切换到高半区,保证稳定性
				low = m + 1; //插入点在高半区
			} else {
				high = m - 1; //插入点在低半区
			}
		}
		for (j = i; j > low; j--) {
			//步骤 3: 插入位置之后的元素全部后移一位
			array[j] = array[j - 1];
			console.log('array2 :', JSON.parse(JSON.stringify(array)));
		}
		array[low] = current; //步骤 4: 插入该元素
	}
	console.log('array2 :', JSON.parse(JSON.stringify(array)));
	return array;
};

测试

const array2 = [5, 4, 3, 2, 1];
console.log('原始 array2:', array2);
binaryInsertionSort(array2);
// 原始 array2:  [5, 4, 3, 2, 1]
// array2 :     [5, 5, 3, 2, 1]
// array2 :     [4, 5, 5, 2, 1]
// array2 :     [4, 4, 5, 2, 1]
// array2 :     [3, 4, 5, 5, 1]
// array2 :     [3, 4, 4, 5, 1]
// array2 :     [3, 3, 4, 5, 1]
// array2 :     [2, 3, 4, 5, 5]
// array2 :     [2, 3, 4, 4, 5]
// array2 :     [2, 3, 3, 4, 5]
// array2 :     [2, 2, 3, 4, 5]
// array2 :     [1, 2, 3, 4, 5]

注意:和直接插入排序类似,折半插入排序每次交换的是相邻的且值为不同的元素,它并不会改变值相同的元素之间的顺序,因此它是稳定的。

三、希尔排序

希尔排序是一个平均时间复杂度为 O(n log n) 的算法,会在下一个章节和 归并排序、快速排序、堆排序 一起讲,本文就不展开了。

3.3 选择排序(Selection Sort)

思路

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

步骤

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

实现

const selectionSort = array => {
	const len = array.length;
	let minIndex, temp;
	for (let i = 0; i < len - 1; i++) {
		minIndex = i;
		for (let j = i + 1; j < len; j++) {
			if (array[j] < array[minIndex]) {
				// 寻找最小的数
				minIndex = j; // 将最小数的索引保存
			}
		}
		temp = array[i];
		array[i] = array[minIndex];
		array[minIndex] = temp;
		console.log('array: ', array);
	}
	return array;
};

测试

// 测试
const array = [5, 4, 3, 2, 1];
console.log('原始array:', array);
selectionSort(array);
// 原始 array:  [5, 4, 3, 2, 1]
// array:  		 [1, 4, 3, 2, 5]
// array:  		 [1, 2, 3, 4, 5]
// array: 		 [1, 2, 3, 4, 5]
// array:  		 [1, 2, 3, 4, 5]

分析

  • 第一,选择排序是原地排序算法吗 ?
    选择排序空间复杂度为 O(1),是一种原地排序算法。
  • 第二,选择排序是稳定的排序算法吗 ?
    选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,这样破坏了稳定性。所以,选择排序是一种不稳定的排序算法。
  • 第三,选择排序的时间复杂度是多少 ?
    无论是正序还是逆序,选择排序都会遍历 n2 / 2 次来排序,所以,最佳、最差和平均的复杂度是一样的。
    最佳情况:T(n) = O(n2)。
    最差情况:T(n) = O(n2)。
    平均情况:T(n) = O(n2)。

动画

selection-sort.gif

3.4 归并排序(Merge Sort)

**

排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。

归并排序采用的是分治**

分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。

merge-sort-example.png

注:x >> 1 是位运算中的右移运算,表示右移一位,等同于 x 除以 2 再取整,即 x >> 1 === Math.floor(x / 2) 。

实现

const mergeSort = arr => {
	//采用自上而下的递归方法
	const len = arr.length;
	if (len < 2) {
		return arr;
	}
	// length >> 1 和 Math.floor(len / 2) 等价
	let middle = Math.floor(len / 2),
		left = arr.slice(0, middle),
		right = arr.slice(middle); // 拆分为两个子数组
	return merge(mergeSort(left), mergeSort(right));
};

const merge = (left, right) => {
	const result = [];

	while (left.length && right.length) {
		// 注意: 判断的条件是小于或等于,如果只是小于,那么排序将不稳定.
		if (left[0] <= right[0]) {
			result.push(left.shift());
		} else {
			result.push(right.shift());
		}
	}

	while (left.length) result.push(left.shift());

	while (right.length) result.push(right.shift());

	return result;
};

测试

// 测试
const arr = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
console.time('归并排序耗时');
console.log('arr :', mergeSort(arr));
console.timeEnd('归并排序耗时');
// arr : [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
// 归并排序耗时: 0.739990234375ms

分析

  • 第一,归并排序是原地排序算法吗 ?
    这是因为归并排序的合并函数,在合并两个有序数组为一个有序数组时,需要借助额外的存储空间。
    实际上,尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)。
    所以,归并排序不是原地排序算法。

  • 第二,归并排序是稳定的排序算法吗 ?
    merge 方法里面的 left[0] <= right[0] ,保证了值相同的元素,在合并前后的先后顺序不变。归并排序是稳定的排序方法。

  • 第三,归并排序的时间复杂度是多少 ?
    从效率上看,归并排序可算是排序算法中的佼佼者。假设数组长度为 n,那么拆分数组共需 logn 步,又每步都是一个普通的合并子数组的过程,时间复杂度为 O(n),故其综合时间复杂度为 O(n log n)。
    最佳情况:T(n) = O(n log n)。
    最差情况:T(n) = O(n log n)。
    平均情况:T(n) = O(n log n)。

动画

merge-sort.gif

3.5 快速排序 (Quick Sort)

快速排序的特点就是快,而且效率高!它是处理大数据最快的排序算法之一。

**

  • 先找到一个基准点(一般指数组的中部),然后数组被该基准点分为两部分,依次与该基准点数据比较,如果比它小,放左边;反之,放右边。
  • 左右分别用一个空数组去存储比较后的数据。
  • 最后递归执行上述操作,直到数组长度 <= 1;

特点:快速,常用。

缺点:需要另外声明两个数组,浪费了内存空间资源。

实现

方法一:

const quickSort1 = arr => {
	if (arr.length <= 1) {
		return arr;
	}
	//取基准点
	const midIndex = Math.floor(arr.length / 2);
	//取基准点的值,splice(index,1) 则返回的是含有被删除的元素的数组。
	const valArr = arr.splice(midIndex, 1);
	const midIndexVal = valArr[0];
	const left = []; //存放比基准点小的数组
	const right = []; //存放比基准点大的数组
	//遍历数组,进行判断分配
	for (let i = 0; i < arr.length; i++) {
		if (arr[i] < midIndexVal) {
			left.push(arr[i]); //比基准点小的放在左边数组
		} else {
			right.push(arr[i]); //比基准点大的放在右边数组
		}
	}
	//递归执行以上操作,对左右两个数组进行操作,直到数组长度为 <= 1
	return quickSort1(left).concat(midIndexVal, quickSort1(right));
};
const array2 = [5, 4, 3, 2, 1];
console.log('quickSort1 ', quickSort1(array2));
// quickSort1: [1, 2, 3, 4, 5]

方法二:

// 快速排序
const quickSort = (arr, left, right) => {
	let len = arr.length,
		partitionIndex;
	left = typeof left != 'number' ? 0 : left;
	right = typeof right != 'number' ? len - 1 : right;

	if (left < right) {
		partitionIndex = partition(arr, left, right);
		quickSort(arr, left, partitionIndex - 1);
		quickSort(arr, partitionIndex + 1, right);
	}
	return arr;
};

const partition = (arr, left, right) => {
	//分区操作
	let pivot = left, //设定基准值(pivot)
		index = pivot + 1;
	for (let i = index; i <= right; i++) {
		if (arr[i] < arr[pivot]) {
			swap(arr, i, index);
			index++;
		}
	}
	swap(arr, pivot, index - 1);
	return index - 1;
};

const swap = (arr, i, j) => {
	let temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
};

测试

// 测试
const array = [5, 4, 3, 2, 1];
console.log('原始array:', array);
const newArr = quickSort(array);
console.log('newArr:', newArr);
// 原始 array:  [5, 4, 3, 2, 1]
// newArr:     [1, 4, 3, 2, 5]

分析

  • 第一,快速排序是原地排序算法吗 ?
    因为 partition() 函数进行分区时,不需要很多额外的内存空间,所以快排是原地排序算法。

  • 第二,快速排序是稳定的排序算法吗 ?
    和选择排序相似,快速排序每次交换的元素都有可能不是相邻的,因此它有可能打破原来值为相同的元素之间的顺序。因此,快速排序并不稳定

  • 第三,快速排序的时间复杂度是多少 ?
    极端的例子:如果数组中的数据原来已经是有序的了,比如 1,3,5,6,8。如果我们每次选择最后一个元素作为 pivot,那每次分区得到的两个区间都是不均等的。我们需要进行大约 n 次分区操作,才能完成快排的整个过程。每次分区我们平均要扫描大约 n / 2 个元素,这种情况下,快排的时间复杂度就从 O(nlogn) 退化成了 O(n2)。
    最佳情况:T(n) = O(n log n)。
    最差情况:T(n) = O(n2)。
    平均情况:T(n) = O(n log n)。

动画

quick-sort.gif

解答开篇问题

快排和归并用的都是分治**,递推公式和递归代码也非常相似,那它们的区别在哪里呢 ?

快速排序与归并排序

可以发现:

  • 归并排序的处理过程是由下而上的,先处理子问题,然后再合并。
  • 而快排正好相反,它的处理过程是由上而下的,先分区,然后再处理子问题。
  • 归并排序虽然是稳定的、时间复杂度为 O(nlogn) 的排序算法,但是它是非原地排序算法。
  • 归并之所以是非原地排序算法,主要原因是合并函数无法在原地执行。
  • 快速排序通过设计巧妙的原地分区函数,可以实现原地排序,解决了归并排序占用太多内存的问题。

3.6 希尔排序(Shell Sort)

**

  • 先将整个待排序的记录序列分割成为若干子序列。
  • 分别进行直接插入排序。
  • 待整个序列中的记录基本有序时,再对全体记录进行依次直接插入排序。

过程

  1. 举个易于理解的例子:[35, 33, 42, 10, 14, 19, 27, 44],我们采取间隔 4。创建一个位于 4 个位置间隔的所有值的虚拟子列表。下面这些值是 { 35, 14 },{ 33, 19 },{ 42, 27 } 和 { 10, 44 }。

栗子

  1. 我们比较每个子列表中的值,并在原始数组中交换它们(如果需要)。完成此步骤后,新数组应如下所示。

栗子

  1. 然后,我们采用 2 的间隔,这个间隙产生两个子列表:{ 14, 27, 35, 42 }, { 19, 10, 33, 44 }。

栗子

  1. 我们比较并交换原始数组中的值(如果需要)。完成此步骤后,数组变成:[14, 10, 27, 19, 35, 33, 42, 44],图如下所示,10 与 19 的位置互换一下。

image.png

  1. 最后,我们使用值间隔 1 对数组的其余部分进行排序,Shell sort 使用插入排序对数组进行排序。

栗子

实现

const shellSort = arr => {
	let len = arr.length,
		temp,
		gap = 1;
	console.time('希尔排序耗时');
	while (gap < len / 3) {
		//动态定义间隔序列
		gap = gap * 3 + 1;
	}
	for (gap; gap > 0; gap = Math.floor(gap / 3)) {
		for (let i = gap; i < len; i++) {
			temp = arr[i];
			let j = i - gap;
			for (; j >= 0 && arr[j] > temp; j -= gap) {
				arr[j + gap] = arr[j];
			}
			arr[j + gap] = temp;
			console.log('arr  :', arr);
		}
	}
	console.timeEnd('希尔排序耗时');
	return arr;
};

测试

// 测试
const array = [35, 33, 42, 10, 14, 19, 27, 44];
console.log('原始array:', array);
const newArr = shellSort(array);
console.log('newArr:', newArr);
// 原始 array:   [35, 33, 42, 10, 14, 19, 27, 44]
// arr      :   [14, 33, 42, 10, 35, 19, 27, 44]
// arr      :   [14, 19, 42, 10, 35, 33, 27, 44]
// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]
// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]
// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]
// arr      :   [14, 19, 27, 10, 35, 33, 42, 44]
// arr      :   [10, 14, 19, 27, 35, 33, 42, 44]
// arr      :   [10, 14, 19, 27, 35, 33, 42, 44]
// arr      :   [10, 14, 19, 27, 33, 35, 42, 44]
// arr      :   [10, 14, 19, 27, 33, 35, 42, 44]
// arr      :   [10, 14, 19, 27, 33, 35, 42, 44]
// 希尔排序耗时: 3.592041015625ms
// newArr:     [10, 14, 19, 27, 33, 35, 42, 44]

分析

  • 第一,希尔排序是原地排序算法吗 ?
    希尔排序过程中,只涉及相邻数据的交换操作,只需要常量级的临时空间,空间复杂度为 O(1) 。所以,希尔排序是原地排序算法。

  • 第二,希尔排序是稳定的排序算法吗 ?
    我们知道,单次直接插入排序是稳定的,它不会改变相同元素之间的相对顺序,但在多次不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,可能导致相同元素相对顺序发生变化。
    因此,希尔排序不稳定

  • 第三,希尔排序的时间复杂度是多少 ?
    最佳情况:T(n) = O(n log n)。
    最差情况:T(n) = O(n log2 n)。
    平均情况:T(n) = O(n log2 n)。

动画

shell-sort.gif

3.7 堆排序(Heap Sort)

堆的定义

堆其实是一种特殊的树。只要满足这两点,它就是一个堆。

  • 堆是一个完全二叉树。
    完全二叉树:除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。
  • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。
    也可以说:堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。这两种表述是等价的。

对于每个节点的值都大于等于子树中每个节点值的堆,我们叫作大顶堆
对于每个节点的值都小于等于子树中每个节点值的堆,我们叫作小顶堆

区分堆、大顶堆、小顶堆

其中图 1 和 图 2 是大顶堆,图 3 是小顶堆,图 4 不是堆。除此之外,从图中还可以看出来,对于同一组数据,我们可以构建多种不同形态的堆。

**

  1. 将初始待排序关键字序列 (R1, R2 .... Rn) 构建成大顶堆,此堆为初始的无序区;
  2. 将堆顶元素 R[1] 与最后一个元素 R[n] 交换,此时得到新的无序区 (R1, R2, ..... Rn-1) 和新的有序区 (Rn) ,且满足 R[1, 2 ... n-1] <= R[n]。
  3. 由于交换后新的堆顶 R[1] 可能违反堆的性质,因此需要对当前无序区 (R1, R2 ...... Rn-1) 调整为新堆,然后再次将 R[1] 与无序区最后一个元素交换,得到新的无序区 (R1, R2 .... Rn-2) 和新的有序区 (Rn-1, Rn)。不断重复此过程,直到有序区的元素个数为 n - 1,则整个排序过程完成。

实现

// 堆排序
const heapSort = array => {
	console.time('堆排序耗时');
	// 初始化大顶堆,从第一个非叶子结点开始
	for (let i = Math.floor(array.length / 2 - 1); i >= 0; i--) {
		heapify(array, i, array.length);
	}
	// 排序,每一次 for 循环找出一个当前最大值,数组长度减一
	for (let i = Math.floor(array.length - 1); i > 0; i--) {
		// 根节点与最后一个节点交换
		swap(array, 0, i);
		// 从根节点开始调整,并且最后一个结点已经为当前最大值,不需要再参与比较,所以第三个参数为 i,即比较到最后一个结点前一个即可
		heapify(array, 0, i);
	}
	console.timeEnd('堆排序耗时');
	return array;
};

// 交换两个节点
const swap = (array, i, j) => {
	let temp = array[i];
	array[i] = array[j];
	array[j] = temp;
};

// 将 i 结点以下的堆整理为大顶堆,注意这一步实现的基础实际上是:
// 假设结点 i 以下的子堆已经是一个大顶堆,heapify 函数实现的
// 功能是实际上是:找到 结点 i 在包括结点 i 的堆中的正确位置。
// 后面将写一个 for 循环,从第一个非叶子结点开始,对每一个非叶子结点
// 都执行 heapify 操作,所以就满足了结点 i 以下的子堆已经是一大顶堆
const heapify = (array, i, length) => {
	let temp = array[i]; // 当前父节点
	// j < length 的目的是对结点 i 以下的结点全部做顺序调整
	for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
		temp = array[i]; // 将 array[i] 取出,整个过程相当于找到 array[i] 应处于的位置
		if (j + 1 < length && array[j] < array[j + 1]) {
			j++; // 找到两个孩子中较大的一个,再与父节点比较
		}
		if (temp < array[j]) {
			swap(array, i, j); // 如果父节点小于子节点:交换;否则跳出
			i = j; // 交换后,temp 的下标变为 j
		} else {
			break;
		}
	}
};

测试

const array = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
console.log('原始array:', array);
const newArr = heapSort(array);
console.log('newArr:', newArr);
// 原始 array:  [4, 6, 8, 5, 9, 1, 2, 5, 3, 2]
// 堆排序耗时: 0.15087890625ms
// newArr:     [1, 2, 2, 3, 4, 5, 5, 6, 8, 9]

分析

  • 第一,堆排序是原地排序算法吗 ?
    整个堆排序的过程,都只需要极个别临时存储空间,所以堆排序原地排序算法。

  • 第二,堆排序是稳定的排序算法吗 ?
    因为在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序。
    所以,堆排序是不稳定的排序算法。

  • 第三,堆排序的时间复杂度是多少 ?
    堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn),所以,堆排序整体的时间复杂度是 O(nlogn)。
    最佳情况:T(n) = O(n log n)。
    最差情况:T(n) = O(n log n)。
    平均情况:T(n) = O(n log n)。

动画

heap-sort.gif

heap-sort2.gif

3.8 桶排序(Bucket Sort)

桶排序是计数排序的升级版,也采用了分治**

**

  • 将要排序的数据分到有限数量的几个有序的桶里。
  • 每个桶里的数据再单独进行排序(一般用插入排序或者快速排序)。
  • 桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

比如:

桶排序利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。

为了使桶排序更加高效,我们需要做到这两点:

  • 在额外空间充足的情况下,尽量增大桶的数量。
  • 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中。

桶排序的核心:就在于怎么把元素平均分配到每个桶里,合理的分配将大大提高排序的效率。

实现

// 桶排序
const bucketSort = (array, bucketSize) => {
  if (array.length === 0) {
    return array;
  }

  console.time('桶排序耗时');
  let i = 0;
  let minValue = array[0];
  let maxValue = array[0];
  for (i = 1; i < array.length; i++) {
    if (array[i] < minValue) {
      minValue = array[i]; //输入数据的最小值
    } else if (array[i] > maxValue) {
      maxValue = array[i]; //输入数据的最大值
    }
  }

  //桶的初始化
  const DEFAULT_BUCKET_SIZE = 5; //设置桶的默认数量为 5
  bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
  const bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
  const buckets = new Array(bucketCount);
  for (i = 0; i < buckets.length; i++) {
    buckets[i] = [];
  }

  //利用映射函数将数据分配到各个桶中
  for (i = 0; i < array.length; i++) {
    buckets[Math.floor((array[i] - minValue) / bucketSize)].push(array[i]);
  }

  array.length = 0;
  for (i = 0; i < buckets.length; i++) {
    quickSort(buckets[i]); //对每个桶进行排序,这里使用了快速排序
    for (var j = 0; j < buckets[i].length; j++) {
      array.push(buckets[i][j]);
    }
  }
  console.timeEnd('桶排序耗时');

  return array;
};

// 快速排序
const quickSort = (arr, left, right) => {
	let len = arr.length,
		partitionIndex;
	left = typeof left != 'number' ? 0 : left;
	right = typeof right != 'number' ? len - 1 : right;

	if (left < right) {
		partitionIndex = partition(arr, left, right);
		quickSort(arr, left, partitionIndex - 1);
		quickSort(arr, partitionIndex + 1, right);
	}
	return arr;
};

const partition = (arr, left, right) => {
	//分区操作
	let pivot = left, //设定基准值(pivot)
		index = pivot + 1;
	for (let i = index; i <= right; i++) {
		if (arr[i] < arr[pivot]) {
			swap(arr, i, index);
			index++;
		}
	}
	swap(arr, pivot, index - 1);
	return index - 1;
};

const swap = (arr, i, j) => {
	let temp = arr[i];
	arr[i] = arr[j];
	arr[j] = temp;
};

测试

const array = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
console.log('原始array:', array);
const newArr = bucketSort(array);
console.log('newArr:', newArr);
// 原始 array:  [4, 6, 8, 5, 9, 1, 2, 5, 3, 2]
// 堆排序耗时:   0.133056640625ms
// newArr:  	 [1, 2, 2, 3, 4, 5, 5, 6, 8, 9]

分析

  • 第一,桶排序是原地排序算法吗 ?
    因为桶排序的空间复杂度,也即内存消耗为 O(n),所以不是原地排序算法。

  • 第二,桶排序是稳定的排序算法吗 ?
    取决于每个桶的排序方式,比如:快排就不稳定,归并就稳定。

  • 第三,桶排序的时间复杂度是多少 ?
    因为桶内部的排序可以有多种方法,是会对桶排序的时间复杂度产生很重大的影响。所以,桶排序的时间复杂度可以是多种情况的。
    总的来说
    最佳情况:当输入的数据可以均匀的分配到每一个桶中。
    最差情况:当输入的数据被分配到了同一个桶中。
    以下是桶的内部排序快速排序的情况:
    如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k =n / m 个元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)。
    m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k = n / m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。
    当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。
    最佳情况:T(n) = O(n)。当输入的数据可以均匀的分配到每一个桶中。
    最差情况:T(n) = O(nlogn)。当输入的数据被分配到了同一个桶中。
    平均情况:T(n) = O(n)。

桶排序最好情况下使用线性时间 O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为 O(n)。
很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

适用场景

  • 桶排序比较适合用在外部排序中。
  • 外部排序就是数据存储在外部磁盘且数据量大,但内存有限,无法将整个数据全部加载到内存中。

动画

bocket-sort.gif

3.9 计数排序(Counting Sort)

**

  • 找出待排序的数组中最大和最小的元素。
  • 统计数组中每个值为 i 的元素出现的次数,存入新数组 countArr 的第 i 项。
  • 对所有的计数累加(从 countArr 中的第一个元素开始,每一项和前一项相加)。
  • 反向填充目标数组:将每个元素 i 放在新数组的第 countArr[i] 项,每放一个元素就将 countArr[i] 减去 1 。

关键在于理解最后反向填充时的操作。

使用条件

  • 只能用在数据范围不大的场景中,若数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序。
  • 计数排序只能给非负整数排序,其他类型需要在不改变相对大小情况下,转换为非负整数。
  • 比如如果考试成绩精确到小数后一位,就需要将所有分数乘以 10,转换为整数。

实现

方法一:

const countingSort = array => {
	let len = array.length,
		result = [],
		countArr = [],
		min = (max = array[0]);
	console.time('计数排序耗时');
	for (let i = 0; i < len; i++) {
		// 获取最小,最大 值
		min = min <= array[i] ? min : array[i];
		max = max >= array[i] ? max : array[i];
		countArr[array[i]] = countArr[array[i]] ? countArr[array[i]] + 1 : 1;
	}
	console.log('countArr :', countArr);
	// 从最小值 -> 最大值,将计数逐项相加
	for (let j = min; j < max; j++) {
		countArr[j + 1] = (countArr[j + 1] || 0) + (countArr[j] || 0);
	}
	console.log('countArr 2:', countArr);
	// countArr 中,下标为 array 数值,数据为 array 数值出现次数;反向填充数据进入 result 数据
	for (let k = len - 1; k >= 0; k--) {
		// result[位置] = array 数据
		result[countArr[array[k]] - 1] = array[k];
		// 减少 countArr 数组中保存的计数
		countArr[array[k]]--;
		// console.log("array[k]:", array[k], 'countArr[array[k]] :', countArr[array[k]],)
		console.log('result:', result);
	}
	console.timeEnd('计数排序耗时');
	return result;
};

测试

const array = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];
console.log('原始 array: ', array);
const newArr = countingSort(array);
console.log('newArr: ', newArr);
// 原始 array:  [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2]
// 计数排序耗时:   5.6708984375ms
// newArr:  	 [1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9]

测试结果

方法二:

const countingSort2 = (arr, maxValue) => {
	console.time('计数排序耗时');
	maxValue = maxValue || arr.length;
	let bucket = new Array(maxValue + 1),
		sortedIndex = 0;
	(arrLen = arr.length), (bucketLen = maxValue + 1);

	for (let i = 0; i < arrLen; i++) {
		if (!bucket[arr[i]]) {
			bucket[arr[i]] = 0;
		}
		bucket[arr[i]]++;
	}

	for (let j = 0; j < bucketLen; j++) {
		while (bucket[j] > 0) {
			arr[sortedIndex++] = j;
			bucket[j]--;
		}
	}
	console.timeEnd('计数排序耗时');
	return arr;
};

测试

const array2 = [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2];
console.log('原始 array2: ', array2);
const newArr2 = countingSort2(array2, 21);
console.log('newArr2: ', newArr2);
// 原始 array:  [2, 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2]
// 计数排序耗时:   0.043212890625ms
// newArr:  	 [1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9]

例子

可以认为,计数排序其实是桶排序的一种特殊情况

当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成 k 个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。

我们都经历过高考,高考查分数系统你还记得吗?我们查分数的时候,系统会显示我们的成绩以及所在省的排名。如果你所在的省有 50 万考生,如何通过成绩快速排序得出名次呢?

  • 考生的满分是 900 分,最小是 0 分,这个数据的范围很小,所以我们可以分成 901 个桶,对应分数从 0 分到 900 分。
  • 根据考生的成绩,我们将这 50 万考生划分到这 901 个桶里。桶内的数据都是分数相同的考生,所以并不需要再进行排序。
  • 我们只需要依次扫描每个桶,将桶内的考生依次输出到一个数组中,就实现了 50 万考生的排序。
  • 因为只涉及扫描遍历操作,所以时间复杂度是 O(n)。

分析

  • 第一,计数排序是原地排序算法吗 ?
    因为计数排序的空间复杂度为 O(k),k 桶的个数,所以不是原地排序算法。
  • 第二,计数排序是稳定的排序算法吗 ?
    计数排序不改变相同元素之间原本相对的顺序,因此它是稳定的排序算法。
  • 第三,计数排序的时间复杂度是多少 ?
    最佳情况:T(n) = O(n + k)
    最差情况:T(n) = O(n + k)
    平均情况:T(n) = O(n + k)
    k 是待排序列最大值。

动画

counting-sort.gif

3.10 基数排序(Radix Sort)

**

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。

例子

假设我们有 10 万个手机号码,希望将这 10 万个手机号码从小到大排序,你有什么比较快速的排序方法呢 ?

这个问题里有这样的规律:假设要比较两个手机号码 a,b 的大小,如果在前面几位中,a 手机号码已经比 b 手机号码大了,那后面的几位就不用看了。所以是基于来比较的。

桶排序、计数排序能派上用场吗 ?手机号码有 11 位,范围太大,显然不适合用这两种排序算法。针对这个排序问题,有没有时间复杂度是 O(n) 的算法呢 ? 有,就是基数排序。

使用条件

  • 要求数据可以分割独立的来比较;
  • 位之间由递进关系,如果 a 数据的高位比 b 数据大,那么剩下的地位就不用比较了;
  • 每一位的数据范围不能太大,要可以用线性排序,否则基数排序的时间复杂度无法做到 O(n)。

方案

按照优先从高位或低位来排序有两种实现方案:

  • MSD:由高位为基底,先按 k1 排序分组,同一组中记录, 关键码 k1 相等,再对各组按 k2 排序分成子组, 之后,对后面的关键码继续这样的排序分组,直到按最次位关键码 kd 对各子组排序后,再将各组连接起来,便得到一个有序序列。MSD 方式适用于位数多的序列。
  • LSD:由低位为基底,先从 kd 开始排序,再对 kd - 1 进行排序,依次重复,直到对 k1 排序后便得到一个有序序列。LSD 方式适用于位数少的序列。

实现

/**
	* name: 基数排序
	* @param  array 待排序数组
	* @param  max 最大位数
	*/
const radixSort = (array, max) => {
	console.time('计数排序耗时');
	const buckets = [];
	let unit = 10,
		base = 1;
	for (let i = 0; i < max; i++, base *= 10, unit *= 10) {
		for (let j = 0; j < array.length; j++) {
			let index = ~~((array[j] % unit) / base); //依次过滤出个位,十位等等数字
			if (buckets[index] == null) {
				buckets[index] = []; //初始化桶
			}
			buckets[index].push(array[j]); //往不同桶里添加数据
		}
		let pos = 0,
			value;
		for (let j = 0, length = buckets.length; j < length; j++) {
			if (buckets[j] != null) {
				while ((value = buckets[j].shift()) != null) {
					array[pos++] = value; //将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞
				}
			}
		}
	}
	console.timeEnd('计数排序耗时');
	return array;
};

测试

const array = [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48];
console.log('原始array:', array);
const newArr = radixSort(array, 2);
console.log('newArr:', newArr);
// 原始 array:  [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48]
// 堆排序耗时:   0.064208984375ms
// newArr:  	 [2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]

分析

  • 第一,基数排序是原地排序算法吗 ?
    因为计数排序的空间复杂度为 O(n + k),所以不是原地排序算法。

  • 第二,基数排序是稳定的排序算法吗 ?
    基数排序不改变相同元素之间的相对顺序,因此它是稳定的排序算法。

  • 第三,基数排序的时间复杂度是多少 ?
    最佳情况:T(n) = O(n * k)
    最差情况:T(n) = O(n * k)
    平均情况:T(n) = O(n * k)
    其中,k 是待排序列最大值。

动画

LSD 基数排序动图演示:

radixSort.gif

4. 复杂度对比

十大经典排序算法的 时间复杂度与空间复杂度 比较。

名称 平均 最好 最坏 空间 稳定性 排序方式
冒泡排序 O(n2) O(n) O(n2) O(1) Yes In-place
插入排序 O(n2) O(n) O(n2) O(1) Yes In-place
选择排序 O(n2) O(n2) O(n2) O(1) No In-place
归并排序 O(n log n) O(n log n) O(n log n) O(n) Yes Out-place
快速排序 O(n log n) O(n log n) O(n2) O(logn) No In-place
希尔排序 O(n log n) O(n log2 n) O(n log2 n) O(1) No In-place
堆排序 O(n log n) O(n log n) O(n log n) O(1) No In-place
桶排序 O(n + k) O(n + k) O(n2) O(n + k) Yes Out-place
计数排序 O(n + k) O(n + k) O(n + k) O(k) Yes Out-place
基数排序 O(n * k) O(n * k) O(n * k) O(n + k) Yes Out-place

名词解释:

  • n:数据规模;
  • k:桶的个数;
  • In-place: 占用常数内存,不占用额外内存;
  • Out-place: 占用额外内存。

5. 算法可视化工具

  • 算法可视化工具 algorithm-visualizer
    算法可视化工具 algorithm-visualizer 是一个交互式的在线平台,可以从代码中可视化算法,还可以看到代码执行的过程。旨在通过交互式可视化的执行来揭示算法背后的机制。
    效果如下图:
    算法可视化工具

  • 算法可视化动画网站 https://visualgo.net/en
    效果如下图:
    quick-sort.gif

  • 算法可视化动画网站 www.ee.ryerson.ca
    效果如下图:
    insert-sort.gif

  • illustrated-algorithms
    变量和操作的可视化表示增强了控制流和实际源代码。您可以快速前进和后退执行,以密切观察算法的工作方式。
    效果如下图:
    binary-search.gif

6. 系列文章

JavaScript 数据结构与算法之美 系列文章,暂时写了如下的 11 篇文章,后续还有想写的内容,再补充。

所写的内容只是数据结构与算法内容的冰山一角,如果你还想学更多的内容,推荐学习王争老师的 数据结构与算法之美

从时间和空间复杂度、基础数据结构到排序算法,文章的内容有一定的关联性,所以阅读时推荐按顺序来阅读,效果更佳。

标题 链接
1. 时间和空间复杂度 #29
2. 线性表(数组、链表、栈、队列) #34
3. 实现一个前端路由,如何实现浏览器的前进与后退 ? #30
4. 栈内存与堆内存 、浅拷贝与深拷贝 #35
5. 递归 #36
6. 非线性表(树、堆) #37
7. 冒泡排序、选择排序、插入排序 #39
8. 归并排序、快速排序、希尔排序、堆排序 #40
9. 计数排序、桶排序、基数排序 #41
10. 十大经典排序算法汇总 #42
11. 强烈推荐 GitHub 上值得前端学习的数据结构与算法项目 #43

如果有错误或者不严谨的地方,请务必给予指正,以免误人子弟,十分感谢。

7. 最后

文中所有的代码及测试事例都已经放到我的 GitHub 上了。

笔者为了写好这系列的文章,花费了大量的业余时间,边学边写,边写边修改,前后历时差不多 2 个月,入门级的文章总算是写完了。

如果你觉得有用或者喜欢,就点收藏,顺便点个赞吧,你的支持是我最大的鼓励 !

面试题之从敲入 URL 到浏览器渲染完成

前言

小汪最近在看【WebKit 技术内幕】一书,说实话,这本书写的太官方了,不通俗易懂。
但是看完书,对浏览器内核的 WebKit 有了进一步的了解,所以从浏览器内核出发,写这篇文章以记录学到的知识,以加深对 WebKit 的理解。

相信很多开发人员在面试时都遇到这个问题,这道题可说是非常非常难的,因为深度可以非常深,广度可以非常广。这题是非常能考查一个前端开发人员的知识体系的题目。

写这篇文章的时候,边写边觉得难 !!!

1. 大致过程

当你这样子回答的时候:

  • 用户输入 url 地址,浏览器查询 DNS 查找对应的请求 IP 地址

  • 建立 TCP 连接

  • 浏览器向服务器发送 http 请求,如果服务器段返回以 301 之类的重定向,浏览器根据相应头中的 location 再次发送请求

  • 服务器端接受请求,处理请求生成 html 代码,返回给浏览器,这时的 html 页面代码可能是经过压缩的

  • 浏览器接收服务器响应结果,如果有压缩则首先进行解压处理,紧接着就是页面解析渲染

  • 解析该过程分为:解析 HTML,构建 DOM 树,DOM 树与 CSS 样式进行附着构造呈现树,布局、绘制

虽然这大致的过程是对的,但回答不上细节 !深度不够!!!

面试官给你的脸色是:“很遗憾,这不是我们要的回答 ! ”

2. 详细过程

下面让我们扒下各个过程细节的外衣,坦诚相见吧 !

2.1 输入地址

浏览器引入了 DNS 预取技术。它是利用现有的 DNS 机制,提前解析网页中可能的网络连接。

当我们开始在浏览器中输入网址的时候,浏览器其实就已经在智能的匹配可能得 url 了。它会从历史记录,书签等地方,找到已经输入的字符串可能对应的 url ,找到同输入的地址很匹配的项,然后给出智能提示,让你可以补全 url 地址。用户还没有按下 enter 键, 浏览器已经开始使用 DNS 预取技术解析该域名了。

对于 chrome 的浏览器,如果有该域名相关的缓存,它会直接从缓存中把网页展示出来,就是说,你还没有按下 enter,页面就出来了。如果没有缓存,就还是会重新请求资源。

2.2 查询 DNS 查找对应的请求 IP 地址

假设输入 www.baidu.com,大概过程:

  • 浏览器搜索自己的 DNS 缓存。

  • 在浏览器缓存中没找到,就在操作系统缓存中查找,这一步中也会查找本机的 hosts 看看有没有对应的域名映射。

  • 在系统中也没有的话,就到你的路由器来查找,因为路由器一般也会有自己的 DNS 缓存。

  • 若没有,则操作系统将域名发送至 本地域名服务器——递归查询方式,本地域名服务器 查询自己的 DNS 缓存,查找成功则返回结果,否则,采用迭代查询方式。本地域名服务器一般都是你的网络接入服务器商提供,比如**电信,**移动。

  • 本地域名服务器 将得到的 IP 地址返回给操作系统,同时自己也将 IP 地址缓存起来。

  • 操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起来,以备下次别的用户查询时,可以直接返回结果,加快网络访问。

  • 至此,浏览器已经得到了域名对应的 IP 地址。

参考文章:

https://blog.csdn.net/wlk2064819994/article/details/79756669
https://blog.csdn.net/dojiangv/article/details/51794535

2.3 建立 TCP 连接

TCP 是一种面向有连接的传输层协议。
它可以保证两端(发送端和接收端)通信主机之间的通信可达。
它能够处理在传输过程中丢包、传输顺序乱掉等异常情况;此外它还能有效利用宽带,缓解网络拥堵。

三次握手的步骤:(抽象派)

客户端:hello,你是server么?
服务端:hello,我是server,你是client么
客户端:yes,我是client

在 TCP 连接建立完成之后就可以发送 HTTP 请求了。

然后,待到断开连接时,需要进行四次挥手(因为是全双工的,所以需要四次挥手)

四次挥手的步骤:(抽象派)

主动方:我已经关闭了向你那边的主动通道了,只能被动接收了
被动方:收到通道关闭的信息
被动方:那我也告诉你,我这边向你的主动通道也关闭了
主动方:最后收到数据,之后双方无法通信

2.4 服务器收到请求并响应 HTTP 请求

在接收和解释请求消息后,服务器返回一个HTTP响应消息。

HTTP 响应由三个部分组成,分别是:状态行、消息报头、响应正文。

状态代码:由三位数字组成,第一个数字定义了响应的类别,且有五种可能取值:

  • 1xx:指示信息--表示请求已接收,继续处理
  • 2xx:成功--表示请求已被成功接收、理解、接受
  • 3xx:重定向--要完成请求必须进行更进一步的操作
  • 4xx:客户端错误--请求有语法错误或请求无法实现
  • 5xx:服务器端错误--服务器未能实现合法的请求

常见状态代码、状态描述、说明:

  • 200 OK :客户端请求成功
  • 400 Bad Request :客户端请求有语法错误,不能被服务器所理解
  • 401 Unauthorized :请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
  • 403 Forbidden :服务器收到请求,但是拒绝提供服务
  • 404 Not Found :请求资源不存在,eg:输入了错误的URL
  • 500 Internal Server Error :服务器发生不可预期的错误
  • 503 Server Unavailable :服务器当前不能处理客户端的请求,一段时间后可能恢复正常

HTTP消息报头包括:普通报头、请求报头、响应报头、实体报头。具体不作介绍。

响应正文:就是服务器返回的资源的内容

2.5 浏览器接收服务器响应结果并处理

在浏览器没有完整接受全部HTML文档时,它就已经开始显示这个页面了,不同浏览器可能解析的过程不太一样,这里我们只介绍 WebKit 的渲染过程。

渲染步骤大致可以分为以下几步:

1. 解析HTML,构建 DOM 树

2. 解析 CSS ,生成 CSS 规则树

3. 合并 DOM 树和 CSS 规则,生成 render 树

4. 布局 render 树( Layout / reflow ),负责各元素尺寸、位置的计算

5. 绘制 render 树( paint ),绘制页面像素信息

6. 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成( composite ),显示在屏幕上

其中每个解释的过程中,WebKit 都提供了很多相关的类来一步一步地解释对应的内部模块,这里面不做详细描述。

下面根据上面的大致过程来一步步细解。

2.5.1 构造 DOM 树

浏览器在解析html文件时, 是WebKit 中的 HTML 解释器的将网络或者本地磁盘获取的 HTML 网页和资源从字节流解释成 DOM 树结构。具体过程如下 :

在 WebKit 中这一过程如下:首先是字节流,经过解码之后是字符流,然后通过词法分析器会被解释成词语(Tokens),之后经过语法分析器构建成节点,最后这些节点被组建成一棵 DOM 树。

浏览器在解析html文件过程中,会 ”自上而下“ 加载,并在加载过程中进行解析渲染。在解析过程中,如果遇到请求外部资源时,如图片、外链的CSS、iconfont等,请求过程是异步的,并不会影响html文档进行加载,且统一交由 Browser 进程来处理,这使得资源在不同网页间的共享变得很容易。

HTML 的解释、布局和渲染等工作基本上就是工作在渲染线程完成的(这不是绝对的)。因为 DOM 树只能在渲染线程上创建和访问,这也就是说构建 DOM 树的过程只能在渲染线程中进行,但是,从字符到词语这个阶段可以交给另外的单独的线程来做。

而且因为有 DNS 预取技术,当用户正在浏览当前网页的时候,Chromium 提取网页中的超链接,将域名抽取出来,利用比较少的 CPU 和网络带宽来解析这些域名或者 IP 地址,这样一来,用户根本感觉不到这一过程。当用户单击这些链接的时候,可以节省不少时间,特别在域名解析比较慢的时候,效果特别明显。

解析过程中,浏览器首先会解析 HTML 文件构建 DOM 树,然后解析 CSS 文件构建 Render树,等到 Render 树构建完成后,浏览器开始布局 Render 树并将其绘制到屏幕上。

详情参考小汪之前写的文章:浏览器内核之 HTML 解释器和 DOM 模型

2.5.2 解释 CSS

CSS 解释过程是指从 CSS 字符串 经过 CSS 解释器 处理后变成渲染引擎内部规则的表示过程。

生成样式规则之后,会进行样式规则匹配,WebKit 会为其中的一些节点(只限于可视节点)选择合适的样式信息,规则的匹配则是由 ElementRuleCollector 类来计算并获得,它根据元素的属性等,并从 DocumentRuleSets 类中获取规则集合,依次按照 ID、类别、标签等选择器信息逐次匹配获得元素的样式。

最后,WebKit 对这些规则进行排序。对于该元素需要的样式属性,WebKit 选择从高优先级规则中选取,并将样式属性值返回。

从整个网页的加载和渲染过程来看,CSS 解释和规则匹配处于 DOM 树建立之后,RenderObject 树建立之前,CSS 解释器解释后的结果会保存起来,然后 RenderObject 树基于该结果来进行规范匹配和布局计算。当网页有用户交互或者动画等动作的时候,通过 CSSDOM 等技术,JavaScript 代码同样可以非常方便地修改 CSS 代码,WebKit 此时需要重新解释样式并重复以上这一过程。

参考小汪之前写的文章:浏览器内核之 CSS 解释器和样式布局

2.5.3 渲染过程遇到 JavaScript

当文档加载过程中遇到 js 文件,html 文档会挂起渲染(加载解析渲染同步)的线程,不仅要等待文档中 js 文件加载完毕,还要等待解析执行完毕,才可以恢复 html 文档的渲染线程。因为 JS 有可能会修改 DOM,最为经典的 document.write,这意味着,在 JS 执行完成前,后续所有资源的下载可能是没有必要的,这是 js 阻塞后续资源下载的根本原因。所以我们平时的代码中,js 是放在 html 文档末尾的。

而且当遇到执行 JavaScript 代码的时候,WebKit 先暂停当前 JavaScript 代码的执行,使用预先扫描器 HTMLPreloadScanner 类来扫描后面的词语。如果 WebKit 发现它们需要使用其他资源,那么使用预资源加载器 HTMLPreloadScanner 类来发送请求,在这之后,才执行 JavaScript 代码。预先扫描器本身并不创建节点对象,也不会构建 DOM 树,所以速度比较快。

当 DOM 树构建完之后,WebKit 触发 “DOMContentLoaded” 事件,注册在该事件上的 JavaScript 函数会被调用。当所在资源都被加载完之后,WebKit 触发 “onload” 事件。

WebKit 将 DOM 树创建过程中需要执行的 JavaScript 代码交由 HTMLScriptRunner 类来负责。工作方式很简单,就是利用 JavaScript 引擎来执行 Node 节点中包含的代码。

JS 的解析是由浏览器中的 JavaScript 引擎完成的。JS是单线程运行,也就是说,在同一个时间内只能做一件事,所有的任务都需要排队,前一个任务结束,后一个任务才能开始。但是又存在某些任务比较耗时,如 IO 读写等,所以需要一种机制可以先执行排在后面的任务,这就是:同步任务(synchronous)和异步任务(asynchronous)。

JS 的执行机制就可以看做是一个主线程加上一个任务队列(task queue)。同步任务就是放在主线程上执行的任务,异步任务是放在任务队列中的任务。所有的同步任务在主线程上执行,形成一个执行栈; 异步任务有了运行结果就会在任务队列中放置一个事件;脚本运行时先依次运行执行栈,然后会从任务队列里提取事件,运行任务队列中的任务,这个过程是不断重复的,所以又叫做事件循环(Event loop)。

参考小汪之前写的文章:浏览器之 javaScript 引擎

2.5.4 渲染合成 Render 树

HTML 经过 WebKit 解释之后,生成 DOM 树。在 DOM 树构建完成之后,WebKit 会为 DOM 树节点构建 RenderObject 树,再通过 RenderObject 树构建出 RenderLayer 树。

RenderObject 树是基于 DOM 树建立起来的一棵新树,是为了布局计算和渲染等机制而构建的一种新的内部表示。RenderObject 树节点和 DOM 节点不是一一对应关系,因为有可视节点(常用的 div img 标签等)与不可视节点(如 head、meta 标签),不可视节点是不会构成 RenderObject 树的。

网页是有层次结构的,可以分层的,一是为了方便设置网页的层次,二是为了 WebKit 处理上的便利,为了简化渲染的逻辑。

而且 RenderLayer 节点和 RenderObject 节点不是一一对应关系,而是一对多的关系。

2.5.5 布局

当 WebKit 创建 RenderObject 对象之后,每个对象是不知道自己的位置、大小等信息的,WebKit 根据框模型来计算它们的位置,大小等信息的过程称为布局计算。

布局计算是一个递归的过程,因为一个节点的大小通常需要先计算它的子女节点的位置,大小等信息。

当用户 网页的动画、翻滚网页、JavaScript 代码通过 CSSDOM 等操作时还会有重新布局。

参考小汪之前写的文章:浏览器内核之 CSS 解释器和样式布局

2.5.6 绘图

在 WebKit 中,绘图操作就是绘图上下文,所有绘图的操作都是在该上下文中来进行的。

绘图上下文可以分成两种类型:

一是 2D 图形上下文(GraphicsContext),用来绘制 2D 图形的的上下文;

二是 3D 绘图上下文,是用来绘制 3D 图形的上下文。

2D 绘图上下文具体的作用:提供基本绘图单元的绘制接口以及设置绘图的样式。绘图接口包括画点,画线、画图片、画多边形、画文字等,绘图样式包括颜色、线宽、字号大小、渐变等。

关于 3D 绘图上下文,它的主要用处是支持 CSS3D、WebGL 等。

网页的渲染方式,有三种方式,一是软件渲染,二是硬件加速渲染,三可以说是混合模式。

如果绘图操作使用 CPU 来完成,称之为软件绘图。

如果绘图操作由 GPU 来完成,称之为 GPU 硬件加速绘图。

理想情况下,每个层都有个绘制的存储区域,这个存储区域用来保存绘图的结果。最后,需要将这些层的内容合并到同一个图像之中,可以称之为合成(Compositing),使用了合成技术的渲染称之为合成化渲染。

所以,在完成构建 DOM 树之后,WebKit 会调用绘图操作、软件渲染或者硬件加速渲染或者两者都有,将模型绘制出来,呈现在屏幕上。
至此,浏览器渲染完成。

详情参考小汪之前写的文章:浏览器内核之渲染基础

最后

现在,当面试官再问你 “从敲入 URL 到浏览器渲染完成” 的时候,你的内心是不是这样的 ?

强烈推荐 GitHub 上值得前端学习的开源实战项目

强烈推荐 GitHub 上值得前端学习的开源实战项目。

Vue.js

React.js

Angular

Node.js

最后

笔者博客首更地址 :https://github.com/biaochenxuying/blog

JavaScript 数据结构与算法之美 - 实现一个前端路由,如何实现浏览器的前进与后退 ?

1. 需求

如果要你实现一个前端路由,应该如何实现浏览器的前进与后退 ?

2. 问题

首先浏览器中主要有这几个限制,让前端不能随意的操作浏览器的浏览纪录:

  • 没有提供监听前进后退的事件。
  • 不允许开发者读取浏览纪录,也就是 js 读取不了浏览纪录。
  • 用户可以手动输入地址,或使用浏览器提供的前进后退来改变 url。

所以要实现一个自定义路由,解决方案是自己维护一份路由历史的记录,从而区分 前进、刷新、回退。

下面介绍具体的方法。

3. 方法

目前笔者知道的方法有两种,一种是 在数组后面进行增加与删除,另外一种是 利用栈的后进先出原理

3.1 在数组最后进行 增加与删除

通过监听路由的变化事件 hashchange,与路由的第一次加载事件 load ,判断如下情况:

  • url 存在于浏览记录中即为后退,后退时,把当前路由后面的浏览记录删除。
  • url 不存在于浏览记录中即为前进,前进时,往数组里面 push 当前的路由。
  • url 在浏览记录的末端即为刷新,刷新时,不对路由数组做任何操作。

另外,应用的路由路径中可能允许相同的路由出现多次(例如 A -> B -> A),所以给每个路由添加一个 key 值来区分相同路由的不同实例。

注意:这个浏览记录需要存储在 sessionStorage 中,这样用户刷新后浏览记录也可以恢复。

笔者之前实现的 用原生 js 实现的轻量级路由 ,就是用这种方法实现的,具体代码如下:

// 路由构造函数
function Router() {
        this.routes = {}; //保存注册的所有路由
        this.routerViewId = "#routerView"; // 路由挂载点 
        this.stackPages = true; // 多级页面缓存
        this.history = []; // 路由历史
}

Router.prototype = {
        init: function(config) {
            var self = this;
            //页面首次加载 匹配路由
            window.addEventListener('load', function(event) {
                // console.log('load', event);
                self.historyChange(event)
            }, false)

            //路由切换
            window.addEventListener('hashchange', function(event) {
                // console.log('hashchange', event);
                self.historyChange(event)
            }, false)

        },
        // 路由历史纪录变化
        historyChange: function(event) {
            var currentHash = util.getParamsUrl();
            var nameStr = "router-history"
            this.history = window.sessionStorage[nameStr] ? JSON.parse(window.sessionStorage[nameStr]) : []

            var back = false, // 后退
                refresh = false, // 刷新
                forward = false, // 前进
                index = 0,
                len = this.history.length;

            // 比较当前路由的状态,得出是后退、前进、刷新的状态。
            for (var i = 0; i < len; i++) {
                var h = this.history[i];
                if (h.hash === currentHash.path && h.key === currentHash.query.key) {
                    index = i
                    if (i === len - 1) {
                        refresh = true
                    } else {
                        back = true
                    }
                    break;
                } else {
                    forward = true
                }
            }
            if (back) {
                 // 后退,把历史纪录的最后一项删除
                this.historyFlag = 'back'
                this.history.length = index + 1
            } else if (refresh) {
                 // 刷新,不做其他操作
                this.historyFlag = 'refresh'
            } else {
                // 前进,添加一条历史纪录
                this.historyFlag = 'forward'
                var item = {
                    key: currentHash.query.key,
                    hash: currentHash.path,
                    query: currentHash.query
                }
                this.history.push(item)
            }
            // 如果不需要页面缓存功能,每次都是刷新操作
            if (!this.stackPages) {
                this.historyFlag = 'forward'
            }
            window.sessionStorage[nameStr] = JSON.stringify(this.history)
        },
    }

以上代码只列出本次文章相关的内容,完整的内容请看 原生 js 实现的轻量级路由,且页面跳转间有缓存功能

3.2 利用栈的 后进者先出,先进者后出 原理

在说第二个方法之前,先来弄明白栈的定义与后进者先出,先进者后出原理。

3.2.1 定义

栈的特点:后进者先出,先进者后出

举一个生活中的例子说明:就是一摞叠在一起的盘子。我们平时放盘子的时候,都是从下往上一个一个放;取的时候,我们也是从上往下一个一个地依次取,不能从中间任意抽出。

因为栈的后进者先出,先进者后出的特点,所以只能栈一端进行插入和删除操作。这也和第一个方法的原理有异曲同工之妙。

下面用 JavaScript 来实现一个顺序栈:

// 基于数组实现的顺序栈
class ArrayStack {
  constructor(n) {
      this.items = [];  // 数组
      this.count = 0;   // 栈中元素个数
      this.n = n;       // 栈的大小
  }

  // 入栈操作
  push(item) {
    // 数组空间不够了,直接返回 false,入栈失败。
    if (this.count === this.n) return false;
    // 将 item 放到下标为 count 的位置,并且 count 加一
    this.items[this.count] = item;
    ++this.count;
    return true;
  }
  
  // 出栈操作
  pop() {
    // 栈为空,则直接返回 null
    if (this.count == 0) return null;
    // 返回下标为 count-1 的数组元素,并且栈中元素个数 count 减一
    let tmp = items[this.count-1];
    --this.count;
    return tmp;
  }
}

其实 JavaScript 中,就是执行 main 函数求和,main 函数里面又调用了 add 函数,先调用的先进入栈。

3.2.2 应用

栈的经典应用: 函数调用栈

操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。为了让你更好地理解,我们一块来看下这段代码的执行过程。

function add(x, y) {
   let sum = 0;
   sum = x + y;
   return sum;
}

function main() {
   let a = 1; 
   let ret = 0;
   let res = 0;
   ret = add(3, 5);
   res = a + ret;
   console.log("res: ", res);
   reuturn 0;
}

main();

上面代码也很简单,就是执行 main 函数求和,main 函数里面又调用了 add 函数,先调用的先进入栈。

执行过程如下:

3.2.3 实现浏览器的前进、后退

第二个方法就是:用两个栈实现浏览器的前进、后退功能。

我们使用两个栈,X 和 Y,我们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当我们点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。

比如你顺序查看了 a,b,c 三个页面,我们就依次把 a,b,c 压入栈,这个时候,两个栈的数据如下:

当你通过浏览器的后退按钮,从页面 c 后退到页面 a 之后,我们就依次把 c 和 b 从栈 X 中弹出,并且依次放入到栈 Y。这个时候,两个栈的数据就是这个样子:

这个时候你又想看页面 b,于是你又点击前进按钮回到 b 页面,我们就把 b 再从栈 Y 中出栈,放入栈 X 中。此时两个栈的数据是这个样子:

这个时候,你通过页面 b 又跳转到新的页面 d 了,页面 c 就无法再通过前进、后退按钮重复查看了,所以需要清空栈 Y。此时两个栈的数据这个样子:

如果用代码来实现,会是怎样的呢 ?各位可以想一下。

其实就是在第一个方法的代码里面, 添加多一份路由历史纪录的数组即可,对这两份历史纪录的操作如上面示例图所示即可,也就是对数组的增加和删除操作而已, 这里就不展开了。

其中第二个方法与参考了 王争老师的 数据结构与算法之美

5. 最后

博客首更地址 :https://github.com/biaochenxuying/blog

参考文章:数据结构与算法之美

JS 是单线程,你了解其运行机制吗?

一. 区分进程和线程

很多新手是区分不清线程和进程的,没有关系。这很正常。先看看下面这个形象的比喻:

进程是一个工厂,工厂有它的独立资源-工厂之间相互独立-线程是工厂中的工人,多个工人协作完成任务-工厂内有一个或多个工人-工人之间共享空间

如果是 windows 电脑中,可以打开任务管理器,可以看到有一个后台进程列表。对,那里就是查看进程的地方,而且可以看到每个进程的内存资源信息以及 cpu 占有率。

image

所以,应该更容易理解了:进程是 cpu 资源分配的最小单位(系统会给它分配内存)

最后,再用较为官方的术语描述一遍:

  • 进程是 cpu 资源分配的最小单位(是能拥有资源和独立运行的最小单位)

  • 线程是 cpu 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

提示:

  • 不同进程之间也可以通信,不过代价较大

  • 现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多。(所以核心还是得属于一个进程才行)

二. 浏览器是多进程的

理解了进程与线程了区别后,接下来对浏览器进行一定程度上的认识:(先看下简化理解)

  • 浏览器是多进程的

  • 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)

  • 简单点理解,每打开一个 Tab 页,就相当于创建了一个独立的浏览器进程。

关于以上几点的验证,请再第一张图:

image

图中打开了 Chrome 浏览器的多个标签页,然后可以在 Chrome 的任务管理器中看到有多个进程(分别是每一个 Tab 页面有一个独立的进程,以及一个主进程)。

感兴趣的可以自行尝试下,如果再多打开一个 Tab 页,进程正常会 +1 以上(不过,某些版本的 ie 却是单进程的)

**注意:**在这里浏览器应该也有自己的优化机制,有时候打开多个 tab 页后,可以在 Chrome 任务管理器中看到,有些进程被合并了(所以每一个 Tab 标签对应一个进程并不一定是绝对的)

三、为什么 JavaScript 是单线程 ?

JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript 不能有多个线程呢 ?这样能提高效率啊。

JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。

四. JavaScript是单线程,怎样执行异步的代码 ?

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
js 引擎执行异步代码而不用等待,是因有为有 消息队列和事件循环。

  • 消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
  • 事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。

实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。

事件循环用代码表示大概是这样的:

while(true) {
    var message = queue.get();
    execute(message);
}

那么,消息队列中放的消息具体是什么东西?消息的具体结构当然跟具体的实现有关,但是为了简单起见,我们可以认为:

消息就是注册异步任务时添加的回调函数。

再次以异步 AJAX 为例,假设存在如下的代码:

$.ajax('http://segmentfault.com', function(resp) {
    console.log('我是响应:', resp);
});

// 其他代码
...
...
...

主线程在发起 AJAX 请求后,会继续执行其他代码。AJAX 线程负责请求 segmentfault.com,拿到响应后,它会把响应封装成一个 JavaScript 对象,然后构造一条消息:

// 消息队列中的消息就长这个样子
var message = function () {
    callbackFn(response);
}

其中的 callbackFn 就是前面代码中得到成功响应时的回调函数。

主线程在执行完当前循环中的所有代码后,就会到消息队列取出这条消息(也就是 message 函数),并执行它。到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,AJAX 线程在收到 HTTP 响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。

用图表示这个过程就是:

image

从上文中我们也可以得到这样一个明显的结论,就是:

异步过程的回调函数,一定不在当前这一轮事件循环中执行。

事件循环进阶:macrotask 与 microtask

一张图展示 JavaScript 中的事件循环:

image

一次事件循环:先运行 macroTask 队列中的一个,然后运行 microTask 队列中的所有任务。接着开始下一次循环(只是针对 macroTask 和 microTask,一次完整的事件循环会比这个复杂的多)。

JS 中分为两种任务类型:macrotask 和 microtask,在 ECMAScript 中,microtask 称为 jobs,macrotask 可称为 task。

它们的定义?区别?简单点可以按如下理解:

macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)

每一个 task 会从头到尾将这个任务执行完毕,不会执行其它

浏览器为了能够使得 JS 内部 task 与 DOM 任务能够有序的执行,会在一个 task 执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
(task -> 渲染 -> task ->...)

microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务

也就是说,在当前 task 任务后,下一个 task 之前,在渲染之前

所以它的响应速度相比 setTimeout(setTimeout是task)会更快,因为无需等渲染

也就是说,在某一个 macrotask 执行完后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染前)

分别很么样的场景会形成 macrotask 和 microtask 呢 ?

macroTask: 主代码块, setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering(可以看到,事件队列中的每一个事件都是一个 macrotask)

microTask: process.nextTick, Promise, Object.observe, MutationObserver

补充:在 node 环境下,process.nextTick 的优先级高于 Promise,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的 nextTickQueue 部分,然后才会执行微任务中的 Promise 部分。

另外,setImmediate 则是规定:在下一次 Event Loop(宏任务)时触发(所以它是属于优先级较高的宏任务),(Node.js 文档中称,setImmediate 指定的回调函数,总是排在 setTimeout 前面),所以 setImmediate 如果嵌套的话,是需要经过多个 Loop 才能完成的,而不会像 process.nextTick 一样没完没了。

实践:上代码

我们以 setTimeout、process.nextTick、promise 为例直观感受下两种任务队列的运行方式。

console.log('main1');

process.nextTick(function() {
    console.log('process.nextTick1');
});

setTimeout(function() {
    console.log('setTimeout');
    process.nextTick(function() {
        console.log('process.nextTick2');
    });
}, 0);

new Promise(function(resolve, reject) {
    console.log('promise');
    resolve();
}).then(function() {
    console.log('promise then');
});

console.log('main2');

别着急看答案,先以上面的理论自己想想,运行结果会是啥?

最终结果是这样的:

main1
promise
main2
process.nextTick1
promise then
setTimeout
process.nextTick2

process.nextTick 和 promise then在 setTimeout 前面输出,已经证明了macroTask 和 microTask 的执行顺序。但是有一点必须要指出的是。上面的图容易给人一个错觉,就是主进程的代码执行之后,会先调用 macroTask,再调用 microTask,这样在第一个循环里一定是 macroTask 在前,microTask在后。

但是最终的实践证明:在第一个循环里,process.nextTick1 和 promise then 这两个 microTask 是在 setTimeout 这个 macroTask 里之前输出的,这是为什么呢 ?

因为主进程的代码也属于 macroTask(这一点我比较疑惑的是主进程都是一些同步代码,而 macroTask 和 microTask 包含的都是一些异步任务,为啥主进程的代码会被划分为 macroTask,不过从实践来看确实是这样,而且也有理论支撑:【翻译】Promises/A+ 规范)。

主进程这个 macroTask(也就是 main1、promise 和 main2 )执行完了,自然会去执行 process.nextTick1 和 promise then 这两个 microTask。这是第一个循环。之后的 setTimeout 和process.nextTick2 属于第二个循环。

别看上面那段代码好像特别绕,把原理弄清楚了,都一样 ~

requestAnimationFrame、Object.observe(已废弃) 和 MutationObserver 这三个任务的运行机制大家可以从上面看到,不同的只是具体用法不同。重点说下 UI rendering。在 HTML 规范:event-loop-processing-model 里叙述了一次事件循环的处理过程,在处理了 macroTask 和 microTask 之后,会进行一次 Update the rendering,其中细节比较多,总的来说会进行一次 UI 的重新渲染。

事件循环机制进一步补充

这里就直接引用一张图片来协助理解:(参考自 Philip Roberts 的演讲《Help, I’m stuck in an event-loop》)

image

上图大致描述就是:

  • 主线程运行时会产生执行栈,栈中的代码调用某些 api 时,它们会在事件队列中添加各种事件(当满足触发条件后,如 ajax 请求完毕)
  • 而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
  • 如此循环
  • 注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件

五. 最后

看到这里,应该对 JS 的运行机制有一定的理解了吧。

参考:

  1. JavaScript 运行机制详解:再谈Event Loop

  2. 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

  3. 总是一知半解的Event Loop

  4. JavaScript:理解同步、异步和事件循环

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.