tip:只记录本人记得不牢固的,或者有启发的点,新手建议多看书
实例对象的constructor也会指向构造函数
因为没有constructor属性会通过原型链找(容易忽略,是个小陷阱)
function Person() {
}
var person = new Person();
console.log(person.constructor === Person); // true
__proto__
来自于 Object.prototype,更像是一个 getter/setter,使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)
原型链继承:子函数的原型是父函数的实例对象。
缺点不能传参,引用属性共享
构造函数继承:子函数中通过call调用父函数,改变this
缺点:每次都要调用父函数
组合继承缺点:调用两次父构造函数
一次是设置子类型实例的原型的时候:
Child.prototype = new Parent();
一次在创建子类型实例的时候:
var child1 = new Child('kevin', '18'); // 调用了Child中的Parent.call(this, name);
新版ES2018中规定执行上下文包含了:
词法环境(这就是旧版的作用域链和this合在一起)
变量环境
…其他
[[scope]]中保存了当前函数的作用域链,这个属性无法访问,属于内部属性
简单栗子:
var scope = "global scope"
function checkscope(){
var scope2 = 'local scope'
return scope2
}
checkscope()
执行过程,伪代码:
1)函数创建,保存作用域链到 内部属性[[scope]]
checkscope.[[scope]] = [
globalContext.VO //有全局环境
]
2)执行上下文压入执行栈
ECStack = [
checkscopeContext, //压入栈
globalContext
]
3)执行上下文初始化:
上下文对象复制函数的[[scope]]属性创建作用域链
checkscopeContext = {
//创建上下文
Scope: checkscope.[[scope]],
this: undefined,
}
用 arguments 创建活动对象AO,加入形参、函数声明、变量声明
checkscopeContext = {
AO: {
//创建这个对象
arguments: {
length: 0
},
scope2: undefined,
},
Scope: checkscope.[[scope]],
this: undefined,
}
将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]], // 压入栈
this: undefined,
}
4)执行函数:修改AO的属性值
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope' // 修改这里
},
Scope: [AO, [[Scope]]],
this: undefined,
}
5)函数返回后,执行上下文从栈中弹出
ECStack = [
globalContext // 只剩全局上下文
];
MDN
闭包定义:闭能够访问自由变量的函数
自由变量:在函数中使用的,但既不是函数参数也不是函数的局部变量的变量(就是上层上下文中的变量)
定义:
1)从理论角度:所有的函数。因为创建的时候就讲上层上下文的数据保存,并可以引用
2)从实践角度:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();
data[1]();
data[2]();
通过IIFE创建了函数上下文
data[0]
执行函数时,作用域链多了一层
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}
data[0]Context = {
Scope: [AO, 匿名函数Context.AO globalContext.VO]
}
能找到i的值,就不会再去全局上下文找,所以值是对的
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
在全局上下文中,全局对象就是变量对象
只有当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以叫activation object
1)进入执行上下文初始化
变量对象包括:
简单栗子
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){
},
}
2)执行代码
根据代码修改AO中的值
ECMAScript的类型分为两种:语言类型、规范类型
语言类型 就是7种基本类型:string,number,bigint,boolean,null,undefined,symbol 和一种引用类型:obj
规范类型 用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型,用来描述语言底层行为逻辑。包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。
定义: 用来解释诸如 delete、typeof 以及赋值等操作行为
三部分组成:
Reference 组成部分的方法,比如 GetBase 和 IsPropertyReference。
两个组成部分的方法
1.GetBase
返回 reference 的 base value
2.IsPropertyReference
简单的理解:如果 base value 是一个对象,就返回true。
GetValue:用于从 Reference 类型获取对应值的方法
调用 GetValue,返回的将是具体的值,而不再是一个 Reference
步骤:
1.计算 MemberExpression 的结果赋值给 ref
2.判断 ref 是不是一个 Reference 类型
2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
ImplicitThisValue 该方法始终返回 undefined
2.3 如果 ref 不是 Reference,那么 this 的值为 undefined
什么是 MemberExpression ?
说白了就是比如 foo.bar()、foo[0]、foo.obj 这些运算中,括号、点运算符、中括号运算符之前的表达式要先进行计算,为 null 或者其他不能用的情况就会报错。
几种调用情况下的this
var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
foo.bar()
1、计算 MemberExpression 的结果 赋值给 ref 如下:
var ref = {
base: foo,
name: 'bar',
strict: false
};
2、IsPropertyReference(ref) 由于 ref.base 是 foo,所以返回 true
3、执行 GetBase(ref) 返回 foo, 赋值给 this
------------------------------------------------
(foo.bar)()
1、括号没有对 foo.bar 做任何计算,所以结果同上
------------------------------------------------
(foo.bar = foo.bar)()
1、赋值计算调用了 GetValue, 返回的不再是 Reference 类型, this 为 undefined
------------------------------------------------
(false || foo.bar)()
同上,调用了 GetValue
------------------------------------------------
(foo.bar, foo.bar)()
同上,调用了 GetValue
------------------------------------------------
foo()
1、计算 MemberExpression 的结果 赋值给 ref 如下:
var ref = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};
2、base value 是 EnvironmentRecord, this 的值为 ImplicitThisValue(ref), 返回 undefined
上述情况是从规范的角度去理解 this,大部分人是从调用的角度去理解,但是这个角度会无法去理解为何 (false || foo.bar)() 这种情况的 this 值
先看一组比较:
function foo(){
}() 报错,js解析器会当成函数声明
var foo = function(){
console.log(1)}() 可以执行
function foo(){
}(1) 不会报错,等同于下面的代码
function foo(){
}
(1)
在 js 里圆括号中不能包含声明,所以一般使用此方法将函数声明变成表达式
用类似 JQ 的返回对象来做私有变量会更好点,也是早期的模块化
js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息
两个特殊值:
null:所有机器码均为0
undefined:用 −2^30 整数来表示
所以 typeof 判断 null 为对象,机器码低位相同
instanceof 原理:右边变量的 prototype 在左边变量的原型链上
function new_instance_of(leftVaule, rightVaule) {
let rightProto = rightVaule.prototype // 取右表达式的 prototype 值
leftVaule = leftVaule.__proto__ // 取左表达式的__proto__值
while (true) {
if (leftVaule === null) {
return false;
}
if (leftVaule === rightProto) {
return true;
}
leftVaule = leftVaule.__proto__
}
}
特点:
1)返回函数
2)传参2次:调用bind的时候可以传参,返回的新函数调用时也可以传参 3)绑定之后返回的新函数,作为构造函数时,绑定的this应该失效
具体实现
Function.prototype.bind2 = function (context) {
let self = this;
let args = [...arguments].slice(1) // 拿到第一次调用时,除了上下文之外的其他参数
let fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments); // 获取第二次调用的参数
// 第三个特点,如果是构造函数调用,绑定这个构造函数的实例为 this, 否则是我们传的上下文
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
// 将被绑定函数的原型 放到 返回函数的原型链上,
// 通过空函数中转,防止修改一个影响另一个
let fNOP = function () {
}
fNOP.prototype = this.prototype
fBound.prototype = new fNOP()
return fBound; // 第一个特点,返回函数
}
第一个参数指定为 null 或 undefined 时会自动替换为指向全局对象
call 的实现
Function.prototype.call = function (thisArg) {
// 先判断当前的甲方是不是一个函数(this就是Product,判断Product是不是一个函数)
if (typeof this !== 'function') {
throw new TypeError('当前调用call方法的不是函数!')
}
// 保存甲方给的参数
const args = [...arguments].slice(1)
// 传入的是 null 或者 undefined
thisArg = thisArg || window
// 将调用call的函数保存为乙方的一个属性,为了保证不与乙方中的key键名重复使用Symbol
const fn = Symbol('fn')
thisArg[fn] = this
// 执行保存的函数,这个时候作用域就是在乙方的对象的作用域下执行,改变的this的指向
const result = thisArg[fn](...args)
// 执行完删除刚才新增的属性值
delete thisArg[fn]
// 返回执行结果
return result
}
apply 的实现
Function.prototype.appy= function (thisArg) {
if (typeof this !== 'function') {
throw new TypeError('当前调用apply方法的不是函数!')
}
// 此处与call有区别,因为只有2个参数,其他一样
const args = arguments[1]
thisArg = thisArg || window
const fn = Symbol('fn')
thisArg[fn] = this
const result = thisArg[fn](...args)
delete thisArg[fn]
return result
}
Function.length 表示形参的个数,不包括剩余参数个数,同时只计算第一个有默认值之前的参数
柯里化(Curry):一个函数接收一个多参函数,并且返回多个嵌套的只接受一个参数的函数
简单栗子:
fn(1)(2)(3)
偏函数应用(Partial Application):每个嵌套的函数可以接受不止一个参数
简单栗子:
fn(1,2)(3)
实现(不考虑占位符)
占位符根据多种不同情况用 if-else 处理,用一个数组保存占位符在总的参数列表中的位置,然后替换
function curry(targetFn) {
return function curried(...args) {
// 如果参数个数 达到 目标函数所需的参数,执行目标函数
if (args.length >= targetFn.length) {
return targetFn.apply(this, args)
} else {
// 否则递归柯里化函数:将上次递归抛出的函数获得的参数 args2,和以前累计的参数 args 传递给柯里化函数
return function(...args2) {
return curried.apply(this, [...args, ...args2])
}
}
}
}
V8引擎在64位系统下最多只能使用约1.4GB的内存,在32位系统下最多只能使用约0.7GB的内存。
原因:
1)浏览器端很少需要操作太多内存资源的场景
2)JS 单线程机制
没有复杂的多线程执行场景,对程序内存要求低
3)垃圾回收机制
垃圾回收耗时久。假设V8的堆内存为1.5G,那么V8做一次小的垃圾回收需要50ms以上,而做一次非增量式回收甚至需要1s以上。内存使用过高,必然垃圾回收时间变长,主线程等待时间也变长。
node 中可以手动设置内存最大与最小值
设置新生代内存中单个半空间的内存最小值,单位MB
node --min-semi-space-size=1024 xxx.js
设置新生代内存中单个半空间的内存最大值,单位MB
node --max-semi-space-size=1024 xxx.js
设置老生代内存最大值,单位MB
node --max-old-space-size=2048 xxx.js
查看当前node进程所占用的实际内存
heapTotal:V8 当前申请到的堆内存总大小。
heapUsed:当前内存使用量。
external:V8 内部的 C++ 对象所占用的内存。
rss(resident set size):表示驻留集大小,是给这个node进程分配了多少物理内存,这些物理内存中包含堆,栈和代码片段。
对象,闭包等存于堆内存,变量存于栈内存,实际的JavaScript源代码存于代码段内存
使用 Worker 线程时,rss 也包括 Worker 线程的值,但其他的值只针对当前线程
总结:基于分代式垃圾回收机制,根据对象的存活时间将内存进行不同的分代,然后采用不同的垃圾回收算法
分为几个部分:
新生代(new_space)
:大多数的对象开始都会被分配在这里,这个区域相对较小但是垃圾回收特别频繁。该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来。老生代(old_space)
:新生代中的对象在存活一段时间后就会被转移到老生代内存区,垃圾回收频率较低。老生代又分为老生代指针区和老生代数据区,前者包含大多数可能存在指向其他对象的指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。大对象区(large_object_space)
:存放体积超越其他区域大小的对象,每个对象都会有自己的内存,垃圾回收不会移动大对象区。代码区(code_space)
:代码对象,会被分配在这里,唯一拥有执行权限的内存区域。map区(map_space)
:存放Cell和Map,每个区域都是存放相同大小的元素,结构简单构成:两个 semispace (半空间)
使用算法:Scavenge算法,牺牲空间换时间。老生代内存生命周期长,可能会存储大量对象,不适用这种算法
具体实现使用了 Cheney 算法。
1、激活状态的区域叫做 From 空间,垃圾回收时把 From 空间中不能回收的对象复制到 To 空间
2、清除 From 中所有的非存活对象,两个空间呼唤身份
缺点:浪费空间,一半的内存用于复制
反思:为什么不标记完直接清除,而使用 Scavenge ,应该也是为了整理内存碎片
两个条件满足其一:
使用算法:Mark-Sweep (标记清除) 和 Mark-Compact (标记整理)
总步骤:标记、整理、清除
1)Mark-Sweep (标记清除)
详细步骤:
- 垃圾回收器在内部构建一个根列表, 保存所有的根节点
- 从所有根节点出发,遍历其可以访问到的子节点,标记为活动的
- 释放所有非活动的内存块
根节点类型
- 全局对象
- 本地函数的局部变量和参数
- 当前嵌套调用链上的其他函数的变量和参数
问题
一次标记清除后,内存空间可能会出现不连续的状态-----内存碎片
后面如果需要分配一个大对象而空闲内存不足以分配,就会提前触发垃圾回收,所以需要 标记整理
2)Mark-Compact (标记整理)
详细步骤:
- 将所有活动对象往堆内存的一端移动
3)性能提升
全停顿
:由于 JS 是单线程的,垃圾回收的过程会阻塞主线程同步任务
增量标记
:标记、交给主线程、回到标记暂停的地方继续标记
如果在老生代中,对堆内存中所有的存活对象遍历,势必会造成性能问题。
于是 V8 引擎先标记内存中的一部分对象,然后暂停,将执行权重新交给 JS 主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。
挺像使用 setTimeout 优化技巧,也是把一个大的任务拆成很多个小任务,这样就可以间断性的渲染 UI,不会有卡顿的感觉
基于增量标记, V8 引擎后续继续引入了延迟清理(lazy sweeping)和增量式整理(incremental compaction)、并行标记、并行清理
避免使用全局变量
:因为 window 对象可以作为根节点,上面的属性都是常驻的
手动清除定时器
少用闭包
清除DOM引用
:对保存在属性中的 dom 引用及时释放成 null
使用弱引用
:WeakMap 和 WeakSet 中的引用都是弱引用,只要对象没有其他的引用,这个对象中所有属性的内存都会被释放掉
Number 类型使用 IEEE 二进制浮点数算术标准 中的 双精度64位表示法,也就是64位字节存储一个浮点数
浮点数 (Value) 可以这样表示
Value = sign * exponent * fraction
1)1 位存储 S,0 表示正数,1 表示负数。
2)11 位存储 E(阶码) + bias,对于 11 位来说,bias 的值是 2^(11-1) - 1,也就是 1023。
最大值是1024,因为E可能为1,所以bias的值是固定的1023,存储的时候通过存储的二进制值减去1023反推得到E的值。
Sign 是 0,E + bias 是 -4 + 1023 = 1019,1019 用二进制表示是 1111111011,Fraction是1001100110011…(下方位1.不用存,是固定的)
1 * 1.1001100110011…… * 2^-4
64字节位表示
0 01111111011 1001100110011001100110011001100110011001100110011010
0.2 对应的 64 字节
0 01111111100 1001100110011001100110011001100110011001100110011010
例如:0.1 + 0.2
1)对阶
把阶码调整为相同
0.1 是 1.1001100110011…… * 2^-4,阶码是 -4
0.2 是 1.10011001100110…* 2^-3,阶码是 -3
小阶对大阶:0.1 的 -4 调整为 -3, 数字会变大,所以前面的应该变小,也就是右移,符号位补0
2)尾数运算
0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
———————————————————————————————————————————————————
10.0110011001100110011001100110011001100110011001100111
结果:10.0110011001100110011001100110011001100110011001100111 * 2^-3
3)规格化
移一位:1.0011001100110011001100110011001100110011001100110011(1) * 2^-2
4)舍入处理(0 舍 1 入)
括号里的1是多出来的,会舍弃,并进1
5)溢出判断(这里没有)
6)结果
0 01111111101 0011001100110011001100110011001100110011001100110100
十进制就是 0.30000000000000004440892098500626
由于两次存储时的精度丢失,再加上运算时的精度丢失,导致了这个结果
扩展:为什么(2.55).toFixed(1)等于2.5?
简单总结:2.55的存储要比实际存储小一点,导致0.05的第1位尾数不是1,所以就被舍掉了
特点:
1)返回的对象,可以访问传入的构造函数里的属性
2)返回的对象,可以访问传入的构造函数 原型 里的属性
3)判断构造函数是否有返回值,如果是对象就返回对象,不是的话就返回我们创建的
实现(使用一个函数模拟)
function objectFactory() {
var obj = new Object(),
Constructor = [].shift.call(arguments); // 拿到传入的构造函数
obj.__proto__ = Constructor.prototype; // 创造的实例对象 连接 构造函数的prototype
var ret = Constructor.apply(obj, arguments); // 应用剩余的传入参数,this 改为创造的实例对象
return typeof ret === 'object' ? ret : obj; // 判断返回值
};
特点:
当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。
同一次事件循环中,微任务永远在宏任务之前执行。、
node 选择 chrome v8 引擎作为js解释器,v8 引擎将 js 代码分析后去调用对应的 node api,而这些 api 最后则由 libuv 引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。
实际上node中的事件循环存在于libuv引擎中
1)先查看 poll queue 中是否有事件
2)当 poll queue 为空时,检查是否有 setImmediate() 的 callback,进入 check 阶段
3)同时检查是否有到期的 timer,按照调用顺序放到timer queue中,进入 timer 阶段
4)2、3步顺序不一定,看具体的代码环境。
5)如果两者的 queue 都是空的,那么loop会在poll阶段停留,直到有一个i/o事件返回,循环会进入 i/o callback 阶段并立即执行这个事件的 callback
check 阶段 和 timer 阶段
check 阶段专门用来执行 setImmediate() 方法的回调,当 poll 阶段进入空闲状态进入
timer 阶段执行 setTimeout 或者 setInterval 函数的回调
I/O callback阶段
执行大部分I/O事件的回调,包括一些为操作系统执行的回调。
例如一个TCP连接生错误时,系统需要执行回调来获得这个错误的报告。
close阶段
当一个 socket 连接或者一个 handle 被突然关闭时(例如调用了 socket.destroy() 方法),close 事件会被发送到这个阶段执行回调。否则事件会用 process.nextTick()方法发送出去。
process.nextTick
node中存在着一个特殊的队列,即nextTick queue
当事件循环准备进入下一个阶段之前,会先检查nextTick queue中是否有任务,如果有,那么会先清空这个队列,且不会停止,所以可能造成内存泄漏。
setTimeout 与 setImmediate 的区别与使用场景
在在定时器回调或者 I/O 事件的回调中,setImmediate 方法的回调永远在 timer 的回调前执行。
其他场景取决于当时机器情况
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
immediate
timeout
手写代码链接
for of 可以自动遍历迭代器的值
简易状态机(不用设初始变量,不用切换状态,更简洁,更安全)
let clock = function*() {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
手写代码链接
默认情况下,块级元素的内容宽度是其父元素的宽度的100%,并且与其内容一样高。
内联元素高宽与他们的内容高宽一样
标准模型和IE模型的区别
IE模型元素宽度 width = content + padding + border,高度计算相同
标准模型元素宽度 width = content,高度计算相同
js 如何 设置 获取 盒模型对应的宽和高
- dom.style.width/height 只能取到行内样式的宽和高,style 标签中和 link 外链的样式取不到。
- window.getComputedStyle(dom).width/height 取到的是最终渲染后的宽和高, 多浏览器支持,IE9以上支持。
- dom.getBoundingClientRect().width/height 也是得到渲染后的宽和高,大多浏览器支持。IE9以上支持,除此外还可以取到相对于视窗的上下左右的距离
定义
决定了元素如何对其内容进行定位,以及与其他元素的关系和相互作用。提供了一个环境,一个环境中的元素不会影响到其他环境中的布局。
原理(渲染规则)
- BFC 元素垂直方向的边距会发生重叠。属于不同 BFC 外边距不会发生重叠
- BFC 的区域不会与浮动元素的布局重叠。
- BFC 元素是一个独立的容器,外面的元素不会影响里面的元素。里面的元素也不会影响外面的元素。
- 计算 BFC 高度的时候,浮动元素也会参与计算(清除浮动)
创建BFC
- html 根元素
- overflow不为visible
- float的值不为none
- position的值不为static或relative
- display属性为inline-blocks,table,table-cell,table-caption,flex,inline-flex
场景:防止 margin 合并、给普通盒子加上可以清除浮动,父元素加上 BFC 可以包含浮动子元素高度等
类别:
- 简单选择器: id 、class
- 属性选择器:通用语法由方括号([]) 组成,其中包含属性名称。[attr]、[attr=val]、[attr~=val](attr中包含val的元素,a[class~=“logo”],包含 logo 类名的 a),[attr^=val],[attr$=val],[attr*=val](包含 val 的元素)
- 伪类(Pseudo-classes):hover、active
- 伪元素(Pseudo-elements): ::after
- 组合器(Combinators):+ - > ~ (+ ~ 选择兄弟元素只会向后选择,不会选择前面的兄弟,+是相邻的兄弟)
- 多用选择器
确定包含块:
完全依赖于这个元素的 position 属性
- position 属性为 static 、 relative 或 sticky:最近的祖先块元素(inline-block, block 或 list-item)的内容区的边缘组成
- position 属性为 absolute:最近的 position 的值不是 static 的祖先元素的内边距区的边缘
- position 属性是 fixed:连续媒体的情况下包含块是 viewport(视口),分页媒体是分页区域
- absolute 或 fixed:也可能是满足以下条件的最近父级元素的内边距
1)transform 或 perspective 的值不是 none
2)will-change 的值是 transform 或 perspective
3)filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效).
4)contain 的值是 paint (例如: contain: paint;)
包含块计算百分值
1、计算 height 、top 及 bottom 中的百分值,是通过包含块的 height 的值。如果包含块的 height 值会根据它的内容变化,而且包含块的 position 属性的值被赋予 relative 或 static ,那么,这些值的计算值为 auto。
2、要计算 width, left, right, padding, margin 这些属性由包含块的 width 属性的值来计算它的百分值。
定位上下文
绝对定位的元素的相对位置元素
stickey
设置了 top 值,当这个元素距离顶部 30px 时,会变成 fixed 定位粘在顶部
.positioned {
position: sticky;
top: 30px;
left: 30px;
}
默认情况下,flex 容器中有一些设置:
元素不会在主维度方向拉伸,但是可以缩小。
元素被拉伸来填充交叉轴大小。
flex-basis 属性为 auto。
flex-wrap 属性为 nowrap。
注意交叉轴的拉伸,如果一些元素比其他元素高的话,会拉伸矮的元素
flex-flow
是 flex-direction 和 flex-wrap 的简写属性
flex 的一些简写含义
flex: initial === flex: 0 1 auto (把 flex 元素重置为 Flexbox 的初始值)
flex: auto === flex: 1 1 auto (自由伸缩)
flex: none === flex: 0 0 auto (无法伸缩)
flex: 2 === flex: 2 1 0% 单值语法只改变 grow
flex-basis
默认设置为 auto:先检测是否设置了绝对值,没有设置的话就使用 flex 子元素的 max-content 大小作为 flex-basis,不会超过元素最大宽度
如果要让三个不同尺寸的flex子元素,在剩余空间分配后保持同一宽度,应使用 flex: 1 1 0,尺寸计算值是 0 表示所有的空间都用来争夺
flex-shrink
数值越大收缩的越快,并且最小不会小于内容的 min-content(也就是能把内容显示出来)
从0开始,一个行内样式+1000,一个id选择器+100,一个属性选择器、class或者伪类+10,
一个元素选择器,或者伪元素+1,通配符+0
!important > 行内样式 > 内联样式 and 外联样式
样式指向同一元素,权重规则生效,权重大的被应用
样式指向同一元素,权重规则生效,权重相同时,就近原则生效,后面定义的被应用
样式不指向同一元素时,权重规则失效,就近原则生效,离目标元素最近的样式被应用
相同点:中间栏要在放在文档流前面优先渲染。前一半是相同的,也就是三栏全部 float 浮动,左右两栏加上负 margin 让其跟中间栏 div 并排,以形成三栏布局。
不同点:解决”中间栏div内容不被遮挡“问题的思路不一样,圣杯使用相对定位配合 right和 left 属性,双飞翼通过 middle 的子元素使用 margin 为左右两栏留出位置
圣杯
双飞翼
注意:左栏 margin-left: -100% 以包含块内容区左侧(当然以相邻元素右侧 margin 为基准也可以,一个道理)为基准线,负值表示向基准线移动靠近。
同时给 left、middle、right设置上 padding-bottom: 9999px; margin-bottom: -9999px; 可以形成三列保持等高(利用背景会显示在 padding 区域,视觉上欺骗,只能用与纯色背景)
多列等高布局原理就是通过 padding 撑开盒子,同时相同的 负margin 告诉浏览器计算文档流布局时减去对应的值,让下方的元素上来占据位置。同时父元素 overflow:hidden 形成 BFC 并且遮挡超出部分,以最高元素为准。
还可以给 ul 加上负 margin以消除每行最后一项的正margin
过渡
transition: CSS属性,花费时间,效果曲线(默认ease),延迟时间(默认0)
transition:width,.5s,ease,.2s
可以使用 scale(0)~scale(1) 制作下拉列表展开效果
动画
animation:动画名称,一个周期花费时间,运动曲线(默认ease),动画延迟(默认0),
播放次数(默认1),是否反向播放动画(默认normal),是否暂停动画(默认running)
animation-fill-mode : none | forwards | backwards | both;
none:不改变默认行为。
forwards :当动画完成后,保持最后一个关键帧。
backwards:在动画显示之前,应用第一个关键帧。
both:向前和向后填充模式都被应用
选择器
p:nth-child(2): 表示选中 父元素的第二个子元素并且是p标签
p:nth-of-type(2): 表示选中 父元素的第二个是p标签的元素
背景
background-clip: border-box、padding-box、content-box (背景全部绘制,只显示某些部分)
background-origin:属性同上(背景从哪里开始绘制)
background-size
文字
word-break:
normal 浏览器默认规则
break-all 允许单词内换行(随意换)
keep-all; 半角空格或连字符处换行
word-wrap:
normal:默认
break-word: 实在没有好的换行点就换行
省略号
单行:禁止换行,超出隐藏,超出省略号
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis;
多行:
兼容性不太好
overflow:hidden;
text-overflow:ellipsis;
display:-webkit-box;
-webkit-line-clamp:2; (两行文字)
-webkit-box-orient:vertical;
伪元素方案:兼容性还可以
p{
position:relative;
line-height:1.4em;
/*设置容器高度为3倍行高就是显示3行*/
height:4.2em;
overflow:hidden;
}
p::after{
content:'...';
font-weight:bold;
position:absolute;
bottom:0;
right:0;
padding:0 20px 1px 45px;
background:#fff;
}
手写时代
行内样式缺点
- 样式不能复用。
- 样式权重太高,样式不好覆盖。
- 表现层与结构层没有分离。
- 不能进行缓存,影响加载效率。
导入样式缺点
- 导入样式,只能放在 style 标签的第一行,放其他行则会无效。
- @import 声明的样式表不能充分利用浏览器并发请求资源的行为,其加载行为往往会延后触发或被其他资源加载挂起。
- 由于 @import 样式表的延后加载,可能会导致页面样式闪烁。
所以一般我们只用内嵌样式和外部样式
预处理器时代 Sass/Less
打包出来的结果和源生的 css 都是一样的,只是对开发者友好,写起来更顺滑
平台 PostCSS
提供各种插件构建复杂功能
使用场景:
- 配合 stylelint 校验 css 语法
- 自动增加浏览器前缀 autoprefixer
- 编译 css next 的语法
CSS Modules
打包的时候会自动将类名转换成 hash 值,CSS Modules 不能直接使用,而是需要进行打包。
webpack 中进行配置
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use:{
loader: 'css-loader',
options: {
modules: {
// 自定义 hash 名称
localIdentName: '[path][name]__[local]--[hash:base64:5]',
}
}
}
]
}
};
CSS In JS
最出名的是 styled-components
- 合并 css 文件,如果页面加载10个css文件,每个文件1k,那么也要比只加载一个100k的css文件慢。
- 减少 css 嵌套,最好不要嵌套三层以上。
- 不要在 ID 选择器前面进行嵌套,ID本来就是唯一的而且权限值大,嵌套完全是浪费性能。
- 建立公共样式类,把相同样式提取出来作为公共类使用。
- 减少通配符 * 或者类似 [hidden=“true”] 这类选择器的使用,挨个查找所有…这性能能好吗?
- 巧妙运用css的继承机制,如果父节点定义了,子节点就无需定义。
- 拆分出公共 css 文件这样一次下载后就放到缓存里,当然这种做法会增加请求,具体做法应以实际情况而定。
- 不用 css 表达式,对性能的浪费可能是超乎你想象的。
- 少用 css rest,可能会觉得重置样式是规范,但是其实其中有很多操作是不必要不友好的,有需求有兴趣,可以选择 normolize.css。
- cssSprite,减少了 http 请求。
- 善后工作,css压缩(在线压缩工具 YUI Compressor)
- GZIP压缩
避免使用@import
- 影响浏览器的并行下载
- 多个@import会导致下载顺序紊乱
避免过分重排 与 重绘
- 一个节点触发来reflow,会导致他的子节点和祖先节点重新渲染
- 常见重排元素
- 大小有关的 width,height,padding,margin,border-width,border,min-height
- 布局有关的 display,top,position,float,left,right,bottom
- 字体有关的 font-size,text-align,font-weight,font-family,line-height,white-space,vertical-align
- 隐藏有关的 overflow,overflow-x,overflow-y
- 建议
- 不要一条条的修改 dom 样式,每一次设置都会触发一次reflow,预先定义好 class,然后修改 dom 的 classname
- 不要修改影响范围较大的 dom
- 动画元素使用绝对定位
- 不要table布局,因为一个很小的改动会造成整个table重新布局
- 常见重绘元素
- 颜色 color,background
- 边框样式 border-style,outline-color,outline,outline-style,border-radius,box-shadow,outline-width
- 背景相关 background,background-image,background-position,background-repeat,background-size
- tips:选择器是从右向左匹配的,出于性能考虑,选择器选择时大部分元素是不会被选择的
定义:浏览器三维概念,Z轴上的每一层可以视为一个层叠上下文
层叠水平
同一个层叠上下文中用来区分元素距离用户的远近,所有元素都有层叠水平,z-index 只能影响定位元素以及 flex 盒子的孩子元素
层叠顺序
内容为王,所以内联元素在上
层叠准则
文档流后面的元素会覆盖前面的
明显的层叠水平标示时,谁大谁在上
比较时先比父级
常见的层叠上下文的创建
1、页面根元素
2、设置了 z-index 的定位元素
3、设置了 z-index 的 flex 的子元素
4、元素的opacity值不是1
5、元素的transform值不是none
flex
绝对定位 + transform
绝对定位 left right top bottom为 0,margin 为 auto
这种方案子元素不设置宽高,就可以铺满父级(用做遮罩层)
table-cell,一般不用
浮动元素只能影响行内元素,间接影响了包含块的布局
浮动元素只会浮动在文档流后面的块元素上,不会侵犯前面的块元素领地
让浮动元素撑开包含块:BFC、空内容伪元素设置 clear:both(把伪元素的边界放到所有浮动元素下面,所以撑开)、包含块自己也浮动(其实也是 BFC)
区别
块级元素:
① 总是在新行上开始,占据一整行;
② 高度,行高以及外边距和内边距都可控制;
③ 不加控制的话宽度会撑满浏览器,与内容无关;
④ 它可以容纳内联元素和其他块元素。
行内元素:
① 和其他元素都在一行上;
② 行高及外边距和内边距部分可改变(水平方向有效,竖直方向无效)。 如果是可替换元素,比如 input ,竖直方向是有效的
③ 宽度只与内容有关;
④ 行内元素只能容纳文本或者其他行内元素。
const page = new BroadcastChannel('channel');
page.onmessage = function (e) {
const data = e.data;
const text = '[receive] ' + data.msg + ' —— tab ' + data.from;
console.log('[BroadcastChannel] receive message:', text);
};
page.postMessage(mydata);
本身不具备通信属性,但是可以作为后台长期运行的 worker,建立通信站
/* 页面中注册 */
navigator.serviceWorker.register('../service.js').then(function () {
console.log('Service Worker 注册成功');
});
/* 页面中监听 */
navigator.serviceWorker.addEventListener('message', function (e) {
const data = e.data;
});
/* 页面中发送消息 */
navigator.serviceWorker.controller.postMessage(mydata);
// service worder 代码,监听 message 事件,通过 self.clients.matchAll 获取所有注册页面,
// 然后循环将消息通过 postMessage 发送给所有页面
self.addEventListener('message', function (e) {
console.log('service worker receive message', e.data);
e.waitUntil(
self.clients.matchAll().then(function (clients) {
if (!clients || clients.length === 0) {
return;
}
clients.forEach(function (client) {
client.postMessage(e.data);
});
})
);
});
特性:当 LocalStorage 变化时,会触发 storage 事件
// 根据传入的 key 区分值
window.addEventListener('storage', function (e) {
if (e.key === 'yangyi') {
const data = JSON.parse(e.newValue);
}
});
// 传输消息的页面,正常 setItem,加上时间戳(因为 storage 事件只在值真的改变时触发)
mydata.st = +(new Date);
window.localStorage.setItem('ctc-msg', JSON.stringify(mydata));
上面三个属于订阅发布模式,下面两个是共享存储+轮询
普通的 Worker 之间独立运行、数据互不相通;而多个 Tab 注册的 Shared Worker 可以实现数据共享
缺点:无法主动通知所有页面,必须轮询
// 页面中注册,第二个参数是 Shared Worker 名称,也可以留空
const sharedWorker = new SharedWorker('../worker.js', 'worker-name');
/* Shared Worker 思路 */
1、监听 connect 事件
2、只能根据传入的数据中的字段,区分是否是获取数据还是发送数据,只有 postMessage 方法
3、每个页面需要轮询请求数据:sharedWorker.port.postMessage({
get: true});
轮询查询指定的数据是否被更新,不是很友好
window.open 会返回打开的页面的 window 对象引用,然后通过window.opener.postMessage(mydata) 发送消息
缺点:必须通过 window.open,并且只能一个传一个
如上图,每个业务页面都有一个 iframe,所有 iframe 的 url 是相同的(也可以不同,同源就行),iframe 之间使用上面的同源页面的通信方式
此外还有基于服务端的:Websocket、SSE(服务端推送事件)
他俩区别:
原理:使用 window.location.hash 属性及窗口的 onhashchange 事件
原理
属性
History.length
:当前窗口访问过的网址数量(包括当前网页)History.state
:History 堆栈最上层的状态值(默认为 undefined)方法
History.back():移动到上一个网址,等同于浏览器的后退键。对于第一个访问的网址,该方法无效果。
History.forward():移动到下一个网址,等同于浏览器的前进键。对于最后一个访问的网址,该方法无效果。
History.go():接受一个整数作为参数,以当前网址为基准,移动到参数指定的网址。如果参数超过实际存在的网址范围,该方法无效果;如果不指定参数,默认参数为0,相当于刷新当前页面。
History.pushState:在历史中添加一条记录, 不会触发页面刷新,,三个参数: object
、title
、url
,分别为传递给新页面的对象、标题、新的网址(必须同域,防止恶意代码让用户以为还在同站)
注意:URL 参数设置了一个新的锚点值(即 hash),并不会触发 hashchange 事件。
History.replaceState:修改当前历史记录,参数同上
事件 popstate
缺点
改变页面地址后,强制刷新浏览器时会404,因为会触发请求,而服务器中没有这个页面,所以一般单页应用会全部重定向到 index.html 中
HTML 文件字节流无法直接被渲染引擎理解,需要转化为对 HTML 文档结构化的表述,也就是 DOM。
渲染引擎内部,有一个叫 HTML 解析器(HTMLParser)的模块,将 HTML 字节流转换为 DOM 结构
HTML 解析器,是网络进程加载了多少数据,便解析多少数据。过程如下:
喂给数据之后,字节流转换为 DOM 的三个阶段:
分词器做词法分析,将字节流转换为 Token
Token 解析为 DOM 节点 3. 同时将 DOM 节点添加到 DOM 树中
三种情况:
- 如果入栈的是StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。
- 如果是文本 Token,会生成一个文本节点,然后将该节点加入到 DOM 树中。文本 Token 不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点。
- 如果是 EndTag 标签,HTML 解析器会查看 Token 栈顶的元素是否是 StartTag div,如果是,就将 StartTag div 从栈中弹出,表示该 div 元素解析完成。
简单示例图:
<html>
<body>
<div>1div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang'
script>
<div>testdiv>
body>
html>
遇到 js 时,渲染引擎判断这是一段脚本,HTML 解析器就会暂停 DOM 的解析,因为接下来的 JavaScript 可能要修改当前已经生成的 DOM 结构,执行完毕之后继续解析,流程是一样的。
<html>
<body>
<div>1div>
<script type="text/javascript" src='foo.js'>script>
<div>testdiv>
body>
html>
chrome 有一个优化操作,当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件
解析过程同上是一样的
<head>
<style src='theme.css'>style>
head>
<body>
<div>1div>
<script>
let div1 = document.getElementsByTagName('div')[0]
div1.innerText = 'time.geekbang' // 需要 DOM
div1.style.color = 'red' // 需要 CSSOM
script>
<div>testdiv>
body>
html>
渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载(因为引擎无法确定是否已下载),解析操作,再执行 JavaScript 脚本。JavaScript 脚本是依赖样式表的,这又多了一个阻塞过程。
总结:JavaScript 会阻塞 DOM 生成,而样式文件又会阻塞 JavaScript 的执行
CDN 加速
压缩文件的体积
如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过 async 或 defer 来标记代码。
二者都是异步的,但使用 async 标志的脚本文件一旦加载完成,会立即执行;而使用了 defer 标记的脚本文件,需要在 DOMContentLoaded 事件之前执行。
addEventListener 的第三个参数默认是 false 冒泡,还可以设置为属性配置对象
- capture:布尔值,是否在捕获阶段触发。
- once:布尔值,监听函数是否只触发一次,然后自动移除。
- passive:布尔值,表示监听函数不会调用事件的preventDefault方法。如果监听函数调用了,浏览器将忽略这个要求,并在监控台输出一行警告。
当添加多个监听时,先添加先触发
removeEventListener 没有返回值
dispatchEvent 手动触发事件,参数为某个 event, 比如 click
stopPropagation
阻止冒泡和捕获,但不会阻止当前节点的事件触发后面的监听函数
stopImmediatePropagation
彻底取消当前事件,后面的监听函数也不会触发
Event 对象
当 Event.cancelable 属性为true时,调用 Event.preventDefault() 才可以取消这个事件,阻止浏览器对该事件的默认行为
Event.currentTarget 属性返回事件当前所在的节点
Event.target 属性返回原始触发事件的那个节点
Event.isTrusted 表示该事件是否由真实的用户行为产生
Event.composedPath() 返回一个数组,成员是事件的最底层节点和依次冒泡经过的所有上层节点。
mousemove:当鼠标在一个节点内部移动时触发。鼠标持续移动会连续触发。为了避免性能问题,应该做节流。
节流:每隔一段时间,只执行一次函数
防抖:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时
mouseenter:鼠标进入一个节点时触发,进入子节点不会触发这个事件
mouseleave:鼠标离开一个节点时触发,离开父节点不会触发这个事件
mouseover:鼠标进入一个节点时触发,进入子节点会再一次触发这个事件
mouseout:鼠标离开一个节点时触发,离开父节点也会触发这个事件
wheel:滚动鼠标的滚轮时触发
触发顺序:mousedown、mouseup、click、dblclick
几个计算距离的属性:clientX/Y(浏览器可视)、pageX/Y(相对文档区域左上角距离,会随着页面滚动而改变)、offsetX/Y(当前DOM)、screenX/Y(显示器)
HTTP请求报文格式
请求行
HTTP头(通用信息头,请求头,实体头)
请求报文主体(只有POST才有报文主体)
HTTP报文格式为:
状态行
HTTP头(通用信息头,响应头,实体头)
响应报文主体
浏览器是否需要向服务器重新发送 HTTP 请求,取决于 我们选择的缓存策略
三种情况:
Expires
HTTP/1.0 的字段,值是服务器返回的过期时间。
缺点:时区不同的话,客户端和服务端有一方的时间不准确发生误差,那么强制缓存则会直接失效
HTTP/1.1 的字段
注意:
刷新:浏览器会在 js 和图片等文件解析执行后直接存入内存缓存中,刷新页面从内存缓存中读取(from memory cache);而css文件则会存入硬盘文件中,每次渲染页面都需要从硬盘读取缓存(from disk cache)。
关闭再打开:之前的进程内存已清空,所以都是硬盘缓存
缓存结果失效后,根据缓存标识发送 HTTP 请求,服务器进行判断
标识
前者:响应头中,表示文件在服务器最后被修改的时间
后者:请求头,值同上,告诉服务器进行判断,文件是否改变,没变则使用缓存,变了就返回最新的
前者:响应头中,表示文件在服务器中唯一标识
后者:请求头,值同上,告诉服务器进行判断,文件是否改变,没变则使用缓存,变了就返回最新的
注:Etag / If-None-Match 优先级高于 Last-Modified / If-Modified-Since,同时存在则只有Etag / If-None-Match生效。
总结
一个进程是应用正在运行的程序,操作系统会为进程分配私有的内存空间以供使用。
协程是运行在线程中更小的单位,async/await 就是基于协程实现的。
一个进程可以让操作系统开启另一个进程处理不同的任务。进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,这就是IPC(Inter Process Communication)。
套接字(socket)
凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行
套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型。
三种套接字:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取 TCP 协议的数据,数据报套接字只能读取 UDP 协议的数据。
有名管道(FIFO)
相比上面可以非亲缘关系
管理 Chrome 应用本身,包括地址栏、书签、前进和后退按钮。同时也负责网络请求、文件访问等,也负责其他进程的调度。
渲染进程负责站点的渲染,其中也包括 JavaScript 代码的运行,web worker 的管理等。
GPU 进程负责提供成像的功能
当 Chrome 运行在拥有强大硬件的计算机上时,会将一个服务以多个进程的方式实现,提高稳定性
当计算机硬件资源紧张时,则可以将多个服务放在一个进程中节省资源。
出于安全考虑,从 Chrome 67 开始每个 iframe 打开的站点由独立的渲染进程处理被默认启用。
包括几个线程
UI 线程会先判断我们输入的内容是要搜索的内容还是要访问一个站点,因为地址栏同时也是一个搜索框。
按下回车访问,UI 线程将借助网络线程访问站点资源,网络线程根据适当的网络协议,例如 DNS lookup 和 TLS 为这次请求建立连接
根据 Content-Type ,如果是 HTML ,网络线程会将数据传递给渲染进程做进一步的渲染工作。
如果数据类型是 zip 文件或者其他文件格式时,会将数据传递给下载管理器做进一步的文件预览或者下载工作
在开始渲染之前,网络线程要先检查数据的安全性。如果返回的数据来自一些恶意的站点,网络线程会显示警告的页面。同时,Cross Origin Read Blocking(CORB) 策略也会确保跨域的敏感数据不会被传递给渲染进程。
在第二步,UI 线程将请求地址传递给网络线程时,UI 线程就已经知道了要访问的站点。此时 UI 线程就同时查找或启动一个渲染进程。如果网络线程按照预期获取到数据,则渲染进程就已经可以开始渲染了,减少了等待时间。
当然,如果出现重定向的请求时,提前初始化的渲染进程可能就不会被使用,但相比正常访问站点的场景,重定向往往是少数。
当数据和渲染进程后,浏览器进程通过 IPC 向渲染进程提交这次访问,同时也会保证渲染进程可以通过网络线程继续获取数据。渲染进程在所有 onload 事件都被触发后向浏览器进程发送完毕的消息,访问结束,文档渲染开始。
这时可能还有异步的 js 在加载资源
为了能恢复访问历史信息,当页签或窗口被关闭时,访问历史的信息会被存储在硬盘中。
当访问其他页面时,一个独立的渲染进程将被用于处理这个请求,为了支持像unload的事件触发,老的渲染进程需要保持住当前的状态,知道用户做出选择。
开发者可以决定用本地存储的数据还是网络访问。当访问开始时,网络线程会根据域名检查是否有 Service worker 会处理当前地址的请求,如果有,则 UI 线程会找到对应的渲染进程去执行 Service worker 的代码。
如果 worker 决定使用网络,进程间的通信已经造成了一些延迟,这时候可以使用 Navigation Preload:sw 启动时并行网络请求,加上下面的请求头,服务器进行配合,sw 中进行开启
await self.registration.navigationPreload.enable();
请求头:Service-Worker-Navigation-Preload: true
渲染进程最重要的工作就是将 HTML、CSS 和 Javascript 代码转换成一个可以与用户产生交互的页面
主线程负责解析,编译或运行代码等工作,如果使用 Worker ,Worker 线程会负责运行一部分代码。合成线程和光栅线程也是运行在渲染进程中的,负责更高效和顺畅的渲染页面。
解析过程
主线程解析 HTML 文本字符串,并且将其转化成 Document Object Model(DOM),静默处理标签的丢失、未闭合等错误
1.额外资源的加载
当 HTML 主解析器发现了类似 img 或 link 这样的标签时,预加载扫描器(副解析器)就会启动,它会马上找出接下来即将需要获取的资源(比如样式表,脚本,图片等资源)的 URL ,然后发送请求给浏览器进程的网络线程,而不用等到主解析器恢复运行,从而提高了整体的加载时间
2.JavaScript 会阻塞转化过程
解析执行还是要等主线程空闲,并且只能读到 HTML 中的资源,当 HTML 分析器发现
主线程遍历 DOM 结构中的元素及其样式,同时创建出带有坐标和元素尺寸信息的布局树(Layout tree),只包含将会在页面中显示的元素
伪元素会出现在布局树中,不会在 DOM 树中
一、渲染过程是昂贵的
布局树改变时,绘制需要重构页面中变化的部分,数据变化会引起后续一系列的的变化
渲染操作运行在主线程中,可能被正在运行的 Javascript 代码所阻塞。可以将 Javascript 操作优化成小块,然后使用 requestAnimationFrame()
使用 setTimeout 或 setInterval 来执行动画之类的视觉变化,这种做法的问题是,回调将在帧中的某个时点运行,可能刚好在末尾,而这可能经常会使我们丢失帧,导致卡顿
二、合成(Compositing)
1)光栅化
浏览器将文档结构、每一个元素的样式,元素的几何信息,绘制的顺序等转化成屏幕上像素的过程
2)层(Layer): 主线程遍历布局树找到 层 需要生成的部分,可以使用 css 属性 will-change、transform、Z-index 让浏览器创建层
分层优点:减少不必要的重新绘制、实现较为复杂的动画、方便实现复杂的CSS样式
3)栅格线程与合成线程
合成线程将层拆分成许多块,并决定块的优先级,发送给栅格线程。栅格线程光栅化这些块并将它们存储在 GPU 缓存中,合成线程使用 draw quads 收集这些信息并创建合成帧
4)好处
合成的好处在于其独立于主线程,不需要等待样式计算和 Javascript 代码的运行,但如果布局或者绘制需要重新计算则主线程是必须要参与的
浏览器的渲染过程就是将文本转换成图像的过程
渲染进程中的主线程完成计算工作,合成线程和栅格线程完成图像的绘制工作
发生交互时,浏览器进程首先接收到事件,将事件类型和位置信息等发送给负责当前页签的渲染进程,渲染进程找到事件发生的元素并且触发事件监听器。
当页面被合成线程合成过,合成线程会标记那些有事件监听的区域。当事件发生在响应的区域时,合成线程就会将事件发送给主线程处理(这里会阻塞 UI 变化,详情见 passive 改善滚屏)。如果在非事件监听区域,则渲染进程直接创建新的帧而不关心主线程。
touchmove 这样的事件每秒向主线程发送 120 次可能会造成主线程执行时间过长而影响性能
Chrome 合并了连续的事件,类似 mousewheel,mousemove,touchmove这样的事件会被延迟到下一次 requestAnimationFrame 前触发
类似 keydown, keyup, mouseup 的离散事件会立即被发送给主线程处理。
定义:将文档转化成为有意义的结构,称作解析树或者语法树
过程:词法分析 和 语法分析 ,迭代过程
1.词法分析器
将输入内容分解成一个个有效标记,将无关的字符(比如空格和换行符)分离出来
2.解析器
根据语言的语法规则分析文档的结构,构建解析树(由 DOM 元素和属性节点构成的树结构)。
解析器向词法分析器请求一个新标记,尝试将其与某条语法规则进行匹配。
如果发现了匹配规则,解析器会将一个对应于该标记的节点添加到解析树中,然后继续请求下一个标记。如果没有规则可以匹配,解析器就会将标记存储到内部,并继续请求标记,直至所有内部存储的标记都有对应匹配的规则。如果找不到,解析器就会引发一个异常。这意味着文档无效,包含语法错误。
无法用常规的 自上而下 或 自下而上 的解析器进行解析,原因在于:
所以使用专有的 标记化算法(状态机) 和 树构建算法(状态机)
标记化算法:
- 初始状态是数据状态
- 遇到字符 < 时,状态更改为“标记打开状态”
- 遇到标签名时,“标记名称状态”
- 遇到 > 标记时,会发送当前标记给构建器,状态改回“数据状态”
- 遇到标签中的每一个字符时,会创建发送字符标记,知道遇到下一个 <
树构建算法:根据接收的标记,创建并插入对应的 DOM 元素,改变对应的状态。
“initial mode”、“before html”、“before head” 之类的状态
预加载扫描器(预解析器)会提前去请求如CSS、JavaScript和web字体。
构建 render 树(也叫呈现树、渲染树):非可视化的 DOM 元素不会插入呈现树中,处理 html 和 body 标记就会构建呈现树根节点,对应于 CSS 规范中所说的容器 block,也是最上层的 block
浏览器利用规则树来优化构建时的样式计算,保存计算过的匹配路径重复使用
这里没有说 cssom树,其实就是把 css 解析成树的结构
呈现树中的元素(也叫呈现器),并不包含位置和大小信息。计算这些值的过程称为布局或重排。
1.Dirty 位系统:浏览器给每个需要重新布局的元素进行标记
“dirty” 和 “children are dirty”一个表示自身,一个表示至少有一个子代
2.全局布局(同步)和增量布局(异步)
全局布局是指触发了整个呈现树范围的布局,触发原因可能包括:
- 字体大小更改。
- 屏幕大小调整。
增量布局:当来自网络的额外元素添加到 DOM 树之后
系统遍历呈现树,并调用呈现器的“paint”方法,将呈现器布局阶段计算的每个框转换为屏幕上的实际像素
绘制可以将布局树中的元素分解为多个层。将内容提升到GPU上的层(而不是CPU上的主线程)可以提高绘制和重新绘制性能,但会以内存管理为代价
当文档的各个部分以不同的层绘制,相互重叠时,必须进行合成,以确保它们以正确的顺序绘制到屏幕上,并正确显示内容。
单线程,在 Firefox 和 Safari 中,该线程就是浏览器的主线程。而在 Chrome 浏览器中,该线程是标签进程的主线程
由大量触发器组成,每个触发器包含几个晶体管,能够存储一个位。单个触发器可以通过唯一标识符寻址,我们可以读取和覆盖它们。
内存分配 -> 内存使用 -> 内存释放
const map = new Map([[obj, 'info']])
obj = null // 重写obj,obj 代表的内存不会被回收
const map = new WeakMap([[obj, 'info']])
obj = null // 重写obj,obj 代表的内存会被回收
意外的全局变量
被遗忘的计时器(vue 组件中的一定要在 beforeDestroy 时清掉)
被遗忘的事件监听器(同上)
被遗忘的订阅发布事件监听器,需要用 off 删掉(同上)
强引用中没有使用 api 释放,只是单纯删除掉变量的引用
let map = new Set();
let value = {
test: 22};
map.add(value);
map.delete(value); // 有效
value = null; // 无效
被遗忘的未使用的闭包
脱离 DOM 的引用
let elements = {
btn: document.querySelector(’#button’)
}
document.body.removeChild(elements.btn)
// elements .btn = null 加上这一句才不泄露,因为 DOM 占用的那块内存还被对象引用
打开谷歌开发者工具,切换至 Performance 选项,勾选 Memory 选项,点击运行按钮
上图红框内就是内存变化,如果是一直递增,那基本可以确定存在泄漏
切换至 Memory 选项,点击运行获取网页快照
根据内存占用大小,点击左侧元素,再找到具体的文件与代码位置即可
含义:
每一次事件循环结束时的空闲时间,完成一些延后的工作,比如加载剩余不可见页面。 requestIdleCallback API
测试与优化
从输入URL按下回车开始,每一步可以做的优化如下
localStorage、sessionStorage、indexedDB,对于一些特殊的、轻量级的业务数据,可以考虑使用本地存储作为缓存(比如每日排行榜列表)
浏览器帮我们实现的优化
不规定该缓存什么、什么情况下需要缓存,也不必须搭配 Service Worker 。
当然 Service Worker 与 Cache API 还是一个功能非常强大的组合,能够实现堆业务的透明。
Cache API 提供的缓存可以认为是“永久性”的,关闭浏览器或离开页面之后,下次再访问仍然可以使用,每个域可以有多个不同的 Cache 对象。
navigator.storage.estimate().then(function(estimate) {
console.log(estimate.quota)
});
153634836480 约等于 153GB
如果前面的步骤都没没有命中缓存,就会到 HTTP request 的阶段
强缓存:直接读取「disk cache」,不够灵活,服务器更新资源不能及时通知
响应头:
Expires
和Cache-Control
,前者设置过期时间,与本地时间对比,后者设置一个最大时间比如max-age=300,300s内走强缓存协商缓存
- 最后修改时间:服务器第一次响应时返回 Last-Modified,而浏览器在后续请求时带上其值作为 If-Modified-Since(精度不够,如果时间很短)
- 文件标识:服务器第一次响应时返回 ETag,而浏览器在后续请求时带上其值作为 If-None-Match,一般会用文件的 MD5 作为 ETag
最后一个缓存检查
HTTP/2 的 Push 功能所带来的。请求一个资源的同时,服务端可以为你“推送”一些其他资源 --不久的将来会用到的一些资源。比如样式表,避免了浏览器收到响应、解析到相应位置时才会请求所带来的延后
特点:
- 匹配上时,并不会在额外检查资源是否过期
- 存活时间很短,甚至短过内存缓存(Chrome 中为 5min 左右)
- 只会被使用一次
- HTTP/2 连接断开将导致缓存直接失效
请求网站流程:
上述服务前端不好切入,但可以通过设置属性,告诉浏览器尽快解析(并不保证,根据网络、负载等做决定)
<link rel="dns-prefetch" href="//yourwebsite.com">
建立连接不仅需要 DNS 查询,还需要进行 TCP 协议握手,有些还会有 TLS/SSL 协议,这些都会导致连接的耗时
使用预连接时浏览器处理:
浏览器也不一定完成连接,视情况
<link rel="preconnect" href="//sample.com" crossorigin>
// 值不写具体的 use-credentials 都相当于设置成 Anonymous