前端会80%进大厂系列---阅读笔记(施工中)

tip:只记录本人记得不牢固的,或者有启发的点,新手建议多看书

JS 部分


1、原型链

实例对象的constructor也会指向构造函数
因为没有constructor属性会通过原型链找(容易忽略,是个小陷阱)

function Person() {
     }
var person = new Person();
console.log(person.constructor === Person); // true

__proto__

来自于 Object.prototype,更像是一个 getter/setter,使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)



2、继承

原型链继承:子函数的原型是父函数的实例对象。
缺点不能传参,引用属性共享

构造函数继承:子函数中通过call调用父函数,改变this
缺点:每次都要调用父函数

组合继承缺点:调用两次父构造函数

一次是设置子类型实例的原型的时候:

Child.prototype = new Parent();

一次在创建子类型实例的时候:

var child1 = new Child('kevin', '18'); // 调用了Child中的Parent.call(this, name);


3、作用域链

新版ES2018中规定执行上下文包含了:
词法环境(这就是旧版的作用域链和this合在一起)
变量环境
…其他

[[scope]]中保存了当前函数的作用域链,这个属性无法访问,属于内部属性
前端会80%进大厂系列---阅读笔记(施工中)_第1张图片

函数执行上下文中,作用域链 和 变量对象 的创建过程

简单栗子:

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 // 只剩全局上下文
];


4、闭包

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的值,就不会再去全局上下文找,所以值是对的



5、变量对象

在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。

在全局上下文中,全局对象就是变量对象

只有当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以叫activation object

执行上下文的代码分成两个阶段:

1)进入执行上下文初始化

变量对象包括:

  1. 函数的所有形参 (如果是函数上下文)
  2. 函数声明,后声明的会覆盖之前的
  3. 变量声明,不会干扰已存在的同名形参或者函数名

简单栗子

AO = {
     
    arguments: {
     
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){
     },
}

2)执行代码

根据代码修改AO中的值



6、this

ECMAScript的类型分为两种:语言类型、规范类型

语言类型 就是7种基本类型:string,number,bigint,boolean,null,undefined,symbol 和一种引用类型:obj

规范类型 用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型,用来描述语言底层行为逻辑。包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。

Reference

定义: 用来解释诸如 delete、typeof 以及赋值等操作行为

三部分组成:

  • base value (属性所在的对象或者是EnvironmentRecord,值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种)
  • referenced name (属性名称)
  • strict reference (是否是严格引用)

Reference 组成部分的方法,比如 GetBase 和 IsPropertyReference。

两个组成部分的方法

1.GetBase

返回 reference 的 base value

2.IsPropertyReference

简单的理解:如果 base value 是一个对象,就返回true。

GetValue:用于从 Reference 类型获取对应值的方法

调用 GetValue,返回的将是具体的值,而不再是一个 Reference

如何确定this的值

步骤:

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 ?

  • PrimaryExpression // 原始表达式 可以参见《JavaScript权威指南第四章》
  • FunctionExpressio // 函数定义表达式
  • MemberExpression [ Expression ] // 属性访问表达式
  • MemberExpression . IdentifierName // 属性访问表达式
  • new MemberExpression Arguments // 对象创建表达式

说白了就是比如 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 类型, thisundefined
------------------------------------------------
(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 值



7、立即执行函数表达式(IIFE)

先看一组比较:

function foo(){
     }() 报错,js解析器会当成函数声明

var foo = function(){
     console.log(1)}() 可以执行

function foo(){
     }(1) 不会报错,等同于下面的代码
function foo(){
     }
(1)

在 js 里圆括号中不能包含声明,所以一般使用此方法将函数声明变成表达式

用类似 JQ 的返回对象来做私有变量会更好点,也是早期的模块化



8、instanceof 和 typeof 的实现原理

js 如何存储数据类型信息

js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息

  • 000:对象
  • 010:浮点数
  • 100:字符串
  • 110:布尔
  • 1:整数

两个特殊值:
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__ 
    }
}


9、bind

特点:

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; // 第一个特点,返回函数
}


10、call 和 apply

第一个参数指定为 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
}


11、柯里化

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])
      }
    }
  }

}


12、垃圾回收

v8引擎的内存限制

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 线程的值,但其他的值只针对当前线程

垃圾回收策略

总结:基于分代式垃圾回收机制,根据对象的存活时间将内存进行不同的分代,然后采用不同的垃圾回收算法

V8的内存结构

分为几个部分:

  • 新生代(new_space):大多数的对象开始都会被分配在这里,这个区域相对较小但是垃圾回收特别频繁。该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来。
  • 老生代(old_space):新生代中的对象在存活一段时间后就会被转移到老生代内存区,垃圾回收频率较低。老生代又分为老生代指针区和老生代数据区,前者包含大多数可能存在指向其他对象的指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。
  • 大对象区(large_object_space):存放体积超越其他区域大小的对象,每个对象都会有自己的内存,垃圾回收不会移动大对象区。
  • 代码区(code_space):代码对象,会被分配在这里,唯一拥有执行权限的内存区域。
  • map区(map_space):存放Cell和Map,每个区域都是存放相同大小的元素,结构简单
新生代

构成:两个 semispace (半空间)
使用算法:Scavenge算法,牺牲空间换时间。老生代内存生命周期长,可能会存储大量对象,不适用这种算法

具体实现使用了 Cheney 算法。
1、激活状态的区域叫做 From 空间,垃圾回收时把 From 空间中不能回收的对象复制到 To 空间
2、清除 From 中所有的非存活对象,两个空间呼唤身份

缺点:浪费空间,一半的内存用于复制

反思:为什么不标记完直接清除,而使用 Scavenge ,应该也是为了整理内存碎片

对象晋升

