achen的个人博客

一个能力有限的前端


  • 首页

  • 归档49

  • 公益 404

  • 搜索

知识点

发表于 2020-03-31 | 更新于 2020-04-12

React实现一个场景,渲染多张图片,保证图片的底部尽量是对齐的,然后滚动条拉到底,在请求图片再次渲染

React 中 key 的用法,常用使用场景

key的概念

react 中的 key 属性是一个特殊的属性,它的出现不是给开发者用的,而是给 react 自身用的。
简单的来说,react 利用 key 来识别组件,他是一种身份标识,就像每个人的身份证一样。每个 key 对应一个组件,相同的 key, react 会认为是同一个组件,这样后续相同的 key 对应的组件都不会被创建。(经测试,16版本之后key重复也会渲染出来?)

key的使用场景

在项目开发中,key属性的使用场景最多的还是由数组动态创建的子组件情况,需要为每个子组件添加唯一的key属性值。那有的人会自然而然想到,key 和动态渲染的子元素获取的index的值很接近,是不是我们可以直接使用index值 赋值给key呢?

1
2
3
4
5
{
data.map((item, index) => (
<div key={index}>item.name</div>
))
}

在尝试后我们发现报错没了,渲染也没问题。但是这里我们强烈不推荐使用数组的index 值来作为key。
如果数据更新仅仅是数组重新排序或在其中间位置插入新元素,那么所有元素都将重新渲染。

例如:
本来index=2 的元素向前移动后,那该元素的key 不也同样发生了改变,那这样改变,key 就没有任何存在的意义了,既然是作为身份证一样的存在,那就不容有失,当然,在你用key值创建子组件的时候,若数组的内容只是作为纯展示,而不涉及到数组的动态变更,其实是可以使用index 作为key的,

key的值必须保证唯一且稳定

我们在与key值打过几次交到以后,感觉key值就类似于数据库中的主键id一样,有且唯一。

React如何处理更新

React 内部 setState 是如何批处理的

setState 的批量更新按照先进先出的原则,顺序更新。

  1. 在 react 的 event handler 内部同步的多次 setState 会被 batch 为一次更新
  2. 在一个异步的事件循环里面多次 setState,react 不会 batch
  3. 可以使用React.unstable_batchedUpdates 来强制 batch

为什么在 setTimeout 中多次 setState,react 不会 batch?

因为 React 的更新是基于 Transaction(事务)的,Transacation 就是给目标执行的函数包裹一下,加上前置和后置的 hook (有点类似 koa 的 middleware),在开始执行之前先执行 initialize hook,结束之后再执行 close hook,这样搭配上 isBatchingUpdates 这样的布尔标志位就可以实现一整个函数调用栈内的多次 setState 全部入 pending 队列,结束后统一 apply 了。

但是 setTimeout 这样的方法执行是脱离了事务的,react 管控不到,所以就没法 batch 了。 React没有控制权的函数 setTimeout

为什么 react 要这么设计?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Parent() {
let [count, setCount] = useState(0);
return (
<div onClick={() => setCount(count + 1)}>
Parent clicked {count} times
<Child />
</div>
);
}

function Child() {
let [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Child clicked {count} times
</button>
);
}

上面这样的 demo,由于点击事件冒泡的缘故,我们假设如果 react 不 batch 立即更新的话,那么点了 child button 之后的逻辑会是如下这样

1
2
3
4
5
6
7
8
9
*** 进入 react click 的事件函数 ***
Child (onClick) 触发点击
- setState 修改 state
- re-render Child 重新渲染 // 😞 不必要的
Parent (onClick) 触发点击(冒泡)
- setState 修改 state
- re-render Parent 重新渲染
- re-render Child 重新渲染 (渲染是自顶向下的,父亲更新会导致儿子更新)
*** 退出 react click 的事件函数 ***

从上面可以看出,第一次子组件的重新渲染完全是浪费的。

所以 React 设计成 setState 不立即触发重新渲染,而是先执行完所有的 event handler,然后用一次重新渲染完成所有更新。

forceUpdate的说明

forceUpdate 从函数名上理解:“强制更新”。

  1. forceUpdate 是同步的吗?“强制”会保证调用然后直接dom-diff吗?

forceUpdate在批量与否的表现上,和setState是一样的。在React有控制权的函数里,是批量的。

  1. “强制”更新整个组件树吗?包括自己,子孙后代组件吗?

forceUpdate只会强制本身组件的更新,即不调用“shouldComponentUpdate”直接更新,对于子孙后代组件还是要调用自己的“shouldComponentUpdate”来决定的。

React15如何去优化

useCallback useMemo的区别

useMemo

1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

把创建函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算memoized值。这种优化有助于避免在每次渲染时都进行高开销的计算。

useCallback

1
2
3
4
const memoizedCallback = useCallback(
() => doSomething(a, b),
[a, b]
);

