浏览器的组成

User Interface(用户交互)

浏览器的交互界面,比如前进、返回按钮,导航栏等。

Browser Engine(浏览器引擎)

是用户交互和渲染引擎的桥梁,接收用户交互通过渲染引擎将信息展示在用户界面。

Rendering Engine(渲染引擎)

浏览器的渲染引擎,负责解析html、css、js等代码,将代码渲染到用户界面。

  1. Chrome、Opera: Blink引擎
  2. Firefox:Gecko引擎
  3. Safari: WebKit引擎
  4. Edge:Trident

Networking(网络请求)

浏览器通过http、https、webbsocket、ftp等协议发送请求,接收响应。

JS Interpreter(JS解释器)

  1. Chrome: V8引擎
  2. Mozilla:SpiderMonkey引擎
  3. Safari:JavaScriptCore / Nitro WebKit
  4. Edge:Chakra
  5. Opera: Carakan

UI Backend(用户界面后台)

用于绘制基本的窗口小部件,比如下拉列表、文本框、按钮等,向上提供公开的接口,向下调用操作系统的用户界面。

Data Persistent(数据可持续化)

  1. Cookie
    Cookie用于存储少量数据,一般 < 4K。
  2. Web Storage
    LocalStorage、SessionStorage存储的数据一般是5M到10M。
  3. Web SQL
    Web SQL 已经过时,不再成为W3C的标准。
  4. IndexedDB
    W3C标准中推荐的数据持久化方案,浏览器允许起容量为>250M大小。通过建值对存储、支持事物、异步执行、同源策略且支持二进制存储。
  5. File System
    主要是支持浏览器访问本地文件资料。注意File API和File System Access API是两套规范,File API只能读不能写,File System Access API支持读写,但是目前浏览器支持的不是很好。

浏览器的事件循环

浏览器的进程模型

  1. 进程和线程
    进程是系统进行资源调度的基本单位,一个进程可包含多个线程,线程是系统进行运算调度的最小单位。

  2. 浏览器的三大进程

  • 浏览器进程
    浏览器事件、管理tab等浏览器操作

  • 网络进程
    实现浏览中的网络请求

  • 渲染进程
    每个tab新开一个进程,处理html、css、js等代码执行

  1. 渲染主线程
  • 渲染主线程如何实现异步

    1
    2
    3
    4
    5
    js是一⻔单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。
    渲染主线程承担着诸多的工作,渲染⻚面、执行js都在其中运行。如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。
    这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致⻚面无法及时更新,给用户造成卡死现象。
    所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。
    当其他线程完成时将事先传递的回调函数包装成任务,加入到消息队列的末尾排队等待主线程调度执行。在这种异步模式下浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。
  • JS的事件循环

    1
    2
    3
    事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
    在Chrome的源码中,它开启一个不会结束的for循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的 浏览器环境,取而代之的是一种更加灵活多变的处理方式。
    根据W3C官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级, 在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。
  • 消息队列优先级
    1)延时队列:用于存放计时器到达后的回调任务,优先级「中」
    2)交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
    3)微队列:用户存放需要最快执行的任务,优先级「最高」

    1
    Promise.resolve().then(function) 可将function函数放入微队列

浏览器的渲染原理

  1. 浏览器通过网络进程获取html交给渲染进程渲染。

  2. 渲染进程生成渲染任务交给消息队列,渲染主线程根据任务队列中的任务进行渲染。

  • 解析html (Parse html)

    • 生成dom树,js中通过document对象操作dom树。
    • 生成cssom树,js中通过documn.styleSheets获取cssom树对其操作。
    • 解析遇到css代码,浏览器会启动一个预解析器去先下载和解析css。解析遇到js代码,渲染主线程会暂停等网络线程下载完js后再解析js。
  • 样式计算 (Recalculate style)
    主线程依次为树中的每个节点计算出它最终的样式,在这一过程中很多预设值会变成绝对值,比如red会变成rgb(255,0,0);相对单位会变成绝对单位,比如em会变成px。这一步完成后,会得到一棵带有样式的DOM树。

  • 布局 (Layout)
    根据css样式计算dom节点的几何信息,例如节点尺寸、相对包含块的位置。完成后会得到布局树,因为受css样式影响,dom树和布局树并不一一对应。

  • 分层 (Layer)
    为了提高重绘效率,浏览器会将页面划分若干层,每次重绘只会重新渲染对应分层的内容。可以通过will-change: [transform]属性来影响分层。

  • 生成绘制指令 (Paint)
    主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。之后主线程将绘制指令集交给合成线程,之后的工作将由合成线程完成。

  • 分块 (Tiling)
    为了提高效率,合成线程将每个图层进行更小区域的分块,合成线程通过线程池完成此工作。

  • 光栅化 (Raster)
    分块完成后合成线程将分块信息交给GPU进程,GPU进程会先将靠近视口的分块光栅化(即计算出每块的位图[像素信息])。此步骤会涉及GPU硬件加速。

  • 画 (Draw)
    合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息。指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。变形发生在合成线程,与渲染主线程无关,这就是transform效率高的本质原因。最终合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。