两个条件满足其一:

  • 对象是否经历过一次Scavenge算法
  • To空间的内存占比是否已经超过25%(防止变成 From 空间后,后续对象内存分配时内存过高溢出)
老生代

使用算法:Mark-Sweep (标记清除) 和 Mark-Compact (标记整理)

总步骤:标记、整理、清除

1)Mark-Sweep (标记清除)

详细步骤:

  1. 垃圾回收器在内部构建一个根列表, 保存所有的根节点
  2. 从所有根节点出发,遍历其可以访问到的子节点,标记为活动的
  3. 释放所有非活动的内存块

根节点类型

  1. 全局对象
  2. 本地函数的局部变量和参数
  3. 当前嵌套调用链上的其他函数的变量和参数

问题

一次标记清除后,内存空间可能会出现不连续的状态-----内存碎片
后面如果需要分配一个大对象而空闲内存不足以分配,就会提前触发垃圾回收,所以需要 标记整理

2)Mark-Compact (标记整理)

详细步骤:

  1. 将所有活动对象往堆内存的一端移动

3)性能提升

全停顿:由于 JS 是单线程的,垃圾回收的过程会阻塞主线程同步任务

增量标记:标记、交给主线程、回到标记暂停的地方继续标记

如果在老生代中,对堆内存中所有的存活对象遍历,势必会造成性能问题。
于是 V8 引擎先标记内存中的一部分对象,然后暂停,将执行权重新交给 JS 主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。
挺像使用 setTimeout 优化技巧,也是把一个大的任务拆成很多个小任务,这样就可以间断性的渲染 UI,不会有卡顿的感觉

基于增量标记, V8 引擎后续继续引入了延迟清理(lazy sweeping)和增量式整理(incremental compaction)、并行标记、并行清理

如何避免内存泄漏

避免使用全局变量:因为 window 对象可以作为根节点,上面的属性都是常驻的

手动清除定时器

少用闭包

清除DOM引用:对保存在属性中的 dom 引用及时释放成 null

使用弱引用:WeakMap 和 WeakSet 中的引用都是弱引用,只要对象没有其他的引用,这个对象中所有属性的内存都会被释放掉



13、浮点数精度

数字类型

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的值。

3)52 位存储 Fraction。
前端会80%进大厂系列---阅读笔记(施工中)_第2张图片
0.1 对应的二进制

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,所以就被舍掉了



14、new

特点:
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; // 判断返回值

};


15、事件循环

特点:

当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。
同一次事件循环中,微任务永远在宏任务之前执行。、

node环境

node 选择 chrome v8 引擎作为js解释器,v8 引擎将 js 代码分析后去调用对应的 node api,而这些 api 最后则由 libuv 引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。

实际上node中的事件循环存在于libuv引擎中

前端会80%进大厂系列---阅读笔记(施工中)_第3张图片
poll 阶段

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


16、Promise

手写代码链接

17、Generator

for of 可以自动遍历迭代器的值

简易状态机(不用设初始变量,不用切换状态,更简洁,更安全)

let clock = function*() {
     
  while (true) {
     
    console.log('Tick!');
    yield;
    console.log('Tock!');
    yield;
  }
};

手写代码链接








CSS 部分


1、盒模型

默认情况下,块级元素的内容宽度是其父元素的宽度的100%,并且与其内容一样高。

内联元素高宽与他们的内容高宽一样

标准模型和IE模型的区别

IE模型元素宽度 width = content + padding + border,高度计算相同

标准模型元素宽度 width = content,高度计算相同

js 如何 设置 获取 盒模型对应的宽和高

  1. dom.style.width/height 只能取到行内样式的宽和高,style 标签中和 link 外链的样式取不到。
  2. window.getComputedStyle(dom).width/height 取到的是最终渲染后的宽和高, 多浏览器支持,IE9以上支持。
  3. dom.getBoundingClientRect().width/height 也是得到渲染后的宽和高,大多浏览器支持。IE9以上支持,除此外还可以取到相对于视窗的上下左右的距离

2、BFC

定义

决定了元素如何对其内容进行定位,以及与其他元素的关系和相互作用。提供了一个环境,一个环境中的元素不会影响到其他环境中的布局。

原理(渲染规则)

  1. BFC 元素垂直方向的边距会发生重叠。属于不同 BFC 外边距不会发生重叠
  2. BFC 的区域不会与浮动元素的布局重叠。
  3. BFC 元素是一个独立的容器,外面的元素不会影响里面的元素。里面的元素也不会影响外面的元素。
  4. 计算 BFC 高度的时候,浮动元素也会参与计算(清除浮动)

创建BFC

  1. html 根元素
  2. overflow不为visible
  3. float的值不为none
  4. position的值不为static或relative
  5. display属性为inline-blocks,table,table-cell,table-caption,flex,inline-flex

场景:防止 margin 合并、给普通盒子加上可以清除浮动,父元素加上 BFC 可以包含浮动子元素高度等

3、选择器

类别:

  1. 简单选择器: id 、class
  2. 属性选择器:通用语法由方括号([]) 组成,其中包含属性名称。[attr]、[attr=val]、[attr~=val](attr中包含val的元素,a[class~=“logo”],包含 logo 类名的 a),[attr^=val],[attr$=val],[attr*=val](包含 val 的元素)
  3. 伪类(Pseudo-classes):hover、active
  4. 伪元素(Pseudo-elements): ::after
  5. 组合器(Combinators):+ - > ~ (+ ~ 选择兄弟元素只会向后选择,不会选择前面的兄弟,+是相邻的兄弟)
  6. 多用选择器

4、Position

确定包含块:

完全依赖于这个元素的 position 属性

  1. position 属性为 static 、 relative 或 sticky:最近的祖先块元素(inline-block, block 或 list-item)的内容区的边缘组成
  2. position 属性为 absolute:最近的 position 的值不是 static 的祖先元素的内边距区的边缘
  3. position 属性是 fixed:连续媒体的情况下包含块是 viewport(视口),分页媒体是分页区域
  4. 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;
}