把内联回调函数及依赖项数组作为参数传入useCallback, 它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数
传递给经过优化的并使用引用相等性去避免非必要的渲染的子组件时,它将非常有用

看起来似乎和useMemo差不多,我们来看看有什么异同:

useMemo 和 useCallback 接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于 useMemo 返回的是函数运行的结果,useCallback返回的函数。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

React Hooks

没有破坏性改动

  • 完全是可选的。
  • 100%向后兼容的。
  • 现在可用,hook 发布于 v16.8.0

没有计划从React移除class。
Hook不会影响你对 React 概念的理解。

动机

Hook 解决了我们五年来编写和维护成千上万的组件时遇到的各种各样看起来不相关的问题。

在组件之间复用状态逻辑很难

你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。

复杂组件变得难以理解

我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

难以理解的 class

除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。大家可以很好地理解 props,state 和自顶向下的数据流,但对 class 却一筹莫展。即便在有经验的 React 开发者之间,对于函数组件与 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景。class 也给目前的工具带来了一些问题。例如,class 不能很好的压缩,并且会使热重载出现不稳定的情况。因此,我们想提供一个使代码更易于优化的 API。

为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。

React Context 如何去使用

  • React.createContext
1
const MyContext = React.createContext(null);
  • Context.Provider
1
<MyContext.Provider value={value}>

每个 Context 组件都会返回一个 Provider React组件,它允许消费组件订阅 context 的变化。

  • Class.contextType
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Myclass extends React.Component {
static contextType = MyContext;

componentDidMount() {
let value = this.context;
}

componentDidUpdate() {
let value = this.context;
}

render() {
let value = this.context;
}
}

挂载在class上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。 这能让你使用
this.context 来消费最近 Context 上的值。你可以在任何生命周期中访问到它,包括render函数中。

  • Context.Consumer
1
2
3
4
5
<MyContext.Consumer>
{
value => /* 基于context的值渲染 */
}
</MyContext.consumer>

Redux 和 Mobx 的区别

  1. 开发难度低,redux 需要引入很多第三方库来完善工程需求。
  2. 开发代码少, redux 需要写大量的样板代码。
  3. 增加渲染性能,redux需要借助 shouldComponentUpdate 或者 immutable 来优化。

在使用Redux中,当修改一个数据,发现组件没有更新,可能的原因有哪些

  1. 是否 key 值重复导致
  2. 是否正确的引入数据, 或者传递props
  3. 是否组件内部忘记connect

React16 函数式编程怎么去优化

  • useMemo
  • useCallback
  • fragments

知识点

发表于 2020-03-30 | 更新于 2020-03-31

进程和线程的区别

CDN

CDN 内容分发网络,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络阻塞,提高用户访问响应效率和命中率。
CDN 的关键技术有内容存储和分发技术。

打印一个树结构的对象

flex布局

js函数的4种调用方式

  1. 作为函数直接调用
  2. 作为对象的方法调用
  3. 使用call、apply动态调用
  4. new命令间接调用

this的指向有哪几种情况?

  1. 作为函数直接调用,非严格模式下,this指向window,严格模式下,this指向undefined
  2. 作为某对象的方法调用,this指向这个对象
  3. 使用apply、call、bind调用,this是第一个传递进去的值
  4. 在构造函数中调用,this指向这个新创建的对象
  5. 箭头函数中没有this,this在箭头函数创建时确定,它与声明所在的上下文相同。

regexp正则,讲讲贪婪模式

正则默认是贪婪模式,默认的贪婪模式会尽可能多的匹配所搜索的字符串。

linux常用命令

面向对象和面向过程的区别,以及他们各自的优缺点

Css实现瀑布流

Css 实现圆形进度条

less sass的区别

链表和数组的区别

js为什么单线程

说说 typescript 的特性,有什么好处

移动端适配怎么做

HTTP----HTTP缓存机制

发表于 2020-03-24 | 更新于 2020-05-06

HTTP—-HTTP缓存机制

缓存的规则

我们知道http的缓存属于客户端缓存,后面会提到为什么属于客户端缓存。所以我们认为浏览器存在一个缓存数据库,用于储存一些不经常变化的静态文件(图片、css、js等)。我们将缓存分为强制缓存和协商缓存。

强制缓存

强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信。

命中强缓存的情况下,返回的 HTTP 状态码为 200 (如下图)。

avatar

协商缓存

又称对比缓存,客户端会先从缓存数据库中获取到一个缓存数据的标识,得到标识后请求服务端验证是否失效,如果没有失效服务端会返回304,此时客户端直接从缓存中获取所以请求的数据,如果标识失效,服务端会返回更新后的数据。

两类缓存机制可以同时存在,强制缓存的优先级高于协商缓存,当执行强制缓存时,如若缓存命中,则直接使用缓存数据库数据,不在进行缓存协商。

