全栈工程师大牛好文

浏览器工作原理

2021-05-15  本文已影响0人  攻城狮_前端程序媛

参考:《浏览器工作原理与实践》— 李兵

一、浏览器的多进程架构

(一)进程 、线程、协程

参考文档: https://blog.csdn.net/ThinPikachu/article/details/121325198

进程

【进程】:一个进程就是一个程序的运行实例,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放进程运行所需的所有状态信息,包含代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程的上下文。

线程

【线程】: 线程是CPU调度和执行的最小单位,线程依附于进程,是程序的实际执行者。

协程

【协程】:运行在线程之上,协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程。

进程、线程、协程上切换比较.png

(二)浏览器的多进程架构

早期的浏览器是单进程,浏览器的所有功能模块都是运行在同一个进程里,带来一系列问题,如:

为了解决这些问题,现在的浏览器操作系统实行了多进程架构,但是也带来了一些问题,如:

  1. 进程拥有太多的资源,系统线程会占用非常多的内存空间
  2. 进程之间切换、线程之间切换都是通过CPU调度的,占用了大量的系统时间

浏览器的架构是多进程的,Chrome 打开一个页面需要启动多少进程呢?

Chrome 的任务管理器窗口.png

Chrome 浏览器的架构是多进程的,它主要包含以下几个进程:

  1. 浏览器主进程(Browser process): 是浏览器的顶层进程,主要负责页面显示、用户交互、管理子进程、提供存储等功能;

  2. 渲染进程(Renderer Process):核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页;

  3. GPU进程(GPU Process) :负责图形处理

  4. 网络进程(Network Process):负责网络资源的下载

  5. 插件进程Plugin Process:负责网页插件

(三)浏览器的渲染进程

参考文档:https://zhuanlan.zhihu.com/p/398901440

浏览器的渲染进程也是浏览器内核的,在前端开发过程中,我们主要和渲染进程打交道,它主要包含以下几个线程:

1. GUI渲染线程

用于解析html、css,构建DOM树LayoutTree布局树(渲染树),布局和绘制生成最终渲染页面,传递给浏览器主进程进行展示。

2. Javascript引擎线程

也就是我们常说的js内核,也称JS引擎(例如V8引擎),用于解析、运行javascript代码;
Javascript引擎线程是单线程的。

·注意:GUI渲染线程和Javascript引擎线程是互斥的,不能同时执行,因此JS的执行会阻塞页面解析和渲染。

3. 事件触发线程

用于监听和触发事件。

注意:事件触发线程归属于渲染进程,不受JS引擎线程控制

如何理解事件触发线程 和 Javascript引擎线程的关系 ?

4. 定时触发器线程

就是经常使用的setInterval与setTimeout所使用的线程。

5. 异步http请求线程

负责发送发送http请求。

6. 合成线程

负责浏览器页面中各个图层的绘制。

7. IO线程:处理和其他进程进行通信
浏览器的渲染进程.jpg

二、浏览器的渲染流程

按照渲染的时间顺序,可以把渲染流程分为以下几个阶段:

第一步,解析HTML,浏览器收到HTML页面后,GUI渲染线程开始解析HTML页面
  1. HTML解析器开始解析HTML,生成DOM Tree并输出,保存在浏览器内存中;
    -- 同时开启一个预解析线程,用来分析 HTML 文件中包含的Javascript、 CSS 、Img等需要下载的资源,通知网络进程提前加载这些资源


    DOM 树的构建过程.png
  1. 解析遇到CSS(style、行内、link),CSS解析器开始对CSS进行解析,生成CSSOM,计算出 DOM 节点中每个元素的具体样式;包含以下3个过程:
  1. 遇到 <script> ,GUI渲染线程会停止解析剩余的 HTML ,等待Javascript 资源加载,Javascript引擎线程执行完脚本之后,再继续解析HTML页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script>
        alert('哈哈哈,我阻挡了HTML页面的继续解析。。。。')
    </script>
    <style>
        div {
            width: 100px;
            height: 100px;
            background: rosybrown;
        }
    </style>
</head>
<body>
    <div></div>
