在写这篇前端散记之前有写过另外两篇散记,可点击 前端散记前端散记 2 访问。所谓散记,东西都比较零散,更谈不上什么深入,但是至少可以让读者知道一些概念理论,如果深入可以自行去查询相关知识。

async await 实现原理

async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。

1
2
3
4
5
6
7
8
9
10
11
async function fn(args){
// ...
}

// 等同于

function fn(args){
return spawn(function*() {
// ...
});
}

所有的 async 函数都可以写成上面的第二种形式,其中的 spawn 函数就是自动执行器。

下面给出 spawn 函数的实现,基本就是前文自动执行器的翻版。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();

function step(nextF) {
try {
var next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}

async 函数是非常新的语法功能,新到都不属于 ES6,而是属于 ES7。目前,它仍处于提案阶段,但是转码器 Babel 和 regenerator 都已经支持,转码后就能使用。

async await 注意点

await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sync function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}

// 另一种写法

async function myFunction() {
await somethingThatReturnsAPromise().catch(function (err){
console.log(err);
});
}

await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。

1
2
3
4
5
6
7
8
async function dbFuc(db) {
let docs = [{}, {}, {}];

// 报错
docs.forEach(function (doc) {
await db.post(doc);
});
}

上面代码会报错,因为 await 用在普通函数之中了。但是,如果将 forEach 方法的参数改成 async 函数,也有问题。

1
2
3
4
5
6
7
8
async function dbFuc(db) {
let docs = [{}, {}, {}];

// 可能得到错误结果
docs.forEach(async function (doc) {
await db.post(doc);
});
}

上面代码可能不会正常工作,原因是这时三个 db.post 操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用 for 循环。

1
2
3
4
5
6
7
async function dbFuc(db) {
let docs = [{}, {}, {}];

for (let doc of docs) {
await db.post(doc);
}
}

如果确实希望多个请求并发执行,可以使用 Promise.all 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));

let results = await Promise.all(promises);
console.log(results);
}

// 或者使用下面的写法

async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));

let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}

ES5 实现继承

第一种方式是借助call实现继承:

1
2
3
4
5
6
7
8
function Parent1(){
this.name = 'parent1';
}
function Child1(){
Parent1.call(this);
this.type = 'child1'
}
console.log(new Child1);

复制代码这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法。

第二种方式借助原型链实现继承:

1
2
3
4
5
6
7
8
9
10
function Parent2() {
this.name = 'parent2';
this.play = [1, 2, 3]
}
function Child2() {
this.type = 'child2';
}
Child2.prototype = new Parent2();

console.log(new Child2());

看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:

1
2
3
4
var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play);

可以看到控制台:

明明我只改变了s1的play属性,为什么s2也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象。

那么还有更好的方式么?

第三种方式:将前两种组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
function Child3() {
Parent3.call(this);
this.type = 'child3';
}
Child3.prototype = new Parent3();
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);

可以看到控制台:

之前的问题都得以解决。但是这里又徒增了一个新问题,那就是Parent3的构造函数会多执行了一次(Child3.prototype = new Parent3();)。这是我们不愿看到的。那么如何解决这个问题?

第四种方式: 组合继承的优化1

1
2
3
4
5
6
7
8
9
function Parent4 () {
this.name = 'parent4';
this.play = [1, 2, 3];
}
function Child4() {
Parent4.call(this);
this.type = 'child4';
}
Child4.prototype = Parent4.prototype;

这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试一下:

1
2
3
var s3 = new Child4();
var s4 = new Child4();
console.log(s3)

子类实例的构造函数是Parent4,显然这是不对的,应该是Child4。

第五种方式(最推荐使用):优化2

1
2
3
4
5
6
7
8
9
10
function Parent5 () {
this.name = 'parent5';
this.play = [1, 2, 3];
}
function Child5() {
Parent5.call(this);
this.type = 'child5';
}
Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;

这是最推荐的一种方式,接近完美的继承。

因此,小小的继承虽然ES6当中简化了很多,但毋庸置疑的是,js是一门基于原型的语言,所以,用ES5来完成继承非常考验面试者对JS语言本身的理解,尤其是对于原型链是否理解清楚。

虚拟DOM和Diff算法

虚拟DOM

虚拟Dom(virtual dom)到底是什么,简单来讲,就是将真实的dom节点用JavaScript来模拟出来,而Dom变化的对比,放到 Js 层来做。

  • 传统DOM节点
1
2
3
4
<ul id="list">
<li class="item">jelon1</li>
<li class="item">jelon2</li>
</ul>
  • 对应虚拟DOM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
tag: 'ul',
attrs: {
id: 'list'
},
children: [
{
tag: 'li',
attrs: {
className: 'item'
},
children: [ 'jelon1' ]
},
{
tag: 'li',
attrs: {
className: 'item'
},
children: [ 'jelon2' ]
}
]
}
  • render方法

diff算法

关键词:patch

可参考详解vue的diff算法

工作中项目优化实践

  1. css sprite(雪碧图),iconfont;
  2. 资源懒加载、异步路由;
  3. 前端缓存 LocalStorage 及混合应用 Storage;
  4. 使用 CDN(JS、CSS等资源使用外部域名);
  5. Tree Shaking (Webpack 4.0+);
  6. 预请求;
  7. 资源按需加载;
  8. gzip (服务器);
  9. 减少 DOM 操作;
  10. 防抖节流;
  11. 尽量少用iframe;
  12. 尽量规避 CSS 计算。

参考思路

雅虎军规

Best Practices for Speeding Up Your Web Site

defineProperty vs proxy

  • Proxy可以直接监听对象而非属性;
  • Proxy直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty;
  • Proxy可以直接监听数组的变化;
  • Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。