缓存的方案

强制缓存

对于强制缓存,服务器响应的header中会用两个字段来表明 —– Expires和Cache-Control

Expires

Expires的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。但由于服务端时间和客户端时间可能有误差,这也就导致缓存命中的误差,另一方面,Expires是HTTP1.0的产物,故现在大多数使用Cache-control替代。

Cache-Control

Cache-control有很多属性,不同的属性代表的意义也不同。
private:客户端可以缓存
public:客户端和代理服务器都可以缓存
max-age:缓存内容将在t秒后失效
no-cache:需要使用协商缓存来验证数据
no-store:所有内容都不会缓存

协商缓存

协商缓存依赖于服务端与浏览器之间的通信。

协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。

如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是 304(如下图)。

avatar

协商缓存的实: 从 Last-midified 到 Etag

Last-Modified 是一个时间戳,如果我们启用了协商缓存,它会在首次请求时随着 Response Headers 返回:

1
Last-Modified: Fri, 27 Oct 2020 06:35:57 GMT

随后我们每次请求时,会带上一个叫 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值:

1
If-Modified-Since: Fri, 27 Oct 2020 06:35:57 GMT

服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值;否则,返回如上图的 304 响应,Response Headers 不会再添加 Last-Modified 字段。

使用 Last-Modified 存在一些弊端,这其中最常见的就是这样两个场景:

  • 我们编辑了文件,但文件的内容没有改变。服务端并不清楚我们是否真正改变了文件,它仍然通过最后编辑时间进行判断。因此这个资源在再次被请求时,会被当做新资源,进而引发一次完整的响应——不该重新请求的时候,也会重新请求。

  • 当我们修改文件的速度过快时(比如花了 100ms 完成了改动),由于 If-Modified-Since 只能检查到以秒为最小计量单位的时间差,所以它是感知不到这个改动的——该重新请求的时候,反而没有重新请求了。

这两个场景其实指向了同一个 bug ——服务器并没有正确感知文件的变化。为了解决这样的问题, Etag 作为 Last-Modified 的补充出现了。

Etag: 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的,反之亦然。因此 Etag 能够精准地感知文件的变化。

Etag 和 Last-Modified 类似,当首次请求时,我们会在响应头里获取到一个最初的标识符字符串,举个🌰,它可以是这样的:

1
ETag: W/"wa3b-1231452"

那么下一次请求时,请求头里就会带上一个值相同的、名为 if-None-Match 的字符串供服务端比对了:

1
If-None-Match: W/"wa3b-1231452"

缓存的优点

  1. 减少了冗余的数据传递,节省宽带流量
  2. 减少了服务器的负担,大大提高了网站性能
  3. 加快了客户端加载网页的速度

不同刷新的请求执行过程

  1. 浏览器地址栏写入URL,回车,浏览器发现缓存中有这个文件了,不用继续请求了,直接去缓存拿。(最快)
  2. F5刷新,别偷懒好歹去服务器看看这个文件是否过期了。于是浏览器就在请求上带上一个if-modify-since。
  3. Ctrl+F5告诉浏览器,你先把你缓存中的这个文件给我删了,然后再去服务器请求个完整的资源文件下来。于是客户端完成了强行更新的操作。

css常见布局

发表于 2020-03-24

双飞燕布局

圣杯布局

1
2
3
4
5
6
7
<header>圣杯布局</header>
<div class="bd">
<div class="main">main</div>
<div class="left">left</div>
<div class="right">right</div>
</div>
<footer>footer</footer>

flex

常见数据结构

发表于 2020-03-19 | 更新于 2020-07-08

时间复杂度

通常使用最差的时间复杂度来衡量一个算法的好坏。
常数时间O(1)代表这个操作和数据量没有关系,是一个固定时间的操作,比如说四则运算。

对于一个算法来说,可能会计算出操作次数为 aN + 1,N代表数据量。那么该算法的时间复杂度就是O(N)。因为我们在计算时间复杂度的时候,数据量通常是非常大的,这时候低阶项和常数项可以忽略不计。

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)

栈

概念

栈是一个线性结构,在计算机中是一个相当常见的数据结构。
栈的特点是只能在某一端添加或删除数据,遵循先进后出的原则。

实现

每种数据结构都可以用很多种方式来实现,其实可以把栈当做一个数组的子集,所以这里使用数组来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Stack {
constructor() {
this.stack = [];
}
push(item) {
this.stack.push(item);
}
pop() {
this.stack.pop();
}
getCount() {
return this.stack.length;
}
peek() {
return this.stack[this.getCount - 1];
}
isEmpty() {
return this.getCount() === 0;
}
}

应用

