css3实现动画主要有3种方式,第一种是:transition实现渐变动画,第二种是:transform转变动画,第三种是:animation实现自定义动画。
transition的中文含义是过渡。过渡是CSS3中具有颠覆性的一个特征,可以实现元素不同状态间的平滑过渡(补间动画),并且设置过渡持续的时间。
transform属性应用于2D 或 3D转换。该属性允许我们对元素进行旋转(rotate)、缩放(scale)、倾斜(skew)、移动(translate)这四类操作。一般是配合transition的属性一起使用。
animation可通过设置多个节点来精确控制一个或一组动画,常用来实现复杂的动画效果。
定义动画的步骤
(1)通过@keyframes定义动画;
(2)将这段动画通过百分比,分割成多个节点;然后各节点中分别定义各属性;
(3)在指定元素里,通过 animation 属性调用动画。
定义动画:
@keyframes 动画名{
from{ 初始状态 }
to{ 结束状态 }
}
调用:
animation: 动画名称 持续时间;
1、减少对 dom 的操作
(1)当需要添加多个dom时,应将添加的操作合并为一次,可以使用 createDocumentFragment 方法创建虚拟的 dom 对象,对创建的 dom 进行相应的修改,将新 dom 添加到dom对象中,最终再把dom对象添加到真实dom中,这样做对 dom 的多次修改合并为一次,大大减少了回流和重绘的次数。
let box = document.querySelector('#box')
let fragment= document.createDocumentFragment()
for (let i = 0; i < 5; i++) {
let li = document.createElement("li")
li.appendChild(document.createTextNode(i))
fragment.appendChild(li)
}
box.appendChild(fragment)
(2)先使用 display: none把需要修改的 dom 隐藏,修改完成后再将 dom 重新显示。由于使用 display: none 后渲染树中将不再渲染当前 dom,所以多次操作也不会触发多次回流和重绘。
let box = document.querySelector('#box')
box.style.display = 'none';
for (let i = 0; i < 5; i++) {
let li = document.createElement("li")
li.appendChild(document.createTextNode(i))
box.appendChild(li)
}
box.style.display = 'block';
(3)将原始元素复制到一个脱离文档的节点中,对该节点进行修改,然后再替换原始的元素。通过这种方式,我们可以在不影响主文档的情况下对元素进行操作,这样只会触发一次回流。
const el = document.querySelector('.el');
const clone = el.cloneNode(true);
//一系列修改样式、大小或添加删除子节点操作
...
el.parentNode.replaceChild(clone, ul);
2、使元素脱离文档流
当元素浮动( float )、元素的position属性为absolute或fixed时,会使元素脱离文档流,它们样式的修改不会对其他元素的布局产生影响,因此在进行样式修改时,只有该元素本身及其子元素会触发回流和重绘。这样可以减小回流的范围,提高页面的渲染性能。
3、尽量避免频繁读取布局信息
在读取元素的布局信息(如offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle、getBoundingClientRect)时,会触发强制同步布局导致回流重绘。应尽量避免频繁读取布局信息,可以通过缓存布局信息或一次性读取多个属性来减少回流和重绘。
4、使用节流和防抖
对于一些频繁触发的事件(如scroll和resize),可以使用节流(throttle) 或防抖(debounce) 来限制事件的触发频率,从而减少回流和重绘。
5、合并样式修改
以下操作会导致 3次重绘 1次回流:
const el = document.querySelector('.el');
el.style.color = 'blue'; // 导致重绘
el.style.backgroundColor = '#96f2d7'; // 导致重绘
el.style.margin = '10px'; // 导致回流(回流会引起重绘)
如果采用动态添加class或者使用cssText方式的话,只会导致1次回流,从而减少重绘次数:
.change{
color: blue;
background-color: #96f2d7;
margin: 10px;
}
const el = document.querySelector('.el');
el.classList.add('change')
或者使用cssText:
el.style.cssText = "color: blue; background-color: #96f2d7; margin: 10px;";
vue2主要是采用了数据劫持结合发布者-订阅者模式来实现数据的响应式,vue在初始化的时候,会遍历data中的数据,使用object.defineProperty为data中的每一个数据绑定setter和getter,当获取数据的时候会触发getter,在getter中会收集对应的依赖,即收集订阅者,将这些订阅者存储起来;当数据被赋值或者修改时,就会触发setter,在setter中会调用notify方法通知订阅者数据发生变化,订阅者收到消息后调用update方法更新视图。以上是model(数据)改变,view(视图)随之一起改变的原理,而要做到view改变,model也随之改变的话,主要就是监听dom事件,在事件回调函数中对model数据进行修改。
其实vue2和vue3响应式的实现思路差别并不大,主要是将数据劫持的api换成了Proxy,proxy api的第二个参数是一个对象,在这个对象里就可以对数据的操作进行劫持,即setter和getter。get 函数主要做了四件事情:1、对特殊的 key 做了代理;2、通过 Reflect.get 方法返回源对象的值;3、执行 track 函数收集依赖(最核心);4、对计算的值 (Reflect.get返回的值)res 进行判断,如果它也是数组或对象,则递归执行 reactive 把 res 变成响应式对象。set 函数主要做两件事情:1、通过Reflect.set 设置源对象的值;2、通过 trigger 函数派发通知(最核心),并依据 key 是否存在于 target 上来确定通知类型,即新增还是修改。
beforeCreate: 在组件实例初始化完成之后立即调用,会在实例初始化完成、props 解析之后、data() 和 computed 等选项处理之前立即调用。在这个阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。
created: 在组件实例处理完所有与状态相关的选项后调用。在这个阶段 data、methods、computed 以及 watch 上的数据和方法都可以访问了。然而,此时挂载阶段还未开始,因此 $el 属性和跟dom相关的操作仍不可用。
beforeMount :在组件被挂载之前调用。组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。
mounted :在组件被挂载之后调用。在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以进行dom操作。
beforeUpdate :在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个阶段,数据和视图是不一致。
updated :在组件因为一个响应式状态变更而更新其 DOM 树之后调用。父组件的更新钩子将在其子组件的更新钩子之后调用。这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。
beforeDestroy :实例被卸载之前调用。在这一步,实例仍然完全可用。我们可以在这时进行 善后收尾工作,比如清除定时器。
destroyed :实例卸载后调用。调用后,Vue实例指示的东西都会卸载,所有的事件监听器会被移除,所有的子实例也会被卸载。
activated :若组件实例是缓存树的一部分,当页面显示的时候这个生命周期会被调用。
deactivated :若组件实例是缓存树的一部分,当页面隐藏的时候这个生命周期会被调用。
在vue3中,除了beforecate和created(它们被setup方法本身所取代),我们可以在setup方法中访问的API生命周期钩子有9个:
onBeforeMount :在挂载开始之前被调用,还没有创建 DOM 节点,相关的 render 函数首次被调用。
onMounted :组件挂载时调用。
onBeforeUpdate : 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
onUpdated : 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
onBeforeUnmount : 在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
onUnmounted : 卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
onActivated : 被 keep-alive 缓存的组件激活时调用。
onDeactivated : 被 keep-alive 缓存的组件停用时调用。
onErrorCaptured : 当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播。
1、vue2 源码使用flow进行类型检测;vue3 源码使用typescript进行重构,vue对typescript支持更加友好。
2、vue2 使用object.defineProperty来劫持数据的setter和getter方法,对象改变需要借助api去深度监听;vue3 使用Proxy来实现数据劫持,删除了一些api(
o
n
,
on,
on,once,$off) fiter等,优化了Block tree,solt,diff 算法等。
3、vue2 是选项API(Options API),一个逻辑会散乱在文件不同位置(data、props、computed、watch、生命周期钩子等),导致代码的可读性变差。当需要修改某个逻辑时,需要上下来回跳转文件位置;vue3 组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性,不需要在多个options里查找。
4、vue2使用mixins进行代码逻辑共享,mixins也是由一大堆options组成,如果有多个mixins则可能造成命名冲突等问题;vue3可以通过hook函数 将一部分独立的逻辑抽离出去,并且也是响应式的。
5、vue3支持在template中写多个根,vue2只能有一个根。
6、vue2和vue3生命周期写法也有所不同。
核心思想不同
Vue的主要特点是灵活易用的渐进式框架,进行数据拦截/代理,对侦测数据的变化更敏感、更精确,一旦数据改变就去通知依赖者更新视图。
React 推崇函数式编程(纯组件),数据不可变以及单向数据流。
组件写法不同
Vue 推荐的做法是 template 的单文件组件格式(SFC,简单易懂,从传统前端转过来易于理解),即 html,css,JS 写在同一个文件(vue也支持JSX写法)。
React推荐的做法是JSX + inline style, 也就是把 HTML 和 CSS 全都写进 JavaScript 中,即 all in js。
diff算法不同
Vue的Diff算法采用了双端比较的算法,同时从新旧虚拟dom树的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。(相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。)
React的Diff算法首先对新集合进行遍历,是那种从上到下依次遍历,通过唯一key来判断老集合中是否存在相同的节点,再进行相关操作。
响应式原理不同
Vue数据劫持结合发布者订阅者模式实现响应式。
React需要通过setState方法让新的state替换老的state,然后内部自动调用render函数更新视图,具有数据不可变性。
匿名函数是一种没有名称的函数定义,也被称为无名函数、拉姆达函数(lambda function)、闭包(closure)或函数表达式。与具名函数不同,匿名函数在定义时不需要指定函数名,而是将整个函数定义作为一个表达式赋值给一个变量或作为其他函数的参数传递。
匿名函数的语法形式如下:
const functionName = function(parameters) {
// 函数体
};
在上述语法中,function(parameters)表示匿名函数的参数列表,{}内的代码块表示函数体,可以包含一系列语句和逻辑操作。
匿名函数的特点和用途包括:
可以将匿名函数赋值给一个变量,以便在后续使用中调用该函数。
可以将匿名函数作为其他函数的参数传递,用于回调函数、事件处理等场景。
可以在函数内部定义局部作用域,避免全局变量的污染。
可以作为立即执行函数(Immediately Invoked Function Expression,IIFE)使用,即定义后立即调用。
匿名函数的一个示例是使用匿名函数作为回调函数传递给setTimeout函数,实现延迟执行的效果:
setTimeout(function() {
console.log('延迟执行');
}, 1000);
匿名函数在函数式编程、异步编程等场景中经常被使用,它提供了一种灵活的方式来定义和使用函数。
RESTful接口是一种基于HTTP协议和REST(Representational State Transfer,表述性状态转移)架构风格的API设计。RESTful接口基于标准的HTTP方法,如GET、POST、PUT、DELETE等,以提供对资源的创建、读取、更新和删除(CRUD)操作。
GET(SELECT):从服务器取出资源(一项或多项),不改变资源状态。
POST(CREATE):在服务器创建新资源或者执行某个动作。
PUT(UPDATE):在服务器更新资源(客户端提供完整资源数据)。
PATCH(UPDATE):在服务器更新资源(客户端提供需要修改的资源数据)。
DELETE(DELETE):从服务器删除资源。
RESTful接口的设计原则包括无状态性(每次请求都应包含所有信息,不依赖于服务器保存的上下文信息)、客户端-服务器架构(客户端负责用户界面和用户体验,服务器提供数据和服务)、可缓存(响应可以被标记为可缓存或不可缓存,以提高性能)等。
RESTful接口通常以JSON或XML格式返回数据,使得它们可以被各种不同的客户端(如网页、移动应用、其他服务器等)使用。
如果想加速以上及之后的http请求过程的话可以使用缓存服务器CDN,CDN过程如下:
用户输入url地址后,本地DNS会解析url地址,不过会把最终解析权交给CNAME指向的CDN的DNS服务器,CDN的DNS服务器会返回给浏览器一个全局负载均衡IP,用户会根据全局负载均衡IP去请求全局负载均衡服务器,全局负载均衡服务器会根据用户的IP地址,url地址,会告诉用户一个区域负载均衡设备,让用户去请求它。区域负载均衡服务器会为用户选择一个离用户较近的最优的缓存服务器,并把ip地址给到用户,用户想缓存服务器发送请求,如果请求不到想要的资源的话,会一层层向上一级查找,直到查找到为止。
返回304说明客户端缓存可用,直接使用客户端缓存即可,该过程属于协商缓存 返回200的话会同时返回对应的数据
其中遇到CSS加载的时候,CSS不会阻塞DOM树的解析,但是会阻塞DOM树的渲染,并且CSS会阻塞下面的JS的执行
然后是JS加载,JS加载会影响DOM的解析,之所以会影响,是因为JS可能会删除添加节点,如果先解析后加载的话,DOM树还得重新解析,性能比较差。如果不想阻塞DOM树的解析的话,可以给script添加一个defer或者async的标签。
defer:不会阻塞DOM解析,等DOM解析完之后在运行,在DOMContentloaed之前 async:
不会阻塞DOM解析,等该资源下载完成之后立刻运行 进行DOM渲染和Render树渲染 获取html并解析为Dom树
解析css并形成一个cssom(css树) 将cssom和dom合并成渲染树(render树) 进行布局(layout)
进行绘制(painting) 回流重绘 回流必将引起重绘,重绘不一定引起回流 当改变 width、height
等影响布局的属性时会引起回流,或者当获取 scroll、client、offset的值时,浏览器为获取这些值也会进行回流,getComputedStyle 也会引起回流
HTTPS是在HTTP上建立SSL加密层,并对传输数据进行加密,是HTTP协议的安全版,具有不可否认性,可以保证对方身份的真实性,默认端口是443端口,而且会保证数据的完整性。采用 对称加密 和 非对称加密 结合的方式来保护浏览器和服务端之间的通信安全。
HTTPS实现原理:
首先客户端向服务端发送一个随机值和一个客户端支持的加密算法,并连接到443端口。
服务端收到以后,会返回另外一个随机值和一个协商好的加密算法,这个算法是刚才发送的那个算法的子集
随后服务端会再次发送一个 CA 证书,这个 CA 证书实际上就是一个公钥,包含了一些信息(比如颁发机构和有效时间等)
客户端收到以后会验证这个 CA 证书,比如验证是否过期,是否有效等等,如果验证未通过,会弹窗报错。
如果验证成功,会生成一个随机值作为预主密钥,客户端使用刚才两个随机值和这个预主密钥组装成会话密钥;再使用刚才服务端发来的公钥进行加密发送给服务端;这个过程是一个非对称加密(公钥加密,私钥解密)
服务端收到以后使用私钥解密,随后得到那两个随机值和预主密钥,随后再组装成会话密钥。
客户端在向服务端发起一条信息,这条信息使用会话秘钥加密,用来验证服务端时候能收到加密的信息
服务端收到以后使用刚才的会话密钥解密,在返回一个会话密钥加密的信息,双方收到以后 SSL 建立完成;这个过程是对称加密(加密和解密是同一个)。
若想更加深入了解https的加密原理的话可以阅读以下文章:
彻底搞懂https的加密原理
建立连接的目的是为了可靠地传输数据,因此我们必须保证客户端和服务端都能正常的发送和接收数据,如果某一方不能正常的发送或者接收数据,那整个数据的传输就不能成功,也就不可靠。
三次握手:
第一次握手:第一次握手是客户端发送同步报文到服务端,这个时候客户端是知道自己具备发送数据的能力的,但是不知道服务端是否有接收和发送数据的能力;
第二次握手:当服务端接收到同步报文后,回复确认同步报文,此时服务端是知道客户端具有发送报文的能力,并且知道自己具有接收和发送数据的能力,但是并不知道客户端是否有接收数据的能力;
第三次握手:当客户端收到服务端的确认报文后,知道服务端具备接收和发送数据的能力,但是此时服务端并不知道客户端是否具有接收数据的能力,所以还需要发送一个确认报文,告知服务端客户端是具有接收数据能力的。最后,当整个三次握手结束过后,客户端和服务端都知道自己和对方具备发送和接收数据的能力,随后整个连接建立就完成了,可以进行后续数据的传输了。
四次挥手:
第一次挥手:客户端发起关闭连接的请求给服务端;
第二次挥手:服务端收到关闭请求的时候可能这个时候数据还没发送完,所以服务端会先回复一个确认报文,表示自己知道客户端想要关闭连接了,但是因为数据还没传输完,所以还需要等待;
第三次挥手:当数据传输完了,服务端会主动发送一个 FIN 报文,告诉客户端,表示数据已经发送完了,服务端这边准备关闭连接了。
第四次挥手:当客户端收到服务端的 FIN 报文过后,会回复一个 ACK 报文,告诉服务端自己知道了,再等待一会就关闭连接。
建立连接时,被动方服务器端进入“握手”阶段并不需要任何准备,可以将SYN和ACK报文一起发给客户端,开始建立连接。释放连接时,被动方服务器,突然收到主动方客户端释放连接的请求时并不能立即释放连接,因为还有必要的数据需要处理,所以服务器先返回ACK确认收到报文,经过CLOSE-WAIT阶段准备好释放连接之后,才能返回FIN释放连接报文,所以会比握手多一步。
等待 2MSL 是因为要保证服务端接收到了 ACK 报文,因为网络是复杂的,很有可能 ACK 报文丢失了,如果服务端没接收到 ACK 报文的话,会重新发送 FIN 报文,只有当客户端等待了 2MSL 都没有收到重发的 FIN 报文时就表示服务端是正常收到了 ACK 报文,那么这个时候客户端就可以关闭了。
越权访问漏洞:越权漏洞是一种很常见的逻辑安全漏洞。是某应用在检查授权的时候存在纰漏问题,是由于服务器端对客户提出的数据操作请求过分信任,忽略了对该用户操作权限的判定,导致修改相关参数就可以拥有了其他账户的增、删、查、改功能,从而导致越权漏洞。
水平越权:同一权限下的不同用户可以互相访问。攻击者尝试访问与他拥有相同权限的用户的资源,怎么理解呢?比如某系统中有个人资料这个功能,A账号和B账号都可以访问这个功能,但是A账号的个人信息和B账号的个人信息不同,可以理解为A账号和B账号个人资料这个功能上具备水平权限的划分。此时, A账号通过攻击手段访问了B账号的个人资料,这就是水平越权漏洞。
水平越权常见场景:
1、基于用户身份的ID
在使用某个功能时通过用户提交的身份ID (用户ID、账号、手机号、证件号等用户唯一标识)来访问或操作对应的数据。
2、基于对象ID
在使用某个功能时通过用户提交的对象ID (如订单号、记录号)来访问或操作对应的数据。
3、基于文件名
在使用某个功能时通过文件名直接访问文件,最常见于用户上传文件的场景。
垂直越权:权限低的用户可以访问到权限高的用户。
垂直越权是不同级别之间或不同角色之间的越权,垂直越权还可以分为向上越权和向下越权。向上越权指的是一个低级别用户尝试访问高级别用户的资源,比如说某个系统分为普通用户和管理员用户,管理员有系统管理功能,而普通用户没有,那我们就可以理解成管理功能具备垂直权限划分,如果普通用户能利用某种攻击手段访问到管理功能,那我们就称之为向上越权(就是以下犯上)。向下越权是一个高级别用户访问低级别用户信息。
垂直越权常见场景:
1、未认证账户访问无需认证就能访问该功能;
2、不具备某个功能权限的账户认证后成功访问该功能。
1、设计稿上样式相似,多个地方使用: 当多个地方需要类似的功能或UI样式相似时,可以将其抽离为一个组件,以便在多个地方重复使用。
2、复杂的UI结构: 如果某个页面或部分的UI结构设计得非常复杂,可以将其拆分为更小、更易管理的组件,以提高可维护性。
3、可复用的逻辑: 如果某个组件包含了复杂的业务逻辑,这些逻辑可能在其他地方也需要使用,可以将这些逻辑抽离为一个可复用的组件。
4、应用需要较高性能: 通过按需加载组件,可以在页面渲染时减少不必要的资源加载,从而提高应用性能。
5、团队协作: 在多人协作的项目中,拆分组件可以让团队成员更容易独立开发、测试和维护不同的部分。
6、考虑技术栈变化: 如果考虑项目技术栈发生变化的情况,组件的抽离可以使迁移工作更加容易,因为你只需要关注一个个独立的组件。
注意:过度抽离可能会导致组件层次过深,增加理解和调试的难度,因此需要在合理的范围内进行抽离。
1、配置webpack,在生产环境中去除console.log的打印;
2、将项目中用到的图标制作成雪碧图,减少http 请求数,并且充分利用缓存来提升性能;
3、去除不必要的请求,将项目中残留的无效请求连接去除(废弃的后台接口等),避免重复的资源请求,合理设置HTTP缓存;;
4、图片懒加载,在图片即将进入可视区域的时候进行加载;
5、使用 webpack 插件 image-webpack-loader对图片进行压缩;
6、使用事件委托,防抖和节流,尽量不要使用JS动画,css3动画和canvas动画都比JS动画性能好;
7、将外部脚本置底(将脚本内容在页面信息内容加载后再加载);
8、在首页不需要使用的脚本文件,可以使用懒加载的方式对其进行加载(只有在需要加载的时候加载,在一般情况下并不加载脚本内容。)使用import(“…/…/xxxx.js”)方式加载文件即懒加载,webpack 的懒加载实现在打包时会将懒加载的代码切割出去单独打包,然后在主包中进行按需加载,最后执行调用;
9、精简javaScript和css;
10、减少不必要的 HTTP跳转 。
【关于性能优化这个问题也可以用具体的实例说明,回答的模板:
性能问题的出现:在今年的xx月,测试同事发现在这个项目的xx页面加载的时候出现卡顿。(这点其实能编,注意 对于产品、测试、用户 而言,能直观感受到的就是卡顿、慢)
问题复现:随后我打开页面,通过工具测试发现(这里的工具可以是performance、lighthouse、前端埋点SDK亦或者其他第三方的监测工具,你要说你直接调用浏览器的performance API估计也能行但不推荐,容易被面试官反问为什么不封装个性能检测工具…orz)几个性能指标存在问题:FCP、TTI这两个性能指标都过长,FCP达到了3.x秒,TTI更是长达5.x秒(不要选太多性能指标,很多性能指标可以不纳入你们公司的衡量范围,or你编的衡量范围~~)。
问题分析:(分析过程相信大家都有,这段大家可以自己想想,在此我以FCP为例)我发现在xx页面加载的时候会先获取几张比较大的图片,导致FCP指标过长。
优化方案:采取了图片优化策略xxx执行优化。(下文提及哪些优化策略)
量化优化效果:在经过上述的优化方案后,我们最终将FCP优化到了1.8秒,TTI优化到了3.8秒。(量化你的优化成果)
(非必要)优化是否达标:如果同学们的公司对性能指标的数据有强要求,比如FCP必须在2秒以内诸如此类…,可以提一下,可以代表你在之前的公司是有完善的性能优化流程的。】

