不奢望岁月静好 只希望点滴积累

0%

有些句子、简简单单、曼妙无比、收藏谨记~

  1. 时光不老、青春不散、繁华落幕、漫看似水流年~

  2. 星光不问赶路人、时光不负有心人

  3. 生活从来不会给我们从容思考的机会

  4. 勿以流沙造高台

  5. 人生如逆旅,我亦是行人
    ps: 生如逆旅单行道、从无岁月可回头

  6. 时光是种极好的东西、
    原谅了不可原谅的、过去了曾经过不去的、也许偶尔想回到之前的时光、但我知道 人 始终要学会向前看…

  7. 我也愿学习蝴蝶、一再的蜕变、一再的祝愿 既不思虑、也不彷徨;既不回顾、也不忧伤~

  8. 不雨花犹落、无风絮自飞

  9. 终于明白、有些路只能一个人走、
    那些约好同行的人、一起相伴、走过雨季、走过年华、但总有一天会在某个渡口离散

  10. 真正的宁静、不是避开车马喧嚣、而是在心中修篱种菊、尽管如流往事、每一天都有涛声依旧、只要消除执念、便可寂静安然~

  11. 愿你此生清澈明朗、流年无殇; 也愿你时光静好、安暖如初

  1. rpm -qa 查看安装的软件列表
    -q --query 查看 -a --all 所有
    后可用管道输出 eg. rpm -qa | less rpm -qa | grep mysql
    -i --install 从软件包安装
    -e --erase 删除软件包

  2. man rpm 查看rpm帮助文档

  3. yum配置文件 /etc/yum.repos.d/CentOS-Base.repo

    1
    2
    3
    4
    5
    [base]
    name=CentOS-$releasever - Base - 163.com
    baseurl=http://mirrors.163.com/centos/$releasever/os/$basearch/
    gpgcheck=1
    gpgkey=http://mirrors.163.com/centos/RPM-GPG-KEY-CentOS-7
  4. nohup no hang up 程序不挂起、退出命令行、程序依然执行

  5. & 表示后台运行

    1
    2
    3
    4
    nohup command > out.file 2>&1 &
    1表示文件描述符1, 代表标准输出
    2表示文件描述符2, 代表标准错误输出
    2>&1 合并标准错误输出和标准输出 输出到out.file

    ps -ef | grep command | awk ‘{print $2}’ | xargs kill -9

    关闭进程

  6. 创建新的进程 fork

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    父进程返回0、子进程返回进程号、调用execve来执行另外的程序(先copy再修改)
    每个进程有独立的内存空间、相互不干扰
    代码段: Code Segment 存放程序代码
    数据段: Data Segment 存放运行中数据
    局部变量、当前函数执行时分配、运行结束释放
    动态分配、长时间保存、指明才销毁 堆heap
    一个进程的内存空32位是 4G、64

    进程内存的分配是在使用时、进程需要使用内存的时候调用内存管理系统登记、但此时不会真正分配、真正写入数据的时候发现无对应物理内存、才会触发中断、分配物理内存

    需要内存小的时候、使用brk分配和原来的数据堆连在一起、
    需要内存大的时候、使用mmap、重新划分一整块区域

    等待子进程 waitpid

  7. 文件管理

    • 已有文件, 使用 open 打开文件, close 关闭文件
    • 不存在的文件, 使用 create 创建文件
    • 打开后的文件, 使用 lseek 跳转到指定位置
    • 文件读取 read 文件写入 write