资源提示关键词

为了提高页面资源加载的效率,浏览器提供了资源提示关键词。资源提示关键词可以通过 caniuse 网站查询浏览器的兼容性。

针对script资源的加载

  1. async
    script资源的网络请求和渲染主线程异步执行,资源下载完之后渲染主线程等待script执行完再继续。

    1
    <script async src="xxx.js"></script>
  2. defer
    script资源的网络请求和渲染主线程异步执行,资源下载完之后需等待渲染主线程执行完再执行script。

    1
    <script defer src="xxx.js"></script>

针对script、css和img等其他资源

  1. preload
    通过preload向浏览器申明一个需要提前加载的资源,当资源真正被使用时,无需再发用网络请求立即执行。

    1
    2
    3
    4
    <link rel="preload" href="xxx.js" as="script">
    <link rel="preload" href="xxx.css" as="style">
    <link rel="preload" href="xxx.png" as="image">
    <link rel="preload" href="xxx" as="font">
  2. prefetch / dns-prefetch
    利用浏览器空闲时间加载非首页的后续页面的资源。

    1
    2
    3
    <link rel="prefetch" href="xxx.js" as="script">
    // 通过dns-prefetch提前解析域名,资源请求速度
    <link rel="dns-prefetch" href="xxx.com">
  3. prerender
    prerender 和 prefetch 类似,同样会收集用户接下来可能会用到的资源,不同的是prerender会在后台提前渲染出页面。

    1
    <link rel="prerender" href="xxx.html">
  4. preconnect
    通过preconnect提前连接资源,提高资源加载速度。

    1
    <link rel="preconnect" href="xxx.js" as="script">

浏览器缓存

Service Worker

Memery Cache

Disk Cache

Push Cache

跨标签页通信

  1. BroadCast(需要同源)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // tab1
    const bc = new BroadcastChannel('testChannel')
    bc.postMessage({value: 'Hello World'})

    // tab2
    const bc = new BroadcastChannel('testChannel')
    bc.onmessage = function(msg){
    console.log(msg.data) // {value: 'Hello World'}
    }
  2. LocalStorage(需要同源)
    利用LocalStorage存储信息时所触发的onstorage事件实现跨tab通信

    1
    2
    3
    4
    5
    6
    7
    // tab1
    localStorage.age = 20

    // tab2
    localStorage.onstorage = function(e) {
    console.log(e) // {key: 'age', oldValue: undefinded, newValue: 20, url: 'xxxx', storageArea: xxx}
    }
  3. ServiceWorker(需要同源)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // tab1
    const sw = navigator.serviceWorker.register('sw.js').then(()=>{
    console.log('register success')
    })
    btn.onclick = function() {
    navigator.serviceWorker.controller.postMessage({value: 'Hello World'})
    }

    // sw.js
    self.addEventListener('message', async e => {
    const clients = await self.clients.matchAll()
    clients.forEach(client => {
    client.postMessage(e.data)
    })
    })

    // tab2
    const sw = navigator.serviceWorker.register('sw.js').then(()=>{
    console.log('register success')
    })
    navigator.serviceWorker.onmessage = function(e) {
    console.log(e.data)
    }
  4. SharedWorker(需要同源)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // tab1
    <input id="input"/>
    <button id="btn">test1</button>
    <script>
    const input = document.getElementById('input');
    const btn = document.getElementById('btn');
    const worker = new SharedWorker('worker.js');
    btn.onclick = function() {
    worker.port.postMessage(input.value);
    }
    </script>

    // worker.js
    var data = '';
    onconnect = function(e) {
    const port = e.ports[0];
    port.onmessage = function(_e) {
    if(_e.data === 'get') {port.postMessage(data);} else {data = _e.data;}
    };
    }

    // tab2
    <button id="btn">test2</button>
    <span id="content">0</span>
    <script>
    const btn = document.getElementById('btn');
    const content = document.getElementById('content');
    const worker = new SharedWorker('worker.js');
    worker.port.start();
    worker.port.onmessage = function(e) {
    console.log(e);
    content.innerHTML = e.data
    }
    btn.onclick = function() {
    worker.port.postMessage('get');
    }
    </script>
  5. cookie (需要同源)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // tab1
    document.cookie = 'name=Tom';
    // tab2
    const cookie = document.cookie;
    setInterval(() => {
    if(document.cookie !== cookie){
    console.log('cookie changed');
    }
    },1000)
  6. window.postMessage (可跨源)
    两个tab必须是具有相同的通信协议(比如https),必须是处于同意服务器(document.domain相同)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // index1.html
    const win = window.open('index2.html', '_blank', 'width=1000,height=1000,resizeable=yes')
    btn.onclick = function() {
    win.postMessage('test');
    }
    // index2.html
    window.addEventListener('message', function(e) {
    console.log(e.data);
    })
  7. WebSocket
    WebSocket区别后台HTTP服务器最大的特点就是它是双向通信的(服务器可以向客户端主动推送消息)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // server.js
    // 先安装依赖: npm i ws
    const WebSocketServer = require('ws').Server;
    const wss = new WebSocketServer({post: 8080})
    const clients = []
    wss.on('connection', client => {
    clients.push(client)
    client.on('message', function(data) {
    for(var c of clients){if(c !== client) {c.send(data)}}
    })
    client.on('close', function() {
    clients.splice(clients.indexOf(client), 1)
    })
    })
    // 运行执行:node server.js

    // index1.html
    const ws = new WebSocket('ws://localhost:8080')
    btn.onclick = function() {ws.send('test')}
    window.onbeforeunload = function() {ws.close()}
    // index2.html
    const ws = new WebSocket('ws://localhost:8080')
    ws.onopen = function(e) {
    ws.onmessage = function(e) {console.log(e.data)}
    }
    window.onbeforeunload = function() {ws.close()}