import 和 import() 都是 ES6 中用于导入模块的语句,而 require() 则是 Node.js 中用于导入模块的函数。
使用 import 语句导入模块时,模块会被静态加载,也就是在编译时就已经确定了导入的模块;import() 和 require() 都是动态加载模块的方式。它们都允许在代码运行时根据需要加载模块,而不是在编译时就将所有模块都加载进来。不过两者的实现方式略有不同:import() 是基于 Promise 的异步加载,而 require() 是同步加载模块。
在整个应用程序中,使用 import 和 import() 语句导入的模块都是动态只读引用:
动态,即原始值发生变化,import加载的值也会发生变化,不论是基本数据类型还是复杂数据类型。
只读,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型,若对import的变量进行修改则会报错(但复杂数据类型可以修改其内部的属性,因为并没有将复杂数据类型的指向修改)。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
导入的数据是复杂数据类型:


运行index.js, 结果如下:

导入的数据是基本数据类型:


运行index.js, 结果如下:

修改index.js文件:

运行结果:

import()异步加载的证明:

import()加载数据的结果与import是一样的:

运行结果:

使用 require() 导入的模块,属于浅拷贝。如果导入的数据是复杂数据类型,由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。如果导入的数据是基本数据类型,则属于复制,是两个单独的模块,因此对该模块的值做修改时不会影响另一个模块。同时,在另一个模块可以对该模块输出的变量重新赋值。当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块(require)无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
导入的数据是复杂数据类型:


运行index.js, 结果如下:

导入的数据是基本数据类型:


再次运行index.js, 结果如下:

import 和 import() 语句支持模块的默认导出和命名导出,而 require() 只支持模块的默认导出 (module.exports) 导出。
判断页面是否触底主要是通过监听页面滚动事件(scroll),判断页面被卷去的高度(scrollTop)加可视区域的高度(clientHeight)是否大于等于整个文档的高度(scrollHeight),如果是则代表页面已经触底了
// 浏览器触底加载功能的实现
/* 1.视口的高度+页面被卷去的高度=滚动条的长度 */
window.addEventListener("scroll",function(){
//页面被卷去的高度: window.scrollY
//页面被卷去的高度: window.pageYOffset
//页面被卷去的高度: document.documentElement.scrollTop
//页面被卷去的高度: document.body.scrollTop
//document.body.scrollTop与document.documentElement.scrollTop两者有个特点: 同时只会有一个值生效。比如
//document.body.scrollTop能取到值的时候,document.documentElement.scrollTop就会始终为0;反之亦然。
//所以,如果要得到网页的真正的scrollTop值,可以这样:
//scrollTop=document.body.scrollTop+document.documentElement.scrollTop;
//这两个值总会有一个恒为0,所以不用担心会对真正的scrollTop造成影响。
// console.log("页面被卷去的高度:",window.scrollY,window.pageYOffset,document.documentElement.scrollTop,document.body.scrollTop);
// body页面的滚动条高度(整个文档的高度): document.body.scrollHeight
// 整个页面你的滚动条高度(整个文档的高度): document.documentElement.scrollHeight
// console.log(document.body.scrollHeight,document.documentElement.scrollHeight);
// 可视区域的高度: document.documentElement.clientHeight
// console.log(document.documentElement.clientHeight);
if(document.documentElement.clientHeight+document.documentElement.scrollTop>=document.documentElement.scrollHeight){
console.log("触底了!!!!");
}
})

