一、html
1、html语义化
定义:语义化是根据内容结构化(内容语义化),选择合适的标签(代码语义化),便于开发者阅读和写出更优雅代码的同时,让浏览器的爬虫和机器更好的解析
作用:
1、有利于SEO,有助于爬虫抓取更多的有效信息,爬虫是依赖于标签来确定上下文和各个关键字的权重
2、语义化的HTML在没有CSS的情况下也能呈现较好的内容结构与代码结构
3、方便其他设备的解析
4、便于团队开发和维护
注意:
1、尽可能少的使用无语义的标签 div 和 span
2、在语义不明显时,既可以使用 div 或者 p 时,尽量用 p, 因为 p 在默认情况下有上下间距,对兼容 特殊终端有利
3、不要使用纯样式标签,如:b、font、u 等,改用 css 设置
4、需要强调的文本,可以包含在 strong 或者 em 标签中(浏览器预设样式,能用CSS 指定就不用他 们),strong 默认样式是加粗(不要用 b),em 是斜体(不用i)
5、使用表格时,标题要用 caption,表头用 thead,主体部分用 tbody 包围,尾部用 tfoot 包围。表 头和一般单元格要区分开,表头用 th,单元格用 td
6、表单域要用 fieldset 标签包起来,并用 legend 标签说明表单的用途
7、每个 input 标签对应的说明文本都需要使用 label 标签,并且通过为 input 设置id 属性,在 lable 标签中设置 for=someld 来让说明文本和相对应的 input 关联起来
html5新标签:
1、article: 定义文档内的文章
2、footer: 页脚
3、header: 页眉
4、section: 定义文档中的章节
5、dialog: 定义对话框或窗口
6、canvas: 可被用来通过JavaScript(Canvas API 或 WebGL API)绘制图形及图形动画
2、canvas相关
使用前需先获得上下文环境,暂不支持3d
常用api:
1、fillRect(x, y, width, height)实心矩形
2、strokeRect(x, y, width, height)空心矩形
3、fillText('杀死那个河南人',200, 200)实心文字
4、strokeRect('杀死那个河南人', 200, 200)空心文字
新版本兼容低版本:
1、ie9 之前版本通过 createElement 创建 html5 新标签
2、引入 html5shiv.js
3、svg和canvas的区别
1、canvas是h5提供的新的绘图方法
2、svg已经有了十多年的历史 canvas画图基于像素点,是位图,如果进行放大或缩小会失真
3、svg基于图形,用html标签描绘形状,放大缩小不会失真
4、canvas需要在js中绘制,svg在html绘制
5、canvas支持颜色比svg多
6、canvas无法对已经绘制的图像进行修改、操作,svg可以获取到标签进行操作
4、html5新特性
HTML5主要是关于图像,位置,存储,多任务等功能的增加。
1、拖拽释放(Drag and drop) API
2、语义化更好的内容标签(header,nav,footer,aside,article,section) 音频、视频API(audio,video)
3、画布(Canvas) API
4、地理(Geolocation) API
5、本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失; sessionStorage 的数据在浏览器关闭后自动删除
6、表单控件,calendar、date、time、email、url、search
5、如何处理HTML5新标签的浏览器兼容问题
IE8/IE7/IE6支持通过document.createElement方法产生的标签,可以利用这一特性让这些浏览器支持 HTML5新标签,当然最好的方式是直接使用成熟的框架、使用最多的是html5shim框架
6、说说 title 和 alt 属性
1、两个属性都是当鼠标滑动到元素上的时候显示
2、alt 是 img 的特有属性,是图片内容的等价描述,图片无法正常显示时候的替代文字。
3、title 属性可以用在除了base,basefont,head,html,meta,param,script和title之外的所有标签,是对dom元素的一种类似注释说明
7、HTML全局属性(global attribute)有哪些
class :为元素设置类标识
data-* : 为元素增加自定义属性
draggable : 设置元素是否可拖拽
id : 元素 id ,文档内唯一
lang : 元素内容的的语言
style : 行内 css 样式
title : 元素相关的建议信息
8、clientWidth, offsetWidth, scrollWidth区别
二、CSS
1、让一个元素水平垂直居中,到底有多少种方案?
2、浮动布局的优点?有什么缺点?清除浮动有哪些方式?
浮动布局简介
当元素浮动以后可以向左或向右移动,直到它的外边缘碰到包含它的框或者另外一 个浮动元素的边框为止。元素浮动以后会脱离正常的文档流,所以文档的普通流中的框就变现的好 像浮动元素不存在一样。
优点
这样做的优点就是在图文混排的时候可以很好的使文字环绕在图片周围,因为浮动元素会脱离网页文档,与其他元素发生重叠,但是,不会与文字内容发生重叠。另外当元素浮动了起来之后, 它有着块级元素的一些性质例如可以设置宽高等,但它与inline-block还是有一些区别的,第一个就是关 于横向排序的时候,float可以设置方向而inline-block方向是固定的;还有一个就是inline-block在使用 时有时会有空白间隙的问题
缺点
最明显的缺点就是浮动元素一旦脱离了文档流,就无法撑起父元素,会造成父级元素高度塌陷。
清除浮动的方式
1)添加额外标签
<div class="parent"> //添加额外标签并且添加clear属性
<div style="clear:both"></div> //也可以加一个br标签
</div>
2)父级添加overflow属性,或者设置高度
<div class="parent" style="overflow:hidden">//auto 也可以 //将父元素的overflow设置为hidden
<div class="f"></div>
</div>
3)建立伪类选择器清除浮动(推荐)
//在css中添加:after伪元素
.parent:after {
/* 设置添加子元素的内容是空 */
content: '';
/* 设置添加子元素为块级元素 */
display: block;
/* 设置添加的子元素的高度0 */
height: 0;
/* 设置添加子元素看不见 */
visibility: hidden;
/* 设置clear:both */
clear: both;
}
<div class="parent">
<div class="f"></div>
</div>
3、使用display:inline-block会产生什么问题?解决方法?
问题复现
问题: 两个display:inline-block元素放到一起会产生一段空白。
如代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title>
<style>
.container { width: 800px; height: 200px;
}
.left {
font-size: 14px; background: red; display: inline-block; width: 100px;
height: 100px;
}
.right {
font-size: 14px; background: blue; display: inline-block; width: 100px;
height: 100px;
}
</style>
</head>
<body>
<div class="container">
<div class="left">
左
</div><div class="right"> 右
</div>
</div>
<div class="container">
<div class="left">
左
</div>
<div class="right">
右
</div>
</div>
</body>
</html>
效果如下:
产生空白的原因
元素被当成行内元素排版的时候,元素之间的空白符(空格、回车换行等)都会被浏览器处理,根据 CSS中white-space属性的处理方式(默认是normal,合并多余空白),原来HTML代码中的回车换行被 转成一个空白符,在字体不为0的情况下,空白符占据一定宽度,所以inline-block的元素之间就出现了空隙。
解决办法
1)将子元素标签的结束符和下一个标签的开始符写在同一行或把所有子标签写在同一行
<div class="container">
<div class="left">
左
</div><div class="right"> 右
</div>
</div>
2)父元素中设置font-size: 0,在子元素上重置正确的font-size
.container{ width:800px; height:200px; font-size: 0;}
3)为子元素设置float:left
.left{
float: left; font-size: 14px; background: red; display: inline-block; width: 100px;
height: 100px;
} //right是同理
4、布局题:div垂直居中,左右10px,高度始终为宽度一半
问题描述: 实现一个div垂直居中, 其距离屏幕左右两边各10px, 其高度始终是宽度的50%。同时div 中有一个文字A,文字需要水平垂直居中。
思路一:利用height:0; padding-bottom: 50%;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
width: 100%;
}
.outer_wrapper {
margin: 0 10px;
height: 100%;
/* flex布局让块垂直居中 */
display: flex;
align-items: center;
}
.inner_wrapper {
background: red;
position: relative;
width: 100%;
height: 0;
padding-bottom: 50%;
}
.box {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
}
</style>
</head>
<body>
<div class="outer_wrapper">
<div class="inner_wrapper">
<div class="box">A</div>
</div>
</div>
</body>
</html>
强调两点:
1)padding-bottom究竟是相对于谁的?
答案是相对于 父元素的width值 。 那么对于这个out_wrapper的用意就很好理解了。 CSS呈流式布局,div默认宽度填满,即100%大小, 给out_wrapper设置margin: 0 10px;相当于让左右分别减少了10px。
2)父元素相对定位,那绝对定位下的子元素宽高若设为百分比,是相对谁而言的?
相对于父元素的(content + padding)值, 注意不含border 延伸:如果子元素不是绝对定位,那宽高设为百分比是相对于父元素的宽高,标准盒模型下是 content, IE盒模型是content+padding+border。
思路二: 利用calc和vw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
* {
padding: 0;
margin: 0;
}
html,
body {
width: 100%;
height: 100%;
}
.wrapper {
position: relative;
width: 100%;
height: 100%;
}
.box {
margin-left: 10px;
/* vw是视口的宽度, 1vw代表1%的视口宽度 */
width: calc(100vw - 20px);
/* 宽度的一半 */
height: calc(50vw - 10px);
position: absolute;
background: red;
/* 下面两行让块垂直居中 */
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="box">A</div>
</div>
</body>
</html>
5、怎样让div等块级元素宽度自适应文字宽度
方法一: width: fit-content;
方法二: display: inline-block;
6、介绍下 BFC 及其应用
BFC 就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于一个独立的容器,里面的元素和外部的元素相互不影响。创建 BFC 的方式有:
- html 根元素
- float 浮动
- 绝对定位
- overflow 不为 visiable
- display 为表格布局或者弹性布局
BFC 主要的作用是:
- 清除浮动
- 防止同一 BFC 容器中的相邻元素间的外边距重叠问题
- 防止父元素高度塌陷
7、盒模型和bfc
8、css预处理
sass和less等,可以丰富css的功能
- 文件切分:改进css的@import指令,将@import分割的文件再合成一个大文件,解决加载过多小文件以及大文件不方便维护问题
- 模块化
- 选择器嵌套:结构更加清晰
- 变量:可以让重复出现的数值统一取个名字,也具有含义
- 运算
- 函数
- mixin:可以封装类似的css代码,节省代码
postcss既可以预处理也可以后处理,一般用作后处理,就是把css编译成ast,再运行Postcss的插件进行重新编译,做一些处理
常用插件
- autoPrefix:添加浏览器前缀
- precss: 语法几乎和sass及less相同
- cssnano: 压缩css文件
- css-mqpacker: 合并相同的媒体查询
- postcss-import: 通过@import,整合多个css文件
9、css优化、提高性能的方法
主要分为以下四个方面:
加载性能
- 压缩css文件,比如用gzip压缩
- 使用link而不使用@import, link是页面加载同时被加载,@import引用的css事页面加载完再加载
- css单一样式,比如margin-bottom: 1px solid red的加载速度优于margin: 1px solid red
选择器性能
- 避免使用通配符,比如*{}
- 尽量少用标签选择,标签会遍历元素,比如div{},尽量用类名
- id选择器前面就不需要其他选择器了,因为id选择器是唯一的,比如div #hhh{},直接用#hhh
- 后代选择器不要超过3层,选择器是从右往左的,浏览器会遍历所有子元素
- 多用继承,能从父元素继承的属性,就不用再写了
渲染性能
- 高性能属性:浮动、定位
- 减少重排重绘
- 属性为0时不加单位
- 属性值为浮动小数0.xx,可以省略小数点之前的0
- 抽象提取公共样式,减少代码量
- css雪碧图
- style放在head里,不要放在body后面
10、响应式布局
响应式布局是为了适应不同设备展示不同页面效果
- 响应式和自适应不同点就在于,自适应是缩放,改变大小,不改变布局,响应式可能布局都会改变,比如三栏变两栏
属性设置
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" />
属性对应如下:
- width=device-width: 是自适应手机屏幕的尺寸宽度
- maximum-scale:是缩放比例的最大值
- inital-scale:是缩放的初始化
- user-scalable:是用户的可以缩放的操作
实现响应式布局的方式有如下:
- 媒体查询
移动优先 OR PC优先
不管是移动优先还是PC优先,都是依据当随着屏幕宽度增大或减小的时候,后面的样式会覆盖前面的样式。因此,移动端优先首先使用的是min-width
,PC端优先使用的max-width
。
移动优先:
/* iphone6 7 8 */
body {
background-color: yellow;
}
/* iphone 5 */
@media screen and (max-width: 320px) {
body {
background-color: red;
}
}
/* iphoneX */
@media screen and (min-width: 375px) and (-webkit-device-pixel-ratio: 3) {
body {
background-color: #0FF000;
}
}
/* iphone6 7 8 plus */
@media screen and (min-width: 414px) {
body {
background-color: blue;
}
}
/* ipad */
@media screen and (min-width: 768px) {
body {
background-color: green;
}
}
/* ipad pro */
@media screen and (min-width: 1024px) {
body {
background-color: #FF00FF;
}
}
/* pc */
@media screen and (min-width: 1100px) {
body {
background-color: black;
}
}
复制代码
PC优先:
/* pc width > 1024px */
body {
background-color: yellow;
}
/* ipad pro */
@media screen and (max-width: 1024px) {
body {
background-color: #FF00FF;
}
}
/* ipad */
@media screen and (max-width: 768px) {
body {
background-color: green;
}
}
/* iphone6 7 8 plus */
@media screen and (max-width: 414px) {
body {
background-color: blue;
}
}
/* iphoneX */
@media screen and (max-width: 375px) and (-webkit-device-pixel-ratio: 3) {
body {
background-color: #0FF000;
}
}
/* iphone6 7 8 */
@media screen and (max-width: 375px) and (-webkit-device-pixel-ratio: 2) {
body {
background-color: #0FF000;
}
}
/* iphone5 */
@media screen and (max-width: 320px) {
body {
background-color: #0FF000;
}
}
-
百分比
-
vw/vh 虽然采用
vw
适配后的页面效果很好,但是它是利用视口单位实现的布局,依赖视口大小而自动缩放,无论视口过大还是过小,它也随着时候过大或者过小,失去了最大最小宽度的限制,此时我们可以结合rem
来实现布局-
给根元素大小设置随着视口变化而变化的
vw
单位,这样就可以实现动态改变其大小 -
限制根元素字体大小的最大最小值,配合
body
加上最大宽度和最小宽度
// rem 单位换算:定为 75px 只是方便运算,750px-75px、640-64px、1080px-108px,如此类推 $vm_fontsize: 75; // iPhone 6尺寸的根元素大小基准值 @function rem($px) { @return ($px / $vm_fontsize ) * 1rem; } // 根元素大小使用 vw 单位 $vm_design: 750; html { font-size: ($vm_fontsize / ($vm_design / 2)) * 100vw; // 同时,通过Media Queries 限制根元素最大最小值 @media screen and (max-width: 320px) { font-size: 64px; } @media screen and (min-width: 540px) { font-size: 108px; } } // body 也增加最大最小宽度限制,避免默认100%宽度的 block 元素跟随 body 而过大过小 body { max-width: 540px; min-width: 320px; }
-
-
rem/em
REM
是CSS3
新增的单位,并且移动端的支持度很高,Android2.x+,ios5+都支持。rem
单位都是相对于根元素html的font-size
来决定大小的,根元素的font-size
相当于提供了一个基准,当页面的size发生变化时,只需要改变font-size
的值,那么以rem
为固定单位的元素的大小也会发生响应的变化。 因此,如果通过rem
来实现响应式的布局,只需要根据视图容器的大小,动态的改变font-size
即可(而em
是相对于父元素的)
11、rem怎么设置
rem 是相对文档根元素(html)字体大小的尺寸单位,当元素的尺寸或文字字号等使用 rem 单位时,会随着根元素的 font-size变化而变化,那么在不同分辨率的设备下动态设置根元素的字体大小就可以实现页面自适应。那么如何动态设置呢,看到很多文章都讲的是使用js获取设备屏幕尺寸来操作,而我在工作中一直使用的方法是通过设置文档根元素 font-size: calc(100vw/18.75)来实现 rem 自适应。
以常见的750px的设计稿为例,如果想要规定1rem = 40px(基准值,后面会讲到),便是设置 html { font-size: calc(100vw / 18.75) }
其中,100vw是设备视口的总宽度,当设备的宽与设计稿的等宽时,则此时有:
100vw = 750px => 1px = 100vw / 750
如果设置基准值为 1px,则 1rem = 1px,代码则是 html { font-size: calc(100vw / 750) }
但是一般不会设置为1px,而是设置10px、20px或者40px,即1 rem等于10px、20px 或者 40px ,以 1rem = 40px为例,则此时有:
40px = 40 * 100vw / 750 => 40px = 100vw / (750 /40) => 40px = 100vw / 18.75 即 1rem = 100vw / 18.75,所以设置 根元素字体大小为 calc(100vw / 18.75) 即可。
现在 750px的设计稿中有一个400*200的div元素,我们在样式中如果写
div {
width: 400px;
height: 200px;
}
那么无论设备的尺寸如何,这个元素的宽高始终都是固定的400*200,缺少灵活性,所以需要使用 rem 单位做自适应,现在设置文档根元素字体大小为 calc(100vw / 18.75) },即 文档中 1rem = 100vw / 18.75 ,
div {
width: 10rem;
height: 5rem;
}
当屏幕尺寸为 750px时,该元素的宽为:
10rem = 10 * 100vw /18.75
=>10rem = 10 * 750px / 18.75
=>10rem = 10 * 750px / (750 / 40)
=>10rem = 400px
当屏幕尺寸为 375px时,该元素的宽为:
10rem = 10 * 100vw / 18.75
=>10rem = 10 * 375px / (750 / 40)
=>10rem = 200px
可以看到,元素尺寸随着设备尺寸的变化,同时发生了变化,这就是 rem 自适应的能力。
12、实现文字跑马灯效果
最简方法:
<View className={styles.tips}>
<View>
{Array(2).fill(<Text>以上这些比较,更多的是对于框架开发研究者提供一些参考。主流的框架 + 合理的优化,足以应对绝大部分应用的性能需求。如果是对性能有极</Text>)}
</View>
</View>
@keyframes marqueeText {
0% {
transform: translateX(0%);
-webkit-transform: translateX(0%);
}
100% {
transform: translateX(-50%);
-webkit-transform: translateX(-50%);
}
}
.tips {
overflow: hidden;
height: 20px;
position: relative;
width: 278px;
line-height: 0;
&>view {
position: absolute;
top: 0;
animation: marqueeText 18s linear 1s infinite;
white-space: nowrap;
&>text {
line-height: 20px;
font-family: PingFangSC-Regular;
font-size: 14px;
color: #48AC0D;
font-weight: 400;
padding-right: 139px;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
}
}
核心思想就是让最后一帧的画面和第一帧一致
13、图片自适应
- 使用max-width, height: auto
- 使用img标签的secret属性,决定使用几倍图
- 使用@media,结合background-image决定使用哪张图
- 使用picture标签
picturefill.min.js :解决IE等浏览器不支持 的问题
<picture>
<source srcset="banner_w1000.jpg" media="(min-width: 801px)">
<source srcset="banner_w800.jpg" media="(max-width: 800px)">
<img src="banner_w800.jpg" alt="">
</picture>
<!-- picturefill.min.js 解决IE等浏览器不支持 <picture> 的问题 -->
<script type="text/javascript" src="js/vendor/picturefill.min.js"></script>
14、等高布局
- border模拟,利用border和内容高度一致
- 利用absolute设置top为0,bottom为0
- flex布局,align-items为stretch
- grid布局
- table布局,table-cell天然等高
- 负margin, padding-bottom为99999px,margin-bottom为-99999px
15、可继承属性
- 文字类
font-size, font-weight, font-family, font-style, color - 文本类 line-height, text-align, word-spacing, letter-spacing
- 可见性
visiblity - 光标属性
cursor - 表格布局属性 border-collapse,border-sapcing, empty-cells, table-layout
16、css3新特性
transform, transition, animation, box-shadow, text-shadow, text-overflow, gradient, @media, border-radius, box-sizing, word-break
17、flex:1含义
语法 | 等值 |
---|---|
flex: initial | flex: 0 1 auto |
flex: 0 | flex: 0 1 0% |
flex: none | flex: 0 0 auto |
flex: 1 | flex: 1 1 0% |
flex: auto | flex: 1 1 auto |
三、javascript
1、状态机
flux框架
redux
和 Flux 比较一下:Flux 中 Store 是各自为战的,每个 Store 只对对应的 View 负责,每次更新都只通知对应的View:
Redux 中各子 Reducer 都是由根 Reducer 统一管理的,每个子 Reducer 的变化都要经过根 Reducer 的整合:
简单来说,Redux有三大原则: 单一数据源:Flux 的数据源可以是多个。 State 是只读的:Flux 的 State 可以随便改。 * 使用纯函数来执行修改:Flux 执行修改的不一定是纯函数。
Redux 和 Flux 一样都是单向数据流。
2、node和浏览器事件循环的区别
浏览器和Node 环境下,microtask 任务队列的执行时机不同
- Node端,microtask 在事件循环的各个阶段之间执行
- 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
3、cookie 和 token 都存放在 header 中,为什么不会劫持 token?
cookie:登陆后后端生成一个sessionid放在cookie中返回给客户端,并且服务端一直记录着这个sessionid,客户端以后每次请求都会带上这个sessionid,服务端通过这个sessionid来验证身份之类的操作。所以别人拿到了cookie拿到了sessionid后,就可以完全替代你。
token:登陆后后端不返回一个token给客户端,客户端将这个token存储起来,然后每次客户端请求都需要开发者手动将token放在header中带过去,服务端每次只需要对这个token进行验证就能使用token中的信息来进行下一步操作了。
xss:用户通过各种方式将恶意代码注入到其他用户的页面中。就可以通过脚本获取信息,发起请求,之类的操作。
csrf:跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。csrf并不能够拿到用户的任何信息,它只是欺骗用户浏览器,让其以用户的名义进行操作。
csrf例子:假如一家银行用以运行转账操作的URL地址如下: www.examplebank.com/withdraw?ac…
那么,一个恶意攻击者可以在另一个网站上放置如下代码:<img src="<http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman>">
如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。
上面的两种攻击方式,如果被xss攻击了,不管是token还是cookie,都能被拿到,所以对于xss攻击来说,cookie和token没有什么区别。但是对于csrf来说就有区别了。
以上面的csrf攻击为例:
- cookie:用户点击了链接,cookie未失效,导致发起请求后后端以为是用户正常操作,于是进行扣款操作。
- token:用户点击链接,由于浏览器不会自动带上token,所以即使发了请求,后端的token验证不会通过,所以不会进行扣款操作。
4、改造下面的代码,使之输出0 - 9,写出你能想到的所有解法。
for (var i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}
解法一
for (let i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}
解法二
for (let i = 0; i< 10; i++){
(i) => {
setTimeout(() => {
console.log(i);
}, 1000)))}(i)
}
3、浏览器缓存方案
cache-control控制缓存图解:
缓存机制流程图:
4、判断是否为数组
首先大忌是用typeof判断,因为typeof []是object 方法一: instance of 如果这个Object的原型链上能够找到Array构造函数的话,那么这个Object应该及就是一个数组,如果这个Object的原型链上只能找到Object构造函数的话,那么它就不是一个数组。
const a = [];
const b = {};
console.log(a instanceof Array);//true
console.log(a instanceof Object);//true,在数组的原型链上也能找到Object构造函数
console.log(b instanceof Array);//false
方法二: constructor 实例化的数组拥有一个constructor属性,这个属性指向生成这个数组的方法。
const a = [];
console.log(a.constructor);//function Array(){ [native code] }
以上的代码说明,数组是有一个叫Array的函数实例化的。
如果被判断的对象是其他的数据类型的话,结果如下:
const o = {};
console.log(o.constructor);//function Object(){ [native code] }
const r = /^[0-9]$/;
console.log(r.constructor);//function RegExp() { [native code] }
const n = null;
console.log(n.constructor);//报错
看到这里,你可能会觉得这也是一种靠谱的判断数组的方法,我们可以用以下的方式来判断:
const a = [];
console.log(a.constructor == Array);//true
但是,很遗憾的通知你,constructor属性是可以改写的,如果你一不小心作死改了constructor属性的话,那么使用这种方法就无法判断出数组的真是身份了,写到这里,我不禁想起了无间道的那段经典对白,梁朝伟:“对不起,我是警察。”刘德华:“谁知道呢?”。
//定义一个数组
const a = [];
//作死将constructor属性改成了别的
a.contrtuctor = Object;
console.log(a.constructor == Array);//false (哭脸)
console.log(a.constructor == Object);//true (哭脸)
console.log(a instanceof Array);//true (instanceof火眼金睛)
可以看出,constructor属性被修改之后,就无法用这个方法判断数组是数组了,除非你能保证不会发生constructor属性被改写的情况,否则用这种方法来判断数组也是不靠谱的。
方法三: 用Object的toString方法判断 另一个行之有效的方法就是使用Object.prototype.toString方法来判断,每一个继承自Object的对象都拥有toString的方法。
如果一个对象的toString方法没有被重写过的话,那么toString方法将会返回"[object type]",其中的type代表的是对象的类型,根据type的值,我们就可以判断这个疑似数组的对象到底是不是数组了。
你可能会纠结,为什么不是直接调用数组,或则字符串自己的的toString方法呢?我们试一试就知道了。
const a = ['Hello','Howard'];
const b = {0:'Hello',1:'Howard'};
const c = 'Hello Howard';
a.toString();//"Hello,Howard"
b.toString();//"[object Object]"
c.toString();//"Hello,Howard"
方法四: 用Array对象的isArray方法判断 是es5新增方法,可能会有兼容性问题
4、介绍下 BFC 及其应用
BFC 就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于一个独立的容器,里面的元素和外部的元素相互不影响。创建 BFC 的方式有:
- html 根元素
- float 浮动
- 绝对定位
- overflow 不为 visiable
- display 为表格布局或者弹性布局
BFC 主要的作用是:
- 清除浮动
- 防止同一 BFC 容器中的相邻元素间的外边距重叠问题
5、实现一个 sleep 函数,比如 sleep(1000) 意味着等待1000毫秒,可从 Promise、Generator、Async/Await 等角度实现
//Promise
const sleep = time => {
return new Promise(resolve => setTimeout(resolve,time))
}
sleep(1000).then(()=>{
console.log(1)
})
//Generator
function* sleepGenerator(time) {
yield new Promise(function(resolve,reject){
setTimeout(resolve,time);
})
}
sleepGenerator(1000).next().value.then(()=>{console.log(1)})
//async
function sleep(time) {
return new Promise(resolve => setTimeout(resolve,time))
}
async function output() {
let out = await sleep(1000);
console.log(1);
return out;
}
output();
//ES5
function sleep(callback,time) {
if(typeof callback === 'function')
setTimeout(callback,time)
}
function output(){
console.log(1);
}
sleep(output,1000);
6、介绍 HTTPS 握手过程
- 客户端使用https的url访问web服务器,要求与服务器建立ssl连接
- web服务器收到客户端请求后, 会将网站的证书(包含公钥)传送一份给客户端
- 客户端收到网站证书后会检查证书的颁发机构以及过期时间, 如果没有问题就随机产生一个秘钥
- 客户端利用公钥将会话秘钥加密, 并传送给服务端, 服务端利用自己的私钥解密出会话秘钥
- 之后服务器与客户端使用秘钥加密传输
7、HTTPS 握手过程中,客户端如何验证证书的合法性
(1)首先浏览器读取证书中的证书所有者、有效期等信息进行校验,校验证书的网站域名是否与证书颁发的域名一致,校验证书是否在有效期内
(2)浏览器开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁发
(3)如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的。
(4)如果找到,那么浏览器就会从操作系统中取出颁发者CA 的公钥(多数浏览器开发商发布
版本时,会事先在内部植入常用认证机关的公开密钥),然后对服务器发来的证书里面的签名进行解密
(5)浏览器使用相同的hash算法计算出服务器发来的证书的hash值,将这个计算的hash值与证书中签名做对比
8、call 和 apply 的区别是什么,哪个性能更好一些
两者区别在参数传递格式:
apply( ):两个参数,第一个是运行函数的作用域,第二个是参数数组(可以是array的实例,或者arguments对象)。
call( ):参数个数不定,第一个是运行函数的作用域,其余传递给函数的参数逐个列出。
apply()和 call()的2个作用:给函数传参、扩充作用域;
call 比 apply 的性能好, 我的理解是内部少了一次将 apply 第二个参数解构的操作
apply、call实例
数组之间追加
var array1 = [12 , "foo" , {name:"Joe"} , -2458];
var array2 = ["Doe" , 555 , 100];
Array.prototype.push.apply(array1, array2);
// array1 值为 [12 , "foo" , {name:"Joe"} , -2458 , "Doe" , 555 , 100]
获取数组中的最大值和最小值
var numbers = [5, 458 , 120 , -215 ];
var maxInNumbers = Math.max.apply(Math, numbers), //458
maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //458
number 本身没有 max 方法,但是 Math 有,我们就可以借助 call 或者 apply 使用其方法。
验证是否是数组(前提是toString()
方法没有被重写过)
functionisArray(obj){
return Object.prototype.toString.call(obj) === '[object Array]' ;
}
类(伪)数组使用数组方法
var domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
Javascript中存在一种名为伪数组的对象结构。比较特别的是 arguments
对象,还有像调用 getElementsByTagName
, document.childNodes
之类的,它们返回NodeList
对象都属于伪数组。不能应用 Array下的 push
, pop
等方法。
但是我们能通过 Array.prototype.slice.call
转换为真正的数组的带有 length
属性的对象,这样 domNodes
就可以应用 Array 下的所有方法了。
面试题
定义一个 log
方法,让它可以代理 console.log
方法,常见的解决方法是:
function log(msg) {
console.log(msg);
}
log(1); //1
log(1,2); //1
上面方法可以解决最基本的需求,但是当传入参数的个数是不确定的时候,上面的方法就失效了,这个时候就可以考虑使用 apply
或者 call
,注意这里传入多少个参数是不确定的,所以使用apply
是最好的,方法如下:
function log(){
console.log.apply(console, arguments);
};
log(1); //1
log(1,2); //1 2
接下来的要求是给每一个 log
消息添加一个"(app)"的前辍,比如:
log("hello world"); //(app)hello world
该怎么做比较优雅呢?这个时候需要想到arguments
参数是个伪数组,通过 Array.prototype.slice.call
转化为标准数组,再使用数组方法unshift
,像这样:
function log(){
var args = Array.prototype.slice.call(arguments);
args.unshift('(app)');
console.log.apply(console, args);
};
9、为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?
- 能够完成整个 HTTP 请求+响应(尽管不需要响应内容)
- 触发 GET 请求之后不需要获取和处理数据、服务器也不需要发送数据
- 跨域友好
- 执行过程无阻塞
- 相比 XMLHttpRequest 对象发送 GET 请求,性能上更好
- GIF的最低合法体积最小(最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节)
10、实现 (5).add(3).minus(2) 功能
Number.prototype.add = function(n) {
return this.valueOf() + n;
};
Number.prototype.minus = function(n) {
return this.valueOf() - n;
};
11、跨域解决方案
www.chuchur.com/article/web… 1)jsonp 2)document.domain + iframe 跨域 3)location.hash + iframe 跨域 4)window.name + iframe 跨域 5)postMessage 跨域 6)跨域资源共享(CORS) 7)nginx 反向代理跨域 8)Nodejs 中间件代理跨域 9)WebSocket 协议跨域
12、防抖节流手写
防抖和节流函数其实就一句代码不一样
防抖
// 防抖
function debounce(cb, delay) {
var timer = null
return function() {
var _this = this
var args = arguments
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(function() {
cb.apply(_this, args)
timer = null
}, delay)
}
}
节流
// 防抖
function debounce(cb, delay) {
var timer = null
return function() {
var _this = this
var args = arguments
if (timer) return
timer = setTimeout(function() {
cb.apply(_this, args)
timer = null
}, delay)
}
}
13、类数组转变为数组
类数组:a = document.getElementsByTagName('*') 1、使用slice方法,call方法 b = Array.prototype.slice.call(a) 2、使用数组解构方法 b = [...a]
14、手写深拷贝
15、保证函数只执行一次
使用高阶函数+闭包
function once(fn) {
let done = false
return function() {
if (!done) {
fn.apply(this, arguments)
} else {
console.log('已经执行过了')
}
done = true
}
}
// 举例
a = once((b) => console.log(b + '实现过了'))
a(1)
a(2)
a(3)
16、javascript 实现一个带并发限制的异步调度器,保证同时最多运行2个任务
17、函数科里化手写
function curry(fn) {
return function curryFn(...args) {
if (args.length < fn.length) {
return function() {
return curryFn(...args.concat(Array.from(arguments)))
}
}
return fn(...args)
}
}
let a = (b,c,d,e) => b+c+d+e
let b = curry(a)
console.log(b(1)(2)(3)(4))
18、前端内存泄漏常见原因
1、全局变量 2、闭包 3、dom引用 4、定时器和回调函数
19、手写bind
Function.prototype.myBind = function(context) {
if (typeof this !== 'function') return
const _args = Array.prototype.slice.call(arguments, 1)
const fn = this
function fNon() {}
function fBound() {
const args = Array.prototype.slice.call(arguments)
return fn.apply(this instanceof fNon ? this : context, _args.concat(args))
}
fNon.prototype = fn.prototype
fBound.prototype = new fNon()
return fBound
}
20、手写new
21、生成随机颜色
`#${(Math.random() * 0xffffff).toString(16)}`
22、小数用二进制怎么表示
1. 小数用二进制如何表示
首先,给出一个任意实数,整数部分用普通的二进制便可以表示,这里只说小数部分如何表示
例如0.6
文字描述该过程如下:将该数字乘以2,取出整数部分作为二进制表示的第1位;然后再将小数部分乘以2,将得到的整数部分作为二进制表示的第2位;以此类推,知道小数部分为0。
特殊情况: 小数部分出现循环,无法停止,则用有限的二进制位无法准确表示一个小数,这也是在编程语言中表示小数会出现误差的原因
下面我们具体计算一下0.6的小数表示过程
0.6 * 2 = 1.2 ——————- 1
0.2 * 2 = 0.4 ——————- 0
0.4 * 2 = 0.8 ——————- 0
0.8 * 2 = 1.6 ——————- 1
0.6 * 2 = 1.2 ——————- 1
…………
我们可以发现在该计算中已经出现了循环,0.6用二进制表示为 1001 1001 1001 1001 ……
如果是10.6,那个10.6的完整二进制表示为 1010.100110011001……
2. 二进制表示的小数如何转换为十进制
其实这个问题很简单,我们再拿0.6的二进制表示举例:1001 1001 1001 1001
文字描述:从左到右,v[i] * 2^( - i ), i 为从左到右的index,v[i]为该位的值,直接看例子,很直接的
0.6 = 1 * 2^-1 + 0 * 2^-2 + 0 * 2^-3 + 1 * 2^-4 + ……
23、前端实现继承的几种方式
/* 原型链继承
** 缺点: 子类实例可以修改父类属性,造成其他子类实例同步被修改
*/
function father() {
this.cat = ['深深'];
}
father.prototype = {
catName: function () {
console.log(this.cat);
}
};
function son() {
this.bark = function () {
console.log(this.cat + "在狂叫");
};
this.addCat = function (name) {
this.cat.push(name);
};
}
son.prototype = new father();
var instance = new son();
instance.addCat("浅浅");
instance.bark();
// 缺点示例
var _instance = new son();
_instance.bark(); // "深深,浅浅在狂叫"
24、prototype, proto, constructor关系
__proto__是隐式原型,是实例的原型 prototype是构造函数的原型
function A() {
this.name = "深深";
}
var a = new A();
console.log(a.__proto__ === A.prototype); // true
console.log(a.__proto__.__proto__ === Object.prototype); // true
console.log(A.prototype.constructor === A); // true
尽量不要用__proto__方式获取原型上的属性,es6提供了新方法getPrototypeOf获取实例对象原型上的属性,调用方式: Object.getPrototypeOf(a)
25、js中数组在内存中是如何存储的?
Javascript的内存分为堆内存和栈内存,数组作为对象,在建立后存储在堆内存中
数组本来应该是一个连续的内存分配,但是在Javascript中不是连续分配的,而是类似哈希映射的方式存在的。对于上述的实现方式,熟悉数据结构的同学应该知道,对于读取操作,哈希表的效率并不高,而修改删除的效率比较高。 现在浏览器为了优化其操作,对数组的创建时候的内存分配进行了优化:
- 对于同构的数组,也就是,数组中元素类型一致,会创建连续的内存分配
- 对于不同构数组,按照原来的方式创建。
- 如果你想插入一个异构数据,那么就会重新解构,通过哈希映射的方式创建
为了进一步优化功能的实现,Javascript中出现了ArrayBuffer,它可以创建连续的内存供编程人员使用。
- ArrayBuffer是创建一块连续的内存,不能直接操作
- 通过视图对分配的内存进行读写操作
26、直接在script标签中export为什么会出错
在 ES6 引入了模块机制后,JavaScript 可以分为两种源文件:一种叫做脚本、一种叫做模块;而在 ES5 和之前的版本中,就只有一种脚本源文件类型。然而脚本是可以由浏览器或者 node 环境引入执行的,而模块只能由 JavaScript 代码用 import 引入执行。
从概念上可以认为脚本是具有主动性的 JavaScript 代码段,是控制宿主完成一定任务的代码;模块是被动性的 JavaScript 代码段,是等待被调用的库;也不难发现,模块和脚本之间的区别仅仅在于是否包含 import 和 export。
但是现代浏览器可以支持直接用 script 标签引入模块或脚本,在引入模块的前提条件就是必须给 script 标签添加 type=”module” 属性,而引入脚本不需要加 type 属性,它的默认值是 text/javascript;如下引入模块:
<script type="module" src="****.js"></script>
这样,题目中的问题:在 script 标签中写 export 为什么会报错? 就可以解释了,因为在默认条件下我们加载的文件会被认为是脚本文件而不是模块,所以在脚本文件中写了 export 就会报错。
27、实现一个扫描二维码登录网页
28、Promise的值穿透和异常穿透
因为promise.then入参希望是函数,如果不是函数就会发生值穿透现象 juejin.cn/post/700259…
29、cookie、sessionStorage、localStorage区别
30、isNaN 和 Number.isNaN区别
isNaN: is not a number,不是一个数,即不是数字类型 isNaN: 不光要不是数字类型,还要等于NaN
31、js错误类型
- SyntaxError: 语法错误
- Reference: 引用错误,要找的东西没找到比如let、const暂时性死区
- RangeError: 范围错误,比如new Array(-1)
- TypeError 情况一:变量或参数不是预期类型,比如,对字符串、布尔值、数值等原始类型的值使用new命令,就会抛出这种错误,因为new命令的参数应该是一个构造函数。
var a= new abc;
//Uncaught TypeError: abc is not a function
情况二:调用对象不存在的方法
var b;
b.c();
//Uncaught TypeError: Cannot read property c of undefined
- URLError 与url相关函数参数不正确,主要是encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()这六个函数。
decodeURI('%2')
//Uncaught URIError: URI malformed
- EvalError eval函数没有被正确执行
eval(2b)
//Uncaught SyntaxError: Invalid or unexpected token
- 自定义错误
如果不想使用系统设置的错误信息(例如前面提到的6种),可以自定义错误,例如让一个函数需要传入一个字符串,但是传入了空值,可以
new
不同的错误类型,并自定义错误提示语来让系统抛出信息。
function check(string){
if(!string){
throw new Error("内容不存在");
//throw new TypeError("内容不存在")
}
}
32、es5和es6类的区别
es5中主要通过原型链方法和构造函数方法实现类,es6直接使用class
一、es6的class只能使用new关键词使用,不能直接使用
二、class不存在变量提升,es5的类因为本身就是函数,存在函数提升现象
三、class类无法遍历它实例原型链上的属性和方法
四、new.target属性
es6为new提供了new.target属性,可以返回该实例的构造函数,如果不是通过new方法或者Reflect.constructor()方法创建的实例,new.target会返回undefined
- 可以判断实例是不是由new创建
- 可以实现抽象类,比如父类的某个方法必须由子类实现,不能直接由父类实现
五、static静态方法
- static只能由类调用,不能由实例调用
- static里的this指向类,不是实例
- static方法可以被子类继承
四、框架
1、Virtual DOM 真的比操作原生 DOM 快吗
原生 DOM 操作 vs. 通过框架封装操作。
这是一个性能 vs. 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。
对 React 的 Virtual DOM 的误解。
React 从来没有说过 “React 比原生操作 DOM 快”。React 的基本思维模式是每次有变动就整个重新渲染整个应用。如果没有 Virtual DOM,简单来想就是直接重置 innerHTML。很多人都没有意识到,在一个大型列表所有数据都变了的情况下,重置 innerHTML 其实是一个还算合理的操作... 真正的问题是在 “全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个 innerHTML,这时候显然就有大量的浪费。
我们可以比较一下 innerHTML vs. Virtual DOM 的重绘性能消耗:
- innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)
- Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)
Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。可以看到,innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关的。前面说了,和 DOM 操作比起来,js 计算是极其便宜的。这才是为什么要有 Virtual DOM:它保证了 1)不管你的数据变化多少,每次重绘的性能都可以接受;2) 你依然可以用类似 innerHTML 的思路去写你的应用。
MVVM vs. Virtual DOM
相比起 React,其他 MVVM 系框架比如 Angular, Knockout 以及 Vue、Avalon 采用的都是数据绑定:通过 Directive/Binding 对象,观察数据变化并保留对实际 DOM 元素的引用,当有数据变化时进行对应的操作。MVVM 的变化检查是数据层面的,而 React 的检查是 DOM 结构层面的。MVVM 的性能也根据变动检测的实现原理有所不同:Angular 的脏检查使得任何变动都有固定的
O(watcher count) 的代价;Knockout/Vue/Avalon 都采用了依赖收集,在 js 和 DOM 层面都是 O(change):
- 脏检查:scope digest O(watcher count) + 必要 DOM 更新 O(DOM change)
- 依赖收集:重新收集依赖 O(data change) + 必要 DOM 更新 O(DOM change)可以看到,Angular 最不效率的地方在于任何小变动都有的和 watcher 数量相关的性能代价。但是!当所有数据都变了的时候,Angular 其实并不吃亏。依赖收集在初始化和数据变化的时候都需要重新收集依赖,这个代价在小量更新的时候几乎可以忽略,但在数据量庞大的时候也会产生一定的消耗。
MVVM 渲染列表的时候,由于每一行都有自己的数据作用域,所以通常都是每一行有一个对应的 ViewModel 实例,或者是一个稍微轻量一些的利用原型继承的 "scope" 对象,但也有一定的代价。所以,MVVM 列表渲染的初始化几乎一定比 React 慢,因为创建 ViewModel / scope 实例比起 Virtual DOM 来说要昂贵很多。这里所有 MVVM 实现的一个共同问题就是在列表渲染的数据源变动时,尤其是当数据是全新的对象时,如何有效地复用已经创建的 ViewModel 实例和 DOM 元素。假如没有任何复用方面的优化,由于数据是 “全新” 的,MVVM 实际上需要销毁之前的所有实例,重新创建所有实例,最后再进行一次渲染!这就是为什么题目里链接的 angular/knockout 实现都相对比较慢。相比之下,React 的变动检查由于是 DOM 结构层面的,即使是全新的数据,只要最后渲染结果没变,那么就不需要做无用功。
Angular 和 Vue 都提供了列表重绘的优化机制,也就是 “提示” 框架如何有效地复用实例和 DOM 元素。比如数据库里的同一个对象,在两次前端 API 调用里面会成为不同的对象,但是它们依然有一样的 uid。这时候你就可以提示 track by uid 来让 Angular 知道,这两个对象其实是同一份数据。那么原来这份数据对应的实例和 DOM 元素都可以复用,只需要更新变动了的部分。或者,你也可以直接 track by index 的话,后续重绘是不会比 React 慢多少的。甚至在 dbmonster 测试中,Angular 和 Vue 用了 track by $index 以后都比 React 快: dbmon (注意 Angular 默认版本无优化,优化过的在下面)
顺道说一句,React 渲染列表的时候也需要提供 key 这个特殊 prop,本质上和 track-by 是一回事。
性能比较也要看场合
在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不同的表现和不同的优化需求。Virtual DOM 为了提升小量数据更新时的性能,也需要针对性的优化,比如 shouldComponentUpdate 或是 immutable data。
- 初始渲染:Virtual DOM > 脏检查 >= 依赖收集
- 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化
- 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化)>> MVVM 无优化
不要天真地以为 Virtual DOM 就是快,diff 不是免费的,batching 么 MVVM 也能做,而且最终 patch 的时候还不是要用原生 API。在我看来 Virtual DOM 真正的价值从来都不是性能,而是它 1) 为函数式的 UI 编程方式打开了大门;2) 可以渲染到 DOM 以外的 backend,比如 ReactNative。
总结
以上这些比较,更多的是对于框架开发研究者提供一些参考。主流的框架 + 合理的优化,足以应对绝大部分应用的性能需求。如果是对性能有极致需求的特殊情况,其实应该牺牲一些可维护性采取手动优化:比如 Atom 编辑器在文件渲染的实现上放弃了 React 而采用了自己实现的 tile-based rendering;又比如在移动端需要 DOM-pooling 的虚拟滚动,不需要考虑顺序变化,可以绕过框架的内置实现自己搞一个。
3、SPA和MPA区别、优缺点及改进
4、mobx和redux的区别
共同点
- 都是为了解决状态混乱
- 通常store只有一个
- 更新状态方式统一,并可控,通常都以action形式
- 支持将store和react组件连接,如mobx-react、react-redux
区别
-
redux
- store: 是个对象,存数据
- getstate: 获取store里的数据
- dispatch: store数据不能直接改,必须通过这个方法调动action
- subsribe: 订阅
store
改变时,要进行的操作。比如在react
中,当store
改变时,我们需要调用render
方法对视图进行更新
- action: 是个对象,必须包含
type
这个属性,reducer
将根据这个属性值来对store
进行相应的处理。除此之外的属性,就是进行这个操作需要的数据 - reducer: 是个函数,返回更新后的状态
这三者的关系是store通过dispatch方法,触发action,
dispatch
方法接受action
对象作为参数,再有reducer函数做处理,返回新状态,再调用store的subsribe方法更新页面render() - 异步流∶ 由于Redux所有对store状态的变更,都应该通过action触发,异步任务(通常都是业务或获取数据任务)也不例外,而为了不将业务或数据相关的任务混入React组件中,就需要使用其他框架配合管理异步任务流程,如redux-thunk,redux-saga等;
- store: 是个对象,存数据
-
react-redux
- connect方法:连接store、action与组件,connect高阶函数,接受两个函数
mapStateToProps
,mapDispatchToProps
,顾名思义 - provider组件:保证他的子孙组件都能连接到store
- connect方法:连接store、action与组件,connect高阶函数,接受两个函数
-
mobx Mobx是一个透明函数响应式编程的状态管理库,它使得状态管理简单可伸缩∶
- Action∶定义改变状态的动作函数,包括如何变更状态;
- Store∶ 集中管理模块状态(State)和动作(action)
- Derivation(衍生)∶ 从应用状态中派生而出,且没有任何其他影响的数据
对比总结
- redux将数据保存在单一的store中,mobx将数据保存在分散的多个store中
- redux使用
plain object
保存数据,需要手动处理变化后的操作;mobx适用observable
保存数据,数据变化后自动处理响应的操作 - redux使用不可变状态,这意味着状态是只读的,不能直接去修改它,而是应该返回一个新的状态,同时使用纯函数;mobx中的状态是可变的,可以直接对其进行修改
- mobx相对来说比较简单,在其中有很多的抽象,mobx更多的使用面向对象的编程思维;redux会比较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助一系列的中间件来处理异步和副作用
- mobx中有更多的抽象和封装,调试会比较困难,同时结果也难以预测;而redux提供能够进行时间回溯的开发工具,同时其纯函数以及更少的抽象,让调试变得更加的容易
5、React主要流程
-
scheduler 调度器 ,调度任务的优先级,高优任务优先进入Reconciler
-
reconciler 协调器,又被称为render阶段,负责找出变化的组件
首先表明,jsx !== fiber节点,fiber节点还有state、effecttag以及和其他节点关系的字段。render阶段
开始于performSyncWorkOnRoot
或performConcurrentWorkOnRoot
方法的调用。这取决于本次更新是同步更新还是异步更新。他们唯一的区别是是否调用shouldYield
。如果当前浏览器帧没有剩余时间,shouldYield
会中止循环,直到浏览器有空闲时间后再继续遍历。 这个阶段分为两步beginWork和commpleteWork,也就是”递“和”归“,使用深度优先遍历。-
beginwork阶段,调用reconcileChildren方法,也就是diff算法
-
completeWork阶段,
effectList
相较于Fiber树
,就像圣诞树上挂的那一串彩灯。 第一次渲染只插入一次根节点dom,因为递归特性,归到根节点其实所有节点都走完了。更新是根据effectList这个单向链表。
-
- renderer 渲染器,又被称为commit阶段,负责将变化的组件渲染到页面上
- before mutation阶段(执行DOM操作前)
- 处理
DOM节点
渲染/删除后的autoFocus
、blur
逻辑 - 调用
getSnapshotBeforeUpdate
生命周期钩子 - 调度
useEffect
- mutation(执行DOM操作)
- 递归调用
Fiber节点
及其子孙Fiber节点
中fiber.tag
为ClassComponent
的componentWillUnmount
(opens new window)生命周期钩子,从页面移除Fiber节点
对应DOM节点
- 解绑
ref
- 调度
useEffect
的销毁函数
- layout(执行DOM操作后)
遍历effectList
,依次执行commitLayoutEffects
。该方法的主要工作为“根据effectTag
调用不同的处理函数处理Fiber
并更新ref
6、diff算法
单节点的diff不用多说,主要就是根据节点类型和Key来判断是否可以复用节点。
多节点的diff需要重点关注,比如array类型。处理办法主要是通过两轮遍历实现,第一轮遍历是找到可以复用的节点,第二轮遍历处理不能复用的节点。
第一轮遍历结束后,会有四种可能:
- old和new都遍历完了,最佳结果,全部都可以复用
- old遍历完了,new还没有,old都能复用,new剩下的全部插入
- old没遍历完,new遍历完了,old没遍历完的全部删除
- old和new都没遍历完,这种情况一定是有节点位置变了,比如:
/* old */
<ul>
<li key='1'>1</li>
<li key='2'>2</li>
<li key='3'>3</li>
</ul>
/* new */
<ul>
<li key='1'>1</li>
<li key='3'>2</li>
<li key='2'>3</li>
</ul>
对于这种情况,react会做这样的处理,记录第一轮循环最后能复用节点在old中的lastPlacedindex,取出old剩下节点,以在old中的key为key,以index为value,生成一个map。遍历new中剩下的节点,在map中寻找key值一样的节点,得到在old中的index,如果index >= lastPlaceindex,不做任何处理,lastPlacedindex = index,如果index < lastPlaceIndex,则把该节点移动到最右边。举两个栗子,key值abcd->acdb和abcd->dabc
从这里也可以看出,react只会往最右边移动,所以尽量减少节点位置提前的操作
7、immutable.js的作用,对react有什么帮助
react对象及时一样,但是引用不一样,也会重新渲染,React.memo是浅比较,比较的是引用,还是会出现内容一致,引用不一致,重新渲染的情况。 immutable保留了相同的结构,共享相同节点,效率高,和reducer这样的纯函数一样每次操作都会生成一个新的对象,比较是比较的值,不是引用,判断是否一致就很方便,达到深比较的效果。不直接用深比较的原因是效率太低,一些没必要比较的节点也比了。
- 使用场景
- react.memo
- shouldComponentUpdate
- redux 中的 mapStateToProps
8、react高阶组件的原理、用途和案例
- redux中的connect
- withRouter组件,react router
- 分片渲染,图片一个一个出来(渲染劫持)
- 组件懒加载(渲染劫持)
9、Vue为什么采用异步渲染
promise.nexttick是微任务,在宏任务之后进行,这样可以合并this.setstate的操作,只关注结果,不用每变化一次都重新渲染一次
10、react和Vue中的key为什么不建议用下标
对数组来说,如果用下标,改变一个可能所有下标都变了,diff算法是优先比较key,如果key不一致要重新比较,性能消耗巨大。比如一个数组删除了第一个,如果用下标表示key,那么后面所有的key都变了
五、Typescript
1、Typescript相较于JavaScript有什么优势和劣势?
- 优势
- Typescript是js的超集,兼容js,具有一切js的方法
- Ts是静态类型,支持静态类型检查,可以在编译阶段显示出所有语法错误
- 良好的代码编写体验,可以提供方法提示,错误校验,自动联想等
- 由于是静态类型,在编译时省略了判断类型这一环节,编译速度更快
- 类型在一定程度上可以充当文档
- 劣势
- 有一定学习成本
- 老项目重构需要花费一定的精力
2、const func = (a, b) => a + b; 要求编写Typescript,要求a,b参数类型一致,都为number或者都为string
type numString = number | string
const func = (a: numString, b: numString) => {
if ((typeof a === 'string' && typeof b === 'string') || (typeof a === 'number' && typeof b === 'number')) {
return a + b
}
throw new Error('参数类型必须同为number或者同为string')
}
3、TS内置工具类型
- exclude<T,U>从T可分配给的类型中排除U
exclude<T,U> = T extends U ? never : T
type E = exclude<string|number, string>
let e: E = 10
- extruct<T,U>从T可分配的类型中提取出U
extruct<T,U> = T extends U ? T : never
type E = exclude<string|number, string>
let e: E = 10
- NonNullable从T中排除null和undefined
type NonNullable<T> = T extends null|undefined ? never :
- ReturnType infer最早出现在此pr中,表示extends中待推断的类型
ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]) => infer R ? R : never
function getUserInfo() {
return { name: 'hh', age: 29 }
}
type userInfo = ReturnType<type of getUserInfo>
const info: userInfo = { name: '2r', age: 23 }
该工具类型主要用来获取函数的返回类型
- Parameters该工具类型主要用来获取函数的参数类型
Parameters<T> = T extends (...args: infer R) => any ? R : any
type T0 = Parameters<() => string> // []
type T1 = Parameters<(s: string) => void> // [string]
- Partial可以让传入的属性由必选变为可选
type Partial<T> = { [P in keyof T]?: T[P]}
interface A {
a1: string
a2: number
}
type partial = Partial<A>
const a3: partial = {} // 不会报错
- Required可以让传入的属性由可选变为必选
type Required<T> = { [P in keyof T]-?: T[P] }
- Readonly传入的每个属性都修改为只读属性
type Readonly<T> = { readonly [P in keyof T]: T[P] }
interface Person {
name: string
age: number
}
const a: Readonly<Person> = { name: 'sfsf', age: 23 }
a.name = '234' // Error
- Pick<T,K> 从传入的属性中摘取部分返回
type Pick<T,K> = { [P in keyof K]: T[P] }
interface Todo {
title: string;
description: string;
done: boolean;
}
type TodoBase = Pick<Todo, "title" | "done">;
type TodoBase = { title: string; done: boolean; };
- Record<T,K> 对象类型,T是key的类型,K是value的类型
type Record<T,K> = { [P in T]: K}
type keyType = 'x' | 'y'
type objType = Record<keyType, number>
const a: objType = { x: 1, y: 2 }
- Omit<T,K>从传入的属性中剔除部分返回
type Omit<T,K> = Pick<T, exclude<key of T, K>>
type User = { id: string; name: string; email: string; };
type UserWithoutEmail = Omit<User, "email">; // UserWithoutEmail ={id: string;name: string;} };
4、never
5、type和interface区别
六、算法
1、两个数不使用四则运算得出和
1)按位与
8 & 7 // -> 0
// 1000 & 0111 -> 0000 -> 0
每一位都为 1,结果才为 1
2)按位或
8 | 7 // -> 15
// 1000 | 0111 -> 1111 -> 15
其中一位为 1,结果就是 1
3)按位异或 每一位都不同,结果才为 1
8 ^ 7 // -> 15
8 ^ 8 // -> 0
// 1000 ^ 0111 -> 1111 -> 15 // 1000 ^ 1000 -> 0000 -> 0
从以上代码中可以发现按位异或就是不进位加法
这道题中可以按位异或,因为按位异或就是不进位加法,8 ^ 8 = 0 如果进位了,就是 16 了,所以我们只需要将两个数进行异或操作,然后进位。那么也就是说两个二进制都是 1 的位置,左边应该有一个 进位 1,所以可以得出以下公式 a + b = (a ^ b) + ((a & b) << 1) ,然后通过迭代的方式模拟加法
答案:
function sum(a, b) {<br>
if (a == 0) return b<br>
if (b == 0) return a<br>
let newA = a ^ b<br>
let newB = (a & b) << 1<br>
return sum(newA, newB)<br>
}
2、冒泡排序
冒泡排序的原理如下,从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大, 那么就交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。 下一轮重复以上操作,但是此时最后一个元素已经是最大数了,所以不需要再比较最后一个元素,只需 要比较到 length - 1 的位置。
function checkArray(array) {
if (!array || array.length <= 2) return
}
function swap(array, left, right) {
let rightValue = array[right]
array[right] = array[left]
array[left] = rightValue
}
function bubble(array) {
checkArray(array);
for (let i = array.length - 1; i > 0; i--) {
// 从 0 到 `length-1` 遍历
for (let j = 0; j < i; j++) {
if (array[j] > array[j + 1]) swap(array, j, j + 1)
}
}
return array;
}
时间复杂度: 等差数列,去掉常数项后为O(n*n)
3、插入排序
有点像冒泡排序的逆向操作。冒泡排序是从最后一个开始排,插入排序是从第一个开始排
冒泡排序的原理如下,从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大, 那么就交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。 下一轮重复以上操作,但是此时最后一个元素已经是最大数了,所以不需要再比较最后一个元素,只需 要比较到 length - 1 的位置。
function checkArray(array) {
if (!array || array.length <= 2) return
}
function swap(array, left, right) {
let rightValue = array[right]
array[right] = array[left]
array[left] = rightValue
}
function sort(arr) {
for(let i = 1; i < arr.length; i++) {
for(let j = i -1; j >= 0 && arr[j] > arr[j + 1]; j--) {
swap(arr, j, j + 1)
}
}
return arr
}
时间复杂度: 等差数列,去掉常数项后为O(n*n)
4、选择排序
选择排序的原理
遍历数组,设置最小值的索引为 0,如果取出的值比当前最小值小,就替换最小 值索引,遍历完成后,将第一个元素和最小值索引上的值交换。如上操作后,第一个元素就是数组中的 最小值,下次遍历就可以从索引 1 开始重复上述操作。
function selection(array) { checkArray(array);
for (let i = 0; i < array.length - 1; i++) {
let minIndex = i;
for (let j = i + 1; j < array.length; j++) {
minIndex = array[j] < array[minIndex] ? j : minIndex;
}
swap(array, i, minIndex);
}
return array;
}
时间复杂度: 等差数列,去掉常数项后为O(n*n)
5、数组拍平
let arr = [1, 2, [3, 4, 5, [6, 7], 8], 9, 10, [11, [12, 13]]]
解法一:
function flatten(arr, result = []) {
for(let i = 0;i < arr.length;i++) {
if (Object.prototype.toString.call(arr[i]) !== '[object Array]') {
result.push(arr[i])
} else {
flatten(arr[i], result)
}
}
return result
}
解法二:
const flatten = function (arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr)
}
return arr
}
6、深度优先遍历和广度优先遍历
假设树形结构数据:
var a = [{idx: 0, children: [{idx: 1, children: [{idx: 4, children: []}, {idx: 5, children: [{idx: 10, chidren: []}]}]}, {idx: 2, children: [{idx: 6, children: []}, {idx: 7, children: []}, {idx: 8, children: []}]}, {idx: 3, children: [{idx: 9, children: []}]}]}]
深度优先遍历
// 递归法
function dfs(tree, result = []) {
for (let i = 0; i < tree.length; i++) {
const cur = tree[i]
result.push(cur.idx)
if (cur.children?.length > 0) dfs(cur.children, result)
}
return result
}
// 非递归法
function dfs(tree) {
let result = []
let stack = [tree[0]]
while (stack.length > 0) {
const cur = stack.pop()
result.push(cur.idx)
if (cur.children?.length > 0) {
for (let i = cur.children?.length - 1; i >= 0; i--) {
stack.push(cur.children[i])
}
}
}
return result
}
广度优先遍历
// 递归
// 非递归
function bfs(tree) {
let result = []
let stack = [tree[0]]
while (stack.length > 0) {
const cur = stack.shift()
result.push(cur.idx)
if (cur.children?.length > 0) {
for (let i = 0; i < cur.children?.length; i++) {
stack.push(cur.children[i])
}
}
}
return result
}
七、小程序
八、浏览器
1、浏览器每帧执行动作
-
首先需要处理输入事件,能够让用户得到最早的反馈;
-
接下来是处理定时器,需要检查定时器是否到时间,并执行对应的回调;
-
接下来处理 Begin Frame(开始帧),即每一帧的事件,包括 window.resize、scroll、media query change 等;
-
接下来执行请求动画帧 requestAnimationFrame(rAF),即在每次绘制之前,会执行 rAF 回调;
-
紧接着进行 Layout 操作,包括计算布局和更新布局,即这个元素的样式是怎样的,它应该在页面如何展示;
-
接着进行 Paint 操作,得到树中每个节点的尺寸与位置等信息,浏览器针对每个元素进行内容填充;
-
到这时以上的六个阶段都已经完成了,接下来处于空闲阶段,可以在这时执行 requestIdleCallback 里注册的任务;
2、浏览器渲染流程
-
解析HTML生成DOM树。
-
解析CSS生成CSSOM规则树。
-
解析JS,操作 DOM 树和 CSSOM 规则树。
-
将DOM树与CSSOM规则树合并在一起生成渲染树。
-
遍历渲染树开始布局,计算每个节点的位置大小信息。
-
浏览器将所有图层的数据发送给GPU,GPU将图层合成并显示在屏幕上。
3、浏览器线程有哪些?
- javascript引擎
- GUI渲染线程
- 定时触发器线程
- 事件触发线程
- 异步http请求线程
ps: chrome是多进程架构
原因是chrome觉得现在浏览器任务已经越来越复杂,如果都在一个进程里,现在一个tab窗口崩了,可能都崩了
浏览器是多进程的
-
Browser 进程:浏览器的主进程,只有一个。
- 负责浏览器界面的显示与交互;
- 各个页面的管理,创建和销毁其他进程;
- 网络的资源管理、下载等。
-
Renderer 进程:也称为浏览器渲染进程或浏览器内核,内部是多线程的。主要负责页面渲染,脚本执行,事件处理等。
-
第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建。
-
GPU 进程:最多一个,用于 3D 绘制等。
4、浏览器缓存策略
强缓存
- expires
http1.0产物,缓存过期时间,用的是浏览器本地时间,可以改本地时间,不准 - cache control
优先级高于expires,五种值类型:- max-age: 相对时间,返回头中的date加上相对时间就是是否有效
- no cache: 不用本地缓存,用协商缓存
- no store: 不缓存数据
- public: 终端和代理服务器都可以缓存数据
- private: 只能被终端缓存
协商缓存
5、重排和重绘,如何减少他们
重排
当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫重排。
重绘
当重新生成渲染树后,就要将渲染树每个节点绘制到屏幕,这个过程叫重绘。不是所有的动作都会导致重排,例如改变字体颜色,只会导致重绘。记住,重排会导致重绘,重绘不会导致重排 。
重排和重绘这两个操作都是非常昂贵的,因为 JavaScript 引擎线程与 GUI 渲染线程是互斥,它们同时只能一个在工作。
什么操作会导致重排?
- 添加或删除可见的 DOM 元素
- 元素位置改变
- 元素尺寸改变
- 内容改变
- 浏览器窗口尺寸改变
如何减少重排重绘?
-
减小重排范围 在局部dom进行重排;尽量不要使用table布局
-
减少重排次数 1、样式集中改变,不要改style,批量改class
2、分离读写操作,DOM 的多个读操作(或多个写操作),应该放在一起。不要两个读操作之间,加入一个写操作。
3、将 DOM 离线 -
使用 display:none
一旦我们给元素设置
display:none
时(只有一次重排重绘),元素便不会再存在在渲染树中,相当于将其从页面上“拿掉”,我们之后的操作将不会触发重排和重绘,添加足够多的变更后,通过display
属性显示(另一次重排重绘)。通过这种方式即使大量变更也只触发两次重排。另外,visibility : hidden
的元素只对重绘有影响,不影响重排。 -
通过 documentFragment 创建一个
dom
碎片,在它上面批量操作dom
,操作完成之后,再添加到文档中,这样只会触发一次重排。 -
复制节点,在副本上工作,然后替换它!
4、使用 absolute 或 fixed 脱离文档流
使用绝对定位会使的该元素单独成为渲染树中 body
的一个子元素,重排开销比较小,不会对其它节点造成太多影响。当你在这些节点上放置这个元素时,一些其它在这个区域内的节点可能需要重绘,但是不需要重排。
5、优化动画
-
可以把动画效果应用到
position
属性为absolute
或fixed
的元素上,这样对其他元素影响较小。动画效果还应牺牲一些平滑,来换取速度,这中间的度自己衡量: 比如实现一个动画,以1个像素为单位移动这样最平滑,但是Layout就会过于频繁,大量消耗CPU资源,如果以3个像素为单位移动则会好很多
-
启用GPU加速
GPU
硬件加速是指应用GPU
的图形性能对浏览器中的一些图形操作交给GPU
来完成,因为GPU
是专门为处理图形而设计,所以它在速度和能耗上更有效率。GPU
加速通常包括以下几个部分:Canvas2D,布局合成, CSS3转换(transitions),CSS3 3D变换(transforms),WebGL和视频(video)。
6、ric和raf
segmentfault.com/a/119000001… react fiber技术就是基于RIC,就是闲时任务操作
7、event loop
宏任务
# | 浏览器 | Node |
---|---|---|
I/O | ✅ | ✅ |
setTimeout | ✅ | ✅ |
setInterval | ✅ | ✅ |
setImmediate | ❌ | ✅ |
requestAnimationFrame | ✅ | ❌ |
微任务
# | 浏览器 | Node |
---|---|---|
process.nextTick | ❌ | ✅ |
MutationObserver | ✅ | ❌ |
Promise.then catch finally | ✅ | ✅ |
8、检测浏览器是否处于暗黑模式
window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
true
9、浏览器从输入url开始经历了什么
1、dns解析
2、tcp三次握手
3、发送http请求
4、tcp四次挥手
5、开始渲染流程
10、浏览器垃圾回收机制
1)为什么要回收垃圾
let test = { name: "isboyjc" };
test = [1,2,3,4,5]
test被重新赋值为数组,之前的对象就没用了,就需要回收
- 回收策略
-
标记回收
-
引用数回收
11、什么是service-worker
service-worker是一个服务于前台页面的后台进程,基于web worker实现,异步执行。旨在提高离线缓存能力,缩小native app和web app的差距
应用场景:
- 离线缓存:缓存不变化的或者很少用到的资源,减少服务器压力
- 发通知:推送消息
- 事件同步:用户关闭应用也会继续之前的任务,比如发邮件、web即时工具等
- 定时同步:定时执行serverce-worker中的定时任务,比如资讯客户端定时刷新资讯
12、DOMContentLoaded、load、beforeUnload以及unload事件
- DOMContentLoaded 事件在 html文档加载完毕,并且 html 所引用的内联 js、以及外链 js 的同步代码都执行完毕后触发
- load事件,当页面 DOM 结构中的 js、css、图片,以及 js 异步加载的 js、css 、图片都加载完成之后,才会触发 load 事件
HTML文档的加载与页面的首次渲染
当我们输入一个页面地址时,发生了哪些事情呢?
-
1、浏览器首先下载该地址所对应的 html 页面。
-
2、浏览器解析 html 页面的 DOM 结构。
-
3、开启下载线程对文档中的所有资源按优先级排序下载。
-
4、主线程继续解析文档,到达 head 节点 ,head 里的外部资源无非是外链样式表和外链 js。
- 发现有外链 css 或者外链 js,如果是外链 js ,则停止解析后续内容,等待该资源下载,下载完后立刻执行。如果是外链 css,继续解析后续内容。
-
5、解析到 body
body 里的情况比较多,body 里可能只有 DOM 元素,可能既有 DOM、也有 css、js 等资源,js 资源又有可能异步加载 图片、css、js 等。DOM 结构不同,浏览器的解析机制也不同,我们分开来讨论。
-
只有 DOM 元素
- 这种情况比较简单了,DOM 树构建完,页面首次渲染。
-
有 DOM 元素、外链 js。
- 当解析到外链 js 的时候,该 js 尚未下载到本地,则 js 之前的 DOM 会被渲染到页面上,同时 js 会阻止后面 DOM 的构建,即后面的 DOM 节点并不会添加到文档的 DOM 树中。所以,js 执行完之前,我们在页面上看不到该 js 后面的 DOM 元素。
-
有 DOM 元素、外链 css
- 外链 css 不会影响 css 后面的 DOM 构建,但是会阻碍渲染。简单点说,外链 css 加载完之前,页面还是白屏。
-
有 DOM 元素、外链 js、外链 css
- 外链 js 和外链 css 的顺序会影响页面渲染,这点尤为重要。当 body 中 js 之前的外链 css 未加载完之前,页面是不会被渲染的。
- 当body中 js 之前的 外链 css 加载完之后,js 之前的 DOM 树和 css 合并渲染树,页面渲染出该 js 之前的 DOM 结构。
-
-
6、文档解析完毕,页面重新渲染。当页面引用的所有 js 同步代码执行完毕,触发 DOMContentLoaded 事件。
-
7、html 文档中的图片资源,js 代码中有异步加载的 css、js 、图片资源都加载完毕之后,load 事件触发。
另外,如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过 async 或 defer 来标记代码,使用方式如下所示:
<script async type="text/javascript" src='foo.js'></script>
<script defer type="text/javascript" src='foo.js'></script>
async和defer区别:
- async:脚本并行加载,加载完成之后立即执行,执行时机不确定,仍有可能阻塞HTML解析,执行时机在load事件派发之前。
- defer:脚本并行加载,等待HTML解析完成之后,按照加载顺序执行脚本,执行时机DOMContentLoaded事件派发之前。
九、工程化
1、npm和yarn的区别
2、模块化有哪些方式?
模块化的好处,方便管理,防止全局变量污染 www.processon.com/view/link/5…
十、前端性能优化
前端性能优化大致分为三个方向 1、网络优化
- 减少http请求
- 使用http2.0
2.0优势:
- 解析速度快,http1必须不断读入字符,直到遇到分隔符CRLF为止,http2.0以帧为单位,每一帧都有长度字段
- 多路复用,http1是一个tcp连接只能处理一个请求
十一、前端安全
1、前端攻击种类
- xss攻击
- 存储型
- 反射型
- dom型
-
csrf攻击
-
dos攻击 不停的发请求给服务器,冲垮服务器
-
sql注入 简单来说 就是在提交表单的时候插入SQL命令 最终是web服务器执行恶意SQL命令的过程
- 不要使用动态SQL 避免将用户提交的数据直接放入sql语句中
- 限制数据库权限和特权 将数据库用户的功能设置为最低要求
- 避免直接向用户显示数据库错误信息
- 在发布之前 用专业的sql注入检测工具进行检测 及时发现和修补
- dns劫持
2、详细讲解下csrf攻击及防御措施
1. 什么是csrf攻击
csrf攻击,跨站点请求伪造: 攻击者诱导受害者访问第三方网站,在第三方网站中,向被攻击网站发送跨站请求
2. 常见攻击类型
- get型攻击 利用img等标签的跨域特性,自动发送get请求到被攻击域名
- post型攻击 利用form标签action跨域特性,自动提交表单请求
- 链接类型的csrf 点击a标签等,利用a标签src的跨域特性
3. 防御方法
阻止不明外域的访问
- 同源检测,检测方法有利用header中的origin header和referer header字段判断来源域名
- samesite cookie
- 严格模式(strict) 这个 Cookie 在任何情况下都不可能作为第三方 Cookie
- 宽松模式(lax) 假如这个请求是这种请求(改变了当前页面或者打开了新页面)且同时是个GET请求
- 缺陷 不支持子域。例如,种在topic.a.com下的Cookie,并不能使用a.com下种植的SamesiteCookie。这就导致了当我们网站有多个子域名时,不能使用SamesiteCookie在主域名存储用户登录信息。每个子域名都需要用户重新登录一次。
提交时附加本域才能添加的校验信息
- csrf token
防护方法一:
1)服务器生成csrf token,返回时塞到session中 2)客户端从seesionid中取出csrftoken再加入到请求里 3)后端校验
防护方法二:
分布式校验,机器多了以后,由于负载均衡,会随机分配资源,可能拿不到上个session数据,所以采用token方式,token其实就是个统一加密算法,这样每个机器都能保持一致了
防护方法三:
最简单直接验证码和密码其实也可以起到CSRF Token的作用哦,而且更安全。
为什么很多银行等网站会要求已经登录的用户在转账时再次输入密码
- 双重cookie 双重Cookie采用以下流程:
- 在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串(例如
csrfcookie=v8g9e4ksfhw
)。 - 在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例
POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw
)。 - 后端接口验证Cookie中的字段与URL参数中的字段是否一致,不一致则拒绝。
此方法相对于CSRF Token就简单了许多。可以直接通过前后端拦截的的方法自动化实现。后端校验也更加方便,只需进行请求中字段的对比,而不需要再进行查询和存储Token。
当然,此方法并没有大规模应用,其在大型网站上的安全性还是没有CSRF Token高,原因我们举例进行说明。
由于任何跨域都会导致前端无法获取Cookie中的字段(包括子域名之间),于是发生了如下情况:
- 如果用户访问的网站为
www.a.com
,而后端的api域名为api.a.com
。那么在www.a.com
下,前端拿不到api.a.com
的Cookie,也就无法完成双重Cookie认证。 - 于是这个认证Cookie必须被种在
a.com
下,这样每个子域都可以访问。 - 任何一个子域都可以修改
a.com
下的Cookie。 - 某个子域名存在漏洞被XSS攻击(例如
upload.a.com
)。虽然这个子域下并没有什么值得窃取的信息。但攻击者修改了a.com
下的Cookie。 - 攻击者可以直接使用自己配置的Cookie,对XSS中招的用户再向
www.a.com
下,发起CSRF攻击。
防止网站被利用
- 管理上传接口,严格禁止上传预期之外的内容,比如html
- header设置为x-content-type-options: nosniff,防止黑客上传的html内容(比如图片)被解析为网页
- 对于用户上传的图片,进行转存或者校验,不直接使用用户上传的图片链接
- 当前用户打开其他用户填写的链接时,需告知风险(这也是很多论坛不允许直接在内容中发布外域链接的原因之一,不仅仅是为了用户留存,也有安全考虑)
总结
简单总结一下上文的防护策略:
- CSRF自动防御策略:同源检测(Origin 和 Referer 验证)。
- CSRF主动防御措施:Token验证 或者 双重Cookie验证 以及配合Samesite Cookie。
- 保证页面的幂等性,后端接口不要在GET页面中做用户操作。
为了更好的防御CSRF,最佳实践应该是结合上面总结的防御措施方式中的优缺点来综合考虑,结合当前Web应用程序自身的情况做合适的选择,才能更好的预防CSRF的发生。
3、为什么要使用https
http三大问题:
http是明文传输,容易被监听窃取数据 -- 对称加密和非对称加密解决问题
https握手是非对称加密,后面的数据传输是对称加密 https握手过程:
- 客户端请求
- 服务端发送CA证书和公钥
- 客服端验证CA证书,验证通过生成加密随机数,通过公钥加密生成对称密钥发给服务端
- 服务端使用私钥解密,获得对称密钥。后续使用对称密钥传输数据
无法验证对方的身份,容易有中间人攻击 -- CA证书验证身份
无法保证数据完整性 -- 数字签名保证
数字签名验证过程:
- 发送方根据hash算法对原文处理,生成消息摘要,使用私钥加密,连同原文发送给接收方
- 接收方使用hash算法对原文处理,生成消息摘要,使用公钥解密发送方发来的加密内容,获取另一个摘要,两个摘要对比,一致就数据完整
4、get请求和post请求区别
- get请求只能用url编码,post请求有多种
- get请求参数只能为ASC码,post可以是多种格式
- get请求有长度限制,post没有
- get请求浏览器回退可以保留参数,post不行
十二、git
git提交 git add . git commit -m "fix: 由于.hooks钩子限制,无法使用sourcetree提交,只能使用git命令提交" git push origin -u feat-001