JS
有几种不同的策略来确保 JavaScript 只在 HTML 解析之后运行:
在上面的内部 JavaScript 示例中,脚本元素放在文档正文的底部,因此只能在 HTML 正文的其他部分被解析以后运行。
在上面的外部 JavaScript 实例中,脚本元素放在文档的头部,在解析 HTML 正文之前解析。但是由于我们使用了
<script type="module">
,代码被视为一个模块,并且浏览器在执行 JavaScript 模块之前会等待所有的 HTML 代码都处理完毕(也可以把外部脚本放在正文的底部,但是会拖慢)。如果仍然想在文档头部使用非模块脚本,可能阻塞整个页面的显示,并且可能出现错误,因为脚本在文档解析之前执行:
- 对于外部脚本,应该在 script 元素上添加
defer
(或者如果不需要 HTML 解析完成,则可以使用async
)属性。 - 对于内部脚本,应该将代码封装在
DOMContextLoaded
事件监听器中。
- 对于外部脚本,应该在 script 元素上添加
input
中的值属性为value
;p
中的文本属性为textContent
,样式为style
。默认情况下,
querySelectorAll()
仅验证选择器中的最后一个元素是否在搜索范围内。shift 和 unshift
unshift()
方法将指定元素添加到数组的开头,并返回数组的新长度。shift()
方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。
任何不是
false
、undefined
、null
、0
、NaN
、或空字符串(''
)的值在作为条件语句进行测试时实际返回true
,因此可以简单地使用变量名称来测试它是否为真,甚至是否存在(即它不是未定义的)。如果你正在编写一个函数,并希望支持可选参数,你可以在参数名称后添加
=
,然后再添加默认值来指定默认值javascriptfunction hello(name = "克里斯") { console.log(`你好,${name}!`); } hello("阿里"); // 你好,阿里! hello(); // 你好,克里斯!
1
2
3
4
5
6函数参数通常作为匿名函数传递。匿名函数也可以作为表达式赋值给另一个变量,与函数声明不同,函数表达式不会被提升。 箭头函数其实是匿名函数传参的简化写法
javascripttextBox.addEventListener("keydown", function (event) { console.log(`You pressed "${event.key}".`); }); // is the same as textBox.addEventListener("keydown", (event) => { console.log(`You pressed "${event.key}".`); });
1
2
3
4
5
6
7事件处理器(原始事件模型,DOM0级)不能为一个事件添加一个以上的处理程序,但事件监听器(标准事件模型,DOM1级)可以
javascriptelement.addEventListener("click", function1); element.addEventListener("click", function2); // OK element.onclick = function1; element.onclick = function2; // rewrite previous
1
2
3
4
5事件冒泡是从内到外,捕获是从外到内。使用
event.stopPropagation()
可以阻止事件继续传播(无论是冒泡还是捕获)。默认事件冒泡,事件捕获需要在添加事件监听器时设置第三个参数为{ capture: true }
。HTML
节点是 DOM 树的根结点。常用的 DOM 方法有querySelector("selector")
createElement("p")
appendChild(node)
removeChild(node)
remove()
可以将 JSON 作为 JavaScript 对象导入,或将 CSS 作为
CSSStyleSheet
对象导入。javascriptimport colors from "./colors.json" with { type: "json" }; import styles from "./styles.css" with { type: "css" }; console.log(colors.map((color) => color.value)); document.adoptedStyleSheets = [styles];
1
2
3
4
5只能在模块内使用
import
和export
语句,不能在常规脚本中使用。使用模块需要在 script 元素中包含type="module"
。模块不一定需要 import/export 语句——唯一需要的是入口点有
type="module"
。可以将每一个模块功能导入到一个模块功能对象中。可以使用以下语法形式:
javascriptimport * as Module from "/modules/module.js"; Module.function1(); Module.function2();
1
2
3
4允许将
import()
作为函数调用,将模块的路径作为参数传入。它返回一个Promise
,会兑现为一个可以让你访问其导出的模块对象(参见创建模块对象)。javascriptimport("/modules/mymodule.js").then((module) => { // 使用模块做一些事情。 });
1
2
3then()
函数会返回一个和原来不同的新的 Promise,这样就可以链式调用多个异步操作(不用嵌套回调函数)。还可以使用Promise.all()
方法。它接收一个 Promise 数组,并返回一个单一的 Promise。在全部被兑现后得到通知。自然,还有对应的Promise.any()
。另外,async
和await
关键字使得从一系列连续的异步函数调用中建立一个操作变得更加容易,避免了创建显式 Promise 链,并允许你像编写同步代码那样编写异步代码。javascriptconst fetchPromise1 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); const fetchPromise2 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found", ); const fetchPromise3 = fetch( "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json", ); Promise.all([fetchPromise1, fetchPromise2, fetchPromise3]) .then((responses) => { for (const response of responses) { console.log(`${response.url}:${response.status}`); } }) .catch((error) => { console.error(`获取失败:${error}`); });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19传入
then()
的函数不会立即运行,而是被放入微任务队列中。事件循环
在执行 JavaScript 代码的时候,JavaScript 运行时实际上维护了一组用于执行 JavaScript 代码的代理。每个代理由一组执行上下文的集合、执行上下文栈、主线程、一组可能创建用于执行 worker 的额外的线程集合、一个任务队列以及一个微任务队列构成。
每个代理都是由事件循环(Event loop)驱动的,事件循环负责收集事件(包括用户事件以及其他非用户事件等)、对任务进行排队以便在合适的时候执行回调。然后它执行所有处于等待中的 JavaScript 任务,然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操作。
网页或者 app 的代码和浏览器本身的用户界面程序运行在相同的线程中,共享相同的事件循环。该线程就是主线程,它除了运行网页本身的代码之外,还负责收集和派发用户和其他事件,以及渲染和绘制网页内容等。
有如下三种事件循环:Window 事件循环、Worker 事件循环、和Worklet 事件循环。
事件循环的执行流程:
- 执行一个宏任务(如 script 主代码、
setTimeout
回调等)。这当中会将可能的微任务或宏任务加入至队列。主代码是一开始执行的入口。 - 清空微任务队列:执行所有微任务(包括执行过程中新产生的微任务)。同样,这当中会将可能的微任务或宏任务加入至队列。
- 渲染页面(如果需要)。
- 重复下一轮事件循环,执行下一个宏任务。
由于你的代码和浏览器的用户界面运行在同一个线程中,共享同一个事件循环,假如你的代码阻塞了或者进入了无限循环,则浏览器将会卡死。无论是由于 bug 引起还是代码中进行复杂的运算导致的性能降低,都会降低用户的体验。
当来自多个程序的多个代码对象尝试同时运行的时候,一切都可能变得很慢甚至被阻塞,更不要说浏览器还需要时间来渲染和绘制网站和 UI、处理用户事件等。
使用 web worker 可以让主线程另起新的线程来运行脚本,这能够缓解上面的情况。一个设计良好的网站或应用会把一些复杂的或者耗时的操作交给 worker 去做,这样可以让主线程除了更新、布局和渲染网页之外,尽可能少的去做其他事情。
通过使用像 promise 这样的异步 JavaScript 技术可以使得主线程在等待请求返回结果的同时继续往下执行,这能够更进一步减轻上面提到的情况。然而,一些更接近于基础功能的代码——比如一些框架代码,可能更需要将代码安排在主线程上一个安全的时间来运行,它与任何请求的结果或者任务无关。
微任务是另一种解决该问题的方案,通过将代码安排在下一次事件循环开始之前运行而不是必须要等到下一次开始之后才执行,这样可以提供一个更好的访问级别。
- 执行一个宏任务(如 script 主代码、
Polyfill 也由第三方的 JavaScript 文件组成,你可以把它们放到你的项目中,但它们与库不同。库倾向于加强现有的功能,使一些需求可以更容易实现,而 Polyfill 提供的是根本不存在的功能。例如,你可以使用 es6-promise 这样的 polyfill 来使 promise 在没有原生支持的浏览器中也能工作。
对于那些想要使用现代 JavaScript 特性的开发者来说,另一个选择是将采用 ECMAScript 6/ECMAScript 2015 特性的代码转换成能够在旧版浏览器上运行的版本。Babel.js 是一种常见的转译器,但还有其他转译器。
常考
Symbol (符号)是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
等于符号的区别:
- 等于操作符(==)在比较中会先进行类型转换,再确定操作数是否相等。
- 全等操作符由 3 个等于号( === )表示,只有两个操作数在不转换的前提下相等才返回
true
。即类型相同,值也需相同 - 但在比较
null
的情况的时候,我们一般使用相等操作符==
(null == undefined,这样写法简洁)
前提为拷贝类型为引用类型的情况下:
浅拷贝是拷贝一层(属性是基本类型的时候是新增内存),属性为对象时,浅拷贝是复制,两个对象指向同一个地址。
深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址。 深拷贝的一种实现:
jsfunction deepCopy(obj: any): any { if(obj == null || typeof obj !== 'object') { return obj; } if(Array.isArray(obj)) { // 保证拷贝过后还是数组(还有数组属性) let newArr: any[] = []; for(let item in obj) { newArr.push(deepCopy(item)); } return newArr; } let newObj: any = {}; for(let key in obj) { if(obj.hasOwnProperty(key)) { newObj[key] = deepCopy(obj[key]); } } return newObj; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
任何闭包的使用场景都离不开这两点:
- 创建私有变量
- 延长变量的生命周期
我们一般将作用域分成:
- 全局作用域:任何不在函数中或是大括号中声明的变量,都是在全局作用域下。
- 函数作用域:函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。
- 块级作用域:ES6引入了
let
和const
关键字,和var
关键字不同,在大括号中使用let
和const
声明的变量存在于块级作用域中。在大括号之外不能访问这些变量。
原型对象: 每个函数都有一个特殊的属性叫作原型
prototype
。原型对象prototype
有一个自有属性constructor
,这个属性指向该函数。每个对象的
__proto__
都是指向它的构造函数的原型对象(prototype
)的- 原型对象(
xxx.prototype
)本身是一个普通对象,而普通对象的构造函数都是Object
。 - 所有的构造器都是函数对象,函数对象都是
Function
构造产生的。
js//因此 实例.__proto__ === 构造函数.prototype; 构造函数.__proto__ === Function.prototype 任何东西.prototype.__proto__ === Object.prototype Object.prototype.__proto__ === null //此外 函数.prototype.constructor === 函数 //特例 Function.__proto__ === Function.prototype Object.__proto__ === Function.prototype
1
2
3
4
5
6
7
8
9
10
11
12例如:
tstype Cons = { new (a: number, b: number): {a: number, b: number}; } let cons: Cons = class { a: number; b: number; constructor(a: number, b: number){ this.a = a; this.b = b; } } let obj = new cons(1, 2); console.log(obj.__proto__ === cons.prototype); console.log(cons.__proto__ === Function.prototype); console.log(cons.prototype.__proto__ === Object.prototype); console.log(Function.prototype.__proto__ === Object.prototype); console.log(Object.prototype.__proto__ === null); console.log(cons.prototype.constructor === cons); // all true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23区别
prototype
是函数属性,__proto__
是对象属性。- 只有函数有
prototype
(实例没有),所有对象(包括函数)都有__proto__
。 - 联系
- 实例的
__proto__
指向其构造函数的prototype
。 - 函数的
__proto__
指向Function.prototype
(因为函数是Function
的实例)。 - 作用
- 当你用
new
关键字创建实例时,实例的__proto__
会指向构造函数的prototype
。这样所有实例可以共享prototype
上的方法,节省内存。 __proto__
形成原型链。当访问对象的属性时,如果对象自身没有,JavaScript会通过__proto__
逐层向上查找,直到找到或到达null
。
- 原型对象(
原生实现OOP继承
js// Father class function construct(name, age) { this.name = name; this.age = age; } construct.prototype.introduce = function (){ console.log(`My name is ${this.name} and I am ${this.age} years old.`); } // Father class instance let obj = new construct("John", 21); obj.introduce(); // My name is John and I am 21 years old. // Child class let Student = function (school){ this.school = school; } Student.prototype = new construct("John", 21); // Create Father class instance first Student.prototype.introduceSchool = function (){ console.log(`I am a student at ${this.school}.`); } // Child class instance let obj1 = new Student("Harvard"); obj1.introduceSchool(); // I am a student at Harvard. obj1.introduce(); // My name is John and I am 21 years old. // Prototype chain console.log(obj1.__proto__ === Student.prototype); // true console.log(obj1.__proto__.__proto__ === construct.prototype); // true console.log(obj1.__proto__.__proto__.__proto__ === Object.prototype); // true console.log(obj1.__proto__.__proto__.__proto__.__proto__ === null); // true
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缺点:多个子类实例无法复用父类的函数,创建实例的时候复杂,父类引用属性共享。
js//最优解:使用Object.create 的寄生继承 // Father class function construct(name, age) { this.name = name; this.age = age; } construct.prototype.introduce = function (){ console.log(`My name is ${this.name} and I am ${this.age} years old.`); } // Father class instance let obj = new construct("John", 21); obj.introduce(); // My name is John and I am 21 years old. // Child class let Student = function (name, age, school){ this.school = school; construct.call(this, name, age); } Student.prototype = Object.create(construct.prototype); // Create Father class instance first Student.prototype.constructor = Student; Student.prototype.introduceSchool = function (){ console.log(`I am a student at ${this.school}.`); } // Child class instance let obj1 = new Student("Lisa", 18, "Harvard"); obj1.introduceSchool(); // I am a student at Harvard. obj1.introduce(); // My name is Lisa and I am 18 years old. // Prototype chain console.log(obj1.__proto__ === Student.prototype); // true console.log(obj1.__proto__.__proto__ === construct.prototype); // true console.log(obj1.__proto__.__proto__.__proto__ === Object.prototype); // true console.log(obj1.__proto__.__proto__.__proto__.__proto__ === null); // true
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根据不同的使用场合,
this
有不同的值,主要分为下面几种情况:默认绑定:全局环境中定义
person
函数,内部使用this
关键字,指向window
(非严格环境下)隐式绑定:函数还可以作为某个对象的方法调用,这时
this
就指这个上级对象jsfunction test() { console.log(this.x); } var obj = {}; obj.x = 1; obj.m = test; obj.m(); // 1
1
2
3
4
5
6
7
8
9new绑定:通过构建函数
new
关键字生成一个实例对象,此时this
指向这个实例对象。但当返回一个非空对象时,指向这个非空对象。如果在箭头函数中使用this,可能会出现问题。
显式绑定:
apply()、call()、bind()
是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。它们都是纯函数,不改变原来的函数!!!
jsvar x = 0; function test() { console.log(this.x); } var obj = {}; obj.x = 1; obj.m = test; obj.m.apply(obj) // 1
1
2
3
4
5
6
7
8
9
执行上下文的生命周期包括三个阶段:创建阶段 → 执行阶段 → 回收阶段
创建阶段:即当函数被调用,但未执行任何其内部代码之前。主要确定
this
的值,创建词法环境(建立作用域链)和变量环境。词法环境和变量环境的区别在于前者用于存储函数声明和变量(
let
和const
)绑定,而后者仅用于存储变量(var
)绑定执行阶段:执行变量赋值、代码执行。如果
Javascript
引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配undefined
值回收阶段:执行上下文出栈等待虚拟机回收执行上下文
事件流的三个阶段:捕获阶段 → 目标阶段 → 冒泡阶段
JS单线程,但实际上参与工作的线程共4个:一个主线程,其余3个辅助。
- JS 引擎线程(主线程):JS内核,负责解析JS脚本程序的主线程,例如V8
- 事件触发线程:属于浏览器内核线程,主要控制事件调度,当事件触发时,将事件的处理回调推进事件队列,等待JS引擎线程执行。
- 定时器触发线程:控制
setInterval
和setTimeout
,计时完毕把回调函数推进事件队列,等JS引擎执行。 HTTP
异步请求线程:通过XMLHttpRequest
连接后,通过浏览器新开一个线程,监控readyState
状态改变,状态变更时,将回调函数推进事件队列,等 JS 引擎执行。
instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。返回的是一个布尔值.jsfunction myInstanceOf(obj, constructor){ let proto = obj.__proto__; while(proto != null){ if(proto === constructor.prototype) return true; proto = proto.__proto__; } return false }
1
2
3
4
5
6
7
8如果我们有一个列表,列表之中有大量的列表项,我们需要在点击列表项的时候响应一个事件。如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的。这时候就可以事件委托,把点击事件绑定在父级元素
ul
上面,然后执行事件的时候再去匹配目标元素。new
关键字主要做了以下的工作:- 创建一个新的对象
obj
- 将对象与构建函数通过原型链连接起来
- 将构建函数中的
this
绑定到新建的对象obj
上 - 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
jsfunction mynew(Func, ...args) { // 1.创建一个新对象 const obj = {} // 2.新对象原型指向构造函数原型对象 obj.__proto__ = Func.prototype // 3.将构建函数的this指向新对象 let result = Func.apply(obj, args) // 4.根据返回值判断 return result instanceof Object ? result : obj }
1
2
3
4
5
6
7
8
9
10- 创建一个新的对象
bind/apply/call
辨析- 三者都可以传参,但是
apply
是数组,而call
是参数列表,且apply
和call
是一次性传入参数,而bind
可以分为多次传入 bind
是返回绑定this之后的函数,apply
、call
则是立即执行
jslet obj = { myname:"张三" } fn.apply(obj,[1,2]);//参数为数组 fn.call(obj, 1, 2);//参数为变长 let objFn = fn.bind(obj);//返回一个绑定好的新函数 objFn(1,2);
1
2
3
4
5
6
7
8- 三者都可以传参,但是
BOM:浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象
- window:在浏览器中,
window
对象有双重角色,即是浏览器窗口的一个接口,又是全局对象。主要可以控制窗口的移动和打开新窗口。 - location:对浏览器地址进行读取和操作,
href
是读取完整地址,除此还有hash
/hostname
/port
/search
等。 - navigator:对象主要用来获取浏览器的属性,区分浏览器类型等。属性较多,且兼容性比较复杂
- screen:是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度
- history:
history
对象主要用来操作浏览器URL
的历史记录,可以通过参数向前,向后,或者向指定URL
跳转。
- window:在浏览器中,
ES 6
for
循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。函数内部的变量i
与循环变量i
不在同一个作用域,有各自单独的作用域。只要块级作用域内存在
let
命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。ES6 明确规定,如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错,称为暂时性死区。考虑到环境导致的行为差异太大,块级作用域内部,优先使用函数表达式。
ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。(如if后面的)
如果真的想将对象冻结(const),应该使用
Object.freeze
方法。冻结后添加新属性不起作用。除了将对象本身冻结,对象的属性也应该冻结。javascriptvar constantize = (obj) => { Object.freeze(obj); Object.keys(obj).forEach( (key, i) => { if ( typeof obj[key] === 'object' ) { constantize( obj[key] ); } }); };
1
2
3
4
5
6
7
8全局环境中,
this
会返回顶层对象。在Node.js中为global
,在浏览器环境中为window
。ES6通过扩展元素符
...
,好比rest
参数的逆运算,将一个数组转为用逗号分隔的参数序列javascriptconsole.log(...[1, 2, 3]) // 1 2 3 console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5 [...document.querySelectorAll('div')] // [<div>, <div>, <div>] [...'hello'] // [ "h", "e", "l", "l", "o" ]
1
2
3
4
5
6
7
8
9
10
11Array.from()
用于将类似数组的对象和可遍历(iterable)
的对象转换为真正的数组。Array.of()
用于将一组值,转换为数组。javascriptArray.from([1, 2, 3], (x) => x * x) // [1, 4, 9] Array.of(3, 11, 8) // [3,11,8]
1
2
3
4flat()
默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()
方法的参数写成一个整数,表示想要拉平的层数,默认为1javascript[1, 2, [3, [4, 5]]].flat() // [1, 2, 3, [4, 5]] [1, 2, [3, [4, 5]]].flat(2) // [1, 2, 3, 4, 5]
1
2
3
4
5扩展运算符:在解构赋值中,未被读取的可遍历的属性,分配到指定的对象上面
javascriptlet { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; x // 1 y // 2 z // { a: 3, b: 4 }
1
2
3
4函数的length属性:返回没有指定默认值的参数个数(rest 参数也不会计入length属性)
javascript(function (a) {}).length // 1 (function (a = 5) {}).length // 0 (function (a, b, c = 5) {}).length // 2 (function(...args) {}).length // 0
1
2
3
4箭头函数:使用“箭头”(
=>
)定义函数。如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return
语句返回javascriptvar f = v => v; var f = () => 5; var sum = (num1, num2) => num1 + num2; var sum = (num1, num2) => { num1 += 10; return num1 + num2; }
1
2
3
4Set 数据结构:增删改查有
add()/delete()/has()/clear()/size
,迭代器有- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回键值对的遍历器
- forEach():使用回调函数遍历每个成员
javascriptlet set = new Set(['red', 'green', 'blue']); for (let item of set.keys()) { console.log(item); } for (let item of set.values()) { console.log(item); } for (let item of set.entries()) { console.log(item); } set.forEach((value, key) => console.log(key + ' : ' + value))
1
2
3
4
5
6
7
8
9
10
11
12Map 数据结构:
set()
,其他增删改查与 Set 相同。遍历迭代器与 Set 相同。WeakSet
只能成员只能是引用类型,而不能是其他类型的值。WeakSet
里面的引用只要在外部消失,它在WeakSet
里面的引用就会自动消失。Promise 的构造函数:
all():将多个
Promise
实例,包装成一个新的Promise
实例。当所有都fulfilled
的话,就返回fulfilled
;当有一个rejected
,就返rejected
。注意,如果作为参数的Promise
实例,自己定义了catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法通过
all()
实现多个请求合并在一起,汇总所有请求结果,只需设置一个loading
即可race():只要
p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。率先改变的 Promise 实例的返回值则传递给p
的回调函数javascriptconst p = Promise.race([p1, p2, p3]);
1通过
race
可以设置图片请求超时allSettled():等到所有这些参数实例都返回结果,不管是
fulfilled
还是rejected
,包装实例才会结束。resolve():将现有对象转为
Promise
对象。分四种情况(reject()
同理)- 参数是一个 Promise 实例,
promise.resolve
将不做任何修改、原封不动地返回这个实例 - 参数是一个
thenable
对象,promise.resolve
会将这个对象转为Promise
对象,然后就立即执行thenable
对象的then()
方法 - 参数不是具有
then()
方法的对象,或根本就不是对象,Promise.resolve()
会返回一个新的 Promise 对象,状态为resolved
- 没有参数时,直接返回一个
resolved
状态的 Promise 对象
- 参数是一个 Promise 实例,
async
实质是Generator
的语法糖,相当于会自动执行Generator
函数。async
使用上更为简洁,将异步代码以同步的形式进行编写,是处理异步编程的最终方案。Proxy
用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。其语法var proxy = new Proxy(target, handler)
,target
表示所要拦截的目标对象,handler
通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理proxy
的行为- get(target,propKey,receiver):拦截对象属性的读取
- set(target,propKey,value,receiver):拦截对象属性的设置
- has(target,propKey):拦截
propKey in proxy
的操作,返回一个布尔值 - deleteProperty(target,propKey):拦截
delete proxy[propKey]
的操作,返回一个布尔值 - ownKeys(target):拦截
Object.keys(proxy)
、for...in
等循环,返回一个数组
一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用
export
关键字输出该变量。当加载整个模块的时候,需要用到星号*
。ES6
中Decorator
是一个普通的函数,用于扩展类属性和类方法。装饰者模式的核心思想是在不改变原有对象结构的情况下,扩展其功能。使用
Decorator
两大优点:- 代码可读性变强了,装饰器命名相当于一个注释
- 在不改变原有代码情况下,对原来功能进行扩展
TS
TypeScript 官方提供的编译器叫做 tsc,可以将 TypeScript 脚本编译成 JavaScript 脚本。本机想要编译 TypeScript 代码,必须安装 tsc。 为了保证编译结果能在各种 JavaScript 引擎运行,tsc 默认会将 TypeScript 代码编译成很低版本的 JavaScript,即 3.0 版本(以es3表示)。这通常不是我们想要的结果。这时可以使用--target参数,指定编译后的 JavaScript 版本。建议使用es2015,或者更新版本。
TypeScript 允许将tsc的编译参数,写在配置文件tsconfig.json。只要当前目录有这个文件,tsc就会自动读取。运行时只需输入tsc
TypeScript 有两个“顶层类型”(any和unknown),但是“底层类型”只有never唯一一个。 只有经过“类型缩小”,unknown类型变量才可以使用。
typescriptlet a: unknown = 1; if (typeof a === "number") { let r = a + 10; // 正确 }
1
2
3
4
5底层类型一般用于表示不可能出现的情况
typescripttype Result<T> = | { type: "success"; value: T } | { type: "error"; message: string }; function handleResult(result: Result<number>) { if (result.type === "success") { console.log(result.value); } else if (result.type === "error") { console.error(result.message); } else { // 其他情况应不可能存在 const _impossible: never = result; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14TypeScript 对五种原始类型分别提供了大写和小写两种类型。
- Boolean 和 boolean
- String 和 string
- Number 和 number
- BigInt 和 bigint
- Symbol 和 symbol
其中,大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象。
大写的Object类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是Object类型,这囊括了几乎所有的值。空对象{}是Object类型的简写形式。 小写的object类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值。
TypeScript 规定,单个值也是一种类型,称为“值类型”。
JavaScript 的 typeof 遵守 JavaScript 规则(值),TypeScript 的 typeof 遵守 TypeScript 规则(类型)。它们的一个重要区别在于,编译后,前者会保留,后者会被全部删除。
TypeScript 不会对数组边界进行检查,越界访问数组并不会报错。
当初始是空数组时,类型为any [],TypeScript 会随着数组变化自动修改推断的数组类型。
TypeScript 允许声明只读数组,方法是在数组类型前面加上readonly关键字 TypeScript 将readonly number[]与number[]视为两种不一样的类型,后者是前者的子类型。
Symbol 是 ES2015 新引入的一种原始类型的值。它类似于字符串,但是每一个 Symbol 值都是独一无二的,与其他任何值都不相等。 Symbol 值通过Symbol()函数生成。在 TypeScript 里面,Symbol 的类型使用symbol表示。 TypeScript 设计了symbol的一个子类型unique symbol,它表示单个的、某个具体的 Symbol 值。因为unique symbol表示单个值,所以这个类型的变量是不能修改值的,只能用const命令声明,不能用let声明。
函数类型可以采用箭头函数的形式还可以采用对象的写法。
typescripttype t = { (txt: string): void; //version: string; } type h = (txt: string) => void;
1
2
3
4
5
6后者比前者的好处在于可以使用其他属性
一个函数的返回值还是一个函数,那么前一个函数就称为高阶函数(higher-order function)。
typescript(someValue: number) => (multiplier: number) => someValue * multiplier;
1函数重载:TypeScript 对于“函数重载”的类型声明方法是,逐一定义每一种情况的类型。
typescriptfunction reverse(str: string): string; function reverse(arr: any[]): any[]; function reverse(stringOrArray: string | any[]): string | any[] { if (typeof stringOrArray === "string") return stringOrArray.split("").reverse().join(""); else return stringOrArray.slice().reverse(); }
1
2
3
4
5
6
7上面示例中,分别对函数
reverse()
的两种参数情况,给予了类型声明。但是,到这里还没有结束,后面还必须对函数reverse()
给予完整的类型声明。 重载声明的排序很重要,因为 TypeScript 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明。
对象类型和接口
一旦声明了类型,对象赋值时,就不能缺少指定的属性,也不能有多余的属性。
如果某个属性是可选的(即可以忽略),需要在属性名后面加一个问号。可选属性等同于允许赋值为
undefined
。属性名前面加上readonly
关键字,表示这个属性是只读属性,不能修改。对象后面加了只读断言
as const
,就变成只读对象了,不能修改属性了。注意,上面的
as const
属于 TypeScript 的类型推断,如果变量明确地声明了类型,那么 TypeScript 会以声明的类型为准。typescriptconst myUser: { name: string } = { name: "Sabrina", } as const; myUser.name = "Cynthia"; // 正确
1
2
3
4
5有些时候,无法事前知道对象会有多少属性,比如外部 API 返回的对象。这时 TypeScript 允许采用属性名表达式的写法来描述类型,称为“属性名的索引类型”,或属性索引。
typescripttype MyObj = { [property: string]: string; }; const obj: MyObj = { foo: "a", bar: "b", baz: "c", };
1
2
3
4
5
6
7
8
9JavaScript 对象的属性名(即上例的
property
)的类型有三种可能,除了上例的string
,还有number
和symbol
。如果属性名为number
还可以表示数组。但这样表示的数组不如内置类型,损失了很多方法。对象类型或接口的方法可以这样写:
typescript//匿名 interface a { (x: boolean): string; } //具名 // 写法一 interface A { f(x: boolean): string; } // 写法二 interface B { f: (x: boolean) => string; } // 写法三 interface C { f: { (x: boolean): string }; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20解构赋值用于直接从对象中提取属性。
const { id, name, price } = product;
需要注意目前没法为解构变量指定类型,因为解构变量冒号后面代表解构出来的变量的值typescriptlet { x: foo, y: bar } = obj; // 等同于 let foo = obj.x; let bar = obj.y;
1
2
3
4
5“结构类型”原则:如果对象 A 中的属性对象 B 中都有,那么可以在使用对象 A 的地方替换为 B 而不会产生任何类型错误。对象 A 与对象 B 互为父子类型。
严格字面量检查
如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查(strict object literal checking)。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错。
typescriptconst point: { x: number; y: number; } = { x: 1, y: 1, z: 1, // 报错 };
1
2
3
4
5
6
7
8上面示例中,等号右边是一个对象的字面量,这时会触发严格字面量检查。只要有类型声明中不存在的属性(本例是
z
),就会导致报错。如果等号右边不是字面量,而是一个变量,根据结构类型原则,是不会报错的。
TypeScript 对字面量进行严格检查的目的,主要是防止拼写错误。
最小可选属性规则:如果一个对象的所有属性都是可选的,必须至少存在一个可选属性,不能所有可选属性都不存在。
空对象作为类型,其实是
Object
类型的简写形式。各种类型的值(除了null
和undefined
)都可以赋值给空对象类型,跟Object
类型的行为是一样的。如果想强制使用没有任何属性的对象,可以采用下面的写法。
typescriptinterface WithoutProperties { [key: string]: never; } // 报错 const a: WithoutProperties = { prop: 1 };
1
2
3
4
5
6Interface 和 type 两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。interface 与 type 的区别有下面几点。
(1)
type
能够表示非对象类型,而interface
只能表示对象类型(包括数组、函数等)。(2)
interface
可以继承其他类型,type
不支持继承。继承的主要作用是添加属性,type
定义的对象类型如果想要添加属性,只能使用&
运算符,重新定义一个类型。(3)同名
interface
会自动合并,同名type
则会报错。也就是说,TypeScript 不允许使用type
多次定义同一个类型。(4)
interface
不能包含属性映射(mapping),type
可以。(5)
this
关键字只能用于interface
。(6)type 可以扩展原始数据类型,interface 不行。
(7)
interface
无法表达某些复杂类型(比如交叉类型和联合类型),但是type
可以。interface 里面的函数重载,不需要给出实现。但是,由于对象内部定义方法时,无法使用函数重载的语法,所以需要额外在对象外部给出函数方法的实现。
typescriptinterface A { f(): number; f(x: boolean): boolean; f(x: string, y: string): string; } function MyFunc(): number; function MyFunc(x: boolean): boolean; function MyFunc(x: string, y: string): string; function MyFunc(x?: boolean | string, y?: string): number | boolean | string { if (x === undefined && y === undefined) return 1; if (typeof x === "boolean" && y === undefined) return true; if (typeof x === "string" && typeof y === "string") return "hello"; throw new Error("wrong parameters"); } const a: A = { f: MyFunc, };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19interface 可以使用
extends
关键字,继承其他 interface。interface 允许多重继承,实际上相当于多个父接口的合并。如果子接口与父接口存在同名属性,那么子接口的属性会覆盖父接口的属性。并且子接口和父借口的类型不可冲突。 interface 也可以继承type
命令定义的对象类型。接口合并:当有同名接口时,会自动合并。同名接口合并时,同一个属性如果有多个类型声明,彼此不能有类型冲突。 同名接口合并时,如果同名方法有不同的类型声明,那么会发生函数重载。而且,后面的定义比前面的定义具有更高的优先级。这个规则有一个例外。同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级。
typescriptinterface Document { createElement(tagName: any): Element; } interface Document { createElement(tagName: "div"): HTMLDivElement; createElement(tagName: "span"): HTMLSpanElement; } interface Document { createElement(tagName: string): HTMLElement; createElement(tagName: "canvas"): HTMLCanvasElement; } // 等同于 interface Document { createElement(tagName: "canvas"): HTMLCanvasElement; createElement(tagName: "div"): HTMLDivElement; createElement(tagName: "span"): HTMLSpanElement; createElement(tagName: string): HTMLElement; createElement(tagName: any): Element; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
类
类中声明的属性,如果不给出类型,TypeScript 会认为类型都是
any
。如果声明时给出初值,可以不写类型,TypeScript 会自行推断属性的类型。属性名前面加上 readonly 修饰符,就表示该属性是只读的。实例对象不能修改这个属性。仅可在构造函数修改这个属性。
类的方法跟普通函数一样,可以使用参数默认值,以及函数重载。构造方法不能声明返回值类型,否则报错,因为它总是返回实例对象。
存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。它们用于读写某个属性,取值器用来读取属性,存值器用来写入属性。如果某个属性只有
get
方法,没有set
方法,那么该属性自动成为只读属性。类允许定义属性索引。由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。
interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。(施加限制) 它们只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明。
typescriptinterface Country { name: string; capital: string; } // 或者 type Country = { name: string; capital: string; }; class MyCountry implements Country { name = ""; capital = ""; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14implements
关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。(实现接口)要求前面的类实现所有的属性和方法。TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。
类名只能表示实例的类型,类本身的类型需要用
typeof
来获取。类的自身类型就是一个构造函数类型,可以单独定义一个接口来表示。typescriptinterface PointConstructor { new (x: number, y: number): Point; } function createPoint( PointClass: PointConstructor, x: number, y: number ): Point { return new PointClass(x, y); }
1
2
3
4
5
6
7
8
9
10
11**如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。**同样,类也遵循“结构类型原则”。
类继承:类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法,不需要在类中再次定义。如果再次定义,会被覆盖掉,但是可以用
super
来指代基类。
ES2022 标准的 Class Fields 部分,与早期的 TypeScript 实现不一致,导致子类的那些只设置类型、没有设置初值的顶层成员在基类中被赋值后,会在子类被重置为
undefined
public
修饰符是默认修饰符,如果省略不写,实际上就带有该修饰符。注意,子类不能定义父类私有成员的同名成员。TypeScript 对于访问私有成员没有严格禁止,使用方括号写法(
[]
)或者in
运算符,实例对象就能访问该成员。ES 6 引入了更合理私有成员#propName
解决了这一问题。构造方法也可以是私有的,这就直接防止了使用
new
命令生成实例对象,只能在类的内部创建实例对象。可以用这一特性实现单例模式。typescriptclass Single{ private static instance?: Single; private readonly _str?: string; private constructor(str: string) { this._str = str; } get str():string{ return this._str ? this._str : ""; } static getInstance(_str: string): Single{ if(!Single.instance){ return Single.instance = new Single(_str); } return Single.instance; } } let a = Single.getInstance("123"); let b = Single.getInstance("frefef"); console.log(a.str);//123 console.log(b.str);//123
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25TypeScript 允许在类的定义前面,加上关键字
abstract
,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abastract class)。抽象类只能当作基类使用,用来在它的基础上定义子类。 抽象类中可以有抽象成员,它代表该属性或者方法需要子类实现。抽象成员前也不能有private
修饰符,否则无法在子类中实现该成员。抽象成员不能给出默认定义。This 问题:
有些场合需要给出
this
类型,但是 JavaScript 函数通常不带有this
参数,这时 TypeScript 允许函数增加一个名为this
的参数,放在参数列表的第一位,用来描述函数内部的this
关键字的类型。typescriptclass A { name = "A"; getName(this: A) { return this.name; } } const m = new F(); m.getName(); //可以 const n = m.getName() n(); //报错
1
2
3
4
5
6
7
8
9
10
11
12上面示例中,类A的getName()添加了this参数,如果直接调用这个方法,this的类型就会跟声明的类型不一致,从而报错。
在类的内部,
this
本身也可以当作类型使用,表示当前类的实例对象。set()
方法的返回值类型就是this
。有些方法返回一个布尔值,表示当前的
this
是否属于某种类型。这时,这些方法的返回值类型可以写成this is Type
的形式,其中用到了is
运算符。typescriptclass FileSystemObject { isFile(): this is FileRep { return this instanceof FileRep; } isDirectory(): this is Directory { return this instanceof Directory; } }
1
2
3
4
5
6
7
8
其他
泛型就是带有“类型参数”(type parameter)。类型参数可以用等号设置默认值。
函数泛型:在返回值和参数中均可使用
javascriptfunction getFirst<T>(arr: T[]): T { return arr[0]; }
1
2
3接口泛型:主要有两种,写在大括号外面对整个接口都可用,写在大括号内部只能对应使用。
typescript//写法1 interface Box<Type> { contents: Type; } let box: Box<string>; //写法2 interface Fn { <Type>(arg: Type): Type; }
1
2
3
4
5
6
7
8
9
10
11类泛型:泛型类的类型参数写在类名后面。继承时必须声明父类泛型的类型。
javascriptclass Pair<K, V> { key: K; value: V; }
1
2
3
4注意,泛型类描述的是类的实例,不包括静态属性和静态方法,因为这两者定义在类的本身。因此,它们不能引用类型参数。
类型泛型:type 命令定义的类型别名,也可以使用泛型。
typescripttype Nullable<T> = T | undefined | null;
1
泛型可用
extend
关键字约束tstype cons = number | string; function getFirst<T extends cons>(arr: T[]): T { return arr[0]; }
1
2
3
4Enum 结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。 Enum 结构本身也是一种类型。比如,上例的变量
c
等于1
,它的类型可以是 Color,也可以是number
。typescriptenum Operator { ADD, DIV, MUL, SUB, }
1
2
3
4
5
6由于 Enum 结构编译后是一个对象,所以不能有与它同名的变量(包括对象、函数、类等)。
Enum 成员可以是字符串和数值混合赋值。注意,字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前。
枚举的本质
枚举的本质是字符串和数字的正反映射
tsenum Direction { Up, Down, Left, Right }
1
2
3
4
5
6编译为 js
typescriptvar Direction; (function (Direction) { Direction[Direction["Up"] = 0] = "Up"; Direction[Direction["Down"] = 1] = "Down"; Direction[Direction["Left"] = 2] = "Left"; Direction[Direction["Right"] = 3] = "Right"; })(Direction || (Direction = {}));
1
2
3
4
5
6
7类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。类型断言的方式是:
expr as T
。对于那些可能为空的变量(即可能等于
undefined
或null
),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!
。typescriptvalidateNumber(x); // 自定义函数,确保 x 是数值 console.log(x!.toFixed());
1
2
declare 关键字用来告诉编译器,某个类型是存在的,可以在当前文件中使用。它的主要作用,就是让当前文件可以使用其他文件声明的类型。
使用类型映射,就可以从类型
A
得到类型B
。关键技巧是in keyof
:typescripttype A = { foo: number; bar: number; }; type B = { [prop in keyof A]: string; };
1
2
3
4
5
6
7
8还有键名映射 [p in keyof A as `${p}ID`]
tsc 基本用法:
bash# 使用 tsconfig.json 的配置 tsc # 只编译 index.ts tsc index.ts # 编译 src 目录的所有 .ts 文件 tsc src/*.ts # 指定编译配置文件 tsc --project tsconfig.production.json # 只生成类型声明文件,不编译出 JS 文件 tsc index.js --declaration --emitDeclarationOnly # 多个 TS 文件编译成单个 JS 文件 tsc app.ts util.ts --target esnext --outfile index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
常考点
接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法。简单来讲,一个接口所描述的是一个对象相关的属性和方法,但并不提供具体创建此对象实例的方法。接口信息会在编译为
javascript
之后去掉。在
ES6
之后,JavaScript
拥有了class
关键字,虽然本质依然是构造函数,但是使用起来已经方便了许多。但是JavaScript
的class
依然有一些特性还没有加入,比如修饰符和抽象类。TypeScript
的class
支持面向对象的所有特性,比如 类、接口等TypeScript
与ECMAScript
2015 一样,任何包含顶级import
或者export
的文件都被当成一个模块相反地,如果一个文件不带有顶级的
import
或者export
声明,那么它的内容被视为全局可见的命名空间本质上是一个对象,作用是将一系列相关的全局变量组织到一个对象的属性。
CSS
(TODO:CSS 排版)
引入 css :
<link rel="stylesheet" href="styles.css" />
如果一个浏览器在解析你所书写的 CSS 规则的过程中遇到了无法理解的属性或者值,它会忽略这些并继续解析下面的 CSS 声明。当浏览器遇到无法解析的选择器的时候,他会直接忽略整个选择器规则,然后解析下一个 CSS 选择器。
CSS 选择器:
classa.classb
是选择所有同时拥有classa
和classb
的元素。,
是将不同的选择器组合在一起的方法,它选择所有能被列表中的任意一个选择器选中的节点。- “ ”(空格)组合器选择前一个元素的后代节点。
>
组合器选择前一个元素的直接子代的节点。~
组合器选择兄弟元素,也就是说,后一个节点在前一个节点后面的任意位置,并且共享同一个父节点。+
组合器选择相邻元素,即后一个元素紧跟在前一个之后,并且共享同一个父节点。:
伪选择器支持按照未被包含在文档树中的状态信息来选择元素。::
伪选择器用于表示无法用 HTML 语义表达的实体。
选择器的优先级:
!important
> 内联的style
> ID 选择器 > 类选择器 > 元素选择器。如果都有的话按位加分。
盒子模型
外部/内部显示类型
- 在 CSS 中,我们有几种类型的盒子,一般分为区块盒子(block boxes)和行内盒子(inline boxes)。类型指的是盒子在页面流中的行为方式以及与页面上其他盒子的关系。盒子有内部显示(inner display type)和外部显示(outer display type)两种类型。
外部显示类型:block
和 inline
内部显示类型:flex
和 grid
,还可用 inline-flex
等。
一个拥有 block
外部显示类型的盒子会表现出以下行为:
- 盒子会产生换行。
width
和height
属性可以发挥作用。- 内边距、外边距和边框会将其他元素从当前盒子周围“推开”。
- 如果未指定
width
,方框将沿行向扩展,以填充其容器中的可用空间。在大多数情况下,盒子会变得与其容器一样宽,占据可用空间的 100%。
某些 HTML 元素,如
<h1>
和<p>
,默认使用block
作为外部显示类型。
一个拥有 inline
外部显示类型的盒子会表现出以下行为:
- 盒子不会产生换行。
width
和height
属性将不起作用。- 垂直方向的内边距、外边距以及边框会被应用但是不会把其他处于
inline
状态的盒子推开。 - 水平方向的内边距、外边距以及边框会被应用且会把其他处于
inline
状态的盒子推开。
某些 HTML 元素,如
<a>
、<span>
、<em>
以及<strong>
,默认使用inline
作为外部显示类型。
一个元素使用 display: inline-block
,实现我们需要的块级的部分效果:
- 设置
width
和height
属性会生效。 padding
、margin
和border
会推开其他元素。- 但是不会换行。
使用 box-sizing
属性可以指定元素的盒子模型:
box-sizing: content-box;
:使用标准盒子模型(默认)(宽高不算padding 和 border)。box-sizing: border-box;
:使用IE盒子模型。
外边距折叠
外边距折叠:根据外边距相接触的两个元素是正边距还是负边距,结果会有所不同:
- 两个正外边距将合并为一个外边距。其大小等于最大的单个外边距。
- 两个负外边距会折叠,并使用最小(离零最远)的值。
- 如果其中一个外边距为负值,其值将从总值中减去。
- 外边距折叠仅与垂直方向有关。
display
设置为flex
或grid
的容器中不会发生外边距折叠。
可以将某个元素设置为
overflow: hidden;
或display: flow-root;
来创建新的 BFC,从而阻止外边距折叠。使用百分比作为元素外边距(margin)或填充(padding)的单位时,值是以包含块的内联尺寸进行计算的,也就是元素的水平宽度。
溢出
scroll
visible
(默认):内容不能被裁减并且可能渲染到边距盒(padding)的外部。hidden
:如果需要,内容将被裁减以适应边距(padding)盒。不提供滚动条,也不支持允许用户滚动(例如通过拖拽或者使用滚轮)。内容可以以编程的方式滚动(例如,通过设置scrollLeft
等属性的值或scrollTo()
方法), 因此该元素仍然是一个滚动的容器。clip
:类似于hidden
,内容将以元素的边距(padding)盒进行裁剪。clip
和hidden
之间的区别是clip
关键字禁止所有滚动,包括以编程方式的滚动。该盒子不是一个滚动的容器,并且不会启动新的格式化上下文。如果你希望开启一个新的格式化上下文,你可以使用display: flow-root
来这样做。scroll
:如果需要,内容将被裁减以适应边距(padding)盒。无论是否实际裁剪了任何内容,浏览器总是显示滚动条,以防止滚动条在内容改变时出现或者消失。打印机可能会打印溢出的内容。auto
:取决于用户代理。如果内容适应边距(padding)盒,它看起来与visible
相同,但是仍然建立了一个新的块级格式化上下文。如果内容溢出,则浏览器提供滚动条。overlay
已弃用:行为与auto
相同,但是滚动条绘制在内容之上,而不是占据空间。
定位
大多数情况下,height
和width
被设定为 auto 的绝对定位(fixed
和 absolute
)元素,按其内容大小调整尺寸。但是,被绝对定位的元素可以通过指定top
和bottom
,保留height
未指定(即auto
),来填充可用的垂直空间。它们同样可以通过指定left
和 right
并将width
指定为auto
来填充可用的水平空间。
各个关键字:
static
(最特殊、默认):该关键字指定元素使用正常的布局行为,即元素在文档常规流中当前的布局位置。此时 top, right, bottom, left 和 z-index 属性无效。relative
:该关键字下,元素先放置在未添加定位(仍在文档流中)的位置,再在不改变页面布局的前提下调整元素位置。absolute
:元素会被移出正常文档流,并不为元素预留空间,通过指定元素相对于最近的非 static 定位祖先(非未定位)元素的偏移,来确定元素位置。绝对定位的元素设置的外边距不会与其他边距合并。fixed
:元素会被移出正常文档流,并不为元素预留空间。与absolute
的区别是通过指定元素相对于屏幕视口(viewport)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变。打印时,元素会出现在的每页的固定位置。fixed
属性会创建新的层叠上下文。当元素祖先的transform
、perspective
、filter
或backdrop-filter
属性非none
时,容器由视口改为该祖先。sticky
:元素根据正常文档流进行定位,然后相对于最近的滚动祖先和包含块(nearest block container)定位。平常它表现得像position: relative
,直到元素滚动到特定的偏移位置,然后它“固定”在那里(即,它的定位行为就像position: fixed
)。
图片填充方式
图片可以采用 object-fit: cover
来裁剪铺满,contain
来完全包含(有白边),fill
来拉伸。
居中方式
块级元素
不知道子元素的高度
margin auto+absolute
:子元素采用position: absulute
,且设定top/left/right/bottom: 0
以及margin: auto
。要求父元素不能为static
。注意
子元素不采用 relative的原因:可能会外边距合并
当父元素与第一个子元素之间没有边框
border
、内边距padding
、overflow: auto/hidden
(这些属性会创建BFC,阻止外边距合并)或内容分隔时,它们的上下外边距会合并。合并后的外边距取两者中的较大值。absolute+50%定位+transform
:子元素采用position: absulute
,且设定top/left: 50%
以及transform(-50%, -50%)
。要求父元素不能为static
。flex
:父元素采用display: flex
,并设置align-items: center
和justify-content: center
grid
:同flex
知道子元素的高度
absolute+50%定位+50%自身margin
:子元素采用position: absulute
,且设定top/left: 50%
以及margin:自身宽度和高度的-50%
。要求父元素不能为static
。
内联元素:
- 水平:
text-align
、flex/grid
- 竖直:
height === line-height
、flex/grid
- 水平:
层叠上下文
以下从上到下,越往下的元素越靠近用户:
- 层叠上下文的
border
和background
z-index < 0
的子节点- 标准流内块级非定位的子节点
- 浮动非定位的子节点
- 标准流内行内非定位的子节点
z-index: auto/0
的子节点z-index > 0
的子节点
比较方式:
- 在同一个层叠上下文中,比较两个元素就是按照上图的介绍的层叠顺序进行比较。
- 如果不在同一个层叠上下文中的时候,那就需要比较两个元素分别所处的层叠上下文的等级。
- 如果两个元素都在同一个层叠上下文,且层叠顺序相同,则在 HTML 中定义越后面的层叠等级越高。
文本溢出
单行文本溢出可以采用以下方式:
.text {
overflow: hidden; // 溢出隐藏
text-overflow: ellipsis; // 溢出用省略号显示
white-space: nowrap; // 规定段落中的文本不进行换行
}
2
3
4
5
多行文本溢出,可以通过 -webkit-line-clamp
或者伪元素实现:
.text {
overflow: hidden; // 溢出隐藏
text-overflow: ellipsis; // 溢出用省略号显示
display:-webkit-box; // 作为弹性伸缩盒子模型显示。
-webkit-line-clamp:3; // 显示的行数
-webkit-box-orient:vertical; // 设置伸缩盒子的子元素排列方式:从上到下垂直排列
}
// 加省略号
.mulLineTruncate {
position: relative;
max-height: 40px;
overflow: hidden;
line-height: 20px;
&::after {
position: absolute;
right: 0;
bottom: 0;
padding: 0 20px 0 10px;
content: "...";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Vue
核心特性:数据驱动(MVVM)、组件化、指令系统
MVVM表示的是 Model-View-ViewModel
- Model:模型层,负责处理业务逻辑以及和服务器端进行交互
- View:视图层:负责将数据模型转化为UI展示出来,可以简单的理解为HTML页面
- ViewModel:视图模型层,用来连接Model和View,是Model和View之间的通信桥梁
- view 和 viewmodel 之间的数据属性采取双向绑定,命令属性从view到viewmodel单向传递。
指令 (Directives) 是带有 v- 前缀的特殊属性作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM
new Vue()
这个过程中究竟做了些什么?new Vue
的时候调用会调用_init
方法- 定义
$set
、$get
、$delete
、$watch
等方法 - 定义
$on
、$off
、$emit
、$off
等事件 - 定义
_update
、$forceUpdate
、$destroy
生命周期
- 定义
- 调用
$mount
进行页面的挂载 - 挂载的时候主要是通过
mountComponent
方法 - 定义
updateComponent
更新函数 - 执行
render
生成虚拟DOM
_update
将虚拟DOM
生成真实DOM
结构,并且渲染到页面中
生命周期
生命周期 描述 行为 beforeCreate 组件实例被创建之初 初始化 vue
实例,进行数据观测created 组件实例已经完全创建 完成数据观测,属性与方法的运算, watch
、event
事件回调的配置。可调用methods
中的方法,访问和修改data数据触发响应式渲染dom
,可通过computed
和watch
完成数据计算beforeMount 组件挂载之前 在此阶段可获取到 vm.el
此阶段vm.el
虽已完成DOM初始化,但并未挂载在el
选项上mounted 组件挂载到实例上去之后 vm.el
已完成DOM
的挂载与渲染,此刻打印vm.$el
,发现之前的挂载点及内容已被替换成新的DOMbeforeUpdate 组件数据发生变化,更新之前 更新的数据必须是被渲染在模板上的( el
、template
、render
之一)
此时view
层还未更新
若在beforeUpdate
中再次修改数据,不会再次触发更新方法updated 组件数据更新之后 完成 view
层的更新
若在updated
中再次修改数据,会再次触发更新方法(beforeUpdate
、updated
)beforeDestroy 组件实例销毁之前 实例被销毁前调用,此时实例属性与方法仍可访问 destroyed 组件实例销毁之后 完全销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
并不能清除DOM,仅仅销毁实例数据请求在created和mouted的区别:讨论这个问题本质就是触发的时机,放在
mounted
中的请求有可能导致页面闪动(因为此时页面dom
结构已经生成),但如果在页面加载前完成请求,则不会出现此情况。建议对页面内容的改动放在created
生命周期当中。永远不要把
v-if
和v-for
同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断),而是应该:外层判断:
vue<template v-if="isShow"> <p v-for="item in items"> </template>
1
2
3内层判断:
javascriptcomputed: { items: function() { return this.list.filter(function (item) { return item.isShow }) } }
1
2
3
4
5
6
7首屏加载提速
减小入口文件体积:动态加载路由
静态资源本地缓存:Http 缓存,如设置
cache-control
、last-modified
和etag
之类的。还可以利用localStorage
。UI框架按需加载:引用库的时候按需饮用。
图片资源的压缩:雪碧图,矢量图
组件重复打包:在
webpack
的config
文件中,修改CommonsChunkPlugin
的配置javascriptminChunks: 3
1开启GZip压缩
使用SSR
根实例对象
data
可以是对象也可以是函数(根实例是单例),不会产生数据污染情况;组件实例对象data
必须为函数,目的是为了防止多个组件实例对象之间共用一个data
,产生数据污染。采用函数的形式,initData
时会将其作为工厂函数都会返回全新data
对象。动态给vue的data添加一个新的属性时会发生什么? vue用
Object.defineProperty
实现数据响应式。当我们访问foo
属性或者设置foo
值的时候都能够触发setter
与getter
。但是我们为obj
添加新属性的时候,却无法触发事件属性的拦截。原因是一开始obj
的foo
属性被设成了响应式数据,而bar
是后面新增的属性,并没有通过Object.defineProperty
设置成响应式数据。javascriptconst obj = {} Object.defineProperty(obj, 'foo', { get() { console.log(`get foo:${val}`); return val }, set(newVal) { if (newVal !== val) { console.log(`set foo:${newVal}`); val = newVal } } }) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14组件
(Component)
是用来构成你的App
的业务模块,它的目标是App.vue
。主要分为全局注册与局部注册。插件
(Plugin)
是用来增强你的技术栈的功能模块,它的目标是Vue
本身。通过Vue.use()
的方式进行注册(安装)。注意,注册插件的时候,需要在调用new Vue()
启动应用之前完成。组件间通信的方案:通过 props 传递、通过 $emit 触发自定义事件、使用 ref、Provide 与 Inject,全局reactive对象。
javascript// father component provide(){ return { foo:'foo' } } // children component inject:['foo'] // 获取到祖先组件传递过来的值
1
2
3
4
5
6
7
8
9双向绑定: 主要有两个部分:监听器(对所有数据的属性进行监听)和解析器(对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数)。
new Vue()
首先执行初始化,对data
执行响应化处理,这个过程发生Observe
中- 同时对模板执行编译,找到其中动态绑定的数据,从
data
中获取并初始化视图,这个过程发生在Compile
中 - 同时定义⼀个更新函数和
Watcher
,将来对应数据变化时Watcher
会调用更新函数 - 由于
data
的某个key
在⼀个视图中可能出现多次,所以每个key
都需要⼀个管家Dep
来管理多个Watcher
- 将来data中数据⼀旦发生变化,会首先找到对应的
Dep
,通知所有Watcher
执行更新函数
数据在发现变化的时候,
vue
并不会立刻去更新Dom
,而是将修改数据的操作放在了一个异步操作队列中。如果我们一直修改相同数据,异步操作队列还会进行去重。等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行DOM
的更新 如果想要在修改数据后立刻得到更新后的DOM
结构,可以使用Vue.nextTick()
。组件内使用vm.$nextTick()
实例方法只需要通过this.$nextTick()
,并且回调函数中的this
将自动绑定到当前的Vue
实例上。javascriptthis.message = '修改后的值' console.log(this.$el.textContent) // => '原始的值' await this.$nextTick() console.log(this.$el.textContent) // => '修改后的值'
1
2
3
4Mixin
类通常作为功能模块使用,在需要该功能时“混入”,有利于代码复用又避免了多继承的复杂。本质其实就是一个js
对象,它可以包含我们组件中任意功能选项,如data
、components
、methods
、created
、computed
等等。我们只要将共用的功能以对象的方式传入mixins
选项中,当组件使用mixins
对象时所有mixins
对象的选项都将被混入该组件本身的选项中来。父组件中在使用时在默认插槽的基础上加上
slot
属性,值为子组件插槽name
属性值vue// Child.vue <template> <slot>插槽后备的内容</slot> <slot name="content">插槽后备的内容</slot> </template> // Father.vue <child> <template v-slot:default>具名插槽</template> <!-- 具名插槽⽤插槽名做参数 --> <template v-slot:content>内容...</template> </child>
1
2
3
4
5
6
7
8
9
10
11
12observable:在非父子组件通信时,可以使用通常的
bus
或者使用vuex
,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,observable
就是一个很好的选择。observable
返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器。当我们在某些场景下不需要让页面重新加载时我们可以使用
keepalive
。当我们从首页
–>列表页
–>商详页
–>再返回
,这时候列表页应该是需要keep-alive
事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符:
- stop:阻止了事件冒泡,相当于调用了
event.stopPropagation
方法 - prevent:阻止了事件的默认行为,相当于调用了
event.preventDefault
方法 - self:只当在
event.target
是当前元素自身时触发处理函数 - once:绑定了事件以后只能触发一次,第二次就不会触发
- capture:使事件触发从包含这个元素的顶层开始往下触发
- passive:在移动端,当我们在监听元素滚动事件的时候,会一直触发
onscroll
事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll
事件整了一个.lazy
修饰符
- stop:阻止了事件冒泡,相当于调用了
为什么需要虚拟 DOM?你用传统的原生
api
或jQuery
去操作DOM
时,浏览器会从构建DOM
树开始从头到尾执行一遍流程。当你在一次操作时,需要更新10个DOM
节点,浏览器没这么智能,收到第一个更新DOM
请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程而通过
VNode
,同样更新10个DOM
节点,虚拟DOM
不会立即操作DOM
,而是将这10次更新的diff
内容保存到本地的一个js
对象中,最终将这个js
对象一次性attach
到DOM
树上,避免大量的无谓计算。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI
目录结构:
- 就近原则,紧耦合的文件应该放到一起,且应以相对路径引用
- 公共的文件应该以绝对路径的方式从根目录引用
项目结构
my-vue-test:.
│ .browserslistrc
│ .env.production
│ .eslintrc.js
│ .gitignore
│ babel.config.js
│ package-lock.json
│ package.json
│ README.md
│ vue.config.js
│ yarn-error.log
│ yarn.lock
│
├─public
│ favicon.ico
│ index.html
│
└─src
├─apis //接口文件根据页面或实例模块化
│ index.js
│ login.js
│
├─components //全局公共组件
│ └─header
│ index.less
│ index.vue
│
├─config //配置(环境变量配置不同passid等)
│ env.js
│ index.js
│
├─contant //常量
│ index.js
│
├─images //图片
│ logo.png
│
├─pages //多页面vue项目,不同的实例
│ ├─index //主实例
│ │ │ index.js
│ │ │ index.vue
│ │ │ main.js
│ │ │ router.js
│ │ │ store.js
│ │ │
│ │ ├─components //业务组件
│ │ └─pages //此实例中的各个路由
│ │ ├─amenu
│ │ │ index.vue
│ │ │
│ │ └─bmenu
│ │ index.vue
│ │
│ └─login //另一个实例
│ index.js
│ index.vue
│ main.js
│
├─scripts //包含各种常用配置,工具函数
│ │ map.js
│ │
│ └─utils
│ helper.js
│
├─store //vuex仓库
│ │ index.js
│ │
│ ├─index
│ │ actions.js
│ │ getters.js
│ │ index.js
│ │ mutation-types.js
│ │ mutations.js
│ │ state.js
│ │
│ └─user
│ actions.js
│ getters.js
│ index.js
│ mutation-types.js
│ mutations.js
│ state.js
│
└─styles //样式统一配置
│ components.less
│
├─animation
│ index.less
│ slide.less
│
├─base
│ index.less
│ style.less
│ var.less
│ widget.less
│
└─common
index.less
reset.less
style.less
transition.less
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
权限管理:
- 接口权限:使用jwt,验证token
- 路由权限:
router.beforeEach((to, from, next) => {})
- 菜单权限:写成组件,然后同样做路由权限。
- 按钮权限:
v-if
/自定义指令
跨域问题解决:
- 后端:CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应
- 前端:网络代理
404问题:
Vue
是属于单页应用(single-page application)。而SPA
是一种网络应用程序或网站的模型,所有用户交互是通过动态重写当前页面,前面我们也看到了,不管我们应用有多少页面,构建物都只会产出一个index.html
。当输入/login
时,由于nginx
没有配置相关的location
,因此会出现问题。并且hash
模式没有问题(hash
虽然出现在URL
中,但不会被包括在HTTP
请求中,对服务端完全没有影响,因此改变hash
不会重新加载页面)Vue3的特性:
- 速度更快:重写了虚拟
Dom
实现,更高效的组件初始化,ssr优化等。 - 体积减少:
webpack
的tree-shaking
功能,可以将无用模块“剪辑”,仅打包需要的。 - 更易维护:组合式api,支持ts
- 更接近原生
- 更易使用
- 速度更快:重写了虚拟
计网
以下两个属于MAC广播地址
FF:FF:FF:FF:FF:FF
255.255.255.255
在网络包传输的过程中,源 IP 和目标 IP 始终是不会变的,一直变化的是 MAC 地址,因为需要 MAC 地址在以太网内进行两个设备之间的包传输。
内核中的 ksoftirqd 线程专门负责软中断的处理,当 ksoftirqd 内核线程收到软中断后,就会来轮询处理数据。 ksoftirqd 线程会从 Ring Buffer 中获取一个数据帧,用 sk_buff 表示,从而可以作为一个网络包交给网络协议栈进行逐层处理。 在使用 TCP 传输协议的情况下,从传输层进入网络层的时候,每一个 sk_buff 都会被克隆一个新的副本出来。副本 sk_buff 会被送往网络层,等它发送完的时候就会释放掉,然后原始的 sk_buff 还保留在传输层,目的是为了实现 TCP 的可靠传输,等收到这个数据包的 ACK 时,才会释放原始的 sk_buff 。
GET 的语义是请求获取指定的资源。GET 方法是安全、幂等、可被缓存的。
POST 的语义是根据请求负荷(报文主体)对指定的资源做出处理,具体的处理方式视资源类型而不同。POST 不安全,不幂等,(大部分实现)不可缓存。
RFC 规范并没有规定 GET 请求不能带 body 的。理论上,任何请求都可以带 body 的。只是因为 RFC 规范定义的 GET 请求是获取资源,所以根据这个语义不需要用到 body。
另外,URL 中的查询参数也不是 GET 所独有的,POST 请求的 URL 中也可以有参数的。
协商缓存可以通过时间和Etag,其中Etag的优先级更高,相对更加准确。协商缓存如果服务器说使用缓存,则状态码为304,否则为200。
对于 HTTPS 连接来说,中间人要满足以下两点,才能实现真正的明文代理: 中间人,作为客户端与真实服务端建立连接这一步不会有问题,因为服务端不会校验客户端的身份; 中间人,作为服务端与真实客户端建立连接,这里会有客户端信任服务端的问题,也就是服务端必须有对应域名的私钥;
优化 HTTP/1.1 协议
第一个思路是,通过缓存技术来避免发送 HTTP 请求。客户端收到第一个请求的响应后,可以将其缓存在本地磁盘,下次请求的时候,如果缓存没过期,就直接读取本地缓存的响应数据。
第二个思路是,减少 HTTP 请求的次数,有以下的方法:
将重定向请求,交给代理服务器处理
将多个小资源合并成一个大资源再传输
按需访问资源,只访问当前用户看得到/用得到的资源。
第三思路是,通过压缩响应资源,降低传输资源的大小,从而提高传输效率,所以应当选择更优秀的压缩算法。
HTTP/2 协议改进
- 第一点,对于常见的 HTTP 头部通过静态表和 Huffman 编码的方式,将体积压缩了近一半,而且针对后续的请求头部,还可以建立动态表,将体积压缩近 90%,大大提高了编码效率,同时节约了带宽资源。
- 第二点,HTTP/2 实现了 Stream 并发,多个 Stream 只需复用 1 个 TCP 连接,节约了 TCP 和 TLS 握手时间,以及减少了 TCP 慢启动阶段对流量的影响。不同的 Stream ID 可以并发,即使乱序发送帧也没问题。另外,可以根据资源的渲染顺序来设置 Stream 的优先级,从而提高用户体验。
- 第三点,服务器支持主动推送资源,大大提升了消息的传输性能,服务器推送资源时,会先发送 PUSH_PROMISE 帧,告诉客户端接下来在哪个 Stream 发送资源,然后用偶数号 Stream 发送资源给客户端。
HTTP/3 协议改进
- HTTP/2协议基于TCP,会产生队头阻塞和慢启动等问题。HTTP/3 就将传输层从 TCP 替换成了 UDP,并在 UDP 协议上开发了 QUIC 协议,来保证数据的可靠传输。
TCP
TCP 是有三个特点,面向连接、可靠、基于字节流。
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,如果中途丢失了一个分片,只需要传输丢失的这个分片。
UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片。
TCP第三次握手是可以携带数据的,前两次握手是不可以携带数据的。
三个方面分析三次握手的原因:
三次握手才可以阻止重复历史连接的初始化(主要原因)
三次握手才可以同步双方的初始序列号
三次握手才可以避免资源浪费
既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?因为当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。
ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
四次挥手主动发起关闭连接的一方,才会有 TIME-WAIT 状态。需要 TIME-WAIT 状态,主要是两个原因:
防止历史连接中的数据,被后面相同四元组的连接错误的接收;
保证「被动关闭连接」的一方,能被正确的关闭;
滑动窗口:TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。
关于PING:ICMP 报文是封装在 IP 包里面,它工作在网络层,是 IP 协议的助手。
杂项
- 使用了 RSA 密钥协商算法,TLS 完成四次握手后,才能进行应用数据传输,而对于 ECDHE 算法,客户端可以不用等服务端的最后一次 TLS 握手,就可以提前发出加密的 HTTP 数据,节省了一个消息的往返时间(这个是 RFC 文档规定的,具体原因文档没有说明,所以这点我也不太明白);
浏览器
1. 浏览器的主要功能
- 请求和获取资源:从服务器获取网页的HTML、CSS、JavaScript、图片、视频等资源。
- 解析和渲染:将获取的资源解析并渲染成用户可见的页面。
- 执行脚本:运行网页中的JavaScript代码,实现动态交互。
- 管理用户交互:处理用户的点击、输入、滚动等操作。
- 缓存和存储:缓存资源以提高加载速度,并提供本地存储功能(如Cookies、LocalStorage)。
2. 浏览器的主要组件
- 用户界面(UI):包括地址栏、前进/后退按钮、书签栏等用户可见的部分。
- 浏览器引擎:协调用户界面与渲染引擎之间的交互。
- 渲染引擎:负责解析HTML、CSS,并将内容渲染到屏幕上(如Chrome的Blink引擎,Firefox的Gecko引擎)。
- JavaScript引擎:解析和执行JavaScript代码(如Chrome的V8引擎)。
- 网络模块:处理网络请求,获取网页资源。
- 数据存储:管理缓存、Cookies、本地存储等数据。
3. 浏览器的工作流程
当用户在地址栏输入URL并按下回车时,浏览器会执行以下步骤:
1) 解析URL
- 浏览器解析用户输入的URL,确定协议(如HTTP/HTTPS)、域名、路径和查询参数。
2) DNS解析
- 浏览器通过DNS(域名系统)将域名解析为对应的IP地址。
3) 建立连接
- 浏览器通过TCP/IP协议与服务器建立连接(如果是HTTPS,还会进行TLS/SSL握手)。
4) 发送HTTP请求
- 浏览器向服务器发送HTTP请求,请求网页资源(如HTML文件)。
5) 接收响应
- 服务器返回HTTP响应,包含状态码(如200表示成功)和资源内容。
6) 解析和渲染
- 解析HTML:浏览器解析HTML文件,构建DOM(文档对象模型)树。
- 解析CSS:解析CSS文件,构建CSSOM(CSS对象模型)树。
- 构建渲染树:将DOM树和CSSOM树结合,生成渲染树(Render Tree)。
- 布局/回流(Layout):计算渲染树中每个元素的位置和大小。
- 绘制/重绘(Paint):将渲染树的内容绘制到屏幕上。
7) 执行JavaScript
- 浏览器解析并执行网页中的JavaScript代码,实现动态交互。
8) 处理用户交互
- 浏览器监听用户的操作(如点击、滚动),并触发相应的事件处理程序。
浏览器的多个线程
- 主线程(Main Thread):负责处理用户界面(UI)、JavaScript 执行、DOM 渲染等任务。
- 网络线程(Network Thread):专门负责处理网络请求的发送和响应的接收。
- 渲染线程(Renderer Thread):负责页面的渲染和布局。
- IO 线程(IO Thread):负责文件读写等 I/O 操作。
- GPU 线程(GPU Thread):负责图形渲染和 GPU 加速。
为什么网络请求在独立线程中处理?
将网络请求放在独立的网络线程中处理有以下好处:
- 避免阻塞主线程:网络请求通常是 I/O 密集型操作,可能会耗时较长。如果放在主线程中处理,会导致页面卡顿或无响应。
- 提高并发性:浏览器可以同时处理多个网络请求,提升页面加载速度。
- 资源隔离:网络线程与主线程分离,可以更好地管理资源,避免网络操作影响页面渲染和用户交互。
减少重排
1. 减少DOM操作
- 批量操作DOM:避免频繁地单独操作DOM元素,尽量将多个操作合并为一次。例如,使用
DocumentFragment
或innerHTML
来批量插入或更新DOM。 - 离线操作DOM:将元素从DOM树中移除,进行修改后再重新插入。这样可以避免在修改过程中触发重排。
2. 使用CSS类名切换
- 批量样式修改:通过添加或移除CSS类名来一次性修改多个样式,而不是逐个修改
style
属性。这样可以减少重绘和重排的次数。
3. 避免强制同步布局
- 避免强制布局:在JavaScript中,某些操作(如读取
offsetWidth
、offsetHeight
等)会强制浏览器进行同步布局计算。尽量避免在布局过程中读取这些属性,或者将读取操作放在修改操作之前。
4. 使用transform
和opacity
- 使用硬件加速:
transform
和opacity
属性的变化通常不会触发重排,并且可以利用GPU加速,减少重绘的开销。例如,使用transform: translate()
来移动元素,而不是修改top
和left
。
5. 优化CSS选择器
- 避免复杂选择器:复杂的CSS选择器会增加样式计算的时间,尤其是在大型DOM树中。尽量使用简单的选择器,减少样式计算的复杂度。
6. 使用will-change
属性
- 提示浏览器优化:使用
will-change
属性来提前告知浏览器哪些元素可能会发生变化,以便浏览器进行优化。例如,will-change: transform
可以提示浏览器该元素可能会进行变换操作。
杂项
Websocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的应用层协议,允许客户端和服务器之间进行实时、双向的数据传输。与传统的 HTTP 请求-响应模式不同,WebSocket 建立连接后,客户端和服务器可以随时发送数据,而不需要频繁地建立和关闭连接。
- 握手阶段:
- 客户端通过 HTTP 请求发起 WebSocket 连接,请求头中包含
Upgrade: websocket
和Connection: Upgrade
。 - 服务器响应 101 状态码,表示协议切换成功,连接升级为 WebSocket。
- 客户端通过 HTTP 请求发起 WebSocket 连接,请求头中包含
- 数据传输阶段:
- 连接建立后,客户端和服务器可以通过 WebSocket 协议发送和接收数据。
- 数据以帧(Frame)的形式传输,可以是文本帧或二进制帧。
- 连接关闭:
- 客户端或服务器可以发送关闭帧来终止连接。
- 连接关闭后,资源被释放。
优点
- 实时性:支持实时数据传输,适用于需要低延迟的应用。
- 高效性:减少了 HTTP 请求的开销,节省带宽。
- 灵活性:支持文本和二进制数据,适用于多种应用场景。
缺点
- 复杂性:相比 HTTP,WebSocket 的实现和维护更复杂。
- 兼容性:虽然现代浏览器都支持 WebSocket,但在某些旧版浏览器中可能需要降级处理。
- 资源消耗:持久连接会占用服务器资源,需要合理管理连接。
Ajax
Ajax(Asynchronous JavaScript and XML)是一种用于创建动态、异步 Web 应用的技术。它允许网页在不重新加载整个页面的情况下,与服务器进行数据交换并更新部分页面内容,从而提升用户体验和性能。
Ajax 的核心原理
异步通信
- 传统网页:用户每次操作(如点击链接、提交表单)都会触发页面刷新,需要重新加载整个页面。
- Ajax:通过 JavaScript 在后台与服务器交换数据,仅更新页面的特定部分,用户操作不会被中断。
技术组成
- JavaScript:驱动整个流程,处理用户事件、发送请求、解析响应并更新页面。
- XMLHttpRequest 对象(或现代
fetch API
):浏览器提供的 API,用于与服务器通信。 - 数据格式:早期使用 XML,现更常用 JSON、HTML 或纯文本。
- DOM 操作:通过 JavaScript 动态修改页面内容。
工作流程
- 用户触发事件(如点击按钮)。
- JavaScript 创建
XMLHttpRequest
对象或使用fetch()
。 - 向服务器发送异步请求(HTTP GET/POST)。
- 服务器处理请求并返回数据(如 JSON)。
- JavaScript 解析响应数据并更新页面 DOM。
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
document.getElementById('result').innerHTML = data.content;
})
.catch(error => console.error('Error:', error));
2
3
4
5
6
SSR
SSR(Server-Side Rendering,服务端渲染)是指将网页内容在服务器端生成完整的 HTML,再直接返回给客户端的技术。与传统的客户端渲染(CSR,如 SPA)不同,SSR 的页面初次加载时已是可呈现的内容,而非依赖浏览器执行 JavaScript 动态生成。
优点:
- 更好的 SEO 支持(爬虫直接解析 HTML)。
- 更快的首屏加载速度(减少白屏时间)。
- 兼容低端浏览器(内容无需依赖 JS 执行)。
缺点:
- 服务器压力增大(渲染消耗 CPU 资源)。
- 开发复杂度提高(需处理服务端与客户端环境差异)。
- 部分前端库可能无法兼容 SSR。
Mock.js
Mock.js
是一个用于生成随机数据、拦截 Ajax 请求并返回模拟数据的 JavaScript 库。它在前端开发中非常有用,尤其是在开发阶段,当后端 API 尚未完成时,可以使用 Mock.js
来模拟数据,从而加快开发进度。
const Mock = require('mockjs');
// 拦截 GET 请求
Mock.mock('/api/user', 'get', {
'id': 1,
'name': '@cname',
'age|18-60': 1,
'email': '@email',
'address': '@county(true)'
});
// 拦截 POST 请求
Mock.mock('/api/login', 'post', function(options) {
const { username, password } = JSON.parse(options.body);
if (username === 'admin' && password === '123456') {
return {
code: 200,
message: '登录成功',
token: 'mock-token'
};
} else {
return {
code: 401,
message: '用户名或密码错误'
};
}
});
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
// src/main.js
import Vue from 'vue';
import App from './App.vue';
import './mock'; // 引入 mock 数据
new Vue({
render: h => h(App),
}).$mount('#app');
2
3
4
5
6
7
8
进程和线程
特性 | 进程 | 线程 |
---|---|---|
定义 | 进程是操作系统分配资源的基本单位,是程序的一次执行实例。 | 线程是进程内的一个执行单元,是 CPU 调度的基本单位。 |
资源占用 | 进程拥有独立的地址空间、文件描述符、堆栈等资源,资源开销较大。 | 线程共享进程的资源(如内存、文件等),资源开销较小。 |
创建和销毁 | 进程的创建和销毁开销较大,涉及资源的分配和回收。 | 线程的创建和销毁开销较小,因为共享进程的资源。 |
独立性 | 进程之间相互独立,一个进程崩溃不会影响其他进程。 | 线程共享进程的资源,一个线程崩溃可能导致整个进程崩溃。 |
通信方式 | 进程间通信(IPC)需要通过特定的机制,如管道、消息队列、共享内存等。 | 线程间可以直接通过共享内存进行通信,效率更高。 |
上下文切换 | 进程的上下文切换开销较大,涉及地址空间、寄存器、文件描述符等的切换。 | 线程的上下文切换开销较小,因为共享地址空间和资源。 |
并发性 | 进程的并发性较低,因为创建和切换开销较大。 | 线程的并发性较高,适合处理多任务并行执行。 |
应用场景 | 适合需要高隔离性和安全性的任务,如独立的应用程序。 | 适合需要高并发和资源共享的任务,如多线程服务器、并行计算等。 |
浏览器的主要进程有:渲染进程、网络进程、GPU 进程、浏览器进程、插件进程等。
JavaScript 引擎的相关线程:主线程、事件循环线程、定时器线程、HTTP 请求线程等。
Docker 的优势
1. 轻量化和高效性
- 资源占用少:Docker 容器共享宿主机的操作系统内核,不需要为每个容器单独启动一个操作系统,因此资源占用更少。
- 启动速度快:容器启动时间通常在秒级,比传统虚拟机(VM)快得多。
- 性能接近原生:由于容器直接运行在宿主机的内核上,性能几乎与原生应用无异。
2. 环境一致性
- 开发与生产环境一致:Docker 容器将应用程序及其依赖打包在一起,确保开发、测试和生产环境的一致性,避免了“在我机器上能运行”的问题。
- 跨平台兼容性:Docker 容器可以在任何支持 Docker 的平台上运行,无论是 Linux、Windows 还是 macOS。
3. 隔离性和安全性
- 进程隔离:每个容器运行在独立的命名空间中,相互隔离,避免应用程序之间的冲突。
- 资源限制:可以为容器设置 CPU、内存等资源限制,防止单个容器占用过多资源。
- 安全性:Docker 提供了多种安全机制,如用户命名空间、Seccomp 等,增强了容器的安全性。
4. 镜像管理和版本控制
- 镜像分层:Docker 镜像采用分层存储,相同的层可以复用,减少了存储空间和传输时间。
- 版本控制:Docker 镜像可以打标签,方便版本管理和回滚。
- 镜像仓库:Docker Hub 等镜像仓库提供了丰富的公共镜像,同时支持私有仓库,方便团队共享和管理镜像。
5. 微服务架构支持
- 模块化设计:Docker 容器天然适合微服务架构,每个服务可以独立开发、部署和扩展。
- 服务发现和负载均衡:结合 Kubernetes 等工具,可以轻松实现服务发现、负载均衡和自动伸缩。
6. 持续集成和持续交付(CI/CD)
- 快速构建和测试:Docker 可以集成到 CI/CD 流水线中,实现代码的快速构建、测试和部署。
- 环境一致性:确保开发、测试和生产环境的一致性,减少部署过程中的问题。
项目
音频进度条拖动:可以通过
getBoundingClientRect()
方法找出元素左/右和上/下侧的 X 和 Y 值,而且你可以通过Document
对象调用的 click 事件的事件对象找到鼠标单击的坐标。举个例子:javascriptdocument.onclick = function (e) { console.log(e.x, e.y); };
1
2
3