5、Flex

默认情况下,flex 容器中有一些设置:

元素不会在主维度方向拉伸,但是可以缩小。
元素被拉伸来填充交叉轴大小。
flex-basis 属性为 auto。
flex-wrap 属性为 nowrap。

注意交叉轴的拉伸,如果一些元素比其他元素高的话,会拉伸矮的元素
前端会80%进大厂系列---阅读笔记(施工中)_第4张图片
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(也就是能把内容显示出来)

6、样式优先级

从0开始,一个行内样式+1000,一个id选择器+100,一个属性选择器、class或者伪类+10,
一个元素选择器,或者伪元素+1,通配符+0

!important > 行内样式 > 内联样式 and 外联样式

样式指向同一元素,权重规则生效,权重大的被应用
样式指向同一元素,权重规则生效,权重相同时,就近原则生效,后面定义的被应用
样式不指向同一元素时,权重规则失效,就近原则生效,离目标元素最近的样式被应用

7、圣杯/双飞翼

相同点:中间栏要在放在文档流前面优先渲染。前一半是相同的,也就是三栏全部 float 浮动,左右两栏加上负 margin 让其跟中间栏 div 并排,以形成三栏布局。

不同点:解决”中间栏div内容不被遮挡“问题的思路不一样,圣杯使用相对定位配合 right和 left 属性,双飞翼通过 middle 的子元素使用 margin 为左右两栏留出位置

圣杯

  1. 4个元素:container、middle、left、right
  2. 父元素设置 overflow: hidden; 形成 BFC, 同时左右 padding 设置成左右子元素宽度
  3. 子元素全部 float:left;
  4. left、right 设置各自的宽度,同时 position: relative; left: -leftWidth, right 设置 right: -rightWidth;
  5. middle设置width: 100%;

双飞翼

  1. 5个元素:container、middle、middle 的儿子 inner、left、right
  2. 父元素设置overflow: hidden; 形成BFC, 同时左右 padding 设置成左右子元素宽度
  3. 子元素全部 float:left;
  4. left、right 设置各自的宽度
  5. middle 设置width: 100%;
  6. inner 设置左右边距为左右栏宽度,为左右栏腾出宽度

注意:左栏 margin-left: -100% 以包含块内容区左侧(当然以相邻元素右侧 margin 为基准也可以,一个道理)为基准线,负值表示向基准线移动靠近

同时给 left、middle、right设置上 padding-bottom: 9999px; margin-bottom: -9999px; 可以形成三列保持等高(利用背景会显示在 padding 区域,视觉上欺骗,只能用与纯色背景)

8、margin 负值原理

  1. left 负值就是以包含块(Containing Block)内容区域的左边 或 该元素左侧相连元素 margin 的右边为参考线
  2. top 负值就是以包含块(Containing Block)内容区域的上边 或 该元素上方相连元素 margin 的下边为参考线
  3. right 负值是以元素自身的 border-right 为参考线
  4. bottom 负值是以元素自身的 border-bottom 为参考线

多列等高布局原理就是通过 padding 撑开盒子,同时相同的 负margin 告诉浏览器计算文档流布局时减去对应的值,让下方的元素上来占据位置。同时父元素 overflow:hidden 形成 BFC 并且遮挡超出部分,以最高元素为准。

还可以给 ul 加上负 margin以消除每行最后一项的正margin

9、CSS3新特性

过渡

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;
}

10、CSS模块化

手写时代

行内样式缺点

  • 样式不能复用。
  • 样式权重太高,样式不好覆盖。
  • 表现层与结构层没有分离。
  • 不能进行缓存,影响加载效率。

导入样式缺点

  • 导入样式,只能放在 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
前端会80%进大厂系列---阅读笔记(施工中)_第5张图片

11、CSS 性能优化

  1. 合并 css 文件,如果页面加载10个css文件,每个文件1k,那么也要比只加载一个100k的css文件慢。
  2. 减少 css 嵌套,最好不要嵌套三层以上。
  3. 不要在 ID 选择器前面进行嵌套,ID本来就是唯一的而且权限值大,嵌套完全是浪费性能。
  4. 建立公共样式类,把相同样式提取出来作为公共类使用。
  5. 减少通配符 * 或者类似 [hidden=“true”] 这类选择器的使用,挨个查找所有…这性能能好吗?
  6. 巧妙运用css的继承机制,如果父节点定义了,子节点就无需定义。
  7. 拆分出公共 css 文件这样一次下载后就放到缓存里,当然这种做法会增加请求,具体做法应以实际情况而定。
  8. 不用 css 表达式,对性能的浪费可能是超乎你想象的。
  9. 少用 css rest,可能会觉得重置样式是规范,但是其实其中有很多操作是不必要不友好的,有需求有兴趣,可以选择 normolize.css。
  10. cssSprite,减少了 http 请求。
  11. 善后工作,css压缩(在线压缩工具 YUI Compressor)
  12. GZIP压缩

避免使用@import

  1. 影响浏览器的并行下载
  2. 多个@import会导致下载顺序紊乱

避免过分重排 与 重绘

  1. 一个节点触发来reflow,会导致他的子节点和祖先节点重新渲染
  2. 常见重排元素
  1. 大小有关的 width,height,padding,margin,border-width,border,min-height
  2. 布局有关的 display,top,position,float,left,right,bottom
  3. 字体有关的 font-size,text-align,font-weight,font-family,line-height,white-space,vertical-align
  4. 隐藏有关的 overflow,overflow-x,overflow-y
  1. 建议
  1. 不要一条条的修改 dom 样式,每一次设置都会触发一次reflow,预先定义好 class,然后修改 dom 的 classname
  2. 不要修改影响范围较大的 dom
  3. 动画元素使用绝对定位
  4. 不要table布局,因为一个很小的改动会造成整个table重新布局
  1. 常见重绘元素
  1. 颜色 color,background
  2. 边框样式 border-style,outline-color,outline,outline-style,border-radius,box-shadow,outline-width
  3. 背景相关 background,background-image,background-position,background-repeat,background-size
  1. tips:选择器是从右向左匹配的,出于性能考虑,选择器选择时大部分元素是不会被选择的