匹配括号,可以通过栈的特性来完成

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
var isValid = function(s) {
let map = {
'(': -1,
')': 1,
'[': -2,
']': 2,
'{': -3,
'}': 3,
};
let stack = [];

for (let i of s) {
if (map[i] < 0) {
stack.push(i);
} else {
let last = stack.pop();
if (map[last] + map[i] !== 0) {
return false;
}
}
}

if (stack.length > 0) {
return false;
}

return true;
}

队列

概念

队列是一个线性结构,特点是在某一端添加数据,在另一端删除数据,遵循先进先出的原则。

实现

单链队列和循环队列

单链队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Queue {
constructor() {
this.queue = [];
}
enQueue(item) {
this.queue.push(item);
}
deQueue() {
return this.queue.shift();
}
getHeader() {
return this.queue[0];
}
getLength() {
return this.queue.length;
}
isEmpty() {
return this.queue.length === 0;
}
}

循环队列

链表

总结

发表于 2020-03-17 | 更新于 2020-03-20

如何实现一个babel插件

Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。

抽象语法树(AST)

Babel 的处理步骤

Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),生成(generate)。


如何实现一个webpack-loader


如何实现一个webpack-plugins


如何实现一个promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
const PENDING = 'PENDING';
const RESOLVED = 'RESOLVED';
const REJECTED = 'REJECTED';

function MyPromise(fn) {
const that = this;

that.value = null;
that.state = PENDING;

that.resolvedCallbacks = [];
that.rejectedCallbacks = [];

function resolve(value) {
if (value instanceof MyPromise) {
return value.then(resolve, reject);
}

if (that.state === PENDING) {
that.state = RESOLVED;
that.value = value;
that.resolvedCallbacks.map(cb => cb(that.value));
}
}

function reject(value) {
if (that.state === PENDING) {
that.state = REJECTED;
that.value = value;
that.rejectedCallbacks.map(cb => cb(that.value));
}
}

try {
fn(resolve, reject);
} catch(e) {
reject(e);
}
}

MyPromise.prototype.then = function(onFulfilled, onRejected) {
const that = this;
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : r => {
throw r
};

if (that.state === PENDING) {
that.resolvedCallbacks.push(onFulfilled);
that.rejectedCallbacks.push(onRejected);
}
if (that.state === RESOLVED) {
onFulfilled(that.value);
}
if (that.state === REJECTED) {
onRejected(that.value);
}

return that;
}

new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(111);
}, 0);
}).then(value => {
console.log(value);
}).then(value => {
console.log(value);
})

node调试,错误监控


http和https区别

https就是http和TCP之间有一层SSL层,这一层的实际作用是防止钓鱼和加密。防止钓鱼通过网站的证书,网站必须有CA证书,证书类似于一个解密的签名。另外是加密,加密需要一个密钥交换算法,双方通过交换后的密钥加解密。

  • https协议需要到ca申请证书,一般免费证书很少,需要交费。
  • http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议。
  • http和https使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443。
  • http的连接很简单,是无状态的。
  • HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。
  • 百度和谷歌两大搜索引擎都已经明确表示,HTTPS网站将会作为搜索排名的一个重要权重指标。也就是说HTTPS网站比起HTTP网站在搜索排名中更有优势。

react,vue生命周期

vue

  • brforeCreate
  • create
  • beforeMountd
  • mounted
  • beforeUpdate
  • updated
  • activated
  • deactivated
  • beforeDestroy
  • destroy
  • errorCaptured

react

calss编程生命周期

初始阶段
  • constructor
挂载阶段
  • componentWillMount
  • render
  • componentDidMount
更新阶段
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate
卸载阶段
  • componentWillUnmount

到了React v16.3,大改动来了,引入了两个新的生命周期函数: getDerivedStateFromProps,getSnapshotBeforeUpdate

static getDerivedStateFromProps(props, state) 在组件创建时和更新时的render方法之前调用,它应该返回一个对象来更新状态,或者返回null来不更新任何内容。

getSnapshotBeforeUpdate() 被调用于render之后,可以读取但无法使用DOM的时候。它使您的组件可以在可能更改之前从DOM捕获一些信息(例如滚动位置)。此生命周期返回的任何值都将作为参数传递给componentDidUpdate()。

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
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}

getSnapshotBeforeUpdate(prevProps, prevState) {
//我们是否要添加新的 items 到列表?
// 捕捉滚动位置,以便我们可以稍后调整滚动.
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}

componentDidUpdate(prevProps, prevState, snapshot) {
//如果我们有snapshot值, 我们已经添加了 新的items.
// 调整滚动以至于这些新的items 不会将旧items推出视图。
// (这边的snapshot是 getSnapshotBeforeUpdate方法的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}

render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}

redux和rematch


webpack性能优化

减少webpack打包时间
  • 优化 Loader

对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,我们是有办法优化的。