</body>
</html>
上面代码的执行结果.png
css解析和HTML解析并行,不会阻塞HTML解析,但是会阻塞页面渲染
javascript 会阻塞HTML解析和页面渲染
第二步,生成布局树, 根据DOM 树和计算后的CSS样式和生成布局树LayoutTree(也叫渲染树);

有 DOM了 树和 计算后的CSS样式,还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息,那么接下来就需要计算出 DOM 树中可见元素的几何位置,这个计算过程叫做【布局】。

注意:所有不可见的元素会被忽略,如head标签 , display:none的元素,script标签等;

布局树.png
第三步,布局计算,计算出布局树的每个节点在浏览器屏幕上的具体坐标位置
第四步,分层,生成图层树LayerTree

如果页面中有复杂的效果,如使用了3D 变换、页面滚动、 z-index(z 轴排序)等情况下,渲染引擎还需要为这些特定的元素节点生成专用图层,并生成一棵对应的图层树(LayerTree);

并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。

渲染引擎会为哪特定的节点创建新的图层呢?

  1. 拥有层叠上下文属性的元素会被提升为单独的一层;
    -- 定位元素
    -- 使用了z-index的元素
    -- 定义透明属性的元素
    -- 使用 CSS 滤镜的元素
拥有层叠上下文属性的元素.png
  1. 需要剪裁(clip)的地方也会被创建为图层,及内容超出了容器元素时需要创建一个新的图层来放置。

渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面。

图层树.png
第五步, 绘制图层

染引擎会对图层树中的每个图层进行绘制,生成待【绘制列表】,交给合成线程

第六步,栅格化,生成位图

【视口】:屏幕的可是窗口成为视口(viewport);
页面可能很大,视口相对较小,为了节省开销,合成线程会将图层划分为图块(tile),然后合成线程会按照视口附近的图块来优先生成位图

-- 实际生成位图的操作是由栅格化来执行的;
-- 所谓【栅格化】,是指使用 GPU 进程将图块转换为位图的过程;

GPU进程根据不同图块生成位图,还给合成线程

第七步,合成

所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。

第八步,显示界面

浏览器进程收到合成线程发过来的 DrawQuad 命令后,其内部组件viz 的组件会根据DrawQuad 命令生成页面并保存在内存中,最后再将页面显示在屏幕上。

浏览器渲染流程图.png

渲染流程中的特殊情况:

1. 重排(回流):

指修改了元素几何属性,如位置、尺寸、内容、结构等变化,引发元素几何位置变化,浏览器需要重新计算样式、构建布局树,开始之后的一系列子阶段,这个过程就叫重排。

重排需要更新完整的渲染流水线,所以开销也是最大的。

触发重排的情况:(Javascript操作DOM,引发不同渲染流水线重新工作)

2. 重绘:

指修改了元素的外观样式,不会引起几何位置变化,直接入绘制阶段,生成绘制列表,然后执行之后的一系列子阶段,这个过程就叫重绘。如背景颜色、边框颜色,文字颜色等

重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。重排必然带来重绘,但是重绘未必带来重排
3. 直接合成:

指更改一个既不要布局也不要绘制的属性,直接分割图块阶段,然后交给浏览器主进程并不线上显示,这个过程叫做直接合成。
如 transform:translate(100px, 100px)

相对于重绘和重排,直接合成能大大提升效率

减少重排(回流)、重绘, 方法:

.box {
will-change: transform, opacity;
}

三、JavaScript执行机制

(一)JavaScript代码执行流程
第一步,代码编译:JavaScript 引擎对代码进行编译,并保存在内存中

编译结果为两部分:执行上下文、可执行代码

showName();//函数showName被执行
console.log(myname);//undefined
var myname = '小白'
function showName() {
    console.log('我是小白');
}

编译时的执行上下文如下:(变量环境部分)

{
  showName: xxx, //showName 函数在堆内存的引用地址
  myname: undefined
}