12、层叠上下文

定义:浏览器三维概念,Z轴上的每一层可以视为一个层叠上下文

层叠水平

同一个层叠上下文中用来区分元素距离用户的远近,所有元素都有层叠水平,z-index 只能影响定位元素以及 flex 盒子的孩子元素

层叠顺序

前端会80%进大厂系列---阅读笔记(施工中)_第6张图片

内容为王,所以内联元素在上

层叠准则

文档流后面的元素会覆盖前面的

明显的层叠水平标示时,谁大谁在上

比较时先比父级

常见的层叠上下文的创建

1、页面根元素

2、设置了 z-index 的定位元素

3、设置了 z-index 的 flex 的子元素

4、元素的opacity值不是1

5、元素的transform值不是none

13、居中

flex

绝对定位 + transform

绝对定位 left right top bottom为 0,margin 为 auto

这种方案子元素不设置宽高,就可以铺满父级(用做遮罩层)

table-cell,一般不用

14、浮动元素

浮动元素只能影响行内元素,间接影响了包含块的布局

浮动元素只会浮动在文档流后面的块元素上,不会侵犯前面的块元素领地

让浮动元素撑开包含块:BFC、空内容伪元素设置 clear:both(把伪元素的边界放到所有浮动元素下面,所以撑开)、包含块自己也浮动(其实也是 BFC)

HTML & 浏览器

1、行内元素、块级元素

区别

块级元素:

① 总是在新行上开始,占据一整行;

② 高度,行高以及外边距和内边距都可控制;

③ 不加控制的话宽度会撑满浏览器,与内容无关;

④ 它可以容纳内联元素和其他块元素。

行内元素:

① 和其他元素都在一行上;

② 行高及外边距和内边距部分可改变(水平方向有效,竖直方向无效)。 如果是可替换元素,比如 input ,竖直方向是有效的

③ 宽度只与内容有关;

④ 行内元素只能容纳文本或者其他行内元素。

2、跨标签页通信

同源页面间的通信

BroadCast Channel

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);

Service Worker

本身不具备通信属性,但是可以作为后台长期运行的 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

特性:当 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));

上面三个属于订阅发布模式,下面两个是共享存储+轮询

Shared Worker

普通的 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});

IndexDB

轮询查询指定的数据是否被更新,不是很友好

window.open

window.open 会返回打开的页面的 window 对象引用,然后通过window.opener.postMessage(mydata) 发送消息

缺点:必须通过 window.open,并且只能一个传一个

非同源页面之间的通信

前端会80%进大厂系列---阅读笔记(施工中)_第7张图片

如上图,每个业务页面都有一个 iframe,所有 iframe 的 url 是相同的(也可以不同,同源就行),iframe 之间使用上面的同源页面的通信方式

此外还有基于服务端的:Websocket、SSE(服务端推送事件)

他俩区别:

  1. WebSocket 很复杂, SSE 简洁轻量
  2. WebSocket 是二进制协议,SSE 是文本协议(一般是 utf-8 编码),用 SSE 传输二进制数据时数据会变大,所以如果传输二进制数据还是 WS 厉害。
  3. WebSocket 最大的优势在于它是双向交流的,SSE 是单向的。如果需要1次/秒以上的频率,那么选 WS

3、hash 和 history 路由模式

路由需要实现的功能

  1. 浏览器地址变化,切换页面
  2. 点击【后退】、【前进】按钮,内容可以跟随变化
  3. 刷新浏览器,也可以显示当前路由对应内容

hash 模式

原理:使用 window.location.hash 属性及窗口的 onhashchange 事件

  1. hash 为 #号后面跟着的字符,也叫散列值。
  2. 散列值的改变不会触发浏览器请求服务器,从而导致页面重载

触发 hashchange 事件的几种情况

  • 散列值的变化(浏览器的前进、后退,JS 修改)
  • URL 直接输入带哈希的链接,请求完毕之后会触发
  • URL 只改变哈希的值按回车
  • a 标签的 href 属性设置

history模式

原理

  1. window.history 指向 History 对象,它表示当前窗口的浏览历史。当发生改变时,只会改变页面的路径,不会刷新页面。
  2. 浏览器工具栏的“前进”和“后退”按钮,其实就是对 History 对象进行操作

属性

  • History.length:当前窗口访问过的网址数量(包括当前网页)
  • History.state:History 堆栈最上层的状态值(默认为 undefined)

方法

History.back():移动到上一个网址,等同于浏览器的后退键。对于第一个访问的网址,该方法无效果。

History.forward():移动到下一个网址,等同于浏览器的前进键。对于最后一个访问的网址,该方法无效果。

History.go():接受一个整数作为参数,以当前网址为基准,移动到参数指定的网址。如果参数超过实际存在的网址范围,该方法无效果;如果不指定参数,默认参数为0,相当于刷新当前页面。

History.pushState:在历史中添加一条记录, 不会触发页面刷新,,三个参数: objecttitleurl,分别为传递给新页面的对象、标题、新的网址(必须同域,防止恶意代码让用户以为还在同站)

注意:URL 参数设置了一个新的锚点值(即 hash),并不会触发 hashchange 事件。

History.replaceState:修改当前历史记录,参数同上

