输入URL到页面展现的过程

前言:浏览器(参考文献:https://blog.csdn.net/ch834301/article/details/114826592)

    1.进程和线程

     进程和线程是操作系统的基本概念。

    进程:

     进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。我们这里将进程比喻为工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
     注:一个计算机下的所有应用程序算是一个进程,每个进程之间相互独立,那么浏览器对于操作系统的CPU来说也是一个进程,其占据CPU的一些内存。那么浏览器这个软件(进程)又是一个多进程,每个进程可能又分为好多线程,这些线程之间公用他们自己的进程的内存。     

     线程:

      在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元。这里把线程比喻一个车间的工人,即一个车间可以允许由多个工人协同完成一个任务。即一个进程分为多个线程来执行不同的任务,这些线程可以同时运行分别执行不同的任务,也可以不同时执行。

     进程和线程的区别和关系:

  • 进程是操作系统分配资源的最小单位,线程是程序执行的最小单位。
  • 一个进行由一个或多个线程组成,线程是进程中代码的不同执行路线
  • 进程之间是独立的,但是同一进程下的各线程之间共享程序的内存空间(包括代码段、数据集、等)及一些进程级的资源(如打开文件和信号)。
  • 调度和切换:线程上下文切换比进程上下文切换要快得多
  •  总结:(一个进程相当于一个程序)进程相当于一个一个进程将任务分配给多个线程,这些线程分别执行不同的任务,他们协助完成这个任务。这些线程可以同时运行,也可以不同时运行。

     多进程和多线程:

      多进程:多进程指的是在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的,比如你可以听歌的同时,打开编辑器敲代码,编辑器和听歌软件的进程之间丝毫不会相互干扰。【多个进程应该指的是多个app(进程)的同一时间运行;说明同时有多个CPU,因为一个CPU,在某一时间内只允许运行一个进程】
     多线程:是指程序(一个进程)中包含多个执行流,即在一个程序(一个进程)中可以同时运行多个不同的线程执行不同的任务,也就是说允许单个程序(一个进程)创建多个并行执行的线程来完成各自的任务
     

     打个比方:

  •  假如进程是一个工厂,工厂有它的独立的资源
  •  工厂之间相互独立
  •  线程是工厂中的工人,多个工人协作完成任务
  •  工厂内有一个或多个工人
  •  工人之间共享空间

     再完善完善概念: 

  • 工厂的资源 -> 系统分配的内存(独立的一块内存)
  • 工厂之间的相互独立 -> 进程之间相互独立
  • 多个工人协作完成任务 -> 多个线程在进程中协作完成任务
  • 工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成
  • 工人之间共享空间 -> 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)

      以上内容为进程和线程的概念

      我们首先了解一下浏览器。浏览器是一个多进程框架。从浏览器输入URL到页面渲染的整个过程都是由浏览器框架中的各个进程之间的配合完成。

      浏览器进程:主进程,它负责用户界面(地址栏、菜单等等)、子进程的管理(例如,进程间通信和数据传递)、存储等等。即管理子进程、提供服务功能

      网络进程它负责网络资源的请求,例如 HTTP请求、WebSocket 模块 

      渲染进程它负责将接收到的 HTML 文档、css文档和 JavaScript 等转化为用户界面。将HTML、CSS、JS渲染成界面,js引擎v8和排版引擎Blink就在上面,他会为每一个tab页面创建一个渲染进程。在浏览器中打开一个网页相当于新起了一个进程(进程内有自己的多线程).如果浏览器是单进程,那么某个Tab页或者第三方插件崩溃了,就影响了整个浏览器,体验有多差,而且多进程还有其它的诸多优势,当然,多进程内存等资源消耗也会更大】。

      GPU(图形处理器)进程本来是负责处理3Dcss的,后来慢慢的UI界面也交给GPU来绘制。用于硬件加速图形绘制

      插件进程:它负责对插件的管理,负责插件的运行的,因为插件很容易崩溃,把它放到独立的进程里不要让它影响别人

 

     分析渲染进程 渲染进程(浏览器内核/渲染引擎):chrom浏览器,会给每个Tab页面创建一个进程(渲染进程),也就是说每个页面都是一个独立的程序。每个页面都有自己的window对象。打印a页面和b页面的window对象就不一样。那么其js引擎线程当然也属于各自的进程。所有的一切都属于自己进程。我们这里讲的所有的都是某个tab页面即某个进程。【其他浏览器不一定,有的每个页面都是一个线程,从而一个页面崩了,其他的页面也受影响】

      浏览器内核(渲染引擎):通常所谓的浏览器内核也就是浏览器所采用的渲染引擎,渲染引擎决定了浏览器如何显示网页的内容以及页面的格式信息。不同的浏览器内核对网页编写语法的解释也有不同,因此同一网页在不同的内核的浏览器里的渲染(显示)效果也可能不同,这也是网页编写者需要在不同内核的浏览器中测试网页显示效果的原因。最开始渲染引擎和js引擎并没有区分的很明确,后来JS引擎越来越独立,内核就倾向于只指渲染引擎。即渲染引擎(浏览器内核主要把控GUI渲染吧即浏览器如何显示网页的内容以及页面的格式信息。因此,我们一般需要在有的css属性中添加-webkit-  -mos-等等,代表不同的内核)

      在浏览器内核(渲染引擎)控制下各线程相互配合保持同步,一个渲染进程通常由以下常驻线程组成

        a. GUI 渲染线程

            1、 GUI渲染线程负责当浏览器收到响应的html后,该线程开始解析HTML文档构建DOM树,解析CSS文件构建CSSOM,合并构成渲染树,并计算布局样式,绘制在页面上。

           2、当界面样式被修改的时候可能会触发reflow和repaint,该线程就会重新计算,重新绘制,是前端开发需要着重优化的点

           3、注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

        b.JavaScript引擎线程(js线程)【所有的异步【事件、定时、http请求等】都不属于JavaScript引擎线程,只是执行的时候需要在JavaScript引擎线程的执行栈中执行】

         

          注:heap:内存堆,应该是属于进程的空间吧。这也就是说,我们定义了一个对象比如var b={name:'张三'},那么对象{name:'张三'}就被存放在内存堆中,当其不被引用的时候即没有变量指向它的地址,那么,这个对象就等着被回收。当然如果这个进程关闭,其当然也就被销毁了。一定要记住,所有的例如b={name:"张三"},只是某个作用域中的变量b指向内存堆中的这个对象,当这个变量的变量对象被销毁,那么这个变量当然也就被销毁了。而对象仍然存在浏览器内存堆中。

               事件循环、webAPI、任务队列都认为是浏览器的一些东西吧。因为它们不属于js线程也不属于定时器、异步、等线程。 

        Javascript 有一个 main thread 主线程 和 call-stack 调用栈(执行栈即Stack执行栈),所有的任务都会被放到调用栈等待主线程执行

  • JS 调用栈(Stack即执行栈)

     JS 调用栈是一种后进先出的数据结构。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。

  • 同步任务、异步任务

     JavaScript 单线程中的任务分为同步任务异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行。

          异步任务则会在异步有了结果后对应的线程将注册的回调函数从WEB API中拉出来,安排进入任务队列将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。

  • Event Loop

     调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环

     

        javascript引擎线程的具体描述

           1、也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)即<script>标签中的代码的加载、解析、运行。

          2、JS引擎线程负责解析Javascript脚本,运行代码。<script>标签中的代码分为同步代码和异步代码,同步代码属于JavaScript引擎线程自身的代码。所有的异步即需要等待在任务队列中的代码都有自己对应的线程,只不过这些异步都需要在JavaScript引擎线程的执行栈中执行,当然,在执行的时候,JavaScript引擎线程启动,GUI线程挂载。

           3、JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序

           4、同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

             javascript线程和GUI线程的关系:GUI渲染线程与JS引擎线程互斥的.至于为什么互斥,这里不深究了,总之:记住,GUI渲染线程和JavaScript引擎线程是互斥的,具体表现为:当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时,才会接着执行。           

       注: DOMCountentLoaded:(https://www.cnblogs.com/caizhenbo/p/6679478.html)

                DOMContentLoaded顾名思义,就是即dom树构建完成即HTML解析完成触发。差不多也是页面渲染出来的时间。那什么是dom内容加载完毕呢?我们从打开一个网页说起。当输入一个URL,页面的展示首先是空白的,然后过一会,页面会展示出内容,但是页面的有些资源比如说图片资源还无法看到,此时页面是可以正常的交互,过一段时间后,图片才完成显示在页面。从页面空白到展示出页面内容,会触发DOMContentLoaded事件。而这段时间就是HTML文档被加载和解析完成。 

                这里需要注意一点,在现在浏览器中,为了减缓渲染被阻塞的情况,现代的浏览器都使用了猜测预加载。当解析被阻塞的时候,浏览器会有一个轻量级的HTML(或CSS)扫描器(scanner)继续在文档中扫描,查找那些将来可能能够用到的资源文件的url,在渲染器使用它们之前将其下载下来。

                在这里我们可以明确DOMContentLoaded所计算的时间,当文档中没有脚本时,浏览器解析完HTML文档便能触发 DOMContentLoaded 事件;如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等位于脚本前面的css加载完才能执行。也就是说只有当所有<script>标签的内容全部解析完成了,没有其他需要解析的元素了,HTML才解析完成也就是dom树才构建完成。在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。【因为img src等没有专门的线程处理,因此dom构建和其没有关系。】即HTML解析完成了也就是DOM树创建完成才触发DOMContentLoaded事件。这里一定要注意,<script>中的同步代码才是JavaScript引擎线程的代码,所有的异步都不属于这个线程的代码,因此HTML的解析只包括这些同步代码的执行,不包括异步代码的执行,只不过这些异步在JavaScript引擎线程的执行栈中执行,在执行这些异步代码的时候,GUI渲染线程是挂起的。

                                                            接下来,我们来说说load,页面上所有的资源(图片,音频,视频等)被加载以后才会触发load事件,简单来说,页面的load事件会在DOMContentLoaded被触发之后才触发。

                                                            我们在 jQuery 中经常使用的 $(document).ready(function() { // ...代码... }); 其实监听的就是 DOMContentLoaded 事件,而 $(document).load(function() { // ...代码... }); 监听的是 load 事件。在用jquery的时候,我们一般都会将函数调用写在ready方法内,就是页面被解析后,我们就可以访问整个页面的所有dom元素,可以缩短页面的可交互时间,提高整个页面的体验。

          因此如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。更具体的表现看后面的渲染进程部分

        c. 定时触发器线程(setTimeout等异步,顾名思义,负责执行异步定时器一类的函数的线程,如: setTimeout,setInterval

          1、传说中的setInterval与setTimeout所在线程

          2、浏览器的定时器并不是由JavaScript引擎计数的,因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响计时的准确,因此通过单独的线程来计时并触发定时器。

  •   3、主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,定时触发线程会将计数完毕后的事件加入到任务队列的尾部,等待JS引擎线程执行。 

        d. 事件触发线程(事件)

           归属于渲染(浏览器内核)进程,不受JS引擎线程控制。主要用于控制事件(例如鼠标,键盘等事件),当事件被触发时候,事件触发线程就会把该事件的处理函数添加进任务队列中,等待JS引擎线程空闲后执行。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。

           即:主线程依次执行代码时,遇到事件处理程序如:onclick='myclick'等,交给实现触发线程去处理。当用于点击或者触发了某个事件,浏览器给包含该坐标点的所有元素创建并分发事件,当实际目标元素接收到本次事件后,事件触发程序就将该元素的事件处理程序添加到任务队列中等待被执行。事件继续传播,同理事件触发程序将对元素的事件处理程序添加到任务队列中等待被执行。

        e. 异步http请求线程(顾名思义,负责执行异步请求一类的函数的线程,如: Promise,axios,ajax等) 

           主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,异步http请求线程会将回调函数加入到任务队列的尾部,等待JS引擎线程执行           

            注意:浏览器对通一域名请求的并发连接数是有限制的,Chrome和Firefox限制数为6个,ie8则为10个。
         

       总结b-e这四个线程参与了JS的执行,但是永远只有JS引擎线程在执行JS脚本程序,其他三个线程只负责将满足触发条件的处理函数推进任务队列,等待JS引擎线程执行。

               

 

 

当用户在浏览器窗口输入一个URL后:大体流程是这样的:

                

    浏览器进程:

     1.浏览器会对我们输入的URL进行解析,主要将其分为以下几个部分:协议网络地址资源路径。其中,网络地址指的是该连接网络上的哪台电脑上(从哪台电脑(服务器)上获取资源即服务器地址),可以是IP也可以是域名,也可以包括端口号。协议指的是从计算机上(服务器)获取资源的方式,常见的是HTTP,HTTPS和FTP,不同协议有不同的通讯内容格式。资源路径指的是从服务器获取资源的具体路径。

      这里浏览器对输入的url解析为如下内容:

      url:https://www.jianshu.com/p/879fada10661

      协议:https

      网络地址:www.jianshu.com

      资源路径:/p/879fada10661;再例如:/index.html 、 /js/index.js .... 客户端指定请求的路径名称,服务器端根据这个信息把具体的文件[一定要记住,这里是文件不是文件夹。一个url对应一个文件。也就是说一次请求只能请求到一个文件中的数据]中的源代码读取到然后给客户端返回即可。当我们没有指定的情况,大部分情况默认请求的都是项目根目录下的index.html,也就是默认会增加 /index.html (当然这个默认的值是服务器可以修改配置的)

     【浏览器的主要功能:将用户选择的web资源呈现出来。而这,它需要从服务器请求资源(比如我的一台电脑,并将其显示在浏览器窗口中。资源的格式通常是html,也包括PDFimage等其他格式。用户用URL(Uniform Resource Identifier统一资源标识符)来指定所请求资源的位置(比如我的电脑的地址,只不过这个地址不是Ip。url相当于人的名字,IP相当于电话号码。那么我们要打电话就需要通过人名找到对应的手机号;因此,通过DNS解析来找到URL对应的IP),通过DNS(域名系统(英文:DomainNameSystem,缩写:DNS))查询,将网址转换为IP地址【用户输入URL到浏览器从服务器请求资源的过程,这就跟文件共享差不多一个概念。我要从另外一个电脑复制文件,找到该同事的电脑的IP,然后进行连接,最后就可以随便复制文件了整个浏览器的工作流程如下:

       1.输入URL

       2.浏览器查找域名的IP地址(DNS解析完成,即找到所要找的资源的ip地址)

       3.浏览器给web服务器发送一个http请求

       4.网站服务的永久重定向响应

       5.浏览器跟踪重定向地址,现在,浏览器知道了要访问的正确地址,所以它会发送另一个获取请求

       6.服务器“处理”请求,服务器接收到获取请求,然后处理并返回一个响应

       7.服务器发回一个html响应

       8.浏览器开始显示html

       9.浏览器发送请求,以获取嵌入在html中的对象。在浏览器显示HTML时,它会注意到需要获取其他地址内容的标签。这时,浏览器会发送一个获取请求来重新获得这些文件。这些文件就包括CSS/JS/图片等资源,这些资源的地址都要经历一个和HTML读取类似的过程。所以浏览器会在DNS中查找这些域名,发送请求,重定向等等…

         例如:你发现快过年了,于是想给你的女朋友买一件毛衣,你打开了 www.taobao.com,这时你的浏览器首先查询DNS服务器,将 www.taobao.com转换成IP地址。但是,你首先会发现,在不同的地区或者不同的网络下,转换后的IP地址很可能是不一样的,这首先涉及负载均衡的第一步,通过DNS解析域名时,将你的访问分配的不同的入口,同时尽可能的保证你所访问的入口时所有入口中较快的一个。 你通过这个入口成功的访问了www.taobao.com实际的入口IP地址,……经过一系列的复杂的逻辑运算和数据处理,用于给你看的淘宝首页HTML内容便生成了,浏览器下一步会加载页面中用到的CSS /JS/图片等样式、脚本和资源文件。】

         

    2.URL解析到页面展示的详解(网络进程)

     2.1用户在浏览器中输入URL(https://www.jianshu.com/p/879fada10661),浏览器进程会将完整的url通过进程间通信,即 IPC,发送给网络进程(浏览器进程 URL      >网络进程)                                                                                                                                                                                                                                                                            进程间通信

      2.2网络进程接收到 URL 后,并不是马上对指定 URL 进行请求。而是因为要知道用户想要用http访问一个网络地址是“www.jianshu.com”的网站,那么如何找到这个地址呢?就像你打的回家,你跟司机说去阮老师家,他哪儿知道阮老师家是哪里呢?你得告诉他地址呀。网站服务器的地址就是IP地址。所有浏览器首先要确认的是域名所对应的ip是啥?因此我们需要进行 DNS 解析域名得到对应的 IP然后通过 ARP 解析 IP 得到对应的 MACMedia Access Control Address)地址。   

     1.DNS的定义: 

      域名系统(DomainNameSystem)是互联网的一项服务。它作为将域名和ip地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。

     2.域名的定义:

     域名:(英语:Domain Name)是由一串用点分割的名字组成的internet上某一台计算机或计算机组的名称,用于在数据传输表示计算机的电子方位(有时也指地理位置)。

    

     3.IP地址 

     IP地址是Internet主机的作为路由寻址用的数字体标识,人不容易记忆。因而产生了域名这一种字符型标识。

     例如,www.wikipedia.org是一个域名,和IP地址208.80.152.2相对应。DNS就像是一个自动的电话号码簿,我们可以直接拨打wikipedia的名字来代替电话号码(IP地址)。我们直接调用网站的名字以后,DNS就会将便于人类使用的名字(如www.wikipedia.org)转化成便于机器识别的IP地址(如208.80.152.2)。

      域名是我们取代记忆复杂的 IP 的一种解决方案,而 IP 地址才是目标在网络中所被分配的节点。MAC 地址是对应目标网卡所在的固定地址。(url(域名) dns解析  >ip

      dns解析的过程:

                          1》浏览器会先看看是否存在本地缓存,如果有就直接返资源浏览器进程,无则下一步 DNS-> IP -> TCP

                            2》浏览器会首先查看本地硬盘 hosts 文件,看看其中有没有和这个域名对应的规则,如果有的话就直接如果有,将ip直接返回给网络进行,完成域名解析。

                          3》如果在本地的 hosts 文件没有能够找到对应的 ip 地址,浏览器会发出一个 DNS请求本地DNS服务器 。本地DNS服务器一般都是你的网络接入服务器商提供,比如中国电信,中国移动。查询你输入的网址的DNS请求到达本地DNS服务器之后,本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,就将包含url的ip地址响应报文发给网络进程,此过程是递归的方式进行查询。如果没有,本地DNS服务器还要向DNS根服务器发起请求进行进行查询。 

                          4》根DNS服务器有该对应关系,则将对应关系响应给本地DNS服务器本地DNS服务器再将含有url的ip地址的响应报文发送给网络进行;如果根DNS服务器没有记录具体的域名和IP地址的对应关系,就是告诉本地DNS服务器,你可以到域服务器上去继续查询,并给出域服务器的地址。这种过程是迭代的过程                    

                            5》本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址

                            6》最后,本地DNS服务器域名的解析服务器发出请求,这时就能收到一个域名和IP地址对应关系本地DNS服务器将含有https://www.jianshu.com/p/879fada10661IP地址的响应报文发送给客户端返回用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。

                  

    ————————————————————————————————————截止这里,我们的服务器的ip终于找到了——————————————————————

       3、浏览器获取端口号

        知道ip地址(服务器的地址)后,还需要端口号。例如:

        好了,阮老师家的地址知道了,正常来讲是可以出发了。可是对于网络有些不一样,你还需要指定端口号。端口号之于计算机就像窗口号之于银行,一家银行有多个窗口,每个窗口都有个号码,不同窗口可以负责不同的服务。端口只是一个逻辑概念,和计算机硬件没有关系。现在可以这么说,阮老师家好几扇们,办不同的业务走不同的门,你得告诉师傅你走那扇门,你要不说,就默认你是个普通客人,丢大门得了。http协议默认端口号是80

       浏览器会以一个随机端口(1024<端口<65535)向服务器的WEB程序(常用的有httpd,nginx等)80端口发起TCP的连接请求。(向服务器的WEB程序80端口发起请求)        

       4、TCP建立连接「再三确认是我要连接的服务器」【浏览器是指客户端的浏览器即用户在自己的电脑上的浏览器,浏览器作为客户端的代表和服务器进行交互。
       IP端口都有了,在http消息发送前,浏览器发起建立客户端服务器TCP连接接(三次握手)的请求(即客户端通过浏览器向服务器发起一个TCP连接的请求)。   

    • 客户端发送标有 SYN 的数据包,表示我将要发送请求
    • 服务端发送标有 SYN/ACK 的数据包,表示我已经收到通知,告知客户端发送请求
    • 客户端发送标有 ACK 的数据包,表示我要开始发送请求,准备接收

              这种建立连接的方法可以防止产生错误的连接

          三次握手完成,TCP客户端服务器端成功地建立连接,可以开始传输数据了。(相当于两台电脑连上了,可以实现文件共享了)

        5.发送HTTP请求(还是网络进程)即浏览器

        浏览器(客户端发起一个http请求。一个典型的 http request header 一般需要包括请求的方法,例如 GET 或者 POST 等,不常用的还有 PUT 和 DELETE 、HEAD、OPTION以及 TRACE 方法,一般的浏览器只能发起 GET 或者 POST 请求。         

       客户端服务器发起http请求的时候,会有一些请求信息,请求信息包含三个部分:

      | 请求方法URI协议/版本

      | 请求头(Request Header)

      | 请求正文:

     下面是一个完整的HTTP请求例子:     

GET/sample.jsp  HTTP/1.1   
Accept:image/gif.image/jpeg,*/* Accept-Language:zh-cn Connection:Keep-Alive Host:localhost User-Agent:Mozila/4.0(compatible;MSIE5.01;Window NT5.0) Accept-Encoding:gzip,deflate
username=jinqiao
&password=1234 

    (1)请求的第一行是“方法URL协议/版本”:GET/sample.jsp HTTP/1.1
  (2)请求头(Request Header)

         请求头包含许多有关的客户端环境和请求正文的有用信息。例如,请求头可以声明浏览器所用的语言,请求正文的长度等。

    Accept:image/gif.image/jpeg.*/*    Accept-Language:zh-cn    Connection:Keep-Alive    Host:localhost    User-Agent:Mozila/4.0(compatible:MSIE5.01:Windows NT5.0)    Accept-Encoding:gzip,deflate.

  (3)请求正文

        请求头和请求正文之间是一个空行,这个行非常重要,它表示请求头已经结束,接下来的是请求正文。请求正文中可以包含客户提交的查询字符串信息:

    username=jinqiao&password=1234     
        6.服务器响应(返回响应结果)【】
       服务器收到请求信息后即请求到达服务器之后,接下来服务器需要响应浏览器的请求。服务器端收到请求后,由web服务器(准确说应该是http服务器)处理请求,诸如Apache、Ngnix、IIS等,web服务器解析用户请求,知道了需要调度哪些资源文件,再通过相应的这些资源文件处理用户请求和参数,并调用数据库信息,最后将结果通过web服务器返回浏览器客户端。在HTTP里,有请求就会有响应,哪怕是错误信息
        

         HTTP响应与HTTP请求相似,HTTP响应也由3个部分构成,分别是:

         l 状态行

         l 响应头(Response Header)

         l 响应正文

HTTP/1.1 200 OK    Date: Sat, 31 Dec 2005 23:59:59 GMT    Content-Type: text/html;charset=ISO-8859-1   Content-Length: 122   
<html>    <head>    <title>http</title>    </head>    <body>    <!-- body goes here -->    </body>    </html>        

       状态行:

                 状态行由协议版本、数字形式的状态代码、及相应的状态描述,各元素之间以空格分隔。

                 格式:    HTTP-Version Status-Code Reason-Phrase CRLF

                 例如:    HTTP/1.1 200 OK \r\n

                 -- 协议版本:是用http1.0还是其他版本

                 -- 状态描述:状态描述给出了关于状态代码的简短的文字描述。比如状态代码为200时的描述为 ok

                 -- 状态代码:状态代码由三位数字组成,第一个数字定义了响应的类别,且有五种可能取值。如下

         服务器将响应行响应头响应体并发给网络进程。网络进程接受了响应信息之后,就开始解析响应头的内容。
         注:在HTTP里,有请求就会有响应,哪怕是错误信息。这里我们同样看下响应报文的组成结构:
             

         在响应结果中都会有个一个HTTP状态码,比如我们熟知的200、301、404、500等。通过这个状态码我们可以知道服务器端的处理是否正常,并能了解具体的错误。
状态码由3位数字和原因短语组成。根据首位数字,状态码可以分为五类:

               状态码 的解析:2xx,请求成功;

                                                                                                                                                      4XX,客户端错误状态码(用户在客户端通过客户端的浏览器输入一个URL,有错的话,一般就是就是url有错即发送请求失败。)                                                                                          5XX,服务器错误状态码(服务器处理请求出错,这种情况,我们客户端是无法处理的)

       响应头:

       响应头部:由关键字/值对组成,每行一对,关键字和值用英文冒号":"分隔,典型的响应头有:

              

      响应正文

      包含着我们需要的一些具体信息,比如cookie,html,image,后端返回的请求数据等等。这里需要注意,响应正文和响应头之间有一行空格,表示响应头的信息到空格为止,下图是fiddler抓到的请求正文,红色框中的:响应正文:

     

       

        7.断开连接(为了避免服务器与客户端双方的资源占用和损耗,当双方没有请求或响应传递时,任意一方都可以发起关闭请求。

        当浏览器收到数据后,收发数据的过程就结束了(网络进程响应之前吧),。

         建立一个连接需要三次握手,而终止一个连接要经过四次挥手,这是由TCP的半关闭(half-close)造成的。具体过程如下图所示。

      

      

       而这整个过程的客户端则是网络进程。并且,在数据传输的过程还可能会发生的重定向的情况,即当网络进程接收到状态码为 3xx 的响应报文,则会根据响应报文首部字段中的 Location 字段的值进行重新向,即会重新发起请求

       8.数据处理
       当网络进程接收到的响应报文状态码,进行相应的操作。例如状态码为 200 OK 时,会解析响应报文中的 Content-Type 首部字段,例如我们这个过程 Content-Type 会出现 application/javascripttext/csstext/html,即对应 Javascript 文件、CSS 文件、HTML 文件。

      9.开始渲染【网络进程  html,javascript,css等数据 渲染进程】

                                                    传递给      

      整个渲染的过程其实就是将URL对应的各种资源,通过浏览器渲染引擎的解析,输出可视化的图像。 

      

 

        

        在创建完渲染进程后,网络进程会将接收到的数据传递给渲染进程。而在渲染进程接收到HTML数据后开始渲染。

        而页面渲染的过程可以分为 9 个步骤:    

    • 解析 HTML 生成 DOM 树
    • 解析 CSS 生成 CSSOM
    • 加载或执行 JavaScript
    • 生成渲染树(Render Tree
    • 布局
    • 分层
    • 生成绘制列表
    • 光栅化

     可概括为:

  1. GUI渲染线程解析HTML生成DOM树 。在此过程中遇到<link>则加载样式表,然后解析,遇到<style>则直接解析,则同时解析CSS生成CSSOM树。遇到<script>则,GUI渲染线程暂且挂起,JavaScript引擎线程运行,即解析JavaScript文档并执行。

  2. GUI渲染线程构建Render树 - 接下来不管是内联式,外联式还是嵌入式引入的CSS样式会被解析生成CSSOM树,根据DOM树CSSOM树生成另外一棵用于渲染的树-渲染树(Render tree),

  3. 布局Render树 - 然后对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置

  4. 绘制Render树 - 最后遍历渲染树并用UI后端层将每一个节点绘制出来   

       a.解析HTML -> 构建 DOM 树【GUI线程】

           解析HTML生成DOM树,网络进程将拿到HTML文件发给渲染进程(网路进程仍还在接收HTML数据)。(因为我们请求的url资源第一次都是都是某个html页面。比如https://www.songma.com/news/txtlist_i62138v.html,这个url,我们请求的资源就是一个HTML文件。一个url只能请求对应的一个文件。) 渲染引擎首先解析HTML文档并生成DOM树。               

            

             需要注意的是这个 DOM 树不同于 Chrome-devtool 中 Element 选项卡的 DOM 树,它是存在内存中的,用于提供 JavaScript 对 DOM 的操作。 

            注:DOM树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。DOM树的根节点就是document对象。

                  DOM树的生成过程中可能会被CSS和JS的加载执行阻塞,具体可以参见下一章。当HTML文档解析过程完毕后,浏览器继续进行标记为defer模式的脚本加载【】,然后就是整个解析过程的实际结束触发DOMContentLoaded事件,并在async文档文档执行完之后触发load事件。

            b.构建 CSSOM 【GUI线程】

            当HTML解析到包含<link>标签的时候:浏览器再发起一个url请求,进行url解析、DNS解析、发送请求等前面的一系列的操作,最后客户端的浏览器网络进程将接收到css文件通过进程之间的通信发给渲染进程即在GUL线程中解析CSS文件并生成CSSOM树。当然,在这个过程中HTML文件仍然在解析并构建DOM树。

           当HTML解析到<style>标签的时候->直接解析css->构成CSSOM树。同时,HTML文件仍然在解析并构建DOM树  

           生成DOM树的同时会生成样式结构体CSSOM(CSS Object Model)Tree接下来不管是内联式,外联式还是嵌入式引入的CSS样式会被解析生成CSSOM树。构建 CSSOM 的过程,即通过解析 CSS 文件style 标签行内 style 等,生成 CSSOM。而这个过程会做这几件事:          

  •        规范 CSS,即将 color: blue 转化成 color: rgb() 形式,可以理解成类似 ES6 转 ES5 的过程
  •        计算元素样式,例如 CSS 样式会继承父级的样式,如 font-sizecolor 之类的。

 

                

               CSS Object Model 是一组允许用 JavaScript 操纵 CSS 的 API

             c.加载 JavaScript【JavaScript引擎线程】

               当HTML解析器找到 <script> 标签后,将会暂停HTML解析,并且必须加载、解析和执行 JavaScript的代码。为什么?因为JavaScript 可以使用诸如 document.write() 更改整个DOM结构!所以开发人员在写代码的时候可以在 <script> 标签上加 async 或者 defer 属性。然后浏览器将会异步加载并运行JavaScript,不会阻止解析。

               当HTML解析到包含<script>标签的时候,如果是从外部引入的资源,则:浏览器再发起一个url请求,进行url解析、DNS解析、发送请求等前面的一系列的操作,最后客户端的浏览器网络进程将接收到的JavaScript文件通过进程之间的通信发给渲染进程,JavaScript引擎线程开始解析并执行JavaScript文件中的代码。从遇到<script>标签到执行完JavaScript代码的过程中,GUL线程挂起(HTML解析停止,为什么CSS解析不停止呢?),等JavaScript线程中的代码执行完成了,再抽空将GUL线程运行即HTML解析继续->构建dom树。

               注意:如果CSS文件还没有解析完成,则JavaScript引擎线程则需要等到css解析完成才能运行。因此JavaScript线程开启时,必须保证如果GUL线程中有CSS解析,那么这个已经完成解析才能开启。

              当HTML解析到包含<script>标签的时候,如果不是外部引入的资源,则直接编译执行;

              关于加载执行js程序:1.浏览器在解析HTML文档时,将根据文档流从上到下逐行解析和显示。Javascript代码也是HTML文档的组成部分,因此Javascript脚本默认的执行顺序也是根据<script>标签位置来确定的。<script src="1.js">  <script src="2">即使2因为预加载比1先预加载完成,但是2还是等1.js加载执行完之后才执行。

                                            2.默认JavaScript文件加载完就立马在执行栈中执行(defer改变这个默认规则,即加载完成还是等所有HTML其他内容都已解析完成,最后才执行这些script.但是注意,只有所有的<Script>也都执行完了,dom树才算是构建完成,这时候才会触发DOMContentLoaded事件。)【asycn异步加载,但是加载完成也是立马就执行。】

                                            3.在执行js代码的时候,都是在执行栈中由js主线程执行。在此过程中,遇到事件处理程序,交由事件线程处理;【当某元素接收到事件之后,事件线程会将该元素对应的事件处理程序加到任务队列中等待被执行】

                                                                                                                                           遇到定时器,交由定时器线程处理;【定时线程计时,到时间了就将该定时中的回调扔进任务队列中等待被执行】

                                                                                                                                           遇到http请求,比ajax或者axios、promise等则,交由异步http请求线程处理;【当状态等发生变化,就将对应的回调函数扔进任务队列中等待被执行】

  •    提示:对于导入的js文件,也将按照<script>标签在文档中出现的顺序来执行,而执行过程是HTML文档解析的一部分,不会单独解析或者延期执行。 

          一般情况下,在文档<head>标签中包含js脚本,或者导入的js文件。这意味着必须等到全部js代码都被执行完以后才能继续解析后面的HTML部分。 那么,如果加载的js文件很大,HTML文档解析就容易出现延迟,用户首次加载,等待的时间就太长了,体验效果非常不好。 为了避免这个问题,浏览器自身也做了一些优化,比如如果CSSOM构建完成,可以部分HTML解析构建的dom树可以和CSSOM创建一个没有完成的render树,进行布局绘制首屏页面即first paint,然后同时继续解析HTML直到解析完成,构建完整的dom树,然后和CSSOM再创建完成的render树,然后布局、绘制即first paint的核心就是css构建完成、有部分dom元素就可以,但是要注意一点,如果如果js还没有执行,first paint出来页面,用户是不能操作的开发web应用程序时,我们就可以根据这一特点,进行优化页面就是改变js的位置,因为js也是HTML解析的一部分内容,而且first paint不需要js,因此,尽量把js放在HTML的后面。:

                    【1】建议把导入js文件操作放在<body>后面,让浏览器先将网页内容解析并呈现出来后,再去加载js文件,以便加快网页响应速度。【也就是说HTML解析一般的元素,只要cssOM构建完成、只要有DOM元素,在遇到script 的时候,就先进行进行first paint ,然后再进行<script>的相关动作,注意这时候HTML还没有解析完成即dom树还没有彻底构建完成哦,即DOMContentloaded还没有触发哦】

                    【2】延迟执行js文件:<scirpt>标签有一个布尔型属性defer,设置该属性,能够将js文件延迟到页面解析完毕后再运行。这里defer之后,用于开启新的线程下载脚本文件,并使脚本在HTML文档解析完成即dom树创建完成后执行。去请求到网络进程请求到js文件发给渲染进程的这个过程中,HTML是可以继续解析的(即新的线程和GUI渲染线程可以同时运行),并且等到dom树构建完成了,才去开启javascript引擎线程去执行js。那么,相比之前,有个时间是节约了,就是异步加载的时间;还有首屏渲染也不会被阻塞。之前从遇到<script>标签开始HTML解析就停止了。

          【3】异步加载js文件:默认情况下,网页都是同步加载外部js文件的,如果js文件比较大,会影响后面解析HTML代码的速度。上面介绍的方法是最后加载js文件。而现在我们可以使用<script>标签的async属性,让浏览器异步加载js文件,即在加载js文件时,浏览器仍然继续解析HTML文档。这样能节省时间,提升响应速度。

提示:async是HTML5新增的布尔型属性,通过设置async属性,就不用考虑<script>标签的放置位置,用户可以根据习惯吧很多大型js库文件放在<head>标签内。【HTML5新增属性,用于异步下载脚本文件,这个过程中HTML是可以继续解析的(下载js文件和解析HTML同时进行),但是js文件下载完毕立即停止GUL线程并开启js引擎线程去执行代码。而且,哪个<script>中的文件先下载完就执行哪个,不会按照<script>在HTML文档中的顺序执行。这样虽然相比之前节约了加载文件的时间,但是1.<script>中的文件执行顺序改变;2.还是可能会阻塞html解析;】

           通常情况下,在构建 DOM 树或 CSSOM 的同时,如果也要加载 JavaScript,则会造成前者的构建的暂停。当然,我们可以通过 defer 或 sync 来实现异步加载 JavaScript。虽然 defer 和 sync 都可以实现异步加载 JavaScript,但是前者是在加载后,等待 CSSOM 和 DOM 树构建完后才执行 JavaScript,而后者是在异步加载完马上执行,即使用 sync 的方式仍然会造成阻塞。

                    defer和async[简介]:https://blog.csdn.net/liuhe688/article/details/51247484   

                      deferasyncscript标签的两个属性,用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行                       

                         在介绍他们之前,我们有必要先了解一下页面的加载和渲染过程:
                         1. 浏览器通过HTTP协议请求服务器,获取HMTL文档并开始从上到下解析,构建DOM;
                         2. 在构建DOM过程中,如果遇到外联的样式声明和脚本声明,则暂停文档解析,创建新的网络连接,并开始下载样式文件和脚本文件;
                         3. 样式文件下载完成后,构建CSSDOM;脚本文件下载完成后,解释并执行,然后继续解析文档构建DOM
                         4. 完成文档解析后,将DOM和CSSDOM进行关联和映射,最后将视图渲染到浏览器窗口
                         在这个过程中,脚本文件的下载和执行是与文档解析同步进行,也就是说,它会阻塞文档的解析,如果控制得不好,在用户体验上就会造成一定程度的影响。(一般都是空白页面等待的时间太长了,很久很久才能加载出来页面即页面渲染完成)
                         所以我们需要清楚的了解和使用defer和async来控制外部脚本的执行。
                         在开发中我们可以在script中声明两个不太常见的属性:deferasync,下面分别解释了他们的用法:
              defer:用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。
              async:HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。

                         defer

                          

                       显而易见,1.js被延后致至文档解析中其他元素全部解析完成,只剩余包含defer的<script>标签了,这之后defer标签中代码才执行,它的执行顺序比body中的<script>还要靠后。与默认的同步解析不同,defer下载外部脚本的不是阻塞的,浏览器会另外开启一个线程,进行网络连接下载,这个过程中,文档解析及构建DOM仍可以继续进行,不会出现因下载脚本而出现的页面空白。

                      关于defer我们需要注意下面几点:
                                                                1. defer只适用于外联脚本,如果script标签没有指定src属性,只是内联脚本,不要使用defer
                                                                2. 如果有多个声明了defer的脚本,则会按顺序下载和执行
                                                                3. defer脚本会在DOMContentLoaded和load事件之前执行。DOMCountentLoaded:就是HTML解析完成触发的事件。即包含defer的<script>标签中的代码只有等所有的其余HTML中的标签都已经解析完成了,才去一次执行,(如果是外部资源)至于什么时候加载这取决于<script>标签的位置,如果放置在body的最后面,那么请求回来资源立马执行,如果放在head中,则先另起线程加载,然后等最后再一次执行。等HTML中的所有资源都加载并解析或者执行(不包括图片等,这里的资源指的是所有标签以及link、script中的资源),HTML才算解析完成即dom树创建完成。

                       async

                      我们发现,3个脚本的执行是没有顺序的,我们也无法预测每个脚本的下载和执行的时间和顺序。async和defer一样,不会阻塞当前文档的解析,它会异步地下载脚本,但和defer不同的是,async会在脚本下载完成后立即执行,如果项目中脚本之间存在依赖关系,不推荐使用async
                      关于async,也需要注意以下几点:
                                                             1. 只适用于外联脚本,这一点和defer一致
                                                             2. 如果有多个声明了async的脚本,其下载和执行也是异步的,不能确保彼此的先后顺序
                                                             3. async会在load事件之前执行,但并不能确保与DOMContentLoaded的执行先后顺序。【script加上async还是HTML的标签吧,HTML的解析不是等所有标签都加载解析或者执行完成吗?】

                                                            最后我们来回答这个问题:我们为什么一再强调将css放在头部,将js文件放在尾部(这里的js指没有加defer或者async) ?

                                                        按照我们之前的分析用户在输入一个url之后,经过解析URL、网络进程中从服务器请求url中所指向的资源,网络进程再将请求到的资源通过进程之间的通信发给渲染进程,GUI线程运行,进行解析HTML文件,当解析到<link>或者<style>等css的时候,如果是<link>则在经过解析url、网络进程获取数据等一些列的操作获取到css文件然后进行解析,当然这个过程中,HTML仍然在继续解析。当解析到<script>标签的时候HTML解析就停止,如果是外部的资源还需要进行前面的一系列的请求文件,文件请求会之后在JavaScript引擎线程中解析执行,但是JavaScript的加载还需等CSSOM解析完成,因此,当CSSOM解析完成,JavaScript加载执行。

                                                        那么这样,对于<script>放在头部,先加载并执行js,然后继续解析HTML生成dom,从而js阻塞了HTML解析,因为渲染的过程是HTML解析成dom树即HTML解析完成dom树构建完成,css解析完成生成CSSOM树,二者生成render树,然后render树布局,然后绘制,这样页面才能显示出来。

                                                                        对于<script>放在尾部,解析HTML生成dom树,遇到css解析css生成CSSOM树,那么当最后遇到<script>标签的时候,开始请求执行等。那么这个时候,因为<script>是HTML的标签,因此HTML解析还没有完成,因此dom树也还没有彻底创建完成,等到所有的<script>中的代码执行完毕,HTML继续解析,这时候因为后面没有标签,因此HTML解析结束,也就是dom创建完成,这时候cssom创建完成了,二者生成render树,然后布局、绘制页面显示。

                                                        综上分析,那么对于<Script>放在头部和尾部没什么区别,因为都是花一样的时间页面才能渲染出来。那么,为什么说把js放在尾部可以加快网络响应速度呢?经常会有人在回答页面的优化中提到将js放到body标签底部,原因是因为浏览器生成Dom树的时候是一行一行读HTML代码的,script标签放在最后面就不会影响前面的页面的渲染。那么问题来了,既然Dom树完全生成好后页面才能渲染出来,浏览器又必须读完全部HTML才能生成完整的Dom树,script标签不放在body底部是不是也一样,因为dom树的生成需要整个文档解析完毕【当然包括<script>标签中的文件也都解析执行完成】。其实现代浏览器为了更好的用户体验,渲染引擎将尝试尽快在屏幕上显示的内容。它不会等到所有HTML解析之后才开始构建和布局渲染树,等到一定的时间如果html还没有解析完成,那么解析完的这部分HTML内容将显示(只要cssOM树构建完成)。也就是说浏览器能够渲染不完整的dom树,尽快的减少白屏的时间【可能首屏根据浏览器窗口的大小,先渲染排在这个窗口的render树,其余再解析生成完整的dom树,然后生成完整render树,最后全部渲染出来,这时候,用户才能够在浏览器中操作】。假如我们将js放在header,js将阻塞解析dom,dom的内容会影响到First Paint,导致First Paint延后。所以说我们会将js放在后面,以减少First Paint的时间,但是不会减少DOMContentLoaded被触发的时间即dom构建完成。

                                                                                                                  

      html解析、css解析、加载执行<script>标签的关系:【<link><style><script>都是html的元素,因此HTML的解析,就会解析到这些标签,从而css和JavaScript文件才能被加载解析,这是本质】

               1.<script>标签阻断HTML解析;现代浏览器总是并行加载资源,例如,当 HTML 解析器(HTML Parser)被脚本阻塞时,解析器虽然会停止构建 DOM,但仍会识别该脚本后面的资源,并进行预加载即这里需要注意一点,在现在浏览器中,为了减缓渲染被阻塞的情况,现代的浏览器都使用了预加载当解析被阻塞的时候,浏览器会有一个轻量级的HTML(或CSS)扫描器(scanner)继续在文档中扫描,查找那些将来可能能够用到的资源文件的url,在渲染器使用它们之前将其下载下来。(提前去发送请求等,加载那些可能用到的资源,请求到之后缓存在某个地方,将来执行到这里的时候,直接使用)

               2.<script>标签中的代码加载执行受CSS解析的影响,即GUI线程中的CSS解析完成,JavaScript引擎线程才开启去加载执行;

               3.HTML解析和CSS解析可以同时进行,都属于GUI线程。

                   CSS解析(GUI线程)→→影响→→→→

                       ↓影                                     >页面渲染

                       ↓                                      ↑影响

               即<script>标签(js线程)影响>html解析(GUI线程)  1.dom树和cssom都构建完成之后才开始渲染成render树,然后render树在布局、最后绘制出来,即页面渲染完成。

                                                                                         2.GUI线程和js线程永远都是互斥的,一个在运行的时候,另一个挂载。这里一定要注意,当html遇到script的时候,虽然GUI线程仍然在运行,是因为在解析CSS文件。而这时候HTML解析仍然停止的。等CSS解析完成,GUI立马挂载,js线程立马运行。

         所以,script 标签的位置很重要。实际使用时,可以遵循下面两个原则:

  1. CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。

  2. JavaScript 应尽量少影响 DOM 的构建。(前面所述的3种方法)

       切记:为什么js线程和gui线程互斥,以及这3个的影响规则,不需要深究,人家就是这么规定的。你只需要明白在这种规则下,如何更好地渲染页面就可以了

        注:具体再分析一下:

             css:

               这样的 link 标签(无论是否 inline)会被视为阻塞渲染的资源,浏览器会优先处理这些 CSS 资源,直至 CSSOM 构建完毕。

               渲染树(Render-Tree)的关键渲染路径中,要求同时具有 DOM 和 CSSOM,之后才会构建渲染树。即,HTML 和 CSS 都是阻塞渲染的资源。HTML 显然是必需的,因为包括我们希望显示的文本在内的内容,都在 DOM 中存放,那么可以从 CSS 上想办法。

               最容易想到的当然是精简 CSS 并尽快提供它

               关于CSS加载的阻塞情况:

                                                1. css加载不会阻塞DOM树的解析

                                                2. css加载会阻塞DOM树的渲染

                                                3. css加载会阻塞后面js语句的执行

               没有js的理想情况下,html与css会并行解析,分别生成DOM与CSSOM,然后合并成Render Tree,进入Rendering Pipeline;但如果有js,css加载会阻塞后面js语句的执行,而(同步)js脚本执行会阻塞其后的DOM解析(所以通常会把css放在头部,js放在body尾)

               js

               这里的 script 标签会阻塞 HTML 解析,无论是不是 inline-script。上面的 P 标签会从上到下解析,这个过程会被两段 JavaScript 分别打断一次(加载、执行)。

               解析过程中无论遇到的JavaScript是内联还是外链,只要浏览器遇到 script 标记,唤醒 JavaScript解析器,就会进行暂停 (blocked )浏览器解析HTML,并等到 CSSOM 构建完毕,才去执行js脚本。因为脚本中可能会操作DOM元素,而如果在加载执行脚本的时候DOM元素并没有被解析,脚本就会因为DOM元素没有生成取不到响应元素,所以实际工程中,我们常常将资源放到文档底部。                        

           d.布局(布局Render Tree) 【GUI线程】[layout](https://blog.csdn.net/weixin_36199334/article/details/117728548)

           通过以上的过程,现在我们已经有了DOM树和DOM树中每个元素的样式,但是这样还是无法显示页面,因为还不知道DOM节点的集合位置。

           所以接下来需要计算出DOM树中「可见元素」的几何位置,这个计算过程即为布局。

           Chrome在布局阶段有两个任务:创建布局树和布局计算。

            1.创建布局树即生成渲染树(Render Tree)  

            在有了 DOM 树和 CSSOM 之后,需要将两者结合生成渲染树 Render Tree,并且这个过程会去除掉那些 display: none 的节点。此时,渲染树就具备元素和元素的样式信息。 即:CSS根据DOM节点,会生成类似于DOM结构的一个布局树,仅包含了页面上可见内容的信息,如果有 display: none 等,则该元素不属于布局树。如果有p::before {content:"123"} 等伪类的存在,就算它不在DOM中,也会包含在布局树中 

            :生成DOM树的同时会生成样式结构体CSSOM(CSS Object Model)Tree,再根据CSSOM和DOM树构造渲染树Render Tree,渲染树包含带有颜色,尺寸等显示属性的矩形,这些矩形的顺序与显示顺序基本一致。从MVC的角度来说,可以将Render树看成是V,DOM树与CSSOM树看成是M,C则是具体的调度者,比HTMLDocumentParser等。

           可以这么说,没有DOM树就没有Render树,但是它们之间不是简单的一对一的关系。Render树是用于显示,那不可见的元素当然不会在这棵树中出现了,譬如 <head>。除此之外,display等于none的也不会被显示在这棵树里头,但是visibility等于hidden的元素是会显示在这棵树里头的

        【截止这里,虽然render树中每个节点都具有元素和样式,但是,这些节点没有大小等信息,只是按照要显示的顺序用元素+css样式这么排列着,这里可以将css样式理解为我们现在用webstrom打开的一个状态一样,没有任何意义

           2.布局计算

           当renderer构造出来并添加到Render树上之后,它并没有位置大小信息,为它确定这些信息的过程,接下来是布局(layout)。即在有了上面的完整布局树之后,就要开始计算布局树节点的「坐标位置」了,这个过程十分复杂

          

          根据 Render Tree 渲染树,对树中每个节点进行计算,确定每个节点在页面中的宽度、高度和位置。 至此,每个节点都拥有自己的样式和布局信息,下面就可以利用这些信息去展示页面了。  

需要注意的是,第一次确定节点的大小和位置的过程称为布局,而第二次才被称为回流          

           注: 布局阶段输出的结果称为box盒模型(width,height,margin,padding,border,left,top,…),盒模型精确表示了每一个元素的位置和大小,并且所有相对度量单位此时都转化为了绝对单位。

        e.分层(layer tree)

       页面中有很多复杂的效果,比如3D变换、页面滚动、使用z-indexing做z轴排序等,为了方便的实现这些效果,渲染引擎需要为特定的节点生成专用的图层(Layer),并生成一棵对应的图层树,正是这些图层叠加在一起构成了最终的页面图像。

在Chrome中的开发者工具中选择“Layers”便签可以可视化的查看页面分层情况。

          通常情况下,不是布局树的每个节点都包含一个图层,如果一个节点没有对应的图层,那么这个节点就从属于父节点的图层。比如span标签没有专属图层,那么它们就从属于它们的父节点。所以,最终每一个节点都会直接或间接的从属于某一个层。

          比如position、float等这时候创建新的图层,这些元素才从原来的位置中脱离出来,然后进行对齐布局等,那么原来的布局需要进行重新布局。这样,页面元素的位置都已经各就各位了。注:这时候其实,render树不会改变。render树只是按照dom流中的元素以及CSSOM进行合成的以及最后可能在布局的时候给添加了位置大小等。对应的图层树中,才会元素重新布局排列。

          值得一提的是,对于内容溢出存在滚轮的情况也会进行分层 

         

 

           一般满足以下两个条件的元素会被提升为单独的一层。

      (1)拥有层叠上下文属性的元素

          页面是一个二维平面,但是层叠上下文能让HTML元素具有三维的概念,它们会按照自身属性的优先级分布在垂直于这个二维平面的z轴上。

          明确定位属性的元素、定义透明属性的元素、使用css滤镜的元素等等都可以拥有层叠上下文属性。

                    

    (2)需要剪裁(Clip)的地方

           示例:把div中的文字限定在一个大小为200px*200px的区域内,由于文字内容比较多,所以文字的显示区域必定超过这个区域,此时就会产生「剪裁」,

        

 

 

         f.绘制图层数【GUI线程】[paint]

           在完成图层树的构建后,渲染引擎会对图层树中的每个图层进行绘制。       

          以上步骤是一个渐进的过程,为了提高用户体验,渲染引擎试图尽可能快的把结果显示给最终用户。它不会等到所有HTML都被解析完才创建并布局渲染树。它会在从网络层获取文档内容的同时把已经接收到的局部内容先展示出来。

           g. 生成绘制列表[composite layers]【GUI线程】【一般DCL即触发DocumentContentLoaded即HTML解析完成即</body>被解析完成后,就会进行这一步。到这里,就要进入绘出页面的阶段了。即页面已成定性,首屏马上就会被绘制完成】

            将上面的paint阶段的东西,composite 对于存在图层的页面部分,需要进行有序的绘制,而对于这个过程,渲染引擎会将一个个图层的绘制拆分成绘制指令,并按照图层绘制顺序形成一个绘制列表。

          H.光栅化[rasterize paint]【光栅进程】【紧接着上一步的composite layers,这里进行光栅化】
         有了绘制列表后,渲染引擎中的合成线程会根据当前视口的大小将图层进行分块处理,然后合成线程会对视口附近的图块生成位图,即光栅化。而渲染进程也维护了一个栅格化的线程池,专门用于将图块转为位图。
          栅格化的过程通常会使用 GPU 加速,例如使用 wil-changeopacity,就会通过 GPU 加速显示           

          i.显示[GPU][经过这一步之后,才能将HTML渲染在页面中,即首屏绘制完成]【浏览器进程】

          当所有的图块都经过栅格化处理后,渲染引擎中的合成线程会生成绘制图块的指令,提交给浏览器进程。然后浏览器进程页面绘制内存中。最后将内存绘制结果显示在用户界面上。

           

       总结:解析HTML到页面绘制的过程。

            

 

             

 

             在这期间,遇到外载的<script>即<Script src></script>,之前解析的HTML先会先绘制一版即首屏绘制。然后再加载执行这个script中的资源,执行完成之后——parseHTml(解析HTML),如果修改了dom,则还会绘制一版------【外载的,只要遇到就先绘制一版,然后立马停止渲染工作,加载执行,加载执行完本外载<script>中的同步代码,一定是先回调渲染线程,进行渲染,等本次渲染完成之后,再检查任务队列,如果有任务则继续执行】

                           遇到内联的<script>即<script>......</script>,相当于一个普通的HTML标签来解析,只不过需要在JavaScript引擎线程中执行。从<script>开始执行到完成,都当做是<html>的parse html即解析,一版情况下执行完这个<script>还会继续解析HTML,但是如果这个script的执行时间比较长,那么因为html隔固定时间就会渲染一次页面,因此执行完比较长的script之后(相当于解析HTML时间过长),就会绘制一版。然后继续解析[一定是本次本次渲染完之后再去检查任务队列]

                           那么,对于异步,在执行代码的过程中遇到就丢给自己的线程处理,到了某个条件或者某个时间被放在任务队列中等待被执行。那么任务队列中的回调和渲染又是怎么交替进行的呢?一般情况下,渲染完一次之后,都会检查一下任务队列,如果有任务就执行,至于什么时候回到渲染线程渲染,这个还是和任务队列中的回调执行的时间长度有关系,如果时间比较长了,就会先渲染以及再继续执行任务队列中的任务,如果这些任务中的执行时间都比较断,则统一执行完之后再回调渲染线程中进行渲染。

            具体的可以查看一下浏览器的performance 

                       因此,综上所述,一般我们将<script>放在底部,这样首屏绘制就会早一些,如果放在head中,必须等到外载的、内联的script都加载完成了再渲染,首屏太慢了。

                       对于,defer,即放在<head>中的外载<script>加上defer,那么相当于将该外载<script></script>放在</body>的前面即紧挨着</body>。也就是相当于将<Script>放在底面。同样等这些脚本都执行完了,才会触发DCL.

       回流与重绘【GUI线程】【无论是回流还是重绘都是直接针对rendertree】

       回流(reflow):将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流

       当浏览器发现某个部分发生了点变化影响了布局,需要倒回去重新布局。reflow 会从 <html>这个 root frame 开始递归往下,依次计算所有的结点几何尺寸和位置。reflow 几乎是无法避免的。现在界面上流行的一些效果,比如树状目录的折叠、展开(实质上是元素的显示与隐藏)等,都将引起浏览器的 reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲染。通常我们都无法预估浏览器到底会 reflow 哪一部分的代码,它们都彼此相互影响着。

           引起回流的操作有哪些?【元素的位置改变或者尺寸的改变都会导致回流】。那么回流的程度到是什么呢?

             reflow回流的成本开销要高于repaint重绘,一个节点的回流往往回导致子节点以及同级节点的回流;因为,回流并不会回流到根节点,而是最小成本的回流。比如,我们改变一个元素的尺寸,那么该元素的子元素或者相邻元素应该会跟着移动位置,因为,该元素及该元素的子孙后代节点、涉及到的相邻节点及其子孙后代元素节点的都会进行回流-重绘。

         例如:在body元素前面插入一个子元素,所有的节点都需要回流和重绘,成本太高了。

 

      什么操作会引起重绘、回流

      其实任何对render tree中元素的操作都会引起回流或者重绘,比如:

          1. 添加、删除元素(回流+重绘)

      2. 隐藏元素,display:none(回流+重绘),visibility:hidden(只重绘,不回流)

      3. 移动元素,比如改变top,left(jquery的animate方法就是,改变top,left不一定会影响回流),或者移动元素到另外1个父元素中。(重绘+回流)

      4. 对style的操作(对不同的属性操作,影响不一样)

      5. 还有一种是用户的操作,比如改变浏览器大小,改变浏览器的字体大小等(回流+重绘)

 

      重绘(repaint):改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。(重新绘制render树,不需要重新布局。)每次Reflow,Repaint后浏览器还需要合并渲染层并输出到屏幕上。所有的这些都会是动画卡顿的原因。Reflow 的成本比 Repaint 的成本高得多的多。一个结点的 Reflow 很有可能导致子结点,甚至父点以及同级结点的 Reflow 。在一些高性能的电脑上也许还没什么,但是如果 Reflow 发生在手机上,那么这个过程是延慢加载和耗电的。可以在csstrigger上查找某个css属性会触发什么事件。

     reflow与repaint的时机:     

        回流必将引起重绘,而重绘不一定会引起回流。

  1. display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发生位置变化。

  2. 有些情况下,比如修改了元素的样式,浏览器并不会立刻 reflow 或 repaint 一次,而是会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow。

  3. 有些情况下,比如 resize 窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行 reflow。

      再比如:

      让我们看看下面的代码是如何影响回流和重绘的:

       var s = document.body.style;

       s.padding = "2px"; // 回流+重绘

       s.border = "1px solid red"; // 再一次 回流+重绘

       s.color = "blue"; // 再一次重绘

       s.backgroundColor = "#ccc"; // 再一次 重绘

      s.fontSize = "14px"; // 再一次 回流+重绘

      // 添加node,再一次 回流+重绘

      document.body.appendChild(document.createTextNode('abc!'));

      请注意我上面用了多少个再一次。

      说到这里大家都知道回流比重绘的代价要更高,回流的花销跟render tree有多少节点需要重新构建有关系,假设你直接操作body,比如在body最前面插入1个元素,会导致整个render tree回流,这样代价当然会比较高,但如果是指body后面插入1个元素,则不会影响前面元素的回流

        聪明的浏览器

     从上个实例代码中可以看到几行简单的JS代码就引起了6次左右的回流、重绘。而且我们也知道回流的花销也不小,如果每句JS操作都去回流重绘的话,浏览器可能就会受不了。所以很多浏览器都会优化这些操作,浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会把reflush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。即:这里的聪明的浏览器,指的是浏览器的渲染机制:一个宏任务——>所有微任务——>渲染——一个宏任务——>所有微任务——>渲染。任何的渲染或者底层一定是通过这样的方式实现的。例如:

       

        虽然有了浏览器的优化,但有时候我们写的一些代码可能会强制浏览器提前reflush队列这样浏览器的优化可能就起不到作用了。当你请求向浏览器请求一些style信息的时候,就会让浏览器flush队列,比如:

                  1. offsetTop, offsetLeft, offsetWidth, offsetHeight

                  2. scrollTop/Left/Width/Height

                  3. clientTop/Left/Width/Height

                  4. width,height

                  5. 请求了getComputedStyle(), 或者 ie的 currentStyle

  当你请求上面的一些属性的时候,浏览器为了给你最精确的值,需要flush队列,因为队列中可能会有影响到这些值的操作。

      如何减少回流、重绘

      减少回流、重绘其实就是需要减少对render tree的操作,并减少对一些style信息的请求,尽量利用好浏览器的优化策略。具体方法有:

     关键渲染路径与阻塞渲染

      在浏览器拿到HTML、CSS、JS等外部资源到渲染出页面的过程,有一个重要的概念关键渲染路径(Critical Rendering Path)。

      例如为了保障首屏内容的最快速显示,通常会提到一个渐进式页面渲染,但是为了渐进式页面渲染,就需要做资源的拆分,那么以什么粒度拆分、要不要拆分,不同页面、不同场景策略不同。具体方案的确定既要考虑体验问题,也要考虑工程问题。了解原理可以让我们更好的优化关键渲染路径,从而获得更好的用户体验。

      现代浏览器总是并行加载资源,例如,当 HTML 解析器(HTML Parser)被脚本阻塞时,解析器虽然会停止构建 DOM,但仍会识别该脚本后面的资源,并进行预加载。

      同时:

           1.CSS 被视为渲染阻塞资源 (包括js):这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕,才会进行下一阶段。CSSOM阻塞html解析和dom的构建,因为需要和dom树一起构建渲染树,所以,CSSOM会阻塞渲染。又由于JavaScript可以操作CSSOM属性,因此,CSSOM构建会阻塞JavaScript的执行。即:                                                                                                                                                                                                                                                                     css加载不会阻塞DOM树的解析

                                                                                                                                                                                       css加载会阻塞DOM树的渲染

                                                                                                                                                                                       css加载会阻塞后面js语句的执行

           

            没有js的理想情况下html与css会并行解析,分别生成DOM与CSSOM,然后合并成Render Tree,进入Rendering Pipeline;但如果有js,css加载会阻塞后面js语句的执行,而(同步)js脚本执行会阻塞其后的DOM解析(所以通常会把css放在头部,js放在body尾)

           2.JavaScript 被认为是解释器阻塞资源:HTML解析会被JS阻塞,因为它不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性 。

            JavaScript 的情况比 CSS 要更复杂一些。如果没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的HTML元素之前,也就是说不等待后续载入的HTML元素,读到就加载并执行。

            解析过程中无论遇到的JavaScript是内联还是外链,只要浏览器遇到 script 标记,唤醒 JavaScript解析器,就会进行暂停 (blocked )浏览器解析HTML,并等到 CSSOM 构建完毕才去执行js脚本。这也会导致一个问题:  因为脚本中可能会操作DOM元素,而如果在加载执行脚本的时候DOM元素并没有被解析,脚本就会因为DOM元素没有生成取不到响应元素,所以实际工程中,我们常常将资源放到文档底部。  

          对于1,即存在css阻塞资源,浏览器会延迟 JavaScript 的执行DOM 构建,即CSSOM 构建时,JavaScript 执行将暂停,直至 CSSOM 就绪       

          对于2,当浏览器遇到一个 script 标记时,因为HTML解析被阻塞,因此DOM 构建将暂停,直至脚本完成执行,这是因为JavaScript 可以查询和修改 DOM 与 CSSOM。     

          通过1,2   结论   script 标签的位置很重要

             实际使用时,可以遵循下面两个原则:

                                                             1.CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。

                                                             2.JavaScript 应尽量少影响 DOM 的构建。

       

         进入渲染进程后,可以总结如下:【解析可以理解为HTML文档解析和JavaScript加载+执行。此消彼长的关系,相互排斥的关系】

         1.解析HTML,同时构建dom树。因为HTML中包含link,script标签,因此当解析到link的时候,渲染线程

         2.下载css资源并开始构建CSSOM树,这时HTML继续解析并继续构建dom树。渲染线程

         3.当解析html遇到script标签(同步的情况下),HTML停止解析HTML元素(这时候当然也停止构建dom树了),加载并执行JavaScript。【当然,如果cssom树还没有构建完成,JavaScript还需要等cssom构建完成才去执行;由于JavaScript有可能需要操作dom树,而dom树没有构建完成,所以会导致很多问题。】【javascript引擎线程】

         因此,针对以上的问题,就用到了上述的一些解决办法。

         4.根据dom树和cssom树构建为渲染树,然后根据渲染树进行布局,最后绘制在页面中。【渲染线程

        注释:在浏览器显示HTML时,它会注意到需要获取其他地址内容的标签。这时,浏览器会发送一个获取请求来重新获得这些文件。

                下面是几个我们访问facebook.com时需要重获取的几个URL:

                图片

                       https://upload.chinaz.com/2013/0228/1362014211396.gif 

                       https://upload.chinaz.com// 

                       …

               CSS 式样表

                         http://static.ak.fbcdn.net/rsrc.php/z448Z/hash/2plh8s4n.css

                         http://static.ak.fbcdn.net/rsrc.php/zANE1/hash/cvtutcee.css

                           … 

              JavaScript 文件

 

                         http://static.ak.fbcdn.net/rsrc.php/zEMOA/hash/c8yzb6ub.js

 

                         http://static.ak.fbcdn.net/rsrc.php/z6R9L/hash/cq2lgbs8.js 

                         …

                         这些地址都要经历一个和HTML读取类似的过程。所以浏览器会在DNS中查找这些域名,发送请求,重定向等等...

   还可参考文档:https://blog.csdn.net/dianxin113/article/details/104351670

      

     最后来一个加载渲染的过程:

  1. 用户输入网址(假设是第一次访问),浏览器向服务器发出请求,服务器返回html文件;
  2. 浏览器开始载入html代码,发现<head>标签内有一个<link>标签引用外部CSS文件;
  3. 浏览器又发出CSS文件的请求,服务器返回这个CSS文件;
  4. 浏览器继续载入html中<body>部分的代码,并且CSS文件已经拿到手了;
  5. 浏览器在代码中发现一个<img>标签引用了一张图片,向服务器发出请求。此时浏览器不会等到图片下载完,而是继续加载后面的代码;
  6. 服务器返回图片文件,由于图片占用了一定面积,影响了后面段落的排布,因此浏览器需要回过头来重新渲染这部分代码;
  7. 浏览器发现了一个包含一行Javascript代码的<script>标签,直接运行该脚本;
  8. Javascript脚本执行了这条语句,它命令浏览器隐藏掉代码中的某个<div> (style.display=”none”)。少了一个元素,浏览器不得不重新渲染这部分代码;
  9. </html>表示暂时加载完成;
  10. 此时用户点了一下界面中的“换肤”按钮,Javascript让浏览器换了一下<link>标签的CSS路径;
  11. 浏览器向服务器请求了新的CSS文件,重新加载页面。然后执行渲染过程

      前端优化:

  1.        减少HTTP请求次数和请求内容的大小:
  2.        CSS/JS进行合并压缩,样式或者JS都压缩成为一个文件,然后再导入
  3.        对于简单的项目(尤其是移动端)我们最好把CSS和JS都采用内嵌方式引入
  4.        雪碧图(图片精灵、sprite) 把一些小图合并到一张大图上,然后通过背景定位来使用
  5.        图片延迟加载(在第一次打开页面的时候不加载真实图片,当页面加载完成后在开始加载真实的图片)
  6.         ...

总结:从请求回来html开始,渲染引擎开启,解析html:

            1.解析html普通标签为dom树;

            2.遇到link标签,去加载css资源,同时继续往下解析html;

            3.遇到<script>标签,渲染引擎停止解析,此时如果css资源还没有加载完成,等待;当css资源加载完成,V8引擎开启去加载并执行<script>中的代码;

           4.当执行完本元素<script>中的同步代码+微任务队列中的代码之后,v8挂起;

           5.渲染引擎开启,继续解析html或者绘制(注意:这时候如果时间花费的比较久了,那么渲染引擎会根据现在已经加载dom和css进行首屏渲染)。如果是首屏渲染,则渲染完之后,继续解析html。如果遇到<script>则渲染引擎挂起,v8引擎开启;重复4-5【疑问:首屏渲染过的,第二次还渲染吗?】

          6.直到dom解析完成。渲染绘制完成

          7.然后渲染引擎挂起,v8引擎开启,执行等待在任务队列中的异步,仍旧是保持宏+微;

          8.v8挂起,渲染引擎开启,渲染刚更新的dom;然后周而复始重复7-8的过程

         从开始到最后,不管什么时候,一定是遵守:渲染——>一个宏任务+所有等待的微——〉渲染——>一个宏任务+所有等待的微.......  下面中的图片就是v8引擎执行代码和核心过程与总结

        

 

6.深入了解v8即js引擎

 上述的从浏览器地址输入url到页面渲染的过程。即打开一个浏览器窗口,在浏览器地址栏输入url请求对应的html文件,资源请求回来之后,浏览器渲染引擎开启开始解析html即先执行HTML形成DOM树,遇到link请求样式Css并解析形成CSSOM,遇到script标签,则渲染引擎挂起,v8引擎开启解析、解释、编译(热点函数)、执行js代码。那么v8引擎到底是如何工作呢?下面具体来学习v8引擎如何处理js代码的(个人理解:当打开一个浏览器窗口,那么操作系统就会给这个进程分配一块内存区域,那么这个进程中的有关数据等都是存放在这块内存区域中)。

 资料v8学习:【V8引擎-js执行原理】https://mbd.baidu.com/ma/s/lSYYTSFM

浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分:

                                                                                       渲染引擎:负责解析HTML,布局,渲染等工作
                                                                                       JS引擎:解析·编译.执行JS代码

V8 是一个快速的 JavaScript 引擎,它在 Chrome 浏览器中使用,也可以作为一个独立的 JavaScript 运行时使用。V8 引擎由 Google 开发,它使用 C++ 语言编写,提供了一个快速、高效的解释器和编译器,用于解析,编译、执行(解释) JavaScript 代码
在说V8的执行JavaScript代码的机制之前,我们先了解一下语言:javascript是一门解释型的高级编程语言, 有高级编程语言,就有低级编程语言,从编程语言发展历史来说,可以划分为三个阶段:
机器语言:10010001111, 一些机器指令
汇编语言:mov ax,bx, 一些汇编指令;
高级语言: C , C++ , Java , Javascript , Python
计算机本身是不认识这些高级语言的,所以我们的代码最终还是需要被转换成机器指令:

高级语言又分编译型解释型我们先来看看编译型和解释型语言的区别。
(1)编译型语言和解释型语言
我们知道,机器( cpu)是不能直接理解代码的。所以,在执行程序之前,需要将代码翻译成机器能读懂的机器语言。按语言的执行流程,可以把计算机语言划分为编译型语言和解释型语言:

编译型语言:在代码运行前编译器直接将对应的代码转换成机器码,运行时不需要再重新翻译,直接可以使用编译后的结果;(比如c语言,编译之后,直接生成一个二进制文件,之后调用方法等,直接在cpu中执行二进制文件中对应的码值)
解释型语言:需要将代码转换成机器码或者能被解释器理解执行的中间码(如字节码),和编译型语言的区别在于运行时需要转换。解释型语言的执行速度要慢于编译型语言,因为解释型语言每次执行都需要把源码转换一次才能执行。

接上面,html遇到script标签,渲染引擎挂起,v8引擎开启,将已经加载的js代码交给V8引擎,开始运行即Stream获取到源码并且进行编码转换;参考:【Javascript引擎-V8引擎】https://mbd.baidu.com/ma/s/d082EncO

v8主要组成:【https://mbd.baidu.com/ma/s/AZMH2uSx(】

也可以简化为下图所示

再具体化为: 

   

  Blink将js代码交给V8引擎,Stream获取到源码并且进行编码转换;

  Scanner:会进行词法分析,词法分析会将代码转换成tokens;即Scanner会进行词法分析,词法分析会将代码转换成tokens;即词法分析也称为分词,是将字符串形式的代码转换为标记(token)序列的过程。这里的token是一个字符串,是构成源代码的最小单位,类似于英语中单词。如:const a = 'hello world'经过esprima词法分析后生成的tokens:

[ { "type": "Keyword", "value": "const" }, { "type": "Identifier", "value": "a" }, { "type": "Punctuator", "value": "=" }, { "type": "String", "value": "'hello world'" } ]

 注:<script>中的所有js代码都将执行词法分析。应该是只会执行一次词法分析即初始化的时候。即要执行的script中的所有代码都将一句一句被解析为tokens,在完成这一过程之后,再根据tokens生产AST树

 Parser: 解析器,负责将tokens解析成AST。即Parser解析器负责将tokens转换成抽象语法树AST。也可以称为语法分析。即语法分析:语法分析是将词法分析产生的token按照某种给定的形式文法转换成AST的过程。也就是把单词组合成句子的过程即tokens会被转换成AST树。注:对于语法报错就在这个阶段发现的,如果发现了语法错误直接报错,并停止转化。 Parser就是直接将tokens转成AST树架构。

             在这个阶段中,也就是说某个执行环境或者函数局部执行环境]中,那些立即执行的代码是会被直接parse为AST树,但是有些不是立即执行的代码如函数声明等会进行预解析:1.这个preparse检查该函数的语法错误,如果有语法错误,会报错并停止parse。2.如果没有语法错误,则只是preparse,而不会parse为AST语法树。而是对函数声名做提升即在堆内存中开辟一块内存区域用于保存该函数,确定该函数的父级的作用域链(所谓的父级就是当前执行环境;父级的作用域链就是执行环境中的(作用域链-arguments-this)即当前执行环境中的vo+父级作用域链;然后将父级的作用域链保存在函数体的[[scopes]]中)等,并且该函数声明的函数名提升为该执行环境对应的变量对象的一个变量,这个变量指向这个函数的对内存地址,整个过程就是提升。(后面会有具体的实例来讲解)即预解析。parse即代码一句一句从上到下解析即解析tokens,遇到不立即执行的代码,则先进行预解析即预解析就是浏览器执行代码前的一项工作,预解析主要工作是查找和声明变量、函数以及它们的作用域,并将它们保存在内存中即这个阶段进行变量提升。只有等真正被调用的时候才会Parse为AST树。PreParser称之为预解析(因为并不是所有的js代码,在一开始时就会被执行,那么对所有的js代码进行解析,必然会影响网页的运行效率即1.一次性解析所有代码,代码执行时间变长,2、内存消耗增加,因为解析完的AST以及根据AST编译后的字节码都会存放在内存中,3、占用磁盘空间,编译后的代码会缓存在磁盘上。,所以V8引擎就实现了Lazy Parsing(延迟解析)方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全局解析是在函数被调用时才会进行)例:

var  a=1
function  foo(x,y){
   return  x+y
}
foo(2,3) 

注:如果一个函数没有被调用是不会被转换成AST的。函数只有在被调用的时候才会真正的parse为AST语法树。为啥只说函数preparse呢?因为除了函数,其他的都在是初始化时会被执行的,因此都会被解析的。只有函数被定义后,在初始化时不会被执行即调用,因此,才会预解析,只有在解释器中真正被执行调用时才真正的parse。对于定义的变量,var 左边=右边,虽然左边的变量名会进行变量提升(函数定义一样在创建执行环境阶段就提升为变量对象的一个变量保存在内存中,并赋值undefied),在执行环境入栈后,执行到该句代码的时候,用右边的值修改内存中该变量的值。var定义的变量也进行了变量提升,但这种行为不是preparse。

        总结:parse阶段其实也就是一段要执行的代码中立即执行语句被编译为AST(包括普通变量的提升)、该段代码中函数声明进行preparse即语法检查和提升函数声明、创建这段代码的执行环境(全局执行环境或者函数局部执行环境)如果这时候还不创建就没办法做变量提升以及确定预解析函数的[[scope]]的值了。还有必须是这个阶段创建执行环境,因为例如一个函数被调用开始parse解析:创建执行环境,然后做变量提升,最后解析代码为AST树。变量对象等。也即是说parse是针对某一段代码即全局代码或者被调用的函数中的所有代码进行parse为AST,而prepares是针对parse某一段代码中,某个函数的声明。如上例中,在parse这段全局代码的过程中,遇到了function foo声明,因此该声明进行preparse:开辟一段堆内存存储该函数声明并且该内存地址保存在正在parse的代作用域的变量对象中、存储该函数的父级作用域链【所处执行环境中的作用域链】。即parse中编译为创建执行环境、变量提升(某个函数声明preparse)即创建变量对象、parse为AST树。ps:有些文档中写道:作用域连是在prepares阶段确定的,这种说法不是很准确,针对函数的作用域链是prepares该函数声明时确定的,但是当全局没有prepares即直接parse全局代码,因此,全局的作用域链应该是parse的时候创建的。具体分析一下执行环境、变量对象、变量提升的在parse阶段的过程:当一段代码要执行的时候,从tokens进入parse解析阶段,在parse解析阶段:具体看如下图所示:

       

             1.先创建执行环境:作用域链(应该是执行环境就包括作用域链,而作用域链中包含当前执行环境的AO+父级作用域链)           

                                      作用域的顶端:AO:

                                             a.先在栈内存开辟存储空间用于存放参数对象arguments,这个对象的值在这个创建阶段就确定好了,就有值了。

                                             b.vo:变量对象即应该是在栈内存中开辟一块空间。【等执行环境创建完成之后,会解析生成AST树,在解析为AST树的过程中对扫描执行环境中代码,在这时候会进行变量提升即将执行环境定义的变量和函数声明保存在之前创建的vo对应的那块栈内存中即保存在变量对象中。在这个过程中,遇到函数声明:首先将函数声明的函数名保存在变量对象中,然后在堆内存中开辟一块内存空间用于保存这个函数对象以及给该函数创建原型、给函数的[[scope]]中赋值为该执行环境的作用域链即:vo+父级作用域链等一系列的操作。整个过程称为对函数声明的preParse即预解析。】

 

                                             c.this:在栈内存中开辟存储空间用于保存this指针,至于这个指针指向哪个对象即this的值是多少由调用函数时,调用方式决定this的值应该是代表哪个对象。即this就是一个引用类型对象,当一个执行环境中的this指向了哪个引用类型对象,那么这个执行环境的代码中的所有对this的操作都是对这个引用类型对象的操作。

                                               例如:obj={name:"zhangsan",age:30};function mess(){this.message=this.name+this.age console.log(this)};

 

                                                        obj.mess()//打印的结果为obj{name:"zhangsan",age:30,message:130}.就是因为在创建mess函数的执性环境时,我们执行了this指向了obj对象,所以在这个执行环境的执行过程中,this都是指向obj对象。

 

                                             父级作用域链:如果是全局执行环境,这里就没有父级作用域链;对于局部作用执行环境,这个就是该函数在声明时在[[scope]]中设置的值。

                                       AO+父级作用域链组成该执行环境的作用域链。

              但是要注意:子函数的[[scope]]的值为当前执行环境的vo(变量对象)+父级作用域链,不是AO,因为AO包含了arguments和this对象。

                                    这上面的AO+父级作用域链中包含的那些变量对象就是本执行环境可以使用的所有的数据来源。arguments、this和vo变量对象一样,在栈内存中开辟空间vo对应的就是变量对象,arguments、this对应的就是那个新开辟的各自空间,当执行环境销毁时,因为AO会被销毁,所以当前执行环境中的this、arguments最后就会被释放;而变量对象vo就不一定了,如果被保存在子函数的[[scope]]中就不会被销毁,直到不再被引用。就是说argument、this和vo对应的变量对象都需要在栈内存中新开辟一块空间,arguments、this对应的存储空间根据执行环境销毁而变量对象不一定。

          2.然后,开始扫描解析执行环境中的代码进行变量提升即将执行环境中定义的变量和方法保存在栈内存中和解析为AST。即从上到下,遇到一句代码如果有定义或者声明则先将变量保存在变量对象中;当然如果是函数,则函数变量名保存在变量对象中,同时在堆内存中开辟一块空间保存函数相关的东西,同时这个变量名指针指向了新开辟的这个堆内存栈(整个过程称为函数的prePares)。

         3.变量提升完成后,解析剩余的立即执行代码为AST。例如:var a=10;先进行2即将var a进行变量提升保存在变量对象中;等变量提升完成后,解析a=10语句为AST。对于函数声明应该就是一次性的即在变量提升的时候就开辟空间等一系列的操作即preParse。

          4.parse为AST后解释为字节码,然后压上面创建的执行环境入栈执行代码。

附加:上述的分析都是普通变量和函数定义的变量提升,那么对于对象变量和对象中的方法进行提升吗?如果提升的话,是在哪个阶段呢?

var obj={
  name:"zahngsna";
  age:99,
  getMessage:function(){
     return 。.name+this.age
   }
}

 首先对于对象的定义,做变量提升;即如上例中obj在parse阶段提升到变量对象中并赋值undefined。

   然后当执行代码的时候,执行赋值语句即将{}对象赋值给obj,即obj的值为指向堆内存中的{}对象的地址。疑问?那么这个对象中的方法getMessage是什么时候进行提升和确定父作用域链的呢?其实上面的创建对象的写法是字面量的写法,本质还是通过new调用Object构造函数创建的对象,即上面等同于var obj=new Object(....)即通过new的方式调用构造函数Object。这就很明确了,当执行function Object函数的时候,会先parse相关的代码,在parse阶段会先创建执行环境:确定arguments的值,VO变量对象,this即创建作用域链即Ao+这个函数体中的[[scopes]]的值,this的值(这里的this是window);然后给vo变量对象赋值即变量提升阶段,遇到了函数getMessage,故而这个函数即对象中的方法进行变量提升即进行preparse即在堆内存中开辟一个区域存放函数体并且将Object的执行环境中的变量对象(vo)+父级作用域链(构造函数Object的[[scope]])赋值给getMessage的[[scopes]]。当调用这个方法的时候,再parse该方法确定this的值为调用该方法的对象等。上例如下:

    

   注:下面具体以实例来讲解:在讲解之前,我们再具体一些概念:

                                                           作用域:作用域应该是一种规定或者规范,JavaScript中约定只有两种作用域:全局作用域和局部作用域(函数作用域)。子级作用域中,可以通过作用域链中的变量对象来访问父级的作用域中的变量,而父级不能访问子级中定义的变量。

                                                   变量对象(VO):变量对象不是一个对象,而是给对应执行环境分配的一块用于保存基本类型变量和对象即引用类型数据地址的栈内存区域;(不然,变量存放在变量对象中,而基本类型变量保存在栈内存中?如果变量对象是一个对象,而对象是引用类型,那么对象是存在堆内存中,怎么又说变量对象中的基本类型保存在栈区呢?)。每一个执行环境都有一个与之关连的变量对象,用于存储该执行环境中的变量。变量对象的定义:每个执行环境都有一个与之关联的变量对象,在环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。一定注意这里说的环境中定义的所有变量和函数都保存在这个对象中即一个全局中或者函数中定义的所有变量和函数都保存在对应的变量对象中,为什么一定要注意呢?因为在JavaScript中,只有全局或者函数作用域,因此对于例如在{}括号中定义的变量或者函数,同样属于全局或对应函数,因此都属于全局或者对应函数的变量对象。【后面以例子为例,进行进一步讲解】。如前面的讲解变量对象这个栈内存是在parse阶段的创建执行环境的vo时开辟的,然后执行环境创建完成后扫描代码进行变量提升是将执行环境中定义的变量和函数保存在这个新开辟的空间中即变量提升。

                                                                     1.普通情况下的函数定义和变量定义的提升:

                                                                        一、函数+变量:var申明的变量会被提升,但是函数的提升优先级高于变量的提升。但虽然变量的var定义不会覆盖函数声明,可是变量的赋值会覆盖函数

例如:var a=1;

         function a(){console.log(2)}

                                                                                       console. log(a)//打印的是1

                                                                                       parse阶段,解析var a的时候,将其提升为变量对象的一个数据保存在内存中;

                                                                                       接着,解析function a,遇到同名的了,因为变量对象中已经有a变量了,所以直接在堆内存创建这个函数的存储空间同时创建该函数的原型对象,函数的属性prototype指向了这个原型对应以及[[scope]]的值为这个执行环境的作用域链。然后变量a指向这个堆内存。因此变量对象中的a的值为指向函数的地址;

                                                                                       然后,因为变量全部都提升完毕,那么解析parse要立即执行的语句:这里就是 a=1;console.log(a)语句为AST.也就是说var a;这样的语句只会提升为变量对象,并不存在解析为字节码。

                                                                                       最后,解释AST为code并执行字节码:a=1;这里相当于修改变量对象中a变量的值为1;

                                                                                                                                          因此执行console.log(a)的结果为1,而不是函数体。

                                                                             即:变量对象中,对于同名的变量定义和函数,只会保留函数的声明。即如果变量对象中有变量同名的定义,则遇到函数定义,会直接用函数地址指针替换;如果变量对象中已有同名的函数定义,如果遇到变量定义直接跳过。

                                                                        二.函数+函数

                                                                           function a(){...}

                                                                           function a(){...}

                                                                           先parse到第一个a,变量提升到变量对象中,接着遇到第2个a则,变量对象中只保存第二个函数体的变量名a,即直接替换上面的函数

                                                                        三.变量+变量

                                                                        其实,这个也不存在替换或者跳过,因为变量对象中只会有一个变量名,并且值为undefined的,所以,遇到同名的变量定义,就是只会有一个。

                                                                        总结:一个执行环境的变量对象中,只能有一个变量名,即变量名都不能重复。如果有同名的function声明的函数,那么同名的parse的结果变量对象中保存的一定是最后面的那个function声明的函数体;如果没有function,则变量名的值为undefined。有一点要注意:在执行代码的时候,赋值语句就会区分函数还是变量,如上例,=右边给变量名赋的什么值,变量对象中对应的变量就被修改为什么值,如上例中,虽然因为函数声明优先级高于变量定义即parse阶段变量对象中a的值为指向a函数体的地址,但是,奈何,执行代码时,函数定义不会执行,而变量定义的赋值语句会执行,因此,最后还是保存了变量的值。【所以啊,三十年河东三十年河西,谁笑到最后还不定一定呢】

                                                                      2.块中的函数定义和变量定义的提升即除了函数之外的其他{}中的提升:

                                                                      变量定义提升:不管变量定义有多深,在其对应的全局代码或者函数体在parse阶段都和其他变量一样被进行提升。如:
1.console.log(a)
if(true){//这里条件变为false,也是在变量提升阶段进行提升
 var a=10;
2.console.log(a)
}
3.console.log(a)

                                                                        执行结果为1.undefined,这里没有报错,因为if里面定义了var a,a在变量提升阶段同时进行了提升,因此在执行1的时候,变量对象中已经有a变量了,并且赋值为undefined。

                                                                                      2当执行代码的时候,a=10,因此console.log(a)为10;

                                                                                      3.因为if中的变量定义被提升到该全局的变量对象中,又因为a的被赋值10,因此打印结果也为10

                                                                        这里,如果改为if(fasle),同样如此,只不过a的值为undefined,因此没执行赋值语句。                     

                                                                        函数定义提升,这个现在有些混乱,不好确定 ,不同的浏览器解析的结果都不一样,因此尽量还是不要在除了函数之外的块中定义函数                                                                      

                                                  执行环境:在javascript中有两种执行环境:全局执行环境和局部执行环境(函数执行环境),所有的程序的执行(即执行全局代码或者调用执行函数中的代码),都需要创建对应的执行环境,并且执行环境入栈才能被执行,执行完之后,执行环境出栈并销毁。执行环境包括3个部分:

                                                                1.活动对象AO:arguments+VO+this;VO就是上面的变量对象。vo是在创建执行环境时,在栈内存中创建一块存储区域用于存储该执行环境中定义的变量和方法即变量对象。然后等变量提升完成之后,将剩余的立即执行语句解析为AST树,然后解释器解释为字节码并运行。

                                                                 为什么arguments、this在活动对象中而不在变量对象中?因为我们打印一个函数在声明时[[scopes]]中保存父级作用域链。【就是说arguments和局部作用于中的this一样,在parse阶段创建执行环境的时候创建的,当执行环境销毁的时候,随之销毁。】

var  a=1
function  foo(x,y){
    var b="foo"
    console.log(a)
   function bar(){
    var c="bar"
    console.log(b)
   }
   console.dir(bar)
   return  x+y
}
foo(2,3) 

展开:

                                                                2.作用域链:全局执行环境作用域链就是window

                                                                                  函数作用域链:AO+函数定义时保存的[[ scopes]]即父作用域链

                                                                                  个人理解:javascript中的所有代码无论是全局还是函数中的代码,都需要在对应的执行环境中执行即执行该全局或者函数代码,都需要创建执行环境,压入执行栈执行。每个执行环境都对应一个变量对象,而每个执行环境中都有一个作用域链,这个作用域链就是如AO(当前执行环境对应的变量对象+arguments+this)+父级作用域链。也就是说作用域链就是一个个变量对象的组合,作用域的最前端就是当前的AO,最后面的是window。父级作用域链是由函数定义的物理位置决定的。作用域链的使用和意义:1.作用域链的使用:在一个执行环境中执行代码的时候,如果是对变量还是对函数的操作,都需要在作用域链上查找该变量或者函数有没有被定义或者声明,先从作用域链的最顶端查找即本执行环境对应的变量对象,如果没有再向上一级的变量对象查找,直到找到为止即找到就停止再向上查找,如果直到window还没有,就为undefined。2.作用域链的意义:因为一段代码即执行环境中的代码执行完毕之后,对应的执行环境出栈并销毁,与之相关的内容如arguments,this等组成的作用域链都会随之销毁,而因为闭包中要使用到该执行环境的变量对象中的数据,所以在执行环境销毁的时候不能销毁该执行环境对应的变量对象,因此,需要将该执行环境中的vo+父作用域链复制给闭包的[[ scopes]],而这个复制是在该闭包对应的声明进行变量提升也就是对该函数进行preparse时即在定义的该函数的执行环境在(parse)创建时复制的。作用域链和闭包关联性很大。在JavaScript中有闭包的概念:闭包指的就是有权访问另一个函数作用域中的变量的函数,通常见到的创建闭包的方式就是在一个函数内部声明创建另一个函数,故而里面的这个函数就是闭包。具体的闭包的一些东西看JavaScript那一篇内容。注:闭包就是一个函数,这个函数不是在全局执行环境中声明的。而是在局部执行环境(函数)中声明的。

                                                                3. this:执行环境中的this应该是一个指针,指向一个对象即this的值就是一个对象【其实,this一点也不神秘,因为它的值就是一个对象,】。那么至于这个对象是谁,由函数的调用方式决定的。只有执行环境中有this,而执行环境只有全局执行环境和局部执行环境即函数执行环境】。一般this都是用在函数执行环境中,因为全局执行环境中的this就是window对象。那么,在函数执行环境中,该执行环境中的this的值是在该函数调用时,由函数的调用方式决定的。默认有如下三种值:

                                                                     1.普通函数的调用。如foo().   bar(x,y)等。在调用普通函数时,创建对应的执行环境的时候,this被赋值window。

                                                                     2.作用对象方法调用:对象方法调用,this就是拥有(定义)该方法的对象。

                                                                     3.构造函数中的调用:构造函数中,this就是当前实例对象

                                                                     4.this的默认值就是上述三种,但是我们可以在调用函数时通过apply和call来进行修改this的默认值。即强行修改this指向另一个指定的对象[call或者apply的第一个参数]即apply和call的第一个参数就是修改指定调用函数的执行环境中this的值,第二个参数就是调用该函数要传入的参数,具体用法如下:函数.call(this的值,argumens[0],arguments[1],...)   函数.apply(this的值,arguments)                                                      

                                                                     其实this也就是提供给当前执行环境的一种数据,因为this对象是执行环境作用域链的前端AO组成的一部分,在执行环境对应的函数执行时,如果使用到this值,那么这个this就是上述几种对应的对象,就从这个对应对象中获取到this.x数据,因此this和执行环境中的作用域链的功能一样,都是为了给要在当前执行环境中要执行的代码提供数据元。https://m.jb51.net/article/78861.htm

                                                                    附加:关于第2和第3条的this的解释:先看第3条:构造函数中,this就是当前实例对象。当前实例对象是什么时候创建的呢?this是什么时候被赋值这个实例的?以例子来解释说明:

function  Person( name, age){
        this. name= name
        this. age=age
        this. getMessage=function(){
           console. log( this. name)
       }
}
var p=new Person("zhangsan",30)

  当执行p=new Person("zhangsan",30)时,先执行new Person("zhangsan",30)。首先通过new 调用构造函数,new的作用:

             1.在被调用构造函数的parse阶段即创建本次调用的执行环境时,在堆内存中开辟一块区域,这块堆内存区域用于存储接下来创建的实例对象(这个实例对象的类型为new对应的构造函数的类型),然后确定this指向这块新开辟的堆内存区域(因为this的值就是在函数parse阶段创建执行环境的AO时确定的即和arguments一样)。

            2.当执行构造函数时,通过this访问这块堆内存区域即通过this来建立这个新对象,即如上例中,执行构造函数时,this.name="zhangsan" this.age=30 this.getMessage=function(){console.log(this.name)}//这里声明方法名getMessage并且该值为指向function(){console.log(this.name)}函数的地址。即在执行Person函数时,因为this指向这个新创建的堆内存,因此this. name= name就是给这块堆内存中存入一个name属性,并且赋值参数name的值。我们可以大胆推测,其实对象或者引用类型,就是为了把数据存储在堆内存中而开辟的一块新的存储空间,这块堆内存中存的数据集,在执行函数时将这些数据集(变量)存储进去,因为Javascript中不能直接操作堆内存,只能操作栈内存,因此,通过一个变量来访问这些存在堆内存中的数据。

            3.并将this的值即该引用类型的地址赋值给构造函数的父级执行环境中的p变量:a.如果构造函数没有return则,直接将this所指向的堆空间的地址值赋给p变量,从而相当于创建了p实例;b.如果构造函数有return,那么则将return的返回值赋给变量p。这是因为构造函数就是函数,只不过业内协定函数名为大写开头的。任何一个函数都可以使用new操作符来调用,当然,被使用new函数调用的函数里面的代码体最好是创建对象的那种,不然也实现不了什么。所以一般大写开头的函数里面的代码主要是实现给new字符在堆内存中开辟的空间添加属性和方法的。

            4.构造函数Person执行完之后,执行环境退栈并销毁那么this值同样也被销毁了。这时候只有p变量指向这个新创建的实例,当在某句代码中给p=null,那么该实例对象就没有被引用了,之后等着被垃圾回收。当然,也可以把p赋值给别的变量,这时候别的变量就和p同时指向了这个实例,两个中任何一个做实例的操作都是操作的同一个对象即引用类型赋值。

 这段话即解释了对象中的函数是什么时候声明的?又解释了构造函数或者创建对象中this的值。因为构造函数中的方法是通过赋值的形式声明的,因此不存在提升:只有函数的方法名保存在栈内存中即保存在执行环境的变量对象中,才有变量提升,而构造函数中的方法名保存在堆内存中,访问需要变量对象中的p.getMessage才能访问该方法名,并且是赋值声明。所以,方法不存在提升,并且在执行赋值语句时声明函数即确定父级作用域链(构造函数Person的执行环境中的VO+Person的父级作用域链),堆内存中开辟一块存储空间和普通函数一样。如下分析。

                                                                

                                                               关于当调用getMessage时,当然必须通过p.getMessage来调用,因为JavaScript中不能直接访问堆内存中的数据。当执行该方法时,和普通的函数一样执行,其实就是普通函数,只不过需要通过实例名来访问。也就是说创建变量对象,作用域链,执行环境入栈执行。方法getMessage的作用域就是Person构造函数的作用域链+如上的name和age属性(可以叫n和g),这里为了方便参数名就是n和g吧,那么在getMessage中能够访问就是数据就是前面的作用域链+getMessage的变量对象,感觉和age或者name的关系不大,因为方法主要是用于操作实例中其他数据,就现在来说,如果想要在方法中访问name属性,直接访问是访问不到,因为变量对象中存放的都是栈内存数据,而age是堆内存数据。因此,在执行getMessage即p.getMessage()来执行的时候,this的值就为p即this和p同时指向了堆内存中的该实例,因此在getMessage中可以通过this.age来访问该实例的age属性。切记:方法和普通函数的唯一区别就是:方法需要的函数名存在堆内存中,访问需要通过实例名.方法名;而普通函数的函数名存在变量对象中即栈内存中,直接可以访问该函数名。 

                                                             注意:JavaScript有内置的构造函数如Object  Date.  Array等。其中Object和Array引用类型的实例,可以用字面量来创建即:{}和[],但是本质还是new Object()。new Array()。new Function。ps: 

                                                                                                 

                                              总结 :其实执行环境就是为当前环境中要执行的代码提供数据源。arguments调用函数时传入的数据,供该执行环境执行时使用;作用域链:vo+父作用域链(除arguments),同样也是供该执行环境执行时使用;this同样也是调用时由调用方式决定的指向某个具体的对象,这个对象同样也是供该执行环境执行时使用。因此,如JavaScript高级第3版讲的:执行环境定义了变量或函数有权访问的其他数据。言外之意就是,一个执行环境中的数据,只能来源于上述3个方便,再无其他方式或者渠道。    下面的例子中会涉及到变量提升、变量对象、执行环境(活动对象、this、作用域链)等概念以及使用

<script>
var a=1; var b=2; function foo(){ console.log(a,b); var c="foo" function bar(x,y){ var d="bar" console.log(x,y) } } foo();
</script>

 这里,当解析html的时候,遇到script标签,渲染引擎挂起,v8引擎开启:

   先将这些字符串进行词法分析为tokens,然后这些tokens进行parse解析:创建执行环境、变量提升、parse立即执行代码为AST(PS:前面已经对下图的内容做了理解更正,主要是针对arguments、this已经AO的更正。)

  注:在全局执行环境中,this的值即this的指向就是window对象;全局执行环境的作用域链只有window对象,变量对象也为window对象。 其实函数声明的提升就是preparse,只不过如果有语法错误会终止所有的解析报错。   

    现在全局代码中要立即执行的代码已经全部parse为AST了。可以使用Ignition解释执行代码了                                                                                                                     

Ignition: 解释器,负责将AST转换成字节码(注:字节码是介于AST和机器码之间的一种代码)并执行即Ignition会直接解释执行ByteCode同时会标记热点代码(图)。在执行字节码的时候,如果遇到函数,那么,如果函数只调用一次,则ignition直接解释执行该函数,但是,一个函数被调用多次,那么ignition解释器就会将该函数标记为热点函数,那么该函数代码就会经过TurboFan编译器将这段代码编译为机器码并保存下来,下次再调用该函数的时候,则直接在cpu中执行相应的机器码,提高执行效率。
      接上例:现在Ignition解释器将全局代码解释为字节码之后,开始执行执行代码即全局执行环境被压入执行栈,注:此时执行栈只有全局执行环境

   接上例:现在Ignition解释器将foo函数代码解释为字节码之后,开始执行执行代码即foo函数的执行环境被压入执行栈,此时执行栈最底部的是全局执行环境,最上面的是foo函数的执行环境,执行指针指向foo对应的代码中

   console.log(a,b);
   c="foo";
   执行完foo执行环境中的代码之后,foo对应的执行环境退栈,执行环境中的所有东西都随之销毁,即指向变量对象vo的指针也销毁,但是变量对象不会被回收,因为还有bar函数的【【scopse】】中保存着指向该变量对象的指针。执行指针退回到全局执行环境中调用foo函数的地方,然后继续执行全局执行环境中的代码。如上中,当执行完foo函数之后,foo执行环境退栈并销毁,但是在开发中还有一个注意的,那就是递归的使用。递归要慎用,因为每递归一次,就会创建新的一个执行环境入栈并且在此期间没有出栈,如果递归数比较大,会导致栈溢出。例如:
function  floor(num){
            if( num==1){
                return  num
           }
      return  num*floor( num-1)
}
console. log(floor(5)) 

1.调用floor(5)
    创建一个执行环境(包括变量对象),压入执行栈执行:
    i. 因为num为5,所以if条件不成立,不走该判断
    ii.执行return 5* floor(4),在这里先执行右边的,再执行左边的return。当执行右边的5* floor(4)时因为是调用函数,因此创建新的执行环境,压入栈执行(注:左边的return等着执行栈帧回到        这个执行环境之后,再执行)
2.调用floor(4)
   创建一个执行环境(包括变量对象),压入执行栈执行:
   i. 因为num为4,所以if条件不成立,不走该判断
   ii.执行return 4* floor(3),在这里先执行右边的,再执行左边的return。当执行右边的4* floor(3)时因为有调用函数,因此创建新的执行环境,压入栈执行(注:左边的return等着执行栈帧回到       这个执行环境之后,再执行)
3.调用floor(3)
   创建一个执行环境(包括变量对象),压入执行栈执行:
   i. 因为num为3,所以if条件不成立,不走该判断
   ii.执行return 3* floor(2),在这里先执行右边的,再执行左边的return。当执行右边的3* floor(2)时因为有调用函数,因此创建新的执行环境,压入栈执行(注:左边的return等着执行栈帧回到       这个执行环境之后,再执行)
4.调用floor(2)
   创建一个执行环境(包括变量对象),压入执行栈执行:
   i. 因为num为2,所以if条件不成立,不走该判断
   ii.执行return 2* floor(1),在这里先执行右边的,再执行左边的return。当执行右边的2* floor(1)时因为有调用函数,因此创建新的执行环境,压入栈执行(注:左边的return等着执行栈帧回到         这个执行环境之后,再执行)
5.调用floor(1)
   创建一个执行环境(包括变量对象),压入执行栈执行:
   i. num为1,所以if条件成立:执行 return 1
   ii.因为这个执行环境的代码执行完毕,执行环境退栈,变量对象等全部销毁
6.执行栈帧回到4中 执行return 2*1//这里的1就是5中是return 1的结果
  当执行了return 2之后 4中的执行环境退栈,与之相关的全部销毁
7.执行栈帧回到3中执行return 3*2

   执行完成之后,3中的执行环境退栈并销毁执行,栈帧退回到2中

8.执行2中的 return4*6 执行完成之后 2中的执行环境退栈并销毁,栈帧回到1中
9.执行1中的return 5*24执行完成之后 1中的执行环境退栈并销毁,栈帧回到 全局执行环境的console.log(120),打印执行完成,本次递归执行完成

从上面发现递归消耗内存很要用,现在是5要创建5个执行环境入栈,如果1000的话,就会有1000个执行环境加入栈中,如果更大,直接导致栈的内存溢出

ps: js是即时编译:即代码边解析边执行,比如有个函数,之前已经解释成byte code执行过一次了,当我们再次调用这个方法时,还是需要从AST解释为字节码再执行。因此,标记热点函数是很有必要的,因为只需要编译一次,只要参数等一致,当调用该方法的时候,就直接在cpu中执行该函数对应的机器码(一个函数被记为热点函数,被编译为机器码,当再次调用执行这个函数时,还是先创建

,然后parse为AST,然后直接执行对应的机器码即不走Igntion那个过程),如下图所示:


ps:AST为什么不直接转成0101机器语言?
因为无法确定这个代码会运行在怎样的环境上(windows,mac,linux),不同环境的cpu架构不同,不同cpu架构能执行的机器指令不同,所以无法确定机器指令,所以才转化为字节码。字节码可以跨平台,转化为机器指令后就可以运行了。

TurboFan: 编译器,负责将字节码编译为CPU可以直接执行的机器码。TurboFan不会编译所有的了js代码,只会编译热点函数:如果一个函数被多次调用,那么就会被Ignition标记为热点函数。TurboFun将该热点代码编译成机器码,然后保存在内存中,并在cpu中执行。即TurboFan拿到Ignition标记的热点代码后,会先进行优化处理,然后将优化后字节码编译成更高效的机器码存储起来。下次再次执行相同代码时,会直接执行相应的机器码,这样就在很大程度上提升了代码的执行效率。
当一段代码不再是热点代码后,TurboFan会进行去优化的过程,将优化编译后的机器码还原成字节码,将代码的执行权利交还给Ignition进行内存空间回。还有一种情况:机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生的变化,之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码,如:

function sum(num1,num2){
  return num1 + num2
}
sum(20,20)
sum(30,30)
sum("aaa","bbb")
由于转换了数据类型,之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码
ts规定了数据的类型,所以就会减少了平台机器码由于类型改变不能正常操作而导致重新转换成字节码的情况

在这个函数中,如果调用几次都是两个数字相加,因此解释器就将其记录为两个数字相加,编译器转为对应的机器码,但是后面调用的时候,又传入两个字符串拼接,这样在调用这个函数时,和机器码不一致即参数类型不一样,所以需要反编译成字节码,然后由解释器执行,所以为什么使用ts了,因为ts可以规定数据类型,这样热点函数在被从字节码编译为机器码的时候就能编译成准确的机器码,这样,才能被cpu高效执行)

Orinoco: 垃圾回收器,负责进行内存空间回收

执行代码、分配内存以及垃圾回收。JavaScript 引擎通常被称作一种虚拟机,是专门用来解释和执行js脚本的】

具体看js源码到执行的过程:
代码在运行之前需要被解析。解析时(js源代码到ast抽象语法树的解析过程)v8引擎会帮我们初始化全局对象GlobalObject-GO,包含环境(浏览器或者node)内的全局变量(settimeou函数t,String类…),Window属性(指向globalObject)和代码里面定义的变量(尚未赋值的name,num1…,因为还没有执行所以未赋值)
var globalObject = {
String: "类",
Date: "类",
setTimeount: "函数",
window: globalObject,
name: undefined,
num1: undefined,
num2: undefined,
result: undefined
}

注意一个点,对于浏览器加载页面和加载元素资源或者ajax请求数据资源的区别:

     我们这一章主要讲的是页面的加载,记住是页面。那么对于页面的加载,如果想要一个浏览器窗口的对应的页面是某个页面,那么,必定只能通过浏览器地址解析-渲染,没有其他的方法了。(地址栏发起请求、Brower进行请求)

     那么对于需要请求的其他资源即从服务器获取除浏览器地址栏对应的页面之外的资源如html元素src所指向的资源路径、html元素href所指向的资源路径、ajax请求等等,这些都不是请求浏览器地址栏所对应的页面资源。当然,包括iframe元素,虽然它表示一个框架,具有独立的window对象,但是其请求的页面资源路径还是通过src,所以,其及时加载了另一个页面的内容,但是浏览器地址的地址仍旧是其载体页面对应的url,因为iframe是个元素。当然除了a元素的href指向一个地址的时候,这时候,同样也是将自动将a元素href的请求url填充到浏览器地址栏,然后再由浏览器请求、解析、渲染页面。即无论哪个页面的跳转,都需要填充浏览器地址栏+回车

前面所讲解的都是从url到页面渲染完成,以及操作页面后的过程。我们主要讲了页面,即浏览器的内容区部分。那么,对于浏览器自身的功能操作,我们没有提及,只提及了地址栏+回车。其实浏览器很多功能提供给用户使用,比如前进,后退,关闭浏览器窗口等等。了解浏览器操作功能,对于开发人员来说太重要了,因为浏览器提供了一些接口即BOM对象模型,然后可以提供给开发人员使用,从而使得用户在页面区域进行某些操作时,也可以主动去执行或者换起浏览器的操作功能,例如:我们让点击提交某个弹窗的提交按钮后,我们可以调用window.close(),从而关闭当前浏览器窗口。

在这里我们了解一下浏览器的一些功能,在JavaScript那一节中了解浏览器提供给需要框架的接口BOM,例如ECMAScript。那么,在ECMAScript中,BOM提供的接口或者交互,是通过window对象体现出来的,因为window对象就是全局中的Global对象即全局执行环境变量对象

posted @ 2019-06-23 21:53  ccfyyn  阅读(1436)  评论(0编辑  收藏  举报