从性能讲起

一些读书总结

DNS

TCP

HTTP请求

节流防抖?

webpack和Gzip

  • DllPlugin: 库不变更,我绝不打包!
  • Happypack: 你电脑是多核的,我的loaders全都要用!
  • webpack-bundle-analyzer:让我看看你项目内裤的大小
  • require.ensure: 动态加载我最在行
  • Gzip 后端:前端小老弟,你想更快吗?

HTTP 返回

  • 服务端渲染: 服务端跑完虚拟dom后,把页面返回给浏览器,此时将浏览器的一部分工作交给服务器做,能者多劳,但在 “网民横行” 的现在,浏览器数量远大于服务器数量,服务器亚历山大,这算是服务端渲染的一个弊端。优先考虑其他seo方案,无路可走时,最后一步买服务器做服务端渲染! 「吐槽,加缓存后的node中间层压力有多大?」

css

#myList li {}

我们是从 #myList => li 开始读, 坑爹的浏览器是从 li -> #myList,它会先遍历所有 li 节点!!!
再看看它!
……

* {}
so:

  • 别特娘的用 通配符了
  • 最好别用元素选择器,用类名!
  • css 渲染是阻塞的: 我没渲染呢,你们都等等 (css资源先加载先渲染)

js

  • async defer 模式

Dom操作咋就这么慢

  • JS 引擎和渲染引擎 之间的“桥连”,竟然要收tmd过桥费!,所以你以后“送货”的时候尽量一次性送完

Event Loop

  • macro-task 比如: setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作、UI 渲染等
  • micro-task 比如: process.nextTick、Promise、 MutationObserver 等。

macro-task 出栈是一个个出的,micro-task 是一组一组出的。
上面这句话你一定没听懂哈哈哈哈,看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function main() {
setTimeout(() => {
console.log(1)
}, 0)
new Promise((resolve) => {
resolve(2)
}).then(ev => console.log(ev))
setTimeout(() => {
console.log(4)
}, 0)
new Promise((resolve) => {
resolve(3)
}).then(ev => console.log(ev))
}

2 3 1 4
以上结果你肯定能根据经验主义猜出来,但你还可以清晰的理解它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function main() { <- 宏任务 0
setTimeout(() => { <- 宏任务 1
console.log(1)
}, 0)
new Promise((resolve) => { <- 微任务 1
resolve(2)
}).then(ev => console.log(ev))
setTimeout(() => { <- 宏任务 2
console.log(4)
}, 0)
new Promise((resolve) => { 微任务 2
resolve(3)
}).then(ev => console.log(ev))
}

先把这些任务排到队列里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
macro<list> = [宏任务 0, 宏任务 1, 宏任务 2]
micro<list> = [微任务 1, 微任务 2]
```

再读这句话: macro-task 出队列是一个个出的,micro-task 是一组一组出的。
宏任务 0 -> 微任务1 -> 微任务 2 -> 宏任务 1 -> 宏任务 2

别急,再练习一下

```javascript
function main() {
setTimeout(() => {
console.log(1)
}, 0)
new Promise((resolve) => {
resolve(2)
}).then(ev => console.log(ev))
setTimeout(() => {
console.log(4)
}, 0)
new Promise((resolve) => {
resolve(3)
}).then(ev => console.log(ev))

setTimeout(() => {
new Promise((resolve) => {
resolve(3)
}).then(ev => console.log(ev))
setTimeout(() => {
console.log(6)
}, 0)
new Promise((resolve) => {
resolve(4)
}).then(ev => console.log(ev))
setTimeout(() => {
console.log(5)
}, 0)
}, 3000)
}

大声告诉我以上执行结果!!!
如果你想更快的渲染dom,你会让它在宏任务还是微任务/?


开始做木易杨大神的知识整理

2019/8/31 以前做不完是小狗!! –尼古拉斯.杜
https://github.com/yygmind/blog

Javascript 的执行上下文和执行栈

这篇在半年前看过,过了这么久知识点又模糊了,惭愧呀……


执行上下文: 是代码在解析和执行时所在环境的抽象概念
它分为三种:

  • 全局上下文(window/golbal[node]…)
  • 函数上下文
  • eval (性能问题、安全问题)

执行栈:引擎会在运行的时候,将全局上下文以及函数上下文入栈,等栈顶函数运行完毕,再依次出栈。


执行上下文创建:

  • 创建阶段
  • 执行阶段

创建阶段:

  • this绑定
  • 词法环境被创建
  • 变量环境被创建

执行阶段:

  • 直接执行代码

内存空间

  • 堆就是字典,引用类型存在这里
  • 基本类型存在栈里
1
2
3
4
5
6
7
var a = {n: 1};
var b = a;
a.x = a = {n: 2};

a.x // 这时 a.x 的值是多少
b.x // 这时 b.x 的值是多少
// . 操作符优先级大于 =操作符

end


内存机制

闭包又一定义:A 函数返回了 B 函数,且 B 函数引用了 A 函数内部的变量