事件  popstate

  1. 仅仅调用 pushState() 方法或 replaceState() 方法 ,并不会触发该事件;
  2. 只有用户点击浏览器倒退按钮和前进按钮,或者使用 JavaScript 调用 History.back()、History.forward()、History.go() 方法时才会触发。
  3. 该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。
  4. 页面第一次加载的时候,浏览器不会触发 popstate 事件。
  5. 回调函数的参数中的 state ===

缺点

改变页面地址后,强制刷新浏览器时会404,因为会触发请求,而服务器中没有这个页面,所以一般单页应用会全部重定向到 index.html 中

4、DOM 树

什么是 DOM

HTML 文件字节流无法直接被渲染引擎理解,需要转化为对 HTML 文档结构化的表述,也就是 DOM。

作用

  • 页面的视角:DOM 是生成页面的基础数据结构。
  • JavaScript 脚本视角:DOM 提供给 JS 脚本操作的接口,JS 可以访问 DOM 结构,改变文档的结构、样式和内容。
  • 安全视角:一道防线,一些不安全的内容在 DOM 解析阶段就被拒之门外了。

如何生成

渲染引擎内部,有一个叫 HTML 解析器(HTMLParser)的模块,将 HTML 字节流转换为 DOM 结构

HTML 解析器,是网络进程加载了多少数据,便解析多少数据。过程如下:

  1. 网络进程接收到响应头之后,根据响应头中的 content-type 字段来判断文件的类型,从而选择或创建一个渲染进程
  2. 渲染进程准备好之后,网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,并同时将读取的数据“喂”给 HTML 解析器

喂给数据之后,字节流转换为 DOM 的三个阶段:

  1. 分词器做词法分析,将字节流转换为 Token

    前端会80%进大厂系列---阅读笔记(施工中)_第8张图片

  2. Token 解析为 DOM 节点 3. 同时将 DOM 节点添加到 DOM 树中

    三种情况:

    1. 如果入栈的是StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。
    2. 如果是文本 Token,会生成一个文本节点,然后将该节点加入到 DOM 树中。文本 Token 不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点。
    3. 如果是 EndTag 标签,HTML 解析器会查看 Token 栈顶的元素是否是 StartTag div,如果是,就将 StartTag div 从栈中弹出,表示该 div 元素解析完成。

    简单示例图:

    前端会80%进大厂系列---阅读笔记(施工中)_第9张图片

JavaScript 是如何影响 DOM 生成的

一、内嵌 js

<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 结构,执行完毕之后继续解析,流程是一样的。

二、外部引入 js

<html>
<body>
    <div>1div>
    <script type="text/javascript" src='foo.js'>script>
    <div>testdiv>
body>
html>

chrome 有一个优化操作,当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件

解析过程同上是一样的

三、JS 中有操作 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 的执行

四、优化操作

  1. CDN 加速

  2. 压缩文件的体积

  3. 如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过 async 或 defer 来标记代码。

    二者都是异步的,但使用 async 标志的脚本文件一旦加载完成,会立即执行;而使用了 defer 标记的脚本文件,需要在 DOMContentLoaded 事件之前执行。

5、事件

EventTarget 接口

addEventListener 的第三个参数默认是 false 冒泡,还可以设置为属性配置对象

  • capture:布尔值,是否在捕获阶段触发。
  • once:布尔值,监听函数是否只触发一次,然后自动移除。
  • passive:布尔值,表示监听函数不会调用事件的preventDefault方法。如果监听函数调用了,浏览器将忽略这个要求,并在监控台输出一行警告。

当添加多个监听时,先添加先触发

removeEventListener 没有返回值

dispatchEvent 手动触发事件,参数为某个 event, 比如 click

事件模型

三种绑定事件方法

  1. 标签上直接使用 on-xxxx,这种方式只会在冒泡阶段触发,必须加圆括号执行
  2. 元素对象使用 onclick 等事件,window.onload = doSomething,不用加圆括号
  3. addEventListener

事件的传播

  • 第一阶段:从 window 对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。
  • 第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。
  • 第三阶段:从目标节点传导回 window 对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。

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(显示器)

前端会80%进大厂系列---阅读笔记(施工中)_第10张图片

6、缓存机制

HTTP 报文

HTTP请求报文格式

请求行

HTTP头(通用信息头,请求头,实体头)

请求报文主体(只有POST才有报文主体)

前端会80%进大厂系列---阅读笔记(施工中)_第11张图片

HTTP报文格式为:

状态行

HTTP头(通用信息头,响应头,实体头)

响应报文主体

前端会80%进大厂系列---阅读笔记(施工中)_第12张图片

缓存过程

  1. 浏览器每次发起请求,先在浏览器缓存中查找请求的结果以及缓存标识
  2. 浏览器每次拿到返回的请求结果,都会将该结果和缓存标识存入浏览器缓存中

浏览器是否需要向服务器重新发送 HTTP 请求,取决于 我们选择的缓存策略

强制缓存

三种情况

  1. 不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求
  2. 存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存
  3. 存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果

Expires

HTTP/1.0 的字段,值是服务器返回的过期时间。

缺点:时区不同的话,客户端和服务端有一方的时间不准确发生误差,那么强制缓存则会直接失效

Cache-Control

HTTP/1.1 的字段

  1. public:所有内容都将被缓存(客户端和代理服务器都可缓存)
  2. private:所有内容只有客户端可以缓存,Cache-Control 的默认取值
  3. no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定
  4. no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
  5. max-age=xxx (xxx is numeric):缓存内容将在 xxx 秒后失效

注意:

刷新:浏览器会在 js 和图片等文件解析执行后直接存入内存缓存中,刷新页面从内存缓存中读取(from memory cache);而css文件则会存入硬盘文件中,每次渲染页面都需要从硬盘读取缓存(from disk cache)。

关闭再打开:之前的进程内存已清空,所以都是硬盘缓存

协商缓存

缓存结果失效后,根据缓存标识发送 HTTP 请求,服务器进行判断

