再拾docker

记录docker的基本概念和用法,系统的再认识一次docker

docker是什么

  • Docker是对Linux容器的一种封装,并提供简单易用的接口,同时它也是目前最流行的Linux容器解决方案。

  • 在docker出现之前,环境移植一直是非常麻烦的事情,当我们需要把项目从一台机器迁移到另一台机器时,不仅仅是迁移源代码,更重要的是要保证环境的一致性,而一次次环境的搭建则显得冗余且效率极低,所以在思考有没有一种方式能够把环境一起进行迁移。

  • 虚拟机的出现在一定程度上解决了这个问题,但接踵而至的问题是:

    1. 虚拟机占用资源多,它本质就是一台完整的操作系统,
    2. 冗余步骤多,由于是完整的操作系统,一些系统级别的操作步骤不可避免。
    3. 启动慢。
  • 所以Linux发展出了另一种虚拟化技术:Linux容器。而docker则是Linux容器的一种解决方案(也是最流行的)

  • 可以简单的将docker理解为一种非常轻量级的虚拟机

docker镜像(image文件)

  • docker把应用程序及其依赖打包在image文件里,通过image文件,就会生成容器(后续会提到),即镜像文件其实就是容器的模版,而容器则是镜像文件的实例(可以一一对应类与对象
  • image文件是二进制文件。在实际开发中,我们会在别人写好的image文件上加一些自定义设置从而形成我们需要的image文件,而不是自己从零开发一个image文件(当然,有需求另说)。
1
2
3
# 有关image命令
docker image ls # 列出docker镜像
docker image rm [imageName] # 删除某镜像文件

容器文件

  • 由image生成的容器实例,本身也是一个文件,称为容器文件。同时,当关闭容器时不会删除容器文件,只是让容器停止运行而已。
1
2
docker container ls # 本机正在运行的容器
docker container ls --all # 本机所有容器
  • 每个容器都有一个唯一ID,如果需要终结容器的运行,需要使用kill命令docker container kill
  • 如需要删除容器文件,需要使用rm命令。
1
docker container rm [containerID]

go-learning-1

Golang学习记录

关于swtich

  • Golang中的switch的每个case自带break关键字,即不用手动去设置break关键字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import (
"fmt"
)

func main(){
variables := 12
switch variables {
case 24:
fmt.Println('24')
case 12:
fmt.Println('12')
case 36:
fmt.Println('36')
default:
fmt.Println('114514')
}
}

// 12
  • 如果在其他语言中这样编写switch代码块,36和default代码块中的输出也会执行。

fallthrough关键字

  • 当然,golang也提供了能够无视掉默认break的关键字,即fallthrough,在指定的case最后一行加上fallthrough,则对应的case代码块将会忽视掉默认的break操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 接上
func main(){
variables := 12
switch variables {
case 24:
fmt.Println('24')
case 12:
fmt.Println('12')
fallthrough
case 36:
fmt.Println('36')
default:
fmt.Println('114514')
}
}

// 12
// 36

关于数组

  • 在golang中的数组和c一样,一旦定义了大小就不可更改,且声明方式有多种,如下:
1
2
3
4
5
6
7
8
9
// var variable_name [SIZE] variable_type
func main(){
var balance = [3]float32{100, 2.21, 4.12} // {}中的数字个数不能大于[]中定义的数组大小
var c = [5]int{1,2,3}
d := [...] int{1,2,3,4} // 不显示设置数组大小,而是根据{}中定义的数字个数来确定
e := [...] int{4: 100}// [0, 0, 0, 0, 100] -> 这w里的4: 100 表示数组中第五号元素为100,其余位置补0
f := [...] int{1: 12, 4: 12, 9: 12} // [0 12 0 0 12 0 0 0 0 12] -> 也可以指定多个下标元素
}

  • 和js不一样的是,如果需要获得数组的长度,需要调用len方法,并把对应数组当作参数传入。
1
len(array) // array的长度

值类型

  • 在golang中,数组并不是引用类型的变量,而是值类型的变量,即给新变量赋值数组时,新变量接收到的不是指针,而是数组本身的副本copy,新数组和旧数组内部元素的更改不会互相影响。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
a := [...]string{"USA", "China", "India", "Germany", "France"}
b := a // a copy of a is assigned to b
b[0] = "Singapore"
fmt.Println("a is ", a)
fmt.Println("b is ", b)
}
// a is [USA China India Germany France]
// b is [Singapore China India Germany France]
// 可以看到a数组内部的元素并没有同步更新

Slice 切片

  • Slice是对Go语言中数组的抽象。由于数组不可改变,特定场景中这样的集合变量就不太实用,于是slice便出现了,它也可以理解为“动态数组”可以追加元素。
  • 需要注意的是,Slice只是对现有数组的引用,它本身没有任何数据,Slice上的改变都会影响到底层数组内值的更改。
  • 从概念上来说,Slice就像一个结构体,它包含三个元素:
  1. 指针,指向数组中slice指定的开始位置(本质是对数组的引用)
  2. 长度,slice的长度。
  3. 最大长度,slice的开始位置到数组的最后位置的长度

Slice定义语法

1
2
// 声明语法
var identifier []type
  • 切片不需要说明长度,也可以使用make函数来进行切片创建。
1
2
3
4
var slice []type = make([]type, len)
slice := make([]type, len)
// make创建的范式如下
make([]T, length, capacity)
  • 如果要进行初始化,可以如下