局部滚动的触底也是类似的原理,直接上代码吧(vue3):
<template>
<div style="color: red; width: 100%;">
<div class="scroll" style="width: 600px; height: 500px; overflow-y: scroll;">
<p style="padding: 10px;" v-for="item in 100">数据大屏自适应函数</p>
</div>
</div>
</template>
<script >
import { defineComponent, ref, onMounted } from 'vue';
export default defineComponent({
name: 'Wisdom',
setup(props) {
onMounted(() => {
document.querySelector('.scroll').addEventListener('scroll', (e) => {
const target = e.target;
if(target.scrollTop + target.clientHeight >= target.scrollHeight) {
console.log('触底了')
}
})
});
},
});
判断元素是否进入可视区域主要是通过监听页面滚动事件(scroll)然后判断:
竖向滚动:元素距文档顶部距离 - 页面被卷去的高度 < 视口高度 且 元素距文档顶部距离 - 页面被卷去的高度 + 元素高度 > 0
横向滚动:元素距文档左侧距离 - 页面被卷去的高度 < 视口宽度 且 元素距文档左侧距离 - 页面被卷去的高度 + 元素宽度 > 0
这种方法的优点是兼容性好,可以支持 IE8 及以上浏览器。缺点是需要考虑滚动条的影响,也需要获取元素的尺寸和位置,比较繁琐。
function isInViewport(element) {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const offsetTop = element.offsetTop;
const windowHeight = window.innerHeight;
const elementHeight = element.offsetHeight;
return (
offsetTop - scrollTop < windowHeight &&
offsetTop - scrollTop + elementHeight > 0
);
}