两种内存回收的方式:

  • 引用计数 变量被引用就不清除,但如果 a 引用 b b 引用 a,就gg了,造成内存泄漏
  • 标记清除 从全局对象出发,能追溯到的变量就保留,追溯不到的就标记清除
1
2
3
4
5
6
// div 引用了 onclick,并且 回调里又引用了 div
var div = document.createElement("div");
div.onclick = function() {
console.log("click");
console.log(div)
};

node 监测内存泄漏 console.log(process.memoryUsage());

什么姿势内存会泄漏

  • 函数内部未定义的全局变量
  • 未关掉的定时器或回调函数(eg:IE里的click事件)
  • 闭包

一个变量被标记为 null 表示没有引用,则引擎会将它回收,而undfined 会将值置空,但变量还存在
const 的值,保证了引用地址的不可变性,所以 修改 变量的属性没问题,因为地址没变,而基本类型的地址和值是绑定的,so 不能改变


闭包

闭包特性:

  • 可以访问当前函数以外的变量
  • 即使外部函数已返回,还能访问外部函数定义的变量
  • 可以更新外部变量的值

作用域链

这里经常会和原型链混,从字面意思来区分,作用域!就要联想到执行上下文,当访问一个变量时,解析器会从当前作用域找,找不到就去上级找,直到找到,要是找到了全局(window)还没找到,那就要报错了
Uncaught ReferenceError: a is not defined

分析一段闭包代码

首先得搞清楚几个知识点

变量对象

变量对象是一个抽象的概念,在执行上下文 创建阶段,储存着在上下文定义的以下内容:1,函数的形参 2,函数声明 3,变量声明

活动对象

与变量对象对应,当处于 执行阶段 时,变量对象会转为活动对象(全局变量除外)

自己分析去吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 创建全局上下文
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}

var foo = checkscope(); // foo指向函数f
foo();

var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}

checkscope()();

var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}

var foo = checkscope();
foo();

this 绑定

  • apply call bind
  • class内部用 =
  • new Function
  • 箭头函数
    那,手写一个new函数试试,什么?你不会写?复习去吧~
    再写一个硬绑定,函数无论在哪里调用,或者被谁调用,始终绑定初始化对象

浅拷贝和深拷贝

看到这个标题的时候还算淡然,去年毕业的时候不知写过多少次深拷贝,现在写一个应该更简单。

浅拷贝

浅拷贝就只拷贝外面的一层,典型的方法像 Object.assignloadsh.extend... 扩展符

1
2
3
4
5
6
7
function assign(obj) {
let hash = {}
for(let key in obj) {
hash[key] = obj[key]
}
return hash
}

浅拷贝随随便便就撸一个

1
2
3
4
var obj = {a:2,b: {c:3}}
var obj2 = assign(obj)
obj.a = 4
obj.b.c = 3

可以看到,obj 和 obj2 共享 第二层的属性 c,而a作为基本类型,两个对象不共享。

深拷贝

深拷贝则是拷贝所有属性。再撸一个深拷贝?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
{a: 2, b: { c: 3 } }
*/
function deepClone(obj, clone = {}) {
for( let key in obj) {
// 单独对 null 做了判断,因为 typeof null === 'object'
if(typeof obj[key] !== 'object' || obj[key] === null) {
clone[key] = obj[key]
} else {
clone[key] = deepClone(obj[key])
}
}
return clone
}

看来在dxy工作了一年确实有长进,之前写这种函数要“脑梗”很久,现在花了五六分钟就写好了,还一次跑通。

1
2
3
4
5
6
var obj = {a:2,c:{b:3}}
var obj1 = deepClone(obj)
var obj2 = deepClone(obj)
obj1.c.b = 444
// obj1.c.b: 444
// obj2.c.b: 3

没问题,所有属性都不共享了。
这就是tmd 深拷贝,当然项目中经常看到同事省时间这么写: JSON.parse(JSON.stringify(obj))
大多数情况下,客户端代码这么写真心可以,因为数据量小,感受不到性能的差距。在数据量大的情况下,还是寻找一些
开源库去做深拷贝,像:lodash/immutable/jQuery.extend。

我的观点正确吗

以上两节是我“想当然”的写出的,当我看了木易杨的博客之后,发现有很多错误点。分为两点展开。

  • deepClone 完美吗?
  • JSON.parse(JSON.stringify(obj)) 真的可以用在项目里做深拷贝吗?

    JSON 方法

    JSON.parse(JSON.stringify(obj)) 有什么缺点?
    首先我们分析它是怎么实现深拷贝的。
    对象 -> json字符串 -> 对象
    既然是 json 字符串,那肯定有它的标准。一些特殊的对象不能在它里面正确的出现。
    比如:Symbol Function undefined Date
    可以试试
    1
    2
    3
    4
    5
    6
    7
    8
    var obj = {
    sym: Symbol('duhao'),
    date: new Date(),
    b: undefined,
    fn: () => {}
    }
    var obj2 = JSON.parse(JSON.stringify(obj))
    // { date: "2019-08-19T15:57:06.148Z" }