标识

Last-Modified / If-Modified-Since

前者:响应头中,表示文件在服务器最后被修改的时间

后者:请求头,值同上,告诉服务器进行判断,文件是否改变,没变则使用缓存,变了就返回最新的

Etag / If-None-Match

前者:响应头中,表示文件在服务器中唯一标识

后者:请求头,值同上,告诉服务器进行判断,文件是否改变,没变则使用缓存,变了就返回最新的

注:Etag / If-None-Match 优先级高于 Last-Modified / If-Modified-Since,同时存在则只有Etag / If-None-Match生效。

总结

  1. 强制缓存优先于协商缓存进行,若强制缓存( Expires 和 Cache-Control )生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match)
  2. 协商缓存由服务器决定是否使用缓存,若协商缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存
  3. 优先内存,再硬盘

7、Chrome 浏览器架构

进程、线程、协程

一个进程是应用正在运行的程序,操作系统会为进程分配私有的内存空间以供使用。

协程是运行在线程中更小的单位,async/await 就是基于协程实现的。

进程间通信(IPC)

一个进程可以让操作系统开启另一个进程处理不同的任务。进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,这就是IPC(Inter Process Communication)。

前端会80%进大厂系列---阅读笔记(施工中)_第13张图片

套接字(socket)

凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行

套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型。

三种套接字:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取 TCP 协议的数据,数据报套接字只能读取 UDP 协议的数据。

管道/匿名管道(pipe)

  1. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
  2. 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
  3. 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
  4. 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据(队列)。

有名管道(FIFO)

相比上面可以非亲缘关系

浏览器架构

一、多进程架构(每个页面都是单独的)

浏览器进程(Browser process)

管理 Chrome 应用本身,包括地址栏、书签、前进和后退按钮。同时也负责网络请求、文件访问等,也负责其他进程的调度。

渲染进程(Renderer process)

渲染进程负责站点的渲染,其中也包括 JavaScript 代码的运行,web worker 的管理等。

插件进程(Plugin process)
GPU 进程(GPU process)

GPU 进程负责提供成像的功能

好处
  1. 一个页面没有相应不会阻塞其他页面
  2. 借助操作系统对进程安全的控制,浏览器可以将页面放置在沙箱中,核心进程代码可以运行在隔离的环境中,保证安全。
缺点
  1. 相同功能无法共用,会浪费内存,比如 V8 引擎
  2. Chrome 限制了最大进程数,为了节省内存,最大进程数取决于硬件的能力。当使用多个页签访问相同的站点时,浏览器不会创建新的渲染进程

二、面向服务的架构

当 Chrome 运行在拥有强大硬件的计算机上时,会将一个服务以多个进程的方式实现,提高稳定性

当计算机硬件资源紧张时,则可以将多个服务放在一个进程中节省资源。

三、iframe

出于安全考虑,从 Chrome 67 开始每个 iframe 打开的站点由独立的渲染进程处理被默认启用。

浏览器进程

包括几个线程

  • UI 线程负责绘制工具栏中的按钮、地址栏等。
  • 网络线程负责从网络中获取数据。
  • 存储线程负责文件等功能。

一次页面访问

一、输入处理

UI 线程会先判断我们输入的内容是要搜索的内容还是要访问一个站点,因为地址栏同时也是一个搜索框。

二、访问开始

按下回车访问,UI 线程将借助网络线程访问站点资源,网络线程根据适当的网络协议,例如 DNS lookup 和 TLS 为这次请求建立连接

三、处理响应数据

根据 Content-Type ,如果是 HTML ,网络线程会将数据传递给渲染进程做进一步的渲染工作。

如果数据类型是 zip 文件或者其他文件格式时,会将数据传递给下载管理器做进一步的文件预览或者下载工作

在开始渲染之前,网络线程要先检查数据的安全性。如果返回的数据来自一些恶意的站点,网络线程会显示警告的页面。同时,Cross Origin Read Blocking(CORB) 策略也会确保跨域的敏感数据不会被传递给渲染进程。

四、渲染过程

在第二步,UI 线程将请求地址传递给网络线程时,UI 线程就已经知道了要访问的站点。此时 UI 线程就同时查找或启动一个渲染进程。如果网络线程按照预期获取到数据,则渲染进程就已经可以开始渲染了,减少了等待时间。

当然,如果出现重定向的请求时,提前初始化的渲染进程可能就不会被使用,但相比正常访问站点的场景,重定向往往是少数。

五、提交访问

当数据和渲染进程后,浏览器进程通过 IPC 向渲染进程提交这次访问,同时也会保证渲染进程可以通过网络线程继续获取数据。渲染进程在所有 onload 事件都被触发后向浏览器进程发送完毕的消息,访问结束,文档渲染开始。

这时可能还有异步的 js 在加载资源

为了能恢复访问历史信息,当页签或窗口被关闭时,访问历史的信息会被存储在硬盘中。

访问不同的站点

当访问其他页面时,一个独立的渲染进程将被用于处理这个请求,为了支持像unload的事件触发,老的渲染进程需要保持住当前的状态,知道用户做出选择。

Service worker

开发者可以决定用本地存储的数据还是网络访问。当访问开始时,网络线程会根据域名检查是否有 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 线程会负责运行一部分代码。合成线程和光栅线程也是运行在渲染进程中的,负责更高效和顺畅的渲染页面。

解析过程

DOM 的创建

主线程解析 HTML 文本字符串,并且将其转化成 Document Object Model(DOM),静默处理标签的丢失、未闭合等错误

1.额外资源的加载

当 HTML 主解析器发现了类似 img 或 link 这样的标签时,预加载扫描器(副解析器)就会启动,它会马上找出接下来即将需要获取的资源(比如样式表,脚本,图片等资源)的 URL ,然后发送请求给浏览器进程的网络线程,而不用等到主解析器恢复运行,从而提高了整体的加载时间