首先我们可以优化 Loader 的文件搜索范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
module: {
rules: [
{
// js 文件才使用 babel
test: /\.js$/,
loader: 'babel-loader',
// 只在 src 文件夹下查找
include: [resolve('src')],
// 不会去查找的路径
exclude: /node_modules/
}
]
}
}

对于 Babel 来说,我们肯定是希望只作用在 JS 代码上的,然后 node_modules 中使用的代码都是编译过的,所以我们也完全没有必要再去处理一遍。

当然这样做还不够,我们还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间

1
loader: 'babel-loader?cacheDirectory=true'
  • HappyPack

受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。
HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module: {
loaders: [
{
test: /\.js$/,
include: [resolve('src')],
exclude: /node_modules/,
// id 后面的内容对应下面
loader: 'happypack/loader?id=happybabel'
}
]
},
plugins: [
new HappyPack({
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory'],
// 开启 4 个线程
threads: 4
})
]
  • DllPlugin

DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。

减少webpack打包后的体积
  • 按需加载

想必大家在开发 SPA 项目的时候,项目中都会存在十几甚至更多的路由页面。如果我们将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,我们肯定是希望首页能加载的文件体积越小越好,这时候我们就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 loadash 这种大型类库同样可以使用这个功能。


监控

前端监控一般分为三种,分别为页面埋点、性能监控以及异常监控。

页面埋点

页面埋点应该是大家最常写的监控了,一般起码会监控以下几个数据:

  • PV/UV
  • 停留时长
  • 流量来源
  • 用户交互

对于这几类统计,一般的实现思路大致可以分为两种,分别为手写埋点和无埋点的方式。

相信第一种方式也是大家最常用的方式,可以自主选择需要监控的数据然后在相应的地方写入代码。这种方式的灵活性很大,但是唯一的缺点就是工作量较大,每个需要监控的地方都得插入代码。

另一种无埋点的方式基本不需要开发者手写埋点了,而是统计所有的事件并且定时上报。这种方式虽然没有前一种方式繁琐了,但是因为统计的是所有事件,所以还需要后期过滤出需要的数据。

性能监控

性能监控可以很好的帮助开发者了解在各种真实环境下,页面的性能情况是如何的。

对于性能监控来说,我们可以直接使用浏览器自带的 Performance API 来实现这个功能。

对于性能监控来说,其实我们只需要调用 performance.getEntriesByType(‘navigation’) 这行代码就行了。对,你没看错,一行代码我们就可以获得页面中各种详细的性能相关信息。

异常监控

对于异常监控来说,以下两种监控是必不可少的,分别是代码报错以及接口异常上报

对于代码运行错误,通常的办法是使用window.onerror拦截报错。该方法能拦截到大部分的详细报错信息,但是也有例外。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @param {String} msg 错误信息
* @param {String} url 出错文件
* @param {Number} row 行号
* @param {Number} col 列号
* @param {Object} error 错误详细信息
*/
window.onerror = function (msg, url, row, col, error) {
console.log({
msg, url, row, col, error
})
return true; // 注意,在返回 true 的时候,异常才不会继续向上抛出error;
};
  • 对于跨域的代码运行错误会显示Script error。对于这种情况我们需要给script标签添加crossorigin属性
  • 对于某些浏览器可能不会显示调用栈信息,这种情况通过arguments.callee.caller来做栈递归。

对于异步代码来说,可以使用catch的方式捕获错误。比如Promise可以直接使用catch函数,await async可以使用try catch。

但是要注意线上运行的代码都是压缩过的,需要在打包时生成sourceMap文件便于debug。

对于捕获的错误需要上传给服务器**

可以通过ajax发送数据
还可以通过img标签的src发起一个请求。

1
2
3
4
function report(error) {
var report = 'https://xxx/report';
new Image().src = report + 'error=' + error;
}

另外接口异常就相对来说简单了,可以列举出出错的状态码。一旦出现此类的状态码就可以立即上报出错。接口异常上报可以让开发人员迅速知道有哪些接口出现了大面积的报错,以便迅速修复问题。

普通函数跟箭头函数的区别

  • 语法更加简洁、清晰
  • 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this
1
2
3
4
5
6
7
8
9
10
11
12
13
var id = 'GLOBAL';
var obj = {
id: 'OBJ',
a: function(){
console.log(this.id);
},
b: () => {
console.log(this.id);
}
};

obj.a(); // 'OBJ'
obj.b(); // 'GLOBAL'
  • 箭头函数继承而来的this指向永远不变

  • call/apply/bind无法改变箭头函数中this的指向

.call()/.apply()/.bind()方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向,虽然这么做代码不会报错。

1
2
3
4
5
6
7
8
9
10
11
var id = 'Global';
// 箭头函数定义在全局作用域
let fun1 = () => {
console.log(this.id)
};

