今日弄懂—Http缓存、eventloop、闭包

1.Http缓存

浏览器缓存分为强缓存和协商缓存,浏览器加载一个页面的简单流程如下:

浏览器先根据这个资源的http头信息来判断是否命中强缓存。如果命中则直接加在缓存中的资源,并不会将请求发送到服务器。

如果未命中强缓存,则浏览器会将资源加载请求发送到服务器。服务器来判断浏览器本地缓存是否失效。若可以使用,则服务器并不会返回资源信息,浏览器继续从缓存加载资源。

如果未命中协商缓存,则服务器会将完整的资源返回给浏览器,浏览器加载新资源,并更新缓存。


仅基于强制缓存


仅基于协商缓存

强制缓存

对于强制缓存来说,响应header中会有两个字段来标明失效规则(Expires/Cache-Control)

 Expires

  Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。

不过Expires 是HTTP 1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。

Expires=max-age + 请求时间

另一个问题是,到期时间是由服务端生成的,但是客户端时间可能跟服务端时间有误差,这就会导致缓存命中的误差。

所以HTTP 1.1 的版本,使用Cache-Control替代。

Cache-Control

Cache-Control 是最重要的规则。常见的取值有private、public、no-cache、max-age,no-store,默认为private。

private: 客户端可以缓存

public: 客户端和代理服务器都可缓存(前端的同学,可以认为public和private是一样的)

max-age=xxx: 缓存的内容将在 xxx 秒后失效

no-cache: 需要使用对比缓存来验证缓存数据(后面介绍)

no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发(对于前端开发来说,缓存越多越好,so...基本上和它说886)

协商缓存

浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。

再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据。

服务器根据http头信息中的Last-Modify/If-Modify-Since或Etag/If-None-Match来判断是否命中协商缓存。

Last-Modified  /  If-Modified-Since

Last-Modified:

服务器在响应请求时,告诉浏览器资源的最后修改时间。

If-Modified-Since:

再次请求服务器时,通过此字段通知服务器上次请求时,服务器返回的资源最后修改时间。

服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。

若资源的最后修改时间大于If-Modified-Since,说明资源又被改动过,则响应整片资源内容,返回状态码200;

若资源的最后修改时间小于或等于If-Modified-Since,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用所保存的cache。

但是有时候通过最后修改时间来判断资源是否修改还是不太准确(资源变化了最后修改时间也可以一致)。于是出现了ETag/If-None-Match。

ETag/If-None-Match

与Last-Modify/If-Modify-Since不同的是,Etag/If-None-Match返回的是一个校验码(ETag: entity tag)。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化*。ETag值的变更则说明资源状态已经被修改。服务器根据浏览器上发送的If-None-Match值来判断是否命中缓存。

以Apache为例,ETag生成靠以下几种因子:

1.文件的i-node编号,此i-node非彼iNode。是Linux/Unix用来识别文件的编号。是的,识别文件用的不是文件名。使用命令’ls –I’可以看到。

2.文件最后修改时间

3.文件大小

生成Etag的时候,可以使用其中一种或几种因子,使用抗碰撞散列函数来生成。所以,理论上ETag也是会重复的,只是概率小到可以忽略。

既生Last-Modified何生Etag?

你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag(实体标识)呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:

1. Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间

2. 如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存

3.有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形

Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

用户行为与缓存

浏览器缓存行为还有用户的行为有关!!!

总结:

对于强制缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行协商缓存策略。

对于协商缓存,将缓存信息中的Etag和Last-Modified通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。

浏览器第一次请求:

浏览器再次请求时:

2.eventLoop

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

任务队列:

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

(2)主线程之外,还存在一个"任务队列"(task

queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

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

(4)主线程不断重复上面的第三步。

一、js中的异步操作

setTimeOut

setInterval

ajax

promise

I/O

二、同步任务(synchronous):在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

异步任务(asynchronous):不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

三、这里需要注意的是new Promise是会进入到主线程中立刻执行,而promise.then则属于微任务

宏任务(macro-task):整体代码script、setTimeOut、setInterval

微任务(mincro-task):promise.then、promise.nextTick(node)

四、Event Loop事件循环

1、整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为两部分:“同步任务”、“异步任务”;

2、同步任务会直接进入主线程依次执行;

3、异步任务会再分为宏任务和微任务;

4、宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;

5、微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;

6、当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务;

7、上述过程会不断重复,这就是Event Loop事件循环;

事件循环
举例

分析:

1.add()是同步任务,直接执行,打印1;

2.add()里面的setTimeout是异步任务且宏函数,记做timer1放到宏函数队列;

3.add()下面的setTimeout是异步任务且宏函数,记做timer2放到宏函数队列;

4.new Promise是同步任务,直接执行,打印4;

5.Promise里面的setTimeout是异步任务且宏函数,记做timer3放到宏函数队列;

6.Promise里面的for循环,同步任务,执行代码;

7.Promise.then是微任务,放到微任务队列;

8.console.log(8)是同步任务,直接执行,打印8;

9.此时主线程任务执行完毕,检查微任务队列中,有Promise.then,执行微任务,发现有setTimeout是异步任务且宏函数,记做timer4放到宏函数队列;

10.微任务队列中的console.log(7)是同步任务,直接执行,打印7;

11.微任务执行完毕,第一次循环结束;

12.检查宏任务Event Table,里面有timer1、timer2、timer3、timer4,四个定时器宏任务,按照定时器延迟时间得到可以执行的顺序,即Event Queue:timer2、timer4、timer3、timer1,取出排在第一个的timer2;

13.取出timer2执行,console.log(3)同步任务,直接执行,打印3;

14.没有微任务,第二次Event Loop结束;

15.取出timer4执行,console.log(6)同步任务,直接执行,打印6;

16.没有微任务,第三次Event Loop结束;

17.取出timer3执行,console.log(5)同步任务,直接执行,打印5;

18.没有微任务,第四次Event Loop结束;

19.取出timer1执行,console.log(2)同步任务,直接执行,打印2;

20.没有微任务,也没有宏任务,第五次Event Loop结束;

21.结果:1,4,8,7,3,6,5,2

3.闭包

闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

js的内存回收机制:一个函数在执行开始的时候,会给其中定义的变量划分内存空间保存,以备后面的语句所用,等到函数执行完毕返回了,这些变量就被认为是无用的了,对应的空间也就被回收了。下次再执行此函数的时候,所有的变量又回到最初的状态,重新赋值使用。但是如果这个函数内部又嵌套了另一个函数(这就是闭包了),而这个函数是有可能在外部被调用到的。并且这个内部函数又使用了外部函数的某些变量的话.这种内存回收机制就会出现问题。如果在外部函数返回后,又直接调用了内部函数,那么内部函数就无法读取到他所需要的外部函数中变量的值了。所以js解释器在遇到函数定义的时候会自动把函数和他可能使用的变量(包括本地变量和父级和祖先级函数的变量(自由变量))一起保存起来。也就是构建一个闭包,这些变量将不会被内存回收器所回收,只有当内部的函数不可能被调用以后(例如被删除了,或者没有了指针),才会销毁这个闭包,而没有任何一个闭包引用的变量才会被下一次内存回收启动时所回收。

使用闭包有以下几大好处:

    a:希望一个变量长期驻扎在内存中。

    b:避免全局变量的污染。

    c:私有成员的存在。

你可能感兴趣的:(今日弄懂—Http缓存、eventloop、闭包)