2.JavaScript 会阻塞转化过程

前端会80%进大厂系列---阅读笔记(施工中)_第14张图片

解析执行还是要等主线程空闲,并且只能读到 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 的离散事件会立即被发送给主线程处理。

8、浏览器工作原理

高层结构

  1. 用户界面 - 包括地址栏、前进/后退按钮等。除了浏览器主窗口显示的请求的页面外,其他都属于用户界面。
  2. 浏览器引擎 - 在用户界面和呈现引擎之间传送指令。
  3. 呈现引擎(应该也叫做渲染引擎)- 负责显示请求的内容。如果返回 HTML,它就负责解析 HTML 和 CSS 内容,显示在屏幕上。
  4. 网络 - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
  5. 用户界面后端 - 绘制基本的窗口小部件,比如组合框和窗口。使用与平台无关的通用接口,在底层使用操作系统的用户界面方法。
  6. JavaScript 解释器。用于解析和执行 JavaScript 代码。
  7. 数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie

呈现引擎主流程

解析是什么

定义:将文档转化成为有意义的结构,称作解析树或者语法树

过程:词法分析 和 语法分析 ,迭代过程

1.词法分析器

将输入内容分解成一个个有效标记,将无关的字符(比如空格和换行符)分离出来

2.解析器

根据语言的语法规则分析文档的结构,构建解析树(由 DOM 元素和属性节点构成的树结构)。

解析器向词法分析器请求一个新标记,尝试将其与某条语法规则进行匹配。

如果发现了匹配规则,解析器会将一个对应于该标记的节点添加到解析树中,然后继续请求下一个标记。如果没有规则可以匹配,解析器就会将标记存储到内部,并继续请求标记,直至所有内部存储的标记都有对应匹配的规则。如果找不到,解析器就会引发一个异常。这意味着文档无效,包含语法错误。

HTML 解析

无法用常规的 自上而下 或 自下而上 的解析器进行解析,原因在于:

  1. 语言的宽容本质。
  2. 浏览器对一些常见的无效 HTML 用法采取包容态度。
  3. 解析过程需要不断地反复。源内容在解析过程中通常不会改变,但是在 HTML 中,js 如果包含 document.write,就会添加额外的标记,这样解析过程实际上就更改了输入内容。

所以使用专有的 标记化算法(状态机)树构建算法(状态机)

标记化算法:

  1. 初始状态是数据状态
  2. 遇到字符 < 时,状态更改为“标记打开状态”
  3. 遇到标签名时,“标记名称状态”
  4. 遇到 > 标记时,会发送当前标记给构建器,状态改回“数据状态”
  5. 遇到标签中的每一个字符时,会创建发送字符标记,知道遇到下一个 <

树构建算法:根据接收的标记,创建并插入对应的 DOM 元素,改变对应的状态。

“initial mode”、“before html”、“before head” 之类的状态

CSS 解析

  1. 预加载扫描器(预解析器)会提前去请求如CSS、JavaScript和web字体。

  2. 构建 render 树(也叫呈现树、渲染树):非可视化的 DOM 元素不会插入呈现树中,处理 html 和 body 标记就会构建呈现树根节点,对应于 CSS 规范中所说的容器 block,也是最上层的 block

    浏览器利用规则树来优化构建时的样式计算,保存计算过的匹配路径重复使用

    这里没有说 cssom树,其实就是把 css 解析成树的结构

布局

呈现树中的元素(也叫呈现器),并不包含位置和大小信息。计算这些值的过程称为布局或重排。

1.Dirty 位系统:浏览器给每个需要重新布局的元素进行标记

“dirty” 和 “children are dirty”一个表示自身,一个表示至少有一个子代

2.全局布局(同步)和增量布局(异步)

全局布局是指触发了整个呈现树范围的布局,触发原因可能包括:

  1. 字体大小更改。
  2. 屏幕大小调整。

增量布局:当来自网络的额外元素添加到 DOM 树之后

绘制

系统遍历呈现树,并调用呈现器的“paint”方法,将呈现器布局阶段计算的每个框转换为屏幕上的实际像素

绘制可以将布局树中的元素分解为多个层。将内容提升到GPU上的层(而不是CPU上的主线程)可以提高绘制和重新绘制性能,但会以内存管理为代价

合成

当文档的各个部分以不同的层绘制,相互重叠时,必须进行合成,以确保它们以正确的顺序绘制到屏幕上,并正确显示内容。

呈现引擎的线程

单线程,在 Firefox 和 Safari 中,该线程就是浏览器的主线程。而在 Chrome 浏览器中,该线程是标签进程的主线程

9、内存泄漏

什么是内存

由大量触发器组成,每个触发器包含几个晶体管,能够存储一个位。单个触发器可以通过唯一标识符寻址,我们可以读取和覆盖它们。

内存生命周期

内存分配 -> 内存使用 -> 内存释放

强弱引用的垃圾回收区别

const map = new Map([[obj, 'info']])
obj = null // 重写obj,obj 代表的内存不会被回收
const map = new WeakMap([[obj, 'info']])
obj = null // 重写obj,obj 代表的内存会被回收