vue项目的目录结构通常在src目录下有个assets文件夹,和src同级的地方有个static文件夹。
相同点:两个文件夹下都可以用于存储项目中所需的静态资源,像图片,样式文件等等。
区别:assets下存放的静态资源文件在项目打包时,也就是执行 npm run build 指令时,会走webpack的打包流程,做压缩代码体积、代码格式化这种操作;放static中存放的资源文件不会走打包流程,而是直接复制进最终的dist目录里。如果我们把所有资源都放进static下,由于该文件夹下的资源不会走打包流程,在项目打包时会提高一定的效率。但是同时也有一个问题,就是由于不会进行压缩等操作,项目打包后的体积会比把资源都放进assets下来得大。
总结:我们通过npm run build打包项目后,会生成一个dist文件夹,放在assets里面的资源会被webpack打包后放进dist文件夹中,而static里面的资源是直接复制进dist中,由于第三方类库资源一般都已经经过处理了,所以我们可以在static里放一些外部的第三方资源文件,assets放我们自己项目中的图片等资源,让这些资源走打包流程,减少最终包的体积。但是实际开发中情况肯定是多变的,还是要根据实际情况来看把静态资源文件放在哪里更合适。
具体可查阅以下这篇文章:
mvc和mvvm的区别和应用场景
首先,在扫码前,手机端必然是已登陆状态,PC端登录的账号肯定与手机端是同一个账号。不可能手机端登录的是账号A,而扫码登录以后,PC端登录的是账号B。那如何实现呢?有些同学会想到,是不是扫码过程中,把密码传到了PC端呢?但这是不可能的。因为那样太不安全了,客户端也根本不会去存储密码。
大概步骤如下:
1、用户打开PC端,进入二维码登录界面,PC端向服务端发起请求,告诉服务端,我要生成用户登录的二维码,并且把PC端设备信息也传递给服务端,服务端收到请求后,生成二维码ID,并将二维码ID与PC端设备信息进行绑定,然后把二维码ID返回给PC端;
2、PC端收到二维码ID后,生成二维码(二维码中肯定包含了ID),此后,为了及时知道二维码的状态,客户端在展现二维码后,PC端不断的轮询服务端,比如每隔一秒就轮询一次,请求服务端告诉当前二维码的状态及相关信息(或者使用Websocket,Websocket是指前端在生成二维码后,会与后端建立连接,一旦后端发现二维码状态变化,可直接通过建立的连接主动推送信息给前端)。
3、用户用手机去扫描PC端的二维码,通过二维码内容取到其中的二维码ID,再调用服务端接口将移动端的身份信息与二维码ID一起发送给服务端,服务端接收到后,将身份信息与二维码ID进行绑定,生成临时token,返回给手机端。因为PC端一直在轮询二维码状态,所以这时候二维码状态发生了改变,在界面上把二维码状态更新为已扫描。
为什么需要返回给手机端一个临时token呢?临时token与token一样,它也是一种身份凭证,不同的地方在于它只能用一次,用过就失效。在第三步骤中返回临时token,为的就是手机端在下一步操作时,可以用它作为凭证。以此确保扫码,登录两步操作是同一部手机端发出的。
4、手机端在接收到临时token后会弹出确认登录界面,用户点击确认时,手机端携带临时token用来调用服务端的接口,告诉服务端,我已经确认要在PC端登录了,服务端收到确认后,根据二维码ID绑定的设备信息与账号信息,生成PC端登录的token,这时候PC端轮询接口,它就可以得知二维码的状态已经变成了"已确认"。并且从服务端可以获取到PC端登录的token,到这里,登录就成功了,后面PC端就可以用这个token去访问服务端的资源了。
vNode,或称虚拟节点(Virtual Node),是虚拟 DOM 中的一个概念。它是一个用来描述真实 DOM 结构的 JavaScript 对象,用于描述标签或组件具体是怎样的。vNode 包含了元素的类型、属性、子元素等信息,但它只存在于内存中,并不直接映射到实际的浏览器 DOM。
vNode 主要有以下几个属性:
类型(Type): 表示元素的类型,如标签名、组件名等。
属性(Props): 包含元素的属性,例如样式、事件处理程序等。
子元素(Children): 表示当前元素包含的子元素,可以是其他 vNode 对象或者文本节点。
键(Key): 用于在更新时识别 vNode,帮助虚拟 DOM diff 算法更准确地比较新旧虚拟 DOM 树。
虚拟 DOM(vDOM)通常是由多个虚拟节点(vNode)组成的。每个 vNode 对象代表着虚拟 DOM 树中的一个节点,多个 vNode 对象组合在一起形成了完整的虚拟 DOM。它是一个在内存中存在的树形结构,用来表示真实 DOM 的层次结构和状态。虚拟 DOM 的主要目的是为了优化 DOM 操作的效率。
1、由于js需要借助浏览器提供的DOM接口(document对象)才能操作真实dom,所以操作真实dom的代价是比较大的,而有了虚拟dom的话,新的vDom会与旧的vDom进行对比,如果内容没变化的话,就直接复用原先的真实dom,当内容发生改变时才会另外生成新的真实dom替换掉旧的真实dom,这样就减少了操作真实dom的次数,提高了性能;新旧vDom的对比是在内存中进行的,不涉及实际的浏览器渲染,(新旧真实 DOM 元素的对比是在浏览器的渲染引擎中进行的,具体来说是在Reflow和Repaint阶段。当有新的真实 DOM 元素插入、删除或更新时,浏览器会触发重新布局(Reflow)和重绘(Repaint)操作。在这个过程中,浏览器会比较新旧 DOM 结构,确定需要进行怎样的布局和绘制变化。Reflow(或Layout)是指浏览器重新计算元素的位置和尺寸,确保它们正确显示在页面上。Repaint 是在元素样式没有改变但需要重新绘制的情况下触发,例如滚动条的移动。这两个过程的开销相对较大,因此优化库和框架通常会尽量减少对真实 DOM 的直接操作,采用一些策略来最小化布局和绘制的次数,提高性能。)性能和效率都更好。
2、 虚拟 DOM 的抽象层可以帮助实现跨平台开发,因为它使得可以在不同平台上使用相同的虚拟 DOM 结构,而在具体渲染时适配各个平台。
3、 虚拟 DOM 简化了复杂的 DOM 操作,使得开发者更专注于应用的逻辑和结构,提高了开发效率。
1、原型链继承
让子类的原型对象指向父类的实例,当子类的实例找不到对应的属性和方法时,就会沿着原型链往上查找。
function Parent() {
this.name = 'Parent';
}
function Child() {
this.childName = 'Child';
}
Child.prototype = new Parent();
优点:简单易懂。
缺点:子类原型对象指向同一个父类的实例,当有两个子类实例对象时,修改其中一个就会影响其它子类实例;没有实现super功能,无法传递参数给父类构造函数。
2、构造函数继承(借用构造函数)
在子类的构造函数中执行父类的构造函数,并且为其绑定子类的this。
function Parent(x) {
this.name = 'Parent';
}
function Child() {
Parent.call(this, 'aa');
this.childName = 'Child';
}
优点:避免了属性共享问题(即子类实例互相影响),可以传递参数给父类构造函数。
缺点:无法继承父类原型上的方法和属性,每个实例都有一份父类构造函数的副本。
3、组合式继承
结合原型链继承和构造函数继承的继承方法。
function Parent() {
this.name = 'Parent';
}
function Child() {
Parent.call(this);
this.childName = 'Child';
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
优点:兼顾了原型链继承和构造函数继承的优点。
缺点:调用两次父类构造函数,存在一份多余的属性,影响性能。
4、寄生组合式继承
解决组合式继承中对父类构造函数的不必要调用问题,避免在子类原型上创建多余的父类实例。
// 寄生式继承,只继承父类的原型
function inheritPrototype(child, parent) {
var prototype = Object.create(parent.prototype);
prototype.constructor = child; // 修复构造函数指向
child.prototype = prototype;
}
function Parent(name) {
this.name = name;
}
Parent.prototype.sayHello = function() {
console.log("Hello, " + this.name);
};
function Child(name, childName) {
Parent.call(this, name); // 借用构造函数,继承属性
this.childName = childName;
}
// 寄生组合式继承核心
inheritPrototype(Child, Parent);
Child.prototype.sayChildHello = function() {
console.log("Hello, " + this.childName);
};
var childInstance = new Child("ParentName", "ChildName");
childInstance.sayHello(); // 输出: Hello, ParentName
childInstance.sayChildHello(); // 输出: Hello, ChildName
与组合式继承方式对比可知,寄生组合式继承主要是将Child.prototype = new Parent()换成了Child.prototype = Object.create(Parent.prototype),Object.create 用于创建一个新对象,创建的新对象是一个浅拷贝。
优点:避免了多余的父类构造函数调用,保持原型链完整性,支持构造函数传参(es6的类继承原理是寄生组合式继承)。
缺点:相较于其他的继承方式,寄生组合式继承的实现相对复杂,对于初学者来说,寄生组合式继承可能不够直观,理解起来需要一些额外的学习成本。
Vue2 中使用的是基于 Object.defineProperty来实现响应式的。当一个对象被定义为响应式时,Vue 会为对象的每个属性都创建一个 getter 和 setter,这样当属性被访问或修改时,Vue 就能够进行侦测并触发相应的更新。但是对于新增的属性,由于在对象创建时并没有对应的 getter 和 setter,因此默认情况下是非响应式的。为了使新增的属性也能够具有响应性,$set 方法被引入。
使用:
Vue.set( target, key, value ) / this.$set( target, key, value )
target:要更改的数据源(可以是对象或者数组)
key:要更改的具体数据,或者新增的属性名
value :重新赋的值
原理:
set 方法会对参数中的 target 进行类型判断
如果是 undefined 、null 、基本数据类型,直接报错。
如果为数组,取当前数组长度与 key 这两者的最大值作为数组的新长度,然后使用数组的 splice 方法将传入的索引 key 对应的 val 值添加进数组。target 在 observe 的时候,原型链被修改了, splice 方法也已经被重写了,触发之后会再次遍历数组,进行数据劫持,也就是说当使用 splice 方法向数组内添加元素时,该元素会自动被变成响应式的
如果为对象,会先判断 key 值是否存在于对象中,如果在,则直接替换 value(因为这说明key这个属性已经是响应式的了,那么就直接将value赋值给这个属性)。如果不在,就判断 target 是不是响应式对象(其实就是判断它是否有 ob 属性),接着判断它是不是 Vue 实例,或者是 Vue 实例的根数据对象,如果是则抛出警告并退出程序。如果 target 不是响应式对象,就直接给 target 的 key 赋值(我一开始在这里有个疑惑,不是响应式对象的话,那不就应该重新给他绑定setter和getter吗?怎么就直接赋值了呢?其实这里是我想岔了,target并不是那个要新增的属性,而是那个要新增属性的源对象,如果这个源对象都不是响应式数据了,那么就代表它是不需要响应式的,换句话说就是这个源对象并不是vue的data下的属性,直接给它赋值就完事了),如果 target 是响应式对象,就调用 defineReactive(就是调用 Object.defineProperty进行setter和getter的绑定) 将新属性的值添加到 target 上,并进行依赖收集,更新视图。
简化的代码:
function set(target, key, value) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, value);
return value;
}
if (key in target && !(key in Object.prototype)) {
target[key] = value;
return value;
}
const ob = target.__ob__;
if (!ob) {
target[key] = value;
return value;
}
defineReactive(ob.value, key, value);
ob.dep.notify();
return value;
}
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
},
});
}
赋值(Assignment):
当把一个对象赋值给一个新的变量时,赋的是该对象在栈中的地址,而不是堆中的数据。两个变量指向同一个存储空间,无论哪个变量修改了指向的对象的属性值,其实都是改变存储空间的内容,因此两个对象是互相影响的。(修改对象里的基本数据也是会互相影响)
let obj1 = { name: 'John' };
let obj2 = obj1; // 赋值
obj2.name = 'Jane';
console.log(obj1.name); // 输出: Jane
浅拷贝(Shallow Copy):
浅拷贝创建一个新的对象,是重新在堆中创建内存,但仅复制原始对象的一层属性。拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存会互相影响。
let obj1 = { name: 'John', hobbies: ['reading', 'coding'] };
let obj2 = Object.assign({}, obj1); // 浅拷贝
obj2.hobbies.push('gaming');
console.log(obj1.hobbies); // 输出: ['reading', 'coding', 'gaming']
深拷贝(Deep Copy):
深拷贝创建一个新的对象,是重新在堆中创建内存,并递归地拷贝原始对象的所有嵌套属性。新对象与原始对象完全独立,修改其中一个不会影响另一个。
深拷贝更消耗内存和性能,特别是在处理大型对象或对象之间存在循环引用时。
let obj1 = { name: 'John', hobbies: ['reading', 'coding'] };
let obj2 = JSON.parse(JSON.stringify(obj1)); // 深拷贝(简单方式,不适用于包含函数或循环引用的对象)
obj2.hobbies.push('gaming');
console.log(obj1.hobbies); // 输出: ['reading', 'coding']