可执行上下文如下:

  showName();
  console.log(myname);//undefined
  myname = '小白'
  let userInfo = {
      userName: "小白",
      age: 18,
      sayHello: function () {
      setTimeout(function () {
         console.log(`${this.userName},你好`)   //undefined
       }, 100)
   }
}

userInfo.sayHello()

修改一个函数this指向的方法:

第二步,执行可执行代码

问题:

  1. var变量提升
    编译时变量声明提升,并初始化值为undefind,

  2. 函数声明提升

解决: 引入let、const、块级作用域

(二)函数执行(调用)过程
  1. 执行上下文栈:
    用来管理执行上下文,后进先出

一段代码解析完成,即执行上下文创建完成,就立即执行可执行代码

var a = 2
function add(b,c){
  return b+c
}
function addAll(b,c){
  var d = 10
  result = add(b,c)
  return  a+result+d
}
addAll(3,6)

第一步,解析全局代码,创建全局执行上下文,压入调用栈,并全局的执行可执行代码


执行上下文栈.png

第二步,执行到addAll调用时,生成addAll函数的执行上下文,压入上下文,并执执行addAll函数内部的可执行代码


执行上下文栈.png

第三步,执行到add 函数调用,生成add 函数的执行上下文,压入调用栈


执行上下文栈.png

执行add 函数内部的可执行代码,return 结果,然后add函数执行上下文销毁,弹出调用栈

第四部,执行addAll后续可执行代码,return 结果,addAll函数上下文销毁,弹出调用栈,最后只剩下全局执行上下文,伴随页面整个生命周期

问题: 栈溢出(递归函数)

(三)作用域、作用域链、闭包
1. 作用域:是指变量和函数可以被访问的范围

var 、 let、const的区别:

  1. var:
    -- 在javascript解析时, 声明和初始化提升,声明之前访问不报错,值为undefined;
    -- 存放在执行上下文中的变量环境中
    -- 可以多次声明同一个变量,后一个值会覆盖之前的值;
    -- 不支持块级作用域

  2. let :
    -- 用来声明一个变量,在解析时,声明会提升,但是初始化不会提升,声明之前访问报错;
    -- 存放在执行上下中的词法环境中
    -- 同一作用域内不能多次声明;
    -- 支持块级作用域

  3. const :
    -- 用来声明一个常量,不能再次修改
    --声明会提升,但是初始化不会提升,声明之前访问报错;
    -- 存放在执行上下中的词法环境中
    -- 同一作用域内不能多次声明;
    -- 支持块级作用域

function foo(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a); //1
      console.log(b); //3
    }
    console.log(b) ;//2
    console.log(c); //4
    console.log(d); //报错:d is not defined
}   
foo()
2. 作用域链:变量查找沿着各作用域一层层向外部引用指向的执行上下文查找,形成一个链条,即作用域链条

函数的作用域由词法作用域决定
词法作用域:是指作用域是函数声明的位置来决定的,和函数怎么调用无关

3. 闭包:

当函数执行完毕时,函数体内的定义的变量会随着函数执行上下文立即销毁,但是当外部函数包含内部函数,且内部函数使用了外部函数中定义的变量,这些变量就不会销毁,仍然保存在内存,这些变量和内部函数就形成了闭包

闭包的形成条件:

  1. 外部函数里有内部函数
  2. 内部函数中使用了外部函数中定义的变量
 function foo() {
    var myName = "小白";
    var age = 18;

    function sayHello(){
       console.log (`你好,我的名字是:${myName},今年${age}`)
    }
    return sayHello;
}
let hello = foo();
hello()
// myName和age就是foo函数的闭包

问题:内存泄露( 该回收的内存未被及时回收 )

(四)Javascrip的垃圾回收机制
1. Javascript的内存机制
2. Javascript的垃圾回收机制

数据被使用之后,不再需要了,就称为垃圾数据,垃圾数据要及时销毁,释放内存空间,否则会内存泄漏。

全局变量会常驻到老生代内存中是直到整个进程结束后才会释放,建议少使用,释放使用delete即可。

非全局变量在该作用域摧毁时会自动释放,主动释放建议赋值undefined。

(1)栈内存回收

