浏览器运行原理

本文深入探讨浏览器的渲染过程,包括构建DOM树、样式计算、布局、分层、绘制等步骤,以及JavaScript执行机制,涉及变量提升、调用栈、作用域链和闭包、this等内容。同时,文章还涵盖了浏览器安全话题,如同源策略、XSS攻击、CSRF攻击和HTTPS的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

渲染过程

按照时间顺序分为:构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。

构建DOM树

浏览器无法解析HTML,将HTML解析为浏览器可以理解的DOM树

样式计算

  1. 浏览器无法理解CSS,转换为stylesheets。
  2. 转换样式表中的属性值,使其标准化(长度->px,颜色->rgb,变量->数值)。
  3. 计算DOM树中每个节点的具体样式。继承和层叠

最终输出每个节点的ComputedStyle值。

布局过程

计算DOM树中***可见元素***的几何位置。

  1. 创建布局树。添加可见节点,忽略不可见节点。
  2. 布局计算。

分层

渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树。

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

满足以下条件

  1. 拥有层叠上下文属性的元素会被提升为单独的一层。https://developer.mozilla.org/zh-CN/docs/Web/Guide/CSS/Understanding_z_index/The_stacking_context
  2. 需要剪裁的地方也会被创建为图层。

为每个图层生产绘制列表。

提交到合成线程。

分块

将图层分成图块。合成线程会将较大、较长的图层(一屏显示不完,大部分不在视口内)划分为图块(tile, 256256, 512512)。

光栅化

在光栅化线程池中,将视口附近的图块优先生成位图(栅格化执行该操作);
快速栅格化:GPU 加速,生成位图(GPU 进程)。

合成

  1. 绘制图块命令——DrawQuad,提交给浏览器进程;

  2. 浏览器进程的 viz 组件,根据DrawQuad命令,绘制在屏幕上。

重排、重绘、合成

  1. 更新了元素的几何属性(重排)

img

  1. 更新元素的绘制属性(重绘)

    img

    从图中可以看出,如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

  2. 直接合成阶段

    img

    在上图中,我们使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。

JavaScript执行机制

1 变量提升

JavaScript代码执行过程中,需要先做变量提升,这是应为JavaScript代码在执行之前需要先编译

编译阶段,变量和函数会存放到变量环境中,变量的默认值会被设置为undefined;在代码执行阶段,JavaScript引擎会从变量环境中查找自定义函数。

如果编译阶段,存在相同的函数,后定义的会覆盖掉之前定义的。

运行机制:先编译,后执行

2 调用栈

执行上下文:

  1. 当JavaScript执行全局代码的时候,会编译全局代码并创建全局执行上下文,在整个页面的生存周期内,全局执行上下文只有一份。
  2. 当调用一个函数三十,函数体内的代码会被编译,并创建函数执行上下文。一般情况下,执行完成后,创建的函数执行上下文会被销毁。
  3. 当使用eval函数时,eval的代码也会被编译,并创建执行上下文。

执行上下文包包括变量环境、词法环境。

变量环境:var function

词法环境:调用

调用栈就是用来管理函数调用关系的一种数据结构。

JavaScript中用来管理执行上下文的栈成为执行上下文栈,即调用栈。

img

函数调用时,入栈,执行完成出栈。

当一次有多个函数被调用时,通过调用栈(Call Stack)就能够追踪到哪个函数正在被执行以及个函数之间的调用关系。

3 块级作用域

作用域(scope):指程序定义变量的区域,该位置决定了变量的什么生命周期(变量和函数的可见性和生命周期)。

在ES6之前,只有全局作用域和函数作用域。

  • 全局作用域中的对象在代码的任何地方都能访问到,其生命周期伴随着页面的生命周期
  • 函数作用域就是在函数内部定义的变量或函数,并且定义的变量或函数只能在函数内部访问到。执行结束后,内部定义的的变量会被销毁

