知识文档迁移
进程与线程
1、一个程序由很多个进程组成,而每一个进程至少由一个线程组成
2、为了避免各个进程相互影响,所以各个进程之间是相对独立的
浏览器主要进程
- 浏览器进程(显示、用户交互、子进程)
网络进程(负责网络资源的加载)
渲染进程:渲染进程启动后,他会启动一个 渲染主线程,负责执行HTML、CSS、JS代码
默认情况下,浏览器每开启一个新的网页,它会新增一个渲染进程,避免一个标签页崩溃不影响其他网页
Q1: 为什么浏览器的渲染进程不启动多线程来处理事情
因为如果以多线程处理问题,那么就会产生一系列问题,例如程序执行到一半,用户与浏览器产生了交互,这是应该先处理程序还是用户的交互,又例如定时器到达了时间,应该马上执行回调函数还是继续执行当前程序?因此 事件循环就是渲染主线程去解决多线程处理问题 的答案
异步
为什么是渲染主线程是异步
- 首先代码在执行中会遇到一些无法立即完成的任务:例如AJAX、setTimeout、setInterval,addEventListener
- 如果渲染主线程是同步的话就会导致必须等待某个任务执行完才会执行下一个任务,会处于长期【阻塞】的状态导致浏览器卡死。
图片
如何理解JS是异步的
- JS是一门单线程语言,这是因为在浏览器的渲染进程中,只有一个渲染主线程。它既要渲染页面,又要执行JS,如果JS以同步的方式执行,容易导致渲染主线程阻塞,白白消耗时间。导致消息队列其他任务无法执行,页面卡死无法更新,给用户造成卡死的现象。
- 所以浏览器以异步的方式来避免,具体的做法是当遇到某些特定的任务时,例如网络、计时器、事件监听将任务交给其他线程处理,自己立马结束,转而执行后续代码。当其他线程结束时候将事先传递的回调函数包装成任务加入到消息队列末尾,等待渲染主线程执行,在这种情况下渲染主线程永不阻塞,最大限度保证单线程流畅运行
JS为什么会阻塞渲染
因为当JS在执行的时候,如果修改了DOM结构,那么会产生新的任务到任务队列中(绘制任务),然后继续执行JS代码,那么就可能会导致页面并不会马上渲染修改DOM后的结果
/** * 经典例子 * 这里的效果就是,点击了之后,执行回调函数 * 然后更改DOM之后,产生一个重绘的任务到消息队列中 * 然后死循环3秒 * 然后执行重绘的任务 */ function delay(duration){ const now = new Date() while(Date.now() - now < duration){} } btn.onClick = ()=>{ xxx.content = '更改后的文本' delay(3000) }
图片
队列有没有优先级
- 随着浏览器的复杂度的提升,已经抛弃了宏任务和微任务的说法,目前浏览器中,至少包括微队列,延时队列,和一些交互队列。其中微队列被视为优先级最高的队列,渲染主线程在执行任务的时候会优先处理微任务列队
- ps:添加微任务主要的方式是:Promise.resolve().then(这个回调函数)
事件循环
事件循环也叫消息循环,是浏览器渲染进程中的渲染主线程的主要工作方式
在Chrome源码中,它其实就是一个不会结束的for循环,每次循环执行消息队列中的第一个任务,而其他线程则需要在合适的时机把回调函数包装成任务添加到消息队列末尾即可
过去把消息队列分为宏任务队列以及微任务队列,但是目前已经无法满足复杂的工作环境,取而代之的是灵活多变的多队列方式
根据W3C的官方文档解释,每个任务都有其对应的类型,同类型的任务必须放在同队列中,不同的队列具有不同的优先级,其中必须有一个微任务队列是优先级最高的,它需要优先执行,如果它没有任务,则根据其他队列的优先级取任务到渲染主线程中执行。
Promise.resolve().then(() => { console.log(0) return Promise.resolve(4) }).then(res => { console.log(res) }) Promise.resolve().then(() => { console.log(1) }).then(() => { console.log(2) }).then(() => { console.log(3) }).then(() => { console.log(5) }) /** 微任务队列[ (() => { console.log(0) return Promise.resolve(4) }).then(res => { console.log(res) }) (() => { console.log(1) }).then(() => { console.log(2) }).then(() => { console.log(3) }).then(() => { console.log(5) }) 打印结果: ]; --------------------------------------- 微任务队列[ (() => { console.log(0) return Promise.resolve(4) }).then(res => { console.log(res) }) (() => { console.log(1) }).then(() => { console.log(2) }).then(() => { console.log(3) }).then(() => { console.log(5) }) ]; 打印结果:0 这里promise.then中遇到Promise.resolve(4),其实会产生一个微任务到队尾;当他执行的时候会把4,放回原来的位置执行,其实就是会生成一个x=>x的微任务, --------------------------------------- 微任务队列[ (() => { console.log(1) }).then(() => { console.log(2) }).then(() => { console.log(3) }).then(() => { console.log(5) }) new Promise(resolve=>resolve(4)) .then(x=>x) .then(res=>{console.log(res)}) ]; 打印结果:0 1 --------------------------------------- 微任务队列[ new Promise(resolve=>resolve(4)) .then(x=>x) .then(res=>{console.log(res)}) (() => { console.log(2) }).then(() => { console.log(3) }).then(() => { console.log(5) }) ]; 打印结果:0 1 执行resolve(4),把后面的继续加入微任务队列 --------------------------------------- 微任务队列[ (() => { console.log(2) }).then(() => { console.log(3) }).then(() => { console.log(5) }) .then(4=>4) .then(res=>{console.log(res)}) ]; 打印结果:0 1 2 --------------------------------------- 微任务队列[ .then(4=>4) .then(res=>{console.log(res)}) () => { console.log(3) }).then(() => { console.log(5) } ]; 打印结果:0 1 2 执行4=>4,把后面的继续加入微任务队列 --------------------------------------- 微任务队列[ () => { console.log(3) }).then(() => { console.log(5) } 4=>{console.log(4)} ]; 打印结果:0 1 2 3 --------------------------------------- 微任务队列[ (4) => { console.log(4) } () => { console.log(5) } ]; 打印结果:0 1 2 3 4 --------------------------------------- 微任务队列[ () => { console.log(5) } ]; 打印结果:0 1 2 3 4 5 --------------------------------------- */
JS能不能做到精确计时
- 不能,因为计算机硬件没有原子钟,无法做到精确计时
- 操作系统的计时函数有少量偏差,由于JS的计时器任务是调用的操作系统的函数,也会有偏差
- 按照W3C的标准,嵌套超过5层则会带有接近4毫秒的偏差
- 受事件循环影响,计时器的回调函数只能在渲染主线程空间时执行,又因此有时间偏差
常见的宏任务/微任务
//宏任务
script (可以理解为外层同步代码)
setTimeout/setInterval
setImmediate(Node.js)
I/O
UI事件
postMessage
//微任务
Promise
process.nextTick(Node.js)
Object.observe
MutaionObserver
nextTick:process.nextTick
是一个特殊的函数,用于将回调函数插入到事件循环的"next tick"队列中。这意味着回调函数会在当前阶段完成后立即执行,而不是等待下一个阶段。
Node.js的Event Loop
https://juejin.cn/post/6844904100195205133
Node.js是运行在服务端的JS,虽然他也用到了V8引擎,但是服务目的和环境不同,导致了他API与原生JS有些区别;他的Event Loop还要处理一些I/O,比如新的网络连接等,所以与浏览器Event Loop也是不一样的。Node的Event Loop是分阶段的,如下图所示:
图片
- timers:执行 setTimeout 和setInterval的回调
- pending callbacks:执行迟到的下个循环的 I/O 回调函数
- idle、prepare:系统内部使用
- poll:检索新的 I/O 事件、执行相关回调,事实上除了其他几个阶段的事情,所有的异步基本都在这里运行
- check:setImmediate在这里执行
- close callback:关闭一些回调函数
注意:当进入poll阶段的时候分两种情况
- 如果 poll 队列不为空:先遍历队列,并同步执行回调,直到队列清空/执行回调数达到系统上线
- 如果 poll 队列为空:
- 如果设置了 setImmediate ,那么时间循环执行完poll 会到check阶段执行setImmediate
- 如果 没有设置setImmediate
- 如果有timers:事件循环会回到timers,执行有效回调
- 如果没有timers:事件循环会阻塞在 poll 阶段,等待回调被加入到 poll 队列
执行顺序:
首先进入的是timers
阶段,如果我们的机器性能一般,那么进入timers
阶段,一毫秒已经过去了(setTimeout(fn, 0)
等价于setTimeout(fn, 1)
),那么setTimeout
的回调会首先执行。
如果没有到一毫秒,那么在timers
阶段的时候,下限时间没到,setTimeout
回调不执行,事件循环来到了poll
阶段,这个时候队列为空,此时有代码被setImmediate()
,于是先执行了setImmediate()
的回调函数,之后在下一个事件循环再执行setTimemout
的回调函数。
而我们在执行代码的时候,进入timers
的时间延迟其实是随机的,并不是确定的,所以会出现两个函数执行顺序随机的情况。
渲染原理
浏览器是如何渲染到页面的
- 当浏览器通过网络线程接收到HTML文档之后(CSS、JS都在里面),交给了渲染主线程的消息队列中,消息队列通过事件循环的机制把渲染任务从任务队列中调出进行渲染
整个渲染的流程分为:解析HTML、样式计算、布局、分层、绘制...
解析HTML:
解析HTML:在HTML中遇到CSS就解析CSS,遇到JS就会JS,但是在解析之前会启动一个 预解析 的过程,它会率先下载外部的CSS、JS文件
如果主线程遇到 link 的位置,此时如果外部的CSS文件还没下载好,主线程并不会等待,继续解析后续HTML,这是因为
下载和解析
的工作在预解析
线程中进行,这是CSS不会阻塞HTML的根本原因如果主线程遇到 script 的位置,会停止解析HTML,转而等待JS文件下载完成好后执行JS,等执行完JS后才继续进行HTML的解析,这是因为JS可能会操作DOM树,所以这才是JS会阻塞HTML的根本原因。
经过这一步之后会得到DOM树、CSSOM树(包含浏览器的默认样式、内部样式、外部样式等)
图片图片
样式计算
主线程在得到DOM树之后会依次为树的每个节点计算样式,这个过程会把相对值变成绝对值,例如color:red 变成color: rgb(255,0,0);相对单位变成绝对单位,em变成px。这样就能得到一棵带有样式的DOM树。
css
flex
flex-grow: 表示当基准尺寸固定之后,剩余的空白面积占比。flex-grow默认为0,felx-grow表示占剩余空白部分的一份,如果盒子的flex为2,其他的是1,那么则表示这个盒子占两份;
flex-shrink:收缩系数:flex 元素仅在默认宽度之和大于容器的时候才会发生收缩。收缩宽度的计算公式:超出容器的宽度 ((元素宽度 权重) / 弹性项的总宽度)
图片flex-basis:基准尺寸;默认是auto,也就是元素本身是多大就是多大,也就是伸缩前的宽度作为基准尺寸+(grow / shrink的宽度);例如 现在有一个页面需要三等份,需要自适应布局,但是有一份是有文本的,如果全部都设置了flex1,那么就会导致有文本的比其他两份要长,这时候设置伸缩前的基准尺寸都为0即可,也就是都从0开始计算;
flex:flex:1:表示直接缩写<flex-grow, flex-shrink, flex-basis>为1 1 0%
transition 动态添加类名
1、transition允许在状态发生改变的时候过渡到新的样式,可以应用到元素的各种属性,例如颜色、尺寸、位置
2、一般来说transition是由事件的变化而触发,例如:hover、:focus、类名的变化
3、transition:【需要过渡的属性】 【过渡时间】 【延迟函数】
浮动
1、浮动某个元素的时候会使该元素脱离文档流
- 该元素之前的元素如果是标准文档流,那么该元素的垂直距离将跟紧之前的元素
- 该元素之前的元素如果是脱离文档流的也是浮动的,那么将跟在浮动元素的后面
2、清除浮动的影响
- 清除浮动clear:left | right | both | none(default)
- 清除浮动只能清除该元素的左边的浮动效果 或者右边的浮动效果,不能影响其他元素
3、浮动虽然会使元素在普通文档流中移除,不再占用文档流位置,但是仍然会影响其他非浮动的元素:例如文字缠绕,浮动元素的影响就类似是形成了一个障碍,迫使其他元素围绕他布局,而相对于决定定位,绝对定位就好像使得元素不存在于标准文档流上
BFC(block formatting context)
1、什么是bfc:一个独立的渲染区域,有自己的渲染规则,与外部元素不会相互影响
2、开启条件
- float不为none
- position为absolute或者fixed
- overflow不为hidden
- display为inline-block
Box-size宽度的影响
content-box:给盒子设置宽度,其实是设置内容区的widtth
border-box:如果给盒子设置宽度,那么会动态计算内容区的宽度,content_width = width - padding - border,并且防止塌陷,content_width最小是0
CSS选择器
Blog:css选择器
- 通配选择器(*)
- 元素选择器(div{})
- 类选择器(#wrapper)
- 类选择器(.child)
- 交集选择器(div.text#content)div标签且类是text且id是content
- 并集选择器(div , .text , #content)div标签、类为text、id值为content、的样式都可以命中(有“,”为并集选择,没有则为交集选择)
- 关系选择器
- 后代:div a(div标签中的所有a标签)
- 子代:div > p(div标签中所有第一层的p子标签,嵌套的不算)
- 兄弟:div + a (只会命中div的下一个a标签)
- 属性选择器([title="shanghai”]) (只会命中的标签)
响应式方案
- VW + VH
- Flex布局
- Grid布局(1fr)
- 百分比
- 媒体查询
javaScript
CommonJS 与ESModule 的区别
语法区别,一个是require,一个是import,一个是module.export/exports.xxx,一个是export
- 来源:CommonJS是一个社区标准是来源于社区,是跟着node.js出来的,而ESModule是源于官方的
- 时态:
- CommonJS是运行时态,也就是代码要运行起来才知道,因为它是社区标准所以不可能干预V8的编译
- ESModule则是运行时态(import()) + 编译时态(静态),运行时态是在ES7之后就支持了。编译时态是有好处的,也就是在运行之前就能确定依赖关系图,这样子就能做 webpack 的tree shaking,所以tree shaking必须使用的是静态的语法
隐式类型转换
原始 -> 数字
- true:1
- false:0
- null:0
- undefined:NaN (重点undefined转数字是NaN)
- string :
- 空字符串(空格、空白字符(\t、\n、\br)):0
- 去掉引号的话:不是数字就是NaN
原始 -> 字符串
- undefined:'undefined'
- null:'null'
- number:'数字'
- boolean:'true' 或者 'false'
所有 -> 布尔
- Null:false
- undefined:false
- NaN:false
- number
- 0:false
- 其他:true
- string
- 空字符串:false
- 其他:true
- 对象:true
对象转原始
- 首先会调用对象上的 toPrimitive 方法,看这个方法的返回值能否得到原始类型
- 如果没有toPrimitive方法将调用 valueOf 方法,看这个方法能否得到原始类型
- 如果前两个方法都不能得到原始类型,那么将调用 toString 方法
- 最终不行就报错 :typeError : cannot convert object to primitive value
注意:
- 数组如果没有valueOf方法,调用toString的话,其实就是调用了一下数组的join方法,但是会把undefined与null去除再调用join,NaN并不会处理,也就是[undefined,null].toString()的话得到的就是','
减、乘、除法的计算规则
- 我们在对各种非
Number
类型运用数学运算符(- \* /
)时,会先将非Number
类型转换为Number
类型。
加法的特性
以下规则优先级为从高到底:
- 当一侧为
String
类型,被识别为字符串拼接,并会优先将另一侧转换为字符串类型。 - 当一侧为
Number
类型,另一侧为原始类型
,则将原始类型转换为Number
类型。 - 当一侧为
Number
类型,另一侧为引用类型
,将引用类型
和Number
类型转换成字符串后进行拼接拼接。
注意:如果是对象:{} + 1 和 ({} + 1) 是不一样的,{} + 1 中的 {}是作为代码快,例如是if中后面的那个大括号,他是不返回值的,所以就变成了 + 1,所以是1;而使用()可以强制解析成他是一个对象的字面量,( {} + 1 )则是会判断这个空对象是否包含toPrimitive、valueOf,目前都没有的话,就直接调用toString,变成'[object Object]1'
==比较的规则
- NaN和任何类型比较永远是false(包括NaN自己)
- Boolean和其他值做比较,优先会把Boolean转换成数字
- String和Number比较,会把String转换成Number
- 除了null == undefined为true,除此之外 null 和 undefined 和其余所有比对结果都是false
- 对象转原始的规则,toPrimitive、valueOf、toString
0 == ''//true,0==0
0 == '0'//true,0==0
2 == true//false,2==1
2 == false//false,2==0
false == 'false'//false,先把false转换成0,然后把'false'转换成数字为NaN
false == '0'//true,0==0
false == undefined//false,除了undefined == null 为true,其他都是false
false == null //false,除了undefined == null 为true,其他都是false
null == undefined//true,特殊
' \t\r\n '==0//true,把字符串转成数字,空格和空白字符均去除
null + '1' == null + 1 //'null1'== 0+1,false
null + 1 == 1 //true,0+1==1
null == 0 //false,只要null与其他非undefined的值进行判定都是false,只有null==undefined为true
null == undefined//true,特殊
null + 1 == undefined + 1 //false,0 + 1 == NaN + 1
null + null == undefined + undefined //false,0==NaN + NaN
const obj1 = {
a:1,
b:2,
valueOf:function(){
return this.a + this.b;
}
toString:function(){
return 1
}
}
const obj2 = {
toString(){
return 0
}
}
console.log(obj1 + !!obj2)//4,3 + true,因为!!obj2是表达式,所以3+true最终结果为1
Iframe的问题
- Iframe 优点
- 安全性 可以将来自不同源的网站隔离在一个沙盒中 以防攻击首页
- 可以跨域通信 因为iframe 可以与主页进行跨域通信 从而实现丰富嵌入式程序(windouw.parent.postMessage需要复习怎么通信)
- 分离内容,因为每个iframe都是独立的程序,这允许主页将不同的供应商放在一起
- 无需刷新,iframe允许在不刷新整个页面的情况下加载新的内容,
- Iframe 缺点
- 性能问题,每个iframe都是独立的网站,加载时需要浪费资源
- 不利于浏览器搜索
- 兼容性问题
隐藏类(hidden-class)
1、隐藏类是V8引擎用于优化对象访问性能的底层机制,它的工作原理是通过动态创建的对象,生成一种隐藏的“类结构”,使得动态对象操作更加高效。
2、优化建议:
- 在构造函数中初始化的属性避免使用 “动态属性”
- 始终按照 同样的顺序构建对象,使隐藏类可以复用
- 预定义对象的结构,可以通过类或工厂函数创建固定结构的对象
3、隐藏类的应用
function User1(name,age){
this.name = name;
this.age = age
}
function User2(name,age){
this[name] = name
this.age = age
}
for(let i = 0 ; i < 1000000 ; i++){
new User1(`User_${i}`,i) / new User2(`User_${i}`,i)
}
解析:User1创建了三个hidden class,当执行 ‘ this.name = name ’ 这句话的时候创建隐藏类,在后续的执行中是可以复用的,但是User2执行 ' this.User_i ' = name的时候每次创建的都是不同的隐藏类
图片
图片
伪数组
定义:伪数组是一个
拥有类似数组结构
的对象,但他并不具有数组上的方法与功能,它可以 「通过索引属性(非负数)访问元素」 并且 「具有 length 属性」 的 对象。常见的伪数组:function内部的
arguments对象
,DOM元素列表
(例如通过querySelectorAll获取的集合)转换成数组的方法:Array.from、Array.prototype.slice.call(伪数组)、Spread运算符([...伪数组])
注意:数组的concat方法遇到伪数组的时候会把伪数组这个对象与其他值进行拼接
const fake_arr = { '1': 'AAA', '3': 'CCC', length: 8 } [].concat.call(fake_arr, [7, 8]);//[ { '1': 'AAA', '3': 'CCC', length: 8 }, 7, 8 ]
事件三要素
- 事件源:谁发生的,产生的对象或者元素
- 事件类型:
type
、click
、mouseMove
、keydown
- 事件处理程序:指事件触发后要执行的代码块或者函数
函数科里化
函数科里化是一种编程技术,能把一个多参数的函数转换成一系列单一参数的函数,并且每个函数返回接收剩余参数的函数
function curry_add(){
let allArgs = [...arguments]
function inner(){
allArgs = [...allArgs,...arguments]
return inner
}
inner.valueOf = function (){
return allArgs.reduce((pre,cur)=>pre+cur,0)
}
return inner
}
console.log(curry_add(1, 2, 3)(4).valueOf());
InstanceOf原型链
用途:判断a是否是b的一个实例;其实就是判断b的显式原型是否在a的原型链上
//如果a的原型链条上存在b的protoType上则认为true
function instanceOf(a,b){
const PROTOTYPE = b.prototype
let proto = a.__proto__
while(true){
if(proto === null) return false
if(proto === PROTOTYPE) return true
proto = proto.__proto__
}
}
位置信息
offsetWidth:该元素的width + padding + border + 滚动条的距离
offsetLeft:相对于包含块的左侧距离
scrollLeft:元素相对于其可视区域的左侧滚动距离
clientX:光标到可视窗口左侧的距离
pageX:clientX + 横向滚动距离
script的async与defer
默认情况下(无 async 与 defer ):
- 普通的script 无论 下载 还是 执行 都会阻止页面的解析渲染
- 多个 script下载 是并行的,但是执行会有顺序
- 只有执行完script脚本的时候才会继续页面的渲染
async:
- 1、遇到async异步的脚本会在后台进行下载,但是下载并不会阻止解析渲染
- 2、async下载完就会立即执行,并且执行的时候会阻止页面的渲染
- 3、如果设置了多个async,那么就会并行下载
- 4、async的脚本是不按照页面脚本的先后顺序
defer:
- 1、遇到defer的脚本会在后台下载,并且下载不会阻止页面的解析渲染
- 2、defer的脚本执行也不会阻止页面的解析渲染,因为他会等页面解析完毕后才开始执行
- 3、defer的脚本是有顺序的
继承
原型链的继承
缺点:
- 无法传递参数:无法调用父类的构造函数的时候进行传参,因为父类的构造函数已经被调用
- 属性共享:因为所有子类的隐式原型是同一个,所以他们在继承的是同一个对象,所以当这个父类实例中某个属性是引用类型的时候,一旦某个子类的实例进行更改,则会影响其他实例
function Animal(name) {
this.name = name;
this.hobbies = ['sleep', 'eat']; // 引用类型属性
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound`);
}
function Dog1(age) {
this.age = age;
}
Dog1.prototype = new Animal('unknown');
// 测试
const dog1_1 = new Dog1(1);
const dog1_2 = new Dog1(2);
// 测试原型链继承的问题
dog1_1.hobbies.push('play');
console.log(dog1_2.hobbies); // ['sleep', 'eat', 'play'] - 问题:属性被共享了!
构造函数继承
缺点:
- 属性继承:只能继承父类的属性,没办法继承父类方法,子类无法访问父类原型上的方法
- 属性复制:因为是在子类中调用父类的构造函数,因此每个子类实例都会有父类属性的一个副本,这可能导致内存浪费,尤其是当父类属性是复杂对象时
function Animals(name){
this.name = name
}
function Dog(name,breed){
Animal.call(this,name)
this.breed = breed
}
const myDog = new Dog('Buddy','golden Retriever')
console.log(myDog.name) //输出“buddy”
寄生继承
- 寄生继承通过在构造函数调用解决了不能传参的问题;并且不是直接new一个Animal实例,而是通过创建了一个Object.create创建了一个新的对象使其
隐式原型
指向Animal的prototype,这样子避免执行Animals方法,不会创建共享的属性只继承原型上的方法
function Animal(name){
this.name = name
this.hobbies = ['sleep', 'eat']; // 引用类型属性
}
Animal.prototype.speak = function(){
console.log(`${this.name} make a sound`)
}
function Dog(name, age) {
Animal.call(this, name);
this.age = age;
}
Dog.prototype = Object.create(Animal.prototype);
//因为如果不修正的话,当dog的实例去访问constructor的时候,会访问到Dog.prototype, 因为通过Object.create创建的对象也没有就会找到Animals.prototype,所以要重新修正Dog.prototype的constructor的指向
Dog.prototype.constructor = Dog;
const dog1 = new Dog('旺财', 1);
const dog2 = new Dog('小黑', 2);
dog2_1.hobbies.push('play');
console.log(dog1.hobbies); // ['sleep', 'eat', 'play']
console.log(dog2.hobbies); // ['sleep', 'eat'] - 正确:每个实例有自己的属性
Promise
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
const PENDING = 'pending';
/**
* 1、把类的主要结构和构造函数搭建出来,有一个变量储存promise的状态,一个储存promise结果
* 2、接收一个执行器,这里要同步执行promise里面的代码,注意:在executor执行期间,同步的报错是可以捕获得到的,但是异步报错是捕获不到的
* 3、then方法:
* - 3.1 会生成一个新的Promise对象,以便可以链式调用
* - 3.2 判断onFulFilled和onRejected是否为函数,如果onFulFilled不是则发生透传,即上一个promise的值作为下一个promise的值,promise的状态保持成功
* - 3.3 执行onFulFilled的时候如果报错了,那么返回的就应该是一个错误的promise
*
* 思考:
* 1、什么时候执行then方法中的onFulFilled?
* - then会新建一个promise,在里面可以获得旧的promise的onFulFilled,onRejected,也能获取新的promise的resolve、reject
* - 如果旧的promise是同步代码,这里其实可以直接调用onFulFilled,onRejected
* - 但是如果是异步代码,其实then方法是不知道什么时候执行onFulFilled的,但是onChange是知道的,所以可以抽离成一个run方法,让then可以调用,onChange也可以调用
* 2、为什么taskQueue是一个数组而不是直接用对象来处理
* - 因为是同一个promise,它可以被多次then方法调用,而then中的可以有不同的处理方式,所以taskQueue得是一个数组,里面的每一项要存起来等promise完成的时候遍历
* */
class MyPromise {
/**
* 当前promise状态
* @private
*/
#_state_ = PENDING
/**
* 当前promise的结果
* @private
*/
#_result_ = undefined
/**
* 链式调用待处理的queue
* @type {[]}
*/
#taskQueue = []
/**
* @param callback onFulFilled / onRejected
* @param resolve then方法新生成的promise的resolve
* @param reject then方法新生成的promise的resolve
*/
#carryOutByState = (callback, resolve, reject) => {
queueMicrotask(()=>{
if (typeof callback !== 'function') {
const settle = this.#_state_ === FULFILLED ? resolve : reject
return settle(this.#_result_)
}
try {
const data = callback(this.#_result_)
//其实要写一个方法,判断返回值是否为promise,promise只要符合promise A+规范的就行
if (data instanceof MyPromise) {
// 判断返回值,如果返回的是promise,那么此个then方法的状态将取决于data
data.then(resolve, reject)
} else {
resolve(data)
}
}catch (error) {
reject(error)
}
})
}
#run = () => {
//异步的情况
if (this.#_state_ === PENDING) return
while (this.#taskQueue?.length) {
const { onFulFilled, onRejected, resolve, reject } = this.#taskQueue.shift()
const callback = this.#_state_ === FULFILLED ? onFulFilled : onRejected
this.#carryOutByState(callback, resolve, reject)
}
}
#onChange = (state, result) => {
if (this.#_state_ !== PENDING) return
this.#_state_ = state
this.#_result_ = result;
//处理异步任务
this.#run()
}
/**
* 核心then方法
* @param onFulFilled
* @param onRejected
* @returns {MyPromise}
*/
then = (onFulFilled, onRejected) => {
return new MyPromise((resolve, reject) => {
this.#taskQueue.push({ onFulFilled, onRejected, resolve, reject })
//给同步代码用的,如果是异步的话promise状态还是pending,那么则不会调用,会等到异步代码resolve才会run
this.#run()
})
}
constructor(executor) {
const resolve = (result) => {
this.#onChange(FULFILLED, result);
}
const reject = (error) => {
this.#onChange(REJECTED, error);
}
try {
executor(resolve, reject)
} catch (error) {
this.#onChange(REJECTED, error);
}
}
}
const promise = new MyPromise((resolve, reject) => {
reject('this is a error')
}).then(
(result) => {
console.log(`【onFulFilled - result1】: - ${result}`);
return new MyPromise((resolve) => {
resolve(result + 1)
})
},
(reason) => {
console.log(`【onRejected - result1】: - ${reason}`);
return new MyPromise((resolve) => {
resolve('这一次成功了')
})
}
).then(
(result) => {
console.log(`【onFulFilled - result2】: - ${result}`);
},
(reason) => {
console.log(`【onRejected - result2】: - ${reason}`);
}
);
//Promise的catch方法
Promise.prototype.catch = function (onErrorCatch) {
return this.then(undefined, onErrorCatch);
}
//Promise的resolve方法
Promise.resolve = function (value) {
if (value instanceof Promise) return value
//如果是符合PromiseA+规范,并且有then方法的对象,就调用他的then方法
function isThenAble(target) {
return (typeof target === 'object' || typeof target === 'function') && target !== null && typeof target.then === 'function'
}
if (isThenAble(value)) {
return new Promise((resolve, reject) => {
value.then(resolve, reject)
})
}
return new Promise((resolve) => {
resolve(value)
})
}
//Promise的reject方法
Promise.reject = function (reason) {
return new Promise((resolve, reject) => {
reject(reason);
})
}
Promise.all 和 Promise.allSettled的区别
首先他们都需要传入一个iterable(iterable != null && typeof iterable[Symbol.iterator] === 'function'),并且都会返回一个promise
iterable 对象中的元素如果不是Promise数组,是其他iterable,那么 Promise.all / Promise.allSettle 视其为已经完成的值
- 如果是空数组的话,那么会视其为一个已经resolve的promise对象。
如果传入的是Promise数组,Promise.all会等所有Promise实例全部成功才会将promise的状态改成 fulfilled,如果执行期间,有一个失败则直接改为reject;而Promise.allSettle不会,它只有等到参数数组的所有 Promise 对象都发生状态变更(不管是 fulfilled 还是 rejected),才会更改返回的Promise对象的状态,一旦发生状态变更,状态总是
fulfilled
,不会变成rejected
;如果有then的话,那么会把
Promise.all
数组中 resolve 出来的值放进去结果数组中;而Promise.allSettle
则是一个个对象:{ status: 'fulfilled', value: '100' } 或者 { status: 'rejected', reason: -1 };Promise.allSettle能看到promise的状态,而Promise.all不能
Promise.all方法
function myPromiseAll(iterable) {
return new Promise((resolve, reject) => {
const isIterable = iterable != null && typeof iterable[Symbol.iterator] === 'function';
if (!isIterable) {
return reject(new TypeError("Argument must be an iterable"));
}
let results = [];
let completed = 0;
let total = [...iterable].length;
// 如果是空数组,立即 resolve 空数组
if (total === 0) {
return resolve([]);
}
[...iterable].forEach((item, index) => {
Promise.resolve(item)
.then(value => {
results[index] = value; // 保持顺序
completed++;
// 如果所有 Promise 都完成了,resolve 结果
if (completed === total) {
resolve(results);
}
})
.catch(error => {
reject(error); // 只要有一个失败,立即 reject
});
});
});
}
Promise.allSettle
function myPromiseAllSettled(iterable) {
return new Promise((resolve, reject) => {
const isIterable = iterable != null && typeof iterable[Symbol.iterator] === 'function';
// 判断是否为可迭代对象
if (!isIterable) {
return reject(new TypeError("Argument must be an iterable"));
}
const results = [];
let completed = 0;
const items = Array.from(iterable);
const total = items.length;
// 处理空数组,立即 resolve
if (total === 0) {
return resolve([]);
}
items.forEach((item, index) => {
Promise.resolve(item)
.then(value => {
results[index] = { status: "fulfilled", value };
})
.catch(reason => {
results[index] = { status: "rejected", reason };
})
.finally(() => {
completed++;
if (completed === total) {
resolve(results);
}
});
});
});
}
Promise.race
const p = Promise.race([p1, p2, p3]);
//手写promise.race
const pRace = (promises) => {
const isIterable = iterable != null && typeof iterable[Symbol.iterator] === 'function';
// 判断是否为可迭代对象
if (!isIterable) {
return reject(new TypeError("Argument must be an iterable"));
}
// 将可迭代对象转化为数组
const items = Array.from(iterable);
// 如果是空数组,直接返回一个 Pending 状态的 Promise
if (items.length === 0) {
return;
}
// 遍历所有的 Promise
items.forEach(item => {
Promise.resolve(item)
.then(resolve) // 如果有 Promise 成功,直接 resolve
.catch(reject); // 如果有 Promise 失败,直接 reject
});
});
};
上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态(不管成功还是失败),p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。
如果传递的是一个空数组,那么返回的promise的状态则会一直是pending状态
Promise.any
Promise.any()主要是针对只要参数实例有一个变成fulfilled
状态,包装实例就会变成fulfilled
状态;如果直到所有参数实例都变成rejected
状态,包装实例就会变成rejected
状态。
js监听属性的变化
Object.defineProperty
Object.defineProperty可以同来定义或者修改对象属性的一个方法;
主要用法:Object.defineProperty(obj, props, config)
obj
:要操作的对象
prop
:要定义的属性名称(可以是现有属性或者新属性)。
descriptor
:一个描述符对象,定义了该属性的行为。它包含以下可选的配置项:
value
类型:任何类型的值。
作用:设置属性的值。这个值是只读的,如果你没有指定
writable: true
,它就不能被修改。
writable
类型:布尔值。
作用:控制属性值是否可写。如果设置为
false
,则该属性的值不能被修改(即使修改也不会生效)。
enumerable
类型:布尔值。
作用:控制属性是否可被
for...in
循环或Object.keys()
等方法枚举。如果设置为false
,该属性就不会出现在枚举结果中。
- configurable
- 类型:布尔值。
- 作用:控制这个属性之后还能不能修改描述附(例如我一开始定义这个是不能重写的,后面重新对这个值定义为这个值时可以重写,此时JS就会报错,Cannot redefine property)
- get
- 类型:函数。
- 作用:定义一个 getter 方法,用于读取该属性的值。getter 方法在访问该属性时会被调用
- set
- 类型:函数。
- 作用:定义一个 setter 方法,用于设置该属性的值。setter 方法在修改该属性时会被调用。
proxy
- 定义对象的代理器
const target = { name: 'Alice' };
const handler = {
get(target, prop) {
// 统一拦截所有属性的访问
console.log(`Getting ${prop}`);
return target[prop];
},
set(target, prop, value) {
// 统一拦截所有属性的修改
console.log(`Setting ${prop} to ${value}`);
target[prop] = value;
return true;
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // 会触发 `get` 拦截器
proxy.name = 'Bob'; // 会触发 `set` 拦截器
defineProperty和Proxy的区别:
监听数据的角度
defineproperty
只能监听某个属性而不能监听整个对象。proxy
不用设置具体属性,直接监听整个对象。defineproperty
监听需要知道是哪个对象的哪个属性,而proxy
只需要知道哪个对象就可以了。也就是会省去for in
循环提高了效率。
监听对原对象的影响
因为 defineProperty 是通过在原对象身上新增或修改属性增加描述符的方式实现的监听效果,一定会修改原数据。
而proxy只是原对象的代理,proxy会返回一个代理对象不会在原对象上进行改动,对原数据无污染。
实现对数组的监听
- 因为数组 length 的特殊性 (length 的描述符configurable 和 enumerable 为 false,并且妄图修改 configurable 为 True 的话 js 会直接报错:VM305:1 Uncaught TypeError: Cannot redefine property: length)
- defineproperty无法监听数组长度变化, Vue只能通过重写数组方法的方式变现达成监听的效果,光重写数组方法还是不能解决修改数组下标时监听的问题,只能再使用自定义的$set的方式
- 而proxy因为自身特性,是创建新的代理对象而不是在原数据身上监听属性,对代理对象进行操作时,所有的操作都会被捕捉,包括数组的方法和length操作,再不需要重写数组方法和自定义set函数了。(代码示例在下方)
javaScript的autoboxing
当试图访问一个原始数据类型的属性或者方法的时候,就会触发javascrpt的装箱机制。包装对象的使用是临时的,原始值在方法调用后不会保留包装对象的状态。
- JavaScript 在处理原始类型时,会自动地将它们
装箱
成包装类型对象,以便调用对象的方法。这种过程称为自动装箱。例如:
let str = "hello";
console.log(str.toUpperCase()); // 输出 "HELLO"
- 手动创建包装对象
虽然可以手动创建包装对象,但在实际编程中不推荐这样做,因为它可能导致混淆和不必要的性能开销。例如:
let str = new String("hello");
console.log(typeof str); // 输出 "object"
在这个例子中,str
是一个 String
对象,而不是一个原始类型的字符串,这会导致 typeof
操作符返回 object
而不是 string
。同样地,使用 new Number()
和 new Boolean()
创建的对象也有类似的行为。
强引用与弱引用
强引用(Strong Reference):强引用是 JavaScript 中最常见的引用类型。当一个对象被强引用时,垃圾回收器不会回收这个对象。
弱引用(Weak Reference):弱引用允许你引用一个对象,同时允许这个对象被垃圾回收。
- 强引用会阻止对象被垃圾回收
- 弱引用不会阻止对象被垃圾回收
// 只有通过 WeakMap、WeakSet 创建的引用才是弱引用
let user = { name: 'John' }; // 强引用
let weakMap = new WeakMap();
weakMap.set(user, 'userdata'); // 弱引用
user = null; // 断开强引用
// 此时对象可以被回收,因为只剩下 weakMap 的弱引用
实现一个私有属性
// 1、使用 WeakMap 存储私有数据
const privateData = new WeakMap();
class User {
constructor(name,age) {
privateData.set(this, {
name,
age
});
}
getName() {
return privateData.get(this).name;
}
}
Typescript原理
ts编译器中有一下几个关键部分
- scanner 扫描器
- parser 解析器
- binder 绑定器
- checker 检查器
- emitter 发射器
工作原理如下:
1、TypeScript源码经过扫描器扫描之后变成一系列Token
2、解析器解析Token,得到一棵AST抽象语法树
3、绑定器遍历AST语法树,生成一系列Symbol将这些Symbol连接到对应的节点上
4、检查器再次遍历AST语法树,检查类型,把错误收集起来
5、发射器根据 AST语法树 生成JavaScript代码
TypeScript的类型系统
any(任意类型)
any类型表示没有任何限制,该类型的变量可以赋予任意类型的值,一旦类型变成了any,TypeScript实际上会关闭这个变量的类型检查。 集合论角度来看,any是所有类型的全集,Typescript称之为“顶层类型”,意为覆盖了所有下层
- 如果TS无法推断出类型,则会认为类型均为any,导致后面的类型推断失效
- 如果利用let、var声明变量,但是没有赋值的话,TypeScript会将他们的类型推断成any,这时候打开noImplicitAny也不会报错;但是const不会有这个问题,因为const是声明时必须同时赋值,否则将报错,所以const不存在推断为any的问题
- any是可以 把任何类型的值赋值给其他值的,TypeScript也不会检查出错误,也就会产生污染问题
unknow(严格版any)
为了解决any的“污染”问题,TypeScript引入了unknow类型,他与any含义相同,均表示为“类型不确定,可以是任意类型”,但是它却又一些限制
unknow和any相似之处在于:所有类型的值都可以分配给unknow;但是unknow类型的变量不能直接赋值给其他类型的变量(除了unknow和any)
直接使用unknow类型的变量和方法都会报错,因为unknow类型的变量能够进行的运算符是有限的,它能进行的运算如下:
- 运算符 (、=、!=、|| 、&& 、?)
- 取反运算(!)
- typeof运算
- instanceof 运算
怎么样才能使用unknow类型变量?
答案是只能通过类型缩小,例如通过 typeof data === 'string' 这样的运算把他的范围进行缩小
never(空类型,空集,不包含任何值)
为了保证集合论中的关系,typescript引入了“空类型”,即类型为空,不包含任何值;
- never有一个很重要的特点,它可以赋值给任意其他类型;
因为在集合论中,空集是任何集合的子集,所以never类型是任何其他类型所共有的,TypeScript把这种情况称为 底层类型
TypeScript有两个 顶层类型
( any 和 unknow ),但是 底层类型
只有never 一个
包装对象类型 与 字面量类型
因为JavaScript存在auto boxing机制,这就导致了每一个原始类型的值都包含 字面量
与 包装对象
两种情况
'hello' // 字面量
new String('hello') // 包装对象
为了区分这两种情况,TypeScript 对五种原始类型分别提供了大写和小写两种类型。
- 大写类型 同时包含 包装对象 和 字面量 两种情况
- 小写类型 只包含 字面量,不包含包装对象。
Boolean 和 boolean
String 和 string
Number 和 number
BigInt 和 bigint
Symbol 和 symbol
const s1:String = 'hello'; // 正确
const s2:String = new String('hello'); // 正确
const s3:string = 'hello'; // 正确
const s4:string = new String('hello'); // 报错
Object 与 object
- 大写的Object为
广义对象
:代表所有可以转成对象的值,几乎囊括了所有值除了undefined与null - 小写的object为
狭义对象
,只能用字面量表示的对象,只包含对象、数组、函数 不包含原始类型的值
undefined 与 null
他们即是类型也是值,但是可以把他们赋值给任意类型都不会报错,这是因为需要和JavaScript保持一致
值类型
TypeScript单个值也是一种类型,如果声明变量时没有注明类型,那么就会推断为值类型
const x = 'world' // x 的类型为 'world'
如果const命令声明变量赋值为对象,并不会推断为值类型
const x = { foo : 1 } // x的类型为{ foo : number }
值类型存在父子类型的问题:子类型可以赋值给父类型,但是父类型不能赋值给子类型 如
let x:5 = 4 + 1 //会报错,那是因为等号右侧为number类型,而5是number的子类型。
let y:number = 5 //此时把x赋值给y是正确的,但是把y赋值给x是错误的
如果一定要把子类型赋值给父类型,则需要使用断言
联合类型
联合类型可以是多个类型组成的一个新类型,使用符号|表示;当如果一个值为联合类型的时候,直接使用上面的方法往往会报错
const id:number | string
直接使用id.toUppercase会报错,因为number不存在这个方法,此时需要 类型缩小
如 typeof id='string' | typeof id = 'number'做对应的处理
交叉类型
交叉类型指的是多个类型组成的一个新的类型,用&表示;
- let x : number & string 这显然不可能,所以Typescript会认为x实际的类型为never
- 交叉类型往往用于对象的合成,如{ foo:string } & { bar : number} 则变成了 { foo:string bar:number }
typeof
JavaScript中typeof是一个一元运算符,返回一个字符串,代表操作数的类型。而在Typescript中,typeof返回的不是字符串,而是该值的TypeScript类型;
也就是说同一段代码可能存在两种typeof,一种是类型运算
,一种是值的运算
,他们的区别在于JavaScript的typeof在编译后会保留,而Typescript的typeof会删除
//typeof命令的参数不能是类型
type Age = number
type MyAge = typeof Age//会报错,因为Age是一个类型别名
数组
TypeScript数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不定的,可以是0可以是无限多
数组的类型推断
如果数组变量没有声明类型,TypeScript就会推导成员的类型,此时推断行为会因为值的不同而有所不同;如果变量的初始值为空数组,那么TypeScript会推断数组为any[],类型推断的自动更新只发生初始值为空数组的情况。如果初始值不是空数组,类型推断就不会更新。
const arr = [];
arr // 推断为 any[]
//-----------------------------------------------------------------------
arr.push(123);
arr // 推断类型为 number[]
arr.push('abc');
arr // 推断类型为 (string|number)[]
//-----------------------------------------------------------------------
// 推断类型为 number[]
const arr = [123];
arr.push('abc'); // 报错
只读数组,const断言
JavaScript规定,const声明的数组是可以改变成员的,但是往往很多时候有声明只读数组的需求,不允许变动数组成员
const arr = [0,1]
arr[0] = 2//这样子是合法的
//如果要让数组为只读
const arr:readonly number[] = [0,1]
TypeScript 将readonly number[]
与number[]
视为两种不一样的类型,后者是前者的子类型。
这是因为只读数组没有pop()
、push()
之类会改变原数组的方法,所以number[]
的方法数量要多于readonly number[]
,这意味着number[]
其实是readonly number[]
的子类型。
let a1:number[] = [0, 1];
let a2:readonly number[] = a1; // 正确
a1 = a2; // 报错
//因为子类型能赋值给父类型,但是父类型不能赋值给子类型
此外,readonly不能与数组的泛型一起使用
// 报错
const arr:readonly Array<number> = [0, 1];
const a1:ReadonlyArray<number> = [0, 1];
const a2:Readonly<number[]> = [0, 1];
元组(tuple)
元组是TS特有的数据类型,他与数组最大的差异就是,数组的成员类型在中括号外面(number[]),元组在中括号里面([number])
使用元组必须给出类型声明,不然TypeScript就会把他推算成数组
// a 的类型被推断为 (number | boolean)[]
let a = [1, true];
//元组成员的类型可以添加问号后缀(?),表示该成员是可选的。
let a:[number, number?] = [1];
注意:问号只能用于元组的尾部成员,也就是说,所有可选成员必须在必选成员之后。
元组的成员可以添加成员名,这个成员名是说明性的,可以任意取名,没有实际作用
type Color = [
red: number,
green: number,
blue: number
];
const c:Color = [255, 255, 255];
可以通过中括号读取成员类型
type Tuple = [string, number];
type Age = Tuple[1]; // number
type Tuple = [string, number, Date];
type TupleEl = Tuple[number]; // string|number|Date
拓展运算符
- 拓展运算符可以表示不限成员的元组,但是它只能跟一个数组或者元组;
- 此外它可以运用在元组中的任何位置
type NamedNums = [
string,
...number[]
];
const a:NamedNums = ['A', 1, 2];
const b:NamedNums = ['B', 1, 2, 3];
//-----------------------------------------------------------------
type t1 = [string, number, ...boolean[]];
type t2 = [string, ...boolean[], number];
type t3 = [...boolean[], string, number];
只读元组
// 写法一
type t = readonly [number, string]
// 写法二
type t = Readonly<[number, string]>
成员数量的推断
如果没有使用拓展运算符,那么Typescript会推断出元组的成员数量
如果使用了拓展运算符则无法推断成员数量
function f(
point:[number, number?, number?]
) {
if (point.length === 4) { // 报错
// ...
}
}
const myTuple:[...string[]]
= ['a', 'b', 'c'];
if (myTuple.length === 4) { // 正确
// ...
}
函数
返回值
void:表示函数没有返回值,但是返回undefined和null是不会报错的,除非开strictNullChecks
never:never
类型表示肯定不会出现的值。它用在函数的返回值,就表示某个函数肯定不会返回值,即函数不会正常执行结束。
never不同于void,前者代表函数没有执行结束,不可能拥有返回值,后者代表函数正常执行结束,但是不返回值,或者说返回undefined
函数重载
有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)。
例如有一个add方法,可以是数字相加,也可以是数组的拼接
function add(
x:number,
y:number
):number;
function add(
x:any[],
y:any[]
):any[];
function add(
x:number|any[],
y:number|any[]
):number|any[] {
if (typeof x === 'number' && typeof y === 'number') {
return x + y;
} else if (Array.isArray(x) && Array.isArray(y)) {
return [...x, ...y];
}
throw new Error('wrong parameters');
}
构造函数
构造函数的类型写法,就是在参数列表前面加上new
命令。
class Animal {
numLegs:number = 4;
}
type AnimalConstructor = new () => Animal;//构造函数
function create(c:AnimalConstructor):Animal {
return new c();
}
const a = create(Animal);
接口(interface)
如果一个 interface 同时定义了字符串索引和数值索引,那么数值索引必须服从于字符串索引。因为在 JavaScript 中,数值属性名最终是自动转换成字符串属性名。
interface A {
[prop: string]: number;
[prop: number]: string; // 报错
}
interface B {
[prop: string]: number;
[prop: number]: number; // 正确
}
接口重载
在 TypeScript 中,interface 可以用来定义函数重载,这允许同一个函数根据不同的参数类型和数量有不同的实现。让我用例子来说明:
interface A {
f(): number;
f(x: boolean): boolean;
f(x: string, y: string): string;
}
接口重载必须兼容实现签名
重载签名的顺序很重要,应该从最具体到最通用
TypeScript 会按照重载的顺序从上到下匹配,因此更具体的重载应该放在前面
接口继承
interface可以继承 interface 也可以继承 type(只能继承对象类型)
子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错。
interface Foo { id: string; } interface Bar extends Foo { id: number; // 报错 }
多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错
interface Foo { id: string; } interface Bar { id: number; } // 报错 interface Baz extends Foo, Bar { type: string; }
接口合并
多个同名的接口会合成同一个接口,如果同名接口合并时,如果同名方法有不同的类型声明,那么会发生函数重载。而且,后面的定义比前面的定义具有更高的优先级。
interface Cloner {
clone(animal: Animal): Animal;
}
interface Cloner {
clone(animal: Sheep): Sheep;
}
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
}
// 等同于
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
clone(animal: Sheep): Sheep;
clone(animal: Animal): Animal;
}
interface 与 type 的区别
type
能够表示非对象类型,而interface
只能表示对象类型(包括数组、函数等)interface
可以继承其他类型,type
不支持继承。继承的主要作用是添加属性,
type
定义的对象类型如果想要添加属性,只能使用&
运算符,重新定义一个类型。同名
interface
会自动合并,同名type
则会报错。type无法使用 this关键字(表示对象实例)
interface
无法表达某些复杂类型(比如交叉类型和联合类型),但是type
可以。type A = { /* ... */ }; type B = { /* ... */ }; type AorB = A | B; type AorBwithName = AorB & { name: string };
class
readonly
属性的初始值,可以写在顶层属性,也可以写在构造方法里面。如果两个地方都设置了只读属性的值,以构造方法为准。在其他方法修改只读属性都会报错
class A {
readonly id:string;
constructor() {
this.id = 'bar'; // 正确
}
}
class B {
readonly id:string = 'foo';
constructor() {
this.id = 'bar'; // 正确,构造方法内部设置只读属性的初值,这是可以的。
}
}
implement关键字(基类的实现)
interface
或type
都可以定义一个对象类型。类可以使用implements
关键字,表示该类的实例对象满足这个外部类型。
class Car implements MotorVehicle, Flyable, Swimmable {
// ...
}
extend关键字(基类的继承)
子类继承父类时,可以覆盖父类的同名方法。
abstract(抽象类)
Typescript允许在类的前面加上abstract关键字,表示该类不能被实例化,只能被其他类当作模版进行继承
abstract class A {
id = 1;
}
const a = new A(); // 报错
- 抽象类可以继承其他抽象类
abstract class A {
foo:number;
}
abstract class B extends A {
bar:string;
}
- 抽象类中可以有已经实现好的属性与方法,也可以有为实现的
抽象成员
,即属性名中也存在abstract,表示该方法子类必须实现
abstract class A {
abstract foo:string;
bar:string = '';
}
class B extends A {
foo = 'b';
}
overide关键字(重载)
但是有些时候,我们继承他人的类,可能会在不知不觉中,就覆盖了他人的方法。为了防止这种情况,Typescript需要在子类中添加override关键字,表明作者意图。但是还是不能解决无意中覆盖父类同名的方法,这时候需要在 typescript.config.ts
中把 noImplicitOverride
打开,一旦这个参数打开,子类覆盖父类就会报错 除非使用override关键字
可访问性修饰符(public、private、protected)
public(表示公开成员,外部可以自由访问)
在下面的例子中,greet为public成员,表示可以在类的外部进行调用,
public
修饰符石默认的修饰符,可以忽略不写class Greeter { public greet() { console.log("hi!"); } } const g = new Greeter(); g.greet();
private(表示私有成员,只能在当前类的内部使用,类的
实例
和子类
都不能使用)class A { private x = 0; } class B extends A { x = 1; // 报错 }
但是其实在编译成 Javascript 之后private关键字会被剥离,这时外部成员访问该成员就不会报错,可以通过a['x']也是能访问得到该实例对象的属性。
正是因为
private
关键字存在这些问题,ES2022引入了自己私有成员的写法#propName
,所以便不推荐使用private的写法protected(表示保护成员,只能在类本身及其子类中可以使用,不能在实例中进行使用)
静态成员(static)
类的内部可以使用 static
关键字,定义静态成员
静态成员只能通过类的本身使用,不能通过实例对象使用,只能通过类本身调用。
class MyClass { static x = 0; static printX() { console.log(MyClass.x) } }
访问修饰符(public、private、protected、#)可叠加在static前使用
class MyClass { private static x = 0; static #x = 0; } MyClass.x // 报错
public
与protected
的静态成员可被继承class A { public static x = 1; protected static y = 1; } class B extends A { static getY() { return B.y; } } B.x // 1 B.getY() // 1
实例属性的简写形式
开发中往往很多实例属性的值是由构造函数传入的。除了public
修饰符,构造方法的参数名只要有private
、protected
、readonly
修饰符,都会自动声明对应修饰符的实例属性。
class Point {
x:number;
y:number;
constructor(x:number, y:number) {
this.x = x;
this.y = y;
}
}
//简写如下:
class Point {
constructor(
public x:number,
public y:number
) {}
}
const p = new Point(10, 10);
p.x // 10
p.y // 10
Class本身的类型
Typescript的类本身就是一种类型,他代表该类型的实例的类型,而不是class自身的类型,想要获得类自身的类型,一个简便的方法是使用 typeof
运算符
Enum(枚举)
enum的只读性,如果在声明枚举之后重新为枚举赋值则会报错
enum Color { Red, Green, Blue } Color.Red = 4; // 报错 //加上const const enum Color { Red, Green, Blue }
const关键字
为了更加明显的说明枚举的只读性,一般会加上const;
- 加上const还可以优化性能,因为在编译成JavaScript代码之后,Enum成员会被替换成对应的值提高性能,不会生成对应的对象,二十八所有Enum成员出现的场合替换成对应的常量(如果希望加上const还能访问Enum的对象结构,需要在编译时打开 'preserveConstEnum')
const enum Color {
Red,
Green,
Blue
}
const x = Color.Red;
const y = Color.Green;
const z = Color.Blue;
// 编译后
const x = 0 /* Color.Red */;
const y = 1 /* Color.Green */;
const z = 2 /* Color.Blue */;
keyof运算符
keyof运算符可以取出Enum结构中的所有成员名字,作为联合类型返回
enum MyEnum {
A = 'a',
B = 'b'
}
// 'A'|'B'
type Foo = keyof typeof MyEnum;
in运算符
in可以返回 Enum 所有的成员值
enum MyEnum {
A = 'a',
B = 'b'
}
// { a: any, b: any }
type Foo = { [key in MyEnum]: any };
//T[number]返回元组T中所有类型的联合类型,in会把他们依次遍历
type TupleToObject<T extends readonly (number | string | symbol)[]> = {[p in T[number]]: p}
反向映射(仅发生在数值)
enum Weekdays{
Monday = 1,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
//编译成Javascript之后,以Monday为例
var Weekdays;
(function (Weekdays) {
Weekdays[Weekdays["Monday"] = 1] = "Monday";
})(Weekdays || (Weekdays = {}));
//字符串不支持反向映射,因为在编译后,Enum只有一组赋值
enum MyEnum {
A = 'a',
B = 'b'
}
// 编译后
var MyEnum;
(function (MyEnum) {
MyEnum["A"] = "a";
MyEnum["B"] = "b";
})(MyEnum || (MyEnum = {}));
泛型
泛型主要应用于四个场合:函数、接口、类、别名
函数的泛型写法
// 写法一
function getId<T>(arg:T):T{
return arg
}
const getId : (arg:T)=>T = (arg) => arg
接口
interface Box<Type> {
contents: Type;
}
let box:Box<string>;
类
注意,泛型类描述的是类的实例,不包括静态属性和静态方法,因为这两者定义在类的本身。因此,它们不能引用类型参数。
//正常泛型
class Pair<K, V> {
key: K;
value: V;
}
//继承泛型类的例子
class A<T> {
value: T;
}
class B extends A<any> {
}
//用在类表达式
const Container = class<T> {
constructor(private readonly data:T) {}
};
const a = new Container<boolean>(true);
const b = new Container<number>(0);
//泛型不能用于描述静态属性与静态方法
class C<T> {
static data: T; // 报错
constructor(public value:T) {}
}
类型的泛型
type Nullable<T> = T | undefined | null;
type Container<T> = { value: T };
const a: Container<number> = { value: 0 };
type Tree<T> = {
value: T;
left: Tree<T> | null;
right: Tree<T> | null;
};
类型参数的默认值
类型参数可以设置默认值,使用时,如果没有给出类型参数的值那么就会使用默认值;
- 一旦类型参数有默认值,就表示它是可选参数。如果有多个类型参数,可选参数必须在必选参数之后。
function getFirst<T = string>(
arr:T[]
):T {
return arr[0];
}
//类型参数有默认值应该在必须参数之后
<T = boolean, U> // 错误
<T, U = boolean> // 正确
类型参数的约束条件
//这个例子就说明传进来的参数是必须有一个length 属性的对象
function comp<T extends { length: number }>(
a: T,
b: T
) {
if (a.length >= b.length) {
return a;
}
return b;
}
类型断言
Typescript中一旦发现存在类型断言,那么就不会对类型就行推断,直接采用断言给出的类型,但是类型断言并不意味着,可以把某个值断言为任意类型。
使用类型断言的前提是:值的实际类型与断言的类型必须满足值是类型的子类型,或者类型是值的子类型,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。
//expr是实际的值,T是类型断言,它们必须满足下面的条件:expr是T的子类型,或者T是expr的子类型。
expr as T
如果硬要断言成一个完全无关的类型,则可以使用两次断言,例如:,因为 any
和 unknow
是其他所有类型的父类型,所以可以作为中介
expr as unknown as T
as const断言
类型推断的时候,可以将这个值退u但为常量,也就是把let变量断言为const变量,从而把内置的基本类型变更为值类型
let s1 = 'JavaScript'//类型推断成'string'
const s2 = 'JavaScript'//类型推断成字符串'JavaScript'
//-----------------------对象-----------------
const v1 = {
x: 1,
y: 2,
}; // 类型是 { x: number; y: number; }
const v2 = {
x: 1 as const,
y: 2,
}; // 类型是 { x: 1; y: number; }
const v3 = {
x: 1,
y: 2,
} as const; // 类型是 { readonly x: 1; readonly y: 2; }
//-----------------------数组----------------
function add(x:number, y:number) {
return x + y;
}
//因为add明确只要两个参数,而如果传入拓展运算符不能保证数量,除非使用as const把数组变成元组
const nums = [1, 2];
const total = add(...nums); // 报错
const nums = [1, 2] as const;
const total = add(...nums); // 正确
非空断言
对于那些可能为空的变量(即可能等于undefined
或null
),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!
。
断言函数(assert value is type)
使用了断言函数的以后,TypeScript 就会自动识别,只要执行了该函数,对应的变量都为断言的类型。
function isString(value:unknown):asserts value is string {
if (typeof value !== 'string')
throw new Error('Not a string');
}
function toUpper(x: string|number) {
isString(x);
return x.toUpperCase();
}
上面示例中,函数isString()
的返回值类型写成asserts value is string
,其中asserts
和is
都是关键词,value
是函数的参数名,string
是函数参数的预期类型。它的意思是,该函数用来断言参数value
的类型是string
,如果达不到要求,程序就会在这里中断。
另外,断言函数的asserts
语句等同于void
类型,所以如果返回除了undefined
和null
以外的值,都会报错。
保护函数(value is type)
保护函数和断言函数是两种不同的函数,断言函数不返回值(或者返回的只能是undefined或者null),而保护函数返回的总是一个布尔值
function isString(
value:unknown
):value is string {
return typeof value === 'string';
}
//断言参数为true的简写(即不是undefined、null、false)
function assert(x:unknown):asserts x {
// ...
}
上面示例就是一个类型保护函数isString()
,作用是检查参数value
是否为字符串。如果是的,返回true
,否则返回false
。该函数的返回值类型是value is string
,其中的is
是一个类型运算符,如果左侧的值符合右侧的类型,则返回true
,否则返回false
declare
declare关键字用来描述已经存在的变量和数据结构,他只是通知编译器说某个类型是存在的,不用给出具体实现
declare var document;
document.title = 'Hello';
上面示例中,declare 告诉编译器,变量document
的类型是外部定义的(具体定义在 TypeScript 内置文件lib.d.ts
)。
如果 TypeScript 没有找到document
的外部定义,这里就会假定它的类型是any
。
类型运算符
keyof
接受一个对象类型为参数,返回该对象的所有键名组成的联合类型,由于JavaScript对象的键名只有三种类型,所以对于任意对象的联合类型就是 string | number | symbol
- any:返回 string | number | symbol
- 元组:返回number 与 下标
- 联合类型:返回共有键值
- 交叉类型:返回所有键值
interface T {
0: boolean;
a: string;
b(): void;
}
type KeyT = keyof T; // 0 | 'a' | 'b'
//any类型
type KeyT = keyof any;// string | number | symbol
//元组类型
type Result = keyof ['a', 'b', 'c'];// 返回 number | "0" | "1" | "2"
//联合类型,keyof会返回共有键名
type A = { a: string; z: boolean };
type B = { b: string; z: boolean };
type KeyT = keyof (A | B);// 返回 'z'
//交叉类型,keyof会返回所有键名
type A = { a: string; x: boolean };
type B = { b: string; y: number };
type KeyT = keyof (A & B);// 返回 'a' | 'x' | 'b' | 'y'
属性映射:用于将一个类型的所有属性逐一映射成其他值
type NewProps<Obj> = {
[Prop in keyof Obj]: boolean;
};
IN
JavaScript中的 in
用于确定对象是否包含某个属性名
Typescript中 in
运算符则用于取出来遍历联合类型
type U = 'a'|'b'|'c';
type Foo = {
[Prop in U]: number;
};
// 等同于
type Foo = {
a: number,
b: number,
c: number
};
[] 方括号
用于返回对象键值的类型 / 联合类型
type Person = {
age: number;
name: string;
alive: boolean;
};
// number|string
type T = Person['age'|'name'];
// number|string|boolean
type A = Person[keyof Person];
extends 与 三元运算符
条件运算符 extends ... ? :
可以判断当前类型是否满足某种条件返回不同类型
// true
type T = 1 extends number ? true : false;
type Cond<T> = T extends U ? X : Y;
type MyType = Cond<A|B>;
// 等同于 Cond<A> | Cond<B>
// 等同于 (A extends U ? X : Y) | (B extends U ? X : Y)
infer关键字
infer
关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。
它通常跟条件运算符一起使用,用在extends
关键字后面的父类型之中。
//如果Type是一个数组,那么该数组成员的类型推断为Item
type Flatten<Type> =
Type extends Array<infer Item> ? Item : Type;
type Str = Flatten<string[]>;// string
type Num = Flatten<number>;// number
//----------------------------------------------------------=--
type MyType<T> =
T extends {
a: infer M,
b: infer N
} ? [M, N] : never;
type T = MyType<{ a: string; b: number }>;// [string, number]
is关键字
is运算符用于描述返回值是true 还是 false
function isFish(
pet: Fish|Bird
):pet is Fish {
return (pet as Fish).swim !== undefined;
}
//-------------------------------------------------------------
type A = { a: string };
type B = { b: string };
function isTypeA(x: A|B): x is A {
if ('a' in x) return true;
return false;
}
工具类型
ConstructorParameters<Type>
提取构造方法Type
的参数类型,组成一个元组类型返回。type T1 = ConstructorParameters< new (x: string, y: number) => object >; // [x: string, y: number] type T2 = ConstructorParameters< new (x?: string) => object >; // [x?: string | undefined]
Exclude<UnionType, ExcludedMembers>
用来从联合类型UnionType
里面,删除某些类型ExcludedMembers
,组成一个新的类型返回Extract<UnionType, Union>
用来从联合类型UnionType
之中,提取指定类型Union
,组成一个新类型返回。它与Exclude<T, U>
正好相反。Omit<Type, Keys>
用来从对象类型Type
中,删除指定的属性Keys
,组成一个新的对象类型返回。Pick<Type, Keys>
返回一个新的对象类型,第一个参数Type
是一个对象类型,第二个参数Keys
是Type
里面被选定的键名。Parameters<Type>
从函数类型Type
里面提取参数类型,组成一个元组返回。type T4 = Parameters< (x:{ a: number; b: string }) => void >; // [x: { a: number, b: string }]
Partial<Type>
返回一个新类型,将参数类型Type
的所有属性变为可选属性。Record<Keys, Type>
返回一个对象类型,参数Keys
用作键名,参数Type
用作键值类型。type T = Record<'a'|'b', number>; type T = Record<'a', number|string>;
ReturnType<Type>
提取函数类型Type
的返回值类型,作为一个新类型返回。type T1 = ReturnType<() => string>; // string type T2 = ReturnType<() => { a: string; b: number }>; // { a: string; b: number }
React
React如何实现动态导入组件
import React, { Suspense, useState } from 'react';
const Demo = React.lazy(() => import('./demo'));
export default function Index() {
const [state, setState] = useState(false);
return (
<div >
<Suspense fallback={<div>loading...</div>}>
{state && <Demo content={content} />}
</Suspense>
</div>
);
}
React如何缓存一个组件
useMemo/useCallback + React.memo
这种方式主要通过props有没有改变来判断是否要重新re-render子组件,memo接收两个参数,一个是
Component
,另外一个arePropsEqual
,正常来说是不用填写arePropsEqual
的,因为默认是采用Object.is
去做一个浅比较。但是如果要自定义的话,如果返回true,那么则不会发生re-render,如果false,则会发生re-render
对于类组件来说就是shouldComponentUpdate,与 pureComponent(其实就是自动实现shouldComponentUpdate)
class MyComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return this.props.value !== nextProps.value; } render() { return <div>{this.props.value}</div>; } } class MyComponent extends React.PureComponent { render() { return <div>{this.props.value}</div>; } }
useEffect 与 useLayoutEffect 的区别
useEffect
的执行顺序:
1、React组件渲染
2、浏览器绘制渲染结果
3、React执行useEffect回掉函数
- useLayoutEffect的执行顺序:
1、React组件渲染
2、DOM节点插入文档
3、useLayoutEffect在DOM更新之后同步执行
为什么useLayoutEffect可以避免闪烁问题:
因为当DOM更新之后会立即同步执行useLayoutEffect,此时ref.current值已经指向了真实的DOM节点
ssr:因为
useLayoutEffect
可能会导致渲染结果不一样的关系,如果你在 ssr 的时候使用这个函数会有一个 warning。这是因为useLayoutEffect
是不会在服务端执行的,所以就有可能导致 ssr 渲染出来的内容和实际的首屏内容并不一致。
forwardRef
这个方法属于是ref的转发,可以把父组件传过来的 ref 进行转发到其他组件中
参数如下
- render:组件的渲染函数。React 会调用该函数并传入父组件传递的 props 和
ref
。返回的 JSX 将作为组件的输出。
import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
// ...
});
- 返回值:
forwardRef
返回一个可以在 JSX 中渲染的 React 组件。与作为纯函数定义的 React 组件不同,forwardRef
返回的组件还能够接收ref
属性。
import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
const { label, ...otherProps } = props;
return (
<label>
{label}
<input {...otherProps} ref={ref} />
</label>
);
});
function Form() {
const ref = useRef(null);
function handleClick() {
ref.current.focus();
}
return (
<form>
<MyInput label="Enter your name:" ref={ref} />
<button type="button" onClick={handleClick}>
编辑
</button>
</form>
);
}
useImperativeHandle
通常来说,ref可以暴露节点使得父组件得以控制子组件,但是如果直接暴露的话,那么节点的所有方法都会暴露出去,而useImperativeHandle则可以控制节点想暴露什么数据
参数列表:
- ref:该
ref
是你从forwardRef
渲染函数 中获得的第二个参数。 - createHandle:该函数无需参数,它返回你想要暴露的 ref 的句柄。该句柄可以包含任何类型。通常,你会返回一个包含你想暴露的方法的对象。
- dependencies:默认使用Object.is
import { forwardRef, useRef, useImperativeHandle } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
};
}, []);
return <input {...props} ref={inputRef} />;
});
useReducer
- reducer:处理函数,接受两个参数,一个是当前的state,另外一个是dispatch过来的action
- initialArg:初始值
- init函数?:如果有这个值的话,会把initalArg作为参数丢过去init函数执行
function reducer(state, action) {
if (action.type === 'incremented_age') {
return {
age: state.age + 1
};
}
throw Error('Unknown action.');
}
function createInitialState(initialArg) {
return `demo: #${username}`,
}
const [state, dispatch] = useReducer(reducer, initialArg, createInitialState?)
dispatch({
type:"xxx",
//可以存放任何东西,可以是payload可以是其他
})
React的错误边界
可以利用 ErrorBoundary + sentry错误监控来实现错误捕获,日志记录上报的功能
- 使用ErrorBoundary捕获组件书中同步的Javascript错误(主要利用getDerivedStateFromError捕获错误更新state,利用componentDidCatch去做错误捕获,日志上报)
- 然后使用Sentry记录错误信息、上报日志
- 提供Fallback UI,防止整个程序崩溃
注意:ErrorBoundary不能捕获异步代码和事件处理中的错误,只能通过try-catch + useEffect去实现
import React from "react";
import * as Sentry from "@sentry/react";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
// 当子组件抛出错误时,更新 state,显示备用 UI
static getDerivedStateFromError(error) {
return { hasError: true };
}
// 记录错误,并上报到 Sentry
componentDidCatch(error, errorInfo) {
console.error("错误边界捕获到错误:", error);
console.error("组件调用堆栈:", errorInfo.componentStack);
// 发送错误到 Sentry
Sentry.captureException(error, { extra: errorInfo });
}
render() {
if (this.state.hasError) {
return (
<div style={{ textAlign: "center", padding: "20px", color: "red" }}>
<h2>发生错误,请稍后重试。</h2>
<button onClick={() => window.location.reload()}>刷新页面</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
//ErrorBoundary的使用
function App() {
return (
<ErrorBoundary>
<BuggyComponent />
</ErrorBoundary>
);
}
HOC
高阶组件是将组件转换成另外一个组件,我们更应该注意的是,经过包装后的组件,获得了那些强化,节省多少逻辑,或是解决了原有组件的那些缺陷,这就是高阶组件的意义
HOC的强化props
//1、利用高阶组件实现分片渲染:在第一个组件渲染完成之后,用定时器把第一个组件的渲染任务推到宏任务队列;等第一个组件渲染完之后,依次异步调起下一个渲染任务
const renderQueue = []
let isFirstrender = false
const tryRender = ()=>{
const render = renderQueue.shift()
if(!render) return
setTimeout(()=>{
render() /* 执行下一段渲染 */
},300)
}
/* HOC */
function renderHOC(WrapComponent){
return function Index(props){
const [ isRender , setRender ] = useState(false)
useEffect(()=>{
renderQueue.push(()=>{ /* 放入待渲染队列中 */
setRender(true)
})
if(!isFirstrender) {
tryRender() /**/
isFirstrender = true
}
},[])
return isRender ? <WrapComponent tryRender={tryRender} { ...props } /> : <div className='box' ><div className="icon" ><SyncOutlined spin /></div></div>
}
}
/* 业务组件 */
class Index extends React.Component{
componentDidMount(){
const { name , tryRender} = this.props
/* 上一部分渲染完毕,进行下一部分渲染 */
tryRender()
console.log( name+'渲染')
}
render(){
return <div>
~~图片~~
</div>
}
}
/* 高阶组件包裹 */
const Item = renderHOC(Index)
export default () => {
return <React.Fragment>
<Item name="组件一" />
<Item name="组件二" />
<Item name="组件三" />
</React.Fragment>
}
//-----------------------------------------------------------------------------------------------------------------
//2、利用HOC实现路由懒加载
/* 路由懒加载HOC */
export default function AsyncRouter(loadRouter) {
return class Content extends React.Component {
state = {Component: null}
componentDidMount() {
if (this.state.Component) return
loadRouter()
.then(module => module.default)
.then(Component => this.setState({Component},
))
}
render() {
const {Component} = this.state
return Component ? <Component {
...this.props
}
/> : null
}
}
}
const Index = AsyncRouter(()=>import('../pages/index'))
HOC的渲染控制
渲染劫持
const HOC = (WrapComponent) => return class Index extends WrapComponent { render() { if (this.props.visible) { return super.render() } else { return <div>暂无数据</div> } } }
修改渲染树
class Index extends React.Component{ render(){ return <div> <ul> <li>react</li> <li>vue</li> <li>Angular</li> </ul> </div> } } function HOC (Component){ return class Advance extends Component { render() { const element = super.render() const otherProps = { name:'alien' } /* 替换 Angular 元素节点 */ const appendElement = React.createElement('li' ,{} , `hello ,world , my name is ${ otherProps.name }` ) const newchild = React.Children.map(element.props.children.props.children,(child,index)=>{ if(index === 2) return appendElement return child }) return React.cloneElement(element, element.props, newchild) } } } export default HOC(Index)
HOC的赋能组件
function ClickHoc(Component) { return function Wrap(props) { const dom = useRef(null); useEffect(() => { const handerClick = () => console.log("发生点击事件"); dom.current.addEventListener("click", handerClick); return () => dom.current.removeEventListener("click", handerClick); }, []); return ( <div ref={dom}> <Component {...props} /> </div> ); }; }
webpack
webpack的工作原理
- 初始化(initialzation):webpack会加载配置,加载文件中的各种配置(如entry、output、module、plugins等),并且说明启动模式(development、production)
- 解析模块:webpack会根据入口文件(entry)引入对应的所有模块,对于非JS文件使用loader将这些模块转换成JS模块
- 构建模块依赖图:webpack会根据入口文件及其依赖构建一个依赖图
- 转换和编译:这一阶段webpack会根据模块类型和配置的loader进行处理
- 插件处理
- 优化压缩
- 提取公共代码
- 环境变量的注入
- 生成输出
- 代码分割
webpack性能优化
- 代码分割:将程序拆分成多个包,然后按需加载,减少初始加载时间,减少首次加载所需要下载的数据量
- 压缩代码:使用webpack插件(例如gzip)压缩JavaScript代码,压缩代码可以减少文件大小,加速加载时间
- Tree Shaking:通过Webpack的Tree Shaking可以消除未使用的代码,减少生成的包的大小
- 资源缓存:使用Webpack的文件名哈希和输出文件分离,以便在构建时生成带有哈希的文件名,从而有效的利用浏览器缓存
- CDN加速:将静态资源(图片、字体、库)等托管到CDN上,实现资源的加速加载
- 缓存管理:配置WebPack生成长期缓存的文件名,以便更好的利用浏览器缓存
如何实现长缓存
长缓存是一种前端优化策略,它旨在使浏览器能够缓存应用程序的静态资源文件更长时间,减少不必要的网络请求,加速页面的加载速度。通过资源文件的内容与他们的文件名关联可以实现长缓存,通常浏览器会根据资源文件的URL判断是否从缓存中获取资源,如果资源文件的URL不变,浏览器就会继续使用缓存的资源直到URL发生变化
落地方案:
使用文件名哈希:webpack可以生成包涵哈希值的文件名,确保每个文件在内容发生变化的时候生成不同的文件名,这样当文件内容发生改变的时候会重新下载该文件
输出文件分离:将应用程序的代码与第三方库、样式表、和其他资源文件分开打包成多个文件,这样只有发生变化的时候才需要重新下载应用程序代码,其他资源可以长期缓存
配置缓存控制:在服务器端配置HTTP响应头,设置资源文件的缓存控制策略,例如Cache-control、Expires等等。
版本号控制:在资源文件的版本号添加到URL中,强制浏览器重新下载文件
使用文件指纹:生成资源文件的指纹,并指纹添加到资源文件名中,以确保文件内容发生变化的时候URL也发生变化
webpack构建速度提升
1、升级版本号:因为每个版本通常伴随着性能的改进与优化
2、使用持久缓存:配置Webpack生成长期缓存的文件名,在构建的时候构建修改过的文件
3、配置最小loader原则:只使用必要的loader规则,避免不必要的文件处理,减少构建时间
4、code splitting:使用webpack的代码分割功能,减少每次构建所需处理的模块数量
5、使用Tree Shaking:启用Webpack的Tree Shaking功能,删除为使用的代码,减少包大小
6、使用DLL,将不常更改的依赖库(React、Vue)打包成DLL,减少构建时间
webpack的分包
分包的主要原则
- 1、体积原则:
- 单个包的体积不要过大(建议 < 250KB)
- 总包数量适中(避免过多)
- 2、缓存原则:(动静结合)
- 将经常变动的代码和稳定的代码分开打包
- 第三方库单独打包利于缓存
- 3、加载时机:
- 首屏必要的代码打包在一起
- 非首屏内容按需引用
分包案例
Webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// 第三方库包
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 20 // 最高优先级
},
// 公共包
commons: {
name: 'commons',
minChunks: 2,
priority: 10
},
// 首屏包
entry: {
name: 'entry',
test: /[\\/]src[\\/]views[\\/]home[\\/]/, // 匹配首页相关代码
priority: 15 // 优先级高于commons,低于vendors
},
// 其他业务包
business: {
name: 'business',
minChunks: 1,
priority: 5, // 最低优先级
reuseExistingChunk: true
}
}
}
}
}
常见的分包策略:
vendors:存放第三方lib库,基本不会变动,除非版本依赖升级
commons:业务组件的公共代码,改动较少
business:业务包,存放业务代码经常改动
Tree shaking
tree shaking树摇:tree shaking就是可以把未使用的代码摇掉从而达到删除无用代码的目的
entry入口文件相当于一棵树的主干,依赖的模块相当于树枝,项目中可能使用了很多模块,但是可能只使用到了某些某块中的某些功能。Tree shaking高度依赖于ES6以后的静态结构特性,也就是import
与 export
因为在ESModule的下模块间的关系依赖是高度确定的
- 配置sideEffect
:它会告诉compiler哪些模块是纯ES5模块,由此可以安全删除文件中未使用的部分; - 如果在 开发环境 中进行打包会只保留未使用模块的定义语句并且会加上 unused harmony export square 的注释;
- 如果在 生产环境 中则会把未使用的代码删除
- 注意:如果在webpack中直接配置sideEffiect为false,这样子所有文件都会受到影响,所以一般来说是使用 glob 模式去匹配
sideEffect 与 useExports
sideEffect与useExports均与Tree shaking有关,目的相似但是侧重点不同;
- sideEffects:更为有效,因为它允许跳过整个模块或则和文件
- useExports:依赖于 terser 去检测语句中是否保护副作用,但是terser并不能完美解决这些问题,因为JavaScript是动态语言很难确定这一点,但我们可以通过
/*#__PURE__*/
注释来告诉 terser 这段代码并没有副作用可以安全删除
Hot module replace
图片
关键组件作用
- webpack-dev-middleware
- 将webpack编译后的文件储存到内存当中
- 支持监听和实时编译
- 处理静态资源请求
- webpack-hot-middleware
- 建立WebSocket连接
- 发送模块更新信息
- 处理更新失败的情况
- HotModuleReplacementPlugin
- 生成HMR运行时代码
- 创建模块热替换的API
- 管理模块更新的状态
流程: A [文件修改] --> B[webpack 检测变化] B [ webpack 检测变化 webpack-dev-middleware监听 ] B --> C[ 重新编译模块 webpack compiler进行编译 ] C --> D[生成新的模块 hash HotModuleReplacementPlugin生成 ] D --> E[通过 WebSocket 发送更新信息 webpack-hot-middleware处理 ] E --> F[浏览器接收更新 HMR Runtime接收 ] F --> G[下载新模块代码 HMR Runtime请求新模块 ] G --> H[替换旧模块HMR Runtime执行模块替换 ]
Babel的原理
Babel是一个广泛使用的JavaScript编译工具,主要的原理是将新版本的JavaScript(通常是ES6+)向后兼容转,以便在旧版的JavaScript引擎上运行,工作原理如下:
- 解析(parsing):Babel首先将输入的JavaScript代码解析成抽象语法树(AST),AST为代码的抽象表示,它将代码分解成语法树节点,一边后续的分析与转换
- 转换:在抽象语法树(AST)的基础上,Babel执行一系列的插件(plugin)和预设(preset),对代码进行修改与转换。这些转换可以包括将新语法转换为旧语法,应用代码优化,插入PolyFill(布丁代码,例如以前没有的方法,用es5给他写一个)。Babel的转换过程是插件驱动的,每个插件负责特定的转换任务
- 生成:转换完成后,Babel将修改后的AST转换回JavaScript代码字符串,这个过程涉及将AST节点逐个还原为代码,以生成最终代码进行输出
Babel的主要功能是将现代的JavaScript代码转换成ES5或者更早版本的JavaScript,以确保他们在不同的浏览器和JS引擎上运行,使得开发人员可以利用新的语言功能不用担心兼容问题,此外,Babel还能执行其他任务,例如:模块转换、TypeScript支持、Flow类型检查。Babel的插件允许开发人员创建自定义和转换功能(例如ms/api做动态导入)
服务端渲染(SSR)与客户端渲染(CSR)
1、平常浏览器中分为两种渲染方式
- 浏览器渲染(CSR):Client-Sdie-Render,这是大部分场景的渲染模式
- 服务端渲染(SSR):Serve-Side-Render,这是一种由服务端渲染
2、在日常CSR中,一半是后端返回一个空白的HTML,浏览器在接收到这个HTML后开始一系列的初始化工作,请求数据,把页面填充在页面上;而SSR则是由服务端完成这一系列动作,构建好之后从数据库提取要渲染的数据到页面上,这样一来就会减少很多页面请求数据的交互,使得首屏更快完成渲染。但又因为有更多的逻辑在渲染前开始,可能会导致白屏时间过长,服务器压力变大,页面跳转需要频繁刷新页面
3、在真实开发中,不是非此即彼,在日常开发中往往是首评使用服务端渲染保证渲染速度,而次屏使用浏览器渲染
为什么说SSR能做到首屏加载更快
CSR 加载流程
时间轴 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━>
1. 请求HTML ▇
2. 下载JS/CSS ▇▇▇
3. 执行JS ▇▇
4. 请求数据 ▇▇
5. 渲染页面 ▇▇
↑
首屏可见
SSR 加载流程
时间轴 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━>
1. 请求HTML ▇
2. 服务端执行 ▇▇
3. 返回HTML ▇
4. 浏览器渲染 ▇▇
↑
首屏可见
5. 下载JS/CSS ▇▇
6. 激活(Hydration) ▇
主要差异在于
- SSR减少了请求次数,CSR:HTML -> JS -> API数据,而SSR则是直接返回包含数据的HTML
- SSR无需JS的下载和执行
- SSR是服务器渲染,服务器到数据库是内网链接,并且可以并行请求,获取数据更快
垃圾回收机制
分代回收:V8是用分代回收策略,将对象分为新生代与老年代,老年代是包含多次回收仍存活的对象,这种策略有利于提高性能,因为新生代的对象通常比老年代更容易回收
标记清除:是一种更为复杂的垃圾回收策略,他将内存中的对象分为
可达
与不可达
两组,垃圾回收器首先标记所有可达对象,然后清除不可达对象,这个过程分为两个阶段:标记阶段、清除阶段(不可达对象是指那些无法从根对象(Root)通过引用链访问到的对象,例如null、作用域结束后的对象、循环引用但与根断开)
- 标记阶段:从根对象开始,垃圾回收器递归所有对象,标记为可达对象
- 清除阶段:垃圾回收器清除所有未标记的对象,即不可达对象
并行回收:允许多个线程同时进行垃圾回收的操作,提高性能
压缩:减少内存碎片,V8垃圾回收器包括了内存压缩的步骤,把存活的对象移动到内存中的连续的位置,以便可以腾空更多连续的内存空间
幻影依赖
幻影依赖:你代码中可以使用你没有声明的依赖
幻影依赖主要有以下两个问题:
版本问题:例如代码中引用了一个包A,包A引用了包B,在代码中直接引用包B,一开始都没问题。如果把包A进行升级,包B有可能也会跟着升级,但是代码中就出现了报错,这时候就难以判断是哪个依赖导致报错
图片依赖丢失:例如在开发依赖中安装了包A,包A引用了包B,在代码中直接引用包B,在开发环境是没问题,但是如果在生产环境,是不安装包A的,但是代码又引用了包B,这时候就会产生报错
图片
NPM:
一开始NPM是树状结构,但是这种结构会有问题,如果安装的依赖(蓝色)依赖了两个包,但是这两个包又有一个共同的包(D),他们会在他的子目录中重新安装,这就会导致一个包的安装会安装很多子包,而子包又有可能重复安装同一个包;
图片
YARN:
yarn为了避免重复安装,就把所有依赖都拍平放在一层,但是这就导致了幽灵依赖的问题,项目中未声明这个依赖就能引用
图片
pnpm:
pnpm 结合了 npm 和 yarn的做法,首先会做一个依赖关系图,然后把所有依赖都放到一个store中,当package.json中去声明依赖的时候只会在第一层进行声明,然后子包也会像npm那样创建树状的目录结构,但是创建的是一个短链接(也就是快捷方式),这些短链接去引用store中的代码;这样就避免了 重复安装和幽灵依赖的问题
图片
MVC 与 MVVM
MVC(model-view-Controller)
MVC 是一种传统的三层架构模式
Model(模型)
负责管理应用的数据和业务逻辑。
与后端交互以获取或存储数据。
通常不直接与用户界面交互。
View(视图)
负责展示数据给用户(UI)。
通过渲染模型中的数据来更新用户界面。
只关注展示逻辑,不包含复杂的业务逻辑。
Controller(控制器)
负责处理用户输入(如点击、键盘输入)。
充当 Model 和 View 之间的中介,将用户的操作传递给 Model,更新数据后通知 View 更新界面。
工作流程:
View -> Controller -> Model -> View
- 用户通过 View 触发交互(如点击按钮)。
- Controller 捕获用户的输入,并调用 Model 中的逻辑。
- Model 更新数据后,通知 View 来重新渲染。
MVVM(Model-View-ViewModel)
MVVM 是 MVC 的一种改进模式,常用于现代前端框架(如 Angular、Vue、React)中。它的核心特点是去除了Controller 引入了一个 ViewModel,用以实现 View 和 Model 之间的双向绑定
Model(模型)
- 与 MVC 中的 Model 相同,负责管理数据和业务逻辑。
View(视图)
- 用户界面部分,展示数据。
- 通过双向绑定直接与 ViewModel 交互,不直接操作 Model。
ViewModel(视图模型)
- 充当 Model 和 View 之间的中介。
- 通过数据绑定机制(如 Vue 的双向绑定
v-model
或 React 的状态管理useState
)将数据更新同步到视图。
工作流程:
View -> ViewModel -> Model -> ViewModel -> View
- 用户通过 View 进行交互(如输入框输入内容)。
- ViewModel 捕获用户的交互操作,并更新 Model 数据。
- Model 数据变更后,通知 ViewModel,再通过数据绑定机制自动更新 View
计算机网络
当输入URL按下回车会发生什么事情
1、URL检测
- 纠偏/补全(例如补充协议,把URL的中文转换成ASCII码,也就是路由编码)
2、检查DNS
- 寻找IP映射的过程:首先查找浏览器缓存 --> 本地的host文件 --> 本地的DNS服务器 --> 顶级域名服务器 直至找到IP地址找到,然后层层回退并且做缓存
3、TCP三次握手 TLS握手加密(SSL已经废弃)
- 客户端hello:客户端向服务端发送一条消息,包括支持的TLS版本和密码套件,以及一串随机字节 客户端随机数
- 服务端hello和证书:服务端发回一条信息,包括 服务端的SSL证书、服务端选择的 密码套件、以及 服务端随机数
- 认证:客户端会向颁发证书的机构核实SSL证书,这样就能确定这个证书是要找到服务器发出来的
- 创建预主密钥:客户端通过证书上的
服务端公钥
加密一串随机字节(对称密钥)
(后面就用这个作为对称密钥的一部分) - 服务端解密:服务端利用只有自己才有的私钥去解密,得到利用服务端公钥加密的
对称密钥
- 创建会话密钥:利用浏览器和服务端生成的随机数,以及对称密钥生成会话密钥
- 客户端完成
- 服务端完成
- 对称加密完成:握手完成,通信可以继续使用会话密钥
4、浏览器做准备请求(请求头和请求体)
- 例如准备cookie等信息
5、服务器处理请求
- 服务器可能是 后端服务器 又有可能是一个 BFF (Backends For Frontends)中间层
- 如果是BFF就可以说Elpis的知识:从 router --> controller --> 利用KOA渲染模版 / 返回模版
- 服务器收到请求之后可能去查询数据库例如Redis、MySql、Oracle
6、服务器响应
7、浏览器收到响应头
8、浏览器处理响应头
- connection:keep-alive(保活,这样子连接不会立即断开 )
- set-cookie要去保存cookie
- content-type:表示发过来的是一个网页还是图片还是触发下载
- 设置缓存、状态码
9、收响应体
10、渲染
- 解析HTML(预处理线程,资源加载(资源描述符))生成 DOM树 和 CSSOM树
- 样式计算:把相对单位转换为绝对单位
- Layout:布局
- Layer:分层(浏览器优化手段)
- Paint:绘制
----------主线程结束----------
合成线程
- tiles分块:因为浏览器视口有限,无法全部显示,所以没必要全部显示
- 栅格化:绘制过程中那些svg、文本会被转换为像素,然后浏览器会决定每个像素的颜色,然后以位图形式存储形成最终的图像
- 合成绘制:形成最终页面
11、根据情况四次挥手(其实这里这个是根据响应体接收完成就要做决定,如果服务器的相应头的connection不是keep-alive就保持链接,否则正常来说断开链接)
HTTP与HTTPS的区别
主要区别在于安全性与数据传输方式上
- 安全性:HTTP协议传输的数据都是未加密的,明文传输,因此使用HTTP协议传输的数据可以被任何抓包工具查看,而HTTPS协议则是由TSL+HTTP协议构建的,可进行加密传输
- 协议端口:HTTP:80、HTTPS:443
- 证书:HTTPS需要到CA申请证书
- 网络速度:HTTP协议比HTTPS协议要快,因为HTTPS协议有一个加解密的过程
- SEO优化:搜索浏览器更倾向于把HTTPS网站放到更前面的位置,因为其更加安全
HTTPS加密过程
- 1、客户端访问服务端
- 2、服务端返回证书(包含公钥)
- 3、客户端生成对称密钥,并以服务端的公钥进行加密,重新发送给服务端
- 4、服务端用自己的私钥进行解密,得到对称密钥
- 5、后续会话数据以对称密钥进行加解密
总结要点:之所以混合加密原因如下:
初始连接使用非对称加密的原因是,安全传输对称密钥不需要预先共享密钥只需要进行一次
后续通信使用对称加密的原因因为对称性能更好,适合大量数据传输,资源消耗低
组合使用的优势:结合两种加密方式的优点,在安全性和性能间取得平衡,实现了高效的安全通信
这种设计充分利用了两种加密方式的优势,既保证了密钥交换的安全性,又确保了数据传输的效率。这也是为什么 HTTPS 能够在保证安全的同时,仍然保持较好的性能表现。
什么是JWT
jwt的全程为:JSON WEB TOKEN,是目前最流行的跨域认证解决方案。
在没有JWT的时候:客户端待着身份信息登陆,服务器那边会记录这个用户已经登陆,那么下次访问服务器的时候只需要判断这个用户是否已经登陆过即可判断用户状态,这种方式的弊端在于伪造成本极低,只需要知道登陆的结构即可做到伪造
JWT的原理则是,当用户在客户端登陆,服务端会 用一个只存在于服务端的密钥对身份信息进行加密形成TOKEN ,然后把加密后的数据返回给客户端。当下次客户端再请求时,同时发送 用户信息与TOKEN,服务端拿到后会重新对身份信息与密钥进行加密生成TOKEN与传输过来的TOKEN进行比对。这就是为什么JWT无法被伪造的原因
JWT的格式
header
header部分其实就是一个声明结构为JSON对象,结构大差不差,alg为签名算法默认(hash256),而typ则为类型为JWT
{ "alg": "HS256", "typ": "JWT" }
payload
payload部分也是一个JSON对象,官方提供了七个官方字段,也可以有自定义信息
iss (issuer):签发人 exp (expiration time):过期时间 sub (subject):主题 aud (audience):受众 nbf (Not Before):生效时间 iat (Issued At):签发时间 jti (JWT ID):编号
Signature
把
header
和payload
的配置对象通过 base64 编码之后得到两个字符串,然后再用只存在于服务端的唯一 密钥 加上前面两个得到的字符串,通过签名算法得到签名,最后再用base64
编码得到signature最终的格式为:header.payload.signature
跨域
在前端领域中,跨域指的是浏览器允许向服务器发送跨域请求,从而克服Ajax的只能同源使用的限制
同源策略
同源策略是一种约定指的是:“ 协议 + 域名 + 端口” 三者相同
跨域解决方案
JSONP跨域
- 核心原理:利用 script 标签中没有跨域限制,通过 script标签的src属性,发送带有callback的
get
请求给服务端,服务端把数据组装好之后拿到请求路径上的callback 组装成一个函数的形式返回给客户端 - 优点:
- 不受XMLHttpRequest对象实现的Ajax请求收到同源策略的影响
- 兼容性好,古老浏览器也能运行
- 缺点:
- 只支持 GET 请求,不支持 POST 等其他类型的HTTP请求
- 只支持跨域HTTP请求这种情况,不能解决两个不同域的页面之间如何进行JS调用的问题
- 核心原理:利用 script 标签中没有跨域限制,通过 script标签的src属性,发送带有callback的
跨域资源共享(CORS)
CORS是一个W3C的标准,全称是 “跨域资源共享” ,它允许浏览器向跨源服务器发送 XMLHttpRequest 请求,从而克服了AJAX只能同源使用的限制,CORS需要浏览器和服务器同时支持。CORS会把请求分为
简单请求
与预检请求
简单请求
- 请求的方法:GET、POST、HEADER
- 头部字段是否满足CORS安全规范
请求头的 Content-Type为
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
应用:当客户端向服务端发送请求的时候会带上一个
Origin
表示当前的请求域是什么,而服务端需要设置Access-Control-Allow-Origin,一般来说会遇到两种情况,一种就是配置白名单列表,一种就直接是 *
预检请求
- 应用:只要不是简单请求就是预检请求,它和简单请求最大的区别其实就是在正式请求之前,他会先发送一个预检查请求告诉服务器,我这个请求有点危险,先问一下行不行;浏览器会带上Origin 、 Access-Control-Request-Method(当前请求方法)、Access-Control-Request-Headers去访问服务器。如果服务端允许了,一般都会加上一个Access-Control-Max-age的头部给浏览器表示说,在这个秒数内配置都是一样的就不用重复询问
Nginx反向代理
- 如果生产环境中不跨域,静态资源目录和数据服务器是在同一个域名下,并且一般在浏览器和服务器之间存在一个Nginx的反向代理,那么Nginx就会做一个路由转发,例如:直接访问 a.com 就是 代理到静态资源目录 ,直接返回根页面数据,如果后来访问了一个Ajax请求,请求地址是a.com/api/xxx,就会代理到数据服务器。
- 如果在开发环境在跨域,那么只需要借助框架中的dev-server配置即可,其实就是一个反向代理的角色
浏览器缓存策略
强缓存
强缓存是当客户端访问某个资源时,如果使用到了强缓存,会直接从缓存中进行加载,不与服务端产生交互
- cache-control:是常用的缓存控制头部,它允许开发者定义缓存的具体行为
- no-cache:表示每次请求都要向服务器进行新鲜度校验,判断缓存是否有效;
- no-store:表示请求和响应都不允许缓存;
- max-age(seconds):表示资源缓存的最大时间;
- Expires(http1.0):也是一个格林威治时间;由于本地时间是可以随便改的,所以它也被取代了,当cache-control存在时,Expires可以被忽略
协商缓存
协商缓存的作用是:缓存的资源需要与服务器比对确认是否有效,如果资源没有变化则返回304,表示资源可以继续使用
- Last-Modified 与 If-Modified-Since:Last-Modified为服务器设置的一个最后修改时间,If-Modified-Since则为浏览器携带的信息,表示询问这个时间段以来资源是否发生改变,如果资源没有发生变化服务器则返回304表示可以继续使用
- ETag 与 If-None-Match:Etag为服务器生成唯一的版本标识符,If-None-Match则是浏览器请求时带上此标识符,表示希望获取改标识符所标识的资源版本
二者是可以同事存在配合工作的,强缓存是决定了资源是否可以直接从缓存中进行加载,而协商缓存则在强缓存是进行进一步资源有效性的验证
服务端知识
BFF
BFF是一种Web架构,全名为Backends For Frontends,即为服务于前端的后端。
参考链接:https://zhuanlan.zhihu.com/p/634498512
BFF的优势
- 服务端对数据展示服务进行解耦,展示服务由独立的BFF端提供,服务端可以聚焦于业务处理。
- 多端展示或者多业务展示时,对与数据获取有更好的灵活性,避免数据冗余造成消耗服务端资源。
- 对于复杂的前端展示,将数据获取和组装的负责逻辑在BFF端执行,降低前端处理的复杂度,提高前端页面响应效率。
- 部分展示业务,可以抽象出来利用BFF实现,对于服务端实现接口复用。
- 降低多端业务的耦合性,避免不同端业务开发互相影响。
- 其他优势,包括数据缓存,接口安全校验等。