案例知识

  1. 歌词滚动
  • html 标签boolean属性可以直接写属性名,即为true

  • lorem 乱数假文

    1
    2
    3
    lorem 随机生成一段话
    lorem4 随机生成4个单词
    li*30>lorem4 生成30个li,每个里面的内容是4个随机单词
  • 行盒(如span)、块盒(如div)

  • 字体大小变化推荐使用transform控制性能更好,(因为transform是合成线程处理的)

    1
    2
    transform:  scale(1.2);
    transition: 0.2s; // 过渡时间

购物车

  1. 通过class 将业务逻辑抽离封装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Ui {
    constructor() {
    this.goodsData = new GoodsData();
    this.uiData = new UiData();
    }
    }

    class GoodsData{
    constructor() {
    }
    }

    class UiData {
    constructor() {
    }
    }
  2. 水波纹效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // html
    <div class="water-animate">test animate</div>

    // css
    .water-animate {
    animation: water 500ms ease-in-out;;
    }
    @keyframes water {
    0% {transform: scale(1);}
    25% {transform: scale(0.8);}
    50% {transform: scale(1.1);}
    75% {transform: scale(0.9);}
    100% {transform: scale(1.1);}
    }

  3. 抛物线图标移动效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // html
    <div id="wrp" class="wrp-css">
    <img id="icon" class="icon-css" src="xxx.png" />
    </div>

    // js
    // 父层
    wrp.style.transform = 'translateX(start)';
    wrp.clientWidth // 强制渲染
    wrp.style.transform = 'translateX(end)';
    // 子层icon
    icon.style.transform = 'translateY(start)';
    icon.style.transform = 'translateY(end)';

    // css
    .wrp-css{
    transition: 1s linear;
    }
    .icon-css{
    transition: 1s cubic-bezier(0.5, -0.5, 1,1); // 抛物线
    }

  4. 属性描述符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Person {
    constructor(_data) {
    // 属性描述符用于设置对象属性的各种规则,以确保对象属性在使用时的行为符合预期
    Object.defineProperties(this, 'data', {
    value: _data,
    freeze: true, // 冻结(不可更改),默认false
    writable: true, // 可重写,默认true
    configurable: false, // 不可重新定义
    enumerable: true, // 可遍历,默认true
    get: function() {return this.data;}
    set: function(value) {
    if(typeof value !== 'object' ) {
    throw new Error('value must be object')
    }
    this.data = value;
    }
    })
    Object.seal(this); // 对象不可增加属性
    Object.freeze(this); // 对象被冻结
    }
    }

参考

浏览器组成