linux 的核心思想: 一切皆文件

  • 启动一个进程、需要一个程序文件、这是 二进制文件

  • 启动时的配置文件, 如 *.yml, *.properties 等、是文本文件、日志也是文本文件

  • 命令交互控制台输出、是标准输出 stdout文件

  • A进程的输出可以作为另外一个进程的输入、这个方式成为 管道 也是一种文件

  • 不同进程间的通信、通过socket 也是文件

  • 进程需要访问外部设备、设备也是文件

  • 文件夹也是文件

  • 进程运行时、/proc 下对应的进程号 也是文件

  1. 进程间通信

    • 消息队列: Message Queue 在内核实现, msgget 创建、msgsnd 发送、msgrcv 接收, 消息内容不大时使用
    • 共享内存: 交互消息比较大的时候, shmget创建 shmat 将共享内存映射到自己的内存空间, 通过信号量semaphore 来处理竞争
      • 对于只允许一个人访问的资源、将信号量设为1、第二个访问请求进入会调用 sem_wait 进行等待、访问结束调用 ssem_post 释放资源、结束等待
  2. Glibc 提供字符串处理, 数学运算等用户态服务, 提供系统调用封装

    • 每个特定的系统调用至少对应一个Glibc 封装的库函数、

      eg. 打开文件的系统调用 sys_open 对应的是 Glibc 中的 open 函数

    • Glibc 中一个单独的API可能对应多个系统调用、

      eg. Glibc 对应的printf 就会调用 sys_open, sys_mmap, sys_write, sys_close 等系统调用

    • 也可能多个api对应同一个系统调用、

      eg. Glibc 下实现的 malloc, calloc, free 等函数用来分配和释放内存、都利用了内核的 sys_brk 的系统调用

Linux系统调用.png

参考:
https://time.geekbang.org/column/article/89251

【感谢极客时间、有很多优秀的专栏】

CPU 最核心的组件, 中央处理器 Central Processing Unit

Bus 总线, CPU 和 其它设备连接的桥梁、其实就是主板上密密麻麻的集成电路

内存 最重要的设备、与CPU一样、是完成计算任务的核心组件

cpu 包含 运算单元、数据单元 和 控制单元

运算单元: 负责计算, eg. 加法、移位等. 但不知道结果存放在哪里

​ 若每次通过总线到内存去拿、速率很低、所以有了数据单元

数据单元: 包括CPU内部的缓存和寄存器组、空间小、速度高、可暂时存放数据和运算结果

控制单元: 同一指挥中心、获取指令 & 执行指令, 会指导运算单元取出数据单元中的某几个数据、计算出结果、然后存放在数据单元的某个地方

CPU这么执行程序, 操作数据, 并将结果写回内存的呢 ?

1
2
3
4
5
6
7
CPU的控制单元里, 有一个指令指针寄存器、存放的是下一条指针的地址, 控制单元会不断将代码段的指令拿进来、写入指令寄存器

当前指令分为: 做什么操作 ? 操作哪些数据?
数据单元根据数据地址、从数据段读到数据寄存器、然后参与运算、运算结果会暂存在数据单元的数据寄存器、最终会有指令将数据写回内存中的数据段

CPU有两个寄存器, 专门保存当前处理的代码段的起始地址和数据段的起始地址.
里边是哪个进程就执行哪个进程的指令、等切换成另一进程就会执行另一进程的额指令、这个过程叫 进程切换 Process Switch

CPU 和 内存进行数据交换、靠的是总线 Bus, 分为 地址总线数据总线

地址总线的位数、决定可寻址范围, eg 只有两位、那CPU只认 00 01 10 11 四个位置

数据总线 的位数、决定一次可以拿多少个数据进来、

eg. 只有两位、CPU一次只能拿2位数、要想拿8位就需要4次、位数越多、一次拿的数据就越多、访问速度也越快

8086 系统架构

8个16位通用寄存器 AX BX CX DX SP BP SI DI主要用于计算过程中暂存数据 (数据单元)

IP 指令指针寄存器Instruction Pointer Register 指向代码段下一条指令的位置, CPU根据它从内存的代码段加载指令到CPU的指令队列中、交给运算单元去执行

段寄存器

CS 代码段寄存器, 保存代码在内存中的位置 Code Segment Registeer

DS 数据段寄存器, 保存数据在内存中的位置 Data Segment Register

SS 桟寄存器 Stack Register 程序运行中的一个特殊结构、存取只能从一端进行

ES

若运算中需要加载内存中的数据、需要通过DS找到内存中的数据, 加载到通用寄存器中该如何加载?

对于一个段、有一个起始的地址、而段内的具体位置、称为偏移量 Offset

CSDS中都存放着一个段的起始地址、代码段的偏移量在IP寄存器中、数据段的偏移量通常在 通用寄存器

那么问题来了:

CSDS都是16位的、即: 起始地址都是16位的、

IP寄存器和通用寄存器都是16位的、即: 偏移量也是16位的

但: 8086 的地址总线是20位、怎么做?

1
起始地址*16 + 偏移量, 即: 把CS和DS中的值、左移4位 + 16位的偏移量 得到20位的数据地址

