前端基础知识点回顾——JS篇

155 阅读27分钟

Javascript

1、this指针(默认绑定、显示绑定、隐式绑定、new绑定、箭头函数绑定)

1.1 默认绑定

函数调用时无任何调用前缀的情景。这种情景下由于函数调用时前面并未指定任何对象this指向全局对象window(严格模式下指向undefined

function fn1() {
    let fn2 = function () {
        console.log(this); //window
        fn3();
    };
    console.log(this); //window
    fn2();
};

function fn3() {
    console.log(this); //window
};

fn1();

严格模式

"use strict";
function fn1() {
    let fn2 = function () {
        console.log(this); //undefined
        fn3();
    };
    console.log(this); //undefined
    fn2();
};

function fn3() {
    console.log(this); //undefined
};

fn1();

1.2 隐式绑定

如果函数调用时,前面存在调用它的对象,this指向距离调用自己最近的对象(指向直接调用那个对象

function fn() {
    console.log(this.name);
};
let obj = {
    name: 'A',
    func: fn,
};
let obj1 = {
    name: 'B',
    func: fn,
    o: obj
};
obj1.o.func() // 'A'
obj.func() // 'A'
obj1.func() // 'B'

1.3 隐式丢失

在特定情况下会存在隐式绑定丢失的问题,最常见的就是作为参数传递以及变量赋值

// 参数传递
var name = 'A';
let obj = {
    name: 'B',
    fn: function () {
        console.log(this.name);
    }
};

function fn1(param) {
    param();
};
fn1(obj.fn); // 'A'

这个例子中我们将obj.fn 作为一个参数传递进fn1中执行,这里只是单纯传递了一个函数而已并没有调用,最后是在fn1中直接调用了(param),前面没有对象,所以this丢失这里指向了window

// 变量赋值
var name = 'A';
let obj = {
    name: 'B',
    fn: function () {
        console.log(this.name);
    }
};
let fn1 = obj.fn; // 这里只是赋值 没有调用
fn1(); //'A'  调用

与参数传递原理一样,最后是在fn1()调用,前面没有对象,所以指向window

1.4 显示绑定

显式绑定是指我们通过call、apply以及bind方法改变this的行为,相比隐式绑定,我们能清楚的感知this指向变化过程

1.4.1 call

call(this指向, 形参, 形参, 形参...)方法接收可接受多个参数,第一个是this指向,后面都是传递给函数的实参,可以是数字,字符串,数组等类型的数据类型都可以。

var person = {
  name: 'A',
  eat: d => {
    console.log(d)
  }
}
function fn1(d, age){
  console.log(this.name)  // 'A'
  console.log(age)    // 23
  this.eat(d)  // 'rice'
}
fn1.call(person,'rice',23)  // 改变fn1中的this指向,指向person对象

1.4.2 apply

apply(this指向, [形参])和call基本上一致,唯一区别在于传参方式,apply把需要传递给fn()的参数放到一个数组(或者类数组)中传递进去,虽然写的是一个数组,但是也相当于给fn()一个个的传递

 var person = {
  name: 'A',
  eat: d => {
    console.log(d)
  }
}
function fn1(d, age){
  console.log(this.name)  // 'A'
  console.log(age)    // 23
  this.eat(d)  // 'rice'
}
fn1.apply(person,['rice',23])

1.4.3 bind

bind(this指向, 形参, 形参, 形参...):语法和call一模一样,区别在于立即执行还是等待执行,bind不兼容IE6~8,bind需要再调用一次

var person = {
  name: 'A',
  eat: d => {
    console.log(d)
  }
}
function fn1(d, age){
  console.log(this.name)  // 'A'
  console.log(age)    // 23
  this.eat(d)  // 'rice'
}
// fn1.call(person,'rice',23)
fn1.bind(person,'rice',23)()  // 调用后才有输出

1.5 new绑定

我们先来看new做了什么事情

// 定义构造函数
function Person(name, age) {
    this.name = name;
    this.age = age;
}
// 定义原型方法
Person.prototype.say = function () {
    console.log("你好:", this.name)
}
// new一个对象
let obj = new Person("小钻风", 26);
obj.say(); // 你好:小钻风 
console.log(obj.name);  // "小钻风"
  1. 首先会创建一个新的空对象,如我们上面的obj。
  2. obj对象会继承构造函数即Person的原型,即obj.proto === Person.prototype,这样obj就可以调用Person上的原型方法了。
  3. 将Person的this指向obj,也就是将Person上的属性添加到新的obj对象上去,obj则可以调用Person中定义的属性了。
  4. 如果Person构造函数没有返回对象,则返回新创建的对象(即this),否则返回return的对象。

所以new的this最终指向他的实例

function Person(name){
  this.name = name
}
var p1=new Person('A')
var p2=new Person('B')
console.log(p1.name) // 'A'
console.log(p2.name) // 'B'

显式绑定 > 隐式绑定 > 默认绑定

new绑定 > 隐式绑定 > 默认绑定

1.6 箭头函数

箭头函数中没有this,箭头函数的this指向取决于外层作用域中的this,外层作用域或函数的this指向谁,箭头函数中的this便指向谁

var name = 'B'
var person = {
  name: 'A',
  sayHello: () => {
    console.log(this.name)
  }
}
var animal = {
  name: 'C',
  sayHello: function(){
    var s = () => {
      console.log(this.name)
    }
    s()
  }
}
person.sayHello() // 'B' 此处sayHello的作用域是window
animal.sayHello() // 'C' 此处sayHello的作用域是animal

除此之外,箭头函数this还有一个特性,那就是一旦箭头函数的this绑定成功,也无法被再次修改

function fn() {
    return () => {
        console.log(this.name);
    };
};
let obj1 = {
    name: '听风是风'
};
let obj2 = {
    name: '时间跳跃'
};
fn.call(obj1)(); // fn this指向obj1,箭头函数this也指向obj1
fn.call(obj2)(); // fn this指向obj2,箭头函数this也指向obj2

1.7 定时器

如果没有特殊指向,setInterval和setTimeout的回调函数中this的指向都是window。这是因为JS的定时器方法是定义在window下的。

var fn1 = {
  name: 'fn1',
  sayHello: function(){
    setTimeout(function(){
      console.log(this)
    },100)
  },
  fn2: {
    name: 'fn2',
    sayHello: function(){
    setTimeout(function(){
      console.log(this)
    },100)
  },
  }
}
fn1.sayHello() // window  
fn1.fn2.sayHello() // window

2、闭包

2.1 定义

能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量但是函数外部无法读取函数内的局部变量,如何从外部获取局部变量?

function f1() {
    var n = 999;
    function f2() {  
      alert(n);  
    }
    return f2;
}
var result = f1();
result(); // 999

此处f2就是一个闭包

2.2 闭包的用途

2.2.1 防抖节流

window.onresize = debounce(()=>{console.log('fn')},500) 
// 防抖 高频事件触发结束后deay后触发函数
function debounce(fn, deay){
    let timer; // 
    return function(){
        let args = arguments;
        let that = this
        if(timer) {
            clearTimeout(timer)
        }
        timer = setTimeout(() => {
            timer = null
            fn(that, args)
        },deay)
    }
}
// 节流 高频事件持续触发deay后触发函数
function throttle(fn, wait) {
    let timeout;
    return function () {
        let context = this
        let args = arguments
        if (!timeout) {
              timeout = setTimeout(function() {
                timeout = null
                fn.apply(context, args)
              }, wait)
        }
    }
}

2.2.2 延长局部变量的寿命

2.2.3 使外部能够访问到函数内部的变量

3、JS执行上下文和存储空间

3.1 JS执行上下文

浏览器并不能理解我们在应用程序中编写JS代码。它需要转换成一种浏览器和计算机都能理解的格式——机器代码。当通过HTML读取时,如果浏览器遇到要通过<script>标签或包含类似onClick的JS代码的属性运行的JS代码,它会将其发送给它的JS引擎。然后,浏览器的JS引擎创建一个特殊的环境来处理这段JS代码的转换和执行。这个环境称为执行上下文。执行上下文包含当前正在运行的代码,以及帮助其执行的所有内容。

JavaScript中有两种执行上下文:

  • 全局执行上下文(GEC)
  • 函数执行上下文(FEC)

3.1.1 执行上下文创建阶段

创建阶段又可以分为3个阶段,在这3个阶段中定义和设置执行上下文对象的属性。这些阶段是:

  1. 创建变量对象(VO):类对象容器,存储了在执行上下文中定义的变量和函数声明
  2. 创建作用域链:它决定代码库的其他部分如何访问一段代码
  3. 为变量赋值

3.1.2 执行上下文执行阶段

JavaScript是一种单线程语言,这意味着它一次只能执行一个任务。因此,当其他操作、函数和事件发生时,将为每个事件创建一个执行上下文。由于JavaScript的单线程特性,一个堆积的执行上下文堆栈被创建,称为执行堆栈

3.2 存储空间

1.代码空间:用来存放可执行代码。

2.栈空间:用来执行代码,在诸多JS相关文章中被称为调用栈,它承载执行上下文,以及上下文中包含的基本类型数据和引用类型数据的引用,空间小,空间连续,而且空间即用即清理,读取速度快,被设计为先进后出结构。

3.堆空间:用来存放复杂数据,基本数据(number、string、bool等),空间大,读取速度慢。

4、JS原型

4.1 原型prototype

每个函数都有一个属性——prototype,在默认情况下,prototype的值就是一个普通的object对象, 它有一个默认叫做constructor的属性,而constructor是用来指向这个函数本身
当一个函数被用作构造函数来创建实例时,这个函数的prototype属性值会被作为原型赋值给所有对象实例(也就是设置 实例的__proto__属性),也就是说,所有实例的原型引用的是函数的prototype属性

function Person(){} //创建一个构造函数
Person.prototype.name = 'Tom' //在Person构造函数的原型上创建name属性并赋值
Person.prototype.say = function(){ //在Person构造函数的原型上创建say方法 
    console.log('hello')
};
var person = new Person(); // 创建实例对象 => 设置实例对象的_proto_属性(指向构造函数Person的原型prototype)
console.log(person.name);
person.say();

4.1 隐式原型__proto__

所有对象上都有的一个属性,叫做隐式原型——__proto__,它指向创建该对象的构造函数的原型(prototype) ,即 fn._proto_ === Fn.prototype 当实例访问某个属性/方法时,会从自身查找。没找到会自动去__proto__里找

4.2 原型链

每个对象中都有一个 __proto__ 属性,这个属性指向了当前对象的构造函数的原型。对象可以通过自身的 __proto__属性与它的构造函数的原型对象连接起来,而因为它的原型对象(prototype)也有 __proto__,因此这样就串联形成一个链式结构,也就是我们称为的原型链。

4.3 继承

4.3.1 原型链继承

function Person(){
    this.age = [20,21]
}
Person.prototype.say = function(){
    console.log('hello world')
}
function Animal(){}

Animal.prototype = new Person()

const tiger = new Animal()
tiger.age.push(22)
console.log(tiger.age) // [20,21,22]
tiger.say() // 'hello world'

const lion = new Animal()
console.log(lion.age) // [20,21,22]

存在的问题:多个实例对引用类型的操作会被篡改,子类没有自己的私有属性

4.3.2 借用构造函数继承

function Person(){
    this.age = [20, 21]
}

Person.prototype.say = function(){ console.log('hello world') }

function Animal(){
    Person.call(this)
}

const tiger = new Animal()
tiger.age.push(22)
console.log(tiger.age) // [20,21,22]
tiger.say() // 报错

const lion = new Animal()
console.log(lion.age) // [20]

存在的问题:不能继承原型的方法/属性

4.3.3 组合继承

将原型链和借用构造函数的技术组合到一块。用原型链实现对原型属性和方法的继承,用借用构造函数技术来实现实例属性的继承

function Person(){
    this.age = [20,21]
}
Person.prototype.say = function(){
    console.log('hello world')
}
function Animal(){
    Person.call(this)
}

Animal.prototype = new Person()

const tiger = new Animal()
tiger.age.push(22)
console.log(tiger.age) // [20,21,22]
tiger.say() // 'hello world'

const lion = new Animal()
console.log(lion.age) // [20,21]

存在的问题:使用子类创建实例对象时无论什么情况下都会调用两次超类型的构造函数,并且创建的每个实例中都要屏蔽超类型对象的所有实例属性,造成了不必要的内存消耗。

4.3.4 传统继承-原型继承

在函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例

function person(obj){
    function F(){};
    F.prototype = obj;
    return new F();
}

var man = { age: [20] };

var woman = person(man);
woman.age.push(21)
console.log(woman.age) // [20,21]
console.log(man.age) // [20,21]

存在的问题:与原型链继承一样,数据会被篡改,子类没有自己的私有属性

4.3.5 寄生式继承

其实就是在原型式继承得到对象的基础上,在内部再以某种方式来增强对象,然后返回构造函数。

function person(obj){
    function F(){};
    F.prototype = obj;
    return new F();
}
function createPerson(obj){
    var clone = person(obj)
    return clone;
}

var man = { age: [20] };

var woman = createPerson(man);
woman.age.push(21)
console.log(woman.age) // [20,21]
console.log(man.age) // [20,21]

4.3.6 寄生组合式继承(圣杯式继承,最理想的继承方式)

结合借用构造函数传递参数和寄生模式实现继承,因此圣杯模式成为了JS最理想的继承式方法

function Person(name){
  this.name = name
  this.age = [20,21]
}
// 创建子类、添加子类属性。
function Animal(name){
  Person.call(this,name)// 执行父构造,将This指向本身,拉取父私有属性;
}

// 子类的原型对象指向父类的原型对象
// 浅拷贝 解决问题
Animal.prototype = Object.create(Person.prototype)
// 将constructor指向本身,保证原型链不断。
Animal.prototype.constructor = Animal

var tiger = new Animal()
tiger.age.push(22)
var lion = new Animal()

console.log(tiger.age)
console.log(lion.age)

圣杯式继承的核心在于只调用了一次Person构造函数,因此避免了在Animal.prototype 上创建不必要的、多余的属性。于此同时, 原型链还能保持不变;

4.4 原型的应用

hasOwnProperty

判断该属性或方法是否是只存在于实例对象而不存在于原型对象上

function Person(name){
    this.name = name
}
Person.prototype.say = function(){
    console.log('hello')
}
var person = new Person()
console.log(person.hasOwnProperty('name'))
console.log(person.hasOwnProperty('say'))

getPrototypeOf

获取对象的隐式原型

var obj = {}
var arr = []
console.log(Object.getPrototypeOf(obj) === Object.prototype) // true
console.log(Object.getPrototypeOf(arr) === Array.prototype) // true

console.log(obj.constructor === Object) // true
console.log(arr.constructor === Array) // true

instanceof

object instanceof constructor

用于检测构造函数(constructor)的 prototype 属性是否出现在某个实例对象(object)的原型链上

console.log({} instanceof Object) // true
console.log([1,2,3] instanceof Array) // true

5、Promise

5.1 Promise解决了什么问题

  • 解决回调地狱,层层嵌套问题
  • 代码可读性
  • 信任问题,Promise一旦被确认成功或失败,就不能再被更改,Promise成功之后仅调用一次resolve(),不会产生回调多次执行的问题

5.2 Promise规范

Promise规范有很多,如Promise/A,Promise/B,Promise/D 以及Promise/A+。ES6中采用的是Promise/A+ 规范。

Promise/A+规范标准:

  • 一个promise的当前状态只能是pending、fulfilled和rejected三种之一。状态改变只能是pending到fulfilled或者pending到rejected。状态改变不可逆。
  • promise的then方法接收两个可选参数,表示该promise状态改变时的回调(promise.then(onFulfilled, onRejected))。then方法返回一个promise,then方法可以被同一个 promise 调用多次。
    注:Promise/A+并未规范race、all、catch方法,这些是ES6自己规范的。
var p = new Promise(function(resolve, reject){
    console.log('执行')
    setTimeout(function(){
        resolve(2)
    }, 1000)
})
p.then(function(res){
    console.log('suc',res)
},function(err){
    console.log('err',err)
})

new Promise是同步任务(遇到先执行),Promise.resolve().then()是微任务

5.3 Promise.all和Promise.race

Promise.all 所有实例都成功,则该新的promise实例的状态也为成功,通过then方法可以按顺序打印出对应promise的值;如果有一个实例失败,则新的promise实例的状态为失败,通过catch方法可以打印失败的promise的值;如果有多个失败,则返回第一个失败的promise的值

let p1 = Promise.resolve('a')
let p2 = new Promise((resolve, reject)=>{
    setTimeout(()=>{
        resolve('b')
    },1000)
})
let p3 = Promise.resolve('c')
Promise.all([p1,p2,p3]).then(res=>{
    console.log(res) //  1秒后 打印['a','b','c']
}).catch(err=>{
    console.log(err)
})

Promise.race 和promise.all一样,只要有一个失败最后结果就是失败,但是只返回执行结果最快那个

6、事件循环

6.1 浏览器事件循环

(1)所有同步任务都在主线程上执行,形成一个执行栈

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。其中在当前执行的宏任务中,如果产生了微任务先加入微任务队列,等待当前宏任务执行完毕后 再执行微任务队列,最后是宏任务队列,因为浏览器执行时是一个宏任务+一个微任务队列(node 11之前是一整个宏任务队列+一个微任务队列。)

6.2 Node事件循环

node的事件循环比浏览器复杂很多。由6个宏任务队列+6个微任务队列组成。

宏任务按照优先级从高到低依次是:

image.png

其执行规律是:在一个宏任务队列全部执行完毕后,去清空一次微任务队列,然后到下一个等级的宏任务队列,以此往复。一个宏任务队列搭配一个微任务队列。

除此之外,node端微任务也有优先级先后:

    1. process.nextTick;
    1. promise.then 等;

清空微任务队列时,会先执行process.nextTick,然后才是微任务队列中的其他。

浏览器事件循环和Node事件循环区别:

浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js(Node 11之前)中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务

这里用下面的例子说明:

console.log('Script开始')
setTimeout(() => {
  console.log('第一个回调函数,宏任务1')
  Promise.resolve().then(function() {
    console.log('第四个回调函数,微任务2')
  })
}, 0)
setTimeout(() => {
  console.log('第二个回调函数,宏任务2')
  Promise.resolve().then(function() {
    console.log('第五个回调函数,微任务3')
  })
}, 0)
Promise.resolve().then(function() {
  console.log('第三个回调函数,微任务1')
})
console.log('Script结束')

结果:

node端(11版本之前,先执行完所有的setTimeout宏任务队列,再去执行味任务,11版本之后和浏览器一样):
Script开始
Script结束
第三个回调函数,微任务1
第一个回调函数,宏任务1
第二个回调函数,宏任务2
第四个回调函数,微任务2
第五个回调函数,微任务3

浏览器
Script开始
Script结束
第三个回调函数,微任务1
第一个回调函数,宏任务1
第四个回调函数,微任务2
第二个回调函数,宏任务2
第五个回调函数,微任务3

7、前端缓存

image.png 前端缓存主要是指浏览器缓存和http缓存,其中http缓存是前端缓存的核心

7.1 浏览器缓存

localStorage,sessionStorage,cookie

  • cookie数据始终携带在同源的http请求中,即cookie在浏览器和服务器间来回传递,而sessionStorage和Localstorage不会自动把数据发送给服务器,只在本地保存。cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下。

  • 存储大小限制不同,cookie数据不能超过4k,同时因为每次http请求都会携带cookie,所有cookie只适合保存很小的数据。sessionStorage和Localstorage虽然也有大小储存的限制,但比cookie大很多。可以达到5M或更大。

  • 数据有效期不同,sessionStorage,仅在当前浏览器窗口关闭之前有效,Localstorage始终有效,窗口或者浏览器关闭也一直保存,除非手动删除,cookie只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭。

  • 作用域不同,sessionStorage不能在不同的浏览器中共享,即使是同一个页面,locastorage在所有的同源窗口中都是共享的,cookie也是在所有同源窗口中共享的。

  • web Storage支持事件通知机制,可以将数据更新的通知发送给监听者。

  • web storage 的api接口使用更方便。

此外还有indexedDB,浏览器提供的本地数据库,它可以被网页脚本创建和操作

7.2 http缓存

7.2.1 强缓存

强制缓存在缓存数据未失效的情况下,即Cache-Control的max-age没有过期或者Expires的缓存时间没有过期,那么就会直接使用浏览器的缓存数据,不会再向服务器发送任何请求。强制缓存生效时,HTPP的状态码为200。

这种方式页面的加载速度时最快的,性能也是很好的,但是如果在这期间,服务器端的资源修改了,页面上是拿不到的,因为它不会再向服务器发请求了。

跟强制缓存相关的头属性有Expires和Cache-Control,用来表示资源的缓存时间。

  • Expires:响应头的缓存字段。GMT格式日期,代表资源过期时间,由服务器返回。如果时间没过期,不发起请求,直接使用本地缓存;如果时间过期,发起请求。是HTTP1.0的属性,在与max-age共存的情况下,优先级要低。存在缺陷,如果客户端的时间和服务器的时间不一致,会产生问题,或者可以手动修改客户端的时间 image.png
  • Cache-Control:请求头/响应头的缓存字段。 属性值:
    • no-store:所有内容都不缓存;
    • no-cache:缓存,但是浏览器使用缓存前,都会请求服务器判断缓存资源是否是最新。
    • max-age:单位秒,请求资源后的xx秒内不再发起请求。属于HTTP1.1属性,与Expires类似,但优先级要比Expires高。
    • s-maxage:单位秒,代理服务器请求源站资源后的xx秒内不再发起请求,只对CDN有效。
    • public:客户端和代理服务器(CDN)都可缓存。
    • private:只有客户端可以缓存。

image.png

其实这两者差别不大,区别就在于 Expires 是http1.0的产物,Cache-Control是http1.1的产物,两者同时存在的话,Cache-Control优先级高于Expires;在某些不支持HTTP1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。

7.2.2 协商缓存

当浏览器第一次向服务器发送请求时,会在响应头返回协商缓存的头属性:ETag和Last-Modified,其中ETag返回的是一个hash值,资源标识。Last-Modified返回的是GMT格式的时间,标识该资源的最新修改时间;然后浏览器发送第二次请求的时候,会在请求头中带上If-None-Match(对应ETag)和If-Modified-Since(对应Last-Modified);服务器在接收到这两个参数后会做比较,会优先验证ETag,一致的情况下,才会继续比对Last-Modified;如果返回的是304状态码,则说明请求的资源没有修改,浏览器可以直接在缓存中读取数据,否则,服务器直接返回数据。

在强制缓存失效后,服务器会携带标识去请示服务器是不是需要用缓存,
如果服务器同意使用缓存,则返回304,浏览器使用缓存。
如资源已经更新,服务端不同意使用缓存,则会将更新的资源和标识返回给浏览器并返回200。

image.png

为什么需要Etag和Last-Modified两个字段来判断?

Last-Modified标注的最新修改时间只能精确到秒,如果某些文件在1秒以内被多次修改的话,它将不能准确标注文件的修改时间; 如果某些文件是被定期生成的话,内容没有任何改变,但Last-Modified却变了,导致文件无法再使用缓存; 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形; ETag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识,ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化,ETag值的变更则说明资源状态已经被修改,ETag能够更加准确地控制缓存。Last-Modified是可以与ETag一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

7.2.3 缓存位置

上面说的命中强缓存或者协商缓存,会从浏览器本地取缓存,那具体时从取的缓存数据?

从缓存位置上来说分为四种:

Memory Cache

内缓存,内存中的缓存。主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等;
优点:读取速度快;
缺点:一旦我们关闭 Tab 页面,内存中的缓存也就被释放了;
如何触发:当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存; image.png

Disk Cache

存储在硬盘中的缓存
优点:缓存再硬盘中,容量大
缺点:读取速度慢
如何触发:根据浏览器请求头。浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?关于这点,网上说法不一,不过以下观点比较靠得住:

  • 对于大文件来说,大概率是不存储在内存中的,反之优先
  • 当前系统内存使用率高的话,文件优先存储进硬盘 image.png
Service Worker
  • Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。
  • 传输协议必须为 HTTPS
  • Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
Push Cache
  • Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。
  • 它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂

8、在浏览器上输入一个网址的过程

域名解析

  1. 首先会在浏览器缓存中去查询,之前每浏览一个网站,浏览器都会在缓存中存有域名与ip地址的映射关系。不过缓存失效的时间不由浏览器决定,而由操作系统决定。
  2. 浏览器缓存中查询不到后,之后会在系统缓存中查询,由浏览器发起一个系统调用,查询系统缓存中的数据。
  3. 系统缓存中也查询不到后,将会去路由器缓存中查找。
  4. 路由器缓存中也找不到的话,将会从本地DNS服务器的缓存中查找,本地服务器即用户自己配置的DNS服务器。
  5. 如果本地的DNS服务器也找不到的话,本地DNS将会发送请求至根域名服务器,根域名服务器中没有相关缓存数据的时候,就会返回com顶级域名服务器的地址。然后本地DNS服务器再发送请求至com顶级域名服务器,com顶级域名服务器中查询不到的话,就会返回baidu权威服务器的地址,然后本地DNS服务器再发送请求至baidu权威服务器,baidu权威服务器就会返回www主机地址。(这是一种迭代的过程,还有一种递归的过程。即local至根域名,根域名不直接返回com地址,而是发送请求至com,com发送请求至baidu,baidu发送请求至www,www再返回给baidu,baidu返回给com,com再返回给local)至此,整个DNS查询步骤结束,现在浏览器拿到了域名对应的ip地址返回给本地域名服务器;

TCP的三次握手

浏览器发送请求至服务器,会与服务器建立TCP连接进行三次握手:

  1. 第一次握手:建立连接时,客户端发送SYN包至服务器,并进入SYN_SENT状态,等待服务器确认
  2. 第二次握手:服务器收到客户端的SYN包,如同意连接,发送一个ACK,同时发送自己的SYN,此时服务器进入SYN_RCVD状态
  3. 第三次握手:客户端接收到服务器发送的SYN+ACK后,进入ESTABLISHED状态,并发送服务器SYN包的确认ACK,服务器接收到客户端ACK后,进入ESTABLISHED状态
  4. 当客户端和服务器都进入ESTABLISHED状态后,客户端和服务器之间就可以开始双向传递数据了。

图解如下:

image.png

浏览器发送http请求

经过三次握手后,说明浏览器和服务器通信都是正常的,就可以开始发送http请求了;

浏览器构建http请求报文,并通过TCP协议传送到服务器的指定端口。http请求报文一共包括三个部分:

  • 请求行:指定http请求的方法、url、http协议版本等
  • 请求头:描述浏览器的相关信息,语言、编码等。如下
  • 请求正文:当发送POST, PUT,GET等请求时,通常需要向服务器传递数据。这些数据就储存在请求正文中。

image.png

服务器http请求响应

服务器处理http请求,并返回响应报文。HTTP响应报文由四部分组成:响应行、响应头、空行、响应体

  • 响应行:响应行一般由协议版本、状态码及其描述组成
  • 响应头:响应头用于描述服务器的基本信息,以及数据的描述,服务器通过这些数据的描述信息,可以通知客户端如何处理等一会儿它回送的数据
  • 空行:最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头
  • 响应体:响应体就是响应的消息体,如果是纯数据就是返回纯数据,如果请求的是HTML页面,那么返回的就是HTML代码,如果是JS就是JS代码,如此之类。

文件渲染

整个渲染的过程其实就是将http请求的各种资源,通过浏览器渲染引擎的解析,输出可视化的图像。
基本过程图解如下:

image.png

渲染过程:

image.png 从图中可以看出,一个渲染引擎大致包括HTML解释器、CSS解释器、布局和JavaScript引擎。

  • HTML解释器:解释HTML语言的解释器,本质是将HTML文本解释成DOM树(文档对象模型)。
  • CSS解释器:解释样式表的解释器,其作用是将DOM中的各个元素对象加上样式信息,从而为计算最后结果的布局提供依据。
  • 布局:将DOM和css样式信息结合起来,计算它们的大小位置等布局信息,形成一个能够表示这所有信息的内部表示模型即渲染树。
  • JavaScript引擎:JavaScript可以修改网页的内容,也能修改CSS的信息,JavaScript引擎解释JavaScript代码并把代码的逻辑和对DOM和CSS的改动信息应用到布局中去,从而改变渲染的结果。
  • 这些模块依赖很多其他的基础模块,这其中包括网络,存储,2D/3D图形,音频视频和图片解码器等。实际上,渲染引擎中还应该包括如何使用这些依赖模块的部分,这部分的工作其实并不少,因为需要使用它们来高效的渲染网页。例如,利用2D/3D图形库来实现高性能的网页绘制和网页的3D渲染,这个实现非常非常的复杂。最后,当然,在最下面,依然少不了操作系统的支持,例如线程支持,文件支持等等。

浏览器是如何完成网页渲染?
1.解析HTML文件,创建DOM树
2.解析CSS,形成CSS对象模型
3.将CSS与DOM合并,构建渲染树(renderingtree)
4.布局和绘制(重绘、重排)

四次挥手(断开TCP请求)

第一次挥手:主动关闭方发送一个FIN并进入FIN_WAIT1状态。
第二次挥手:被动关闭方接收到主动关闭方发送的FIN并发送ACK,此时被动关闭方进入CLOSE_WAIT状态;主动关闭方收到被动关闭方的ACK后,进入FIN_WAIT2状态。
第三次挥手:被动关闭方发送一个FIN并进入LAST_ACK状态。
第四次挥手:主动关闭方收到被动关闭方发送的FIN并发送ACK,此时主动关闭方进入TIME_WAIT状态,经过2MSL时间后关闭连接;被动关闭方收到主动关闭方的ACK后,关闭连接。

图解如下:

image.png