fun1(); // 'Global'
// this的指向不会改变,永远指向Window对象
fun1.call({id: 'Obj'}); // 'Global'
fun1.apply({id: 'Obj'}); // 'Global'
fun1.bind({id: 'Obj'})(); // 'Global'
  • 箭头函数不能作为构造函数使用

因为箭头函数没有自己的this,它的this其实是继承了外层执行环境中的this,且this指向永远不会随在哪里调用、被谁调用而改变,所以箭头函数不能作为构造函数使用,或者说构造函数不能定义成箭头函数,否则用new调用时会报错!

1
2
3
4
5
6
7
let Fun = (name, age) => {
this.name = name;
this.age = age;
};

// 报错
let p = new Fun('cao', 24);
  • 箭头函数没有自己的arguments,在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 例子一
let fun = (val) => {
console.log(val); // 111
// 下面一行会报错
// Uncaught ReferenceError: arguments is not defined
// 因为外层全局环境没有arguments对象
console.log(arguments);
};
fun(111);

// 例子二
function outer(val1, val2) {
let argOut = arguments;
console.log(argOut); // [111, 222]
let fun = () => {
let argIn = arguments;
console.log(argIn); // // [111, 222]
console.log(argOut === argIn); // true
};
fun();
}
outer(111, 222);
  • 箭头函数没有原型prototype
1
2
3
4
let sayHi = () => {
console.log('Hello World !')
};
console.log(sayHi.prototype); // undefined

手写一个webpack

  • 手写一个webpack

js实现深拷贝

发表于 2020-03-15 | 更新于 2020-03-16

深浅拷贝

浅拷贝

  1. Object.assign
  2. 展开运算符…
  3. Array.slice();

首先可以通过 Object.assign 来解决这个问题,很多人认为这个函数是用来深拷贝的。其实并不是,Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。

1
2
3
4
5
6
let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

另外我们还可以通过展开运算符 … 来实现浅拷贝