so. 无论真正的内存是多大、对于只有20位地址总线的8086来说、能识别出的地址只有 2^20 = 1M

偏移量是16位、段大小最大为 2^16 = 64k

32位处理器

在32位处理器中、有32根地址总线、可以访问 2^32=4G 的内存

那如何去兼容原有8086架构呢?

  1. 通用寄存器扩展、将8个16位的扩展到8个32位的、保留8位和16位的使用方式

image.png

改动较大的是 段寄存器(Segment Register)

CSDSSSES 还是16位, 但不再是段的起始地址、段的起始地址放在内存、该内存是一个保存了段描述符的表格、段寄存器中保存的是在表格中的哪一项、称为选择子(Selector)

这种模式灵活度比较高、将来也可以一直兼容、前边的设计就不够灵活

前一种模式、称为 实模式 Real Pattern, 后一种模式称为保护模式 Protected Pattern

image.png

参考:

https://time.geekbang.org/column/article/89417

#####为什么需要复杂度分析?

1
2
3
4
5
6
7
8
9
通过监控得到的时间和内存占用是正确的、有些地方称为: 事后统计法
但事后统计有很大的局限性:
1. 测试结果严重依赖测试环境
机器配置和设置都会导致测试结果的差异
2. 受数据规模的影响较大
eg. 对于同一排序算法、待排序数据的有序度不同、排序得到的时间差别会比较大、也会受数据本征的影响. 若原数据有序、对有些算法来说 可能不需要任何操作、会特别快
另: 数据规模较小时、可能无法反馈出排序算法的性能

所以、需要一个不受测试数据和测试环境影响、可以用来粗略计算算法执行效率的方法。
大O复杂度表示法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
算法的执行效率简单来说、就是算法代码执行的时间

eg. 下边代码
int cal(int n) {
int sum = 0;
int i = 1;
for (; i <= n; ++i) {
sum = sum + i;
}
return sum;
}

从CPU的角度来看、每一行都有类似的操作: 读数据-运算-写数据
尽管每行代码对应的CPU执行的个数、执行的时间都不同、但对于粗略估计、可以认为是相同的, 假设为: unit_time
45行运行了n遍、需要2n*unit_time, so. 总共需要 (2n+2)*unit_time
所有代码的执行时间 T(n) 与执行次数成正比

将改规律记为:
T(n) = O(f(n))
T(n):执行时间 n:数据规模 f(n):每行代码的执行次数总和、就是大O时间复杂度表示法

大O时间复杂度只是代表执行时间随数据规模增长的变化趋势、而不是真正的代码执行时间、也叫 渐进时间复杂度
时间复杂度分析
1
2
3
1. 只关注执行次数最多的代码
2. 加法法则: 总复杂度等于量级最大的代码的复杂度
3. 乘法法则: 嵌套代码的复杂度=嵌套内外代码复杂度的乘积
常见复杂度量级
1
2
3
4
5
6
7
8
9
常量阶 O(1)
对数阶 O(log n)
线性阶 O(n)
线性对数阶 O(nlog n)
平方阶 O(n<sup>2</sup>)、立方阶 O(n<sup>3</sup>)、... K次方阶 O(n<sup>k</sup>)
指数阶 O(2<sup>2</sup>)
阶乘阶O(n!)

可以粗略的分为 多项式量级和非多项式量级(指数阶 和 阶乘阶)、把时间复杂度为非多项式量级的算法问题叫 NP问题、当n的规模增大时、非多项式量级算法很低效
复杂度量级简单说明
1
2
3
4
5
6
7
8
9
1. O(1)
常量级复杂度、一般只要算法中不存在循环、递归语句、代码行再多、时间复杂度也是 O(1)