1
2
3
s := []int {1, 2, 3} // 注意, []中没有三个点,如果有三个点,表明是数组的初始化。
s := array[startIndex: endIndex] // 通过数组的切片来获得,此时s表示数组array的指针。

修改切片

  • 由于silce只是底层数组的引用,所以slice上的修改会同步反映到底层数组中
1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {  
darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
dslice := darr[2:5]
fmt.Println("array before",darr)
for i := range dslice {
dslice[i]++
}
fmt.Println("array after",darr)
}

// array before [57 89 90 82 100 78 67 69 59]
// array after [57 89 91 83 101 78 67 69 59]
// 可以看到,第三个元素到第五个元素都自增了一次

len() 和 cap()

  • len方法和array的用法一样,用来获得切片此时的长度,而cap方法则是获取切片的最大长度。
1
2
3
4
5
6
7
8
func main() {
a := []string{"USA", "China", "India", "Germany", "France"}
d := a[1:2]
fmt.Println(len(d), cap(d))
}

// 1, 4
// cap(d) = 4 -> 从切片开始的位置(第二个元素)到数组最后一个元素位置有4个

append() 和 copy()

  • append是在切片后增加元素,copy则是获取切片的拷贝(注意,这里的拷贝是深拷贝,即底层的数组也会跟着一起拷贝,两个切片之间不会建立联系)
  • append增加元素也会影响到底层数组,但如果增加元素后,切片已经超过最大长度,则golang会重新在底层创建一个适合于新切片的最大长度的数组,并将对应的slice指向新的数组上,此时便不会影响到旧的底层数组,示例代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
a := []string{"USA", "China", "India", "Germany", "France"}
b := a[1:3]
b = append(b, "Jap")
d := a[1:5]
d = append(d, "smart")
fmt.Println("切片a: ", a)
fmt.Println("切片b: ", b)
fmt.Println("切片d: ", d)
}
/*
切片a: [USA China India Jap France]
切片b: [China India Jap]
切片d: [China India Jap France smart]
可以看到,切片b的增加影响到了切片a,但是切片d的增加却对a没有任何影响。
*/

待解决问题: interface变量是啥

0120-浏览器事件

Javascript和HTML的交互都是通过事件来实现的,而事件的产生与执行则是遵循着传统软件工程领域中的观察者模式,其能够做到页面行为页面展示的解耦合。

本节内容会从事件流谈起,然后逐一介绍几种注册事件的方式以及他们其中的一些细节,最后还会提到关于事件委托(代理)的概念。

事件流

  • 事件流描述了页面接受事件的顺序。因为一个事件的触发可能会影响好几处地方,这很容易理解,比如在页面上嵌套着写了几个div元素,同时在最里层的div元素上进行事件触发,那这不仅仅是最内层的div对事件进行响应,任意一层的嵌套的div都会对事件进行相应处理。(其实通过上述的观察者模式也可以推断出其合理性,因为一个对象可以由多个观察者进行观察)
  • 所以多个监听事件的元素响应顺序需要进行统一,因为一些历史原因,事件的响应顺序有两种模式:冒泡和捕获

事件冒泡

  • 顾名思义,事件的冒泡就如水底下的气泡一下,从内到外,同理,事件冒泡规定的事件流顺序也是从内而外,事件会从最深层的节点开始触发,然后向外传播到document(文档)。
  • 代码如下所示。
1
2
3
4
5
6
7
<html>
<body>
<div id="ddd">
click me
</div>
</body>
</html>
  • 此时我如果对id=ddd的div元素进行click事件触发,那么该事件会以如下顺序发生:
  1. div
  2. body
  3. html
  4. document
  • 现代的浏览器的事件会一直冒泡到window对象。

事件捕获

  • 事件捕获则和事件冒泡相反,事件的响应顺序是从外到内的,还是以上一个例子为例,那么对应click事件讲会以如下顺序发生:
  1. document
  2. html
  3. body
  4. div

DOM事件流

  • DOM2 Events规范里规定里事件流为分三个部分:事件捕获,到达目标和事件冒泡

事件流

  • 需要注意的是,div元素(即直接触发元素)是不会响应捕获事件的,因为通常认为直接触发事件是冒泡阶段发生的,所以它也是冒泡阶段第一个发生的事件
  • 但现在大多数支持DOM事件流的浏览器都实现了一个小小的拓展,即在捕获阶段在事件目标上触发事件。最终结果表现为有两个机会来处理事件

事件处理程序

  • 事件意味着用户或浏览器执行的某种动作,而为响应事件而调用的函数被称为事件处理程序(事件监听器)

HTML事件处理程序

  • HTML事件处理程序是以HTML属性的形式来进行指定的。该属性的值必须是能够执行的javascript代码。
  • 比如下面这个例子,就是按钮在被点击时执行一段代码。
1
2
3
<button onclick="console.log('click')">
click me
</button>
  • 当然也可以以函数的形式来进行响应事件定义。
1
2
3
4
5
6
7
8
<script>
function click(event){
console.log('click')
}
</script>
<button onclick="click(event)">
click me
</button>
  • 可以看到,除了把函数单独拎出来以外,还多了一个event对象,这个是一个特殊的局部变量,它定义了事件触发的一些属性以及被触发元素的一些属性。
  • 除此之外,HTML事件处理程序中的this就是DOM元素本身,所以可以直接使用this对象去获取对应元素上的属性。
1
<input type="button" value="Click Me" onclick="console.log(this.value)">
  • 这里还有个比较有趣的地方,获取元素属性时可以直接省略掉this,直接使用value,即下面写法也能达到同样的效果。