当Javascript代码执行时,记录当前执行状态的指针(称为 ESP),指向当前执行上下文的指针,当前函数代码之前完毕,指针下移指向下一个要执行的函数执行上下文,当前执行上下文弹出调用栈进行销毁,这个过程就是该函数栈内存回收的过程

 function foo(){
    var a = 1
    var b = {name:"极客邦"}
    function showName(){
      var c = 2
      var d = {name:"极客时间"}
    }
    showName()
}
foo()
调用栈.png

(2)堆内存回收
垃圾回收器:

第一步,标记堆内存中活动对象和非活动对象

第二步,回收非活动数据所占据的内存
在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象

第三步,做内存整理

(五)浏览器的事件循环机制

每个渲染进程都有一个主线程,处理以下事件:

要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要一个系统来统筹调度这些任务,这个统筹调度系统就是消息队列和事件循环系统,浏览器页面是由消息队列和事件循环系统来驱动的。

1. 任务队列:是一种数据结构,用来放要执行的任务,先进先出

同步任务:直接进入主线程执行的任务,只有前一个任务执行完毕,才能执行后一个任务
异步任务:以回调函数实现,先在其他的任务队列中排队,等待同步任务执行完成,该任务才会进入主线程执行,分为宏任务、微任务

宏任务队列:宏任务执行队列,回调函数里要执行的任务

微任务队列:JavaScript 执行一段脚本,V8 引擎会首先创建一个全局执行上下文,同时也会创建一个专为V8 引擎内部使用的微任务队列

(1)宏任务:宿主环境即浏览器分配的任务

在 Chrome 中除了正常使用的消息队列之外,还有一个延迟队列,这个队列中维护了需要延迟执行的任务列表,包括了定时器和 Chromium 内部一些需要延迟执行的任务。

延时队列中的任务包含JS通过定时器设置的回调函数、还有一些浏览器内部的延时回调函数,它们属于宏任务;

正常的消息队列中的任务也属于宏任务。

宏任务 主要有以下几种:
1.渲染事件(如解析 DOM、计算布局、绘制)

  1. javascript脚本执行
  2. setInterval、setTimeout
    -- setTimeout回调函数的真正执行时间>=设定时间,原因是受消息队列中其他任务执行时间的影响
  3. DOM事件
  4. XMLHttpRequest


    XMLHttpRequest工作流程.png

XMLHttpRequest 发起请求,是由浏览器的其他进程或者线程去执行,然后再将执行结果利用 IPC 的方式通知渲染进程,之后渲染进程再将对应的消息添加到消息队列中。


 function GetWebData(URL){
    /**
     * 1:新建XMLHttpRequest请求对象
     */
    let xhr = new XMLHttpRequest()

    /**
     * 2:为 xhr 对象注册相关事件回调处理函数,监听网络请求过程中的各种情况 
     */
    xhr.onreadystatechange = function () {
        switch(xhr.readyState){
          case 0: //请求未初始化
            console.log("请求未初始化")
            break;
          case 1://已调用xhr.send(),正在发送请求
            console.log("OPENED")
            break;
          case 2://xhr.send()已经执行完成,已经接收到全部的响应资源
            console.log("HEADERS_RECEIVED")
            break;
          case 3://正在解析响应资源
            console.log("LOADING")
            break;
          case 4://资源解析完成,客户端可以直接使用了
            if(this.status == 200||this.status == 304){
                console.log(this.responseText);
                }
            console.log("DONE")
            break;
        }
    }
  
   // 请求超时了,对应的处理回调函数
    xhr.ontimeout = function(e) { console.log('ontimeout') }

  // 请求出错了了,对应的处理回调函数
    xhr.onerror = function(e) { console.log('onerror') }

    /**
     * 3:创建请求
     */
    xhr.open('Get', URL, true);//创建一个Get请求,采用异步


    /**
     * 4:配置请求参数
     */
    xhr.timeout = 3000 //设置xhr请求的超时时间
    xhr.responseType = "text" //设置响应返回的数据格式
    xhr.setRequestHeader("X_TEST","time.geekbang")

    /**
     * 5:发送请求
     */
    xhr.send();
}
(2)微任务:JavaScript 引擎发起的任务,执行时机为当前宏任务结束之前