2. O(logn)O(nlogn)
对数阶复杂度(归并排序、快速排序的时间复杂度都是O(nlogn)
eg. i=1;while(i<n){i = i*3;}

3. O(m+n)O(m*n)
明显有m和n两个数据规模的示例
空间复杂度

时间复杂度的全称是: 渐进时间复杂度、表示算法的执行时间与数据规模之间的增长关系

类比: 空间复杂度 –> 空间渐进复杂度、表示算法的存储空间与数据规模之间的增长关系

1
常用的空间复杂度只有: O(1)O(n)O(n<sup>2</sup>)

链表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
链表: 用指针将一组零散的内存块串联在一起、其中: 把内存块称为链表的节点
为了将节点串联起来、每个链表的节点除了存储数据、还需要记录链上的下一个节点的地址(后继指针)

分为:单链表、双链表和循环链表

其中: 第一个节点称为头节点(用来记录链表的基地址)、最后一个节点称为尾节点、

单链表的尾节点指向一个空地址NULL、
循环链表: 一种特殊的单链表、与单链表的区别是: 尾节点指向链表的头节点
双向链表: 每个节点有一个后继指针next和一个前驱指针prev

存储相同的数据、双向链表要占据两个额外的空间来存储prev和next指针、需要更多的空间、但也带来了双向链表操作的灵活性

一个重要思想:
空间换时间: 在内存充足的时候、若追求代码的执行速度、可以利用空间换时间、选择空间复杂度高、但时间复杂度相对较低的算法
相反: 若内存较少、eg. 代码跑在单片机或者手机上、就需要考虑时间换空间的思路
链表vs数组
1
2
3
4
5
6
1. 链表可有效使用内存、插入和删除效率较高
2. 数组随机访问效率更高、插入删除效率较低
3. 数组的连续存储性、可借助CPU的缓存机制、预读数组中数据、访问效率更高
链表存储非连续、对CPU缓存支持不够友好
4. 数组大小固定、一旦声明就要占用整块的连续空间、若声明的数组过大、系统无足够连续内存、会导致OOM、链表本身无大小限制、可天然支持动态扩容
5. 若代码对内存要求很高、则数组更合适、且: 对链表频繁的增删会导致频繁的内存申请和释放、容易造成GC
几个写链表代码的技巧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
一、理解指针或者引用的含义
将某个变量赋值给指针、实际上就是将这个变量的地址赋值给指针、即: 指针中存储了这个变量的内存地址、指向了这个变量、通过指针就能找到这个变量

二、警惕指针丢失和内存泄漏
插入节点时需要注意操作顺序、删除节点时也要手动释放内存

三、利用哨兵简化实现难度
针对链表的增删操作、需要对头节点和尾节点进行特殊处理

四、留意边界条件
1. 空链表能否正常工作 ?
2. 只有一个节点的链表能否正常工作 ?
3. 只有两个节点能否正常工作 ?
4. 处理头尾节点时能否正常工作 ?

五、举例画图、辅助思考

六、多写多练

数组: 用一组连续的内存空间、存储一组具有相同数据类型的线性表数据结构

1
2
1.线性表: 数据只有前、后两个方向. 链表、队列、桟也是线性结构
2.连续空间、数据类型相同: 使得数组可以随机访问、但: 插入、删除数据的效率就低很多、需要数据重排
插入和删除
1
2
3
4
5
6
7
8
9
10
11
12
1.插入
1) 末尾插入、无需移动元素、复杂度 O(1)
2) 头部插入、搬移全部元素、复杂度 O(n)
每个位置插入的概率相同、平均复杂度为 (1+2+3+...n)/n = O(n)

若数组有序、只需要搬移k之后的数据
若数组无序、且不要求有序、将k位置的元素直接搬移到最后、只需要一次搬移 O(1)

2.删除
1) 与插入类似、最好O(1) 最坏O(n) 平均O(n)
2) 删除多(m)个元素时、可先标记删除(只标记)、后一次性删除、节省m*O(n)次操作
JVM的标记清除算法的核心

容器能否代替数组 ?

1
2
3
4
5
6
7
8
1. arrayList 将数组操作的细节封装. eg. 数据搬移
2. arrayList 支持动态扩容、默认扩容是1.5
扩容涉及到内存申请和数据搬移、比较耗时、若知道数据存储大小时、最好指定大小

1. 容器无法存储基础类型、eg. int, long, 需要封装为 Integer、Long 而自动装箱则有部分性能损耗
2. 若数据大小已知、且操作特别简单、可直接使用数组
所以、对业务开发基本上直接使用容器就好、省力、不易出错、丢失的性能可以不太关注、不影响整体性能
对底层开发、性能要做到极致、数组就优于容器成为首选
为什么大部分编程语言中、数组从0开始编号
1
2
3
4
5
6
下标确切定义是: 偏移offset、
1.使用a表示首地址、a[0]就是偏移为0的位置、即首地址、a[k]表示第k个type_size的地址
a[k]_addr = base_addr + k*type_size
2.若使用1开始计数、则
a[k]_addr = base_addr + (k-1)*type_size
1开始编号、对于CPU来说、就是多了一次减法运算