1
<input type="button" value="Click Me" onclick="console.log(value)">
  • 因为这个包装函数在创建时其作用域链with操作符给延长了,所以document和元素自身的成员都可以被当成局部变量来使用。
1
2
3
4
5
6
7
function() { 
with(document) {
with(this) {
// 属性值
}
}
}
  • 但最好不要这样做,因为不仅仅会显得很诡异,而且在后期调试时也会造成误解。

HTML事件处理程序一个比较大的问题是:它把HTML和Javascript在代码上进行了强耦合(在逻辑上依然是分开的),如果我们需要更改响应程序,那么两处都需要进行修改。

所以更多的时候,我们使用的是Javascript去指定事件处理程序而不是HTML。

DOM0 事件处理程序

  • 这是Javascript指定事件处理程序的传统方式,在Javascript中对DOM元素的属性进行赋值从而达到事件监听的效果。
1
2
3
4
5
let btn = document.getElementById("myBtn"); 
// 如果在下面的事件赋值之前进行事件点击,是没有任何响应的。
btn.onclick = function() {
console.log("Clicked");
};
  • 如果需要移除事件处理程序,只需要对onclick属性赋值null即可。
1
btn.onclick = null;

DOM2 事件处理程序

  • DOM2 Events为事件处理程序的赋值和移除定义了两个方法: addEventListener 和 removeEventListener。其目的在于统一事件注册接口,因为不只有click这么一个事件,还有诸多的类似于mouseover,scroll等的事件,所以把接口统一是很有必要的。
  • 如果要给按钮添加click事件,可以这样写:
1
2
3
4
5
6
7
let btn = document.getElementById("myBtn"); 
btn.addEventListener("click", (e) => {
console.log(this.id);
}, false);
// 第三个参数表示是冒泡事件还是捕获事件
// false -> 冒泡(缺省状态)
// true -> 捕获
  • 需要注意的是,通过addEventListener添加的匿名函数无法被移除,如上述例子添加的click事件就无法移除,所以事件的处理函数最好单独用一个变量进行存储,方便以后删除。
1
2
3
4
5
6
7
8
let btn = document.getElementById("myBtn");
let handle = () => {
console.log(this.id);
}
btn.addEventListener("click", handle ,false);

// 进行一番处理后移除监听函数
btn.removeEventListener('click', handle)

事件对象 event

  • 事件对象就是之前在HTML事件处理程序中提到的Event对象,只不过在Javascript事件处理程序中不需要显示传入Event对象,因为DOM的所有事件函数都已经装载了event对象,直接使用第一个变量即可。
1
2
3
4
5
let btn = document.getElementById("myBtn");
let handle = (e) => {
console.log(e.target);
}
btn.addEventListener("click", handle ,false);
  • 这里需要提到event对象的两个比较混淆的属性:targetcurrentTarget
  • target指的是事件目标,即直接触发了事件的那个DOM元素本身。
  • 而currentTarget则是当前事件的处理程序所绑定的元素。这可能不怎么好理解,下面有个实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div id="btn">

</div>
</body>
<script>
document.body.addEventListener('click', function(e) {
console.log('事件冒泡获得')
console.log(this === document.body) // true
console.log(this === e.currentTarget) // true
console.log(this === e.target) // false
})
</script>
  • 当我点击id=btn的div元素时,直接触发事件的元素是div,但由于事件冒泡所以body上也会响应该事件,且执行对应的事件函数。
  • 而这里的target指的是id=btn的div元素,currentTarget则指的是body,即绑定事件的DOM元素。

事件委托(代理)

  • 绑定事件监听函数是会占据内存空间的,虽然每个事件本身不会占据太多空间,但一旦数量多起来,还是会对性能造成一定的影响,所以在优化内存占用时,会尽可能地去减少事件的注册数量,尽量用一个代理函数去处理多个事件
  • 从代码实现的层面来考虑,通过事件冒泡和DOM树机制可以联想到,多个子节点的事件进行冒泡时最终会汇聚到某个父节点上,利用这一点,我们就不必在每个子节点上都设置事件监听函数,而是在对应的父节点上设置一个事件冒泡拦截函数,用这一个函数去处理每个子节点的事件触发,这就完成了一次事件委托,极大减少了事件监听的数量。
  • 最常见的应用便是列表节点的事件监听,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- 这里直接使用红宝书上的例子 -->
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>

<script>
let list = document.getElementById("myLinks");
list.addEventListener("click", (event) => {
let target = event.target;
switch(target.id) {
case "doSomething":
document.title = "I changed the document's title";
break;
case "goSomewhere":
location.href = "http:// www.wrox.com";
break;
case "sayHi":
console.log("hi");
break;
}
});
</script>

DOM扩展

虽然原生DOM API已经能做许多事情了,但是仍然不断有标准或专有的扩展出现,以支持更多的功能,由于各个浏览器对DOM扩展的支持是专有的,为了统一这些专有的DOM API,W3C开始着手将这些专有扩展转变为标准规范

Selectors API

  • Selectors APIs 是浏览器原生支持的CSS查询API,由于是原生支持,解析和遍历DOM树可以通过底层编译语言实现,相比其他Javascript库(比如jQuery),性能有数量级的提升。
  • Selectors API Level 1 主要是两个API:querySelector() 和 querySelectorAll()

querySelector()

  • 该方法接受CSS选择符参数,返回匹配到的第一个后代元素,如果没有匹配元素则返回null。
