Giant VM 操作过程

主机环境:Windows 11 64位 core i9 12900p

此次使用VMWare Workstation pro 16进行操作

1.环境配置

step1 系统准备

win+r 打开cmd

sysinfo

查看是否开启了Hyper-V,如果开启应将其关闭

关闭方式

  • 【启用或关闭Windows功能】-> 将虚拟机平台和windows虚拟机监控程序平台关闭
    image-20220930105100032
  • 如果看到了Hyper-V的选项框,将其取消勾选
  • 然后重启生效修改

step2 VMware 设置

VMware 中启动Ubuntu16.04
配置为
内核: Linux4.15.0-112,磁盘分配>40G,在CPU设置中启用嵌套虚拟化

step3 下载必要的包

1
2
3
sudo apt-get install build-essential openssl libncurses5-dev libssl-dev

sudo apt-get install zlibc minizip libidn11-dev libidn11 bison flex

step4 获得Linux-DSM

1
git clone https://github.com/GiantVM/Linux-DSM.git

step5

1
cd Linux-DSM

step6 Enable DSM support

1
make menuconfig

Virtualization –> KVM distributed software memory support –> press 'Y' to include the option
Save –> Exit

step7 Compile the Kernel (make)

make -jN
[N 是]
wait for about three hour(or more)
之前的失败经历: Environment: win11 wsl2 Ubuntu16.04 LinuxKernel version 5.10 output : makefile:976: recipe for target 'vmlinux' failed

step 8 install the Kernel

1
2
sudo make modules_install
sudo make install

step 9 update the grub

[在我的尝试中,这时候应当先打开grub这个文件]
[gedit 比较方便看,用vi也可以]

1
sudo gedit /etc/default/grub

[然后将GRUB_HIDDEN_TIMEOUT 这个属性置为0,不然之后重启的时候没时间换系统]
这自己操作
[然后是核心操作 ]

1
sudo update-grub

[之后重启]

1
reboot

[重启后看到下面界面,按照图片选择]
[之后等待,启动后,在shell里输入]

1
uname -a 

[可以看到版本为ubuntu 4.9.76+]

2.QEMU

step1 Prepartion

sudo apt-get install python pkg-config libglib2.0-dev zlib1g-dev libpixman-1-dev libfdt-dev
git clone https://github.com/GiantVM/QEMU.git
cd QEMU

step2 Configuration

1
./configure --target-list=x86_64-softmmu --enable-kvm

step3 Compilation

1
make -jN

step4 Create hard disk image

1
2
cd ..
wget http://ftp.sjtu.edu.cn/ubuntu-cd/16.04.7/ubuntu-16.04.7-server-amd64.iso