只留下了 date 类型,并且格式和 new Date() 不同,貌似被转换成了 ISO 标准。

所以当你的数据结构 当前或者之后 可能有以上类型时,不推荐使用该方法。
还有一个特殊的表达式:正则表达式。

1
2
3
var obj = {c: /fsf/}
JSON.stringify(obj)
// "{"c":{}}"

可以看到,JSON直接丢掉了我们的正则表达式,取而代之的是一个空对象。

json 结构还有一个缺点,是不能表示循环引用,如果一个对象内部存在循环引用,JSON 会怎么做呢?

1
2
3
4
5
var a = {}
var b = {}
a.b = a
JSON.stringify(a)
// 直接报错 Converting circular structure to JSON 提示循环引用。

既然有这么多雷区,以后还是少用这个方法为妙,说不准哪天就踩上了。

自己写的 deepClone 有什么问题

让我再把这个方法贴过来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
{a: 2, b: { c: 3 } }
*/
function deepClone(obj, clone = {}) {
for( let key in obj) {
// 单独对 null 做了判断,因为 typeof null === 'object'
if(typeof obj[key] !== 'object' || obj[key] === null) {
clone[key] = obj[key]
} else {
clone[key] = deepClone(obj[key])
}
}
return clone
}

我还很“聪明”的对 null 做了一个判断,那用这个方法,再对比下 JSON 做深拷贝,看看能不能通过用例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var obj = {
sym: Symbol('duhao'),
date: new Date(),
b: undefined,
c: null
fn: () => {}
}
var obj2 = deepClone(obj, {})
// obj2
/* { b: undefined
date: {}
fn: () => {}
c: null
sym: Symbol(duhao)
}
*/

可以看到,deepClone 比 json 好了很多,但 Date 类型还是有问题。

再试试循环引用:

1
2
3
4
5
var a = {}
var b = {}
a.b = a
deepClone(a)
// Maximum call stack size exceeded 直接爆栈!因为我们的递归函数一直运行下去了。

正则呢?

1
2
3
deepClone({a: /sfsf/})
// 和JSON表现一致
// {a: {}}

总结一下,存在以下几个问题:

  • Array 对象
  • 函数传参 null
  • 正则表达式
  • 循环引用
  • 数据格式过复杂还是会爆栈(因为是递归)
  • 引用丢失问题
  • Symbol 为key 值如何拷贝?
    我们一个个解决不就可以了。

    完美的deepClone

    兼容 Arraynull

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
{a: 2, b: { c: 3 } }
*/
window.stack = {}
function deepClone(obj, hash = new WeakMap()) {
if(Object.prototype.toString.apply(obj) !== '[object Object]') {
return obj
}
if(hash.has(obj)) {
return hash.get(obj)
}
if(Array.isArray(obj)) {
clone = []
} else {
clone = {}
}
hash.set(obj, clone)
for( let key in obj) {
// 单独对 null 做了判断,因为 typeof null === 'object'
if(Object.prototype.toString.apply(obj[key]) !== '[object Object]') {
clone[key] = obj[key]
} else {
clone[key] = deepClone(obj[key], hash)
}
}
return clone
}
function setMap(key, val) {
stack[key] = val
}

经过以上代码,我们还有两个问题未解决

  • 数据格式过复杂还是会爆栈(因为是递归)
  • Symbol 对象作为key值

Reflect

这个新方法可以用来得到 对象的所有属性,包括 Symbol 对象

1
2
3
4
5
let symbol = Symbol('duhao')
let obj = {'name': 'duhao'}
obj[symbol] = 2
Reflect.ownKeys(obj)
// [Symbol(duhao), 'name']

可以看到,这个方法既可以遍历普通key,也可以遍历Symbol key,我们可以用它来替代 for in,实现我们的深拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
{a: 2, b: { c: 3 } }
*/
function deepClone(obj, hash = new WeakMap()) {
if(Object.prototype.toString.apply(obj) !== '[object Object]') {
return obj
}
if(hash.has(obj)) {
return hash.get(obj)
}
if(Array.isArray(obj)) {
clone = []
} else {
clone = {}
}
hash.set(obj, clone)
Reflect.ownKeys(obj).forEach((key, index) => {
if(Object.prototype.toString.apply(obj[key]) !== '[object Object]') {
clone[key] = obj[key]
} else {
clone[key] = deepClone(obj[key], hash)
}
})
return clone
}

爆栈问题

最终这个问题是比较难遇到的,如果需要拷贝的对象达到了浏览器栈的最大长度,那递归函数就会爆栈。
我们可以通过宏任务 setTimeout 或者 微任务 Promise 来包装上文封装的函数。
也可以将递归改为循环,因为这个情况比较难遇到,就不在此研究了。
???
sfd

文章