1
2
3
4
5
6
7
8
// 匹配 class 为 list 的第一个DOM元素
document.querySelector('.list')
// 匹配 id 为 myDiv 的第一个DOM元素
document.querySelector('#myDiv')

// 匹配 class=header DOM元素子元素中 id为someDiv的元素
const header = querySelector('.header');
header.querySelector('#someDiv')
  • 如果直接在document上使用querySelector方法,会从文档元素开始搜索(前两个例子);在Element上使用querySelector方法,则只会在该元素的后代中查询(第三个例子)

querySelectorAll()

  • 该方法和querySelector类似,接受CSS选择符参数,只不过它会返回匹配的所有节点。即返回的是一个NodeList的静态实例

  • ⚠️注意:无论是querySelector还是querySelectorAll返回的都是DOM元素的静态快照,即和之前提到的getElementById获取的动态实例不同,更改静态快照是不会影响DOM元素在页面上的渲染

matches()

  • matches方法接受一个css选择符参数,用于判断是否存在对应css选择符的DOM元素,如果存在返回true,否则返回false。
1
2
3
if(document.body.matches("body.header")){
// true
}

CSS类扩展

  • HTML5增加了一些特性以方便使用CSS类。

getElementsByClassName()

  • 该方法接受一个参数,包含一个或多个类名的字符串,返回类名中包含对应类的元素的NodeList。
1
2
// 返回 class 包含 username 和 current 的类
document.getElementsByClassName('username current')

classList属性

  • classList属性提供了快速操作DOM元素类名的方法,如果是之前,对一个含有多个class的DOM元素进行类名操作会很麻烦,基本操作都是把其类字符串解析成数组,然后一个一个进行匹配操作,最后再合并为一个新的类字符串重新赋值回去。
1
2
3
4
5
6
7
8
9
10
11
12
// 要删除"user"类
let targetClass = "user";
// 把类名拆成数组
let classNames = div.className.split(/\s+/);
// 找到要删除类名的索引
let idx = classNames.indexOf(targetClass);
// 如果有则删除
if (idx > -1) {
classNames.splice(i,1);
}
// 重新设置类名
div.className = classNames.join(" ");
  • 现在通过classList可以快速对类名进行操作。

    1. add(value) -> 类名列表添加值为value的类
    2. contains(value) -> 返回布尔值,表示给定value是否存在
    3. remove(value) -> 从类名列表中删除指定字符串
    4. toggle(value) -> 如果类名中存在指定的value,则进行删除;如果不存在,则添加。
  • 这个扩展方法在需要通过class来控制样式变换时非常有效

1224-浏览器缓存

浏览器缓存是性能优化中最直接,高效的优化方式,它可以显著减少因为网络传输而带来的损耗。

  • 对于数据请求来说,大致可以分为 请求 -> 处理 -> 响应这三个步骤,而浏览器缓存则主要在第一步和第三步做手脚,即请求发出时寻找合适的缓存,拿到新的响应数据时做新的缓存。
  • 缓存带给我们最直观的感受就是,每次加载页面,第二次之后加载总是比第一次加载的更快,这就是缓存的功劳,下面讲从缓存位置和缓存策略两个方面介绍浏览器相关缓存。

缓存位置

  • 浏览器的缓存位置大概可以分为四种:
    1. Service Worker
    2. Memory Cache
    3. Disk Cache
    4. Push Cache(HTTP 2)
  • 他们有各自的优先级,当发出请求时,浏览器会依次去寻找缓存,如果都没有命中,才会发出请求。

Service Worker

  • Service Worker是浏览器背后的独立线程,可以用来实现缓存功能,但是需要注意,如果要使用Service Worker,传输协议必须是HTTPS
  • Service Worker实现缓存大致三个步骤。
    1. 注册Service Worker。
    2. 监听install事件,并对需要的文件进行缓存。
    3. 拦截HTTPS请求,并根据请求内容去命中缓存,如果命中,则直接使用缓存,否则请求数据。
  • 下面是一串实例代码(源代码来自前端面试之道)
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
// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register('sw.js')
.then(function(registration) {
console.log('service worker 注册成功')
})
.catch(function(err) {
console.log('servcie worker 注册失败')
})
}

// sw.js
// 监听install事件,缓存文件
self.addEventListener('install', e => {
e.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll(['./index.html', './index.js'])
})
)
})

// 拦截请求,并根据请求去命中响应数据
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response
}
console.log('fetch source')
})
)
})

Memory Cache -> 内存缓存

  • 由于是内存缓存,所以速度会比后续马上讲到的磁盘缓存速度快很多,但是内存空间非常有限,所以一般内存缓存的内容都比较小,且生命周期短,会随着进程的释放而释放(关闭页面)。
  • 内存缓存一般是css,js,svg,小图片文件等,打开浏览器的开发者工具,重新刷新网络页面,可以发现很多请求右侧都有memory cache的提示,说明这些请求就是命中内存缓存中的内容而直接返回的内容。

内存缓存

Disk Cache

  • 磁盘缓存虽然速度没有内存缓存快,但是容量大,基本什么内容都可以存在磁盘中,并且可以存储内容的生命周期长。
  • 磁盘缓存会根据HTTP Header中国呢的字段判断哪些内容需要缓存,哪些资源可以直接使用,哪些资源已经过期需要重新请求。需要注意的是,相同地址的资源一旦被磁盘缓存下来,就不会再次去请求数据,比如我直接更改博客图片链接所指向的图片内容(链接不变),重新刷新后会发现图片依然是原来的图片。