如何把数据包送达目的主机

主机A -> 网络层(添加IP头信息) -> 底层 -> 物理网络 -> 主机B -> 解析IP头信息 -> 将数据部分交给应用

如何把数据包送达应用程序

UDP协议: User Datagram Protocol
网络层和上层之间添加一层(传输层)、封装UDP头信息

UDP并不提供重传机制、只是丢弃当前数据包、且发送之后不知道是不是数据能到达目的地

TCP将数据完整送达应用程序

对于数据包丢失的情况、TCP提供重传机制
引入数据包排序机制、保证将乱序的数据包组合成一个完整的文件

1
2
3
浏览器使用HTTP协议作为应用层协议、用来封装请求的文本信息
使用TCP/IP作为传输层协议 将它发在网络上
即: HTTP请求的内容是通过TCP的传输数据阶段来实现的

HTTP请求流程

  • 构建请求

GET /index.html HTTP1.1

  • 查找缓存

浏览器在网络请求之后会保存资源副本在本地

  1. 缓解服务端压力、提升性能
  2. 对于网站来说、可以实现快速下载
  3. 若本地无资源副本、就会进入网络请求
  • 准备IP地址和端口

思考:

  1. http请求的第一步是什么呢 ?- 构建请求信息
  2. 建立连接的信息都包含什么? - ip和端口号
  3. 如果只有url可以拿到建立连接的信息么 ?- 通过DNS解析、http协议默认80端口
  • 等待TCP队列

准备好IP和端口、是不是就可以建立TCP连接?

不一定, Chrome 同一个域名最多建立6个TCP连接、若同时有10个请求、会有4个请求进入排队队列等待、直到进行中的请求完成

  • 建立TCP连接

在http开始工作之前、需要先建立TCP连接

  • 发送HTTP请求
  1. 请求行(请求方法、路由、http版本)
  2. 请求头(cookie等信息)
  3. 请求体(请求参数等)

服务端处理http请求流程

  • 返回请求
  • 断开连接(正常情况下、一单server返回响应数据、就会关闭TCP连接、若头信息加入 Connection:Keep-Alive则保持TCP连接不断开)
  • 重定向

  • 浏览器进程主要负责用户交互、子进程管理和文件存储等功能

  • 网络进程是面向渲染进程和浏览器进程等提供网络下载功能

  • 渲染进程主要职责是把从网络下载的HTML、JS、CSS图片等资源解析为可显示赫尔交互的页面

请求解析流程

1
2
3
4
5
6
大致可以描述为:
1. 首先从浏览器进程输入请求URL
2. 网络进程发起URL请求
3. 服务器响应URL请求之后、浏览器进程开始准备渲染进程
4. 渲染进程准备好之后、向渲染进程提交网络进程响应的数据、称为提交文档阶段
5. 渲染进程接收完整文档信息之后、开始解析加载页面及子资源、完成页面渲染
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
1. 用户输入
用户在地址栏输入查询关键字时、地址栏会判断输入的是关键字还是请求的URL
若是搜索内容、地址栏会使用浏览器默认的搜索引擎、合成包含搜索关键字的URL
若输入内容符合URL规则、则解析为URL、浏览器加载一个地址之后、导航栏图表就进入了加载状态、页面没变、需要等待提交文档阶段、页面内容才会替换
2. URL请求过程
浏览器进程通过进程间通信IPC把url请求发送到网络进程、网络进程接收到URL请求后才发起请求

1) 网络进程先查找本地缓存是否缓存了改资源、若有直接返回
2) 没有、进入网络请求流程(DNS解析->ip、若https协议先建立TLS连接)
3) 利用IP地址和服务器建立TCP连接、建立连接后浏览器会构造请求信息(请求行/头)把相关cookie加入请求头、发送请求信息
4) 服务器收到请求后、生产响应数据(响应行/头/体)、发给网络进程、网络进程接收到响应行和响应头之后解析响应头
a. 重定向 - 响应码 301|302 从响应头提取Location、然后发起新的请求、从新开始
b. 响应数据类型处理
Content-Type标记浏览器响应类型 text/html 代表返回数据是HTML格式、继续后续流程
application/octet-stream 返回数据是字节流类型、按照下载类型来处理、请求提交给浏览器的下载管理器
3. 准备渲染进程
默认情况下、Chrome会为每个页面分配一个渲染进程、但某些情况下会多个页面运行在同一渲染进程中
通常:
打开新的页面会使用单独的渲染进程、
若从A页面打开B页面、且AB属于同一站点、则B复用A页面的渲染进程、若是其它情况、会单独创建进程