当 JavaScript 执行一段脚本的时候,V8 引擎在创建全局执行上下文的同时,也会在内部创建一个微任务队列。

这个微任务队列就是用来存放微任务的,因为在当前宏任务执行的过程中,有时候会产生多个微任务,这时候就需要使用这个微任务队列来保存这些微任务了。

不过这个微任务队列是给 V8 引擎内部使用的,无法通过 JavaScript 直接访问。

每个宏任务都关联了一个微任务队列。

Javascript脚本执行时本身就也是一个宏任务,宏任务中又包含同步任务、微任务、宏任务。

console.log(1);

setTimeout(()=>{
  console.log(3);
  Promise.resolve(4).then((data) => {
      console.log(data)
  })
  setTimeout(() =>{
     console.log(5)
  },0)
}, 0)

Promise.resolve(2).then((data) => {
    console.log(data)
})
//执行结果:1, 2, 3,5

微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列

微任务早于宏任务执行,如果在执行微任务的过程中,产生了新的微任务,同样会将该微任务添加到微任务队列中

微任务的执行时长会影响到当前宏任务的时长

微任务主要有:

  1. MotutaionObserver,监听 DOM 变化的一套方法,是Mutation Events的替代品;

MutationObserver 核心是采用了“异步 + 微任务”的策略。

       const div1 = document.getElementById('div1');

        setTimeout(function() {
            console.log('setTimeout', div1.childNodes)
        })
        
        // 创建MutationObserver实例
        const observer = new MutationObserver(function(mutationsList) {
            console.log('mutations', mutationsList);

            for(let mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    console.log('A child node has been added.');
                }
            }
        })

        observer.observe(div1, {
            childList: true, // 监听节点变化
    
        })

        for(var i=0; i<3; i++) {
            const p = document.createElement('p');
            const textNode1 = '我是新建的';
            p.innerHTML = textNode1;
            div1.append(p);
        }
        

        Promise.resolve().then(function() {
            console.log('Promise', div1.childNodes);
        })

执行结果.png
MutationObserver 和 Mutation Events 的区别?
  1. Promise主要是为了解决回调地狱问题,消灭嵌套调用和多次错误处理
    (1) Promise的三种状态
    pending(待执行状态)、fulfilled(执行成功状态)、rejected(执行失败状态)

(2)执行过状态不可逆,不会再变
要么pending ->fulfilled
要么pending -> rejected

(3)Promise实现原理:
- 采用.then的方式延时绑定回调函数

- 回调函数返回值穿透,then回调函数中的返回值,可以穿透到最外层

- 错误“冒泡”,会一直向后传递,通过链式调用then、catch,不论在那一层出错,都会“冒泡”至catch

-- 就不需要在每个 Promise 对象中单独捕获异常了

       function executor(resolve, reject) {
            let rand = Math.random();
            console.log(rand);
            if (rand > 0.5) 
                resolve()
            else
                reject()
        }
        var p1 = new Promise(executor); // 同步任务

        var p2 = p1.then((value) => {
            console.log("succeed p1")
            const p2 = new Promise(executor); 
            return p2; // p1中的返回值穿透到最外层
        })

        p2.then((value) => {
            console.log("succeed p2");
        })
        
      //错误冒泡,最后一个Promise做错误处理即可
        p2.catch((error) => {
            console.log("error")
        })
Promise 中为什么要引入微任务?

由于promise采用.then延时绑定回调机制,而new Promise时又需要直接执行promise中的方法,即发生了先执行方法后添加回调的过程,此时需等待promise中的方法执行完成后,才能继续执行.then绑定的回调函数,由于宏任务较多容易堵塞,则采用了微任务

(4)Promise.resolve(value):返回一个以给定值解析后的Promise对象

Promise.resolve(value)方法的参数分成四种情况:

-- 参数是一个 Promise对象的实例 ,直接返回这个实例

-- 参数是一个thenable对象(即带有then方法),Promise.resolve()返回的是一个执行then方法之后的Promise对象,状态根据then方法执行之后判断