Push Cache(Http2)

  • push cache是HTTP/2的内容,当上面三个缓存都没有命中时,才会被使用。并且缓存的时间非常短暂,只在session中存在,一旦session结束就会被释放。由于接触的不多,这里不细讲。

缓存策略

缓存策略分为两种:强缓存和协商缓存。并且它们的缓存策略都是通过HTTP Header来实现的。需要注意的是,这两种缓存并不是互斥的,即强缓存和协商缓存在一次请求中是同时存在的,下面会细说。

强缓存

  • 强缓存通过设置两种HTTP Header来实现:ExpiresCache-Control。强缓存表示的是在缓存期间内不需要请求,响应码为200.

Expires

1
Expires: Wed, 22 Oct 2018 08:41:00 GMT
  • Expires是HTTP/1的响应头,它表示的是请求的资源会在上述的时间后过期。如果资源会再次过期,并且Expires是基于本地时间进行判断的,如果人为更改了本地时间,可能会造成缓存失效。

Cache-Control

1
cache-control: public, max-age=31536000, s-maxage=31536000, immutable
  • Cache-Control是HTTP/1.1的请求/响应头,并且它的优先级高于Expires,该资源表示资源是可以被客户端,代理服务器缓存的,且缓存时间为一年。
  • 上述可以看到,Cache-Control可以使用多种指令,从而达到多个目的,下面是一些常见指令的作用。

cache-control指令

协商缓存

  • 协商缓存一般发生在强缓存不能进行的时候,当浏览器需要命中缓存却发现缓存已经过期时,它不会立即删除缓存,而是抱着试一试的态度,带着缓存的唯一标识信息去询问后端资源有没有更改,如果没有更改,就直接返回304(Not Modified),并更新本地缓存有效期

  • 和强缓存类似,协商缓存也需要借助HTTP Header字段的帮助,它们分别是Last-ModifiedE-Tag

Last-Modified 和 If-Modified-Since -> HTTP/1

  • Last-Modified 表示本地文件最后修改日期,If-Modified-Since会把Last-Modified的值发送给服务器,询问服务器在该时间段后,资源是否发生了更改,如果有更新,则会正常请求资源(200),并更新本地缓存,如果没有,则直接返回缓存资源(304)。

ETag 和 If-None-Match

  • ETag类似于文件id,If-None-Match会把ETag的值发送给服务器,并询问对应资源是否有更改,如果没有变动就直接返回本地缓存,如果有变动则返回新的资源,并更新本地缓存。需要注意的是,ETag的优先级比Last-Modified高

1222-DOM

NodeList 和 HTMLCollection区别

NodeList是节点的集合,而HTMLCollection是元素的集合。

节点包括很多类型,文档节点,元素节点,属性节点,文本节点,这一点通过Node类型上的12个数值常量表示就可以看出。

1
2
3
4
5
6
7
8
9
10
11
12
Node.ELEMENT_NODE(1) 
Node.ATTRIBUTE_NODE(2)
Node.TEXT_NODE(3)
Node.CDATA_SECTION_NODE(4)
Node.ENTITY_REFERENCE_NODE(5)
Node.ENTITY_NODE(6)
Node.PROCESSING_INSTRUCTION_NODE(7)
Node.COMMENT_NODE(8)
Node.DOCUMENT_NODE(9)
Node.DOCUMENT_TYPE_NODE(10)
Node.DOCUMENT_FRAGMENT_NODE(11)
Node.NOTATION_NODE(12)
1
2
3
4
5
6
// 常用的获取DOM元素的接口及其返回的数据结合类型
Node.childNodes instanceof Nodelist

Node.chidren instanceof HTMLCollection

document.getElementByxxxx instanceof HTMLCollection

注意:NodeList,HTMLCollection,NamedNodeMap 都是实时的,意味着文档结构的变化会实时地在它们身上反映出来。

1220-客户端检测

如今的浏览器虽然接口已大致统一,但是每家浏览器难免都有自己的“想法”,于是会出现个别的不统一,这些差异迫使Web开发者自己去设计兼容这些差异,客户端检测就是最常见的检测手段,通过检测结果来进一步克服和避免这些缺陷。

客户端检测可大致分为三种:能力检测,用户代理检测,软件与硬件检测。

能力检测

  • 能力检测也成为特性检测,因为不同浏览器提供的接口不是完全相同,于是可以通过简单的逻辑判断来检测在该浏览器环境下能否调用特定API,同时还能间接判断出浏览器类型。
  • 比如,在IE5之前没有document.getElementById这个DOM方法,但是可以通过document.all来实现相同的功能。于是,可以进行如下的能力检测。
1
2
3
4
5
6
7
8
9
const getElementById = (id) => {
if(document.getElementById){
return document.getElementById(id)
} else if (document.all){
return document.all[id]
} else {
throw new Error('该浏览不支持任何通过ID获取DOM元素的方法')
}
}
  • 需要注意的是,实现能力检测是一定要落实到具体的功能上,即某个能力的存在并不能代表其他能力也存在
1
2
3
4
5
6
7
function getWindowWidth() { 
if (document.all) { // 假设 IE
return document.documentElement.clientWidth; // 不正确的用法!
} else {
return window.innerWidth;
}
}
  • 比如上述例子,document.all的存在并不能说明documentElement.clientWidth的存在。其实这段代码的本意是通过document.all来判断当前浏览器是不是IE浏览器,事实document.all的存在并不能一定确认该浏览器就是IE浏览器。