内存泄漏的一些场景

  1. 意外的全局变量

  2. 被遗忘的计时器(vue 组件中的一定要在 beforeDestroy 时清掉)

  3. 被遗忘的事件监听器(同上)

  4. 被遗忘的订阅发布事件监听器,需要用 off 删掉(同上)

  5. 强引用中没有使用 api 释放,只是单纯删除掉变量的引用

    let map = new Set();
    let value = {
            test: 22};
    map.add(value);
    
    map.delete(value); // 有效
    value = null; // 无效
    
  6. 被遗忘的未使用的闭包

  7. 脱离 DOM 的引用

    let elements = {

    btn: document.querySelector(’#button’)

    }

    document.body.removeChild(elements.btn)

    // elements .btn = null 加上这一句才不泄露,因为 DOM 占用的那块内存还被对象引用

发现内存泄漏

  1. 打开谷歌开发者工具,切换至 Performance 选项,勾选 Memory 选项,点击运行按钮

    前端会80%进大厂系列---阅读笔记(施工中)_第15张图片

    上图红框内就是内存变化,如果是一直递增,那基本可以确定存在泄漏

  2. 切换至 Memory 选项,点击运行获取网页快照

    前端会80%进大厂系列---阅读笔记(施工中)_第16张图片

    根据内存占用大小,点击左侧元素,再找到具体的文件与代码位置即可

10、性能

优化性能指标 RAIL

含义:

  • Response
  • Animation
  • Idle
  • Load

Response: 事件处理最好在 50ms 内完成

  1. 事件处理函数在 50ms 内完成,考虑到 idle task 的情况,事件会排队,等待时间大概在50ms。适用于click,toggle,starting animations 等,不适用于 drag 和 scroll 。
  2. 复杂的 js 计算尽可能放在后台,如 web worker,避免对用户输入造成阻塞
  3. 超过 50ms 的响应,一定要提供反馈,比如倒计时,进度百分比等。

Animation: 在10ms内产生一帧

  • 为了保证浏览器60帧,每一帧的时间在16ms左右,但浏览器需要用 6ms 来渲染每一帧。

Idle: 最大化空闲时间

每一次事件循环结束时的空闲时间,完成一些延后的工作,比如加载剩余不可见页面。 requestIdleCallback API

Load: 传输内容到页面可交互的时间Time to Interactive(TTI)不超过5秒

  • 让你的页面在一个中配的3G网络手机上打开时间不超过5秒
  • 对于第二次打开,尽量不超过2秒

测试与优化

  1. F12选用中配的3G网络(400kb/s,400ms RTT)
  2. 延后加载阻塞渲染的资源,
  3. 可以采用 lazy load,code-splitting 等 其他优化 手段,让第一次加载的资源更少

性能优化手段

从输入URL按下回车开始,每一步可以做的优化如下

一、缓存

本地数据存储

localStorage、sessionStorage、indexedDB,对于一些特殊的、轻量级的业务数据,可以考虑使用本地存储作为缓存(比如每日排行榜列表)

内存缓存(Memory)

浏览器帮我们实现的优化

Cache API

不规定该缓存什么、什么情况下需要缓存,也不必须搭配 Service Worker 。

当然 Service Worker 与 Cache API 还是一个功能非常强大的组合,能够实现堆业务的透明。

Cache API 提供的缓存可以认为是“永久性”的,关闭浏览器或离开页面之后,下次再访问仍然可以使用,每个域可以有多个不同的 Cache 对象。

navigator.storage.estimate().then(function(estimate) {
     
 console.log(estimate.quota)
      
});

153634836480 约等于 153GB
HTTP 缓存

如果前面的步骤都没没有命中缓存,就会到 HTTP request 的阶段

强缓存:直接读取「disk cache」,不够灵活,服务器更新资源不能及时通知

响应头:Expires 和 Cache-Control,前者设置过期时间,与本地时间对比,后者设置一个最大时间比如max-age=300,300s内走强缓存

协商缓存

  1. 最后修改时间:服务器第一次响应时返回 Last-Modified,而浏览器在后续请求时带上其值作为 If-Modified-Since(精度不够,如果时间很短)
  2. 文件标识:服务器第一次响应时返回 ETag,而浏览器在后续请求时带上其值作为 If-None-Match,一般会用文件的 MD5 作为 ETag
Push Cache

最后一个缓存检查

HTTP/2 的 Push 功能所带来的。请求一个资源的同时,服务端可以为你“推送”一些其他资源 --不久的将来会用到的一些资源。比如样式表,避免了浏览器收到响应、解析到相应位置时才会请求所带来的延后

特点:

  1. 匹配上时,并不会在额外检查资源是否过期
  2. 存活时间很短,甚至短过内存缓存(Chrome 中为 5min 左右)
  3. 只会被使用一次
  4. HTTP/2 连接断开将导致缓存直接失效

二、请求

1)避免多余重定向
2)DNS 预解析

请求网站流程:

  1. 本地 hosts 文件中的映射
  2. 本地 DNS 缓存
  3. 在 TCP/IP 参数中设置的 DNS 查询服务器,也叫 本地 DNS
  4. 如果该服务器无法解析域名(没有缓存),且不需要转发,会向根服务器请求;
  5. 根服务器根据域名类型判断对应的顶级域名服务器(.com),返回给本地 DNS,然后重复该过程,直到找到该域名;
  6. 如果设置了转发,本地 DNS 会将请求逐级转发,直到转发服务器返回或者也不能解析。

上述服务前端不好切入,但可以通过设置属性,告诉浏览器尽快解析(并不保证,根据网络、负载等做决定)

<link rel="dns-prefetch" href="//yourwebsite.com">
3)预先建立连接

建立连接不仅需要 DNS 查询,还需要进行 TCP 协议握手,有些还会有 TLS/SSL 协议,这些都会导致连接的耗时

使用预连接时浏览器处理:

  • 首先,解析 Preconnect 的 url
  • 其次,根据当前 link 元素中的属性进行 cors 的设置
  • 然后,默认先将 credential 设为 true,如果 cors 为 Anonymous 并且存在跨域,则将 credential 置为 false
  • 最后,进行连接。

浏览器也不一定完成连接,视情况

<link rel="preconnect" href="//sample.com" crossorigin> 
// 值不写具体的 use-credentials 都相当于设置成 Anonymous 

三、服务端响应(了解)

1. 使用流进行响应

你可能感兴趣的:(JS,读书笔记,javascript,node.js,es6)