let thenable = {    
   then: function(resolve, reject) {
        resolve(200)
    }
}
let p1 = Promise.resolve(thenable); //200,因为p1已经是fulfilled状态,因此直接then,可以获取到返回值
p1.then((data) => {
    console.log(data)
})

-- 参数是一个普通值或对象,则直接返回一个状态为fulfilled的 Promise 对象,值为参数本身

-- 参数为空,直接返回一个fulfilled状态的 Promise 对象,值为undefined

(5)链式调用时,
then回调函数执行成功,返回的是一个fulfilled状态的promise,会进入后面的then
then执行失败,返回的是一个rejected的promise,会进入后面的catch
catch回调函数执行成功,返回的也是一个fulfilled状态的promise,进入后面的then
catch执行失败,返回的是一个rejected的promise,进入后面的catch

Promise 的缺点:采用链式回调方式,充满大量的then函数,语义化方面存在缺陷

  1. async/await, 使用await,通过同步代码实现了异步访问资源回调的能力
async function foo() {
    console.log(1);
    let a = await 100; 
    console.log(a); // 100
    console.log(2);
}
console.log(0);
foo();
console.log(3);
//执行顺序:0,1,3,100,2

生成器函数:是一个带星号函数,是可以暂停执行和恢复执行的
-- 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
-- 外部函数可以通过 next 方法恢复函数的执行。

执行器:执行生成器函数的函数,则成为执行器

协程: 是一种比线程更加轻量级的存在,

function* genDemo() {
    console.log("开始执行第一段");
    yield 'generator 1';// 遇到yield 关键字,JavaScript 引擎会暂停该函数的执行,并将关键字后面的内容返回给外部,外部函数可以通过next()恢复继续执行

    console.log("开始执行第二段");
    yield 'generator 2;

 }

console.log('main 0')
let gen = genDemo(); //创建了一个gen协程,但是并没有执行
console.log(gen.next().value) ; //generator 1
console.log('main 1')
console.log(gen.next().value); //generator 2
console.log('main 2')
async/await的原理:

//foo函数
function* foo() {
    let response1 = yield fetch('/some/url1') //fetch基于Promise实现
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('/some/url1')
    console.log('response2')
    console.log(response2)
}

//执行foo函数的代码
let gen = foo()
function getGenPromise(gen) {
    return gen.next().value
}
getGenPromise(gen).then((response) => {
    console.log('response1')
    console.log(response)
    return getGenPromise(gen)
}).then((response) => {
    console.log('response2')
    console.log(response)
})

直接使用async时, 返回结果是一个fulfilled状态的Promise对象;
使用await,await后为异步, 会默认创建一个 Promise 对象

await之后的代码相当于then函数里的代码
如果需要做异常捕获,需要try...catch

(async function () {
    const p1 = Promise.resolve(100)
    const res = await p1
    console.log(res) // 100
})()

(async function () {
    const p2 = Promise.reject('some err')
    const res = await p2
    console.log(res) // 不会执行
})()

(async function () {
     const p2 = Promise.reject('some err')
    try {
        const res = await p2
        console.log(res)
    } catch (ex) {
        console.error(ex) // catch会执行
    }
})()

2. 事件循环机制

参考文档https://www.jianshu.com/p/12b9f73c5a4f/,这个应该是讲的比较详细的

栈的概念理解(3种):