块级作用域:函数、判断语句、循环语句、{}块内的带代码定义的变量,在代码块外是访问不到的。执行完成后,销毁。

变量提升到问题:

  1. 变量容易在不被察觉到情况下被覆盖。
  2. 本应销毁的变量没被销毁。

ES6块级作用域的实现。

function foo(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a)
      console.log(b)
    }
    console.log(b) 
    console.log(c)
    console.log(d)
}   
foo()

第一步编译并创建上下文。

img

  • 函数内部通过var声明的变量,在编译阶段全部都放到变量环境中。
  • 通过let声明的变量,存放在词法环境中
  • 函数内作用域块内部,通过let声明的变量并没有放在词法环境中。

第二步,执行代码

img

在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出。(变量指let const)

再接下来,当执行到作用域块中的console.log(a)这行代码时,就需要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。

即,块级作用域通过词法环境的栈结构实现。

let myname= '极客时间'
{
  console.log(myname) 
  let myname= '极客邦'
}

结果,不能在 变量 myname没被初始化时使用。

var的创建和初始化被提升,赋值不会被提升。
let的创建被提升,初始化和赋值不会被提升。
function的创建、初始化和赋值均会被提升。

4 作用域链和闭包

其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer

作用域链是由词法作用域决定的。

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。

img

词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。