基于能力检测进行浏览器分析

  • 除了上述可以进行基本的功能检测以外,还可以通过能力检测来进行浏览器的特性支持检测,比如是否支持Netscape插件,是否具有DOM Level 1能力等等。
1
2
3
4
5
6
// 红宝书P384
// 检测浏览器是否支持 Netscape 式的插件
let hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);
// 检测浏览器是否具有 DOM Level 1 能力
let hasDOM1 = !!(document.getElementById && document.createElement &&
document.getElementsByTagName);
  • 当然,也可以通过特定的能力检测来判断浏览器的类型,即根据对浏览器特性的检测与已知特性对比,来确认用户使用的是什么浏览器
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
// 红宝书 P385
class BrowserDetector {
constructor() {
// 测试条件编译
// IE6~10 支持
this.isIE_Gte6Lte10 = /*@cc_on!@*/false;
// 测试 documentMode
// IE7~11 支持
this.isIE_Gte7Lte11 = !!document.documentMode;
// 测试 StyleMedia 构造函数
// Edge 20 及以上版本支持
this.isEdge_Gte20 = !!window.StyleMedia;
// 测试 Firefox 专有扩展安装 API
// 所有版本的 Firefox 都支持
this.isFirefox_Gte1 = typeof InstallTrigger !== 'undefined';
// 测试 chrome 对象及其 webstore 属性
// Opera 的某些版本有 window.chrome,但没有 window.chrome.webstore
// 所有版本的 Chrome 都支持
this.isChrome_Gte1 = !!window.chrome && !!window.chrome.webstore;
// Safari 早期版本会给构造函数的标签符追加"Constructor"字样,如:
// window.Element.toString(); // [object ElementConstructor]
// Safari 3~9.1 支持
this.isSafari_Gte3Lte9_1 = /constructor/i.test(window.Element);
// 推送通知 API 暴露在 window 对象上
// 使用默认参数值以避免对 undefined 调用 toString()
// Safari 7.1 及以上版本支持
this.isSafari_Gte7_1 =
(({pushNotification = {}} = {}) =>
pushNotification.toString() == '[object SafariRemoteNotification]'
)(window.safari);
// 测试 addons 属性
// Opera 20 及以上版本支持
this.isOpera_Gte20 = !!window.opr && !!window.opr.addons;
}
isIE() { return this.isIE_Gte6Lte10 || this.isIE_Gte7Lte11; }
isEdge() { return this.isEdge_Gte20 && !this.isIE(); }
isFirefox() { return this.isFirefox_Gte1; }
isChrome() { return this.isChrome_Gte1; }
isSafari() { return this.isSafari_Gte3Lte9_1 || this.isSafari_Gte7_1; }
isOpera() { return this.isOpera_Gte20; }
}

  • 上述代码会随着浏览器的变迁及发展而不同,不过提供的主要API可以保持不变。

用户代理检测

  • 每个浏览器都包含一串用户代理字符串,在浏览器端,可以通过navigator.userAgent来获得。在服务器端,常见的做法是根据接受到的用户代理字符串来确定浏览器并执行响应操作。而用户代理字符串会在浏览器发起HTTP请求时自动附加在HTTP请求头里,及user-agent字段。

  • 但同时用户代理字符串也是饱受争议的,因为用户代理字段有很长一段时间具有很大的欺诈性,无论是人为伪造,还是各大厂商浏览器自带的用户代理字符串。这牵扯到浏览器的发展史,详细浏览器发展史请参考红宝书P386

  • 为啥会出现连浏览器自带的用户代理字符串都会有欺诈性,原因我大概总结一下:各大厂商研发浏览器时,为了让自己浏览器不被冷落而是快速融入当时的Web环境中而不得不选择的手段。当年Netscape Navigator 3浏览器大火之时,IE3页横空出世,但是网景公司的浏览器的用户代理字段代号是Mozilla,而当时市面上基本所有的开发者都会去检测用户代理字段看其是否为Mozilla,如果IE3的用户代理字段不顺应潮流,那么根本融入不进当时的Web环境,因为几乎没有网页去适配IE3,所以微软才打算,把代号改成和网景浏览器相同的代号Mozilla

  • 同理,后面的浏览器都纷纷效仿这钟做法(除了Opera,但是Opera 9以后还是选择了妥协)。但是这违背了用户代理字段设计出来的初衷,于是为了增加浏览器的辨识度,浏览器厂商考虑在用户代理字符串中再加入一段特定的标识符,用来说明浏览器类型,比如Chrome的userAgent如下所示。

1
2
> navigator.userAgent
//'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36'
  • 所以因为这段历史,目前市面上所有的浏览器的用户代理字符串开头的代号都是Mozilla

软件与硬件检测

  • 除开浏览器功能上的检测,浏览器还提供了许多操作系统,硬件和周边设备的信息,这些属性都暴露在window.navigator上。
  • 这一部分的接口很多,提供了诸如地址信息,硬件内存,当前网络情况等信息的API接口,这里不一一列举,需要用的时候再去查找相关用法。(红宝书P394 - P400)

设计模式 - 观察者模式 - 发布订阅模式

观察者模式和发布订阅模式是平常业务开发中最常见的设计模式,虽然网上大多数文章将二者归为一类,其实不然,它们两者之间还是有细微的差距。

先来个观察者模式的定义

  • 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的某个属性(或状态)发生变化时,会通知所有观察者对象,让它们自动更新