obj.hasOwnProperty(‘xx’):判断某个属性是否属于自身对象的,不会查找原型链。
在vue2中,数据的响应式是基于Object.defineProperty实现的,但对于数组却不是使用Object.defineProperty来实现响应式的,而是通过改写数组的方法来实现对数组的监听(例如 push、pop、shift、unshift、splice 等)。主要原因有:
1、Object.defineProperty无法劫持数组长度length属性的变化,而数组length属性会影响数组的变动。

2、Object.defineProperty只能劫持已有属性,要监听数组变化,必须预设数组长度,遍历劫持,但数组长度在实际引用中是不可预料的。
3、数组删除或者新增会导致索引发生变动,每次变动都需要重新遍历,添加劫持,数据量大时非常影响性能。(数组新增的元素都不会被劫持,当数组的长度小于某个被劫持的元素的下标+1时,该索引位置的劫持也会失效)

泛型允许我们在定义的时候不具体指定类型,而是泛泛地说一种类型,并在函数调用的时候再指定具体的参数类型,也就是说泛型也是一种类型,只不过不同于 string, number 等具体的类型,它是一种抽象的类型,我们不能直接定义一个变量类型为泛型。简单来说,区别于平时我们对「值」进行编程,泛型是对「类型」进行编程。
1、interface(接口) 是 TS 设计出来用于定义对象和函数类型的,可以对对象和函数的形状进行描述,无法定义其它类型;type(类型别名)会给一个类型起个新名字, 类型别名有时和interface(接口)很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
interface定义对象:
interface Person {
name: string
age: number
}
interface定义函数:
interface MyFunction {
(param1: number, param2: string): void;
}
type定义任何类型:
type MyNumber = number; //基本类型
type MyUnion = number | string; //联合类型
type MyIntersection = { prop1: number } & { prop2: string }; //交叉类型
type MyObject = { prop1: number; prop2: string }; //对象类型
type MyArray = number[]; //数组类型
type MyTuple = [number, string]; //元组类型
type MyFunction = (param1: number, param2: string) => void; //函数类型
type MyGeneric<T> = Array<T>; //泛型类型
type MyConditionalType<T> = T extends string ? string : number; //条件类型
type MyMappedType = {
[K in 'prop1' | 'prop2']: number;
}; //映射类型
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; sideLength: number };//可辨识联合类型
2、interface 支持声明合并,这意味着你可以定义同名的多个接口,它们会被自动合并为一个接口;type 不支持声明合并,对于同名的 type,会产生冲突,导致报错。
interface 的声明合并:
interface Person {
firstName: string;
}
interface Person {
lastName: string;
}
// 合并后的 Person 接口
/*
interface Person {
firstName: string;
lastName: string;
}
*/
type 不支持声明合并:
type MyType = {
property1: string;
};
type MyType = {
property2: number; // Error: 不能重新定义属性“property2”。
};
3、interface 使用 extends 关键字来实现继承,而 type 使用交叉类型来实现继承。(type使用联合类型进行类型组合并不直接称为“继承”,而是叫做联合类型)
interface的继承:
interface Shape {
color: string;
}
interface Dimensions {
width: number;
height: number;
}
interface RectangularShape extends Shape, Dimensions {
// 继承了 Shape 和 Dimensions 接口的属性
}
type的继承:
type Person = {
name: string;
};
type Employee = {
role: string;
};
type EmployeePerson = Person & Employee;
const employee: EmployeePerson = {
name: 'John',
role: 'Developer',
};
补充:
条件类型的实际用例:

映射类型的实际用例:



可辨识联合类型的实际用例:

1、unknown 和 any 的主要区别是 unknown 类型检查会更加严格,在对 unknown 类型的值执行大多数操作之前,我们必须进行某种形式的检查。而在对 any 类型的值执行操作之前,我们不必进行任何检查。(可以将任何东西赋给 unknown 类型,但在进行类型检查或类型断言之前,不能对 unknown 进行操作;可以把任何东西分配给any类型,也可以对any类型进行任何操作。)
举例说明:
function test(callback: any) {
callback();
}
test(1);
因为 callback 是any类型的,所以 callback()语句不会触发类型错误。我们可以用any 类型的变量做任何事情。但是运行时会抛出一个运行时错误:TypeError: callback is not a function。1 是一个数字,不能作为函数调用,TypeScript并没有保护代码避免这个错误。
将callback 参数的类型换为unknown,unknown 变量接受任何值。但是当尝试使用 unknown 变量时,TypeScript 会强制执行类型检查:
function test(callback: unknown) {
callback();
}
test(1);
如果执行以上代码,因为 callback 的类型是 unknown,所以callback() 语句在代码编译阶段就会报一个类型错误 :Object is of type ‘unknown’。 与 any 相反,TypeScript会保护我们不调用可能不是函数的东西。要想以上代码正确通过编译,那么在使用 unknown 类型的变量之前,必须进行类型检查:
function test(callback: unknown) {
if (typeof callback === 'function') {
callback();
}
}
test(1);
2、any类型的值可以赋值给其他任意类型的变量,其他任意类型(number、string等)的值可以赋值给any类型的变量。