1
2
3
4
5
6
let a = {
age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就可能需要使用到深拷贝了

1
2
3
4
5
6
7
8
9
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到最开始的话题了,两者享有相同的地址。要解决这个问题,我们就得使用深拷贝了。

深拷贝

这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。

1
2
3
4
5
6
7
8
9
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是该方法也是有局限性的:

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

如果你有这么一个循环引用对象,你会发现并不能通过该方法实现深拷贝
在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

1
2
3
4
5
6
7
8
let a = {
age: undefined,
sex: Symbol('male'),
jobs: function() {},
name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}

你会发现在上述情况中,该方法会忽略掉函数和 undefined 。

但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题。

如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel

当然你可能想自己来实现一个深拷贝,但是其实实现一个深拷贝是很困难的,需要我们考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等等,所以这里我们实现的深拷贝只是简易版,并且我其实更推荐使用 lodash 的深拷贝函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepClone(obj) {
function isObject(o) {
return (typeof o === 'object' || typeof o === 'function') && o !== null;
}

if (!isObject(obj)) {
throw new Error('非对象');
}

let isArray = Array.isArray(obj);
let newObj = isArray ? [...obj] : {...obj};

Reflect.hasOwn(newObj).forEach(key => {
newObj[key] = isObject(obj[key]) ? deepClone(newObj[key]) : obj[key];
})

return newObj;
}

前端安全问题

发表于 2020-03-15 | 更新于 2020-03-25

XSS

XSS 简单点来说,就是攻击者想尽一切办法将可以执行的代码注入到网页中。
XSS 可以分为多种类型,但是总体上我认为分为两类:持久型和非持久型。

持久型也就是攻击的代码被服务端写入进数据库中,这种攻击危害性很大,因为如果网站访问量很大的话,就会导致大量正常访问页面的用户都受到攻击。

举个例子,对于评论功能来说,就得防范持久型 XSS 攻击,因为我可以在评论中输入以下内容

1
<script>alert(1);</script>

非持久型相比于前者危害就小的多了,一般通过修改 URL 参数的方式加入攻击代码,诱导用户访问链接从而进行攻击。

举个例子,如果页面需要从 URL 中获取某些参数作为内容的话,不经过过滤就会导致攻击代码被执行

1
2
<!-- http://www.domain.com?name=<script>alert(1)</script> -->
<div>{{name}}</div>

但是对于这种攻击方式来说,如果用户使用 Chrome 这类浏览器的话,浏览器就能自动帮助用户防御攻击。但是我们不能因此就不防御此类攻击了,因为我不能确保用户都使用了该类浏览器。

对于 XSS 攻击来说,通常有两种方式可以用来防御。

转义字符

首先,对于用户的输入应该是永远不信任的。最普遍的做法就是转义输入输出的内容,对于引号、尖括号、斜杠进行转义

1
2
3
4
5
6
7
8
9
10
function escape(str) {
str = str.replace(/&/g, '&amp;')
str = str.replace(/</g, '&lt;')
str = str.replace(/>/g, '&gt;')
str = str.replace(/"/g, '&quto;')
str = str.replace(/'/g, '&#39;')
str = str.replace(/`/g, '&#96;')
str = str.replace(/\//g, '&#x2F;')
return str
}
CSP

CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。

通常可以通过两种方式来开启 CSP:

  1. 设置 HTTP Header 中的 Content-Security-Policy
  2. 设置 meta 标签的方式

CSRF

CSRF 中文名为跨站请求伪造。原理就是攻击者构造出一个后端请求地址,诱导用户点击或者通过某些途径自动发起请求。如果用户是在登录状态下的话,后端就以为是用户在操作,从而进行相应的逻辑。

举个例子,假设网站中有一个通过 GET 请求提交用户评论的接口,那么攻击者就可以在钓鱼网站中加入一个图片,图片的地址就是评论接口

1
<img src="http://www.domain.com/xxx?comment='attack'"/>

那么你是否会想到使用 POST 方式提交请求是不是就没有这个问题了呢?其实并不是,使用这种方式也不是百分百安全的,攻击者同样可以诱导用户进入某个页面,在页面中通过表单提交 POST 请求。

如何防御

防范 CSRF 攻击可以遵循以下几种规则:

  1. Get 请求不对数据进行修改
  2. 不让第三方网站访问到用户 Cookie
  3. 阻止第三方网站请求接口
  4. 请求时附带验证信息,比如验证码或者 Token
SameSite

可以对 Cookie 设置 SameSite 属性。该属性表示 Cookie 不随着跨域请求发送,可以很大程度减少 CSRF 的攻击,但是该属性目前并不是所有浏览器都兼容。

验证 Referer

对于需要防范 CSRF 的请求,我们可以通过验证 Referer 来判断该请求是否为第三方网站发起的。

Token

服务器下发一个随机 Token,每次发起请求时将 Token 携带上,服务器验证 Token 是否有效。


点击劫持

点击劫持是一种视觉欺骗的攻击手段。攻击者将需要攻击的网站通过 iframe 嵌套的方式嵌入自己的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。

对于这种攻击方式,推荐防御的方法有两种。

X-FRAME-OPTIONS

X-FRAME-OPTIONS 是一个 HTTP 响应头,在现代浏览器有一个很好的支持。这个 HTTP 响应头 就是为了防御用 iframe 嵌套的点击劫持攻击。

该响应头有三个值可选,分别是

  • DENY,表示页面不允许通过 iframe 的方式展示
  • SAMEORIGIN,表示页面可以在相同域名下通过 iframe 的方式展示
  • ALLOW-FROM,表示页面可以在指定来源的 iframe 中展示
JS 防御

对于某些远古浏览器来说,并不能支持上面的这种方式,那我们只有通过 JS 的方式来防御点击劫持了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<head>
<style id="click-jack">
html {
display: none !important;
}
</style>
</head>
<body>
<script>
if (self == top) {
var style = document.getElementById('click-jack')
document.body.removeChild(style)
} else {
top.location = self.location
}
</script>
</body>

以上代码的作用就是当通过 iframe 的方式加载页面时,攻击者的网页直接不显示所有内容了。

中间人攻击

中间人攻击是攻击方同时与服务端和客户端建立起了连接,并让对方认为连接是安全的,但是实际上整个通信过程都被攻击者控制了。攻击者不仅能获得双方的通信信息,还能修改通信信息。

通常来说不建议使用公共的 Wi-Fi,因为很可能就会发生中间人攻击的情况。如果你在通信的过程中涉及到了某些敏感信息,就完全暴露给攻击方了。

当然防御中间人攻击其实并不难,只需要增加一个安全通道来传输信息。HTTPS 就可以用来防御中间人攻击,但是并不是说使用了 HTTPS 就可以高枕无忧了,因为如果你没有完全关闭 HTTP 访问的话,攻击方可以通过某些方式将 HTTPS 降级为 HTTP 从而实现中间人攻击。

JS数据类型

发表于 2020-03-13 | 更新于 2020-03-15

原始(Primitive)类型

在JS中,存在6种原始值,分别是:

  • number
  • string
  • boolean
  • null
  • undefined
  • symbol
  • bigInt

首先原始类型存储的都是值,是没有函数可以调用的,比如undefined.toString()

此时你肯定会有疑问,这不对啊,明明’1’.toString()是可以使用的。其实在这种情况下,’1’已经不是原始类型了,而是被强制转换成了String类型也就是对象类型,所以可以调用tostring函数。

另外对于null来说,很多人会认为它是个对象类型,其实这是错的。虽然typeof null会输出object,但是这是JS存在的一个历史bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。

对象类型

在JS中,除了原始类型就是对象类型了。对象类型和原始类型的不同的是,原始类型存储的是值,对象类型存储的地址(指针)。当你创建了一个对象类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。

类型转换

首先我们要知道,在 JS 中类型转换只有三种情况,分别是:

  • 转换为布尔值
  • 转换为数字
  • 转换为字符串

转Boolean

在条件判断时,除了 undefined, null, false, NaN, ‘’, 0, -0,其他所有值都转为 true,包括所有对象。

对象转原始类型

对象在转换类型的时候,会调用内置的 [[ToPrimitive]] 函数,对于该函数来说,算法逻辑一般来说如下:

  • 如果是原始类型,那就不需要转换
  • 如果需要转字符串类型那就调用x.toString(),转换为基础类型的话就会返回转换的值。不是字符串类型的话就先调用valueOf,如果不是基础类型的话在调用toString。
  • 调用x.valueOf(),如果转换为基础类型,就返回基础类型。
  • 如果都没有返回原始类型,就会报错。

当然你也可以重写 Symbol.toPrimitive,该方法在转原始类型时调用优先级最高。

1
2
3
4
5
6
7
8
9
10
11
12
let a = {
valueOf() {
return 0;
},
toString() {
return '1';
}
[Symbol.toPrimitive]() {
return 2;
}
}
console.log(1 + a); // 3
  • 如何实现 a == 1 && a == 2 && a == 3返回true
1
2
3
4
5
6
7
var a = {
value: 1,
toString() {
return a.value++
}
}
a == 1 && a == 2 && a == 3

比较运算符

  • 如果是对象,就通过 toPrimitive 转成原始类型
  • 如果是字符串,就通过 unicode 字符索引来比较
1
2
3
4
5
6
7
8
9
let a = {
valueOf() {
return 0;
},
toString() {
return '1'
}
}
a > -1; // true

TCP

发表于 2020-03-11 | 更新于 2020-07-08

TCP

传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP 基本是和 UDP 反着来,建立连接断开连接都需要先需要进行握手。在传输数据的过程中,通过各种算法保证数据的可靠性,当然带来的问题就是相比 UDP 来说不那么的高效。

三次握手

首先假设主动发起请求的一端称为客户端,被动连接的一端称为服务端。不管是客户端还是服务端,TCP 连接建立完后都能发送和接收数据,所以 TCP 是一个全双工的协议。
起初,两端都为 CLOSED 状态。在通信开始前,双方都会创建 TCB。 服务器创建完 TCB 后便进入 LISTEN 状态,此时开始等待客户端发送数据。

  • 第一次握手

客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。

  • 第二次握手

服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。

  • 第三次握手

当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。

三次握手流程图

三次握手主要是为了规避因网络延迟导致一些服务器开销的问题。

四次挥手

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。

第一次: 当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求 ;

第二次: 主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1;

第三次: 由B 端再提出反方向的关闭请求,将FIN置1 ;

第四次: 主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束。

名词解释

1、ACK 是TCP报头的控制位之一,对数据进行确认。确认由目的端发出, 用它来告诉发送端这个序列号之前的数据段都收到了。 比如确认号为X,则表示前X-1个数据段都收到了,只有当ACK=1时,确认号才有效,当ACK=0时,确认号无效,这时会要求重传数据,保证数据的完整性。

2、SYN 同步序列号,TCP建立连接时将这个位置1。

3、FIN 发送端完成发送任务位,当TCP完成数据传输需要断开时, 提出断开连接的一方将这位置1。

小结TCP与UDP的区别:

1.(基于连接vs无连接)tcp是面向连接的(三次握手;四次挥手);udp不是面向连接的
2.(重量级vs轻量级)tcp是一个重量级的协议;udp则是轻量级的协议。一个tcp数据报的报头大小最少20字节,udp数据报的报头固定8个字节
3.(可靠性)tcp交付保证:如果消息在传输中丢失,那么它将重发;udp没有交付保证,一个数据包在运输过程中可能丢失。
4.(有序性)消息到达网络的另一端可能是无序的,tcp协议将为你拍好序。Udp不提供任何有序性的保证。
5.(速度)tcp慢,适合传输大量数据;udp快,适合传输少量数据。
6.(流量控制和拥塞控制)TCP有流量控制和拥塞控制,udp没有。

  1. tcp面向字节流,udp面向报文
  2. tcp只能单播,不能发送广播和组播;udp可以广播和组播。

流量控制和拥塞控制:

  • 流量控制:就是让发送方发送速率不要太快,要让接收方来的及接收。

  • 拥塞控制:防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提:网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机、路由器,以及与降低网络传输性能有关的所有因素。

123…5
achen

achen

日常复制粘贴,问啥啥不会

49 日志
12 标签
© 2021 achen
由 Hexo 强力驱动 v3.7.1
|
主题 – NexT.Pisces v6.4.0