渲染进程准备好之后、不能立即进入文档解析阶段、因为此时文档还在网络进程中、并未提交给渲染进程、所以下一步是进入提交文档阶段

4. 提交文档
提交文档(响应数据)的请求是浏览器进程发出的、渲染进程接收到消息后、会和网络进程建立传输数据的通道
文档数据传输完成后、渲染进程返回 确认提交 的消息给浏览器进程
浏览器进程在收到确认提交的消息后、更新浏览器界面状态、包含了安全状态、地址栏的url、前进后退的历史状态、并更新web界面

一个完整的导航走完了、进入渲染阶段

5. 渲染阶段
文档一旦被提交、渲染进程就开始页面解析和子资源加载了、一旦页面生成、渲染进程会给浏览器进程发送消息、浏览器进程收到消息停止标签页上的加载动画

浏览器加载页面.png

HTML的内容由标记和文本组成、也称标签

CSS又称 层叠样式表、由选择器和属性组成

JS 使页面内容动起来

渲染模块在执行过程中分为很多子阶段、输入的HTML经过这些子阶段、输出像素、这个处理过程称为 渲染流水线

  1. 构建DOM树

    浏览器无法直接理解和使用html、需要先转化为其能理解的结构 - DOM树

    构建dom树的输入是简单的html文件、经过html解析器解析、输出 树状结构的DOM

    DOM和html的内容基本一致、但dom是保存在内存中的树状就结构、可以通过JS直接修改

  2. 样式计算

    是为了计算出DOM节点中每个元素的具体样式

    1) 将CSS转化为浏览器可理解的结构 stylesheets

    2) 转换样式表中的属性值、使其标准化 eg. 2em -> 32px blue -> rgb(0,0,255)

    3) 计算出dom树中每个节点的具体样式

  3. 布局计算

    计算dom树中可见元素的几何位置

    1) 创建布局树 遍历dom节点、将这些节点加载到布局中

    2) 布局计算 将布局运算的结果写回布局树

  4. 分层

    为了更方便的实现3D变换、页面滚动或者使用z-indexing做z轴排序等

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

    浏览器的页面其实是很多图层、这些图层叠加后合成了最终的页面

    并不是所有的布局树的每个节点都包含一个图层、若节点无对应图层、则从属于父节点的图层

    1) 拥有层叠上下文属性的元素会被提升为单独的一层

    2) 需要裁剪的地方也会被创建为图层

  5. 绘制

    图层的绘制与画画的流程基本一致、会把一个图层的绘制拆分成很多小的绘制指令、然后将指令按照顺序组成一个待绘制列表

  6. 栅格化

    绘制列表只是用来记录绘制顺序和绘制指令的列表、而实际上绘制操作是由渲染引擎中的合成线程来完成(图层绘制列表准备完成后、主线程会把绘制列表提交给合成线程)

    通常、栅格化过程会使用GPU来加速生成、使用GPU生成位图的过程叫 快速栅格化、生成的位图保存在GPU内存中

  7. 合成

    所有图块被光栅化之后、合成进程会生成一个绘制图块的命令、提交给浏览器进程

    浏览器进程的viz组件、接收绘制命令绘制内容显示到浏览器

几个概念

重排: 通过JS或者CS修改元素几何位置、eg. 改变元素的宽、高、等会触发重新布局、解析之后的一系列子阶段、这个过程就叫重排、需要更新完整的渲染流水线、开销很大

重绘: eg. JS更改某些元素的背景色、布局阶段不会重新执行、直接进入绘制阶段、然后执行之后的子阶段、相对重排 少了布局分层阶段、执行效率会高一些

直接合成阶段: eg. 使用CSS的transform来实现动画、可以避开重排重绘阶段、直接在非主线程上执行合成动画操作、大大提升绘制效率