红线部分重点记忆。
本文主要包括自己面试过程中遇到的问题,不一定全。主要是js原理,浏览器渲染原理,性能优化,防抖节流区别,http缓存尤其是cacheControl和Last-modify要清晰地知道怎么传的。还有一些基础篇,要尽可能说的全面,如apply和call除了区别,在说一下哪个性能更好,还有防抖节流、new做了哪些事,cookie,跨域,重绘和重排,都是高频点。这些东西如果有延伸的区别也看一下,这样显得高级一点。年限少简历记得包装,学会引导对方问你擅长的东西。
先讲几个面试答得不好的问题,这些问题自己提前准备好话术,最好的办法专门针对面试学习,主要能说出来就行,相当于给你开卷。不一定要真的做过:
了解vite吗?
了解。vite将模块分为依赖和源码,依赖使用esbuild预构建,是不变的内容。esbuild使用golang语言开发,支持多线程打包,线程之间共享内容不用像js那样开启线程通信增加开销,其次直接转为机器码,node需要先转成字节码,再转成机器码。vite以原生esm方式提供源码,源码除了js文件还有其他文件,浏览器请求源码时,按需转换和提供源码,如按需加载的路由。vite的热更新(HMR)是在esm代码上执行的,本地开发时,源码使用协商缓存,依赖使用强缓存:Cache-Control: max-age=31536000,immutable。
生产环境扔需要打包,代码切割,懒加载等需要做的。而esbuild还不适合生产环境,代码切割和css的处理还需要优化因此vite还不适合生产环境打包。
使用过echarts(或其他)没,数据大屏适配?
使用echarts(D3,highchart等类似)开发数据大屏,数据太多可以采用数据分段渲染,降采样策略,数据聚合处理,延迟渲染,硬件加速,Worker对海量数据渲染进行优化。其中分段渲染和降采样是很好理解的,echart也有现成的配置。
如果是modal框里的图表,使用prefetch或者preload预获取图片资源增加体验感。
数据大屏适配问题可能会是个重点问题,解决方案是用scale,vw和vh,rem这三种方法,scale会有些留白热区偏移的问题,vw和vh虽然更灵活,但是维护成本高,图表的宽高都要计算好,rem是前两个的结合,如果开发周期比较短又喜欢频繁改需求,用scale的话可以做到快速出页面,也方便维护。
开发过什么公共组件吗?
弹幕组件,早期开发过form表单列表一体化的组件,预览组件,tooltip组件,还有上传图片公共组件(将上传的逻辑包在组件里,直接引用即可)。搜搜别人怎么做的,拿来说是自己做的,只要清楚思路即可。
2024.10.23更新:
vue老项目接入vuex作为跨层级节点通信解决方案。
开发高阶组件,切换左边菜单时,使用useEffect监听菜单的变化,触发右边组件卸载和重新挂载,做到重新请求页面数据,避免业务层面的页面监听菜单变化重新发请求。
使用过nextjs(或者小程序)开发没,特点是啥?
一定要回答使用过,./pages下面的文件变成路由,文件的路径就是url的路径,./pages/a/b下面有index文件,url访问/a/b的话直接访问index文件里的react组件,即文件名作为服务端的索引渲染。withRouter负责跳转。router.prefetch用于预获取。import dynamic from 'next/dynamic’。用于动态导入。
如何优化海量数据?使用react-virtual-list虚拟列表
使用过哪些hook和ahooks?
useToggle,useInterval, useDebounce。自己准备几个即可,最好说一下原理,因为这些只是封装好的自定义hook,没什么特别的。如果问自定义hook有什么好处,可以回答既可以获取实时更新的state(作为props传到自定义hook里),又可以使用useCallback记忆函数避免重复声明,然后useCallback里获取最新的props值。还有useEffect,useUpdateEffect,useMemo,useReducer。
useSetState,useMount。
如何监控,做过pv,uv吗,有什么用?
使用埋点,可以帮我们分析用户使用的喜好,经常访问的页面。免费的有Sentry接入前端监控:除了平常会自己try catch捕获,对于没有捕获的报错,使用全局的error绑定事件捕获错误。通常是监控页面的曝光度,http报错,前端代码报错,白屏等问题。埋点的本质是img标签设置src属性发送get请求。try catch捕获同步代码,监听error事件捕获异步代码。
cookie、localstorage和sessionStorage有什么区别?
cookie主要用法是第一次访问后端接口的时候有个set-Cookie方法,浏览器遇到这个方法会帮你去设置cookie,通常cookie的值代表的是用户身份信息,一般不超过4kb。当然可以通过自己设置document.cookie='a=b',但是cookie不是这么用的,因为不安全,也不是后端下发的。因此这样去理解cookie是记录在后端的凭证。删除cookie,可以给他设置一个过去的超期时间。httponly表示cookie不可读取。cookie如果不设置过期时间,就会记在内存里,电脑重启会消失。设置过期时间记在磁盘里。
localstorage不主动删除会一直挂在域名下面。怎么解决呢,可以将一个时间戳拼接到后面。当然有好几M那么大。
这个问题看上去很简单,但是如果没有真正理解,换一种问法就被问懵了。建议用nodejs写一下cookie生成的这个过程。问这个问题的时候,一定要把cookie的setCookie方法讲出来。比如问你cookie不设置时间,浏览器关了还存在吗?不一定会,主要看内存里这个进程是否还在运行。但设置时间的话,即使重启一段时间内都不会消失。
如何处理子应用和主应用css隔离问题?
有很多。前端本身已经对主流浏览器兼容了,如qiankun微服务创建一个不受外部影响的盒子。如果是新项目,可以使用同一个antd版本,应用使用同一个版本也是可取的,或者加前缀(不推荐),如果是antd5,可以使用css变量。如果是嵌套的老项目,全局覆盖吧。把主应用的拷贝到旧的里面吧,一般只有常用的一些会出问题。
平时怎么提升?
看看算法,对项目落地会有所思考,也会思考项目中有哪些可以提升的地方。参考官网的内容优化页面内容的加载。开启css module编写样式,添加webpack别名alias属性。这个环节只有一个目的,就是让人觉得你是能胜任高级开发的,让别人觉得你很厉害,你在前公司里做了很多事。
遇到的比较复杂的困难?
这个环节就是想多看看你实力怎么样,人家也不知道问啥了,就等你吹牛逼,你说不出来,那作为面试官,我感觉你不太行,反正我觉得你不行。可以提前准备回答的问题,尽可能的结合过往经历吹,没做过但是熟悉的都拿出来吹:
比如版本问题,如swiper新版本支持virtual属性,只渲染当前看到的内容。由于项目比较老版本会比较低,又急着上线,只能使用基本的功能,这时会打印出实例,通过控制台[[prototype]]属性或者点到swiper源码看哪些方法可以用,盲目的去官网查看api往往解决不了问题,swiper最新版本是11,项目里用的6,按官网api大概率会报错。比如还优化过性能,比如从0-1负责一个项目。
如果做过数据大屏,就可以说里面的ui比较复杂,开发周期比较长,问你怎么解决就说加班,查文档,ui设计的简单一点。
2024.11.11更:大文件上传,需要对文件切片,如果要支持断点续传的话,需要后端记已上传的位置。前端要做的主要难点是文件切片上传,一般浏览器最大的并发数为6,前端限制为4,同一时间发送四个请求,使用count记录请求的数量,有请求回来count--变为3,发送下一个请求。还有断点续传的时候,后端告诉前端哪些切片已经上传好(根据切片的文件名和状态),前端继续从未上传的地方开始上传。计算hash值使用new Worker计算。
websocket:心跳机制,隔10秒发送‘ping’消息维持连接。
哪些优化?
做过的优化:echarts按需引入,定时器会出现偏差。接入vuex。使用prefetch优化。echarts公共options提取,增加代码可读性。
使用translate代替window滚动,可以优化重排重绘,因为transform使用的是GPU硬件加速,看着答吧。
webpack打包的优化,prefetch,preload对资源的预加载。splitChunk,cdn,webp格式的图片都可以说说。
preload会优先加载当前页面的资源,优先级比较高,发生在dom渲染之前,可能也会阻塞渲染。css文件并行下载,会不阻塞dom解析,会阻塞dom渲染(css规则树和dom树都好了才会合到一起渲染)。
nextjs、umi里扩展webpack配置,如添加alias别名,这样引用组件避免../../../../这样的引入方式。
如何解决异步同时请求,不确定谁先返回的问题,曾经遇到一个人问我输入框连续输内容,不用防抖节流怎么处理请求?
其本质就是事件循环里的任务队列,不能确定先请求的能否先放到宏任务队列里,我们可以借助于同步代码去优化,使用useRef方法,每次输入的onChange事件里,将e.target.value赋值:
currentRef.current.count = e.target.value
然后请求的参数和返回都带上这个value,判断是否渲染根据条件:
if(currentRef.current.count == res.data.params)
判断返回的字段判断是否去setState渲染。本质上是用同步的代码去记输入的顺序,记录自己期望的结果,请求里带上标识位,也就是请求参数返回给前端去对比。然后引导面试官问你事件循环,不然他又开始问别的东西。
1. es6基础
for in: 遍历对象,循环的是key。也能遍历数组,循环的是下标,但是是字符串不是数值。会遍历原型链上的属性,可以使用hasOwnProperty解决。
for of: 遍历数组和实现了迭代器功能的结构,不能遍历对象。
迭代器iterator: Array、Set、Map、函数argument对象内部已经实现了iterator接口,可以使用for of变量。注意Map可以普通对象不可以,因为Map实现了迭代器
generate函数返回一个迭代器对象,通过next方法一步步执行函数体内代码,它的本质是一个指针对象:
- 初始时用该next方法,移动指针使得指针指向数据的第一个元素
- 然后每调用一次next方法,指针就指向数据结构里的下一个元素,直到指向最后一个元素
- 不断调用next方法就可以实现遍历元素的效果了
async函数就是generate函数的语法糖,await对应于yield,用于解决嵌套地调用回调。async基于promise封装的。
自定义object内部迭代器,遍历object内部数组
const obj = {
name: 'abc',
cities:[ 'London', 'New York', 'Tokyo'],
[Symbol.iterator](){
let index = 0;
let _self = this;
return {
next: function(){
if(index >= _self.cities.length){
return {value:undefined,done:true}
}else{
const result = {value: _self.cities[index], done:false}
index++
return result
}
}
}
}
}
for (const v of obj) {
console.log('v=',v)
}
// v= London
// v= New York
// v= Tokyo
使用for of,可以在循环体内部使用await来保证异步代码顺序执行,就是基于迭代器实现的。
普通函数和箭头函数区别:箭头函数不能new,this指向问题,没有arguments(防抖函数里就不能用匿名函数)
apply和call的区别:apply第二个传数组,call一个个传,超过3个参数call更快,因为和内部[[call]]方法参数格式一致。
this指向问题
ts: 规定变量,参数和返回值类型,无法推断类型时用类型断言。泛型允许您创建可与各种类型一起使用的可重用组件或函数,保证使用不同数据类型的灵活性。Partial<Person>使得Person里面所有类型可选。Pick<Person, 'name' | 'age'>。Omit<Person, 'city'>。Exclude<Color, 'green' | 'blue'>从联合类型中排除。keyof获取接口的key,typeof获取变量的值的类型。Person写两个同名的会合并。type和interface区别:type使用交叉类型&来继承,不可重复声明,可声明为基本类型。interface可以extends,只能定义对象,可重复声明相当于交叉类型。
Array.from,forEach,for of遍历类数组。Array.toString()可以将二维数组拍平。
2.继承
原型链继承:缺点是prototype是属于函数的而不是实例,prototype被所有new出来的实例共享,修改了会相互影响。
apply方法继承:不能继承原型上的属性
组合继承:构造函数要执行两次
原型式继承:创建一个function,传参是对象obj。内容new一个实例F,F.prototype = obj,返回这个实例F。 Object.create将此方法规范化,第一个参数是一样的,把传参对象挂到新的对象的prototype上,第二个参数定义额外的属性。
寄生式继承:原型式继承+自己定义的属性。现根据原型式继承创建一个对象obj,在往里面加属性obj.xxx = "xxx";
原型寄生组合继承
class继承:通过super函数继承父类属性方法。
3.深复制和浅复制
深复制是新的对象修改操作不影响老的对象。使用JSON.parse(JSON.stringify(obj))或者Object.assign(obj)或者递归遍历赋值。也可以借助于闭包,将对象属性的操作转移到函数里而不是直接赋值和操作。
4. http三次握手
SYN:同步序列编号,建立连接时使用的握手信号。建立连接时,客户端首先发出一个SYN消息。
(FIN表示申请结束连接)
seq:序号。是报文段发送的数据组的第一个字节的序号。若报文段序号是300,数据部分有100个字节,下一个报文段序号是400。
ACK:确认值,为1表示确认号字段有效,建立连接。ACK为1,报文才能传输。
ack:确认值,下一个期待收到的字节序号,表明该序号之前的数据都收到了。ACK为1时有效。建立连接时,SYN报文的ACK标志为0。
x表示第一个数据字节的序号,最开始为0。客户端第一次握手就发送的是seq=x=0(表示客户端自己的初始序号0),SYN=1。服务器接受后发送ack=x+1=1,表示期望收到的数据序号(客户端的序号)为1,SYN=1,ACK=1,seq=y=0(y表示服务器自己的初始序号,从0开始)。
第二次握手:客户端收到数据后,发送ACK=1,自己发送数据的序号seq=x+1,期望接受的数据序号ack=y+1。
这里的x+1和y+1不是固定+1,取决于每次发送了多少字节的数据,一个字节就+1。
seq和ack都是报文序号,seq是自己发的报文序号,ack是对方的报文序号。
三次握手为了防止失效的报文又发给服务端。第一次是客户端请求连接,第三次是客户端同意连接。seq的初始值也叫ISN,是随机生成的,防止旧的连接和恶意链接。半连接可以防止洪水攻击。
http2多路复用:一个tcp连接里并行请求。http1.x浏览器使用个5-7个tcp连接去并行请求。
5. 闭包
外部访问函数内部作用域变量。可以合理的使用闭包,优点是防止变量全局污染,缺点是不当的使用可能会造成内存泄漏:
function fn() {
const arr = [1];
function fn1() {
console.log(arr[0]);
}
return fn1
}
let test = fn()
test()
test没有及时回收,过段时间内存还占用空间。可以test = null或者fn()(),早期可能外面在包一层函数自调用。未清除定时器和DOM事件监听器会导致内存泄漏,需要remove监听器,clear定时器。
垃圾回收机制:最初是引用计数法,每个对象内部标记引用它的总个数,如果为0,为垃圾对象,如果两个对象相互引用,断开对象的引用后他们不是垃圾对象,因此无法回收互相引用的对象。
标记清除法:从window开始找,所有引用的对象,标记为使用中。若没有标记为使用中的都是垃圾对象。打上标记和去除标记的时机是进入执行环境和离开执行环境,比如执行一个函数,执行完后即使里面还存在引用关系,但是函数的执行已经结束了,这个时候就可以回收函数内部变量。解决了循环引用问题,因为从window找引用的对象过一会是null:
var o1 = {};
var o2 = {};
o1.a = o2; // o1 引用 o2
o2.a = o1; // o2 引用 o1
o1 = null
o2 = null
主动回收:变量=null
6. 浏览器进程
四个:主进程负责协调,渲染进程处理网页呈现和交互,插件进程负责浏览器插件,GPU进程负责图像和视频。
渲染进程:GUI线程,JS引擎线程执行js代码,事件触发线程处理点击等事件,定时器触发线程,异步Http请求线程。
事件循环又叫消息循环,是浏览器渲染进程中js线程的工作方式。主线程先执行同步代码,异步代码从主线程提出放在任务队列(消息队列),执行完后在任务队列放入一个事件(回调函数),主线程执行完同步代码后,执行微任务,再去读取任务队列并执行。js是单进程的,是因为主进程一个时间点只能做一件事,但是js在浏览器或者node环境中还有其他线程,如定时器线程,io线程,他们去处理耗时任务,结束以后把回调函数放在一个任务队列里,主进程进行时间循环,发现任务队列不为空了就取出来执行。
渲染进程中的主线程的js同步代码形成一个执行栈(代码的执行顺序),是一个同步任务,执行完以后执行微任务,然后从宏任务队列取回调按顺序执行。然后检查渲染,GUI线程接管渲染,渲染时遇到js,js线程继续接管,执行下一个事件循环。
浏览器环境微任务有promise,Mutation Observer API,queueMicrotask。宏任务是setTimeout定时器,requestAnimationFrame,网络请求,事件回调函数onload(DOM事件),ui渲染。
nodejs环境会有区别。微任务有nextTick队列和microTask队列。宏任务队列放在6个阶段:
timer阶段是定时器,pending callback是系统错误回调,tcp连接错误和dns解析异常等。idle prepare内部使用的,poll阶段是轮询阶段主要是io操作,check是setImmediate,close 处理关闭的回调如socket.close()。
两个事件循环差异在于环境不同,API不同,node主要是文件操作,网络请求等io操作,数据库访问。
回流:改变大小。重绘:改变样式。回流一定重绘,重绘不一定回流。优化:一次性修改样式,最好使用class,不要一行一行操作修改。transfrom和opacity不会重绘,他们使用GPU硬件加速,本质是合成阶段开启了新的图层。
js线程和GUI线程(负责解析html生成dom树和css树)互斥,即解析HTML然后生成DOM树和生成CSS规则树的过程中,遇到js文件就会阻塞,也就是说遇到js文件其后的元素解析和加载都会暂停。所以很多js放在body的最后,也可以使用js异步加载,即script加上defer或者async,async会在加载完后立即执行阻塞html解析,defer不会。解析css不会阻塞html解析,但是阻塞html渲染,css和html解析都和js互斥。
js和dom树、css树虽然互斥,但dom树和css树的解析是互斥,css树的加载不影响dom的解析,但是会阻塞渲染,即dom树和css规则树都好了才渲染。即css阻塞渲染不阻塞加载(解析)。渲染是渲染,加载是加载。
DOMContentLoaded表示html解析完,生成了dom树,但是外部资源引入的img和样式表还没有加载,此时获取不到外部引入的图片大小。onload表示全部加载完。
7.原型和原型链
显式原型简称原型,即prototype。隐式原型__proto__。
Function.__proto__ == Object.prototype
A.__proto__ = Function.prototype
for in会遍历原型链:
8.协商缓存和强缓存
用的比较多的是cacheControl: max-age=36800。他表示超过时间走协商缓存,否则走强缓存。协商缓存时请求带上Last-Midify或者Etag值,让后端决定是否更新。所以打包文件都会带contenthash。使用contenthash的话,可以有利于减少上线白屏,如果不刷新页面都走旧的js文件访问可以避免白屏。
9.排序算法
function swap(arr, i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 冒泡
// 可以利用一个标志位进行优化,每轮遍历开始的时候flag为0,如果第i轮没有进行交换,则说明数组已经有序不需要在遍历
function Bubble(arr) {
for (var i = 0; i < arr.length; i++) {
for (var j = 0; j < arr.length - i - 1; j++) {
var temp = arr[j];
if (temp > arr[j + 1]) {
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// 选择排序,不稳定 如 5 5 2,第一次交换后5之间的顺序变了
function Choose(arr) {
for (var i = 0; i < arr.length; i++) {
var min = i;
for (var j = i; j < arr.length - 1; j++) {
if (arr[j + 1] < arr[min]) {
min = j + 1;
}
}
var temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
}
// 快速排序
function quickSort(arr, _left, _right) {
var left = _left; // 左边
var right = _right - 1; // 右边
var temp = arr[left]; // 基准值,一般是第一个数,比基准值大的数都在基准值右边,比基准值小的数都在基准值左边。
if (left >= right) {
return;
}
while(left != right) {
while(arr[right] >= temp && left < right) {
right--;
}
arr[left] = arr[right]; // 从最右边找到第一个比基准值小的数,赋值到前面去。留出空位arr[right](虽然是空位,但是实际还有值,只不过该位置的值待填入)
while(arr[left] <= temp && left < right) {
left++;
}
arr[right] = arr[left]; // 从最左边开始找第一个比基准值大的数,放入到上一步留出的空位,然后留出空位arr[left]。
}
// 考虑right = left + 1时,假设left是空位,right上的数据小于temp,arr[right]填到arr[left]上去,left++,此时left和right都指向right。最后把temp填到right上
// 考虑right = left + 1时,假设left是空位,right上的数据大于temp,则right--,空位仍然是left。最后再把temp填到left上
// 此时left = right
arr[left] = temp; // 这样一趟就遍历完了,每趟都会遍历n次
// 递归,最多递归n-1次,此时时间复杂度是n + n - 1 + ... + 1。最少递归logn次,此时时间复杂度是nlogn。
if (_left < left - 1) {
quickSort(arr, _left, left - 1);
}
if (left + 1 < _right) {
quickSort(arr, left + 1, _right);
}
}
// 选择第一个作为基准值,从第二个数开始每次遍历将小的数跟前面大的数交换(或者自己跟自己交换),并且使用index=left+1开始进行计数用于记录小的数的数量
// 遍历结束以后将第一个基准值和最后一个小的数即index位置的数交换,这样基准值右边的都是大的数,左边都是小的数
function quickSort2 (arr, _left, _right) {
var left = _left;
var right = _right;
if (left >= right) {
return;
}
var index = left + 1;
var temp = arr[left]; // 基准值
for (var i = index;i < _right;i++) {
if (arr[i] < temp) {
swap(arr, index, i); // 将小的数放在前面,使用index记录最后一个小的数的位置
index++;
}
}
swap(arr, left, index - 1); // 基准值和最后一个小的数交换,此时基准值在中间位置
if (left < right) {
quickSort2(arr, _left, index - 2); // 将左边的小的数排序
quickSort2(arr, index, _right); // 将右边的大的数排序
}
}
var arr1 = [5, 3, 3, 36, 24, 19, 1, 92];
var arr2 = [5, 3, 1];
// Bubble(arr1);
// console.log(arr1);
// Choose(arr1);
// console.log("选择排序", arr1);
// quickSort2(arr1, 0, arr1.length);
// quickSort2(arr2, 0, arr2.length);
// console.log("快速排序", arr1);
// console.log("快速排序", arr2);
// 归并排序
function mergeSort(arr, left, right) {
const mid = Math.floor((left + right) / 2);
if (left < right) {
mergeSort(arr, left, mid);
mergeSort(arr, mid+1, right);
merge(arr, left, mid, right);
}
}
function merge(arr,left, mid, right) {
const temp = [];
let i = left; // 左侧
let j = mid + 1; // 右侧
let k = 0;
while(i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k] = arr[i];
k++;
i++;
} else {
temp[k] = arr[j];
k++;
j++;
}
}
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= right) {
temp[k++] = arr[j++];
}
for(let s = 0;s < temp.length;s++) {
arr[left++] = temp[s];
}
}
mergeSort(arr1, 0, arr1.length - 1);
console.log(arr1);
10. 防抖和节流
防抖和节流都是为了限制短时间内多次触发事件,区别在于防抖短时间内触发会重置定时器导致函数一直推迟执行(非立即执行和立即执行都会一直推迟),而节流就是按照时间间隔去执行(立即或非立即)。
防抖也是可以立即执行的,立即执行的防抖就是时间间隔内最初就开始执行然后时间间隔内不再执行,频繁点击时会一直推迟状态的翻转,停下来以后也不再执行函数。当下一次触发时又会立即执行。
useDebounce优化了lodash的防抖函数,做了那些优化,或者说普通的防抖函数有什么问题:
这个问题有好几家都问了,大概意思是用了lodash的防抖函数包一层,函数里拿不到最新的state值,而ahooks的useDebounce可以解决。当时觉得这个问题应该是个很常见的问题,我怎么没遇到过?我好菜啊,后来看了一下ahooks以及结合百度到的问题,发现场景就是输入框要实时输入内容去搜,但是请求需要做防抖操作,一开始这样写的:
Button上面加的没问题,但是Input上不能给整个change事件加防抖,因为要受控。这个时候就发现问题了,search方法会执行多次:
究其原因,无非就是search重复赋值,debounce执行了4次,创建了4个定时器执行了4遍,所以需要useCallback来避免重复声明:
const search = useCallback(debounce((v) => {
console.log(v);
}, 1000), []);
然后这样是肯定不能加input输入得内容的依赖,不然重复声明又挂了,所以面试官开始搞事情了, 他直接问使用debounce会拿不到新的state值怎么办,你当然可以回答使用useDebounce,他的原理是声明一个自定义hook,将value值作为props传进去:
function useDebounce<T>(value: T, options?: DebounceOptions) {
const [debounced, setDebounced] = useState(value);
const { run } = useDebounceFn(() => {
setDebounced(value);
}, options);
useEffect(() => {
run();
}, [value]);
return debounced;
}
useDebounceFn方法本质上就是使用useMemo做缓存,useDebounce这样的一个hook在执行时从props拿的value,当然能拿到最新的state,然后监听返回的debounced值变化,这样又得加useEffect去发送api请求,但这样很冗余,对于输入框的场景,你可以直接告诉面试官,e.target.value就是最新的,干嘛还这么麻烦?search(e.target.value)不行吗??因为debounce返回的是个匿名函数,里面的执行是fn.apply(this, args);。注意这个args,他就是传过去的值。或者我把state记在useRef里也是一种方法,非得问甲骨文。
2025.2.25更新:
使用useCallback+debounce的方式代替 ahooks里的useDebounce,对于点击事件useCallback可以添加依赖,只需要保证点击时对函数进行记忆即可,对于输入框的查询useCallback里不能添加e.target.value,利用debounce函数执行fn.apply(this, arguments)的特点,将value值直接传给fn。
11. newWorker开启多进程
12. 函数柯里化
函数的length属性代表了参数的个数,如果参数不够就返回一个curry函数,如果参数一致返回执行结果:
function add(a, b, c) {
return a + b + c;
}
function curry() {
const fn = arguments["0"];
console.log("arguments", arguments);
const args = Array.prototype.slice.call(arguments, 1); // 类数组, instanceof Array
if (args.length === fn.length) { // fn.length 等于形参的参数
return fn.apply(this, args);
}
function _curry() {
args.push(...arguments);
if (args.length === fn.length) {
return fn.apply(this, args);
} else {
return _curry;
}
}
return _curry;
}
a = curry(add, 1)(12)(3);
b = curry(add)(1)(2)(3);
c = curry(add,1,2,3);
d = curry(add,1)(2,3);
console.log("a",a);
13. instanceof
用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
instanceof实现也不复杂,无非就是一层层找,先找到Fn,在找Function,再找Object,最后Object.prototype.__proto__为null找不到返回false
const _instanceof = (target, Fn) => {
if (target == null || typeof target != 'object') {
return false;
}
let pro = target;
while(true) {
pro = Object.getPrototypeOf(pro); // pro = pro.__proto__;
if (!pro) {
return false; // 找到Object的null
} else if (pro === Fn.prototype) {
return true;
}
}
}
14 new干了哪些事
创建空对象,实例对象的隐式原型__proto__等于构造函数的原型(obj.__proto__ = fn.prototype),改变this指向,执行构造函数。
判断return,返回的引用类型,new就不起作用了。
手写new
function myNew(fn, ...args) {
var obj = {};
Object.setPrototypeOf(obj, fn.prototype);
var result = fn.apply(obj, args);
return result instanceof Object ? result : obj;
}
15 手写promise,发布订阅
16 性能优化
开启多线程打包TerserPlugin插件设置parallel: true,webpack4推荐thread-loader,js和ts编译都开启多线程。
检查代码中给组件加key
本地考虑使用vite,热更新。
使用runtimeChunk提取manifest部分代码(管理所有模块的交互代码),生成的文件里有个manifest.js文件。runtime是程序运行时连接模块所需的加载和解析逻辑,是用来连接模块化程序用到的所有代码。manifest是打包时产生的,记录连接模块的详细要点,import和require语法转化成__webpack_require__方法获取到模块,runtime通过manifest的代码找到标识符对应的模块。
Tree Shaking删除import但是没有使用的文件代码,但是会有副作用,如引入一个js文件,文件里设置了document.title='xxx',如果删掉则无法触发修改title。因此不建议使用tree-shaking,否则出现问题难以排查。sideEffects可以帮我们排查副作用的文件,sideEffects的值是个数组,数组里的文件没有使用也可以保证不被删除。
stats: "errors-only",只显示错误信息,删除进度条和其它信息。
IgnorePlugin
noParse处理jquery、lodash或者一些min.js文件,这些文件也没有其它依赖,加到noParse列表里让webpack不去解析,提高打包效率。
devtool: 'source-map'方便定位源文件中的错误。收集错误使用addEventListener("error", callback)
拆包过程需要注意将node_modules拆的更细致一点:
const splitConfig = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 30000, // 大于30kb的才会被分割
maxSize: 0, // 最大没有限制
minChunks: 3, // 要提取的chunk最少被引用1次
maxAsyncRequests: 5, // 按需加载时并行加载的文件的最大数量
maxInitialRequests: 3, // 入口js文件最大并行请求数量
automaticNameDelimiter: '~', // 名称链接符
name: true, //可以使用命名规则
cacheGroups: { // 分割chunk的组
// node_modules文件会被打包到vendors组的chunk中。--> vendors~xxx.js
antd: {
name: 'antd',
test: /[\\/]node_modules[\\/](antd)(.*)/,
chunks: 'all',
priority: 10,
minChunks: 1,
reuseExistingChunk: true
},
react: {
name: "react",
test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
chunks: "all",
minChunks: 1,
priority: 30,//打包权重
reuseExistingChunk: true
},
moment: { //减小代码体积-reac-dom react-router-dom一起打包成单独文件
name: "moment",
test: /[\\/]node_modules[\\/]moment(.*)?[\\/]/,
chunks: "all",
minChunks: 1,
priority: 50,//打包权重
reuseExistingChunk: true
},
design: {
name: 'ant_design',
test: /[\\/]node_modules[\\/](@ant-design)(.*)?[\\/]/,
chunks: 'all',
priority: 20,
minChunks: 1,
reuseExistingChunk: true
},
vendors: {
name: "vendor",
test: /[\\/]node_modules[\\/]/,
priority: -20,
},
}
},
minimize: true,
minimizer: [new TerserPlugin({
parallel: true,
cache: true,
terserOptions: {
compress: {
// drop_console: true,
},
output: {
comments: true,
},
},
}),
new OptimizeCSSAssetsPlugin({}),
],
runtimeChunk: {
name: "manifest", // 模块信息清单
},
},
};
主要比较大的是antd,@ant-design,moment和lodash这几个库,即使拆出来,单个的文件也还是比较大,这时候可以考虑cdn,第一次先打包出来这几个第三方库, 放在静态资源服务器上,webpack添加externals选项,这样以后打包就不打包这几个库,运行时在直接加载这几个库。当然打包出来的文件还可以在压缩一下:npm i compression-webpack-plugin@5.0.1。注意compression-webpack-plugin的版本。
然后还需要nginx配合设置一下:
项目性能优化之用compression-webpack-plugin插件开启gzip压缩,以vue为例 - 知乎
使用babel-loader时,可以使用@babel/plugin-transform-runtime:
antd4和之前版本在.babelrc使用babel-plugin-import按需加载antd中的less或css,antd5使用cssinjs内置了。
17 算法
一般有两个变量i和j同时控制循环或者循环的值i经常具有跳跃性,如i从1变到3,又从3变到2就用
while循环,常规的遍历才用for循环。
表达式求值:
const rl = require("readline").createInterface({ input: process.stdin });
var iter = rl[Symbol.asyncIterator]();
const readline = async () => (await iter.next()).value;
// 表达式求值顺序:先乘除,后加减,先左边,后右边,先括号内,后括号外
// 使用两个栈的方式,一个寄存运算符称为A,一个寄存数据称为B。
// ch不是运算符,则压入B,读入下一个字符。
// ch是运算符,比较A中栈顶元素和ch优先级,ch优先级高的话,ch压入A栈,继续遍历。栈顶优先级高的话,就取出栈顶和B中两个数据计算,得到的结果压入B栈。
// 然后继续循环从A栈顶去运算符和ch比较。
// 当ch为)时且遇到栈顶为(,则从A中弹出左括号,ch也不做操作,相当于(x)换成x,这一步是存在的,因为ch为)时,优先级总是最小的,所以会把()里面的都处理完。
// 即在上一步的循环中就会循环到A栈顶为(的情况。
// 按照数据结构上描述,当遍历整个表达式结束以后,此时还没计算结束,但此时右边的优先级都高于左边,所以需要while循环重复从栈里取数字和运算符,直到运算符栈为空
// 而为了统一,需要在一开始就给表达式加上前缀(后缀),尤其是末尾加上)作用很明显,因为可能到最后了右边都是高优先级,加上),它们都比)优先级高就可以一直循环直到运算符栈为空
// 例如2+3*5,+比*优先级低,优先级函数priority不好复用,改成(2+3*5),遍历到最后的)时,*比)高,计算得(2+15),此时+又比)优先级高计算得(17),然后(出栈,运算符栈为空计算结束。
// 这就不难理解课本的循环是while(ch != "#" || getTop(OPTR) != "#"),当然课本上前后加了#也是为了在末尾加一个低优先级的。
// 当然可以for循环表达式,给表达式加上(),for循环到最后一个字符)时,通过while循环把剩下的计算处理了。
// 运算符栈为空时,此时数据栈的数值就是计算结果。
void async function () {
// Write your code here
while (line = await readline()) {
evalExpression(line);
}
}()
// 传入表达式
function evalExpression(str) {
let numbers = []; // 存数字
let opts = []; // 存运算符
let newStr = str;
if(str[0] != "(") {
newStr = "(" + str + ")";
}
opts.push(newStr[0]); // 第一个左括号直接入栈,且优先级最低
let flag = false; // 表示是否符号是否是正负号,如-1*(-1-1)。出现一次运算符以后置为true,下一次还是运算符的话看做是正负号,走处理数字的分支。
for (let i = 0; i < newStr.length; i++) {
let data = newStr[i];
if (/^\d+$/.test(data) || flag) {
let j = i + 1;
while(/^\d+$/.test(newStr[j])) {
data += newStr[j];
j++;
};
numbers.push(data); // 数字入栈
i = j - 1;
flag = false;
} else {
// 获取栈顶,和当前字符比较优先级,考虑2*(3+4*5),会循环两次,第一次计算4*5得到2*(3+20),这时候3+20优先级高于),在计算一次得2*(23)
while(priority(opts.slice(-1)[0], data)) {
compute(numbers, opts); // 优先级较高就先计算前面的,如果是2*3+1,只会循环一次得6+1,但不会去计算得到7,因为1后面可能是左括号或者*/。
}
if (opts.slice(-1)[0] == "(" && data == ")") {
opts.pop(); // 去掉运算符栈顶的(,继续遍历,如3*(2+1)计算得到3*(3)以后,左括号出栈,有括号也没必要入栈,继续遍历。
} else {
opts.push(data); // 如2+3*4,*优先级更高,不能计算2+3,因此*入栈即可,然后继续遍历
}
if (data == "(") {
flag = true;
}
}
}
console.log(numbers[0]);
}
// 定义运算符优先级,m是栈顶元素,n是取出来的字符ch。上一个运算符优先级高的话,就要先计算,这也符合实际场景。
function priority(m, n) {
// n="("时,优先级总是大于m。m="("时,优先级又总是最低的。
if (m == "(") {
return false;
} else if (m == "+" || m == "-") {
if (n == "*" || n == "/" || n == "(") {
return false;
}
} else if (m == "*" || m == "/") {
if (n == "(") {
return false;
}
}
// n为空时,遍历结束。这时认为是m优先级更高,执行计算的操作。直到m也是空。
return true;
}
// 使用数组模拟栈,计算时从arr1取出两个数字,从arr2取出运算符,计算结果添加到arr1中
function compute(arr1, arr2) {
const a2 = Number(arr1.pop());
const a1 = Number(arr1.pop());
const operate = arr2.pop();
let result;
if (operate == "+") {
result = a1 + a2;
}
if (operate == "-") {
result = a1 - a2;
}
if (operate == "*") {
result = a1 * a2;
}
if (operate == "/") {
result = a1 / a2;
}
arr1.push(result);
}
连续的公共子序列:
const rl = require("readline").createInterface({
input: process.stdin,
output: process.stdout
});
let arr = [];
let str1,str2;
rl.on('line', function (line) {
arr.push(line);
if (arr.length == 2) {
str1 = arr[0];
str2 = arr[1];
commonStr(str1, str2);
}
});
// y a a x 和 z a x y
// 连续的最大公共子序列,两个片段的末尾不相等,则认为是0,如果相等则dp[i][j] = dp[i-1][j-1]+1。最后计算二维数组中最大值。
function commonStr(m, n) {
// 当i或j为0时,i-1或j-1为-1,所以构造的二维数组加1列和1行,初始值记为0。子序列的比较从[1,1]到[n+1,m+1]算。
const dp = [];
let maxLength = 0;
// 初始化数组记录
for (let i = 0; i <= m.length; i++) {
dp[i] = [0];
if (i == 0) {
for (let j = 1; j <= n.length; j++) {
dp[i].push(0);
}
}
}
for (let i = 1; i <= m.length; i++) {
for (let j = 1; j <= n.length; j++) {
if (m[i - 1] == n[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
if (dp[i][j] > maxLength) {
maxLength = dp[i][j];
}
} else {
dp[i][j] = 0;
}
}
}
console.log(maxLength);
}
深度和广度搜索:
其中深度搜索可以直接递归,即先访问数据,然后在循环遍历孩子节点,循环里进行递归。
也可以借助栈
// 1
// 2 3 4
// 5 6 7 8
// 9 10
const tree = {
data: 1,
next: [{
data: 2,
next: [{
data: 5,
next: [{
data: 9,
next: null
}]
}]
},
{
data: 3,
next: [{
data: 6,
next: [{
data: 10,
next: null
}],
}, {
data: 7,
next: null
}]
},
{
data: 4,
next: [null, {
data: 8,
next: null
}]
}
]
}
// 深度优先搜索,将节点放在一个栈里。因为是先一直往下找,所以同一层次的节点从右到左的入栈,然后栈顶出栈访问并压入它的子节点。
// 对于图而言,还需要构造一个visited数组,这样一个节点如果有多个父节点,只会访问一次。
function dfs(tree) {
const stack = [];
stack.push(tree); // 根节点入栈
while(stack.length) {
const currentNode = stack.pop(); // 提取数组最后一个
console.log(currentNode.data);
if (currentNode.next && currentNode.next.length) {
let i = currentNode.next.length;
// 倒序入栈,不要currentNode.next.reverse().forEach,这样会改变原有tree的结构。
while(i) {
currentNode.next[i-1] && stack.push(currentNode.next[i-1]);
i--;
}
}
}
}
console.log("---深度搜索---");
dfs(tree);
// 广度搜索,构造一个队列,将子节点按从左到右入队,然后出队访问
function bfs(tree) {
const stack = [];
stack.push(tree); // 根节点入栈
while(stack.length) {
const currentNode = stack.shift(); // 提取数组第一个
console.log(currentNode.data);
if (currentNode.next && currentNode.next.length) {
currentNode.next.forEach(item => {
if(item) { // 非空节点
stack.push(item);
}
});
}
}
}
console.log("---广度搜索---");
bfs(tree);
KMP算法:
// BMP算法
// 主串 ababcabcacbab
// 子串 abcac
// abcac 不行,因为和abcac和abcab不等。取出主串的abca和子串的abc比较,由于匹配到第5个字符不等,前面4个字符相等的,
// 所以可以看做是子串abca部分和去掉末尾以后的abc对比,因为abca同时是主串和子串序列。这样转化的好处是需要往前移到哪个字符对比只和子串自身有关。
// abca
// abc 不行,因为c不等于a,这里就已经有规律了,即往前匹配时,先要保证找到的字符等于末尾,即a的前面一个a
// a 此时只有一个字符a,这样就匹配上了1个字符,避免了还去从第4个字符开始重新匹配。实际上假设前面还有字符,想要匹配上只有可能是c,即ca去和abca的最后两位匹配。
// 这样的例子很容易推导出: 子串等于cabcax,主串为cabcabcax。第一次匹配卡在了主串的第6个字符b时,则前5个字符cabca和ca匹配上。这样由于第4,5位是ca对上了
// 所以下一次匹配是从子串的第三个字符b和主串的第6个字符b对比,当然如果主串的第6个字符不是b,那实际上就从ca开始往前找了,由于c不等于a,找不到相同的部分
// 所以这时就只能从第一个字符c和主串的第6个字符比(例:子串是cabcax,主串是cabcadcabcax,此时主串第6个字符不是b,要从ca往前找)。
// 将子串需要往前移的index下标放在next数组里记录,如abaabc就定义next是6维数组,next[6]表示第6个字符c匹配不上时,要返回的位置。
// 这时候考虑abaab,和ab匹配上,所以next[6]= 3,表示ab不用匹配了,从第三个字符a开始。又比如next[1]表示第一个字符就匹配不上,那只能拿子串的第一个字符重头开始匹配主串的下一个字符。
// next[j]计算思路方式很简单,如果用T表示子串,那么就是往前找到第一个和T[j-1]相等的位置k,然后还需要保证k之前的k-1个字符都和T[j-2]之前的k-1个字符相等。
// 因此next[j+1]=next[101]=k=10,其含义表示:1-10个字和91到100个字相等,即T[j-1]=T[k-1]。求next[j+1]是一个递归的过程,设next[j]=k。如果T[j-1]=T[k],则带上前面k-1个数就相等。
// 此时next[j+1]= next[j]+1。如果S中第j个数不等于第k个数,就去前面找匹配的字符,这时候去找next[k]= t,这时候如果第j个数T[j-1]= T[t-1],说明前面t-1个字带上第t个字T[t-1],一共t个字符是匹配的
// 此时next[j+1] = t + 1。
const str1 = "abaabcac";
// const str = "aba";
function get_next(T) {
let i = 2;
let next = [];
next[1] = 0; // 第一个匹配不上,应该拿子串的第一个字符去匹配主串的下一个字符了。
j = 0; // 保存next[i]的值。
while (i <= T.length) {
if (j == 0) {
next[i] = j + 1; // 第一次进来的时候next[2] = 1; 子串的第二个字符对不上,表示下一次拿子串的第一个去匹配主串的当前字符。
i++;
j++;
} else if(T[i-2] == T[j-1]) {
// 求next[i]时,考虑第i-1个数和第next[i-1]=k个数是否相等
next[i] = j + 1;
j++;
i++;
} else {
j = next[j];
}
}
// next.shift(); // 去掉第一个即可。不建议去,因为j = next[j],j=1时就是死循环,而j需要从1变成0。
return next;
}
console.log(get_next(str1)); // [undefined, 0, 1, 1, 2, 2, 3, 1, 2 ]
const S = "ababcabcacbab";
const T = "abcac";
function Index_KMP(S, T, _pos = 0) {
let j = 0;
let i = _pos;
const next = get_next(T); // 获取next的计算值
console.log("next:", next);
while(i < S.length && j < T.length) { // 匹配没结束
if (S[i] == T[j]) {
// j = 0意味着也要重新开始匹配
i++;
j++;
} else if(j == 0) {
i++;
} else {
j = next[j];
}
}
if(j == T.length) {
return i - j + 1;
} else return -1;
}
console.log(Index_KMP(S,T));
零钱兑换问题(动态规划):
// 给定一个零钱兑换的例子,其中有不同面额的零钱,如1元,5元,10元,
// 求解如何用这些零钱兑换一个给定的金额,并打印出所有可能的兑换方案。
const coins = [1, 2, 5]; // 面额
const amount = 5; // 总额
// 类似于数结构
const arr = [];
function dfs(amount, depth = []) {
if(amount == 0) {
arr.push(depth);
} else if(amount > 0) {
for(let coin of coins){
if(amount - coin >= 0) {
dfs(amount - coin, [...depth, coin]);
}
}
}
}
dfs(amount);
console.log(arr);
二叉排序树:
/**
* 二叉排序树
*/
class BSTNode {
constructor(data) {
this.key = data; // 索引,用于排序
this.data = data;
this.lchild = null;
this.rchild = null;
}
}
function insertBST(arr) {
let tree = null;
for (let i = 0; i < arr.length; i++) {
let current = new BSTNode(arr[i]);
// 根节点初始化
if(tree == null) {
tree = current;
} else {
// 递归插入节点
insertNode(tree, arr[i]);
}
}
return tree;
// 复杂类型也一定要通过T.lchild = xxx的方式去赋值,T = T.lchild, T = xxx不起作用
function insertNode(T, element) {
if(!T) {
return;
} else {
// element比节点数据小,尝试放在左子树
if(T.data > element) {
if(T.lchild) {
// 去跟左孩子比较
insertNode(T.lchild, element);
} else {
// 左孩子为空,创建左孩子节点
T.lchild = new BSTNode(element);
}
} else if(T.data < element) {
if(T.rchild) {
insertNode(T.rchild, element);
} else {
T.rchild = new BSTNode(element);
}
}
}
}
}
let list = [45,61,24,78,3,100,53,12,90,37];
const BSTree = insertBST(list); // 排序树
const result = []; // 中序遍历结果
function centerTravesal(root) {
if(root) {
centerTravesal(root.lchild);
result.push(root.data);
centerTravesal(root.rchild);
}
}
centerTravesal(BSTree);
console.log(BSTree);
console.log(result);
常见的算法有深度广度搜索,归并、选择、堆排序, 在react调度器中使用堆排序获取优先级最高的任务
18 Diff算法
主要是对同级节点进行复用,通常加了key值以后,React Diff算法根据相同的key去做patch修改,如果有相同的key的两个组件类型(如div)也一样,就直接复用,此时组件内如果改了文本,直接执行修改文本操作。因此如果有4个div,key值给的是索引,里面的文本分别是1,2,3,4。删除第2个div,然后遍历[1,3,4]去展示,展示的分别是1,3,4依然能保证正确。第2个节点进行了复用,但是文本从2->3,第3个节点也会复用,从3->4,但是如果是input框里的输入会显示错误(因为这里真的是复用了value)。key只是告诉react我们期望的复用组件,react还会根据这个key去对比实际内容。
比较过程:如旧的是ABCD,新的是BACD。B在旧的里在第二个位置,这时候就会把第二个位置前面的都移到B后面(index和newIndex取最大值就是这个意思),因此如果是ABCD改成DABC,那么就会把旧的ABC依次放在D后面执行3次移动,而不是D移到ABC执行一次,这就是diff的缺点。如果面试官问你diff怎么优化,就回答可以先求出最长公共子串,然后把将剩下的进行移动,如ABCD和DABC的公共子串是ABC,那么这三个的顺序一致可以复用就不动,仅移动D。
19 react常见面试问题
高阶组件,无状态组件,受控组件和非受控组件的概念。执行上下文createContext解决子组件通信问题。apply和call的区别以及第一个参数通常是实例对象obj。this的指向问题包括执行普通函数Fun(),此时谁调用this就指向谁,和执行let a = new Fun(),this指向实例对象a。使用propsType定义组件props的类型。ts中的常见用法以及泛型。
react redux工作流程如react-thunk中间件判断组件里发出的actions是对象还是函数,对象的话直接dispatch,函数的话通常是请求数据然后去dispatch,这些dispatch先经过store,再到reducer计算state的值,然后调用getState()得到新的state值做为props去更新组件。
class继承的关键在于执行super()方法。
延迟加载defer和async的区别:相对于HTML的解析,他们的加载都是和html的解析(GUI渲染线程执行html解析,和js线程互斥)并行的,区别在于执行。defer中的js代码在dom加载完后才会执行不会阻塞html解析,async加载完就会执行js,并且执行js时会阻塞html解析。不加defer和async的话那就是顺序解析,遇到js就执行完js再去解析下一步。
预加载和预获取:prefetch的作用是该文件要等浏览器空闲时下载,而preload是正常跟父包一起下载,但两者都不会阻塞渲染,其中prefetch使用率更高。
点击需要显示一个组件,组件有用到第三方库,可以动态引入这个组件,通过注释的方式加上prefetch预获取。
在写一个按钮,点击显示Com组件,
浏览器从缓存里获取文件:
tsconfig配置的坑:
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "esnext",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}
1.module是esnext而不是es6,如果你想使用动态import的话,即import("lodash").then()这种。
2. "allowSyntheticDefaultImports": true, "esModuleInterop": true,帮助你可以import _ from "lodash";否则只能import * as _ from "lodash";
20 nodejs面试
nodejs全局变量:global变量,process进程变量,console,几个定时器。
内置模块:os模块,path(resolve相对路径转绝对,__dirname获取绝对路径),url,http模块,fs模块(readDir,stat判断是否是文件,readFile,append追加,unlink删除)。
require('')的过程:先计算绝对路径,然后判断是否在缓存里,没有的话判断是否是内置模块、npm安装的模块、自己写的模块,然后实例化模块放入缓存。然后加载模块,根据扩展名用不同的解析函数解析,export出来。commonjs暴露出来的值类型是深复制的,es暴露出来的值类型是同一个引用。
21 手写bind
Function.prototype.myBind = function () {
// 处理函数
let args = Array.from(arguments);
let thisArg = args.shift();
// 暂存this
let thisFunc = this;
// 因为需要构造函数,所以不能是匿名函数了
const fBound = function () {
const newArgs = args.concat(Array.from(arguments));
// 判断是否为构造函数
thisArg = this instanceof fBound ? this : thisArg;
return thisFunc.apply(thisArg, newArgs);
}
// 直接将原函数的prototype赋值给绑定函数
fBound.prototype = this.prototype;
// 返回
return fBound;
}
面试常见问题:
hook和class组件的区别
为什么不能在if里面使用hook?
state是同步的还是异步的?取决于执行环境。后面有讲,仔细看。
使用哪些hook: useEffect,useCallback(返回函数), useMemo(返回函数执行结果)。避免子组件重复渲染:使用pureComponent或者useMemo。
自适应屏幕:width:100%, 100vw,flex布局。注意flex的三个意思。
qiankun微服务原理
第三方sdk的使用:打印构造函数名称,查看隐式原型__proto__,里面有所有的方法。
优化方案:
优化postcss计算rem方案,统一转化为750px二倍稿,项目里使用rem代替px。
webpack5官方给出的常用的代码分离方法有三种:
- 入口起点:使用 entry 配置手动地分离代码。配置较多·,隐患多。
- 防止重复:使用 入口依赖 或者 SplitChunksPlugin 去重和分离 chunk。
- 动态导入:通过模块的内联函数调用分离代码。
闭包:外部作用域通过内部作用域返回的函数,访问内部作用域的闭包变量,优点是避免全局污染,缺点是造成内存泄漏,闭包访问的内部变量不能及时回收。内存泄漏只要是定时器未清除,DOM引用未清除,全局变量一直在内存中,console.log的使用,闭包用的太多造成的泄漏。
const add = function (a, b) {
return a + b;
};
function addOne(num) {
const one = 1;
return function inner() {
return add(num, one);
};
}
const result= addOne(2);
console.log(result());
写过哪些组件: 早期的上拉刷新组件,swiper组件,表单+列表组件。弹幕组件,维护公司组件库。定制上传组件,验证码组件,antdesign+主题色方案。
强缓存协商缓存:
第一次请求获取cache-control,Last-modify、Etag等字段。
cache-control设置max-age=设置强缓存时长(s)。表示有效时间内走强缓存,超过时间走协商缓存,拿着第一次返回的Last-modify的值作为If-Modify-Since的值(简单理解就是换个名字)传给服务器,服务器拿If-Modify-Since和资源最后修改的时间对比,没变化返回304且不返回资源,继续走浏览器缓存(内部缓存或者磁盘缓存)。
ETag/If-None-Match与Last-modify/If-Modify-Since类似,ETag是文件唯一标识符,文件资源发生变化ETag就会变化,ETag弥补了上面last-modified
可能出现文件内容没有变化但是last-modified
发生了变化出现重新向服务器请求资源情况。这个值也是服务器返回的。
dist/index.html:强缓存
css,js,图片:协商缓存,文件带上contenthash
从0-1设计一个react项目:
1.选择webpack,配置里面的loader和plugin根据package.json里的命令区分环境,可使用cross.env
2.本地使用devserver在内存中运行文件,有哪些文件可以在控制台network里请求。
3. babel-loader编译es6语法,具体配置在babel.config.json(.babelrc)里,支持react的话也需要在里面加上。
4. ts-loader编译ts,具体配置在tsconfig.json,如ts里需要动态引入的话,就是在这个文件加两个属性。
5. react-router的配置
6.网络请求的封装如axios
7.组件库的选择如antdesign
8.如不用自己webpack配置,可以使用umi搭建spa项目,nextjs搭建ssr项目。
http状态码:301,302重定向。400前端语法错误,401未授权,403拒绝访问。404请求不到资源。500后端出错。304与协商缓存有关。
useState传函数递增和传值递增的区别:useState是可以批量合并的(在promise或者定时器里使用的话,还跟react版本有关,据说17和之前这里不会合并),传值的话,实际效果相当于只加一次,传函数可以在更新state队列时将上一次的当作参数获取到,做到真正的递增。state在settimeout有闭包问题,这时候useState要传函数。所以useState到底是同步还是异步的,主要看执行环境,一般情况下在一个点击事件里写是异步的,拿的是旧值,但是如果是
setState(v => v+1);
setState(v => v+1);
第二行的参数v就是+1以后的值,此时在函数体v=> v+1内可以认为是同步的,因为这种写法按照作用域来理解,直接设置值是从你写的业务组件去获取,传一个函数进去,是通过底层callback回调函数的方式传给你的,按照作用域链找发生了变化。如果是useEffect和useCallback加了依赖也可以认为是同步的即生命周期里可以认为是同步,另外传给一个子组件作为他的props也是同步的。如果想把异步变成同步,可以用useRef记一份值。
antd5:使用cssinjs动态加载css。
mac上非安全模式处理跨域:
open -n /Applications/Google\ Chrome.app/ --args --disable-web-security --user-data-dir=/Users/yjx/MyChromeDevUserData/
项目经验:使用echarts和datav开发数据大屏,通过图表组件展示客户公司员工职位、年龄、司龄和院校类别等基本信息,按照近半年和近一个月等多个维度统计月入职数和入职城市分布、月离职人数和离职原因。展示公司人才库里人员大学专业、学历和技术栈等信息。支持i18n国际化翻译,将服务端数据和静态数据都统一写入翻译文件里,然后按年收取维护费,为公司带来实际效益。
具体优化:react-dev-inspector插件将组件路径插入到dom节点上。在开发数据大屏使用umi里的useModel共享数据和状态,并作为大量跨层级节点进行通信的解决方案。fiddler抓包时,需要安装ca证书抓取https的证书。
nextjs使用动态导入的方式接入高德地图。获取地体位置需要dev环境开启https,需要升级next,react等版本
开发支付宝小程序:使用Remax框架
2025.7.10获得算法工程师offer