现实映射

  • 举一个🌰,高中的时候,我会经常去问老师问题,有时候遇到比较难的问题,老师一时半会解不开,老师会说**”你先去做其他的事情吧,一会儿我找到解题思路了来叫你”**。于是乎我先去做其他事情,等待老师的召唤。过了一会儿,老师叫另一个同学来叫我去办公室找他,于是我马上放下手中的活,冲向的老师的办公室……
  • 在这里例子里,我是一位观察者,而老师则是一位我观察的对象,当老师的状态发生了变化(指想出了题的思路),我就会接受到对应的信息,然后马上更新我自己的状态(指润去找老师)。

来点转换

  • 上述例子如果在发布-订阅模式里,我则摇身一变,变成了订阅者,专门订阅老师发布的通知信息,而老师则作为了发布者
  • 其实,上述例子还不能完全展示出定义所说的一对多关系,因为订阅者只有我一个人,但其实稍微扩展一下,变成多位同学向老师询问同一道题目,那这就是标准的观察者模式了,多位观察者“观察”老师的状态。

来点代码

  • 通过上述的定义和描述,大概可以知道,在观察者模式中,一共有两个类:发布者类和订阅者类。作为一个发布者,很容易可以想到它有下面几个基本方法:增加订阅者,通知订阅者,移除订阅者。思路有了,下面就直接实现。
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
// 发布者类
class Publisher {
constructor() {
this.observers = [] // Observer -> 观察员
}

// 添加订阅者
add(observers) {
this.observers.push(...observers)
}
// 移除订阅者
remove(observer) {
this.observers.forEach((item, index) => {
if (item === observer) {
this.observer.splice(index, 1)
}
})
}
// 通知订阅者
notify() {
this.observers.forEach((item) => {
item.update(); // 注意,订阅者的方法应该它们本身定义的
})
}
}


  • 发布者基本类设计完毕,下面开始设计下订阅者,其实订阅者很简单,它最核心的就一个方法:收到发布者的信息后,去进行状态更新。如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
// 定义订阅者类
class Observer {
constructor() {
console.log('创建订阅者')
}

update() {
console.log('更新状态')
}
}


  • 好了,上述两段代码就是最基本的观察者模式实现,实际场景的观察者模式都是基于上面的代码进行迭代。比如下面我将会把之前说的实例进行代码实现
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
// 教师类
class Teachere extends Publisher {
constructor() {
super()
//
this.answers = null
this.observers = []
}

getAnswers() {
return this.answers
}

setAnswers(ques) {
this.answers = answers;
console.log('老师想出了答案,快来办公室找老师')
this.notify()
}

notify() {
const that = this;
this.observers.forEach(item => {
item.update(that)
})
}
}

// 学生类
class Student extends Observer {
constructor() {
console.log("我是学生")
}

update(info) {
const answer = info.getAnswers()
// ... 进行相关的更新操作
}
}

观察者模式和发布-订阅模式的区别

  • 其实在刚才的例子不算严格意义上的发布-订阅模式,因为发布-订阅模式中,发布者和订阅者二者之间是透明的,即它们是彼此感受不到对方的,无论是发布事件,还是订阅事件,都是交给一个统一的管理系统来进行处理的,这就类比于游戏原神中的每日委托一样,委托者在冒险家协会进行每日任务的委托,而旅行者-我则会去冒险家协会去领取每日任务,这个过程中,我们彼此感受不到对方,而是全权交给冒险家协会来进行管理的。
  • 如果放到Javascript世界中,无论是Vue的EventBus还是Nodejs中的EventEmitter类,它们的设计理念就是发布-订阅模式,即发布事件 - 订阅事件这一个过程。
1
2
3
4
5
6
7
8
// 订阅事件
event.on("callTeacher", function(...args){

})

