HTML
DOCTYPE作用
- 告诉浏览器的解析器应该以什么样的文档标准来解析文档
- 文档解析类型:
CSS1Compat
:标准模式:默认模式,浏览器用W3C的标准来解析渲染页面BackCompat
:怪异模式:浏览器使用自己的怪异模式(旧的,不标准的渲染方式)来解析渲染页面(不声明就默认这个模式)
<script>
<script>
放在<head>
标签中(和在body最前面效果一样):- 当
<script>
标签位于<head>
中时,浏览器会在渲染页面内容之前加载并执行JavaScript
代码。 - 这意味着,如果脚本很大或者加载时间较长,它会阻塞页面的渲染,导致用户看到一个空白页面,直到JavaScript加载并执行完成。
- 为了避免阻塞,可以在
<script>
标签中添加async
或defer
属性,这两个属性都允许浏览器异步加载脚本,但它们在执行脚本时机上有所不同:async
:脚本会在加载完成后尽快执行,但不保证脚本间的执行顺序。也就是说JS脚本在加载完成后立刻执行,如果此时DOM未解析完成,执行过程依然会阻塞DOM解析。- 不保证顺序
- 解析完成立刻执行,可能会阻塞页面渲染
defer
:脚本会在文档解析完成后、DOMContentLoaded
事件触发前按照它们在文档中出现的顺序执行。JS脚本加载完成后,如果此时DOM未解析完成,脚本会暂时挂起,延迟到DOM解析完执行。- 保证顺序
- 脚本解析完成后,待DOM解析完后执行
- 当
<script>
放在<body>
标签中:- 当
<script>
标签位于<body>
的最后时,它允许浏览器先加载页面的内容,然后再加载和执行JavaScript代码。 - 这种方式可以提高页面内容的可视化加载速度,因为用户不需要等待JavaScript代码加载和执行就可以看到页面的内容。
- 这是一种常见的做法,特别是当JavaScript代码不需要在页面渲染之前执行时。
- 当
行内,块元素
- 块级元素:
div , dl , form , h1~h5 , li , ul , p , table ,ol , td , th , tr
- 行内元素:
a , img , span , input , textarea , i , strong , iframe
- 区别:行内元素的宽高,上下边距不可设置,由元素的高速决定
iframe
iframe
标签提供了一种简单的方式将网站嵌入页面- 尽量少用
iframe
标签的原因:iframe
标签加载消耗性能,浏览器的onload
事件在iframe
标签加载完毕后,所以会让别人觉得网页非常慢- 不利于
SEO
,搜素引擎蜘蛛只会看到框架,而无法找到链接,那该网站就会被判定为死网站。
iframe
的利处:- 可以放一些不用被“蜘蛛”抓取的东西,比如说广告,这样即可以放广告,但是又可以不为广告输送权重,一举两得。
img的title与alt属性
title:
鼠标停留在图片上时显示的信息alt:
图片无法正常加载时显示的信息- 在
SEO
层面上,推荐添加上alt属性,因为无法抓取图片,所以需要通过alt来描述这张图的内容
src与href的区别
href:
用于建立当前页面与引用资源之间的联系,遇到href
,浏览器会并行加载后续内容src:
指向的内容将会嵌入到文档中标签所在位置,浏览器需要加载完src
的内容才会继续往下走
html5
- 新增的语义化标签:
header , nav , article , section , aside , footer
- 语义化的作用:
- 即使没有CSS,页面结构也会很清晰
- 有利于SEO,爬虫依据标签来确定上下文和各个关键字的权重
- 便于团队开发和维护,语义化具有可读性
<strong>,<b>和<i>,<em>
- <strong>,<b>都表示加粗的意思,<i>,<em>都表示斜体的意思
- <strong>和<em>都属于语义化标签,有利于搜素引擎,而<b>和<i>只是简单的标签
link和@import的区别
- 加载时机不同:
link
是html
标签,页面加载时link
引入的CSS
同时被加载,也就是在页面加载时同时加载CSS
文件,不会延迟页面渲染。@import
引入的CSS在文档解析完毕后加载,可能导致页面以无样式状态展示。 - 兼容问题:
link
由于是html
标签,所以不存在兼容问题,而@import ie5
以上才兼容 - 权重问题:
link
标签样式的权重大于import
引入的样式 - 其他差异:
link
可以用来加载其他资源,而@import
只能用来加载CSS
webworker
web worker
是运行在后端的js
,独立于其他脚本,不影响页面性能,并通过postMessage
将结果传回主线程,使用方式
创建
web worker
文件web_worker.js
var i=0;
function timedCount()
{
i=i+1;
postMessage(i);
setTimeout("timedCount()",500);
}
timedCount();在其他脚本文件中创建
web worker
对象var w;
w = new Work('web_worker.js')
w.onmessage = function(event) {
console.log(event.data)
}
获取DOM元素方法
HTMLCollection
getElementsByClassName/getElementsByTagName/Node.children
使用这三个方法获取DOM
元素时, 返回的数据结构是HTMLCollection
实时更新:
HTMLCollection
是实时的,如果 DOM 发生变化,HTMLCollection
会自动更新以反映这些变化。类数组:
HTMLCollection
具有length
属性,可以通过索引访问元素,但它不是一个真正的数组,不能直接使用数组的方法(如forEach
、map
等)。
Element
或 null
document.querySelector/element.closest
返回的数据为节点元素。
HTMLElement或null
document.getElementById/element.parentElement
- HTMLElement继承自Element, 表示具体的 HTML 元素节点。
Node
element.parentNode
返回类型为NodeElement
类型继承自Node
,Node
是最基本的 DOM 节点类型,包括元素节点、文本节点、注释节点等。
静态NodeList
- document.
querySelectorAll/document.getElementsByName
该方法返回的数据j结构为静态NodeList
.- 静态的
NodeList
,即使 DOM 发生变化,NodeList
也不会更新 - 类数组:
NodeList
具有length
属性,可以通过索引访问节点,但它不是一个真正的数组,不能直接使用数组的方法(如forEach
、map
等)。
- 静态的
实时NodeList
element.childNodes
该方法返回的数据结构为实时NodeList
- 实时
NodeList
, 意味着如果 DOM 发生变化,NodeList
会自动更新 - 类数组
const container = document.querySelector('.scroll-container');
console.log(container!.childNodes, 'container');
const newParagraph = document.createElement('p');
newParagraph.textContent = 'New paragraph';
container!.appendChild(newParagraph);
// 添加后的状态
console.log(container!.childNodes, 'After adding a new child');- 实时
服务器发送事件(Server-Sent Events,SSE)
SSE
是一种在客户端和服务器之间建立单向连接的技术,允许服务器向客户端推送实时更新,chatGPT
返回的数据 就是使用的SSE
技术服务器端使用
// 服务器端
const express = require('express');
const cors = require('cors');
const app = express();
const port = 3000;
app.use(cors());
app.get('/events', (req, res) => {
// 设置响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const sendEvent = (data) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
// 模拟定时发送事件
const intervalId = setInterval(() => {
sendEvent({ message: 'Hello, this is a server-sent event!' });
}, 5000);
// 清理
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
});
app.listen(port, () => {
console.log(`Server is running at http://localhost:${port}`);
});客户端使用:
- **
Accept: text/event-stream
**:告诉服务器客户端期望接收的是服务器发送事件(SSE)数据流。 - **
Cache-Control: no-cache
**:防止缓存响应。 - **
Connection: keep-alive
**:保持连接活动,允许服务器持续发送事件。
<body>
最新数据:
<div id="data-container"></div>
<script>
// EventSource对象在创建时会自动设置一些默认请求头
const eventSource = new EventSource('http://localhost:3000/events');
const dataContainer = document.getElementById('data-container');
eventSource.onmessage = event => {
const data = JSON.parse(event.data);
console.log('Received SSE:', data);
dataContainer.textContent = `Received data: ${data.message}`;
};
eventSource.onerror = error => {
console.error('SSE error:', error);
eventSource.close();
};
</script>
</body>- **
浏览器不同标签页之间实现通信
- 通过
cookies
,localstorage
等可以存储在浏览器本地的方法 - 通过
webSocket
,webSocket
使用方式和web worker
差不多,只不过webSocket
需要经过一个服务器 - 通过
shareWorker
,shareWorker
可以被多个脚本使用,是浏览器所有页面共享的,使用方式和web worker
差不多,但是这些标签页面必须是同源的
document.onload和document.ready
- 区别:ready表示文档已经加载完成(不包含图片等非文字文件),onload是指页面包括图片等文件在内的所有元素都加载完成。
scrollTop.scrollHeight, offsetHeight, clientHeight区别
scrollTop
,scrollLeft
,: 被卷去的高度或者被卷去的左侧距离scrollHeight
,scrollWidth
:: 总宽高, 包括溢出的部分offsetHeight
,offsetWidth
: 可视区域宽高, 包括内容、内边距、边框和滚动条。clientHeight
,clientHeight
: 可视区域宽高, 只包含内容和内边距- 菜鸟教程的图
CSS
CSS初始化
初始化原因:由于浏览器兼容问题,不同浏览器对不同标签的默认值时不同的,如果没有CSS初始化,在不同浏览器之间的页面会有差异。初始化
CSS
样式可以提高编码质量,保持代码的统一性基本的初始化代码
*{/*把所有标签的内外边距取消掉*/
margin: 0px;
padding: 0px;
}
ul{/*去掉ul的小圆点*/
list-style: none;
}
body{/*把主体设为统一格式*/
font-size: 12px;
font-family: "微软雅黑";
color:#716f70 ;
}
a{/*改变a链接的默认格式,颜色和下划线*/
color: #716f70;
text-decoration: none;
}
a.hover{/*改变鼠标经过颜色*/
color: skyblue;
}
盒子模型
- 盒子模型包括:内容
(content)
,内边距(padding)
,外边距(margin)
,边框(border)
- 盒子模型分类:
W3C
标准盒子模型:属性width
和height
只包含content
,不包含border
和padding
IE
盒模型:属性width
和height
为content+border+padding
- 通过
box-sizing
控制,默认值为content-box
(W3C
盒模型),可取值为border-box
(IE
盒模型)
常见的元素隐藏方式
dislay:none
- DOM节点依然存在,但是不占空间,会引起回流(重构),且子元素无条件被隐藏
visibility:hidden
- 占空间,只会引起重绘,子元素设置visibility:visibile可以被看到,不会响应用户的交互事件。
opacity:0
- 占空间,会引起重绘,子元素无条件被隐藏,仍然会响应用户交互事件(如点击、悬停)。
z-index
设为负值,使其位于其他元素下方- 不会引起回流和重绘,元素及其子元素依然可以响应用户的交互事件,可以使用
pointer-events: none
来阻止其响应交互事件。
- 不会引起回流和重绘,元素及其子元素依然可以响应用户的交互事件,可以使用
transform:scale(0,0)
,将元素缩放为0来隐藏- DOM节点依然存在,只是缩放到0大小,由于布局没变,所以只会引起重绘,元素在布局中仍占空间,但是实际尺寸为0,子元素也会被隐藏,但仍然会响应用户交互事件。
回流和重绘
- 当
html
元素加载前会生成DOM
树,CSS
会生成Style Rules
(样式规则),这两个都生成后合并在一起生成Render Tree
(渲染树),最终通过渲染树展示到页面上 - 回流:是指当页面中的某些元素的几何属性发生变化(如尺寸,规模,布局,隐藏等),浏览器需要重新计算元素的布局,以及其在页面上的确切位置和大小。这会影响到元素的子元素和相邻元素,因为它们的布局可能也会因此而变化。浏览器之后会重构这部分渲染树,完成回流后,浏览器将这部分重绘到屏幕中。
- 引起回流的情况:
- 添加或者删除可见的DOM元素
- 元素位置改变,元素尺寸改变–边距、填充、边框、宽度和高度,内容改变
- 浏览器窗口尺寸变化,导致响应式布局调整。
- 重绘:渲染树中一些元素更新了属性,这些属性不会影响元素布局的,比如背景颜色,字体颜色,只需要重绘即可,而不需要重新计算布局。
- 引起重绘的情况:
- 元素颜色、背景色、边框颜色等视觉样式的变化。
- 元素透明度的改变。
- 不影响布局的CSS属性更新,如
box-shadow、outline
等。
- 回流必引起重绘,重绘不一定引起回流
- 减少回流和重绘f方式
- 批量修改样式,尽量减少对 DOM 的多次操作,可以将多个样式修改合并成一次操作或者添加一个类来进行批量更改。
- 将 DOM 操作放在
requestAnimationFrame
回调中,确保在浏览器下一次重绘之前执行,这样所有的DOM操作会被批处理,而不是分散在多个帧中。 - 避免频繁读取和修改依赖于布局的属性,某些属性和方法会强制浏览器同步计算布局
offsetWidth
、offsetHeight
、clientWidth
、clientHeight
等。所以获取元素的宽高也会引起回流- 可以使用**
getBoundingClientRect
**一次性获取元素的多个宽高属性
- 可以使用**
- 使用虚拟 DOM
BFC(Block Formatting Context)
BFC:块级格式化上下文,是CSS的一种渲染机制,BFC就相当于是一个容器,在特定情况下会触发BFC
触发BFC条件:
- 根元素(body)
- 浮动元素 (元素的 float 不是 none)
- 绝对定位元素 (元素具有 position 为 absolute 或 fixed)
- 内联块,表格单元格,表格标题 (元素display 的值为 inline-blocks、table-cells、table-captions)
- 具有overflow 且值不是 visible 的块元素,
……
BFC的特性:
- 计算BFC的高度时,浮动元素也参与计算
- BFC区域不会与float区域重叠
- 垂直方向的距离由外边距决定,BFC可以阻止外边距折叠(Margin collapsing),即相邻的块级元素的垂直外边距不会合并。
……
BFC的使用
解决子组件浮动,父组件高度塌陷问题:下面的代码,父组件就会塌陷,给父组件加上overflow:hideen,即可解决,根据触发BFC条件第e条以及BFC特性第a条
<div class = 'box'>
<div class = 'box1'>
</div>
</div>
<style>
.box {
width: 300px;
background-color: red;
//overflow:hidden
}
.box1 {
float: left;
width: 100px;
height: 100px;
background-color: yellow;
}
</style>解决外边距塌陷问题(外边距合并):下面这段代码,想象的是box顶格,box1距离顶部20px,但是实际情况是box和box1都距离上方20px,这是因为外边距合并,选取最大的作为外边距。解决方法,给box加上overflow:hideen即可。根据触发BFC条件第e条以及BFC特性第c条
<div class = 'box'>
<div class = 'box1'>
</div>
</div>
<style>
.box {
width: 300px;
background-color: red;
//overflow:hidden
}
.box1 {
margin-top:20px;
width: 100px;
height: 100px;
background-color: yellow;
}
</style>自适应两列布局:想让box2自适应剩余的宽度,但是实际情况是box2一部分被压在box1下面。实现方法:在box2加上overflow:hideen即可。根据触发BFC条件第e条以及BFC特性第b条
<div class="box">
<div class="box1"></div>
<div class="box2"></div>
</div>
<style>
.box {
height: 500px;
width: 500px;
background-color: red;
}
.box1 {
float: left;
width: 100px;
height: 100px;
background-color: yellow;
}
.box2 {
/* overflow: hidden; */
height: 200px;
background-color: blue;
}
</style>
清除浮动(防止高度塌陷)
子元素浮动后,父元素如果没设置高度,父元素高度就会为0,影响后续布局
给父元素添加高度
额外标签法:想要清除谁的浮动就在该标签后面加一个空白标签(为浮动元素添加一个兄弟标签),设置其样式clear:both
<div class="box">
<div class="box1"></div>
<div class="box2"></div>
</div>
<style>
.box {
width: 500px;
background-color: red;
}
.box1 {
float: left;
width: 100px;
height: 100px;
background-color: yellow;
}
.box2 {
clear:both
}
</style>clear:both
原理:``clear:both的意思为元素左右两边都不允许有浮动元素,所以在
box1浮动后,
box2应该和
box1在一行,但是由于其添加了
clear:both,所以
box2不能和
box1在一行,
box2就会换行,此时
box1就相当于
box2的
margin-top`,所以父元素的高度就撑开了给父元素添加``overflow:hidden`触发BFC方式
单伪元素清除法
<div class="box">
<div class="box1"></div>
</div>
<style>
.box {
width: 500px;
background-color: red;
}
.box1 {
float: left;
width: 100px;
height: 100px;
background-color: yellow;
}
.box:after{
content: "";
display: block;
height: 0;
clear:both;
visibility: hidden;
}
</style>双伪元素清除法
<div class="box">
<div class="box1"></div>
</div>
<style>
.box {
width: 500px;
background-color: red;
}
.box1 {
float: left;
width: 100px;
height: 100px;
background-color: yellow;
}
.box:after,
.box:before{
content: "";
display: table;
}
.box:after {
clear: both;
}
/* .box {
*zoom: 1; IE浏览器特有属性,代表缩放比例
} */
</style>
垂直居中
vertical-align: middle
实现垂直居中,注意事项:- 垂直居中的元素必须是行内块元素
- 必须有兄弟元素,且兄弟元素高度为父元素的100%,必须是行内块,必须
vertical-align: middle
- 如果想要居于屏幕中间,可以在父元素上
text-align:center
<style>
.box {
width: 500px;
height: 500px;
background-color: red;
}
.box1 {
display: inline-block;
vertical-align: middle;
width: 100px;
height: 100px;
background-color: yellow;
}
.box2 {
display: inline-block;
vertical-align: middle;
height: 100%;
background-color: aqua;
}
</style>
<div class="box">
<div class="box1"></div>
<div class="box2"></div>
</div>利用伪元素实现垂直居中,道理等同于第一种方法.
<style>
.box {
width: 500px;
height: 500px;
background-color: red;
}
.box:after {
content: '';
display: inline-block;
vertical-align: middle;
height: 100%;
}
.box1 {
display: inline-block;
vertical-align: middle;
width: 100px;
height: 100px;
background-color: yellow;
}
</style>
<div class="box">
<div class="box1"></div>
</div>利用隐藏元素实现垂直居中,注意事项:
- 要隐藏的节点必须在垂直居中显示的节点的前面
- 要隐藏的节点高度为剩余高度的一半,比如box1要占50%,剩余50%,所以box2占一半25%
<style>
.box {
width: 500px;
height: 500px;
background-color: red;
}
.box1 {
width: 100px;
height: 50%;
background-color: yellow;
}
.box2 {
height: 25%;
}
</style>
<div class="box">
<div class="box2"></div> <!--隐藏的元素-->
<div class="box1"></div>
</div>利用
line-height
, 适合子元素为文本元素,使子元素的line-height
等于父元素的高度<style>
.box {
width: 500px;
height: 500px;
background-color: red;
}
.box1 {
background-color: yellow;
line-height: 500px;
}
</style>
<div class="box">
<p class="box1">ppppppppppppppppppppp</p>
</div>利用定位和
transform
实现flex
布局
权重
important
:无限大- 行间:1000
id
:100class
,属性选择器,伪类:10- 标签,伪元素:1
- 通配符(*),子选择器(>),相邻选择器(+),兄弟选择器(~):0
- 当两个选择器的权重完全相同时,CSS遵循“后来居上”原则,即最后定义的样式会覆盖之前的样式。
属性继承
- 可继承的CSS属性:字号,字体,颜色
- 不可继承的属性:边框,内外边距,宽高
CSS单位
em
: 作为font-size时,代表父元素的字体大小,作为其他属性时,相当于应用在当前元素的字体尺寸,一些浏览器默认字体大小为16px<style>
p {
font-size:20px;
line-height:2em; //就是40px
}
</style>
<p>hello</p>rem
: 相对于跟元素的字体大小,作用于跟元素,则相对于原始的字体大小,也就是默认的字体大小,作用于非根元素,则相对于根元素的字体大小.<style>
html {
font-size:2rem //相对于默认的字体大小(16px),也就是32px
}
p {
font-size:1rem; //相对于根元素字体大小,也就是32px
line-height:2em; //64px
}
</style>
<p>hello</p>
CSS3新增选择器
- 属性选择器
element[alt]
:所有带alt属性的元素element[alt^='123']
:所有带alt属性且以123开头的元素
- 结构伪类选择器
element:first-child
:匹配第一个子元素element:last-child
:匹配最后一个子元素element:nth-child(n)
:匹配父元素中第N个子元素
- 伪元素选择器
::after
::before
- 注意:
- 创建的是行内元素
- 伪元素必须有
content
属性,可以为空,但是不能没有 - 权重为1
让谷歌浏览器支持小于12px的文字
zoom
属性:放大或缩小图像p {
font-size: 12px;
zoom: 0.5; //字体就是6px
}-weblit-transform:scale()
,但是该属性只能作用于可以定义宽高的元素span {
font-size: 12px;
-webkit-transform: scale(0.5);//6px
display: inline-block;
}
文本溢出显示省略号
- 单行文本溢出
overflow: hidden;(文字长度超出限定宽度,则隐藏超出的内容) |
多行文本溢出:
-webkit-line-clamp: 2;(显示的行数)
display: -webkit-box;(将对象作为弹性伸缩盒子模型显示 )
-webkit-box-orient: vertical;(设置伸缩盒子垂直排列 )
overflow: hidden;(文本溢出限定的宽度就隐藏内容)
text-overflow: ellipsis;(多行文本的情况下,用省略号 “…” 隐藏溢出范围的文本)
flex三合一属性
**
flex-grow
**:定义弹性盒子项(flex item)的放大比例。默认值为0
,意味着如果存在剩余空间,也不放大。**
flex-shrink
**:定义弹性盒子项的缩小比例。默认值为1
,意味着如果空间不足,该项目将缩小,0代表不会被缩放。**
flex-basis
**:定义弹性盒子项在分配多余空间之前的默认大小。可以是长度(例如%
、px
、em
等)或关键字auto
,默认值为auto
。flex-basis
定义了 flex 项目在主轴方向上的初始大小。这个初始大小是在应用flex-grow
和flex-shrink
之前确定的。在计算 flex 项目的最终大小时,flex-basis
是一个重要的参考值。如果flex-basis
设置为一个具体的值(如100px
),则 flex 项目会首先尝试占据这个大小的空间。然后,根据flex-grow
和flex-shrink
的设置,项目可能会增长或缩小。flex: <flex-grow> <flex-shrink> <flex-basis>;
grid布局
Grid 布局主要用于创建复杂的网页布局,例如多列布局、响应式布局等。
使用
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Grid 布局示例</title>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
}
.grid-container {
display: grid;
grid-template-columns: 1fr 3fr; /* 两列,第一列占1份,第二列占3份 */
grid-template-rows: auto 1fr auto; /* 三行,第一行和第三行自动高度,第二行占剩余空间 */
grid-template-areas:
'header header'
'menu content'
'footer footer';
gap: 10px; /* 网格线之间的间距 */
height: 100vh; /* 占满整个视口高度 */
}
.header {
grid-area: header;s
background-color: #f44336;
padding: 20px;
text-align: center;
color: white;
}
.menu {
grid-area: menu;
background-color: #e74c3c;
padding: 20px;
color: white;
}
.content {
grid-area: content;
background-color: #ecf0f1;
padding: 20px;
color: #333;
}
.footer {
grid-area: footer;
background-color: #2c3e50;
padding: 20px;
text-align: center;
color: white;
}
</style>
</head>
<body>
<div class="grid-container">
<div class="header">Header</div>
<div class="menu">Menu</div>
<div class="content">Content</div>
<div class="footer">Footer</div>
</div>
</body>
</html>
屏幕响应式方法
- 媒体查询(Media Queries)
- 弹性盒子(Flexbox)
- 视窗单位(Viewport Units: vw, vh, vmin, vmax)
- 百分比单位(%)
- 使用rem单位
- 插件实现:原理和5一样,只不过是自动将px转换为rem
伪类和伪元素
- 伪类(Pseudo-classes):
- 用于定义元素的特定状态,比如鼠标悬停、聚焦、选中等状态。
- 伪类用冒号
:
来表示,例如:hover
、:focus
、:active
等。 - 示例:
a:hover { color: red; }
表示当鼠标悬停在链接上时,链接变为红色。
- 伪元素(Pseudo-elements):
- 用于创建一些不在文档树中的元素部分,允许我们对这些部分应用样式,比如元素的第一个字母、第一行或者在元素前后添加内容等。
- 伪元素用双冒号
::
来表示,以区别于伪类,例如::before
、::after
、::first-letter
等。 - 示例:
p::first-letter { font-size: 200%; }
表示段落的第一个字母字体大小为200%。
animation
、transition
和 transform
animation
用途:用于创建复杂的动画效果,可以控制元素在多个关键帧之间的变化。
使用:
.element {
animation: fadeIn 2s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}特点
- 可以定义多个关键帧(
@keyframes
)。 - 支持循环播放、反向播放、延迟播放等复杂效果。
- 适用于需要多个步骤或复杂动画的场景。
- 可以定义多个关键帧(
transition
用途:用于在元素状态改变时(如鼠标悬停、点击等)平滑地过渡到新的样式。
使用:
.element {
transition: background-color 0.5s ease;
}
.element:hover {
background-color: red;
}特点
- 只能在两个状态之间进行过渡。
- 适用于简单的状态变化,如颜色、尺寸、位置等。
- 不需要定义关键帧,只需指定过渡的属性、持续时间和缓动函数。
transform
用途:用于对元素进行变形操作,如旋转、缩放、平移等。
使用:
.element {
transform: rotate(45deg);
}
/* 结合使用 */
.element:hover {
transform: scale(1.2);
transition: transform 0.5s ease;
}特点
- 提供了多种变形操作,如平移、缩放、旋转、倾斜等。
- 可以与其他属性(如
transition
和animation
)结合使用,实现更复杂的动画效果。 - 性能较好,因为变形操作通常由 GPU 处理。
position移动和transform移动的性能比较
- GPU 加速:
transform
属性通常会利用 GPU(图形处理单元)进行硬件加速,这使得变换操作(如平移、旋转、缩放等)更加高效和平滑。position
属性的变化通常由 CPU(中央处理单元)处理,可能会导致重排(reflow)和重绘(repaint),这会影响性能,尤其是在处理复杂布局或大量元素时。
- 重排和重绘:
- 使用
position
改变元素的位置时,浏览器可能需要重新计算布局(重排)和重新绘制页面(重绘),这会增加性能开销。 - 使用
transform
改变元素的位置时,通常只会触发重绘,而不会触发重排,因为transform
不会改变元素在文档流中的位置。
- 使用
JavaScript
ES6语法
- 解构赋值
- 箭头函数
- 扩展运算符
- 模板字符串
- Map和Set
- class类
解构赋值
- 对于基本类型,解构赋值赋的是值,改变解构后的变量不会影响源对象。
- 对于引用类型,解构赋值赋的是引用,改变解构后的变量会影响源对象中的属性。
数组解构赋值原理
数组解构赋值就是调用属性的
Symbol.iterator
方法,返回一个迭代器,然后不停调用迭代器的next()方法,然后依次将返回对象的value值赋值给解构的属性,直到返回的对象done属性为true属性的
Symbol.iterator
方法其实就是个生成器方法,*表示生成器方法,执行生成器函数会返回一个迭代器对象,当调用迭代器的next方法时,会返回一个对象,对象有两个属性,一个是value,是yield后面的值,一个是done,表示迭代是否结束。function* () {
yield this.x;
yield this.y;
}当我们使用数组的解构方法对一个对象进行解构赋值时,由于对象不具有迭代器方法,所以会提示对象不可迭代,所以我们可以手动为其添加
Symbol.iterator
方法,完成解构interface IterableObject {
x: number;
y: number;
[Symbol.iterator]: any;
}
const obj: IterableObject = {
x: 1, y: 2,
[Symbol.iterator]: function* () {
yield this.x;
yield this.y;
}
};
const [x, y] = obj;
console.log(x, y);// 1,2
class
ES5对象创建和继承
Object构造函数模式
缺点:语句太多
//创建
var p = new Object()
p.name = 'zhangsan'
p.age = 13
p.setName = function(name) {
this.name = name
}
//使用
p.setName('lisi')
console.log(p.name , p.age) //lisi 13
对象字面量
缺点: 如果创建多个对象,有重复代码
//创建
var p = {
name:'zhangsan',
age:13,
setName:function(name) {
this.name = name
}
}
//使用
p.setName('lisi')
console.log(p.name , p.age) //lisi 13
工厂模式
工厂函数:返回一个对象的函数,都可以称为工厂函数
对象没有个具体的类型,都是Object类型
//创建
function createPerson(name , age) {
var obj = {
name,
age,
setName:function(name) {
this.name = name
}
}
return obj
}
//使用
createPerson('zhangsan',13)
createPerson('lisi',14)
自定义构造函数模式
p是Person类型的,每个对象都有其固定的类型
缺点:每创建一个实例,都会创建一个新的
setName
方法。解决方法:将相同的数据放入原型中
//创建
function Person(name , age){
this.name = name
this.age = age
this.setName = function(name) {
this.name = name
}
}
//使用
var p = new Person('zhangsan' , 13)
p.setName('lisi')
console.log(p.name , p.age) //lisi 13
构造函数+原型
构造函数中只初始化一般函数,相同的函数放入原型当中. 所有Person实例共享一个
setName
方法。//创建
function Person(name , age){
this.name = name
this.age = age
}
Person.prototype.setName = function(name) {
this.name = name
}
//使用
var p = new Person('zhangsan' , 13 )
继承
步骤
- 定义父类型构造函数
- 给父类型添加方法
- 定义子类型的构造函数
- 关键:创建父类型的实例赋值给子类型的原型
- 创建子类型实例,可以调用父类型的方法
//1. 定义父类型构造函数
function Father() {
this.fatherData = 'father data'
}
//2. 给父类型添加方法
Father.prototype.showFatherData = function () {
console.log(this.fatherData);
}
//3. 定义子类型的构造函数
function Son() {
this.sonData = 'son data'
}
Son.prototype.showSonData = function () {
console.log(this.sonData);
}
//4.创建父类型的实例赋值给子类型的原型
Son.prototype = new Father()
//5.创建子类型实例,可以调用父类型的方法
let son = new Son()
son.showFatherData() //father data
//由于son的原型对象指向了Father的实例,所以son.constructor为Father
console.log(son.constructor)//Father
//但是可以这样修改
Son.prototype.constructor = Son
ES6
Class
:constructor
中的属性为实例对象的属性class Person {
constructor(name , age){
this.name = name
this.age = age
}
sayHello() {
console.log('Hello MyName is' + this.name )
}
}
var p1 = new Person('zs' , 13)
p1.sayHello()extends
: super是调用父类的构造函数,必须写在子类this之前class Person {
constructor(name, age) {
this.name = name
this.age = age
}
sayHello() {
console.log('Hello MyName is ' + this.name)
}
}
class Student extends Person {
constructor(name , age){
super(name , age)
}
sayAge() {
console.log('My Age is ' + this.age)
}
}
let stu = new Student('zs' , 13)
stu.sayHello() // Hello MyName is zsstatic
: 声明的属性和方法只能被类调取和使用,和实例对象无关class Person {
static a = 0
constructor(name, age) {
this.name = name
this.age = age
}
static test() {
console.log('test')
}
}
let per = new Person('zs', 13)
Person.test() //test
console.log(per.name, per.age, Person.a)//as 13 0
Map的方法
map
是由key-value
组成的集合,并且key-value可以是任意数据类型,普通对象的key只能为字符串或者Symbol类型Map
的内部实现通常基于哈希表(Hash Table)或哈希映射(Hash Map)。这种数据结构允许高效的键值对存储和检索操作- 定义:
const map = new Map()
map
方法map.get(key):
通过key获取对应的valuemap.set(key , value):
向map中添加元素map.values():
返回一个新的Iterator
对象,该对象中的内容为map中的每一个value
变量提示,函数 提升
变量提升和函数提升是指在代码执行之前,变量和函数声明会被提升到其所在作用域的顶部。这意味着你可以在声明之前使用它们,但它们的行为会有所不同
变量提升
变量提升是指变量声明(**使用
var
**)会被提升到其所在作用域的顶部,但初始化(赋值操作)不会被提升。因此,在变量声明之前使用该变量会得到undefined
。console.log(a); // 输出: undefined
var a = 10;
console.log(a); // 输出: 10
函数提升
函数提升是指使用
function
进行声明的函数会被提升到其所在作用域的顶部。因此,你可以在函数声明之前调用该函数。foo(); // 输出: "Hello, world!"
function foo() {
console.log("Hello, world!");
}函数表达式不会被提升, 即使是使用
var
进行声明的bar(); // TypeError: bar is not a function
var bar = function() {
console.log("Hello, world!");
};
var
特性
浏览器环境声明的全局变量会直接挂在到
window
上, 在Vuejs
中不会成为window
的属性函数作用域: 函数内部声明的var变量在函数外部访问不到, 但是只要在函数内部就可以访问到
function test() {
if (true) {
var x = 5;
}
console.log(x); // 输出: 5
}
test();重复声明:
var
允许在同一作用域内重复声明同一个变量。
var声明的变量和函数重名问题
在预编译阶段,优先级
- 参数
- 函数声明
- 变量声明
函数声明优先级高于变量声明,并且函数声明不会被变量声明覆盖,但是会被变量赋值覆盖
function a(){}
var a = 120
console.log(a) //120
console.log(a()) //报错先声明
function a
, 由于a已经声明过了,所以var a不会再次声明,但是之后执行a = 120,所以 a变为变量
函数传入参数与函数内部声明变量重名
在编译阶段,参数提升>函数提升>变量提升,并且每个变量只能被声明一次,所以在fun函数内部,参数a先声明,var a声明无效,但是赋值语句有效,所以a为50
var a = 100
function fun(a){
var a = 50
console.log(a)
}
fun(a)
综合
在函数内部,由于已经有了参数
a
,所以var a
和function a
都不会再次声明,但是函数的赋值会生效,所以a
变为函数,因此第一次输出为function
,第二次输出由于执行了赋值语句,所以a变为变量,所以第二次输出为40var a = 100
function fun(a){
console.log(a) // function a
var a = 40
console.log(a)
function a(){}
}
fun(a)
原型与原型链
- 每个对象都与另一个对象相关联,关联的对象就是原型。每个函数对象在创建的时候都会被添加一个属性
Prototype
,该属性指向的对象,称之为原型对象,原型对象用来继承属性和方法,当试图访问一个对象的属性或方法时,如果这个对象本身没有定义该属性或方法,JavaScript
引擎会继续在其原型对象中寻找。如果原型对象也没有,那么这个查找过程会继续在原型的原型上进行,直到找到该属性或方法,或者查找到达null
(原型链的终点)。 - 原型链:通过实例对象的
__proto__
可以找到其构造函数的原型的值,在对象的属性查找过程中,如果在当前对象中找不到某个属性或方法,JavaScript
引擎会继续在__proto__
指向的原型对象中查找,这个过程会一直沿着原型链向上直到找到该属性或方法,或者直到__proto__
指向null
,会返回undefined
。 - 显式原型属性:
prototype
,是函数特有的 - 隐式原型属性:
__proto__
,是对象特有的 - 函数是特殊的对象
- 对象的隐式原型的值,为其对应的构造函数的显式原型的值
Object.getPrototypeOf
(实例对象) === 实例对象.__proto
__
浏览器相关
数据类型
基础类型
String,Number,Boolean,null,undefined,symbol
包装类型
将
String , Number , Boolean
包装成对象let str = new String('123')
let number = new Number(123)
let boolean = new Boolean(true)
console.log(typeof(str) , typeof(number) , typeof(boolean)) //object object object目的:使得对象可以覆盖javascript所有的值,使其有一个通用的数据模型。并且对象的一些方法也可以直接使用
null和undefined
null
null
是被明确地赋值为空值,表示啥都没有null
是一个对象类型,typeof(null)=object
- 通常用于表示一个变量应该有一个对象值,但目前为空
undefined
undefined
通常是一个变量已经被声明,但是没有赋值undefined
是一个原始类型。typeof(undefined) = undefined
类型
null !== undefined
但是null == undefined
undefined == false
为false
null == false
为false
null
运算时被转换成0,但是null == 0
为false
,undefined
在运算时被转换为NAN
关于隐式类型转换
字符串和数字比较时,字符串转数字
数字与布尔比较时,布尔转数字
字符串和布尔比较时,两者转数字
数字和字符串进行数学运算时,字符串总是尝试转换成数字,除非转换不可能,这时会得到
NaN
,进行拼接时,数字转换为字符串console.log('5' + 2); // "52", 字符串拼接
console.log('5' - 2); // 3, 字符串转换为数字然后进行减法运算
console.log('5' * 2); // 10, 字符串转换为数字然后进行乘法运算
console.log('5' / 2); // 2.5, 字符串转换为数字然后进行除法运算
console.log('hello' - 2); // NaN, 字符串无法转换为数字
判断数据类型
typeof
对于基本数据类型,除了
null(typeof(null)为object)
,都可以返回正确的结果对于引用数据类型,除了
function,Date , RegExp(),
其他的(数组等)都返回的是原型链最顶端的Object
类型typeof(Date) 'function'
typeof(RegExp) 'function'
typeof(Function) 'function'
typeof([]) 'object'
typeof({}) 'object'
instanceof
使用方式:
A instanceof B
下面代码可以看出,[]即是Array类型,又是Object类型,为什么呢?
- 通过原型与原型链可知,[]的隐式原型与Array的显式原型都指向其原型对象
- 其原型对象的隐式原型对象与Object的显示原型都指向Object的原型对象,所以判断两个对象是否属于实例关系,不能判断一个对象的实例是哪种类型。
- 其他的归根结底都可以认为是Object的实例
console.log([]instanceof Array) //true
console.log([] instanceof Object) //true
console.log(new RegExp instanceof RegExp) //true
console.log(new RegExp instanceof Object) //true
console.log(new Date instanceof Date) //true
console.log(new Number instanceof Number) //true
console.log(new Number instanceof Object) //true
constructor
实例对象的
constructor
指向其构造函数:[].constructor === Array
,prototype
和constructor
之间的关系,每个函数(Array
)都有一个属性prototype
,它指向函数的原型,而函数的原型中也有一个属性constructor
,它也是一个对象,constructor
指向构造函数本身。所以[].__proto__.constructor,Array.prototype.constructor,[].constructor都是Array。但是Array.constructor不是Array是function,原因见第4条
'123'.constructor === String //true
new Number(1) === Number //true
new Error().constructor === Error //true
new Date().constructor === Date //true
[].constructor === Array注意:
null和undefined
是无效的对象,因此不会有constructor
存在每个构造函数
(Array , Number , String , Date....)
的隐式原型都指向Function的显式原型,也就是Array.__proto__ === Function.prototype,因为每个构造函数都是Function的实例化对象与
instanceof
区别:- constructor用于获取对象的构造函数,简单直接,但是容易被修改,可靠性较低
- instanceof用户检测对象是不是某个构造函数的实例,并且是通过对象的原型链进行查找的,更安全
function Person(name) {
this.name = name;
}
const person = new Person('Alice');
// 使用 instanceof
console.log(person instanceof Person); // true
console.log(person instanceof Object); // true
// 使用 constructor
console.log(person.constructor === Person); // true
console.log(person.constructor === Object); // false
// 修改 constructor 属性
person.constructor = null;
console.log(person.constructor === Person); // false
console.log(person.constructor === null); // true
// 修改原型链
Person.prototype = {};
const person2 = new Person('Bob');
console.log(person2.constructor === Person); // false
console.log(person2.constructor === Object); // true
toString
toString
是Object
原型上的方法使用方式:对于
Object
对象,直接调用toString()
就能返回[object Object]
。而对于其他对象,则需要通过call / apply
来调用才能返回正确的类型信息。Object.prototype.toString.call('123') // [object String]
Object.prototype.toString.call([]) //'[object Array]'
深拷贝与浅拷贝
浅拷贝,复制了一层引用,两者指向同一个值
深拷贝,两者毫无关联,互不影响
实现深拷贝的方式:
展开运算符,引用类型为浅拷贝,基本类型为深拷贝
let a = {
name:"zhangsan",
age:18
}
let b = {...a}
b.age = 19
console.log(a.age , b.age) //18 19JSON.parse(JSON.stringify(待拷贝对象)
,缺点如下:不能拷贝函数,当对象中包含函数,拷贝后会丢失
let obj = {
data: "example",
action: () => console.log("action")
};
let clone = JSON.parse(JSON.stringify(obj));
console.log(clone); // 输出:{data: "example"},action函数丢失会忽略
undefined和symbol
类型的值let obj = {
undef: undefined,
sym: Symbol("example")
};
let clone = JSON.parse(JSON.stringify(obj));
console.log(clone); // 输出:{},undef和sym都被忽略了忽略对象的原型链,原型上的方法会丢失
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log("Hello, " + this.name);
};
let person = new Person("John");
let clone = JSON.parse(JSON.stringify(person));
console.log(clone.greet); // 输出:undefined,greet方法丢失无法复制特殊对象:
Date
、RegExp
、Map
、Set
等,在经过JSON.stringify
和JSON.parse
后,无法保持其原有的结构和功能。例如,Date
对象会被转换为字符串,Map
和Set
会被转换为普通对象.let obj = {
date: new Date()
};
let clone = JSON.parse(JSON.stringify(obj));
console.log(typeof clone.date); // 输出:string,Date对象被转换为了字符串let obj = {
regex: /example/g
};
let clone = JSON.parse(JSON.stringify(obj));
console.log(clone.regex); // 输出:{},RegExp对象丢失了let obj = {
set: new Set(),
map: new Map()
}
let clone = JSON.parse(JSON.stringify(obj))
console.log(clone); // 输出:{map:{},set:{}},而obj为}{map: Map(0) {size: 0},set: Set(0) {size:0}}slice和concat
方法,缺点:只能深拷贝一层,再深层就是浅拷贝了手写递归实现
function extend(origin, deep){
// deep true 启动深拷贝
// false 浅拷贝
let obj = {}
// 数组对象
if(origin instanceof Array){
// 如果是数组,obj就得是数组
obj = []
}
for(let key in origin){
let value = origin[key]
// 确定value是不是引用型,前提是deep 是true
obj[key] = (!!deep && typeof value === "object" && value !== null) ? extend(value, deep) : value
}
return obj
}
事件捕获,冒泡与委托
含义
事件捕获:从
window
开始,一级一级往下传递,直到触发事件的节点,该阶段不会触发事件事件冒泡:从当前节点开始触发事件,直到window为止
事件委托:利用事件冒泡,将子元素的事件绑定到父元素身上,当点击子元素,事件都会冒泡到父元素
会触发冒泡的事件:
click、mouseover、mouseout、keydown、keyup、keypress、mousedown,mousemove、scroll
等不会触发冒泡的事件:
blur,foucus,mouseenter,mouseleave
阻止事件冒泡:
event.stopPropagation()
阻止默认事件(例如:点击链接自动跳转):
e.preventDefault()
在给事件添加监听的函数
addEventListener
,第三个参数若为false,则支持冒泡,不支持捕获,为true则相反<div class="box">
<div class="box1"></div>
</div>
//最后点击子元素输出为 :父元素 子元素
<script>
let box = document.querySelector('.box')
let box1 = document.querySelector('.box1')
box1.addEventListener('click' , function() {
console.log('子元素');
} , true)
box.addEventListener('click' , function() {
console.log('父元素');
} , true)
</script>事件委托的优缺点
- 优点:
- 减少了事件监听器
- 减少内存消耗
- 为之后添加的子元素动态绑定事件
- 缺点:
- 事件委托基于事件冒泡,对不冒泡的事件不支持
- 层级过多,冒泡过程中会被某层阻止掉
- 优点:
排序算法
时间复杂度是 O(nlogn)的排序算法包括归并排序、堆排序和快速排序(快速排序的最差时间复杂度是 O(n^2))
冒泡排序
时间复杂度:
O(n^2)
function sort(arr) {
//控制趟数
for(let i = 0; i < arr.length; i++) {
//控制比较次数,后面排好的不用比较,所以这里要减去i
for(let j = 0; j < arr.length - 1 - i; j++) {
if(arr[j] > arr[j+1]) {
let temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
}
return arr
}
console.log(sort([1,4,5,6,3,9,7,5,3,1]))
快速排序
选一个基准值,比基准值小的放左边,大的放右边,第一趟结束后在两边重复此操作
function sort(arr) {
if (arr.length < 2) {
return arr
}
//取中间的数为基准
let index = arr.splice(Math.floor(arr.length / 2), 1)[0]
let left = []
let right = []
for (let i = 0; i < arr.length; i++) {
if (arr[i] < index) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return sort(left).concat([index], sort(right))
}
console.log(sort([7, 6, 4, 8, 9, 3, 4, 1, 6, 3, 6]))
选择排序
从未排序的序列中找出最小的与第一个进行交换,之后在剩下的未排序的序列中找最小的与第一个交换,以此类推,直到最后一个元素。
function sort(arr) {
let minIndex
for(let i = 0; i < arr.length; i++) {
minIndex = i
for(let j = i+1; j < arr.length; j++) {
if(arr[j] < arr[minIndex]) {
minIndex = j
}
}
let temp = arr[minIndex]
arr[minIndex] = arr[i]
arr[i] = temp
}
return arr
}
console.log(sort([7, 6, 4, 8, 9, 3, 4, 1, 6, 3, 6]))
插入排序
维护一个有序序列,初始时有序序列只有一个元素,每次将一个新的元素插入到有序序列中,将有序序列的长度增加 1,直到全部元素都加入到有序序列中。
从未排序的记录中第一个开始,依次与排好序的记录进行比较,找到合适的位置插入
function sort(arr) {
//因为第一个是肯定是有序的,所以从下标为1的第二个开始
for(let i = 1; i < arr.length; i++) {
let key = arr[i]
let j = i - 1
while(key < arr[j]) {
arr[j+1] = arr[j]
j--
}
arr[j+1] = key
}
return arr
}
console.log(sort([7, 6, 4, 8, 9, 3, 4, 0 , 1, 6, 3, 6]))
归并排序
拆分成小块,确保每一部分是有序的
function merge(left, right) {
var result = []
while (left.length > 0 && right.length > 0) {
if (left[0] < right[0]) {
result.push(left.shift())
} else {
result.push(right.shift())
}
}
return result.concat(left, right)
}
function sort(arr) {
if (arr.length <= 1) {
return arr
}
var mid = Math.floor(arr.length / 2)
var left = sort(arr.slice(0, mid))
var right = sort(arr.slice(mid))
return merge(left, right)
}
console.log(sort([7, 6, 4, 8, 9, 3, 4, 0, 1, 6, 3, 6]))
通过new创建对象的过程
创建一个空对象:创建一个新的空对象,并将其
__proto__
链接到构造函数的prototype
属性。**绑定
this
**:将构造函数内部的this
绑定到新创建的对象上。执行构造函数:执行构造函数的代码,为新对象添加属性和方法。
返回对象:如果构造函数返回一个对象,则返回该对象;否则,返回新创建的对象。如果构造函数显式地返回一个对象,那么这个对象会成为
new
表达式的结果。function customNew(constructor, ...args) {
// 1. 创建一个空对象,并将其 __proto__ 链接到构造函数的 prototype 属性
const obj = Object.create(constructor.prototype);
// 2. 将构造函数内部的 this 绑定到新创建的对象上,并执行构造函数
const result = constructor.apply(obj, args);
// 3. 如果构造函数返回一个对象,则返回该对象;否则,返回新创建的对象
return typeof result === 'object' && result !== null ? result : obj;
}
// 使用自定义的 new 函数
const person = customNew(Person, 'Alice', 30);
console.log(person.name); // 输出: Alice
console.log(person.age); // 输出: 30
箭头函数与普通函数区别
箭头函数是匿名函数,普通函数可以是匿名函数,也可以是具名函数
//普通函数匿名函数
(function() {
alert('123')
})()箭头函数不能用于构造函数,普通函数可以用于构造函数,以此来创建实例
普通函数中
this
指向调用他的对象,箭头函数中this
指向外层作用域的this
箭头函数不具有
arguments
对象,没有prototype
原型对象,没有event.target
在ES6中,箭头函数不支持传统的
arguments
对象。但是,你可以使用剩余参数(Rest Parameters)
语法来达到类似的效果。剩余参数允许你在一个函数中接受不定数量的参数,并将它们作为数组来处理。// ...args 是一个剩余参数,它捕获了传递给myFunction的所有参数,并将它们存储在一个名为args的数组中。这样你就可以像使用arguments一样使用这个数组,但拥有更多现代JavaScript数组方法的便利。
const myFunction = (...args: any[]) => {
console.log(args); // 这里 args 将会是一个包含了所有传入参数的数组
};
myFunction('arg1', 'arg2', 'arg3'); // 输出: ['arg1', 'arg2', 'arg3']普通函数的
arguments
对象并不是一个真正的数组,而是一个类数组对象。它具有类似数组的属性(如length
),但不具备数组的方法(如forEach
、map
等)。你可以使用Array.from
或Array.prototype.slice.call
将arguments
对象转换为真正的数组。
this指向问题
this
用于指向某一个对象,this
的值取决于函数的调用方式,而不是函数的定义位置
全局作用域和普通函数调用,
this
指向window
function test(){
console.log(this)
}
let test2 = function(){
console.log(this)
}
test()//window
test2()//window
console.log(this)//window方法中的
this
,谁调用该方法,this就指向谁构造函数中的
this
指向构造函数的实例化对象箭头函数中
this
指向外层作用域的this
,箭头函数的this指向定义时所在的对象,而不是调用时所在的对象call
:第一个是要设置为this
的新上下文,其余参数则是传递给被调用函数的参数列表。立刻执行function greet(firstName, lastName) {
console.log(`Hello, ${this.title} ${firstName} ${lastName}!`);
}
const person = { title: 'Mr.' };
// 使用call改变greet函数内部的this上下文为person对象
greet.call(person, 'John', 'Doe'); // 输出: Hello, Mr. John Doe!Function.prototype.myCall = function(context) {
// 判断调用对象
if (typeof this !== "function") {
console.error("type error");
}
// 获取参数
let args = [...arguments].slice(1), result = null;
// 判断 context 是否传入,如果未传入则设置为 window
context = context || window;
// 将调用函数设为对象的方法
context.fn = this;
// 调用函数
result = context.fn(...args);
// 将属性删除
delete context.fn;
return result;
};apply
:接受两个参数:第一个是要设置为this
的新上下文,第二个是一个数组或类数组对象,其内容将作为单独的参数传递给被调用的函数。立刻执行function sum(a, b) {
console.log(this.start + a + b);
}
const numbers = { start: 10 };
// 使用apply改变sum函数内部的this上下文为numbers对象,并传递一个数组作为参数
sum.apply(numbers, [2, 3]); // 输出: 15Function.prototype.myApply = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
let result = null;
// 判断 context 是否存在,如果未传入则为 window
context = context || window;
// 将函数设为对象的方法
context.fn = this;
// 调用方法
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
// 将属性删除
delete context.fn;
return result;
};bind
- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用
call
等方式调用的情况。 - 保存当前函数的引用,获取其余传入参数值。
- 创建一个函数返回,只有调用这个新函数时,bind才会执行,非立刻执行
- 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
Function.prototype.myBind = function(context, ...args) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 获取参数
var fn = this;
return function Fn(...newArgs) {
// 根据调用方式,传入不同绑定值
return fn.apply(context, args.concat(...newArgs);
);
};
};- 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用
Promise
Promise
是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。我认为的Promise
就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise
是一个对象,从它可以获取异步操作的消息。Promise
提供统一的 API,各种异步操作都可以用同样的方法进行处理。有三种状态pending resolved rejected
。pending
等待 处于unsettled
阶段,表示事情还在等待最终的结果。resolved
已处理 处于setteled
阶段,表示事情已经出现结果,并且可以按照正常的逻辑进行下去的结果。rejected
已拒绝 处于setteled
阶段,表示事情已经出现结果,并且不可以按照正常的逻辑进行下去的结果
Promise
构造函数是同步的,then
方法是异步的.下面代码输出为1 2 5 4 3,原因:promise
属于同步,所以直接输出1 2then
是异步的,但是是微任务,settimeout
是宏任务,所以先输出4,再输出3
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
})
setTimeout(() => {
console.log(3)
}, 0);
promise.then(() => {
console.log(4);
})
console.log(5);Promise.all
方法let p1 = new Promise((resolve , reject) => {
resolve('p1')
})
let p2 = Promise.resolve('OK')
let p3 = Promise.resolve('no problem')
let result = Promise.all([p1 , p2 , p3])
console.log(result); //PromieState:"fulfilled" PromiseResult:['p1' , 'OK' , 'no problem']Promise.all = function(arr){
return new Promise((resolve , reject) => {
//用来计数
let count = 0
//用来存储结果
let res = []
//对数组中每个Promise对象进行遍历
for(let i = 0; i < arr.length; i++) {
arr[i].then(v => {
//如果是成功的,count++
count += 1
res[i] = v
//当count等于数组的长度,说明全部成功了,直接返回成功
if(count === arr.length) {
resolve(res)
}
},r => {
//有一个失败了,直接返回失败的数据
reject(r)
}
}
})
}Promise.race
方法实现:
static race(Promises: Array<any>) {
return new Promise((resolve, reject) => {
for (let i = 0; i < Promises.length; i++) {
if (Promises[i] instanceof Promise) {
Promises[i].then(
(value: any) => {
resolve(value);
},
(err: any) => {
reject(err);
}
);
} else {
resolve(Promises[i]);
}
}
});
}使用
Promise.race
实现Promise
超时处理// 异步任务
function fetchData () {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data fetched');
}, 5000);
});
}
// 判断是否超时
function IsTimeOut (fn, delay) {
const timeOutFun = new Promise((resolve, reject) => {
const timeOut = setTimeout(() => {
clearTimeout(timeOut);
reject();
}, delay)
})
return Promise.race([
fn,
timeOutFun
]);
}
// 调用该方法
IsTimeOut(fetchData(), 3000).then(() => {
console.log('数据获取成功');
}).catch(() => {
console.log('数据获取超时');
})Promise.race
:返回第一个完成(无论成功或失败)的 Promise 的结果。Promise.all
:返回所有 Promise 成功的结果数组,如果有一个失败,则整个Promise.all
失败。Promise.allSettled
:返回所有 Promise 的结果对象数组,无论成功或失败。Promise.any
:返回第一个成功的 Promise 的结果,如果所有 Promise 都失败,则返回一个包含所有失败原因的数组。
Promise的状态改变
在Promise中reject后,通过catch捕获异常,并在catch中返回一个resolce状态,那么该Promise的状态为成功,而不是失败,后面的then方法也可以执行。这是因为:虽然
reject()
,将Promise
的状态设置为rejected
。但是catch
方法不仅可以捕获错误,还可以返回一个新的值,从而使Promise
的状态变为fulfilled
。const res = new Promise((resolve,reject) => {
reject();
}).catch(e => {
return "ok";
}).then(r => {
console.log(r)
})Promise的请求是不可以取消的,当使用axios的CancelToken或者fetch的AbortController取消请求时,Promise的状态为reject
Promise与async,await的区别
Promise
- 语法:使用
new Promise
来创建一个Promise
对象,它接收一个函数作为参数,该函数有两个参数:resolve
和reject
,分别用于异步操作成功或失败时的处理。 - 使用:
Promise
提供了.then()
、.catch()
和.finally()
方法来处理成功、失败的结果或无论结果如何都要执行的操作。
async/await
- 概念:
async/await
是基于Promise
的语法糖,使异步代码看起来更像是同步代码。 - 语法:
async
关键字用于声明一个异步函数,await
关键字用于等待一个Promise
完成,只能在async
函数内部使用。 - 使用:通过
async/await
,可以以更直观、更简洁的方式编写异步代码,避免了Promise
链式调用可能带来的复杂性。
区别
- 语法简洁性:
async/await
使代码更简洁、更易读,特别是在处理多个异步操作时,避免了Promise
的链式调用(回调地狱)。 - 错误处理:
async/await
允许使用传统的try/catch
语法来捕获错误,这对于错误处理来说更直观。
for-in与for-of的区别
for-in:
适合对对象进行遍历,遍历的是对象的key值,如果对数组进行遍历,遍历的是索引for-of:
遍历的是元素值,并且不能遍历对象,只能遍历数组,也可以遍历Map和Set这两种数据结构
网络请求
Ajax与Axios
Ajax请求步骤
- 实例化
XMLHttpRequest
对象 - 建立连接
- 监听服务器对象
- 发送请求
- 获取数据
var http = new XMLHttpRequest() //1. 创建XMLHttpRequest对象 |
Ajax与Axios区别
Ajax
是对原生XHR
的封装,Axios
是用Promise实现的对原生XHR的封装Ajax
更接近底层,提供了基本的网络请求功能,需要使用者处理更多细节。Axios
在Ajax
基础上进行了大量的封装,提供了更丰富的API,方便使用者使用。Ajax
不提供请求取消,自动转换请求和响应数据,错误处理等功能。Axios 自动处理 JSON 数据的序列化和反序列化,支持请求和响应拦截器,可以轻松地添加请求头或修改响应数据,支持请求取消,提供超时控制,以及更强大的错误处理机制。
fetch与axios
fetch使用
GET
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // 解析 JSON 响应
})
.then(data => {
console.log(data); // 处理解析后的数据
})
.catch(error => {
console.error('There has been a problem with your fetch operation:', error);
});POST
const data = { key: 'value' };
fetch('https://api.example.com/data', {
method: 'POST', // 或 'PUT'
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data), // 必须匹配 'Content-Type' 应用类型
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // 解析 JSON 响应
})
.then(data => {
console.log(data); // 处理解析后的数据
})
.catch(error => {
console.error('There has been a problem with your fetch operation:', error);
});请求取消
const controller = new AbortController();
const signal = controller.signal;
// 发起请求
fetch('https://api.example.com/data', { signal })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('There has been a problem with your fetch operation:', error);
}
});
// 取消请求
controller.abort();
fetch与axios区别
- 原生支持:
- Fetch 是浏览器原生支持的 API,无需引入任何外部库。
- Axios是一个独立的库,需要通过 npm 或者 CDN 引入
- 响应处理:
- Fetch 返回的 Promise 中包含了
response
对象,需要显式地调用.json()
或.text()
方法来解析响应体。 - Axios 默认会自动解析 JSON 响应数据。
- Fetch 返回的 Promise 中包含了
- 错误处理:
- Fetch 只在网络请求失败时才会 reject Promise,对于 HTTP 错误状态码(如 404, 500 等),不会自动 reject,需要手动检查
response.ok
或者response.status
。 - Axios 会在 HTTP 状态码不在 2xx 范围时 reject Promise,简化了错误处理。
- Fetch 只在网络请求失败时才会 reject Promise,对于 HTTP 错误状态码(如 404, 500 等),不会自动 reject,需要手动检查
- 请求取消:
- Fetch 使用
AbortController
来取消请求,相较于axios
的cancel token
更加复杂。 - Axios 提供了
cancel token
机制来取消请求,比AbortController
更加直观。
- Fetch 使用
- 数据传递:
- Fetch 在发送 POST 请求时,需要将数据放在
body
属性中,并且通常需要手动序列化为字符串形式。 - Axios 在发送 POST 请求时,将数据放在
data
属性中,通常以对象的形式传递。
- Fetch 在发送 POST 请求时,需要将数据放在
- 数据转化:
- Fetch 不会自动对数据进行转化,例如 JSON 数据需要手动解析。
- Axios 自动处理数据转化,例如将对象转化为 JSON 字符串。
- 使用范围:
- Fetch 主要用于前端浏览器环境,虽然有
node-fetch
这样的库可以让 Fetch 在 Node.js 中使用,但不是开箱即用。 - Axios 同时支持浏览器和 Node.js 环境,提供了统一的 API 接口。
- Fetch 主要用于前端浏览器环境,虽然有
异步加载的方式
defer
async
- 动态创建script标签
- 通过
window.onload = callback()
,onload事件在整个页面加载完毕后触发
函数式编程
函数式编程是一种编程范式, 在函数式编程中,函数被视为一等公民,可以像其他数据类型一样被赋值给变量、作为参数传递给其他函数,甚至作为函数的返回值。这种编程风格鼓励使用纯函数(无副作用的函数),即函数的输出只依赖于输入参数,相同的输入总是产生相同的结果,不依赖也不修改外部状态。函数式编程在可以应用于很多场景,包括使用高阶函数、纯函数、递归以及函数组合。
作用: 可以编写出更加清晰、可读性强且易于维护的代码。
高阶函数
:Array.prototype.map
,Array.prototype.filter
和Array.prototype.reduce
都是高阶函数的例子纯函数
: 纯函数是指每次调用时,给定相同的输入就会产生相同的输出,并且没有副作用的函数。// 纯函数示例:计算两个数的和
function add(a, b) {
return a + b;
}
console.log(add(1, 2)); // 输出: 3
console.log(add(1, 2)); // 输出: 3 (再次调用,结果一致)
// 纯函数不会修改外部状态
let counter = 0;
function increment() {
return ++counter; // 这不是纯函数,因为它修改了外部状态
}
function pureIncrement(counter) {
return counter + 1; // 纯函数,因为它不修改外部状态
}函数组合
: 函数组合是将多个函数串联起来形成新的函数,可以简化复杂的业务逻辑。// 定义几个简单的函数
function double(x) {
return x * 2;
}
function increment(x) {
return x + 1;
}
// 使用函数组合来创建一个新函数,先double再increment
const doubleThenIncrement = x => increment(double(x));
console.log(doubleThenIncrement(5)); // 输出: 11
判断元素是否在可视区域的方法
使用getBoundingClientRect()
getBoundingClientRect()
会返回元素的top
,left
,bottom
,right
值, 可以根据这些值判断元素是否在视口内.
使用IntersectionObserver
API
监听所有文档加载完成后, 为需要判断的元素添加监听
既可以监听多个, 也可以监听一个, 监听多个如下
document.addEventListener('DOMContentLoaded', () => {
const ele = document.querySelectorAll('.container');
if (ele) {
const observer = new IntersectionObserver(
entries => {
console.log(entries); // 输出为被监听的元素集合
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log(`${entry.target.id} 进入可视区域`);
} else {
console.log(`${entry.target.id} 离开可视区域`);
}
});
},
{ threshold: 1 }// 完全进入可视区触发, 取值为0 - 1, 0代表进入立马出发回调
);
ele.forEach(element => observer.observe(element));// 为每个元素添加监听
}
});监听一个
document.addEventListener('DOMContentLoaded', () => {
const ele = document.getElementById('container');
if (ele) {
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {// 由于是一个数组。所以虽然只有一个,还可以使用遍历,也可以使用entries[0]
if (entry.isIntersecting) {
console.log('进入可视区域');
} else {
console.log('离开可视区域');
}
});
},
{ threshold: 1 }
);
observer.observe(ele)
}
});
TypeScript
TypeScript
: 提供了代码类型检查, 也就是运行前进行检查
JS数据类型:
String
Number
Boolean
undefined
null
Symbol
BigInt: 表示任意精度的整数, 它解决了
Number
类型在表示大整数时的精度问题,Number
只能安全表示-2^53到2^53之间的整数, 可以通过在整数后面加上n
或者使用BigInt
构造函数来创建BigInt
。// 使用字面量创建 BigInt
let bigIntLiteral = 1234567890123456789012345678901234567890n;
// 使用 BigInt 构造函数创建 BigInt
let bigIntConstructor = BigInt("1234567890123456789012345678901234567890");Object
TS数据类型(包含JS的所有类型)
any
unknown
void
never
enum
Tuple
: 特殊的数组, 表示固定数量和;类型的元素集合const val:[string,number] = ['string', 111]
const val:[string, ...number[]] = ['string']
const val:[string, ...number[]] = ['string', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
TS定义类型时string
和String
区别
小写的是基本类型, , 占用内存小
const str:string = new String();// 会报错
大写的是包装器类型,
typeof
的结果为object
, 读取包装器类型的值使用valueOf()
读取const str:String = new String("test");
console.log(str.valueOf());// test
设置私有属性
Symbol(JS,TS)
:类中有个属性名为name,为其创建symbol
引用,实例对象通过name访问不到该属性,但是如果知道其引用,也就是sym1
这个属性,也可以访问到。class Study {
sym1 = Symbol("name");
constructor(name) {
this[Study.sym1] = name;
}
getNames() {
return this[Study.sym1];
}
}
const study = new Study("zhaoqr");
console.log(study.name); // undefined使用#号
(JS,TS)
class Study {
#name;
constructor(name) {
this.#name = name;
}
getNames() {
return this.#name
}
}
const study = new Study("zhaoqr");
console.log(study.#name); // 直接访问会报错:属性 "#name" 在类 "Study" 外部不可访问,因为它具有专用标识符。ts(18013)使用
private
关键字(TS专属)
:由于private
关键字为TS特有的,TS编译器会将private
关键字转为普通的JS属性,但是不会改变其访问权限,所以TS文件中使用private
关键字修饰的属性在外部直接访问,TS会有报错提示,但是编译后的JS中,是没问题的,可以正常访问。class Study {
private name: string;
constructor() {
this.name = 'zhaoqr';
}
getName() {
return this.name;
}
}
let study = new Study();
console.log(study.name); // 属性“name”为私有属性,只能在类“Study”中访问。ts(2341) 但是输出为zhaoqr
类型工具
Partial
、Required
、Pick
、Omit
、Exclude
和 Extract
Partial
Partial<T>
将类型T
中的所有属性变为可选的。interface User {
name: string;
age: number;
email: string;
}
type PartialUser = Partial<User>;
const partialUser: PartialUser = {
name: "张三" // age 和 email 是可选的
};
Required
Required<T>
将类型T
中的所有属性变为必填的。interface User {
name?: string;
age?: number;
email?: string;
}
type RequiredUser = Required<User>;
const requiredUser: RequiredUser = {
name: "张三",
age: 18,
email: "zhangsan@example.com" // 所有属性都是必填的
};
Pick<T, K>
Pick<T, K>
从类型T
中选择指定的属性K
,形成一个新的类型。interface User {
name: string;
age: number;
email: string;
}
type UserWithOnlyNameAndAge = Pick<User, 'name' | 'age'>;
const userWithOnlyNameAndAge: UserWithOnlyNameAndAge = {
name: "张三",
age: 18 // 只包含 name 和 age 属性
};
Omit<T, K>
Omit<T, K>
从类型T
中排除指定的属性K
,形成一个新的类型。interface User {
name: string;
age: number;
email: string;
}
type UserWithoutEmail = Omit<User, 'email'>;
const userWithoutEmail: UserWithoutEmail = {
name: "张三",
age: 18 // 不包含 email 属性
};
Exclude<T, U>
Exclude<T, U>
从类型T
中排除U
的类型。type AllTypes = string | number | boolean | null | undefined;
type ExcludeNullAndUndefined = Exclude<AllTypes, null | undefined>;
const value: ExcludeNullAndUndefined = "hello"; // 可以是 string、number 或 boolean
Extract<T, U>
Extract<T, U>
从类型T
中提取可以赋值给U
的类型。type AllTypes = string | number | boolean | null | undefined;
type ExtractNullAndUndefined = Extract<AllTypes, null | undefined>;
const value: ExtractNullAndUndefined = null; // 可以是 null 或 undefined
interface和type
interface
- 适合定义对象的形状和结构。
- 支持合并和继承。
- 可变性高,适合需要扩展的场景。
- 适用于面向对象编程。
type
- 不支持合并和继承。
- 不可变性高,适合定义基本类型和函数类型。
- 适用于需要精确控制类型的场景。
Vue
Vue2
响应式
Vue 2 响应式原理
Vue 2使用了Object.defineProperty来实现响应式系统。这个方法允许对一个对象的属性进行拦截,所以针对一个对象,需要递归去添加监听:
依赖收集:当组件渲染时,会访问响应式对象的属性,这时通过
getter
函数进行依赖收集,将当前属性与当前活跃的组件(观察者)关联起来。派发更新:当响应式对象的属性被修改时,通过
setter
函数通知所有依赖(观察者)进行更新。Object.defineProperty([对象], [对象的某个属性], [描述属性的行为])
Object.defineProperty
只能劫持对象的现有属性,无法检测到对象属性的添加或删除,以及数组索引和长度的变化,Vue 2通过Vue.set
和Vue.delete
方法以及变异方法(如push
,push,pop,shift,unshift
等)来解决这些限制。
Vue 3 响应式原理
Vue 3引入了Proxy作为响应式系统的基石,这是ES6引入的一个新特性,它允许完全拦截对象的操作:
- 更全面的拦截:
Proxy
可以拦截包括属性读取、设置、删除等几乎所有操作,这意味着Vue 3的响应式系统可以自然地支持数组索引和长度的变化,以及对象属性的添加和删除。 - 性能提升:由于
Proxy
可以更精确地拦截操作,Vue 3的响应式系统只需要对根级响应式对象使用Proxy
,Proxy
的代理行为是递归的。当一个对象被转换成响应式后,其所有的属性也会变成响应式的,而Vue 2需要递归地使用Object.defineProperty
,这在大型对象或深层嵌套对象上会有明显的性能提升。 - Composition API:Vue 3通过引入Composition API,提供了更灵活的组织组件逻辑的方式,这与其响应式系统的改进是相辅相成的。
- 更全面的拦截:
Vue响应式原理涉及到的设计模式
- 观察者模式:依赖管理器的设计使用的是观察者模式,通过在getter中收集依赖,在setter中通知依赖,setter中调用依赖类的notify方法遍历所有依赖进行通知。
- 发布订阅模式:Vue的事件总线(Event Bus)或Vuex状态管理就是基于这种模式。
- 代理模式:在Vue 3中,响应式系统的实现从
Object.defineProperty
迁移到了ES6的Proxy
。Proxy
可以理解为在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截。 - 工厂模式:Vue在创建响应式对象、组件实例或虚拟DOM节点时,会使用工厂模式。工厂模式通过定义一个创建对象的接口,允许子类决定实例化哪一个类。这使得Vue在内部可以根据不同的条件来创建不同的实例,而不需要直接实例化类。
- 单例模式:Vue应用中不经常直接使用单例模式,但Vue的全局API和插件系统(如Vuex、Vue Router)通常会被设计为单例,确保全局只有一个实例,这样可以在整个应用中共享状态和逻辑。
总结
- Vue 2:使用
Object.defineProperty
,适用于较早的JavaScript环境,但有一些限制,如无法直接检测到对象属性的添加或删除。 - Vue 3:采用
Proxy
,提供了更全面和高效的响应式系统,能够拦截更多类型的操作,解决了Vue 2中存在的限制,并且为新的组件设计模式(如Composition API)提供了支持。
Vue的优缺点
- 优点
- 双向数据绑定
- 组件化开发:把页面拆分成多个组件,每个组件依赖的CSS,JS,模板等资源放在一起。
- 单页面路由:页面跳转通过路由实现,不用请求服务器
- 虚拟DOM:每次更新通过比对前后两次的虚拟DOM,减少更新代价
- 渐进式框架:需要什么功能再去添加想要的功能,循序渐进
- 缺点:
- 首屏幕加载慢
- 由于DOM都是在客户端生成,所以蜘蛛在爬取的时候爬不到东西,不利于SEO
MVC
和MVVM
MVC
model-view-controller
:- 用户通过视图(View)发起请求。
- 控制器(Controller)接收请求并处理用户的输入。
- 控制器调用模型(Model)的方法进行数据处理。
- 模型更新数据。
- 控制器更新视图,显示新的数据。
- 整体来看,数据流的方向是从视图到控制器,再从控制器到模型,最后从控制器回到视图,形成了一个明确的单向路径。
MVVM
view-view/model-model
- 用户通过视图发送请求。
- 视图通过数据绑定机制将请求传递给 ViewModel。
- ViewModel 处理请求,调用模型的方法进行数据处理。
- 模型更新数据。
- ViewModel 通过数据绑定机制自动更新视图,显示新的数据。
- 双向数据流是指数据可以在视图和模型之间自由流动,
区别
- MVC:强调控制器的作用,视图和模型之间的交互需要通过控制器来完成。
- MVVM:强调数据绑定,视图和视图模型之间的交互是自动的,通过数据绑定机制实现。
生命周期
创建一个Vue流程
- new vue(),开始创建一个Vue实例对象
- Init() Event&LifeStyle,刚初始化了一个vue实例对象,这个实例身上此时只有一些生命周期函数和默认事件,其他的东西都未创建
- beforeCreate()钩子:此时data和method中的数据还没初始化
- Init() injections&reacitive,初始化数据和方法
- created()钩子:在组件实例被创建之后,即Vue实例的初始化和数据观测完成后立即调用。此 时,组件的数据已经被设置,可以访问
data
、computed
、methods
等选项,但是组件尚未挂载,因此无法访问到$el
属性,也就是说DOM还未生成。所以需要调用method中的方法和操作data中的数据,最早只能在created中 - 编译模板,并未挂载到页面中
- beforeMounted()钩子:此时,模板已经编译完成,但是尚未挂载,此时页面还是旧的
- 编译好的模版挂载到页面中
- mounted()钩子: 在组件的模板和数据被渲染到DOM中之后调用。此时,组件已经挂载到了其指 定的挂载点(mount point),可以访问到
$el
属性,即组件的根DOM元素。适合执行依赖于DOM的操作,如使用$refs
访问子组件或子元素,或者是在组件渲染完成后执行DOM操作和初始化第三方库。操作DOM节点,最早在mounted中 - beforeUpdate()钩子:页面中的数据还是旧的,但是data中的数据是新的,知识还没渲染到页面
- updated()钩子:更新完毕,数据和页面同步
- beforeDestory()钩子:vue实例进入销毁阶段,但是身上的所有东西都还可用,可以销毁定时器啥的
- destoryed()钩子:此时已经被销毁,所有的指令方法,数据都不可用
Vue 2 生命周期钩子
- beforeCreate:在实例初始化之后,数据观测(data observer)和event/watcher事件配置之前被调用。
- created:在实例创建完成后被立即调用,此时已完成数据观测、属性和方法的运算,
$el
属性还未显示出来。 - beforeMount:在挂载开始之前被调用,相关的
render
函数首次被调用。 - mounted:
el
被新创建的vm.$el
替换,并挂载到实例上去之后调用该钩子。 - beforeUpdate:数据更新时调用,发生在虚拟DOM打补丁之前。
- updated:由于数据更改导致的虚拟DOM重新渲染和打补丁,在这之后会调用该钩子。
- beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。
- destroyed:Vue实例销毁后调用。调用后,Vue实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
Vue 3 生命周期钩子
Vue 3引入了Composition API,生命周期钩子也有了对应的变化,但它仍然保留了Vue 2的选项API中的生命周期钩子,同时提供了基于Composition API的新钩子。
- setup(新):一个新的在组件创建之前执行的函数,是Composition API的入口点。对应Vue2的beforeCreate和created
- onBeforeMount:对应Vue 2的
beforeMount
。 - onMounted:对应Vue 2的
mounted
。 - onBeforeUpdate:对应Vue 2的
beforeUpdate
。 - onUpdated:对应Vue 2的
updated
。 - onBeforeUnmount:对应Vue 2的
beforeDestroy
。 - onUnmounted:对应Vue 2的
destroyed
。 - onErrorCaptured(新):当捕获一个来自子孙组件的错误时被调用。
- onRenderTracked(新):调试钩子,当虚拟DOM重新渲染时调用。
- onRenderTriggered(新):调试钩子,当虚拟DOM重新渲染的触发器被触发时调用。
diff算法
虚拟DOM
虚拟DOM就是用js去描述一个真实的DOM节点
{
tag:"div", //元素标签
attrs:{ //属性
class:"a",
id:"b"
},
text:"内容", //文本内容
children:[] //子元素
}虚拟DOM存在的原因是在vue中是数据驱动视图,数据变化,视图随之更新,更新视图不可避免操作DOM,而操作真实DOM比较耗费性能,主要还是因为浏览器标准把DOM设计的很复杂,DOM结构很庞大,因此为了避免操作真实DOM,vue设计出虚拟DOM,通过对比DOM结构的变化来计算出视图哪里需要更新,然后更新需要根更新的地方。
Diff算法
Diff算法就是通过比较新旧虚拟DOM来更新需要更新的地方,Vue2采用双端diff算法尽可能对相同的节点进行复用
实现:
以新的VNode为基准,对比旧的VNode
- 新的VNode上有的节点旧的VNode没有,就在旧的VNode加上,就是创建节点
- 新的VNode上没有,而旧的VNode有的,就在旧的VNode上去掉,就是删除节点
- 某些节点在新VNode和旧VNode上都有,以新的VNode为基准,更新旧的VNode
递归操作子节点,主要包含以下操作
- 创建子节点:创建节点,现在的问题是创建节点之后,插入到DOM的哪个位置?合适的位置是放到所有未处理节点之前,而非所有已处理节点之后。这样已创建插入的节点之间就不会有插入顺序的错误。
- 删除子节点:循环完成后,oldChildren中还存在未处理的节点,就将这些节点删除
- 更新子节点:在oldChildren中发现了与newChildren位置相同的子节点,就对其进行更新即可。
- 移动子节点:在oldChildren中找到了与newChildren中相同的子节点,但是位置却不同,就以新的为基准,对老的进行移动。移动到哪?和前面一样,移动到未处理节点之前。
由于如果newChildren和oldChildren中的最后一个节点发生了改变,就需要遍历到最后才可以发现,非常影响性能,所以在双重循环之前,先进行以下步骤:
- 初始化指针:定义
oldStartIdx
、oldEndIdx
、newStartIdx
、newEndIdx
四个指针,分别指向旧 VNode 子节点数组和新 VNode 子节点数组的起始和结束位置。 - 双端比较:依次比较
oldStartVnode
和newStartVnode
、oldEndVnode
和newEndVnode
、oldStartVnode
和newEndVnode
、oldEndVnode
和newStartVnode
,如果相同则移动指针,否则进行处理。
- 初始化指针:定义
在diff算法之后,如何更新真实DOM?
- 生成补丁:在diff过程中,Vue会记录每个需要更新的操作,如创建新节点、更新节点属性、移动节点或删除节点等。这些操作被组织成补丁对象,每个补丁对应一个或一组DOM操作。
- 应用补丁(Apply Patches): Vue会遍历补丁列表,根据补丁描述执行相应的DOM操作。例如,如果补丁说明某个节点需要更新属性,则Vue会找到对应的DOM元素并更新其属性;如果补丁指示某个节点需要移动,则Vue会改变该DOM元素在父节点中的位置;如果需要插入新节点或删除节点,Vue也会执行相应的操作
通信方式
父传子:
props
祖先传子孙:
provide,inject
- 父组件配置项:
provide:{foo:123}
- 孙子组件配置项:
inject:['foo']
- 父组件配置项:
与嵌套iframe通信方式
子传父:子调用
window.parent.postMessage
,父添加message监听window.addEventListener('message', callback)
父传子
// 父中
const iframe = document.getElementById('myIframe');
iframe.contentWindow.postMessage('父窗口发送的消息', '*');
// 在子窗口中(iframe 内部)
window.addEventListener('message', function(event) {
console.log('子窗口接收到消息:', event.data);
});通过在window上绑定全局方法来实现
子传父:
$emit
Vue2事件总线:Vue2通过创建一个Vue实例作为事件总线。
$emit(发送事件)和$on(接收事件)
// eventBus.js
import Vue from 'vue';
export const EventBus = new Vue();
<!--组件A-->
<template>
<button @click="sendEvent">Send Event</button>
</template>
<script>
import { EventBus } from './eventBus';
export default {
methods: {
sendEvent() {
EventBus.$emit('my-event', 'Hello from ComponentA');
}
}
};
</script>
<!--组件B-->
<template>
<div>{{ message }}</div>
</template>
<script>
import { EventBus } from './eventBus';
export default {
data() {
return {
message: ''
};
},
created() {
EventBus.$on('my-event', (data) => {
this.message = data;
});
},
beforeDestroy() {
EventBus.$off('my-event');
}
};
</script>Vue3事件总线:在Vue3中,由于没有全局的Vue对象所以需要借助工具库
mitt
来实现// 创建一个事件总线实例
import mitt from 'mitt';
const eventBus = mitt();
// 导出事件总线
export default eventBus;
<!-------------------->
//使用
import eventBus from './eventBus';
// 触发名为 'message' 的事件,并传递参数
eventBus.emit('message', { text: 'Hello, world!' });
<!-------------------->
import eventBus from './eventBus';
// 监听名为 'message' 的事件
eventBus.on('message', (data) => {
console.log(data.text); // 输出: Hello, world!
});
// 移除监听器
eventBus.off('message', handler);vuex
或者Pinia
vue-router
vue-router
钩子beforeRouterEnter(to , from , next)
beforeRouterUpdate
beforeRouterLeave
vue-router
组件- <router-link>:路由声明式跳转
- <router-view>:渲染路由的容器
- <keep-alive>:缓存组件
vue-router
的实现原理:通过window.addEventListener()方法监听页面地址的变化$router和$route
- $router是vue-router的一个实例,是用来操作路由的,包含了路由跳转方法,钩子等
- $route是用来获取路由信息的
路由传参
params
this.$router.push({
path:'/123',
params:{
id:2,
name:"zs"
}
})
//获取
this.$router.params.idquery
this.$router.push({
path:'/123',
query:{
id:2,
name:"zs"
}
})
//获取
this.$router.query.id区别:query传参,name和path都行,url会带上参数,params传参只能是name,url不会带上参数,刷新就没有了
computed,methods,watch
computed与methods
- computed是属性调用,nethods是函数调用
- computed带有缓存功能,只有当计算属性所依赖的属性发生改变时,才会重新计算。methods不会被缓存。
computed与watch
- watch是观察某一属性的变化,然后进行具体的业务逻辑或重新计算属性值
- computed是通过所依赖的属性的变化重新计算属性值
- 两者区别不大,一般使用computed,但是当数据变化时需要进行异步操作或者开销较大,推荐使用watch
计算属性的实现原理:
组件初始化时,
Vue
会遍历computed
对象中的每个属性,并调用其getter
函数,在调用getter
函数过程中,Vue
会收集所有被访问的响应式数据作为依赖。// 当 fullName 的 getter 被调用时,this.firstName 和 this.lastName 会被访问。
// Vue 会将 fullName 与 firstName 和 lastName 之间的依赖关系记录下来。
// 当 firstName 或 lastName 发生变化时,Vue 会重新调用 fullName 的 getter 函数,重新计算结果并更新缓存。
// 如果 firstName 和 lastName 没有变化,Vue 会直接返回上次计算的结果,避免不必要的计算开销。
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
}计算属性默认时可读的,当你为计算属性提供了set方法时,计算属性就是可以被赋值的
// Vue2
fullName: {
get() {
return `${this.firstName} ${this.lastName}`;
},
set(newValue) {
const names = newValue.split(' ');
this.firstName = names[0];
this.lastName = names[1];
}
}
// vue3
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (newValue) => {
const names = newValue.split(' ');
firstName.value = names[0];
lastName.value = names[1];
}
});
v-show
与v-if
v-if:
条件性的销毁和重建元素,当条件频繁变化时开销较大。v-show:
通过设置display属性来控制元素的显示和隐藏- 条件渲染:无论条件是真是假,元素都会被渲染到DOM中,但是会根据条件的真假切换元素的CSS属性
display
来控制元素的显示和隐藏。 - 渲染方式:
v-show
只是简单地切换元素的显示状态,元素始终保留在DOM中。 - 性能开销:由于元素始终被保留在DOM中,切换显示状态只是修改了元素的CSS属性,因此
v-show
在条件频繁变化时具有更低的性能开销。 - 使用场景:适用于需要频繁切换显示状态的场景。
- 条件渲染:无论条件是真是假,元素都会被渲染到DOM中,但是会根据条件的真假切换元素的CSS属性
总结
**
v-if
**:适用于运行时条件不经常改变,或者条件不满足时不希望渲染元素占用资源的场景。**
v-show
**:适用于需要频繁切换元素显示状态的场景,因为它只是切换元素的display
属性,而不是销毁和重建元素。
v-for与v-if
- 避免一起使用:因为在vue2中v-for优先级高于v-if,使用会造成性能的浪费
- 如果非要一起使用,可以使用以下两种方法
- v-if的数据不依赖于v-for中的数据:将v-if写在外层
- v-if的数据依赖于v-for中的数据:使用计算属性将v-for中的属性先过滤一次
keep-alive
- 引入
keep-alive
,页面第一次加载触发created->mounted->activated(keep-alive组件激活时调用activated)
钩子,退出时触发deactivated(组件失活时调用)
。再次进入只触发activated
。 keep-alive
的include
和exclude
可以通过组件的name指定使用缓存或者不使用缓存- 作用:在组件切换过程中将状态保留在内存中,防止重复渲染DOM,减少加载时间及性能消耗,提高用户体验性。
hash路由和history路由的区别
hash
:hash路由利用了window可以监听onhashchange事件来实现的,也就是说hash值指导浏览器动作时,不会请求服务器,http请求中也不会包括hash值,同时每一次改变 hash 值,都会在浏览器的访问历史中增加一个记录,使用“后退”按钮,就可以回到上一个位置。所以,hash 模式 是根据 hash 值来发生改变,根据不同的值,渲染指定DOM位置的不同数据。history
: 利用了HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法,这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会向后端发送请求- back():后退到上一个路由;
- forward():前进到下一个路由,如果有的话;
- go(number):进入到任意一个路由,正数为前进,负数为后退;
- pushState(obj, title, url):前进到指定的 URL,不刷新页面;
- replaceState(obj, title, url):用 url 替换当前的路由,不刷新页面;
- 区别
- hash路由带有#号,history没有
- history路由使用了H5的新API,因此对浏览器版本有要求
- pushState设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,故只可设置与当前同文档的URL;
- pushState通过stateObject可以添加任意类型的数据到记录中;而hash只可添加短字符串;
- pushState可额外设置title属性供后续使用;
- hash兼容IE8以上,history兼容IE10以上;
- history模式需要后端配合将所有访问都指向index.html,否则用户访问二级页面,刷新页面,会导致404错误。要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面
package.json中^和~的区别
- ^的意思是将版本更新到第一个数字的最新版本
- ~的意思是将版本更新到中间数字的最新版本
权限管理
页面权限:通过路由中的meta字段和路由守卫进行实现
按钮权限:
自定义指令:包装一个自定义指令,在自定义指令中进行权限的判断,
先获取按钮权限表
如果指令传值,就获取参数,参数和按钮权限比较
<!--src/utils/direction文件-->
//自定义指令
import Vue from "vue";
const UserAuthList = new Set(['button_admin'])
Vue.directive('auth', {
inserted:function(el, binding, vnode) {
if(!UserAuthList.has(binding.value)) {
el.parentNode && el.parentNode.removeChild(el)
}
}
});
<!--main.js文件-->
import '@/utils/direction'
权限判定文件
<button v-auth="'button_admin'">测试admin</button>
<button v-auth="'button_user'">测试user</button>
结果
页面只展示测试admin按钮如果没有指令传值,通过路由信息中的meta与按钮权限比较
nextTick
Vue中DOM更新是异步的,nextTick确保了在DOM更新后立刻执行指定的代码,因为直接在数据变更后执行依赖于DOM的操作可能会得到旧的或不准确的DOM状态。使用
$nextTick
可以确保操作执行时DOM已经是最新的状态。在Vue中,更新数据、DOM渲染以及调用
nextTick
的执行时间遵循以下顺序:- 更新数据:当你更改Vue实例中的响应式数据时,这个操作是同步的。也就是说,代码执行到更改数据的那一行时,数据就立即被更改了。
- DOM渲染:Vue的DOM更新是异步的。当数据变化后,Vue会将这次DOM更新任务加入到异步队列中。这意味着,即使数据已经变化,DOM还不会立即更新。Vue会等待所有数据变更完成后,在下一个事件循环“tick”中统一进行DOM更新。这个机制是为了避免频繁且不必要的DOM操作,从而优化性能。
- **调用
nextTick
**:nextTick
方法允许你在DOM更新完成后执行某些操作。当你调用nextTick
时,你提供的回调函数会被加入到Vue的内部nextTick
队列中。一旦当前事件循环中的所有DOM更新任务完成,Vue会执行这个队列中的所有回调。因此,nextTick
的回调函数实际上是在DOM更新之后、下一个事件循环“tick”开始之前执行的。 - 也就是说,由于数据更新后,DOM并不是立即渲染的,而是会等待所有数据操作完成后渲染,而nextTick就是在执行了渲染后立刻执行的代码片段
使用index作为循环的key的缺点
- 当数组的顺序发生改变,由于
key
使用的index
, 每个元素的·key·就会发生变化,导致Vue就会重新渲染该元素,其实元素并没有发生变化,导致不必要的渲染。 - 当插入或删除元素时, 使用
index
作为key
会导致后续所有元素的key
都发生变化,从而导致不必要的DOM更新。
单向数据流
- 单向数据流是指组件之间的传递方式。具体来说,数据从父组件流向子组件,而不是反过来的,并且子组件不能直接修改从父组件接收到的props
- 这样的设计有助于保持数据流动的可预测性和组件之间的独立性。通过
props
和事件机制,父组件和子组件可以进行数据传递和通信,而不破坏单向数据流的原则。这种设计使得 Vue.js 应用更加易于理解、调试和维护。
Vue3
Vue3特性
新的响应式系统
更好的TS支持:提供了更好的类型推断
- 通过
defineComponent
函数,Vue 3 可以自动推断组件的类型。 - Vue 3 的 组合式API 提供了更好的类型支持,使得在使用 TypeScript 时更加方便和灵活。
- 通过
新增的标签组件
Fragment
:允许组件返回多个根节点,解决了 Vue 2 中组件模板必须有一个根元素的限制。Teleport
:允许将组件的 DOM 结构移动到指定的 DOM 节点之外,适用于模态框、弹出框等场景Suspense
:等待异步组件加载时显示备用内容提供
default
插槽和fallback
插槽。default
插槽:异步组件加载成功后显示的内容。fallback
插槽:异步组件加载过程中显示的内容
异步组件
- 使用
defineAsyncComponent
定义异步组件,确保组件在需要时才加载。
- 使用
使用示例
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
单文件组件(SFC)的改进:Vue 3 引入了 <script setup> 语法,简化了单文件组件的编写方式。
diff算法
- Vue3的diff算法在Vue2的基础上有所改变,主要有以下几点
- 静态节点提升
- 引入最长递增子序列的方式来优化节点的移动操作
- 静态节点提升:在编译阶段,将一些不会改变的静态节点提升到外部,避免在每次更新的时候都对节点进行diff操作
- 在 Vue 3 的 diff 算法中,当需要对一组子节点进行比较和更新时,会尝试找到新旧子节点序列中的最长递增子序列。通过找到这个子序列,可以确定哪些节点是可以保持不变的,从而减少不必要的 DOM 操作。
- 初始化:创建一个数组
newIndexToOldIndexMap
,用于记录新节点在旧节点中的位置。 - 遍历新节点:遍历新节点,尝试在旧节点中找到对应的位置,并记录在
newIndexToOldIndexMap
中。 - 计算 LIS:使用
newIndexToOldIndexMap
计算最长递增子序列。 - 移动节点:根据最长递增子序列,确定哪些节点需要移动,哪些节点可以保持不变。
- 初始化:创建一个数组
ref函数和reactive函数
ref
- 作用: 定义一个基本类型的响应式数据
- 语法:
const xxx = ref(initValue)
- 创建一个包含响应式数据的引用对象(reference对象,简称ref对象)。
- JS中操作数据:
xxx.value
- 模板中读取数据: 不需要.value,直接:
<div>{{xxx}}</div>
- 使用:
- 接收的数据可以是:基本类型、也可以是对象类型。
- 基本类型的数据:响应式依然是靠
Object.defineProperty()
的get
与set
完成的。 - 定义对象类型的数据:内部使用的还是
reactive
函数。
- 实现:创建ref对象时,Vue3内部调用了RefImpl这个功能类,这个类主要返回一个包含
_value
属性的代理对象,负责该引用对象的依赖更新和收集
reactive
作用: 定义一个对象类型的响应式数据(基本类型不要用它,要用
ref
函数)语法:
const 代理对象= reactive(源对象)
接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象)reactive定义的响应式数据是“深层次的”。
内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作。
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 拦截属性读取
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
// 拦截属性赋值
const result = Reflect.set(target, key, value, receiver);
trigger(target, key);
return result;
},
deleteProperty(target, key) {
// 拦截属性删除
const result = Reflect.deleteProperty(target, key);
trigger(target, key);
return result;
}
});
}
/**
** 追踪依赖
**/
const targetMap = new WeakMap();
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
/**
**触发更新
**/
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect());
}
}为什么需要结合反射使用?
reactive对比ref
- 从定义数据角度对比:
- ref用来定义:基本类型数据。
- reactive用来定义:对象(或数组)类型数据。
- 备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过
reactive
转为代理对象。
- 从原理角度对比:
- ref通过
Object.defineProperty()
的get
与set
来实现响应式(数据劫持)。 - reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
- ref通过
- 从使用角度对比:
- ref定义的数据:操作数据需要
.value
,读取数据时模板中直接读取不需要.value
。 - reactive定义的数据:操作数据与读取数据:均不需要
.value
。
- ref定义的数据:操作数据需要
watchEffect函数
watch的套路是:既要指明监视的属性,也要指明监视的回调。
watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
watchEffect有点像computed:
- 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
- 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
defineModel
父子组件通信
使用prop和自定义事件触发
<!--父组件-->
<Son :message="message" @changeMessage="changeMessage">
<!--子组件-->
<script setup>
const props = defineProps({
message:String;
})
const emit = defineEmits(['changeMessage']);
// 事件可以通过emit("changeMessage",[传递的数据])来触发父组件的changeMessage方法
</script>还有一个相同逻辑的书写方式
<!--父组件-->
<Son :message="message" @update:message="changeMessage">
<!--子组件-->
<script setup>
const props = defineProps({
message:String;
})
const emit = defineEmits(['update:message']);
// 事件可以通过emit("update:message",[传递的数据])来触发父组件的changeMessage方法
</script>使用v-model,相当于:message=”message” @update:message=”changeMessage”的简写方式
<!--父组件-->
<Son v-model="message">
<!--子组件-->
<script setup>
const props = defineProps({
modelValue:String;
})
const emit = defineEmits(['update:modelValue']);// 这里这个属性名称必须是modelValue,而不是message,因为一个组件只能v-model一次,所以不会重复
// 事件可以通过emit("update:modelValue",[修改后的值])来更新父组件的message属性
</script>
defineModel
defineModel在v-model的基础上,对定义prop和触发emit进行了优化,不用那么繁琐,可以直接对model的值进行修改,父组件中对应的值也会修改。也就是说,父组件传递过来的props在子组件中进行修改后,修改后的值会直接同步到父组件中,不用触发什么自定义事件了。
<!--父组件-->
<Son v-model="message">
<!--子组件-->
<script setup>
const model = defineModel();
// model是ref对象,值value为传递过来的message值,直接修改model.value父组件中的message也会被修改是否破坏了单向数据流呢:不会,因为他只是一种简写形式,内部还是通过prop,emit来实现的
React
生命周期(新)
- 初始化阶段
- constructor
- 接收props和context,想在函数内部使用这两个参数时,需要在super传入参数,使用constructor必须使用super,不然会有this指向问题。如果不使用这两个参数,可以不使用constructor
- getDerivedStateFromProps
- render
- componentDidMount 开始监听,发送ajax请求
- constructor
- update
- shouldComponentUpdate(nextProps, nextState)
- render
- componentDidUpdate
- unmount
- componentWillUnmount
生命周期(旧)
- constructor()—构造器
- componentWillMount()—将要挂载 废弃
- render()
- componentDidMount()
- shouldComponentUpdate
- componentWillUpdate 废弃
- render
- componentDidUpdate
- componentWillUnmount
类组件和函数式组件区别
- 类组件可以有状态管理和生命周期钩子等特性,并且有this,又叫做有状态组件
- 函数式组件没有生命周期钩子,但是有hooks.不能使用this,又叫做无状态组件
- 函数式组件性能高于类式组件,因为类式组件使用需要实例化,函数式组件直接执行取返回值即可
fiber
fiber的作用:使得动画和页面交互更加的顺畅
fiber的出现:
- react16之前,对比更新虚拟DOM是通过循环+递归实现的,但是递归一旦开始就无法终止,如果组件数量庞大,主线程被长期占用,直到整个虚拟DOM树对比更新完成后才会释放,主线程才能执行渲染任务,这就会导致渲染的推迟,1s就达不到60帧,造成页面的卡顿。解决方法:
- 利用浏览器空暇时间执行任务,拒绝长时间占用主线程
- 放弃递归,只采用循环,因为循环可以被中断
- 任务拆分成一个个小任务
- react16之前,对比更新虚拟DOM是通过循环+递归实现的,但是递归一旦开始就无法终止,如果组件数量庞大,主线程被长期占用,直到整个虚拟DOM树对比更新完成后才会释放,主线程才能执行渲染任务,这就会导致渲染的推迟,1s就达不到60帧,造成页面的卡顿。解决方法:
何为fiber
Fiber其实是一种数据结构,他可以用一个纯JS对象来表示
const fiber = {
stateNode, //节点实例
child, //子节点
sibling, //兄弟节点
return, //父节点
.......
}fiber还是一个执行单元,每次执行完一个执行单元,React就会检查现在还剩多少时间,如果没有时间就将控制权让出去。
关键特性
- 增量渲染
- 暂停,终止,复用渲染任务
- 不同更新的优先级
- 并发方面新的基础能力
实现:Fiber把渲染更新过程拆分成多个子任务,每次只做一个执行单元,做完看是否还有剩余时间,如果有继续下一个任务;如果没有,挂起当前任务,将时间控制权交给主线程,等主线程不忙的时候在继续执行,即可以中断与恢复,恢复后也可以复用之前的中间状态,并给不同的任务赋予不同的优先级,其中每个任务更新单元为 React Element 对应的 Fiber节点。
Fiber执行阶段:
- 协调阶段:可以认为是diff阶段,这个阶段可以被终止,这个阶段会找出所有节点的变更,例如节点的新增,删除,更新等等,这些变更React称为副作用
- 提交阶段:将上一阶段计算出来需要处理的副作用一次性执行了,这个阶段必须同步执行,不能被打断
redux流程
- 用户发出Action,通过dispatch方法
- Store自动调用Reducer,并传递两个参数:当前state和action,Reducer会返回新的state
- state一旦有变化,store就会调用监听函数,更新view
render()的目的
- render返回react元素,是原生DOM组件的表示方式
redux三个原则
1.数据来源的唯一性
- 在redux中所有的数据都是存放在你的store中,这样做的目的就是保证数据来源的唯一性。那么为什么要这样做呢?使得创建通用应用程序变得很容易,因为服务器的状态可以序列化并水合到客户机中,而不需要额外的编码工作。单个状态树也使调试或检查应用程序变得更容易;它还使您能够在开发中持久保存应用程序的状态,以获得更快的开发周期。
2.state只能是只读的状态
- state只能是只读的,在你的action中你可以去取它的值,但是不能够去改变它,这个时候采取的方式通常是深度拷贝state,并且将其返回给一个变量,然后改变这个变量,最后将值返回出去。而且要去改变数据你只能够在的reducer中,reducer是一个描述了对象发生了一个什么样过程的函数过程。 只读状态的好处,确保视图和网络回调都不会直接写入状态。
3.使用纯函数进行改变:reducer是个纯函数
受控组件与非受控组件
- 受控组件:在HTML的表单元素中,它们通常自己维护一套state,并随着用户的输入自己进行UI上的更新,这种行为是不被我们程序所管控的,而如果将React里的state属性和表单元素的值建立依赖关系,再通过onChange事件与setState()结合更新state属性,就能达到控制用户输入过程中表单发生的操作,React以这种方式控制取值的表单输入元素就叫做受控组件
- 非受控组件:如果表单元素并不经过
state
,而是通过ref
修改或者直接操作DOM
,那么它的数据无法通过state
控制,这就是非受控组件。
高阶组件(HOC)
- 高阶组件就是一个函数,接收一个组件作为参数,并返回一个新的组件
- 可以用来实现权限控制
权限管理
- 页面权限:
- 通过后端返回用户的权限列表
- 判断权限列表中是否包含该页面需要的权限
- 如果有,就渲染,如果没有就返回403
- 按钮权限
- 通过高阶组件方便统一管理
- 接收一个组件作为参数,然后根据权限返回不同的组件
React和Vue区别和相同点
相同点:
都采用虚拟DOM,避免大量操作DOM
都支持服务器端渲染Vue是Nuxt,React是Next
组件化开发,一些传参方式相同
路由,状态管理等都是和框架分离的
都是JS的UI框架,数据驱动视图
都支持native,react是reactNative,vue是weex
不同点:
- 响应式原理不同:
- vue为Object.definedProperty,为每个属性添加getter和setter,当获取数据时,触发getter将数据添加到依赖中,修改数据时,通过setter通知依赖,双向数据流。
- React主要通过setState()来更新状态,属于单向数据流
- 组件写法不同
- vue:template+script+style
- react:jsx+inlineStyle
- 渲染过程不同:
- vue可以很快计算虚拟DOM之间的差异,跟踪每一个组件的依赖关系,不需要重新渲染组件树
- react:全部子组件都会重新渲染,可以通过shouldComponentUpdate可以进行控制,减少不必要的渲染
- diff算法
- vue中的diff算法:当节点类型相同,类名不同时认为是不同的元素,会删除重建。列表对比时,vue采用两端到中间
- react中的diff算法:react会认为是同种节点,进行修改操作。列表对比时,react从左往右
使用场景
- Vue更快,更灵活,易于开发
- react提倡更低粒度的封装,带来的组件复用性更高,并且react基本使用原生js,所以可定制性,复杂度更高。社区活跃(facebook),适合大型项目
- 响应式原理不同:
浏览器
浏览器JS相关
线程和进程
(操作系统)进程:cpu分配资源的最小单位 ,可以理解为启动的每个应用,进程之间的资源是独立的
(操作系统)线程:cpu调度的最小单位,每个应用中的各个功能
线程不共享的资源:
栈
,寄存器
,状态
,程序计数器
线程共享的资源:
堆
,全局变量
,静态变量
浏览器也采用多进程架构,主要包括以下进程
- 浏览器进程(主进程):负责用户界面、网络请求、存储管理、渲染进程的管理和调度等。只有一个;
- 渲染进程:负责解析 HTML、CSS 和 JavaScript,构建 DOM 树,布局和绘制页面。每个标签页或者iframe都有一个独立的渲染进程
- GPU进程:负责加速3D渲染,用于提高性能和减少主进程负担,只有一个
- 网络进程:负责处理网络请求,如 HTTP/HTTPS 请求。只有一个
- 插件进程:安装的每个谷歌插件对应一个进程
同源策略和跨域
- 同源策略:浏览器最核心,最基本的安全功能,协议,域名,端口,三者一样即为同源,否则为跨域
- 跨域请求可以到达服务器,浏览器也可以接收到服务器返回的数据,只是由于跨域,所以将浏览器数据丢弃了。
- 主域和子域之间涉及跨域
内存泄漏
- 在堆空间中的分配的内存,未释放和无法释放
- 内存泄漏的原因:
- 闭包:闭包是指能够访问其作用域之外变量的函数,作用是可以状态持久化:可以实现计数器,用户信息缓存等功能
- 遗忘的定时器
- 意外的全局变量
- JS在非严格模式下,对未声明但是使用的全局变量处理方式是在全局对象上创建该变量的引用,全局对象就是 window 对象。变量在窗口关闭或重新刷新页面之前都不会被释放,如果未声明的变量缓存大量的数据,就会导致内存泄露。
- 优化方法:
- 减少使用闭包
- 注意清除定时器
- 全局变量先声明后使用
堆和栈
栈(Stack)
- 自动管理:栈内存由JavaScript引擎自动分配和释放,无需开发者手动干预。
- 存储内容:主要存储基本数据类型(如Number、String、Boolean、Undefined、Null)的值以及引用类型的变量(对象、数组等)的地址。基本类型直接存储值,占用空间小且固定;引用类型存储的是指向堆中实际对象的指针。
- 访问速度:栈内存访问速度快于堆内存,因为它遵循后进先出(LIFO, Last In First Out)原则,且内存地址连续。
- 生存周期:栈内存在函数调用结束或变量不再使用时立即释放。
堆(Heap)
- 动态分配:堆内存用于存储复杂数据结构,如对象、数组等,其大小不固定,且分配和释放需要开发者(在JavaScript中通常由垃圾回收机制自动管理)来控制。
- 存储内容:存放引用类型实例的实际数据。当创建一个对象或数组时,会在堆中分配一块内存,并将该内存的地址返回给栈中的引用变量。
- 访问效率:相比栈内存,访问堆内存的速度较慢,因为堆内存分配是分散的,需要通过指针间接访问。
- 生存周期:堆内存的释放依赖于垃圾回收机制,当没有变量再指向堆中的对象时,垃圾回收器会在适当的时候回收这些内存。
总结
- 栈主要用于存储简单数据类型和引用类型的地址,特点是快速访问、自动管理生命周期,但空间有限制。
- 堆则用于存储复杂的数据结构,提供了更大的灵活性和存储空间,但访问速度较慢且需要垃圾回收机制来管理内存。
垃圾回收
- 垃圾回收就是间歇的不定期的寻找不在使用的变量,并释放掉他们所指向的内存
- 作用:防止内存泄漏
- 垃圾回收方式:标记清除,引用计数
url从浏览器到页面的过程
输入URL
DNS解析:浏览器首先需要将域名转换为 IP 地址,这个过程称为 DNS 解析。
- 查缓存,浏览器缓存,本机缓存,本地DNS服务器缓存,
- 如果都没有则由本地DNS服务器向根DNS服务器(总共13个DNS根服务器)发送查询请求,根DNS服务器不存储具体的域名信息,存储的是顶级域(
.com
,.net
等等)DNS服务器地址 - 本地DNS服务器向顶级域名DNS服务器发送请求,顶级域名返回该域名的权威DNS服务器地址。(权威DNS服务器 是指对特定域名及其下属子域名的DNS查询提供权威答案的服务器)
- 本地 DNS 服务器向权威 DNS 服务器发送查询请求。权威 DNS 服务器存储了该域名的具体 IP 地址,权威 DNS 服务器返回该域名的 IP 地址
- 本地DNS服务器缓存结果,将ip返回给操作系统,操作系统返回给浏览器,浏览器向IP发送请求
TCP三次握手建立连接
客户端发送请求
服务器响应请求,并发送数据给客户端
TCP四次挥手关闭连接
浏览器解析资源并渲染页面
- 浏览器使用深度优先遍历解析 HTML,生成 DOM 树(Document Object Model)。DOM 树表示 HTML 文档的结构,每个节点代表一个 HTML 元素。
- 浏览器解析 CSS,生成 CSSOM 树(CSS Object Model)。CSSOM 树表示样式规则的结构,每个节点代表一个样式规则。
- 浏览器将 DOM 树和 CSSOM 树结合,生成渲染树(Render Tree)。渲染树表示页面的可视化结构,每个节点代表一个可见元素。
- 所以display:none的节点不会在渲染树中
- 渲染树每个节点包含布局信息(如位置、大小)和样式信息。
- 浏览器计算每个渲染树节点的几何信息(位置和大小),这个过程称为布局(或回流)。布局过程从根节点开始,递归计算每个节点的位置和大小。
- 浏览器将渲染树的每个节点绘制到屏幕上,这个过程称为绘制(或重绘)。绘制过程包括绘制文本、颜色、边框、阴影等。
- 浏览器解析并执行 JavaScript 代码(根据指定的脚本加载方式进行加载)。JavaScript 代码可以操作 DOM 和 CSSOM,导致页面重新布局和绘制。
- 浏览器处理用户交互(如点击、输入、滚动),并根据用户交互更新页面。
事件循环
- 事件循环:由于JS是单线程的,所以需要事件循环去处理同步和异步任务,从而实现非阻塞的执行。
- 过去会把消息队列简单分为宏任务队列和微任务队列,目前由于浏览器任务类型变多,所以不适用宏任务这种说法,根据最新W3C解释,每个任务有不同的类型,同类型任务必须在同一个队列,不用任务可以属于不同的队列,不同队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列中的任务,但是浏览器必须有一个微队列,微队列具有最高的优先级,必须优先执行。除此之外还有延时队列和交互队列(用户的交互),交互队列优先级高于延时队列
- 宏任务:
setTimeout,setInterval, requestAnimationFrame
(下一帧渲染前执行requestAnimationFrame
回调中的方法)- 当执行栈为空时,事件循环(Event Loop)会从宏任务队列中取出一个任务执行。每执行完一个宏任务,就会检查并执行所有可用的微任务,然后再执行下一个宏任务
- 微任务:
Promise.then
/Promise.catch
/Promise.finally,async/await,process.nextTick(执行顺序高于promise.then),MutationObserver的回调
- 执行顺序
- 执行同步代码
- 执行栈为空时,查询是否有微任务需要执行。
- 执行所有微任务。
- 执行下一个宏任务。
- 重复上述步骤。
- 微任务优先级高于宏任务
Promise.then().then().then
会创建三个微任务。- 当第一个
then
方法被调用时,它会创建一个微任务,将其添加到微任务队列中。第一个then
方法的回调函数执行完毕后,第二个then
方法会被调用,并创建另一个微任务,将其添加到微任务队列中,第三个同理。
- 当第一个
Promise.all[p1,p2,p3]
最多会创建4个微任务,是all
中的三个请求都resolve
了会创建三个微任务,Promise.all
也会创建一个微任务。
- 宏任务:
- 事件循环的一个关键特性是,微任务队列总是在当前宏任务完成后、下一个宏任务开始前清空。这意味着微任务的回调函数会在同一事件循环迭代中执行,而宏任务的回调可能会被推迟到下一个迭代。
- 事件循环确保了JavaScript的非阻塞行为,允许在等待异步操作完成时继续执行代码。这是实现高效、响应式应用的基础。
- 在微任务执行完毕后,浏览器会进行一次渲染,然后从宏任务队列中取出下一个任务继续执行,所以微任务在页面渲染之前执行,宏任务在页面渲染之后执行
新增API
requestIdleCallback
requestIdleCallback(简称RIC)
是一个浏览器 API,它允许你在浏览器空闲时执行回调函数。请求回调:
- 当你调用
requestIdleCallback
并传入一个回调函数时,浏览器会在下一个渲染帧之前的空闲时间执行这个回调函数。
- 当你调用
回调参数:
回调函数会接收到一个
idleDeadline
对象,该对象提供了关于浏览器空闲时间的信息。idleDeadline.timeRemaining()
方法返回剩余的空闲时间(以毫秒为单位)。// 任务过多时可以使用requestIdleCallback
function processTasks(tasks) {
let index = 0;
function processBatch(deadline) {
while (index < tasks.length && deadline.timeRemaining() > 0) {
tasks[index]();
index++;
}
if (index < tasks.length) {
requestIdleCallback(processBatch);
}
}
requestIdleCallback(processBatch);
}
const tasks = Array.from({ length: 1000000 }, (_, i) => () => {
console.log(`Task ${i}`);
});
processTasks(tasks);
requestAnimationFrame
requestAnimationFrame
(简称RAF
)浏览器 API,主要用于动画和图形渲染,它提供了一种高效的方式来请求浏览器在下一次重绘之前执行一个函数,从而确保动画的平滑性和性能。这使得RAF
更适合用于动画和高性能的图形更新。特色
- 与浏览器刷新同步:
RAF
会在浏览器的下一帧刷新之前执行回调函数,通常每秒 60 帧(60 FPS),即大约每 16 毫秒执行一次。 - 性能优化:因为
RAF
与浏览器的刷新周期同步,所以它可以更好地利用浏览器的重绘机制,避免不必要的重绘,从而提高性能。 - 自动暂停:当浏览器标签页失去焦点时,
RAF
会自动暂停,节省资源。并且可以通过cancelAnimationFrame
控制动画的暂停
- 与浏览器刷新同步:
使用
<style>
html,
body {
margin: 0;
padding: 0;
height: 100%;
}
#box {
width: 50px;
height: 50px;
background-color: red;
position: absolute;
top: 20px;
left: 0;
}
#box2 {
width: 50px;
height: 50px;
background-color: blue;
position: absolute;
bottom: 0;
right: 0;
}
#controls {
position: fixed;
top: 10px;
left: 10px;
}
</style>
</head>
<body>
<div id="box"></div>
<div id="box2"></div>
<div id="controls"></div>
<button id="startButton">开始动画</button>
<button id="stopButton">停止动画</button>
</div>
</body>
<script>
const box = document.getElementById('box');
const box2 = document.getElementById('box2');
const startButton = document.getElementById('startButton');
const stopButton = document.getElementById('stopButton');
let posX2 = 0
let posY2 = 0;
let posX = 0;
let posY = 0;
const speed = 2;
let animationId1;
startButton.addEventListener('click', () => {
animationId1 = requestAnimationFrame(animate);
});
stopButton.addEventListener('click', () => {
cancelAnimationFrame(animationId1);
});
function animate() {
posX += speed;
posY += speed;
box.style.transform = `translate(${posX}px, ${posY}px)`;
if (posX < window.innerWidth - box.offsetWidth && posY < window.innerHeight - box.offsetHeight) {
animationId1 = requestAnimationFrame(animate);
}
}
function animate2() {
posX2 -= speed;
posY2 -= speed;
box2.style.transform = `translate(${posX2}px, ${posY2}px)`;
if (Math.abs(posX2) < window.innerWidth - box2.offsetWidth && Math.abs(posY2) < window.innerHeight - box2.offsetHeight) {
setTimeout(animate2, 16);
}
}
setTimeout(animate2, 16);
</script>
pushState
和replaceState
在不刷新页面的情况下改变URL,可以使用HTML5 History API中的
pushState
和replaceState
方法。这两个方法都可以修改浏览器的历史记录,而不会引起页面的重新加载。
pushState
方法:pushState
方法可以向历史记录堆栈中添加一个状态state
:传递的数据,一个与新历史记录条目相关联的状态对象。当用户导航到新的状态时,会触发popstate
事件,事件的state
属性包含这个状态对象。title
:大多数浏览器目前都忽略这个参数,可以安全地传递空字符串。url
:新的历史记录条目的URL。这个URL应该与当前页面同源,否则会抛出异常。
window.history.pushState(state, title, url);
// 假设当前URL是http://example.com/page1,执行下面代码后,可以在不刷新页面的情况下改变URL到http://example.com/page2。此时用户点击浏览器的后退按钮,他们会回到`/page1`。
window.history.pushState({page: 2}, "", "/page2");replaceState
方法:replaceState
方法与pushState
方法类似,但它不会向历史记录添加新的状态,而是替换当前的历史记录条目。window.history.replaceState(state, title, url);
// 假设当前URL是http://example.com/page1,执行下面代码后,可以在不刷新页面的情况下改变URL到http://example.com/page2。此时用户点击浏览器的后退按钮,不会回到`/page1`。
window.history.replaceState({page: 2}, "", "/page2");
浏览器安全相关
XSS攻击
XSS
- XSS(Cross-Site Scripting,跨站脚本攻击)是一种常见的安全漏洞,攻击者通过在网页中注入恶意脚本,使这些脚本在其他用户的浏览器中执行,从而达到窃取用户信息、劫持用户会话或传播恶意软件等目的。XSS 攻击主要分为三种类型:
- 存储型 XSS:
- 攻击者将恶意脚本存储在服务器上,例如通过评论、论坛帖子等。
- 当其他用户访问包含恶意脚本的页面时,脚本被加载并执行。
- 这种类型的 XSS 影响范围更广,因为恶意脚本会持续存在。
- 反射型 XSS:
- 攻击者通过 URL 参数、表单提交等方式将恶意脚本发送给服务器。
- 服务器将未经过滤的脚本返回给客户端浏览器展示。
- 浏览器执行该脚本,导致攻击发生。
- DOM 型 XSS:
- 攻击者利用客户端 JavaScript 代码中的漏洞,通过修改 DOM 节点来注入恶意脚本。
- 这种攻击不涉及服务器端的响应,完全在客户端执行。
- 通常发生在动态生成页面内容的场景中,比如JS代码中使用了innerHTML插入节点
- 存储型 XSS:
- 前两个都是需要通过服务器的,所以是属于后端漏洞,第三个属于前端漏洞
XSS防御措施
输入验证和过滤:通过正则表达式,对用户的输入进行过滤
内容安全策略(CSP):CSP 通过限制网页可以加载的资源和脚本来源,通过 HTTP 响应头
Content-Security-Policy
或者<meta>
标签来声明。常见的指令包括:default-src:默认策略,如果没有指定其他指令,所有资源类型都会遵循这个策略。
script-src:允许的脚本来源。
style-src:允许的样式来源。
img-src:允许的图像来源。
connect-src:允许的 AJAX 请求、WebSocket 连接和 EventSource 连接的来源。
font-src:允许的字体来源。
object-src:允许的 <object>、<embed> 和 <applet> 元素的来源。
media-src:允许的音频和视频来源。
frame-src:允许的 <frame> 和 <iframe> 元素的来源。
form-action:允许的表单提交目标。
base-uri:允许的 <base> 元素的 URI。
report-uri:报告违反策略的 URI(已废弃,使用 report-to)。
report-to:报告违反策略的组名称
通过http响应头设置CSP
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src *; connect-src 'self' https://api.example.com; object-src 'none'; media-src 'self'; frame-src 'none'; form-action 'self'; base-uri 'self'; report-to my-group
通过<meta>标签设置
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src *; connect-src 'self' https://api.example.com; object-src 'none'; media-src 'self'; frame-src 'none'; form-action 'self'; base-uri 'self'; report-to my-group">HTTPOnly 标志:在设置 Cookie 时,使用
HttpOnly
标志,防止 JavaScript 访问 Cookie,减少会话劫持的风险。**避免使用
innerHTML
**,改用textContent
或appendChild
等安全的方法。
CSRF攻击
CSRF
- CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种常见的安全漏洞,攻击者通过诱导用户在已登录的网站上执行非预期的操作。CSRF 攻击通常发生在用户已经登录某个网站的情况下,攻击者利用用户的认证状态,通过第三方网站发送恶意请求,从而执行某些操作,如更改密码、转账等。
- 原理
- 用户登录:
- 用户在某个网站(例如银行网站)上登录,获得一个会话 cookie。
- 访问恶意网站:
- 用户在未退出银行网站的情况下,访问了一个恶意网站。
- 恶意请求:
- 恶意网站包含一个隐藏的表单或链接,指向银行网站的某个敏感操作(例如转账)。
- 当用户点击链接或表单被自动提交时,浏览器会自动将存储的会话 cookie 附带到请求中。
- 执行操作:
- 银行网站收到请求后,认为请求是合法的,因为请求带有有效的会话 cookie。
- 银行网站执行了恶意操作,例如将用户的资金转移到攻击者的账户。
- 用户登录:
防御措施
CSRF令牌:服务器端生成一个唯一的,不可预测的密钥值,作为CSRF令牌被包含在客户端的http请求中,后续请求发出时,服务器对该参数进行验证,拒绝不包含令牌的请求参数。
SameSite属性:设置cookie的SameSite属性为Lax或Strict,限制cookie的跨站传输。
SameSite=Lax
:允许在顶级导航中发送 cookie,但在跨站请求中不会发送。SameSite=Strict
:仅在同站请求中发送 cookie。
// 服务器端设置
res.cookie('session', 'session_value', { httpOnly: true, secure: true, sameSite: 'lax' });http头检查:服务器可以检查Referer和Origin头,确保请求来自可信的源
打包工具
webpack
webpack热更新原理
服务器和客户端的通信:当使用 Webpack Dev Server 启动项目时,它会启动一个 WebSocket 服务器。客户端(浏览器)通过 WebSocket 与这个服务器保持通信连接。
devServer: {
hot: true,
},文件监听:Webpack 会监听项目文件的变化。一旦文件发生变化,Webpack 将对修改的文件进行重新编译,并将更新的模块打包成一个或多个更新块(hot update chunk)。
发送更新:编译完成后,Webpack Dev Server 通过 WebSocket 向客户端发送一个或多个更新块的信息。
接收更新:浏览器端的 HMR 运行时接收到更新块后,会对这些更新块进行处理,替换或更新旧的模块。
模块热替换:在替换模块时,HMR 运行时会根据模块的依赖关系,从更新的模块开始,递归更新所有受影响的模块。
npx webpack打包流程
- 初始化参数阶段:这一步会从我们配置的webpack.config.js中读取到对应的配置参数和shell命令中传入的参数进行合并得到最终打包配置参数。
- 开始编译准备阶段:这一步我们会通过调用webpack()方法返回一个compiler方法,创建我们的compiler对象,并且注册各个Webpack Plugin。找到配置入口中的entry代码,调用compiler.run()方法进行编译。
- 模块编译阶段:从入口模块进行分析,调用匹配文件的loaders对文件进行处理。同时分析模块依赖的模块,递归进行模块编译工作。
- 完成编译阶段:在递归完成后,每个引用模块通过loaders处理完成同时得到模块之间的相互依赖关系。
- 输出文件阶段:整理模块依赖关系,同时将处理后的文件输出到ouput的磁盘目录中。
loader
作用:由于webpack只认识js和json文件,所以需要loader进行翻译,对其他资源进行预处理
less-loader
:less编译成csscss-loader
:将css变成commonjs加载到js中style-loader
:创建style标签,将js中的样式资源插入标签内,并将标签加入head中sass-loader
:负责将Sass文件编译成css文件, sass-loader依赖sass进行编译, 所以安装也必须带着sass
const path = require("path"); |
plugin
作用:解决loader无法解决的事,比如打包优化和代码压缩
html-webpack-plugin
:处理html资源clean-webpack-plugin
每次打包时候,CleanWebpackPlugin 插件就会自动把上一次打的包删除
plugin和loader的区别
- loader:webpack本身只能打包js文件,针对css,图片等文件格式没法打包,就需要引入第三方的模块进行打包,loader虽然扩展了webpack,但是他只专注于转化文件,完成压缩,打包,语言翻译,仅仅是为了打包!!!!!!
- plugin也是为了扩展webpack的功能,但是 plugin 是作用于webpack本身上的。而且plugin不仅只局限在打包,资源的加载上,它的功能要更加丰富。从打包优化和压缩,到重新定义环境变量,功能强大到可以用来处理各种各样的任务。webpack提供了很多开箱即用的插件:CommonChunkPlugin主要用于提取第三方库和公共模块,避免首屏加载的bundle文件,或者按需加载的bundle文件体积过大,导致加载时间过长,是一把优化的利器。而在多页面应用中,更是能够为每个页面间的应用程序共享代码创建bundle。
- 插件可以携带参数,所以在plugins属性传入new实例。
适用场景
- 对于包含多种资源类型、需要复杂加载逻辑和代码拆分的大型应用,Webpack的全面功能会更加适合。
Vite
- vite打包工具集成了Rollup和ESBuild两个工具,在开发环境使用ESBuild快速启动,在生产环境使用Rollup在去除无用代码的同时,对较新的JS代码也有更好的支持性。
- 为什么Vite在开发环境启动很快:
- 依赖预打包:在项目首次启动时,Vite会使用esbuild快速预打包
node_modules
中的依赖,这样浏览器就可以直接加载预打包后的模块,而不是在浏览器中处理数百或数千个单独的模块文件。 - 即时编译: esbuild 的即时编译能力允许 Vite 在开发环境中按需编译源代码。这意味着只有当一个模块被请求时,Vite 才会调用 esbuild 编译这个模块。这种策略避免了在启动时一次性编译整个项目,显著缩短了启动时间。
- **热模块替换 (HMR)**: Vite 结合 esbuild 实现了高效的热模块替换。当源代码发生更改时,esbuild 可以快速地重新编译受影响的模块,并通过 Vite 的 HMR 机制将更新推送到浏览器,而无需重新加载整个页面。这不仅加快了开发迭代速度,还保持了应用的状态。
- 高性能的代码转换: esbuild 的代码转换速度远超传统的 JavaScript 构建工具,如 Babel。这意味着在进行诸如 TypeScript 转换、ES6+ 特性转换,JSX等操作时,esbuild 能够提供更快的处理速度。
- 依赖预打包:在项目首次启动时,Vite会使用esbuild快速预打包
Rollup
- Rollup 的目标是将代码打包成一个单独的文件,而不是多个文件,所以特别适用于构建库(library)和应用程序的最终产物,尤其是那些采用ES模块(ESM)(ES6中引入ES模块系统)的项目。它通过以下特点区别于其他构建工具:
- Tree Shaking: Rollup 强调其出色的“树摇”(Tree-shaking)能力,这意味着它能非常有效地移除未引用的代码,从而输出更小、更高效的代码包。这对于库作者来说尤为重要,因为它可以帮助用户减少应用程序的体积。
- ES模块优先: Rollup原生支持ES模块规范,这使得它在处理现代JavaScript项目时更为得心应手,尤其是那些计划发布到npm或其他包管理平台的代码库。
- 轻量级与简洁: 相比Webpack等多功能的打包工具,Rollup的配置更为简单,核心关注点在于代码的转换和打包,更适合那些不需要复杂加载器或插件配置的场景。
ESBuild
- 采用 Go 语言开发:Go 语言内置了对并发的支持,可以很容易地编写并发代码。这使得 ESBuild 能够在多核 CPU 上并行处理多个文件,进一步提高了打包速度。
- ESBuild 和 Rollup 都属于 JavaScript 模块打包器。它们的主要目标是将多个 JavaScript 文件打包成一个或多个文件,以便在浏览器或 Node.js 环境中运行。然而,它们在实现方式和性能上有所不同。ESBuild 以其极高的打包速度而闻名,而 Rollup 优势则是其灵活的插件系统和对 ES6 模块的优秀支持。
- Vite在开发模式下利用了ESBuild进行快速的模块转换和加载,而在生产构建时,默认使用Rollup作为其打包工具。这意味着Vite在享受快速开发启动的同时,还能借助Rollup的强大能力来优化最终的生产构建产物
- ESBuild 在处理 AST 时,采用了一种更高效的方式。传统的 JavaScript 工具在处理 AST 时,通常会生成一个包含大量 JavaScript 对象的复杂数据结构,这会消耗大量内存,并且在操作这些对象时还会产生一定的性能开销。而 ESBuild 则采用了一种更接近底层的方式,它将 AST 编码为二进制格式,然后直接在这个二进制格式上进行操作。这样可以避免创建大量 JavaScript 对象,从而节省内存,并且由于二进制操作通常比 JavaScript 对象操作更快,因此也可以提高性能。
- 缺点:
- 不支持热模块替换:(vite项目的热模块替换是vite提供的,HMR 的效果直观体现在浏览器端,用户界面的即时更新),以及可配置性较低,不支持一些复杂的插件和加载器
- 兼容性问题:ESBuild 默认生成的是 ES6 代码,这可能会导致一些兼容性问题。虽然你可以通过配置让 ESBuild 生成 ES5 代码,但这会降低打包速度。
- 不支持增量编译(构建阶段,也就是生产打包):源代码的增量编译是一种编译策略,它只编译自上次编译以来发生变化的部分,而不是每次都编译整个项目。这种策略可以大大提高编译速度,特别是对于大型项目来说。在传统的全量编译中,即使只修改了一个小的部分,也需要重新编译整个项目。这在项目较小的时候可能不会有太大问题,但是当项目变得越来越大,编译时间就会变得越来越长。增量编译通过跟踪源代码的变化,只编译那些自上次编译以来发生变化的文件,或者由于依赖关系需要重新编译的文件。这样,即使在大型项目中,只修改了一小部分代码,编译时间也可以保持在一个相对较低的水平。
- 社区支持较弱:由于 ESBuild 是一个相对较新的项目,因此其社区支持和生态系统还不如其他成熟的打包工具。这意味着,如果你在使用 ESBuild 时遇到问题,可能会比使用其他打包工具更难找到解决方案。
webpack与vite区别
构建过程
- Webpack 是一个模块打包器(module bundler)。它通过入口文件开始,分析整个应用结构,将所有的资源(JS、CSS、图片等)打包成一个或多个 bundle。
- Vite 在开发模式下不打包代码,而是利用浏览器的 ES 模块导入能力,按需加载模块。而不是预先打包整个项目。这意味着 Vite 在开发环境下的启动速度非常快。
热更新
- Webpack使用 Webpack Dev Server 支持热模块替换(HMR),但整个过程相对较慢,因为需要重新编译更新的模块,然后打包整个文件。
- Vite 的热更新更快,因为它只需重新请求改变的模块,而不是重新打包整个应用。
配置
- Webpack 的配置相对复杂,尤其是对于大型项目,需要精细的配置来优化打包过程。
- Vite 提供了更简单的配置和开箱即用的功能,减少了配置工作。
兼容性
- Webpack 支持所有类型的模块(ESM、CommonJS、AMD 等),兼容性好。
- Vite 主要支持现代浏览器,因为它依赖浏览器原生的 ES 模块支持。对于老旧浏览器,需要额外的插件
@vitejs/plugin-legacy
进行兼容性处理。
TreeShaking原理
原理
- 静态模块分析:Tree Shaking 能够工作的基础在于 ES6 模块系统(ES Modules,简称 ESM)的特性。ES6 模块的导入(
import
)和导出(export
)声明是静态的,这意味着它们在编译时就可以确定下来,而不是在运行时动态解析。这种静态性使得构建工具可以在编译阶段就了解模块之间的依赖关系。 - 未使用导出的识别:构建工具(如 Webpack、Rollup 或 Vite)可以遍历整个应用程序的依赖图,并标记出哪些模块或模块的部分实际上并没有被导入和使用。如果一个模块的导出从未被其他地方导入,则该导出可以被认定为未使用,会对这些模块进行标记。
- 代码消除:一旦确定了哪些代码片段是未使用的,构建工具就会在打包过程中将被标记的模块移除,从而减少最终输出文件的大小。这对于减少应用程序加载时间和提高性能非常有帮助。
注意
export default:当使用
export default
导出时,Tree Shaking 的效果有限,因为它代表了整个模块的导出,而且通常会被其他模块整体导入。只有当导入时明确指定了只使用了默认导出的一部分时,Tree Shaking 才能更有效地工作。<!-- 在这种情况下,即使只使用了 add 和 subtract 方法,整个模块都会被导入。因此,multiply 方法也会被包含在最终的打包结果中。-->
// utils.js
export default {
add(a, b) {
return a + b;
},
subtract(a, b) {
return a - b;
},
multiply(a, b) {
return a * b;
}
};
// main.js
import utils from './utils';
console.log(utils.add(1, 2));
console.log(utils.subtract(5, 3));<!-- 只有 add 和 subtract 方法会被导入,而 multiply 方法会被移除,从而减少最终的打包体积。 -->
// main.js
import { add, subtract } from './utils';
console.log(add(1, 2));
console.log(subtract(5, 3));副作用:如果模块中有副作用(即执行某些操作,比如初始化代码、注册监听器等),即使某些导出未被使用,整个模块也可能不能被完全移除。
动态导入:动态导入(
import()
,require()
表达式)在运行时解析,这使得 Tree Shaking 更加复杂,因为它涉及到运行时的决策。尽管如此,现代构建工具仍然可以处理动态导入的情况,并在一定程度上进行 Tree Shaking。对于
Webpack
:- 动态导入(
import()
):Webpack 支持对动态导入进行 Tree Shaking,尤其是在条件分支中。 - **CommonJS 的
require()
**:Webpack 不支持对 CommonJS 模块中的require()
进行 Tree Shaking,因为它是动态的。
- 动态导入(
对于
Rollup
;动态导入(
import()
):Rollup 支持对动态导入进行 Tree Shaking,尤其是在条件分支中。CommonJS 模块(
require()
):Rollup 通过commonjs
插件支持 CommonJS 模块的转换,并且在转换后可以进行 Tree Shaking。npm install --save-dev rollup-plugin-commonjs rollup-plugin-node-resolve # commonjs插件
import commonjs from 'rollup-plugin-commonjs';
import resolve from 'rollup-plugin-node-resolve';
export default {
input: 'src/main.js', // 输入文件
output: {
file: 'dist/bundle.js', // 输出文件
format: 'iife', // 立即执行函数表达式
},
plugins: [
resolve(), // 解析模块路径
commonjs(), // 转换 CommonJS 模块为 ES6 模块
],
};
使用
webpack
:- 开发环境:默认情况下,Webpack 在开发模式下不启用 Tree Shaking。这是因为开发环境中通常希望保留更多的调试信息,并且快速迭代代码,频繁地进行 Tree Shaking 可能会增加构建时间。
- 生产环境:在生产模式下,默认情况下 Webpack 会启用 Tree Shaking。这意味着你可以通过设置
mode
为'production'
来自动启用 Tree Shaking。
Rollup
默认情况下就支持 Tree Shaking,并且在所有环境中都启用 Tree Shaking。Rollup 的设计初衷就是为了让 Tree Shaking 更加高效和可靠。
NodeJS
何为nodejs
- nodejs为基于javascript的 一种服务器端语言,nodejs的包管理器npm是一个非常不错的开源库生态系统
为什么使用nodejs
优点
- 轻量级, Node. js本身既是代码又是服务器,前后端使用同一语言
- 功能强大,非阻塞式I/O,在较慢的网络环境中,可以分块传输数据,事件驱动,擅长高并发访问
缺点: 不适合CPU密集型应用,如果有长时间运行的计算(比如大循环),将会导致CPU时间片不能释放,使得后续I/O无法发起。
nodejs全局对象
- global、 process, console、 module和 exports
模块
CommonJS
导出模块:
module.exports
和exports
导入模块:使用
require()
函数。加载机制:同步加载模块。适用于服务器端,用于浏览器端会堵塞UI渲染
特点:
每个文件被视为一个模块。
模块在首次导入时执行,并且导入的值是模块导出值的拷贝,在后续再次加载时会读取缓存。
允许动态导入模块。
ES Modules (ESM)
- 使用场景:旨在成为 ECMAScript 标准的模块系统,用于浏览器和 Node.js。
- 导出模块:使用
export
关键字。 - 导入模块:使用
import
语句。 - 加载机制:支持异步加载模块。
- 特点:
- 静态结构,可以在编译时进行分析。
- 导入的是模块导出值的引用,而不是拷贝。
- 支持导入和导出的重命名。
- 支持循环依赖处理。
- 主要区别
- 语法:CommonJS 使用
require
和module.exports
,而 ES Modules 使用import
和export
。 - 加载机制:CommonJS 模块是同步加载的,主要用于服务器端,而 ES Modules 可以异步加载,适用于浏览器和服务器端。
- 模块解析:在 CommonJS 中,值是被拷贝的,而在 ES Modules 中,导入的是引用。
- 运行时/编译时:CommonJS 模块的导入是在运行时解析的,而 ES Modules 的导入/导出是在编译时静态分析的。
- 运行时解析:指的是模块的依赖关系和导入操作是在代码运行时动态进行的。在 CommonJS 中,当代码执行到
require()
语句时,Node.js 会计算这个语句的值,找到并加载相应的模块文件,然后继续执行代码。这个过程是动态的,意味着require()
可以根据运行时的条件动态地导入不同的模块。特点:由于是运行时解析,CommonJS 允许使用变量、条件语句或循环来动态生成require()
的参数,提供了很大的灵活性。但这也意味着模块的依赖关系直到运行时才被确定,静态分析工具难以优化。 - 编译时静态分析:指的是模块的依赖关系在代码编译阶段就被确定和分析。在 ES Modules 中,
import
和export
语句使得模块之间的依赖关系在代码还没有执行之前就已经明确。这允许 JavaScript 引擎和工具在代码执行前优化模块加载,比如进行树摇(tree-shaking)以去除未使用的代码。 特点:由于依赖关系是静态的,import
语句不能根据运行时条件动态变化,必须位于模块的顶层作用域。这种静态结构使得模块加载更加高效,也更易于分析和优化,但牺牲了一定的灵活性。
- 运行时解析:指的是模块的依赖关系和导入操作是在代码运行时动态进行的。在 CommonJS 中,当代码执行到
- 互操作性:Node.js 提供了一定的互操作性支持,允许在一定程度上混合使用 CommonJS 和 ES Modules,但需要注意兼容性和限制。
npm cnpm pnpm
区别
npm
(Node Package Manager):
是Node.js的默认包管理工具。
安装包时,每个包会被下载并在[
node_modules
](vscode-file://vscode-app/d:/appDownload/Microsoft VS Code/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html)目录下为每个项目独立安装,这可能导致大量的重复文件。支持语义版本控制和包锁定,以确保依赖的一致性。
cnpm
(China npm):
是npm的一个替代品,专为中国开发者设计,以解决npm在中国大陆下载包时速度慢的问题。
它通过镜像npm仓库来加速包的下载。
使用方式和npm基本相同,但是它会默认使用中国的npm镜像
pnpm
(Performant npm):
- 旨在提高包安装速度和减少磁盘空间的使用。
- 通过使用硬链接和符号链接将单个版本的包存储在一个地方,而不是在每个项目中重复安装,从而节省空间。
- 提供了与npm相似的命令行界面(CLI),但在处理依赖和安装包时更高效。
- 并行安装:
pnpm
安装项目依赖时,可以同时下载和安装多个包加快安装速度。 - 如果在处理大型项目,或者在团队中需要高度一致的依赖管理,或者在资源受限的环境中工作,考虑使用
pnpm
可能会带来显著的好处。
http, express, koa
http, express和koa
都是构建服务器的工具
http
http
模块非常轻量级,没有额外的依赖,适合构建简单的 HTTP 服务器或服务。使用
const http = require("http");
const server = http.createServer((req, res) => {
if (req.method == "GET" && req.url == "/") {
res.writeHead(200, { "Content-Type": "text/html" });
res.end("Hello World http");
} else {
res.writeHead(404, { "Content-Type": "text/html" });
res.end("Not Found");
}
});
server.listen(3000, () => {
console.log("Server is running on port 3000");
});优点
- 开发人员可以完全控制 HTTP 请求和响应的处理流程,包括设置头部、状态码等。
- 由于
http
模块是 Node.js 内置的,因此它可以很好地与 Node.js 的其他核心模块协同工作,提供高性能的服务。
缺点
缺少路由管理功能,需手动实现路由匹配
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('<h1>Welcome to the homepage</h1>');
} else if (req.url === '/about') {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('<h1>About us</h1>');
} else {
res.writeHead(404, {'Content-Type': 'text/plain'});
res.end('Not Found');
}
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});缺少中间件功能,需要手动实现中间件功能
const http = require('http');
const fs = require('fs');
const url = require('url');
// 日志中间件
function logMiddleware(req, res, next) {
console.log(`Request received: ${req.method} ${req.url}`);
next(); // 调用 next() 将请求传递给下一个中间件
}
// 静态文件处理中间件
function staticFileMiddleware(req, res, next) {
const parsedUrl = url.parse(req.url, true);
const filePath = `./public${parsedUrl.pathname}`;
fs.readFile(filePath, (err, data) => {
if (err) {
next(); // 如果文件不存在,则传递给下一个中间件
} else {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(data);
}
});
}
// 最终的请求处理器
function requestHandler(req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World!');
}
// 创建 HTTP 服务器
const server = http.createServer((req, res) => {
// 使用中间件
logMiddleware(req, res, () => {
staticFileMiddleware(req, res, () => {
requestHandler(req, res); // 如果前面的中间件都没有处理请求,则最终由 requestHandler 处理
});
});
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});
express
使用
const express = require("express");
const app = express();
const middleware1 = (req, res, next) => {
console.log("Middleware 1");
next();
};
const middleware2 = (req, res, next) => {
console.log("Middleware 2");
next();
};
const getUserById = (id, callback) => {
// 模拟异步操作
setTimeout(() => {
if (isNaN(id)) {
callback(new Error('Invalid ID'));
} else {
callback(null, { id: id, name: 'John Doe' });
}
}, 1000);
}
// 使用全局中间件,它们会在每个请求路径上都执行。
app.use(middleware1);
app.use(middleware2);
const port = 3000;
// 路由中间件,它只在特定的路由路径上执行。
app.get('/user/:id', (req, res, next) => {
getUserById(req.params.id, (err, user) => {
if (err) return next(err);
req.user = user;
next();
});
})
app.get('/user/:id', (req, res) => {
res.json(req.user);
});
app.listen(port, () => {
console.log(`Express Server is running on port ${port}`);
})优点:
- Express 支持中间件,可以轻松地添加功能,如日志记录、身份验证、错误处理等。
- Express 提供了一套强大的路由机制,可以方便地定义各种 URL 路径和 HTTP 方法。
缺点
- Express 原生使用基于回调的异步处理方式,这可能导致回调地狱(Callback Hell)的问题。
- 中间件中的错误处理需要显式地传递给下一个中间件,否则错误可能无法被捕获。
koa
使用:当中间件中有异步请求时,next得加上await,不然会404
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
const port = 3000;
const middleware1 = async(ctx, next) => {
console.log("Middleware 1");
await next();
};
const middleware2 = async(ctx, next) => {
console.log("Middleware 2");
await next();
};
app.use(middleware1);
app.use(middleware2);
// 模拟异步操作
const getUserById = (id) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (isNaN(id)) {
reject(new Error('Invalid ID'));
} else {
resolve({ id: id, name: 'John Doe' });
}
}, 1000);
});
};
// 使用中间件
router.get('/user/:id', async (ctx, next) => {
try {
const user = await getUserById(ctx.params.id);
ctx.state.user = user;
await next();
} catch (err) {
ctx.throw(400, err.message);
}
});
router.get('/user/:id', async (ctx) => {
ctx.body = ctx.state.user;
});
// 启动应用
app
.use(router.routes())
.use(router.allowedMethods());
app.listen(port, () => {
console.log(`Koa Server is running on port ${port}`);
});优点
- 使用
async/await
使得异步代码看起来像同步代码,提高了代码的可读性和可维护性 - 可以使用
try...catch
语句来捕获异步操作中的错误,使得错误处理更加直观。
- 使用
缺点
- koa本身并不支持路由功能,所以需要安装插件
koa-router
, 并且还需要通过注册中间件的方式注册路由app .use(router.routes())
- koa本身并不支持路由功能,所以需要安装插件
洋葱模型
用于中间件的简单封装
- 洋葱模型是Nodejs中间件处理机制的一种形象化描述,它展示了请求如何通过一系列中间件最终到达处理逻辑,然后再返回响应的过程
- Express的中间件并不是严格遵守洋葱模型,express的next()返回void,在递归回调中不会去等待中间件的异步函数执行完毕。
- Koa的中间件严格遵守洋葱模型,koa的next()会返回一个promise实例,所以可以通过async/await来等待异步函数处理完毕。
class TaskPro { |
事件循环
执行同步代码
- 先执行所有同步代码
执行微任务
- 先执行process.nextTick,优先级最高
- 再执行Promise.then(),.catch(),.finally()
按顺序执行宏任务
每个宏任务执行前都会清空微任务队列
定时器任务,setTimeout,setInterval回调
执行IO回调
检索IO事件,将回调加入队列,等待下次循环执行
执行setImmediate回调,setImmediate:当前事件循环的所有 I/O 事件完成后立即执行。适用于希望在当前事件循环结束后立即执行的任务。
关闭回调事件
// 定时器任务
setTimeout(() => {
console.log('setTimeout');
});
// setImmediate 任务
setImmediate(() => {
console.log('setImmediate');
});
// I/O 操作任务
const fs = require('fs');
fs.readFile(__filename, (res) => {
console.log('I/O 操作');
// 在 I/O 回调中添加一个 setImmediate 任务
setImmediate(() => {
console.log('I/O 中的 setImmediate');
});
// 在 I/O 回调中添加一个 setTimeout 任务
setTimeout(() => {
console.log('I/O 中的 setTimeout');
}, 0);
// 在 I/O 回调中添加一个微任务
process.nextTick(() => {
console.log('I/O 中的 process.nextTick');
});
});
// 微任务
process.nextTick(() => {
console.log('process.nextTick');
});
// Promise 微任务
Promise.resolve().then(() => {
console.log('Promise.then');
});
console.log('同步代码');同步代码
process.nextTick
Promise.then
setTimeout
setImmediate
I/O 操作
I/O 中的 process.nextTick
I/O 中的 setImmediate
I/O 中的 setTimeout
计算机网络
http与https区别
http
- 端口:80
- 无状态连接,明文传输,不安全
https
- 端口:443
- 增加了SSL安全层,加密传输更安全
- CA证书,功能越强大,费用越高
- 握手较为费时,缓存不如http高效
- 采用非对称加密交换会话秘钥,之后使用会话秘钥进行加密和解密会话数据
https握手
- TCP 三次握手:这是建立任何 TCP/IP 连接(包括 HTTPS)的标准过程,确保数据包的可靠传输。
- 第一次握手:客户端发送一个 SYN(同步序列编号)消息到服务器,询问服务器是否开放端口。
- 第二次握手:服务器回应一个 SYN-ACK(同步确认)消息给客户端,确认端口是开放的。
- 第三次握手:客户端再次发送 ACK(确认)消息给服务器,确认收到了服务器的同步确认消息。
- TLS 握手:在 TCP 连接建立之后,HTTPS 通过 TLS(传输层安全协议)进行加密通信的设置。(非对称加密加密会话密钥,使用会话密钥进行对称加密通信)
- 客户端发起请求:客户端(浏览器)向服务器发起 HTTPS 请求。
- 服务器响应并发送证书:服务器响应请求,并发送包含公钥的数字证书(通常由 CA 颁发)。
- 客户端验证证书:客户端验证服务器的数字证书,确保证书有效且可信。如果证书验证失败,通信终止。
- 生成会话密钥:客户端生成一个随机的会话密钥(对称密钥),并使用服务器的公钥对会话密钥进行加密,然后将加密后的会话密钥发送给服务器。
- 服务器解密会话密钥:服务器使用自己的私钥解密会话密钥。
- 使用对称加密进行通信:客户端和服务器使用会话密钥进行对称加密通信。所有后续的数据传输都使用对称加密,以确保数据的机密性和完整性。
- TCP 三次握手:这是建立任何 TCP/IP 连接(包括 HTTPS)的标准过程,确保数据包的可靠传输。
http1与http2
HTTP/1.1(HTTP 1)
**请求协商**:增加了请求头来进行请求协商,比如:Accept,Accetp-language,Accept-Encoding
历史与普及:HTTP/1.1是1999年发布的,至今仍被广泛使用。它是互联网上应用最为广泛的协议之一,支持持续连接、分块传输编码等特性。
**串行请求**:在HTTP/1中,在同一域名下对服务器的请求必须按照顺序进行,即使一个请求很快完成,后续请求也必须等待前一个请求的响应结束才能发出,这被称为“队头阻塞”(Head-of-line blocking)。
无状态性:HTTP协议是无状态的,每次请求都需要携带完整的请求和响应头部信息,对于频繁重复的头部信息,这会导致额外的带宽消耗。
明文传输:HTTP/1.1默认不加密,虽然可以通过TLS/SSL协议实现加密通信(即HTTPS),但这并不是协议本身的特性。
**持久连接**:引入了持久连接(Connection:Keep-Alive)的概念,允许在一个TCP连接上发送多个请求和响应,提升效率,不用多次进行连接。同一域名最多同时建立6个TCP连接。
HTTP/2
- 二进制分帧:HTTP/2使用二进制格式而非文本格式传输数据,更高效且易于解析。数据被分割成更小的帧,可以并行交错发送,提高了效率。
- **多路复用**:解决了HTTP/1的队头阻塞问题,允许在一个TCP连接上同时处理多个请求和响应,无需等待前一个请求完成,提高了并发性能。
- **头部压缩**:引入HPACK压缩算法,对请求和响应头进行压缩,显著减少了传输这些元数据所需的网络字节数。
- 服务器推送:服务器可以主动向客户端推送资源,而无需客户端明确请求,有助于提高页面加载速度。
- 安全性:虽然HTTP/2本身不强制要求加密,但大多数浏览器厂商和Web服务器实现仅支持通过TLS加密的HTTP/2连接,提高了通信的安全性。
总之,HTTP/2在性能、效率和安全性上相比HTTP/1有了显著的提升,特别在处理高并发请求、减少延迟和提高页面加载速度方面表现更优。
UDP与TCP
UDP(用户数据包协议)
- 无连接的传输层协议,效率高,适用于实时应用,IP电话,视频会议,直播,以报文方式传输
TCP(传输控制协议)
面向连接的传输层协议,传输慢,适用于要求可靠传输的应用:文件传输,面向字节流
队头阻塞:
- TCP要求接收到的数据包必须按序到达应用层,如果一个数据包丢失,即使后续的数据包已经到达,TCP也会等待丢失的数据包被重传并到达后,才会按顺序将这些数据包交给应用层。
- 这种按序处理的要求确保了数据的可靠性,但也可能导致网络资源的利用率下降,特别是在高延迟或高丢包率的网络环境中。
流量控制:
防止接收方的缓冲区被溢出,确保接收方能够来得及处理接收到的数据。
使用窗口机制进行流量控制,通过滑动窗口大小动态调整发送数据的速度。
TCP窗口机制包括两个主要概念:滑动窗口和拥塞窗口。
滑动窗口(Sliding Window):滑动窗口是TCP流量控制的核心。它由两部分组成:发送窗口和接收窗口。
发送窗口定义了发送方可以发送数据量。
接收窗口(在数据传输过程中,每当接收方向发送方发送一个ACK(确认)报文时,它会在TCP头部的窗口大小字段中包含一个值,这个值表示接收方的缓冲区还能接受多少字节的数据。这个值基于接收方当前的缓冲区使用情况动态变化。)定义了接收方还能接收的数据量,以避免接收方的缓冲区溢出。
拥塞窗口(Congestion Window)
- 拥塞窗口是TCP拥塞控制机制中使用的一个概念,它限制了在任何时刻发送方可以发送到网络上的数据量,以避免网络拥塞。拥塞窗口的大小是根据网络的拥塞程度动态调整的,不同于接收窗口,它是发送方根据网络状况自行维护的一个状态。
五层模型,7层模型
七层网络模型(OSI模型)和五层网络模型(TCP/IP模型)是网络通信中的两种不同的模型,它们各自定义了网络通信的层次和功能。以下是它们之间的主要区别:
七层网络模型(OSI模型)
- 应用层:提供网络与用户应用程序之间的接口。
- 表示层:确保一个系统的应用层发送的信息可以被另一个系统的应用层读取,即数据的表示、安全、压缩。
- 会话层:建立、管理和终止会话。
- 传输层:提供端到端的数据传输。
- 网络层:处理数据包在网络中的活动,如路由、寻址、分包。
- 数据链路层:在相邻的网络节点之间传输数据帧,处理错误检测和重发。
- 物理层:处理通过物理媒介传输的原始比特流。
五层网络模型(TCP/IP模型)
- 应用层:相当于OSI模型的应用层、表示层和会话层,处理所有与应用程序直接交互的问题。
- 传输层:与OSI模型的传输层相同,负责提供端到端的通信服务。
- 网络层:与OSI模型的网络层相同,负责数据包的路由和转发。
- 数据链路层:与OSI模型的数据链路层相同,负责在相邻网络节点之间传输数据帧。
- 物理层:与OSI模型的物理层相同,负责媒介上的原始比特流的传输。
主要区别
- 层数不同:OSI模型分为七层,而TCP/IP模型通常分为五层。
- 模型定义:OSI模型是一个理论上的参考模型,而TCP/IP模型是基于实际的网络协议设计的。
- 灵活性和实用性:TCP/IP模型由于其简化的层次结构和实际应用中的广泛支持,通常被认为更加灵活和实用。
- 应用层合并:TCP/IP模型将OSI模型的应用层、表示层和会话层合并为一个应用层。
- 标准和协议:OSI模型提供了更为详细的标准分层,而TCP/IP模型则直接基于实际使用的协议。
响应码
成功
- 200——服务器成功返回网页
- 201——请求成功处理,并创建了一个新的资源,响应头中Location字段通常包含指向新创建资源的 URI。
- 202——表示请求已被接受,但尚未处理完成。服务器已经收到了请求,并且计划处理它,但还没有完成处理。客户端可以在稍后检查请求的结果。响应头中Location字段有时会包含一个 URI,客户端可以通过这个 URI 来检查请求的处理状态。
- 203——表示服务器已经成功处理了请求,但返回的实体头部不是原始服务器的直接响应,而是来自某个代理或中间件的修改后的响应。这个状态码通常用于告知客户端响应的内容可能已经被修改或增强。
- 204——请求收到,但返回信息为空
- 205——当客户端提交一个表单后,服务器成功处理请求并返回 205 状态码,客户端应该清除表单数据,以便用户可以重新输入数据。
- 206——客户端请求了资源的一部分,服务器返回了请求的部分内容。
重定向
- 300——请求的资源有多个可用的表示形式,客户端需要选择其中一个。
- 301——请求的资源已永久移动到新位置,客户端应该使用新的 URI 重新发送请求。
- 302——请求的资源临时移动到新位置,客户端应该使用新的 URI 重新发送请求,但请求方法可能会变成 GET,即使原始请求是 POST(自动进行的重定向)
- 303——请求的资源可以被另一个 URI 访问,客户端应该使用 GET 方法访问该 URI。通常用于 POST 请求后的重定向
- 304——表示资源未被修改,客户端可以继续使用之前缓存的版本
- 客户端在发送 GET 请求时,通常会带上
If-Modified-Since
或If-None-Match
头,以告知服务器它希望获取的资源的最新状态。If-Modified-Since
:客户端指定一个时间点,询问服务器自该时间点以来资源是否被修改过。If-None-Match
:客户端指定一个 ETag(实体标签),询问服务器该 ETag 是否匹配当前资源的 ETag。
- 如果资源自上次请求以来未被修改,服务器会返回 304 状态码,而不是重新传输整个资源。
- 如果资源已被修改,服务器会返回 200 状态码,并传输最新的资源内容。
- 客户端在发送 GET 请求时,通常会带上
- 305——请求的资源必须通过代理访问,客户端需要使用指定的代理服务器来访问资源。
- 307——类似于302,告知客户端使用新的 URI 重新发送请求。新的请求需要保留原始请求方法和请求体,确保数据的完整性和一致性。
请求错误
- 400——错误请求,如语法错误
- 401——请求授权失败
- 402——保留有效ChargeTo头响应
- 403——请求不允许
- 404——请求的网页不存在
- 405——请求方法不对
- 415——请求数据类型不对,比如需要
application/json
,而传递的是application/x-www-form-urlencoded
就会报这个错 - 429——请求太多,
Too Many Requests
服务器错误
- 500——服务器产生内部错误
- 501——服务器不支持请求的函数
- 502——服务器暂时不可用,有时是为了防止发生系统过载
- 503——服务器超时过载或暂停维修
- 504——关口过载,服务器使用另一个关口或服务来响应用户,等待时间设定值较长
- 505——服务器不支持或拒绝支请求头中指定的HTTP版本
DNS预解析
- 它允许浏览器在用户实际访问某个链接之前,预先解析该链接中的域名。通过这种方式,当用户最终点击链接或加载资源时,DNS解析这一步骤已经被完成,从而减少了等待时间,提升了页面加载速度和整体的用户体验。
- 控制是否开启预解析的方式
- 使用
<link rel="dns-prefetch">
标签在HTML头部指定特定的域名进行预解析。 - HTTP响应头
X-DNS-Prefetch-Control
可以全局启用或禁用DNS预解析。 - 使用
<link rel="preload">
标签预加载关键资源,这通常也会触发DNS预解析。
- 使用
- 资源消耗:预解析会占用额外的计算资源和网络带宽,因此应该谨慎使用,只对关键资源进行预解析。
常用的http请求头
- Accept: 指定客户端可以接受的响应内容类型,例如
text/html
,application/json
等。 - Accept-Language: 客户端首选的语言种类,例如
en-US,en;q=0.9
表示优先接受美国英语,其次是任何英语。 - Accept-Encoding: 客户端支持的数据压缩编码方式,如
gzip
,deflate
或br
(Brotli)。 - User-Agent: 描述客户端软件的名称和版本,以及操作系统等信息。
- Authorization: 包含用于访问资源的认证信息,通常用于需要身份验证的API调用。
- Cookie: 用于存储用户状态信息,如登录状态或个性化设置。
- Referer: 请求来源页面的URL,有助于服务器分析流量来源。
- Content-Type: 当请求体包含数据时,此头指定数据的MIME类型,如
application/x-www-form-urlencoded
或multipart/form-data
。application/x-www-form-urlencoded
只能发送文本数据,而multipart/form-data
可以发送包括文件在内的多种数据类型。
- Origin: 在CORS(跨源资源共享)请求中,表示发起请求的源,即域名、协议和端口号。
- Access-Control-Request-Method:在预检请求中使用,指定实际请求将使用的 HTTP 方法。
- Access-Control-Request-Headers:在预检请求中使用,指定实际请求将使用的自定义头。比如:Access-Control-Request-Headers:X-Custom-Header,在预检请求通过后,发送实际请求时会有该请求头:X-Custom-Header:value
- DNT (Do Not Track): 表示用户的追踪偏好,如果值为
1
,则表明用户不希望被追踪,减少跨站点跟踪和行为广告 - Cache-Control: 控制缓存行为的指令,例如
no-cache
,max-age
,no-store
等。no-store
:指示缓存系统不得存储任何表示的副本。这意味着响应不应被缓存,无论何时请求该资源,都应该从源服务器重新获取。响应永远不会被缓存,确保了数据的实时性和安全性,但可能会增加网络负载和延迟,因为每次都需要从服务器获取数据。no-cache
:指示缓存系统在再次使用缓存的响应之前,必须先向源服务器验证响应的有效性。这通常涉及到使用条件请求(如If-Modified-Since
或If-None-Match
)来检查资源是否已被修改。使用no-cache
时,响应可以被缓存,但每次使用缓存之前都要进行一次验证。如果资源未改变,服务器会返回一个304 Not Modified状态,这样就可以使用缓存的副本,从而减少带宽使用和提高响应速度。
- Pragma: 与
Cache-Control
类似,但主要用于向后兼容旧的HTTP/1.0缓存机制。 - Connection: 指定连接关闭或持续,例如
keep-alive
或close
。
预检请求
条件(符合一个)
- 请求方法不是简单方法:非
GET
、HEAD
和POST
。如果请求方法是PUT
(替换资源(幂等性),如果资源不存在也可以创建资源)、DELETE
、CONNECT
、OPTIONS
、TRACE
或PATCH
(修改部分资源),浏览器会发送预检请求。 - 请求头包含非简单头:简单头包括
Accept
、Accept-Language
、Content-Language
和Content-Type
(仅限于application/x-www-form-urlencoded
、multipart/form-data
和text/plain
)。如果请求头包含其他头,例如Authorization
、X-Custom-Header
等,浏览器会发送预检请求。 - 请求Content-Type不是简单类型:简单类型的
Content-Type
包括application/x-www-form-urlencoded
、multipart/form-data
和text/plain
。如果Content-Type
是其他类型,例如application/json
或text/xml
,浏览器会发送预检请求。
流程
- 发送预检请求
- 浏览器发送一个OPTIONS请求到目标URL
- 请求头中包含:
Origin
:发出请求的源(协议、主机和端口)。Access-Control-Request-Method,Access-Control-Request-Headers
:表示即将发送的请求的方法和自定义头
- 服务器响应预检请求
- 服务器处理预检请求,检查请求头中的信息,决定是否允许该请求。
- 服务器响应预检请求,指示是否允许该跨域请求
Access-Control-Allow-Origin
:允许的源。Access-Control-Allow-Methods
:允许的 HTTP 方法。Access-Control-Allow-Headers
:允许的自定义头。Access-Control-Allow-Credentials
:是否允许发送凭据(如 Cookies)。
- 浏览器处理请求结果:浏览器接收到服务器的预检响应后,检查响应头中的信息,决定是否继续发送实际的跨域请求。
- 如果预检请求得到允许,浏览器会发送实际的跨域请求。
- 如果不被允许则会抛出错误
作用
- 确保服务器允许跨域请求,服务器可以通过响应预检请求指示是否允许该跨域请求
GET和POST区别
- 数据传输位置:
GET
: 数据作为URL的一部分附加在请求后面,如http://example.com/?param=value
。POST
: 数据放在请求体中,不显示在URL中。
- 数据大小限制:
GET
: 数据大小受URL长度限制,大多数浏览器和服务器限制URL长度不超过2048个字符,尽管实际限制可能更小或更大。POST
: 没有固定的大小限制,数据大小取决于服务器配置和客户端的网络能力。
- 安全性:
GET
: 由于数据在URL中可见,可能不适合传输敏感信息。POST
: 数据在请求体中,相对更安全,适合传输敏感信息。
- 幂等性:
GET
: 是幂等的,即多次相同的GET
请求会产生相同的结果。POST
: 通常不是幂等的,多次POST
请求可能会产生不同的结果,比如多次提交表单。
- 缓存:
GET
: 请求可以被缓存。POST
: 请求通常不会被缓存。
- 创建新资源,通常使用
POST
;如果是要更新现有资源,通常使用PUT
数据存储
特性 | sessionStorage |
localStorage |
cookie |
---|---|---|---|
存储位置 | 浏览器内存 | 浏览器本地存储 | 浏览器 cookie 存储 |
生命周期 | 当前会话 | 持久化 | 可设置过期时间 |
作用域 | 当前标签页/窗口 | 整个域名 | 整个域名 |
容量 | 5MB 左右 | 5MB 左右 | 4KB 左右 |
安全性 | 相对安全 | 相对安全 | 存在安全风险 |
传输方式 | 不随请求发送 | 不随请求发送 | 随每个请求发送 |
强缓存与协商缓存
强缓存
强缓存是指浏览器在请求资源时,直接从本地缓存中读取资源,而不会向服务器发送请求。
常见的控制强缓存的HTTP头有
Cache-Control
和Expires
。如果资源在强缓存有效期内,浏览器会直接使用缓存中的资源,不会发送任何请求到服务器。
协商缓存
协商缓存是指当强缓存失效后,浏览器会向服务器发送一个请求,询问资源是否被修改。
服务器根据请求中的
If-None-Match
或If-Modified-Since
头来判断资源是否被修改。如果资源未被修改,服务器返回
304 Not Modified
状态码,告诉浏览器继续使用缓存中的资源。如果资源被修改,服务器返回
200 OK
状态码,并将新的资源返回给浏览器。
数字证书(CA)
数字证书是一种用于确认持有者身份的电子凭证
数字证书包含的关键信息:
- 公钥:证书持有者的公钥。
- 持有者身份信息:如名称、电子邮件地址等。
- 颁发机构(CA):证书的颁发者。
- 有效期:证书的有效日期范围。
- 证书序列号:证书的唯一标识。
- 数字签名:CA使用自己的私钥对证书内容进行的签名,用于验证证书的真实性。
数字证书的作用:
- 身份验证:帮助确认一个网站或个人的身份,确保通信双方是合法的。
- 数据加密:通过与证书相关联的公钥和私钥,实现数据的加密和解密,保证数据传输的安全。
- 数据完整性:数字签名还可以验证数据在传输过程中未被篡改,确保数据的完整性。
使用场景:
- HTTPS:在网站和浏览器之间安全传输数据。
- 电子邮件加密和签名:确保电子邮件的内容只能被指定的接收者阅读,并验证发件人的身份。
- 代码签名:证明软件或应用程序的发布者身份,以及代码自发布以来未被修改过。
实践
性能优化
图片懒加载
<img>
标签的loading
属性,可以设置为lazy
,默认值为eager
,这样浏览器会延迟加载不在视口中的图片,直到用户滚动到它们附近。<img src="placeholder.jpg" alt="An image" loading="lazy">
或者使用自定义属性data-src=”real-image.jpg“,使用
IntersectionObserver
API 监听图像是否进入视口。当图像进入视口时,将data-src
的值赋给src
属性。<img data-src="image3.jpg" alt="Image 3" class="lazy">
<script>
function lazyLoadImages() {
const images = document.querySelectorAll('img.lazy');
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('lazy-loaded');
observer.unobserve(img);
}
});
});
images.forEach(image => {
observer.observe(image);
});
}
window.addEventListener('load', lazyLoadImages);
</script>第二种方式灵活性较高,可以自定义添加加载时的动画
图片压缩
- JPEG和PNG区别
- JPEG 使用有损压缩算法,这意味着在压缩过程中会丢失一些图像细节和质量。压缩比越高,图像质量损失越大。
- PNG 使用无损压缩算法,这意味着在压缩过程中不会丢失图像细节和质量。图像质量始终保持不变。所以PNG相比于JPEG占用空间更大
- 图片压缩插件:png-pngcrush , gif-gifsicle , jpeg-jpegtran
- 图片压缩可以提高首屏加载时间,因为图片变小了,加载就快了
CSS sprite
- 使用方式:将背景图放入一张图中,通过bakcground-image,background-repeat,background-position,对所需要的图片进行定位。
静态文件部署到CDN上
html,js,css压缩
- 压缩后,文件变小了,加载更快了
首屏CSS检测
- 不是首屏的CSS单独打包并移动到首屏之外延迟加载
js按需加载
合理利用缓存
keep-alive:在组件切换过程中,将状态保存在内存中,防止重复渲染DOM,减少加载时间及性能消耗,提高用户体验性
控制服务器的响应头cache-control
Tree Shaking技术
- 借助webpack的Tree Shaking技术,当我们引入一个模块时,不用引入这个模块的所有代码,只引入需要的代码
路由懒加载
日志埋点SDK设计思路
思路
- 设计一个前端日志埋点SDK主要涉及到数据收集、数据上报、性能优化、错误处理和用户隐私等方面。以下是设计思路的详细步骤
流程
定义埋点数据结构
用户信息:用户ID、会话ID等,用于追踪用户行为。
事件信息:事件类型(点击、浏览、错误等)、事件发生时间、事件标签等。
设备信息:设备类型、操作系统、浏览器版本等。
页面信息:当前页面URL、引用页面URL、页面停留时间等。
性能信息:页面加载时间、接口响应时间等。
数据收集
自动收集:自动捕获页面加载、性能数据、未捕获的错误等。
手动埋点:提供API供开发者在特定操作或事件发生时调用,记录自定义事件。
数据上报
实时上报:事件发生后立即上报。
批量上报:将多个事件记录在本地,定时或达到一定数量后批量上报,减少网络请求。
上报策略:提供配置项,允许开发者根据需要选择上报策略。
性能优化
异步加载SDK:确保SDK的加载不会阻塞页面渲染。
数据压缩:使用gzip等算法压缩上报的数据。
错误重试:网络错误时,自动重试上报。
错误处理
错误捕获:捕获并记录JS错误、资源加载错误等,利用
window.onerror
和unhandledrejection
事件监听全局错误和 Promise 错误。对于
Promise
错误,可以使用unhandledrejection
事件监听或者.catch
来捕获对于
async/await
错误可以使用try/catch
来捕获对于外部脚本加载错误,语法错误或者访问不存在对象上的属性可以使用
window.onerror
监听来捕获<!--脚本加载错误,如果跨域需要加上crossorigin="anonymous"属性-->
<script src="https://example.com/script.js" crossorigin="anonymous"></script>对于资源加载错误,可以使用
element.onerror
事件来捕获:<img src="invalid-image.jpg" onerror="handleImageError(event)">
<script>
function handleImageError(event) {
console.error('Image failed to load:', event.target.src);
}
</script>
统一错误收集:在捕获到错误后,统一交给同一个处理函数进行收集
错误上报:将捕获的错误信息上报服务器。
用户隐私
数据脱敏:对敏感信息进行脱敏处理。
遵守法规:确保数据收集和处理遵守GDPR等隐私法规。
示例代码
// 初始化SDK
logSDK.init({
userId: '用户ID',
sessionId: '会话ID',
reportStrategy: 'batch', // 'real-time' or 'batch'
reportInterval: 10000, // 批量上报间隔,仅在批量上报模式下有效
});
// 手动埋点
logSDK.trackEvent({
eventType: 'click',
eventLabel: 'submit_button',
eventData: {
productId: '12345',
productName: 'example product',
},
});
// 自动捕获错误
window.onerror = function(message, source, lineno, colno, error) {
logSDK.trackError({
message,
source,
lineno,
colno,
error,
});
};
大文件切片上传
大文件切片上传并计算文件Hash值的过程可以分为以下几个步骤:
- 选择文件并切片:用户选择文件后,前端将文件切分成多个小片段。
- 计算整个文件的Hash值:在上传切片之前,先计算整个文件的Hash值,以便校验文件的完整性。
- 并发上传切片:将每个切片作为一个独立的请求并发上传到服务器。
- 上传进度反馈:在上传过程中,实时反馈每个切片以及整个文件的上传进度。
- 服务器端文件重组:所有切片上传完成后,服务器端将这些切片重组成原始文件,并校验文件的Hash值以确保文件的完整性。
- 错误处理和重试机制:对于上传失败的切片,提供重试机制。
// HTML部分
<input type="file" id="file" />
<button onclick="uploadFile()">上传文件</button>
// JavaScript部分
async function uploadFile() {
const file = document.getElementById('file').files[0];
const chunkSize = 1024 * 1024 * 1; // 切片大小,这里以1MB为例
const chunkCount = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const uploadPromises = [];
// 首先计算文件的Hash值
const fileHash = await calculateHash(file);
while (currentChunk < chunkCount) {
const blob = file.slice(currentChunk * chunkSize, (currentChunk + 1) * chunkSize);
const formData = new FormData();
formData.append('file', blob);
formData.append('filename', file.name);
formData.append('chunkIndex', currentChunk);
formData.append('chunkCount', chunkCount);
formData.append('fileHash', fileHash); // 将文件Hash值添加到上传数据中
// 将每个切片的上传Promise添加到数组中
uploadPromises.push(retryUploadChunk(formData,3));// 3代表失败的重试次数
currentChunk++;
}
try {
// 并发上传所有切片
await Promise.allSettled(uploadPromises);
console.log('所有切片上传完成,通知服务器合并切片');
// 所有切片上传完成后,通知服务器进行切片合并
await mergeChunks(file.name, chunkCount, fileHash);
} catch (error) {
console.error('上传失败', error);
}
}
// 使用Web Crypto API计算文件的Hash值
async function calculateHash(file) {
const buffer = await file.arrayBuffer();
const digest = await crypto.subtle.digest('SHA-256', buffer);
return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join('');
}
async function retryUploadChunk(formData, maxRetries) {
let retries = 0;
while (retries <= maxRetries) {
try {
const result = await uploadChunk(formData);
return result;
} catch (error) {
if (retries === maxRetries) {
throw error;
}
console.warn(`上传失败,正在重试... (尝试次数: ${retries + 1})`, error);
retries++;
}
}
}
// 上传单个切片
async function uploadChunk(formData) {
}
// 通知服务器合并切片
async function mergeChunks(filename, chunkCount, fileHash) {
}
如何解决优化白屏
代码分割(Code Splitting):
- 使用 Webpack 等模块打包工具来实现代码分割,按需加载页面所需的资源,减少首次加载的时间。
服务端渲染(SSR):
- 通过服务端渲染首屏页面,用户可以尽快看到页面内容,而不是等待所有 JavaScript 都下载并执行完成后才能看到。
静态页面生成(Static Site Generation, SSG):
- 对于不经常变化的内容,可以预先生成静态页面。这样用户请求时可以直接发送静态页面,减少服务器渲染的时间。
使用骨架屏(Skeleton Screens)(
vue
可以使用v-skeleton-loader
):在内容加载过程中显示一个轮廓预览(骨架屏),提升用户的感知加载速度,改善用户体验。
骨架屏(Skeleton Screen)是一种在内容加载过程中提供给用户的占位图形界面,用于提升用户体验,减少用户对加载时间的敏感度。实现骨架屏的方法有多种,以下是一种基于 CSS 的简单实现方法:
定义骨架屏基本样式:
- 使用灰色背景、圆角等样式来模拟文本行、图片或其他内容的基本形状。
使用 CSS 动画增加加载感:
- 通过渐变动画让骨架屏有一个“闪光”过渡效果,模拟数据正在加载中。
在内容加载完成后隐藏骨架屏:
- 一旦相应的数据加载完成,通过 JavaScript 控制隐藏骨架屏,显示真实内容。
优化资源加载:
- 利用浏览器缓存,为静态资源设置合理的缓存策略。
- 压缩资源文件,包括 HTML、CSS、JavaScript 和图片等,减少传输大小。
- 异步加载非关键 JavaScript 和 CSS,避免阻塞渲染。
优先加载关键资源:
- 确定页面渲染的关键路径资源(Critical Path Resources),并优先加载这些资源。
- 使用
<link rel="preload">
预加载关键资源。
利用 CDN(内容分发网络):
- 将资源部署到 CDN,减少资源的加载时间,特别是对于地理位置分散的用户。
性能监控和分析:
- 使用工具(如 Google Lighthouse、WebPageTest)定期分析和监控网站性能,找出性能瓶颈并解决。
requestIdleCallback:该函数将在浏览器每帧的空闲时段内调用。此外,你还可以提供一个配置对象,用于指定一个超时时间,确保在指定时间内,即使没有足够的空闲时间,任务也会被执行。在这个例子中,
myIdleCallback
函数会在浏览器空闲时被调用。deadline
对象提供了timeRemaining
方法,该方法返回当前帧中剩余的空闲时间(以毫秒为单位)。deadline.didTimeout
是一个布尔值,指示任务是否因为超时而执行。这允许你的代码在有限的时间内执行可能的任务,而不会影响到关键的用户交互。requestIdleCallback(myIdleCallback, { timeout: 2000 });
function myIdleCallback(deadline) {
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
doWorkIfNeeded(); // 执行任务
}
}
静态资源加载失败做降级处理
使用onerror
属性
在
<img>
、<script>
、<link>
等标签中使用onerror
属性,指定当资源加载失败时执行的脚本或替换的资源。<img src="/images/main.jpg" onerror="this.onerror=null; this.src='/images/fallback.jpg';">
备份资源
为关键资源准备备份版本,可以在主资源加载失败时加载备份资源。
<link rel="stylesheet" href="/styles.css" integrity="sha384-..." crossorigin="anonymous">
<noscript><link rel="stylesheet" href="/fallback-styles.css"></noscript>
<script src="/scripts.js" integrity="sha384-..." crossorigin="anonymous"></script>
<noscript><script src="/fallback-scripts.js"></script></noscript>
使用CDN
- 使用多个CDN源,当一个源失败时,可以从另一个源加载资源。
异步加载
- 对于非关键的JS文件,可以使用异步加载,这样即使加载失败也不会阻塞页面渲染。
单点登录实现方式
SESSION+COOKIE模式
- 用户登录请求发送到认证中心,认证中心生成唯一
sid
, 将需要保存的信息存到Session
表中, 然后将sid
返回给用户. - 用户访问A系统, 会将认证中心返回的
sid
带给A系统, A系统则带着sid
去认证中心, 认证中心在Session
表中查看有没有这条信息, 将结果返回给A系统., 判断用户sid
是否合法. - 优点: 认证中心拥有绝对的控制权, 可以不需要用户同意, 直接将用户信息删除.
- 缺点: 对于一些用户量大的应用, 认证中心访问压力过大. 服务成本过高
TOKEN模式
- 用户登录请求发送到认证中心,认证中心返回
token
给用户. - 用户访问A系统, 带着
token
过去, A系统可以自行验证, 不用去访问认证中心(可以子系统和认证中心交换一个密钥, 让子系统自行验证). - 优点: 成本低, 认证中心访问量小
- 缺点: 认证中心失去了对用户的控制. 做不到随时让某用户不能登录,
TOKEN + REFRESHTOKEN模式
- 用户登录请求发送到认证中心,认证中心返回两个
token
给用户, 一个是登录token
, 一个是刷新token
, 登录token
,过期时间很短, 第二个token
过期时间很长 . - 当用户访问A系统, 带着第一个
token
, 如果未过期,就说明登录成功, 如果过期了, 用户需要带着刷新token
, 去认证中心进行获取新的token
.
Axios请求取消
Axios
请求取消主要使用CancelToken API
使用方式如下所示, 下面这个例子主要功能为当一个接口请求被触发时, 如果当前有相同的请求未有返回结果时, 会取消后进来的这个请求
mport axios from "axios";
const api = axios.create({
timeout: 3000
})
// 请求列表
const requestList = [];
api.interceptors.request.use(config => {
const cancelToken = axios.CancelToken.source();
config.cancelToken = cancelToken.token;
// 防止重复请求
if (requestList.includes(config.url)) {
cancelToken.cancel("请勿重复请求");
} else {
// 如果没有相同的请求,就将该请求添加进请求列表中
requestList.push(config.url);
}
return config
}, err => {
Promise.reject(err)
})
api.interceptors.response.use(res => {
// 有结果后, 从请求列表中移除请求
const index = requestList.indexOf(res.config.url);
requestList.splice(index, 1);
return Promise.resolve(res)
}, err => {
// 如果接口请求出错,也要将这个请求从请求列表中移除
const index = requestList.indexOf(err.config?.url);
if (index !== -1) {
requestList.splice(index, 1);
}
return Promise.reject(err)
})
export default api
Pinia持久化存储
添加插件
pinia-plugin-persistedstate
在
main.js
中将插件添加到Pinia
实例上import { createApp } from 'vue'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)在新建
Store
时, 设置persist
属性为true
即可实现数据持久化// 用户信息
export const userDataStore = defineStore('data', () => {
let userData = reactive({
data: {}
})
const channelInfo = ref([])
return {
...toRefs(userData),
channelInfo
}
}, { persist: true })
实现截图
html2canvas
- 简单实现
<div id="test"> |
复杂实现:用户滑动鼠标指定区域截图
- 创建一个与屏幕同宽同高的元素
container
(不涉及滑动) - 获取该
container
元素,转为canvas
- 获取用户指定的截图区域的坐标
- 创建一个新的
canvas
,将截图区域通过drawImage
画到新的canvas
中,drawImage
参数如下:- 第一个参数为图像源
- 第二个参数为左上角z坐标从图像源的哪里开始
- 第三个参数为左上角y坐标从图像源的哪里开始
- 图像源上需要裁剪的宽度,为用户需要截图区域的宽度
- 图像源上需要裁剪的高度,为用户需要截图区域的高度
- 第六个参数为图像源的左上角x坐标位于目标画布左上角x坐标的位置,设为0,代表图像源左上角在目标画布上不偏移
- 第七个参数为图像源的左上角y坐标位于目标画布左上角y坐标的位置,设为0,代表图像源左上角在目标画布上不偏移
- 在目标画布上绘制的宽度,如果与裁剪宽度不一致代表需要进行缩放
- 在目标画布上绘制的高度,如果与裁剪高度不一致代表需要进行缩放
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<div id="container">
<div class="box2"></div>
<div class="box1">这个区域</div>
</div>
<script>
// 添加监听事件
document.body.addEventListener('mousedown', handleMouseDown);
document.body.addEventListener('mousemove', handleMouseMove);
document.body.addEventListener('mouseup', handleMouseUp);
let selectionRect = null;
let startX = 0;
let startY = 0;
let endX = 0;
let endY = 0;
let isDragging = false;
let fullCanvas = null;
// 获取整个容器canvas
const element = document.getElementById('container');
html2canvas(element, {
allowTaint: true, // 显示图片
useCORS: true, //允许跨域
}).then(canvas => {
fullCanvas = canvas;
});
// 监听 mousedown 事件开始记录起始位置
function handleMouseDown(e) {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
clearSelectionRect();
}
// 监听 mousemove 事件计算拖动距离
function handleMouseMove(e) {
if (!isDragging) return;
endX = e.clientX;
endY = e.clientY;
if (!selectionRect) {
drawSelectionRect();
}
// 可以在此处添加一些视觉反馈,比如绘制矩形框
updateSelectionRect(endX, endY);
}
// 监听 mouseup 事件完成截图
function handleMouseUp(e) {
if (!isDragging) return;
isDragging = false;
endX = e.clientX;
endY = e.clientY;
// 执行截图逻辑
performCapture();
}
// 绘制选择区域的矩形框,可以设置其样式
function drawSelectionRect() {
const rect = document.createElement('div');
rect.id = 'capture-area';
rect.style.position = 'absolute';
rect.style.border = '1px dashed #000';
rect.style.zIndex = '9999';
rect.style.left = `${Math.min(startX, endX)}px`;
rect.style.top = `${Math.min(startY, endY)}px`;
rect.style.width = `${Math.abs(endX - startX)}px`;
rect.style.height = `${Math.abs(endY - startY)}px`;
document.body.appendChild(rect);
// 保存矩形框以便后续清除
selectionRect = rect;
}
// 更新选择区域的矩形框
function updateSelectionRect(endX, endY) {
selectionRect.style.left = `${Math.min(startX, endX)}px`;
selectionRect.style.top = `${Math.min(startY, endY)}px`;
selectionRect.style.width = `${Math.abs(endX - startX)}px`;
selectionRect.style.height = `${Math.abs(endY - startY)}px`;
}
// 清除选择区域的矩形框
function clearSelectionRect() {
if (selectionRect) {
document.body.removeChild(selectionRect);
selectionRect = null;
}
}
// 执行截图逻辑
async function performCapture() {
try {
// 假设我们想根据拖动距离来裁剪截图
const x = Math.min(startX, endX);
const y = Math.min(startY, endY);
const width = Math.abs(endX - startX);
const height = Math.abs(endY - startY);
const croppedCanvas = document.createElement('canvas');
croppedCanvas.width = width;
croppedCanvas.height = height;
const croppedContext = croppedCanvas.getContext('2d');
// 将截图区域裁剪到新的canvas中,
croppedContext.drawImage(fullCanvas, x, y, width, height, 0, 0, width, height);
// 显示截图
document.body.appendChild(croppedCanvas);
// 保存为图片
const dataURL = croppedCanvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = dataURL;
link.download = 'screenshot.png';
link.click();
} catch (error) {
console.error('Error capturing screen:', error);
}
}
</script>- 创建一个与屏幕同宽同高的元素
以上只是简单的截图操作,可以从开始截图操作进行优化,截图时的样式,添加工具栏,封装成类等方式进行优化
鉴权方式
OAuth 2.0
- 用于授权应用程序访问用户存储在另一个服务中的信息,而不必直接暴露用户的凭据(如用户名和密码),主要用于Web应用。
OAuth 2.0 的参与者
- 资源所有者(Resource Owner):用户,如QQ用户
- 客户端(Client):请求访问资源的应用程序,如
CSDN
。 - 资源服务器(Resource Server):存储资源的服务,通常和授权服务器属于同一应用,如
QQ
服务器。 - 授权服务器(Authorization Server):负责颁发访问令牌的服务器,如
QQ
服务器。
OAuth 2.0的授权模式
授权码模式(Authorization Code Grant):
- 最完整的授权流程,适合有服务器端的应用,适用于需要高安全性的场景。
- 授权流程
- 客户端(
CSDN
)必须在授权服务器上注册,并获得一个客户端标识(Client ID
)和客户端密码(Client Secret
)。这是为了验证客户端的身份,并确保安全性。例如:CSDN网站必须要在QQ平台注册,才能给用户提供通过QQ号登录功能。 - 用户f访问
CSDN
,使用第三方登录功能,那么这里的资源就是用户的QQ昵称和头像等信息 CSDN
将发送请求到授权服务器获取授权,此时需要携带client_id
,client_id
是用来标识CSDN的身份- 授权服务器(QQ)将返回一个界面给用户,用户需要登录到QQ,并同意授权
CSDN
获得某些信息(资源) - 用户同意授权后,授权服务器将返回一个授权码(
Authorization Code
)给第三方应用(CSDN
) - 第三方应用(CSDN)通过授权码,
Client ID
,,Client Secret
去获取获得访问令牌Access Token
和刷新令牌Refresh Token
,此时授权码失效 - 通过访问令牌可以去资源服务器获取资源
- 在访问令牌过期时,可以通过刷新令牌访问授权服务器获取新的访问令牌
- 客户端(
隐式模式(Implicit Grant)
- 授权码模式的简化版,当用户同意授权后,授权服务器直接返回访问令牌,没有授权码和刷新令牌。
- 适用场景
- 适合单页面应用,单页面应用通常运行在浏览器环境中,没有服务器端来处理复杂的认证逻辑。隐式授权模式简化了认证流程,直接在授权请求中返回访问令牌(
Access Token
),不需要额外的步骤来交换令牌。 - SPA应用通常无法安全地存储客户端密钥(
Client Secret
),因为这些信息会暴露在客户端JavaScript代码中。隐式授权模式不需要客户端密钥,因此避免了这个问题。
- 适合单页面应用,单页面应用通常运行在浏览器环境中,没有服务器端来处理复杂的认证逻辑。隐式授权模式简化了认证流程,直接在授权请求中返回访问令牌(
密码凭证模式(Resource Owner Password Credentials Grant)
- 适用场景:适用于信任的客户端,如企业内部的不同产品。
- 与授权码不同的是,用户使用用户名密码登录,第三方应用通过用户名和密码,
Client ID
,,Client Secret
去授权服务器请求token
,校验通过则发放访问令牌和刷新令牌,之后的流程则和授权码模式相同
客户端凭证模式(Client Credentials Grant)
适用场景:
- 适用于客户端直接请求访问令牌,不需要用户介入。
- 适用于机器到机器的通信,如后台服务之间的通信。
协同算法
OT(Operational Transformation)
- OT:通过操作转换来实现数据的一致性,每个用户对数据的操作都被记录下来,并在其他用户的客户端进行相应的转换,从而实现多个用户对同一份数据的协同编辑。
- 优点:可以实时反应用户的操作,并可以很好的处理并发冲突
- 缺点:
- 中心换化:需要中央服务器进行协同调度,服务端对多个客户端的操作进行转换,并发冲突进行修正,所以冲突的处理都是在服务端完成的,客户端得到的结果一定是一致的。
- 网络要求高:因为用户的操作都需要发送到服务器,如果因为用户的网络出现异常,导致操作缺失或者延迟,服务的转换就会出现问题。服务器一般根据时间戳,用户ID,操作ID来判断哪个操作先执行。
CRDT(Conflict-free Replicated Data Type)
- CRDT:无冲突赋值数据类型,在分布式系统中实现数据最终稿一致的技术,允许多个客户端在无需协调的情况下进行并发更新,并确保在所有更新传播到每个客户端后,各个客户端的状态能够达到一致。
- 实现原理:每个客户端在自己本地进行节点状态的管理,然后通过网络将节点的操作发送到其他客户端,其他客户端根据操作在自己本端也执行相应的操作。
- 优点:
- 去中心化:不需要服务器进行处理
- 网络要求低:对网络延迟有很高的容忍度
- 缺点:
- 网络开销大:每个客户端需要传输整个状态,导致网络开销大
- 存储空间:由于客户端需要记录每个节点的状态,所以本地存储空间需求也大
微信小程序
渲染原理
- 传统的web开发种,页面渲染和JS的执行是在同一线程中完成的,在小程序中引入了双线程技术,将渲染和逻辑放到不同的线程中,从而提高渲染效率。
- 渲染线程:负责页面的渲染和 UI 更新,通过解析和编译小程序的代码,构建页面树和组件树,并将其渲染到屏幕上。
- 逻辑线程:处理小程序的逻辑和交互。它执行小程序的 JavaScript 代码,处理用户的输入和事件,并更新页面的状态。
- 逻辑线程与渲染线程通过消息机制进行通信,当逻辑线程有新的指令或数据更新时,会将消息发送给渲染线程,触发页面的更新和重新渲染。通过将逻辑和渲染分离到不同的线程,逻辑线程能够独立执行,不会阻塞页面的渲染,保证了小程序的快速响应和流畅的交互体验。
登录流程
普通默认直接登录
前端小程序通过wx.login获取登录凭证code,并将该code发送到后端
后端通过调用以下链接获取用户的会话密钥和唯一标识uuid
"https://api.weixin.qq.com/sns/jscode2session"+ [appid] + [appSecret] + [登录凭证code] + "&grant_type=authorization_code"
使用用户的唯一标识,通过createJWT加密算法,生成JWT,返回给用户,用户每次请求时携带该token,后端再通过拦截器,在每次请求时对请求拦截,通过parseJWT去校验token的合法性,通过则放行,不通过返回401状态码
手机号快捷登录
通过设置按钮的 open-type为getPhoneNumber进行手机号快速验证,向用户申请,并在用户同意后,触发bindgetphonenumber回调,回调参数中包含动态令牌code,将code传递给后台来获取手机号。
后台拿到code后,通过调用以下接口,将code放入body请求
POST https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=ACCESS_TOKEN
调用成功会返回用户的手机号信息,后端可以验证用户授权、存储用户信息、提供个性化服务和满足合规性等操作。此时前端可以进行页面跳转。
支付流程
前端流程
- 用户点击支付按钮
- 调用后端接口,获取后端生成的支付参数
- 调用 wx.requestPayment接口,传入后端返回的参数即可完成
后端流程
- 前端发送请求到后端,请求生成支付参数
- 后端调用JSAPI下单,获取预支付交易会话标识prepay_id,后端整理参数,包括以下几个,整理好后,返回给前端作为支付参数
- 小程序ID
- 时间戳
- 随机字符串
- 订单详情扩展字符串:prepay_id
- 签名方式:RSA
- 签名:使用上面的参数,根据签名方式生成的签名
- 用户支付结束后,如果微信服务器未通知到后端支付结果,可以主动调用接口进行订单状态的查询
微信客户端,服务器和用户
- 在前端调用 wx.requestPayment接口后,调用微信支付,发送支付请求,验证支付授权
- 用户确认支付,输入密码,支付成功后,微信服务器根据调用预支付接口时传递的通知url通知后端支付结果