[如果找不到,可以直接输入http://ftp.sjtu.edu.cn/ubuntu-cd/16.04.7,在里面找到Ubuntu-16.x-server.iso ,然后wget]

[这里就是得用apt下一个qemu,选择不下,用底下x86-64_softmmu/ 底下的qemu-system-x86_64会卡死]

1
2
3
/qemu-img create -f qcow2 ubuntu-server.img 10G
sudo apt-get install qemu
qemu-system-x86_64 -m 1024 ubuntu-server.img -cdrom ../ubuntu-16.04.7-server-amd64.iso -enable-kvm

[上面会跳出系统设置,基本设置一下用户名和密码,然后会问是否要装载GRUB,选择yes,其他无所谓]

3.Run Giant VM on a single machine

First we install vncviewer to monitor the guest.

1
2
wget https://www.realvnc.com/download/file/viewer.files/VNC-Viewer-6.19.325-Linux-x64.deb
sudo dpkg -i VNC-Viewer-6.19.325-Linux-x64.deb

如果下面报错说内存不够,把虚拟机关掉,多分配给它一点内存(>8G)

terminal 1 :

1
2
3
cd QEMU/

sudo x86_64-softmmu/qemu-system-x86_64 --nographic -hda ubuntu-server.img -cpu host -machine kernel-irqchip=off -smp 4 -m 4096 --enable-kvm -serial mon:stdio -local-cpu 2,start=0,iplist="127.0.0.1 127.0.0.1" -vnc :0

terminal 2:

1
2
cd QEMU/
sudo x86_64-softmmu/qemu-system-x86_64 --nographic -hda ubuntu-server.img -cpu host -machine kernel-irqchip=off -smp 4 -m 2048 --enable-kvm -serial mon:stdio -local-cpu 2,start=2,iplist="127.0.0.1 127.0.0.1"

terminal 3:[启动 vncviewer]

冒号后面的0和前面 terminal1 后面-vnc 后面的数字对应

1
vncviewer :0

如果启动之后看到 nobootable device ,可能需要检查2 QEMU 最后一步的 qemu-system-x86_64 那段是不是正常

CPU虚拟化

MAIN TASK: 执行虚拟机程序指令,响应虚拟机内外部事件

面临的挑战

1. 敏感非特权指令

挑战来源: 基于 陷入-模拟 机制的虚拟化架构只能在所有敏感指令都是特权指令的架构中被建立
敏感指令: 操作敏感物理资源的指令,如I/O指令、页表基地址切换指令等
特权指令; 必须运行在最高特权级的指令,在非特权级中执行这些指令将会触发特权级切换。
解决方案:

软件方案: 解释执行【无脑解释执行所有指令,效率较低】,二进制翻译【将敏感指令替换为其他指令,会增加指令数量】,扫描与修补【在执行前,将敏感指令替换为特权指令,代码局部性较差】,半虚拟化【在执行敏感指令的时候通过超调用主动陷入Hypervisor中,避免扫描二进制代码引入的开销,但是打破了虚拟机和Hypervisor之间的界限】
硬件辅助方案: Intel VT-x,AMD SVM, ARM EL2, RISC-V H-Extension
将所有敏感指令转化为特权指令【可能存在兼容性问题】
引入虚拟化模式

2. 上下文切换

类似进程上下文,虚拟机发生退出时需要保持各寄存器的状态
发生虚拟CPU调度时,需要保存当前虚拟CPU的上下文兵加载待调度虚拟CPU上下文

3. 中断处理

模拟方式:为每一个虚拟机维护一个虚拟中断控制器

QEMU/KVM CPU虚拟化实现

floats

浮点数

基础知识

浮点通过移动二进制小数点来表示尽可能大的取值范围

数字公式

$$
(-1)^S M 2^E
$$

符号位{S} 表示正负

小数{M} 一般是一个1~2之间的小数值

指数位{E} 指的是乘2的E次方

IEEE浮点数标准

single 单精度

1位符号位,8位指数位,23位小数位

Double 双精度

1位符号位,11位指数位,52位小数位

小数位的特殊表示

由于小数位永远是1.010101这样的模式

因此前面的1不放入存储,只记录后面的01010101这种

同时,在计算时,会刻意维持小数位为1.010111这样的模式,就是说维持小数位在1~2之间

零表示法

$$
(-1)^S M 2^E
$$

此时E为1-Bias,之前为0-Bias

由于之前无法表示0,将M设置为0.110101010这种模式,此时可以表示0

当E全0,小数位全0的时候

表示0

这会导致正负0的出现

当E为0,小数位不为0的时候,可以表示一些很接近0的东西

如果E为111111….1 ,frac 不为0,

则代表一个极大值,不代表数字

Bits Bytes and Integers

Bits,Bytes and Integers

基础位运算

分清&,&&,| ,||,~,!之间的区别

位操作: &,|,~,运算时只是单纯进行位操作

逻辑比较: &&,||,!,运算时会添加逻辑考虑,

比如||前面的为True,则提前中止调用

又比如,对一个非0的数进行!两次会获得一个1,这和位操作有很大的区别

算术右移和逻辑右移

逻辑右移: 将整个数字向右移动,同时高位补0

算术右移: 将整个数字向右移动,同时高位按照符号位补充

左移的特殊情况

如果一个八位的数字进行左移八位的操作,在部分计算机中,这个8将被模8,因此并不能得到想象中的0

补码 T

国内讲法: 除了符号位,全部按位取反再加1

这课的讲法:最高位视为负权,如[-16,8,4,2,1]

经典图片

1663413629124

扩展

例如:8位->16位

基本规则:将符号位复制k份,k为扩展位数

解释,想象负数的情况,假设最高位权重暂时是-8,增加一位最高位,新增一位-16,但之前的最高位变为+8,刚好抵消。

加减乘除

无符号数直接不谈

补码加法: 可以通过补码加法简单得到减法,只要加上负的值就行

正/负溢出

正溢出:俩正数加起来等于一个负数

乘法

如果乘2的倍数,只需要【算术】移位就行了,不管是不是补码表示

除法

同样是算术移位,但这回如果是负数,它就不是四舍五入,而是向负无穷舍入,例如 -3/2=-2。而不是-1

获得相反数

按位取反再+1

一个恐怖的案例

1663426748805

这里,i会被隐式转换成unsigned int(因为sizeof是 unsigned int),于是这个for循环不会结束

无符号数的优点

由于无符号数的计算采用取模的方式

在一些模运算时(比如大多数加密算法)

当位表示集合而非数字时,用无符号数最好。

字存储

最低有效字节在小端序电脑中放在首位,在大端机器中放在末位

计组复习

指令系统

基本格式: 操作码| 地址码 (字节的整数倍)

地址码结构

四地址指令: A1 OP A2 -> A3 4次访存

三地址: 四地址-A3 (通过PC+1获得下一条指令位置) 4次访存

二地址: 三地址-A3 ( 结果存放在A1位置)4次访存

一地址: A_cc OP A1->Acc (Acc在累加寄存器中) 2次访存

零地址: 都在堆栈中

操作码

定长: 简化架构,但利用率低

变长:设计复杂,但同字节长度下可支持较多指令

编址方式

目的: 指出操作数的来源和去向

需要编址的:通用寄存器,主存,输入输出设备

编址单位

  1. 字编址
    编址单位=访问单位,每个编址单位所包含的信息量与读写寄存器所得的信息量相同
  2. 字节编址
    编址单位<访问单位
    编址单位和信息的基本单位(一个字节)相一致
  3. 位编址
    先不谈

地址码的位数

与主存容量成正比,和最小寻址单位成反比

数据寻址

目的:找到所需的操作数

  1. 立即寻址
    直接取出操作数
  2. 寄存器寻址
    给出寄存器编号,再访问寄存器取出操作数
  3. 直接寻址
    取出操作数再主存中的地址
  4. 间接寻址
    从主存中取出存放操作数地址的地址
  5. 寄存器间接寻址
    获取寄存器编号,寄存器中该编号存放操作数在主存的地址
  6. 变址寻址
    读取形式地址A加上变址寄存器Rx的值,获取真实地址
  7. 基址寻址
    读取位移量D,与基址寄存器中的值相加获得真是地址
  8. 相对寻址
    相对于基址寻址,由程序寄存器提供基准地址
  9. 页面寻址
    将整个主存空间分成若干个大小相同的页,每页有自己的编号,称为页面地址,业内的主存单元也有自己的编号,称为页内地址,操作数的有效地址就被分为:页面地址和页内地址两部分
    有三种页面寻址方式
    1. 基页寻址
      有效地址为 0//A (//为简单拼接)
    2. 当前页寻址
      页面地址为程序计数器PC的高位部分,有效地址为(PC)h // A
    3. 页寄存器寻址
      页面地址取自页寄存器,与形式地址拼接形成操作数有效地址
      EA=(页寄存器)//A

指令寻址

目的:寻找下一条要执行的指令地址

顺序寻址:

PC+1(程序计数器加1)

跳跃寻址:

通过程序转移类指令实现

直接、相对、间接寻址(详见数据寻址)

堆栈与堆栈操作

堆栈结构

  1. 寄存器堆栈
    一组专门的寄存器构成,栈顶固定,且组中寄存器互相连接,可将一个寄存器中的内容推移到另一个寄存器中去
  2. 存储器堆栈
    从主存中划出一块区域来当堆栈,大小可变,栈底固定,栈顶浮动,需要一个硬件寄存器作为堆栈栈顶指针SP
    PS: 栈底指针地址大于栈顶地址,因此进栈时,栈顶指针先减一,然后将数据压入指针指向的堆栈位置

堆栈操作(用途)

【一般计算机】

  1. 暂存中断断点
  2. 子程序调用时的返回地址
  3. 状态标志
  4. 现场信息
  5. 子程序调用时参数的传递

因此访问堆栈的指令只有进栈和出栈两种。

指令类型

数据传送类指令

用于寄存器与寄存器,寄存器与主存 ,主存与主存之间的数据传送

  1. 一般传送指令:将源地址的数据复制到目的地址
  2. 堆栈操作指令:因为堆栈(主存中开辟的)是一个特殊区域,因此对堆栈的操作也就是对存储器的操作
  3. 数据交换指令:将源操作数和目的地操作数相互交换位置

运算类指令

  1. 算术运算指令
  2. 逻辑运算指令
  3. 移位指令(算数移位【保持操作数符号不变,左移数值*2,右移数值/2】,逻辑移位【不管符号】,循环移位)

程序控制类指令

用于控制程序的执行方向,并使程序具有测试、分析与判断的能力

  1. 转移指令
    无条件(JMP): 直接吧程序转向新的位置执行
    条件转移:条件满足才转移
  2. 子程序调用指令
    子程序是一组可以公用的指令序列,只要知道地址就能调用
    主程序转向子程序称为子程序调用指令(CALL)
    子程序转向主程序成为转移指令(RET)
    一般使用堆栈保持返回地址
  3. 返回指令(RET)

输入输出类指令

实现主机与外部设备之间的信息交换

独立编址的IO

独立编址:外设端口和主存单元独立编址,指令系统有专门的IN/OUT指令,信息从外设到主机称为输入

指令中给出外设端口地址,这些地址是另一个独立的地址空间

统一编址的IO

指令系统没有专门的IO指令,用一般的数据传送类指令来实现

数值的机器运算

基本算数运算的实现

并行可以加快过程,但并行会使计算式过长,因此采用并串联合使用的方式取得性能和逻辑的平衡

  1. 单级先行进位方式
    组内并行,组间串行
  2. 多级先行进位方式
    组内并行,组间也并行

定点加减运算

补码加法:[X+Y]补=[X]补+[Y]补

减法也只是加补码求反后的结果

后面的先不看了

存储系统和结构

组成

按作用分类

  1. 高速缓冲存储器

位于主存和CPU之间

  1. 主存储器

CPU 可直接访问

  1. 辅助存储器

CPU不能直接访问

存取方式分类

  1. RAM 随机存取
    存取时间相同,随机读写访问
  2. ROM 只读
    特殊的RAM,只能读不能写
  3. SAM 顺序存取
    只能按某种顺序存取,存取时间与存储体上的物理位置有关
  4. DAM 直接存取存储器
    读取步骤: 第一步直接指向存储器中的某个小区域,第二部在小区域内顺序检索

存取层次结构

cache 主存 辅存 分为两个层次

Cache-主存 存储层次

目的:解决主存速度不足

方式: 在CPU和主存之间,增加辅助硬件,让他们构成一个整体,使CPU速度接近cache,容量接近主存

主存 - 辅存存储层次

目的:解决主存容量不足

主存储器的组织

基本结构

存储体

地址译码驱动电路

  • 译码器和驱动器,译码器将地址总线输入的地址吗转换成输出线上的有效电平,驱动器提供驱动电流去驱动相应的读写电路

IO和读写电路

存储单元

位: 二进制数的最基本单位,也是存储器存储信息的最小单位

存储字:一个二进制数由若干位组成,当这个二进制数作为一个整体存入或取出时,这个数称为存储字

存储单元(主存单元) 存放存储字或存储字节的主存空间,由计算机的结构确定。是CPU对主存可访问擦欧总的最小存储单位。

存储体: 大量存储单元集合构成一个存储体

技术指标

  1. 存储容量
    指主存所能容纳的二进制信息总量,对字节编址的计算机,以字节数表示,对字编址的计算机,以字数和字长的乘积来表示
  2. 存取速度
    2.1 存取时间
    从启动一次存储器操作到完成该操作所经历的时间
    2.2 存取周期
    指存储器进行一次完整的读写操作所需的全部时间,连续两次访问存储器操作之间所需要的最短时间
  3. 主存带宽
    每秒从主存进出信息的最大数量(字/秒或 字节/ 秒 或 位/ 秒)

半导体随机存储器和只读存储器

动态RAM的刷新

刷新方式

集中式

集中安排若干个刷新周期,刷新时停止读写操作

时间=存储体矩阵行数*刷新周期

分散式

把刷新操作分散到每个存取周期内进行,此时存取周期被分成读写和刷新两个部分。

异步式

把刷新操作平均分配到整个最大刷新间隔时间内进行

刷新间隔=最大刷新间隔时间/ 行数

主存容量的扩展

微型数据库开发记录

前言

近三周应小学期老师要求,和4位队友合作开发了一个微型数据库系统。由于耗费了许多的精力,我想将开发过程记录在这个博客上。

功能性需求:

  1. 创建数据库和表,能够以文件形式保存在磁盘上(操作系统的文 件、进程管理,数据结构 的 B树)
  2. 支持表的增删改查(数据库 中SQL形式,编译的词法语法检查)
  3. SQL中支持通配符(数据结构的查找)、多表连接(操作系统 的 文件管理)
  4. 支持整数、实数、字符(串)、日期等数据类型
  5. 支持索引(数据库、数据结构 )

性能性需求

  1. 单表记录最大行数不少于10万行,不少于10 列
  2. 单表响应时间不多于 1秒(普通笔记本)
  3. 主表不少于5000行,子表不少于20000行时,连接操作响应时 间不多于 2 秒

痛苦的开端

最初的迷茫

开始的时候确实是啥也不知道,一堆人在教室里商量了半天也没想明白该干些什么,就留了两天各自查资料。等大家查完,在教室里一讨论,就发现任务极其重大。由于要实现5000*20000级别的表连接,还有100 000级别的数据插入,因此我们需要考虑底层的文件存储,但由于开发经验的不足和工期较短,我们在讨论中断定,凭空实现完全的页面管理对我们来说是不可能的,因此决定退而求其次,找一份有页面管理的借鉴一下。

因此,我们翻看了sqlite的源码,还通篇学习了一门斯坦福的课程(代码仓库叫redbase),在途中,还重新确定了我们要实现的关键字等需求。

借鉴都借鉴不明白

然后就出现了问题,我们找到了一份很好的页面管理代码,但光页面管理(包括缓冲区管理)的代码量就直接超过了8000行,为了实现它我们需要做的工作过于繁复(虽然我们确实尝试了一天)。

同时,Linux开发也对我们组造成了较大的麻烦,因为有开发经验的仅有一人,其他人光环境配置就花了很长时间。后来采用先富带后富的方式,总算是搞定了整组的环境。

借鉴不了,想一个自己的框架吧!

最后,我们发现,我们的这种特种需求,只有我们自己琢磨一个框架来才能够满足3周内开发完成,且能够实现一定的文件管理和查询优化。

这里用文字形容一下我们初步商量的框架

顶层:词法分析->语法分析,产出一棵抽象语法树

中层:语义分析,并实现一系列数据库操作函数

底层:B+树代码和物理存储管理代码,存储二进制数据。

中层咱暂时实在想不明白,就先大致写了一点儿,之后先把顶层和底层开发出来,到时候再看中层该怎么搞。

辛苦又充满成长的开发过程

经历了鸡飞狗跳,还有一堆学校其他杂事(搬家,做华为云实验)的一周之后,我们正式开始开发。

顶层

顶层被我们归类为最困难的工作,我们想了些取巧的办法让它稍微简单一点儿。这块儿是组里大佬写的,我也只是有所了解。

参考redbase的顶层(就算是斯坦福的课,顶层代码也是预先写好,不用学生写的),我们按照自己的需求写了一份语法文件,并用yacc(应该)生成分析代码。

底层

第二周折腾了老半天,把一份B+树代码折腾出来了,它还满足我们的要求(改泛型改了一个周末):能够存储int,float和string类型的数据(本来还有个要求是,一级索引将和纯数据文件放在一起,方便存取,但泛型这东西实在不是一个初学c++的同志能整的这么明白的,就没实现)

数据文件(初版)

这里给出示例文件框架

save

-table1 示例表名

–table1.data存放二进制数据文件

—-table1.meta存放表说明文件

—-IND//存放索引

——-table1//自增主键索引

—其他索引

字节二面记录

2022/4/2 字节二面记录

啸问题:开头以为只有暑假可以实习,直接被面试官拒了,然后被同学提醒才发现开学也能实习,紧急联系回来了

面试官还挺好的,居然还能继续面

问了些啥

最开始还是最难顶的自我介绍,感觉像是一个垃圾桶被要求介绍自己(x),我下次应该记住重复一下北理是个985,不然听起来像个野鸡大学(

和一面不同的是,这回自我介绍完了就开始做题,还问了python里头元组和字典的关系(答不出来,寄)

计网方面问了http请求一个页面的过程(上学期刚上完这学期就忘得差不多了属于是)

这回没问数据库,不知道是为啥

问了对软件项目管理的理解(?),我就当问我管理上的看法,感觉有点答非所问

哪里不行

对语言的理解比较烂,属于是只知道用不知道学

计网还是得看看http请求的全过程

其实数据库查询语句还有点欠缺,不过能用

React学习20220314 虚拟DOM

React学习20220314 虚拟DOM

虚拟DOM是什么

是JS和DOM之间的一个映射缓存,在形态上表现为一个能够描述DOM结构及其属性信息的JS对象。

在REACT中,表现为

image-20220314142105501

是JS对象,是对真实DOM的描述

如何工作

  • 挂载阶段:结合JSX的描述,构建出虚拟DOM树,然后通过ReactDOM.render实现虚拟DOM到真实DOM的映射
  • 更新阶段,页面的变化在作用于真实DOM之前,先作用于虚拟DOM,虚拟DOM在JS层借助算法先对比出那些真实DOM需要被改变,然后将改变作用于真实DOM

这一段历史讲得好有趣,摘下来

image-20220314142856250

image-20220314145559439

image-20220314145616729

可以看出,模板语法其实就是把 JS 和 HTML 结合在一起的一种规则,而模板引擎做的事情也非常容易理解。

把 staff 这个数据源读进去,塞到预置好的 HTML 模板里,然后把两者融合在一起,吐出一段目标字符串给你。这段字符串的内容,其实就是一份标准的、可用于渲染的 HTML 代码,它将对应一个 DOM 元素。最后,将这个 DOM 元素挂载到页面中去,整个模板的渲染流程也就走完了。

这个过程可以用伪代码来表示,如下所示:

// 数据和模板融合出 HTML 代码
var targetDOM = template({data: students})
// 添加到页面中去
document.body.appendChild(targetDOM)

当然,实际的过程会比我们描述的要复杂一些。这里我补充一下模板引擎的实现思路,供感兴趣的同学参考。模板引擎一般需要做下面几件事情:

  1. 读取 HTML 模板并解析它,分离出其中的 JS 信息;

  2. 将解析出的内容拼接成字符串,动态生成 JS 代码;

  3. 运行动态生成的 JS 代码,吐出“目标 HTML”;

  4. 将“目标 HTML”赋值给 innerHTML,触发渲染流水线,完成真实 DOM 的渲染。

使用模板引擎方案来渲染数据是非常爽的:每次数据发生变化时,我们都不用关心到底是哪里的数据变了,也不用手动去点对点完成 DOM 的修改。只需要关注的仅仅是数据和数据变化本身,DOM 层面的改变模板引擎会帮我们做掉。

如此看来,模板引擎像极了一个只需要接收命令,就能够把活干得漂漂亮亮的“扫地机器人”!可惜的是,模板引擎出现的契机虽然是为了使用户界面与业务数据相分离,但实际的应用场景基本局限在“实现高效的字符串拼接”这一个点上,因此不能指望它去做太复杂的事情。尤其令人无法接受的是,它在性能上的表现并不尽如人意:由于不够“智能”,它更新 DOM 的方式是将已经渲染出 DOM 整体注销后再整体重渲染,并且不存在更新缓冲这一说。在 DOM 操作频繁的场景下,模板引擎可能会直接导致页面卡死。

注:请注意小标题中“早期”这个限定词——本课时所讨论的“模板引擎”概念,指的是虚拟 DOM 思想推而广之以前,相对原始的一类模板引擎,这类模板引擎曾经主导了一个时代。但时下来看,越来越多的模板引擎正在引入虚拟 DOM,模板引擎最终也将走向现代化。

虽然指望模板引擎实现生产力解放有些天方夜谭,但模板引擎在思想上无疑具备高度的先进性:允许程序员只关心数据而不必关心 DOM 细节的这一操作,和 React 的“数据驱动视图”思想如出一辙,实在是高!

那该怎么办呢?

jQuery 救不了加班写 DOM 操作的前端,模板引擎也救不了,那该怎么办呢?

这时候有一批仁人志士,兴许是从模板引擎的设计思想上得到了启发,他们明确了要走“数据驱动视图”这条基本道路,于是便沿着这个思路往下摸索:模板引擎的数据驱动视图方案,核心问题在于对真实 DOM 的修改过于“大刀阔斧”,导致了 DOM 操作的范围过大、频率过高,进而可能会导致糟糕的性能。然后这帮人就想啊:既然操作真实 DOM 对性能损耗这么大,那我操作假的 DOM 不就行了?

沿着这个思路再往下走,就有了我们都爱的虚拟 DOM。

注:出于严谨,还是要解释下。真实历史中的虚拟 DOM 创作过程,到底有没有向模板引擎去学习,这个暂时无从考证。但是按照前端发展的过程来看,模板引擎和虚拟 DOM 确实在思想上存在递进关系,很多场景下,面试官也可能会问及两者的关系。因此在此处,我采取了这样一种表述方式,希望能够帮助你更好地把握住问题的关键所在。

虚拟DOM使得之前的全局刷新,改成了有更新的部分更新,在真实DOM前加了一层

image-20220314151706530

虚拟DOM解决的问题重心不是性能

image-20220314151854443

Reconciliation过程与Diff算法

Diff算法

找两个树结构之间的不同

两个规律

image-20220314152249841

image-20220314153544145

对于Key,下图可以比较好地展示使用了Key后的更新策略

image-20220314153745501

如果不使用key,则仅有AB可保留,其他均被重建

React学习20220314 SetState

React学习20220314 SetState

初始认知

setState之后立马访问对应state,会发现它并没有改变,而是会在之后某个时间发生变化

异步的动机和原理

图片3.png

异步避免重复reRender

image-20220314185248371

从源码角度看异步setState

image-20220314185446537

再查看batchingStrategy

image-20220314185810854

理解Transaction(事务)机制

image-20220314185913490

同步现象的本质

image-20220314190033221

React学习20220313 Hook

React学习20220313 Hook

理解Hook

按我的理解,Hook是对繁琐,学习曲线长,难以更改中间步骤的类的拆分,使得函数可以拥有一些类能够使用的功能:如state和生命周期等,而且Hook所辅助的函数式编程比较符合React所推崇的组件化编程。

对useState的理解

充当函数中的state,但一次只有一个,不像类中一次指定多个

[text,setText] =useState(‘初始文字’)

useState返回一个数组,数组第一个是想要的state变量,第二个是修改变量的Api

对useEffect的理解

弥补生命周期

接收两个参数:回调函数和依赖数组

useEffect(callBack,[])

调用规则:

每次渲染后:传入回调函数,不传依赖数组:

  • useEffect(callBack)

挂载阶段执行一次后不再执行:传入回调函数,且此函数的返回值不是函数,同时传入空数组。

  • useEffect(()=>{/*业务逻辑*/},[])

仅在挂载和卸载阶段执行的:传入回调函数,且此函数的返回值是一个函数

1
2
3
4
5
6
useEffect(()=>{
//业务逻辑
//返回一个函数记为B
return ()=>{
}
},[])

每次渲染都触发,且卸载阶段也会被触发的:传入回调函数,且这个函数的返回值是一个函数,同时不传第二个参数

1
2
3
4
5
6
7
8
useEffect(()=>{
//A的业务逻辑

//返回一个函数记为B
return ()=>{

}
})

上面这段代码在每次渲染都触发A逻辑,并在卸载阶段触发B逻辑

按我的理解,在前面一半写个函数会在渲染阶段一直触发,单纯写一段逻辑就会在挂载阶段触发,在return那写个函数就会在卸载的时候触发

image-20220313160121838

不要在循环,条件或嵌套函数中调用Hook

首次渲染过程

image-20220314135945234

hook相关的所有信息收敛在一个hook对象粒,而hook对象之间以单向链表的形式相互串联,

更新过程

image-20220314140737526

因此,hooks的渲染是通过“依次遍历”(也就是说,它只会管这回需要前进几个next,而不会对对应位置的真实性进行判断)来定位每个hooks的内容的,如果前后两次读到的链表顺序出现差异,那么渲染的结果自然是不可控的。