unknown 类型的值只能赋值给 any 和 unkown 类型的变量,其他任意类型(number、string等)的值都可以赋值给unknown 类型的变量。

如果将注释打开的话就会报错:

补充:
1、在联合类型中,如果任一组成类型是 unknown,那么这一联合类型就是unknown类型( any类型除外)。
type UnionType1 = unknown | null; // unknown
type UnionType2 = unknown | undefined; // unknown
type UnionType3 = unknown | string; // unknown
type UnionType4 = unknown | number[]; // unknown
如果有一种组成类型是 any,那么这一联合类型就是 any类型。
type UnionType5 = unknown | any; // any
2、由于任何类型都可以赋值给 unknown 类型,所以在交叉类型中包含 unknown 不会改变结果。
type IntersectionType1 = unknown & null; // null
type IntersectionType2 = unknown & undefined; // undefined
type IntersectionType3 = unknown & string; // string
type IntersectionType4 = unknown & number[]; // number[]
type IntersectionType5 = unknown & any; // any
1、安装官方的类型声明库
一般来说,如果你用的是一些比较大型、常用的第三方库,那么官方已经帮你写好类型声明了,只要按照这个库的官方文档,安装 @types/库名 这个库就行了。比如,使用 lodash 库的时候,只需要安装它的类型声明库:
npm install --save-dev @types/lodash
2、自己给第三方库写声明文件
并不是所有的第三方库都会写好类型声明库,此时就需要自己给第三方库写声明文件了。在项目的根目录下创建types文件夹(与src文件夹同级),编辑 tsconfig.json 文件,告诉 typescirpt 去哪里找我们自己定义的声明文件:
"baseUrl": "./",
"paths": {
"*": [ "types/*" ]
},
在types文件夹下写对应的第三方库的类型声明,方法就是在types 下新建文件夹,文件夹的名字一定要和第三方库的名字一模一样,然后在新建的文件夹下再新建一个index.d.ts文件,在index.d.ts文件里写类型声明,比如说要给jQuery写类型声明,那么我们就要创建以下目录结构:
|-- test-project //项目名字
|-- ...
|-- src
|-- types
|-- jQuery
|-- index.d.ts // 第三方库的声明文件
|-- ....
在index.d.ts文件中书写的类型,对于不同的库有不同的写法,这里就不展开描述了。但要注意的是声明文件中只是对类型的定义,不能进行赋值。
3、使用 declare module 语法
使用这种方式会把环境包当作 any 类型引入,只能解决代码报错的问题,但是丧失了ts的类型检查功能。
可以在项目根目录下新建 global.d.ts,内容写上
declare module 'xxxx' // 'xxxx'为第三方库的名字
用于实现不同参数输入并且对应不同参数输出的函数,在前面定义多个重载签名,一个实现签名,一个函数体构造,重载签名主要是精确显示函数的输入输出,实现签名主要是将所有的输入输出类型做一个全量定义,防止TS编译报错,函数体就是整个函数实现的全部逻辑。在函数调用的时候根据参数的类型执行不同的函数。
TypeScript 是一种静态类型语言,它在编译阶段执行严格的类型检查以确保代码的正确性和可靠性。
function calculateClockHandRotation(startTime, endTime) {
const FULL_CIRCLE_DEGREES = 360;
const DEGREES_PER_HOUR = 30;
// 处理边界情况,使startTime和endTime始终在1-12范围内
startTime %= 12;
endTime %= 12;
// 计算两点间的实际小时差,取最短距离(考虑时钟是圆形)
const diff = endTime - startTime;
let hourDifference = Math.abs(endTime - startTime);
let rotationDegrees;
// 如果超过6小时,选择通过12点的更短路径
if (hourDifference > 6 && diff < 0) {
hourDifference = 12 - hourDifference;
rotationDegrees = hourDifference * DEGREES_PER_HOUR;
}else if(hourDifference > 6 && diff >= 0){
rotationDegrees = -hourDifference * DEGREES_PER_HOUR;
}else if(diff < 0) {
rotationDegrees = -hourDifference * DEGREES_PER_HOUR;
}else {
rotationDegrees = hourDifference * DEGREES_PER_HOUR;
}
return rotationDegrees;
}