闭包

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = {
        getName:function(){
            console.log(test1)
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
console.log(bar.getName())

首先我们看看当执行到 foo 函数内部的return innerBar这行代码时调用栈的情况,你可以参考下图:

img

根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量

img

除了 setName 和 getName 函数之外,其他任何地方都是无法访问该背包的,我们就可以把这个背包称为 foo 函数的闭包。

在 JavaScript 中,根据词法作用域的规则,内部函数(在内部定义的函数)总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。

那这些闭包是如何使用的呢?当执行到 bar.setName 方法中的myName = "极客邦"这句代码时,JavaScript 引擎会沿着“当前执行上下文–>foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量

例题

var bar = {
    myName:"time.geekbang.com",
    printName: function () {
        console.log(myName)
    }    
}
function foo() {
    let myName = "极客时间"
    return bar.printName
}
let myName = "极客邦"
let _printName = foo() // 函数printName 形成闭包
_printName() // 极客邦
bar.printName() // 极客邦

5 this

img

this是和执行上下文绑定的。

this分为三种,全局执行上下文中的this、函数中的this。

全局执行上下文中的this === window

函数执行上下文中的this

  1. 通过call apply bind 指定
  2. 通过对象调用
    1. 使用对象来调用其内部的一个方法,this指向本身。
    2. 在全局环境下调用一个函数,this指向全部变量。
  3. 在构造函数中未设置。

this的坑

  1. 嵌套函数中this不会从外层函数中继承。

    var myObj = {
      name : "极客时间", 
      showThis: function(){
        console.log(this) // myobj
        function bar(){console.log(this)} // window
        bar()
      }
    }
    myObj.showThis()
    // 解决方法,借助其他值或使用箭头函数
    
  2. 普通函数中this===window

浏览器安全

1 同源策略

在没有安全保障的 Web 世界中,我们是没有隐私的,因此需要安全策略来保障我们的隐私和数据的安全。

如果两个 URL 的协议、域名和端口都相同,我们就称这两个 URL 同源。

浏览器默认两个相同的源之间是可以相互访问资源和操作 DOM 的。两个不同的源之间若想要相互访问资源或者操作 DOM,那么会有一套基础的安全策略的制约,我们把这称为同源策略。

第一个,DOM 层面。同源策略限制了来自不同源的 JavaScript 脚本对当前 DOM 对象读和写的操作。

第二个,数据层面。同源策略限制了不同源的站点读取当前站点的 Cookie、IndexDB、LocalStorage 等数据。由于同源策略,我们依然无法通过第二个页面的 opener 来访问第一个页面中的 Cookie、IndexDB 或者 LocalStorage 等内容。你可以自己试一下,这里我们就不做演示了。

第三个,网络层面。同源策略限制了通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点。你还记得在《17 | WebAPI:XMLHttpRequest 是怎么实现的?》这篇文章的末尾分析的 XMLHttpRequest 在使用过程中所遇到的坑吗?其中第一个坑就是在默认情况下不能访问跨域的资源。

我们只聊浏览器出让了同源策略的哪些安全性。

  1. 页面中可以嵌入第三方资源

    CSP 的核心思想是让服务器决定浏览器能够加载哪些资源,让服务器决定浏览器是否能够执行内联 JavaScript 代码

  2. 跨域资源共享和跨文档消息机制

  3. 页面中可以引用第三方资源,不过这也暴露了很多诸如 XSS 的安全问题,因此又在这种开放的基础之上引入了 CSP 来限制其自由程度。

  4. 使用 XMLHttpRequest 和 Fetch 都是无法直接进行跨域请求的,因此浏览器又在这种严格策略的基础之上引入了跨域资源共享策略,让其可以安全地进行跨域操作。

  5. 两个不同源的 DOM 是不能相互操纵的,因此,浏览器中又实现了跨文档消息机制,让其可以比较安全地通信。

2 跨站脚本攻击

支持页面中的第三方资源引用和 CORS 也带来了很多安全问题,其中最典型的就是 XSS 攻击。

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

可以做那些事情:

  1. 可以窃取 Cookie 信息。

  2. 可以监听用户行为。

  3. 可以通过修改 DOM 伪造假的登录窗口。

  4. 还可以在页面内生成浮窗广告。

  5. ……

常见的注入脚本方式

  1. 存储型 XSS 攻击

    首先黑客利用站点漏洞将一段恶意 JavaScript 代码提交到网站的数据库中;

    然后用户向网站请求包含了恶意 JavaScript 脚本的页面;

    当用户浏览该页面的时候,恶意脚本就会将用户的 Cookie 信息等数据上传到服务器。

  2. 反射型 XSS 攻击

    我们会发现用户将一段含有恶意代码的请求提交给 Web 服务器,Web 服务器接收到请求时,又将恶意代码反射给了浏览器端,这就是反射型 XSS 攻击。在现实生活中,黑客经常会通过 QQ 群或者邮件等渠道诱导用户去点击这些恶意链接,所以对于一些链接我们一定要慎之又慎。

    Web 服务器不会存储反射型 XSS 攻击的恶意脚本,这是和存储型 XSS 攻击不同的地方。

  3. DOM型 XSS 攻击

    在 Web 资源传输过程或者在用户使用页面的过程中修改 Web 页面的数据。

防范策略:

  1. 服务器对输入脚本进行过滤或转码。
  2. 充分利用 CSP。
    1. 限制加载其他域下的资源文件
    2. 禁止向第三方域提交数据
    3. 禁止执行内联脚本和未授权的脚本
  3. 使用 HttpOnly 属性。 JavaScript 无法读取设置了 HttpOnly 的 Cookie 数据。

3 CSRF攻击

CSRF 攻击就是黑客利用了用户的登录状态,并通过第三方的站点来做一些坏事。

和 XSS 不同的是,CSRF 攻击不需要将恶意代码注入用户的页面,仅仅是利用服务器的漏洞和用户的登录状态来实施攻击。

策略:

  1. 充分利用好 Cookie 的 SameSite 属性。
  2. 验证请求的来源站点
    1. Referer 是 HTTP 请求头中的一个字段,记录了该 HTTP 请求的来源地址。
    2. Origin 属性只包含了域名信息,并没有包含具体的 URL 路径,这是 Origin 和 Referer 的一个主要区别。
    3. CSRF Token

4 安全沙箱

页面安全和操作系统安全。

5 HTTPS

img

安全层:对发起 HTTP 请求的数据进行加密操作和对接收到 HTTP 的内容进行解密操作。

非对称加密密钥,对称加密内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值