事件循环执行过程

    setTimeout(function () {
        console.log('timeout1');
    },0)

    new Promise(function (resolve) {
        console.log('promise1');
        resolve(100)
    }).then(() => {
        console.log('then1');
    })

    new Promise(function (resolve) {
        console.log('promise2');
        resolve(200)
    }).then(() => {
        console.log('then2');
    })

    setTimeout(function () {
        console.log('timeout2');
    },0)

    console.log('global1');
  1. 第一步,开始执行Javascript脚本,进入宏任务队列,因为只有一个script(整体代码)任务,直接开始执行
  2. 第二步,遇到setTimeout,setTimeout为一个宏任务,异步处理,等待时机成熟(100ms之后),timeout1加入宏任务队列
  3. 第三步,遇到Promise,Promise本身是同步任务,promise1, resolve(100),立即放到执行栈( 按顺序执行 ),
    then才是回调异步函数,异步处理,处理完成后,then1加入微任务队列(异步任务执行加入微任务队中的时间是任务异步处理完成的时间顺序,不是在代码中的上下位置顺序)
  4. 第四部,遇到第二个Promise,promise2, resolve(200),立即加入执行栈,
    then2加入微任务队列
  5. 第五步,遇到第二个setTimeout,timeout2加入宏任务队列
  6. 至此,调用栈为空,Javascript宏任务中的同步任务函数全部执行完毕
同步任务执行完成时的执行栈.png
  1. 第六步,然后去微任务队列查看可执行的微任务,then1加入执行栈执行,执行完成,执行栈为空,再去微任务队列查看可执行的微任务,加入执行栈执行,反复循环,直到微任务队列为空
  2. 第七部,查看宏任务队列可执行宏任务,timeout2执行完成时间早于timeout,因此先进入执行栈执行,反复循环,直到宏任务任务队列为空
  3. 任务全部执行完毕,调用栈为空

四、浏览器中的页面

页面的生命周期:

(一)页面优化:

从页面的生命周期方向思考:

1. 加载阶段:如何让页面渲染快?

关键资源(核心资源):阻塞页面首次渲染的资源称为页面的关键资源,HTML、CSS、Javascript

具体优化方法

(1)压缩HTML文件,移除 不必要注释
(2)合并并压缩CSS 、JavaScript等文件 ,script 标签加上 async 或 defer属性
(3)避免使用table布局
(4)缓存(第二次请求命中缓存则直接读取缓存)

2. 更新阶段(交互阶段):页面再次渲染速度如何更快?

目标是减少页面渲染过程的重排、重绘

具体优化方法
(1)避免强制同步布局(即避免使用JavaScript 强制将计算样式和布局操作)
(1)减少DOM操作,将多次操作DOM合并为一次,如插入元素节点
(2)减少逐项更改样式,最好一次性更改style,或者将样式定义为class并一次性更新
(3)前端框架Vue、React(虚拟DOM和Diff算法等)
(3)避免多次读取offset等属性,使用变量做缓存
(4)防抖、节流
(5)做动画效果时,使用will-change和transform 做优化

(二)虚拟DOM及算法
  1. 多次
1. 页面加载阶段:
2. 页面加载阶段:
引入虚拟DOM树执行流程.png

五、浏览器安全

同源策略:协议、域名、端口三者都相同则称为同源
1. XSS 攻击:跨站脚本攻击(Cross Site Scripting)

XSS 攻击是指黑客往 HTML 文件中或者 DOM 中注入恶意 JavaScript 脚本,在用户浏览页面用户实施攻击的一种手段

(1)风险:
(2)解决方法:
  1. 对输入脚本进行过滤或转码
如:<script> -->&lt;script&gt;
  1. 响应头Set-Cookie加使用限制
    -- httpOnly,通知浏览器此 Cookie 只能通过浏览器 HTTP 协议传输,浏览器的 JS 引擎就会禁用 document.cookie ;
    -- SameSite=Strict,限制此Cookie不能随着跳转链接跨站发送

  2. 充分利用 CSP
    -- 限制加载其他域下的资源文件,
    -- 禁止向第三方域提交数据;
    -- 禁止执行未授权的脚本;

2. CSRF攻击,跨站请求伪造(Cross Site Request Forgery)

目的是利用服务器的漏洞和用户的登录状态来实施攻击

解决方法:
-- 尽量使用post,避免低级的 CSRF 攻击
-- 增加验证,如使用验证码、短信验证码,验证是否是用户主动发起的
-- 服务器验证Referer,即请求的来源站点与当前发起的请求的站点是否一致
-- 使用Token验证
服务器第一次返回时生成一个Token,客户端存储起来
再次请求客户端带着对应的Token,进行验证

上一篇下一篇

猜你喜欢

热点阅读