// 发布事件
event.emit('callTeacher', arg1, arg2, arg3, ...args)

  • 很明显,和观察者模式不同的是,发布-订阅模式则将订阅者和发布者完全解耦,二者再也没有直接关联,它们的一切处理都统一交给第三方处理
  • 下面是一个简单的EventEmitter实现,为了加深对发布-订阅模式的认识。
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
// 发布-订阅模式的管理站
class EventEmitter {
constructor() {
// handlers -> map,存储事件和回调之间的关系
this.handlers = {}
}

// eventname -> 事件名称 , cb -> 响应的回调函数
on(eventName, cb) {
if (!this.handlers[eventName]) {
this.handlers[eventName] = []
}

this.handlers[eventName].push(cb);
}

emit(eventName, ...args) {
if (this.handlers[eventName]) {
// 这里用拷贝,否则once的删除会导致handlers的forEach遍历失序
const handlers = this.handlers[eventName].slice()
handlers[eventName].forEach(cb => {
cb(...args)
})
}
}

off(eventName, cb) {
if (this.handlers[eventName]) {
this.handlers[eventName].forEach((item, index) => {
if (item === cb) {
this.handlers[eventName].splice(index, 1)
}
})
}
}

// 为事件注册单次监听器
once(eventName, cb) {
const wrapper = (...args) => {
cb(...args)
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}

迭代器and生成器

迭代器 -> Symbol.iterator

  • 当需要对某个迭代对象进行迭代处理时,由于迭代之前需要事先知道如何使用数据结构,以及遍历顺序并不是数据结构固有的,所以想寻求某种机制去统一迭代过程,对每一种可迭代类型,都用同一种迭代方法,从而增加开发体验。(即无需事先知道如何迭代去实现迭代操作
  • 于是基于以上原因,诞生了迭代器概念,意在统一化所有迭代对象的处理方式。
  • 接受可迭代对象的原生语言特性包括如下:(即迭代对象都可以用下面的任意一种方式去处理)
  1. for-of
  2. 数组解构
  3. 扩展操作符号(即...)
  4. Array.from
  5. 创建集合
  6. 创建映射
  7. Promise.all()接受由Promise组成的可迭代对象。
  8. Promise.rice()接受由期约组成的可迭代对象。
  9. yield*操作符,在生成器中使用。
  • 基于以上特性,可以得知,迭代器也可以在宏观意义上理解为类数组

判断与使用

  • 如何判断一个对象是否为迭代类型是看它的Symbol.iterator属性是不是迭代器的工厂函数,如果是,则调用该函数后,会生成一个迭代器对象
1
2
3
4
5
6
7
8
9
10
const a = 1;
const b = {};
const c = [5,3,4,6]

console.log(a[Symbol.iterator]);
console.log(b[Symbol.iterator])
const c_iter = c[Symbol.iterator]() // -> 生成一个临时的迭代器对象,对迭代器对象可以通过统一方法进行迭代处理
// undefined
// undefined
// Object [Array Iterator] {} -> 迭代器对象
  • 想对迭代器整体进行一个迭代处理,那么不需要显示去获取迭代器对象,直接调用上述的一系列方法去处理即可。
  • 但如果是想对这个迭代过程进行更细节的处理,即控制每一个元素,那么需要获得迭代器对象,并调用next()方法去获取每一次迭代的值。
1
2
3
const c = [5,3,4,6]
const c_iter = c[Symbol.iterator]()
console.log(c_iter.next()); // -> { value: 5, done: false }
  • 可以从上述代码发现,调用next返回的值是一个对象,value为当前迭代元素的值,done则标记本次获取值后是否迭代完成

  • 除了next函数,迭代器还包括一个可选的return函数,它会在迭代中止时执行,而迭代中止可能的情况如下:

    1. for-of的break,continue,return,throw提前退出。
    2. 解构操作并未消费所有值。
  • 所以如果需要hook到迭代中止的情景,那可以自定义return方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Counter { 
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {
let count = 1,
limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true };
}
},
return() {
// 在这里面操作
console.log('Exiting early');
return { done: true };
}
};
}
}
  • 接上,如果某次中止后,迭代器并没关闭的化,则还可以接续从上次离开的地方继续迭代,比如数组的迭代器就是不可关闭的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let a = [1, 2, 3, 4, 5]; 
let iter = a[Symbol.iterator]();
for (let i of iter) {
console.log(i);
if (i > 2) {
break
}
}
// 1
// 2
// 3
for (let i of iter) {
console.log(i);
}
// 4
// 5

生成器

  • 生成器是一个非常灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。它的形式是一个函数,函数名称前面加一个*号就表示它是一个生成器。
1
2
function *generartorFn() {}
// 注: 箭头函数不支持生成器声明
  • 需要注意的是,直接调用生成器函数生成的是一个生成器函数,而不是直接执行函数内部的内容。即生成器对象一开始处于的是暂停执行的状态。与迭代器类似,生成器也实现Iterator接口,即它也是可迭代的对象,也具有next方法,next方法就是控制生成器执行的方法
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
function* generator(){
yield 1;
yield 2;
yield 3;
return 4;
}

const genObj = generator();
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.next())
console.log(genObj.next())

// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 4, done: true }

// or
for (let o of genObj){ // genObj是可迭代对象,所以可以使用for-of循环
console.log(o)
}

// 1
// 2
// 3
  • 可以看到上述代码出现了yield关键字,它就是生成器函数中的暂停节点(中断执行),调用一次next方法后,代码会运行到下一个yield之处。
  • 同时yield还提供了输入输出功能,比如上述代码中,yield后的值就是本次调用next方法的返回值,若向本次的next方法中传入值,yield将作为临时变量存储对应的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* generator(){
console.log(yield)
console.log(yield)
console.log(yield)
}

const genObj = generator();
genObj.next(1)
genObj.next(2)
genObj.next(3)
genObj.next(4)
// 2
// 3
// 4
  • 可以看到,1没有打印出来,是因为第一次调用next只是启动函数,并没有实际运行到console.log处。

vuepress开发遇到的一些问题

  1. vuepress是SSR渲染,即vue挂载之前是在服务端进行的,所以尽量不要在before Mounted之前的hooks中调用浏览器API,否则打包时会报错。
  2. 关于css中的 mix-blender滤镜模式,和 z-index关联比较多,具体体现在我在使用darkmodejs时,如何避免图片被mix-blender渲染,虽然官方给的方法是加入isolation:isolate属性(另启层叠上下文),但是并没有什么用,感觉是哪个地方出问题了,关于层叠上下文还有上述提到的属性需要重新学习下。
    • 另外,层叠上下文z-index和position关联很大,这个也要去做深究,我如果只是给image加z-index,则无法避免被滤镜覆盖的事实,应该是需要把他们纳入统一个层叠上下文才行,所以需要position:relative(注意,position默认是static)。这一块儿的知识也要重点去温习。
1
2
3
4
5
6
7
8
9
10
const testFn = async () => {
const a = 2;
for(let i = 0; i < a; i++){
// xxxxxx
}

return new Promise((res) => {
res();
})
}
LR
1
2
3
A-.->B


请我喝杯咖啡吧~

支付宝
微信