浏览器、js基础
proxy和defineProperty
- proxy能拦截到 对象所有的 基础属性
- 数组的 push、pop、shift、unshift、splice、sort,reverse是无法触发 set 方法的
Vue 2.0 中能对数组的这些方法监听到是因为 Vue 源码对数组的这些方法进行了重载
事件循环
事件循环又叫消息循环,是浏览器渲染主线程的工作方式。遇到异步任务,放入队列中,等空闲时从消息队列中取出第一个来执行。
定时器能精确计时吗
不能, 原因: 1. 受事件循环的影响,计时器的回调函数只能在主线程空闲时执行,因此会带来偏差。 2. 按照W3C的标准,浏览器实现计时器时,如果嵌套层级超过5层,会有至少4毫秒延迟。而且计算机硬件没有原子钟,本身就无法做到精确计时。
异步
js是一门单线程语言,这是因为它运行在浏览器的渲染主线程中,且渲染主线程只有一个,而渲染主线程承担着很多任务,如果使用同步的方式,就极有可能导致主线程产生阻塞,所以当定时器、网络请求、事件监听等这些任务产生时,主线程将任务交给其他线程处理,自身立即结束任务的执行,转而去执行后续任务,当其他线程完成时,将事先传递的回调函数包装成任务,加到事件队列的末尾,等待主线程的执行。
垃圾回收机制
浏览器的垃圾回收机制是自动管理和释放内存的机制,主要用于处理不在使用的变量和资源,以免造成内存泄漏,提高浏览器性能和响应能力。
- 引用计数法:每一个对象都有一个引用计数器,当对象被引用时,计数器加1,当引用被移除时,计数器减1,当计数器为0时,则判定该对象不再被使用,可以回收;
- 标记清除法:从一组根元素开始,递归遍历这些元素,标记所有从根元素可到达的对象,未被标记的对象即为垃圾数据,随后被清除,这种方产生内存碎片;
- 标记整理法:标记阶段后,将存活的对象压缩到内存的一边,整理出连续的内存空间,减少内存碎片。 及时回收不必要的内存,减少内存不足而引起页面卡顿和崩溃的风险,提升用户体验
在JavaScript中,以下情况可能会导致对象无法被垃圾回收机制回收:
- 闭包:闭包是一种可以访问其他函数作用域变量的函数。如果一个闭包引用了外部的变量或对象,那么这些变量或对象就无法被垃圾回收,直到闭包不再使用。
- 对象属性引用:如果一个对象属性引用了其他对象,那么这些对象就无法被垃圾回收。例如,如果一个对象包含对另一个对象的引用,那么被引用的对象就无法被垃圾回收,直到引用的对象不再使用。
- 全局变量:全局变量是在整个程序中都可以访问的变量。如果一个全局变量引用了某个对象,那么该对象就无法被垃圾回收,直到全局变量不再使用。
- 事件监听器:如果一个事件监听器引用了某个对象,那么该对象就无法被垃圾回收。例如,如果一个按钮的事件监听器引用了某个对象,那么该对象就无法被垃圾回收,直到事件监听器不再使用。
- 定时器:如果一个定时器引用了某个对象,那么该对象就无法被垃圾回收。例如,如果一个定时器回调函数引用了某个对象,那么该对象就无法被垃圾回收,直到定时器不再使用。
输入url到页面展示发生了什么
- 解析URL,查找当前DNS缓存记录,并比较缓存是否过期。
- DNS域名解析,得到对应IP
- 根据IP建立TCP连接(三次握手)
- 建立链接后发起HTTP请求
- 服务器响应HTTP请求,浏览器得到响应
- 浏览器解析HTML代码,页面渲染(生成DOM、cssom树、样式计算、分块、分层、光栅化、画)
- 服务器关闭TCP链接
ES6 symbol如何使用以及使用场景
ES6中的Symbol是一种特殊的类型,它表示一个独一无二的值。它的主要用途是作为对象的属性键,以避免属性名的冲突。
使用Symbol作为属性键的基本方法如下:
let symbolKey = Symbol();
let obj = {
[symbolKey]: 'value',
};
在这个例子中,symbolKey 是一个Symbol类型的变量,我们使用它作为对象 obj 的属性键。注意到这里使用了方括号 [] 而不是点号 . 来访问对象的属性,这是因为Symbol类型不能直接用点号访问。
Symbol的使用场景通常是在需要唯一标识符的时候。例如,你可能有一个对象,这个对象由多个模块构成,每个模块都有自己的键值对。使用Symbol可以确保每个键都是独一无二的,避免因冲突导致的问题。另一个使用场景是在需要确保对象属性不被外部访问或修改的时候,Symbol的不可变性使得它成为一个很好的选择。
然而,需要注意的是,虽然Symbol可以保证值的唯一性,但它并不适用于需要大量生成唯一标识符的情况。在这种情况下,应该使用其他方法,如使用时间戳或UUID等生成唯一标识符。
http1.1和http2的区别,http3呢?
HTTP/1.1和HTTP/2主要有以下区别:
- 传输方式:HTTP/1.1采用报文形式传输,而HTTP/2采用二进制传输,进行二进制分帧。
- 使用协议:HTTP/1.1使用的协议是http+tcp,而HTTP/2使用的协议是http+hpack+stream + tls1.2。
- 压缩算法:HTTP/1.1没有特定的压缩算法,而HTTP/2使用的是hpack压缩头部信息。
- 通信方式:HTTP/1.1是半双工的,仅有一方可以主动发起请求。而HTTP/2是全双工的,客户端和服务器都可以主动发起请求。
- 并发请求:HTTP/1.1每个请求需要建立连接,等待服务器处理请求并返回响应。HTTP/2可以将多个请求分解为互不依赖的帧,交错发送,并行发送多个请求,请求之间互不影响,并行发送多个响应,响应之间互不影响。
总的来说,HTTP/2在很多方面对HTTP/1.1进行了优化和改进,包括提高传输效率、减少延迟、增加并发请求等,以提供更好的网络性能和用户体验。
HTTP/3主要是为了解决HTTP/2传输相关问题而在所有形式的设备上提供快速、可靠和安全的Web连接。它使用了一种名为QUIC的不同传输层网络协议,该协议最初由Google开发。HTTP/3与HTTP/2类似,但在传输层协议上进行了改进,使用无连接的UDP而不是面向连接的TCP。这意味着HTTP/3具有更高的性能和效率,特别是在高并发请求和网络不稳定的情况下。
监听元素是否在可见区域(可用于上拉加载)IntersectionObserver
// 创建一个IntersectionObserver实例
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 元素进入可视区域
console.log('Element is visible:', entry.target);
} else {
// 元素离开可视区域
console.log('Element is not visible:', entry.target);
}
});
});
// 选择需要观察的目标元素并开始观察
const target = document.getElementById('my-element');
observer.observe(target);
React
react fiber
React Fiber是React框架中一项重大的架构改进,旨在改善React的渲染性能和用户体验。下面是React Fiber的发展演进:
- React 15及之前版本:React的早期版本使用了基于栈的调和算法,称为"Stack Reconciler"。这种调和算法在处理大型组件树或复杂交互时容易出现性能问题,导致用户界面的卡顿和响应性差。
- React 16及引入Fiber:React 16引入了Fiber架构,这是一个全新的调和算法和渲染引擎。Fiber的目标是实现增量渲染和可中断的渲染,以提高React应用的响应性和渲染性能。
- 引入异步渲染:Fiber架构使React能够支持异步渲染,将渲染任务分解为多个优先级不同的子任务,使得React可以更好地响应用户输入,并在空闲时间执行优先级较低的任务。
- 优先级调度和任务切片:Fiber将渲染任务切片为多个小任务,并为每个任务分配优先级,可以根据任务的优先级动态调整任务的执行顺序,以更好地响应用户操作和保持页面的流畅性。
- 错误边界:Fiber架构引入了错误边界的概念,使得React应用能够更好地处理运行时错误,避免整个应用崩溃,并提供优雅的错误处理和回退机制。
- 生命周期重构:Fiber架构对React组件的生命周期进行了重新设计和重构,引入了新的生命周期方法,并提供了更细粒度的控制和更好的性能优化。
为什么react需要fiber&时间分片而vue没有
Diff算法
主要是用于比较两个虚拟DOM树的差异,并以最小的操作代价将旧的DOM树更新为新的DOM树。以下是Diff算法的核心原理和步骤:
-
同层对比:
- Diff算法的比较只会在同层级进行,不会跨层级比较。它会比较两个节点的子节点,而不会比较父节点或兄弟节点。这种策略使得Diff算法能够以相对较小的计算量来找出节点之间的差异。
-
节点类型比较:
- 首先比较新旧节点的类型。如果类型不同,直接替换整个节点及其子树。如果类型相同,则继续比较它们的属性和子节点。
-
属性比较:
- 对于类型相同的节点,会进一步比较它们的属性(如id、class等)。如果属性有变化,则更新相应的属性。
-
子节点比较:
- 子节点的比较是Diff算法的重点和复杂之处。常见的策略有双指针比较和key值比较。双指针比较是从新旧子节点列表的头部和尾部同时开始比较;而key值比较则是通过为节点设置唯一的key属性,以便更准确地识别节点的变化,避免不必要的节点移动。
-
处理新增和删除:
- 如果新节点列表中有新增的节点,则将其添加到适当的位置。如果旧节点列表中有不再存在于新列表中的节点,则将其删除。
-
最小化操作:
- Diff算法的目标是尽量减少DOM操作,例如通过修改节点属性而不是删除和重新创建节点来更新DOM,以此提高应用的性能和响应速度。
useEffect实现原理
useEffect是React中的一个生命周期函数,它允许你在组件渲染后执行副作用操作。useEffect的实现原理基于React的异步调度机制。
在useEffect被调用时,它接收一个回调函数作为参数,该回调函数包含执行副作用的逻辑。当组件渲染完成后,useEffect会将回调函数放入一个异步队列中。这个队列会在组件卸载前一直存在,并且会按照回调函数被放入队列的顺序依次执行。
在队列中的回调函数会在浏览器执行绘制前被执行,这使得它们可以在浏览器更新屏幕前执行一些副作用操作,比如设置订阅、处理事件等。由于useEffect的回调函数是在组件卸载前执行的,因此它们也可以用于清理副作用产生的状态和资源。
为了实现异步调度,React利用了scheduler库中的异步调度函数scheduleCallback。当useEffect的回调函数被放入队列后,React会调用scheduleCallback来安排回调函数的执行。这个函数会返回一个Promise对象,该对象会在回调函数执行完成后被解析。
在实现上,React将useEffect的回调函数放入一个任务队列中,这个队列会被存储在组件的实例上。当组件卸载时,React会清除这个队列以避免内存泄漏。同时,React还提供了一些生命周期方法来处理队列中的任务,比如componentDidMount和componentDidUpdate。这些方法会在组件的生命周期相应的事件触发时执行,并且会按照回调函数被放入队列的顺序依次执行。
总之,useEffect的实现原理是基于React的异步调度机制,通过将回调函数放入任务队列并利用scheduleCallback实现异步调度。这个机制使得我们可以在组件渲染后执行副作用操作,并且可以方便地进行清理操作。
useLayoutEffect和useEffect
useEffect是异步执行的,而useLayoutEffect是同步执行的。useEffect的执行时机是浏览器完成渲染之后,而useLayoutEffect的执行时机是浏览器把内容真正渲染到界面之前,和componentDidMount等价。
虚拟DOM
它是一层对真实DOM的抽象,以JavaScript 对象 (VNode 节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上
- 提高性能:浏览器处理 DOM 很慢,处理 JS 对象很快 。 使用虚拟 DOM,可以避免频繁地对实际 DOM 进行操作,从而减少浏览器的重绘和回流,提高应用程序的性能和效率。
- 简化开发:原生JS很多时候要关注 DOM 操作,对于虚拟 DOM 开发者只需要关注数据和状态的变化,而不必考虑如何手动更新 DOM 。React 会负责一切与 DOM 相关的操作,包括处理事件、调整布局、更新样式等。
- 跨平台:由于虚拟 DOM 只是一个 JavaScript 对象,因此它可以在不同的平台上运行,例如Web、iOS 和 Android 等。这使得开发人员可以在不同的平台上使用相同的代码库,并且只需根据需要使用不同的渲染器即可。
虚拟Dom一定能提升性能吗
不一定! 虚拟 DOM 的性能优势主要在于能够减少实际 DOM 的操作次数。但是,如果应用程序本身的复杂度不高或者虚拟DOM的实现方式不够优秀,可能无法带来性能提升,甚至会引入额外的性能开销。
它的优势是在于 diff 算法和批量处理策略,将所有的 DOM 操作搜集起来,一次性去改变真实的 DOM ,但在首次渲染上,虚拟 DOM 会多了一层计算,消耗一些性能,所以有可能会比 html 渲染的要慢
总之,虚拟DOM在适当的情况下可以提高性能,但并不是一定能够提升性能,需要根据实际情况进行评估。
react.memo适合用在什么场景下?
React.memo是React中的一个高阶组件(Higher-Order Component),用于优化组件的渲染性能。React.memo可以用于包裹函数组件,用于对组件进行浅层的props比较,如果前后props没有变化,则会使用缓存的组件渲染结果。
React.memo适合用在以下场景下:
- 当组件的渲染开销较大,但组件的props没有变化时,可以使用React.memo进行性能优化,避免不必要的重新渲染。
- 当组件的props是通过父组件传递的,而父组件的渲染频率较高,但子组件的props并不频繁变化时,可以使用React.memo来避免子组件的重复渲染。
需要注意的是,React.memo仅进行浅层的props比较,如果传递给组件的props是复杂的对象或数组,且其内部发生了变化,React.memo可能无法正确地检测到变化。在这种情况下,可以考虑使用深层比较或Immutable数据结构来确保正确的比较和渲染。
React memo 和 useMemo
相同点:
它们都可以用来缓存数据,避免子组件的无效重复渲染。
不同点:
React.memo是一个高阶组件,useMemo是一个hook。
React.memo、useMemo、useCallback、useRef都是React进行性能优化的手段,不过我们一定要记得合理运用,不能过度使用,因为深究这几个方法的实现其实都是借助了闭包,会一直占用我们的内存,运用不当可能会导致反向的性能优化问题~
usememo和usecallback
useMemo和useCallback都是React Hook提供的API,用于优化性能。它们的主要区别在于:
useMemo接收的参数中,第一个参数是一个回调函数,第二个参数则是一个依赖的数据数组。当依赖的数据数组中的任何一个数据发生改变时,都会重新调用传入的回调函数进行计算,并将计算结果缓存起来。它主要用于缓存计算结果的值,例如需要计算的状态。useCallback同样接收两个参数,第一个是一个回调函数,第二个是一个依赖的数据数组。不同的是,useCallback缓存的结果是函数,主要用于缓存函数。在组件每次更新时,一些函数是没有必要更新的,此时应该使用useCallback进行缓存,这样可以提高性能,减少对资源的浪费。同时,如果需要配合React.memo使用,useCallback也是必不可少的。
总的来说,useMemo和useCallback都是为了优化React组件的性能而设计的,通过缓存数据和函数来减少不必要的计算和渲染。
React状态管理
Rudux、MobX、Zustand、Recoil
Redux基于函数式编程思想实现,集中式状态管理仓库,即一个项目中通常只定义一个store。Redux的繁琐和冗长的代码编写方式一直以来都是开发者们所诟病的。为了解决这个问题,Redux官方推出了Redux Toolkit
Zustand是基于hooks的状态管理解决方案,原理和redux类似,但它不是集中式管理的。
redux的设计思想
- Web 应用是一个状态机,视图与状态是一一对应的。
- 所有的状态,保存在一个对象里面(唯一数据源)。
redux的流程
-
创建Action:Action是一个用于描述发生的事件的纯对象。它必须包含一个用于描述类型的type字段。可以通过Action Creator函数来创建Action。
-
触发Action:通过调用Redux的dispatch函数来触发Action,将Action发送给Redux的Store。
-
更新Store:Redux的Store接收到Action后,会将其传递给Reducer进行处理。Reducer是一个纯函数,用于根据Action的类型和数据更新Store中的状态。
-
更新View:当Store的状态发生变化时,Redux会通知相关的组件进行重新渲染,使得View与更新后的Store状态保持一致。
-
获取State:组件可以通过调用Redux的getState函数来获取当前的Store状态。
-
订阅State变化:Redux提供了subscribe函数,组件可以通过订阅来监听Store中状态的变化,当状态发生变化时执行相应的操作。
Redux 异步数据流方案对比
Redux 本身只会处理同步的简单对象 action,但可以通过 redux-thunk 拦截处理函数(function)类型的 action,通过回调来控制触发普通 action,从而达到异步的目的
采用 redux-saga 可以保持 action 和 reducer 的简单可读,逻辑清晰,通过采用 Generator ,可以很方便地处理很多异步情况,而 redux-saga 的缺点就是会新增一层 saga 层,增大上手难度;Generator 函数代码调试也比普通函数更复杂
[Dva]是一个基于 [redux] 和 [redux-saga] 的数据流方案,umi用的就是这个
- state是存储状态的
- effects对象里面放得是ajax方法 使用迭代器
- reducers存放的是改变状态state的方法
为什么要在React.js中使用Immutable
- 它是一个完全独立的库,无论基于什么框架都可以用它。意义在于它弥补了
Javascript没有不可变数据结构的问题 - 由于是不可变的,可以放心的对对象进行任意操作。在
React开发中,频繁操作state对象或是store,配合immutableJS快、安全、方便。 熟悉React.js的都应该知道,React.js是一个UI = f(states)的框架,为了解决更新的问题,React.js使用了virtual dom,virtual dom通过diff修改dom,来实现高效的dom更新。但是有一个问题。当state更新时,如果数据没变,你也会去做virtualdom的diff,这就产生了浪费。这种情况其实很常见,使用immutable.js可以解决这个问题。
Immutable 优点
Immutable降低了Mutable带来的复杂度 可变(Mutable)数据耦合了Time和Value的概念,造成了数据很难被回溯- 节省内存
Immutable.js使用了Structure Sharing会尽量复用内存,甚至以前使用的对象也可以再次被复用。没有被引用的对象会被垃圾回收
webpack、微前端
微前端优点
- 解耦: 微前端架构可以将大型项目分解为多个可以独立开发、测试和部署的小型应用。这种解耦可以提高开发效率,减少团队间的协调成本。
- 技术栈无关: 不同的微前端应用可以使用不同的技术栈,这为使用新技术、升级旧技术提供了可能。
- 并行开发: 因为微前端应用是独立的,所以多个团队可以并行开发不同的应用,无需担心相互影响。
- 独立部署: 每个微前端应用可以独立部署,这意味着可以更快地推出新功能,同时降低了部署失败的风险。
wujie、qiankun
webpack模块联邦在微前端的应用
- 模块共享
Webpack 5 的联邦模块允许不同的微前端应用之间共享模块,避免重复加载和代码冗余。通过联邦模块,我们可以将一些公共的模块抽离成一个独立的模块,并在各个微前端应用中进行引用。这样可以节省资源,并提高应用的加载速度。
// main-app webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
// ...其他配置
plugins: [
new HtmlWebpackPlugin(),
new ModuleFederationPlugin({
name: 'main_app',
remotes: {
shared_module: 'shared_module@http://localhost:8081/remoteEntry.js',
},
}),
],
};
// shared-module webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
// ...其他配置
plugins: [
new ModuleFederationPlugin({
name: 'shared_module',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
},
}),
],
};
在上述示例中,main-app 和 shared-module 分别是两个微前端应用的 webpack 配置文件。通过 ModuleFederationPlugin 插件,shared-module 将 Button 组件暴露给其他应用使用,而 main-app 则通过 remotes 配置引入了 shared-module。
- 动态加载
Webpack 5 联邦模块还支持动态加载模块,这对于微前端应用的按需加载和性能优化非常有用。通过动态加载,可以在需要时动态地加载远程模块,而不是在应用初始化时一次性加载所有模块。
// main-app
const remoteModule = () => import('shared_module/Button');
// ...其他代码
// 在需要的时候动态加载模块
remoteModule().then((module) => {
// 使用加载的模块
const Button = module.default;
// ...
});
在上述示例中,main-app 使用 import() 函数动态加载 shared_module 中的 Button 组件。通过动态加载,可以在需要时异步地加载远程模块,并在加载完成后使用模块。
在微前端应用中可以实现模块共享和动态加载,提供了更好的代码复用和可扩展性。通过模块共享,可以避免重复加载和代码冗余,而动态加载则可以按需加载模块,提高应用的性能和用户体验。
webpack的chunkhash和contentHash的区别
chunkHash和contentHash是Webpack在生产环境中用于管理和优化代码的两个重要概念。二者的区别如下:
chunkHash主要用于生产环境,它是根据入口文件(Entry)解析依赖文件,构建对应的chunk,并生成对应的哈希值。具体来说,Webpack会根据入口entry配置文件来分析其依赖项并由此来构建该entry的chunk。不同的chunk会有不同的hash值。在生产环境中,我们会把第三方或者公用类库进行单独打包,所以不改动公共库的代码,该chunk的hash就不会变,可以合理的使用浏览器缓存。但是,只要同一个chunk里面的js修改后,css的chunk的hash也会跟随着改动。
contentHash是表示由文件内容产生的哈希值,内容不同产生的contentHash值也不一样。它是为了解决chunkHash在生产环境中遇到的问题而引入的。在生产环境中,通常做法是把项目中css都抽离出对应的css文件来加以引用。这种方法的缺点是只要css文件内容有任何改变,哪怕只是微小的改变,都会导致css的hash值发生改变,进而导致css的重新构建。使用contentHash后,只要css文件内容不变,就不会重复构建。
总结来说,chunkHash和contentHash的主要区别在于它们的应用场景和作用方式。chunkHash主要用于生产环境中对整个 chunk 进行哈希,而 contentHash 主要是用于保证即使某些文件内容发生改变,只要文件内容不变,就不会重复构建。
webpack中处理image的loader
Webpack中处理图片的loader主要是file-loader或者url-loader。
file-loader是用来处理所有类型的文件的,当然也包括图片文件。当使用file-loader处理图片文件时,它会将图片文件复制到输出目录中。
url-loader则可以用来处理图片文件,并且可以控制图片文件的大小。如果图片文件的大小小于指定的限制,那么url-loader将不会将图片文件复制到输出目录中,而是会将其转换为base64编码的字符串,并添加到CSS样式中。这种方式可以有效地减小CSS文件的大小。如果要使用url-loader来处理图片文件,需要在配置文件中添加以下代码:
{
test: /.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10000,
outputPath: 'images',
},
},
],
}
在上面的代码中,limit参数表示图片文件的大小限制,单位是字节。如果图片文件的大小小于该限制,那么图片将被转换为base64编码的字符串;否则,图片将被复制到输出目录中。outputPath参数表示输出目录的路径。
commonjs和ES6的区别
- commonjs的require语法是同步的
- commonjs输出的是一个值的拷贝,es6输出的是一个值的引用(模块之间的引用关系是内存的地址引用)
- commonjs是运行时加载,es6是编译时,所以es6可以tree-sharking
commonJS的本质
最本质的就是require函数:
- 根据传入的模块路径,获取完整的绝对路径
- 判断缓存
- 真正运行的是辅助函数_require(exports, require, module,...)
- return module.exports
webpack怎么将多个css合并成一个
Webpack 可以通过使用插件来将多个 CSS 文件合并成一个。常用的插件是 mini-css-extract-plugin 和 css-loader。
webpack的树摇对commonjs和es6 module都生效吗
Webpack的Tree Shaking对ES6 Module生效,但对CommonJS不起作用。
这是因为Tree Shaking是针对静态结构进行分析,只有import和export是静态的导入和导出。而CommonJS有动态导入和导出的功能,无法进行静态分析。
webpack proxy
Webpack的proxy功能主要通过配置反向代理来实现,可以解决前端开发中的跨域问题。其工作原理是,在Webpack的配置文件中,可以通过配置devServer.proxy选项来设置代理。代理服务器会拦截前端发送的请求,并将请求转发到目标服务器。在转发请求时,代理服务器会修改请求的域名、端口等信息,以实现跨域请求。目标服务器接收到代理服务器转发的请求,并返回响应结果。代理服务器将响应结果返回给前端应用程序。这样,代理服务器就像一个桥梁,连接了前端和后端,使得两者可以通信,从而解决了跨域问题。
webpack打包原理
-
简单需求
- 浏览器不支持ES6的模块
-
核心打包功能
// 打包工作的基本流程如下: 1. 需要读到入口文件里的内容 2. 分析入口文件,递归的去读取模块所依赖的文件内容,生成AST语法树 3. 根据AST语法树,生成浏览器能够运行的最终代码-
获取模块内容
-
分析模块内容
- 安装@babel/parser包(转AST)
-
对模块内容处理
- 安装@babel/traverse包(遍历AST)
- 安装@babel/core和@babel/preset-env包(Es6转Es5)
-
递归所有模块
-
生成最终代码
-
webpack常见的loader,解决了什么问题
Webpack常见的loader及其解决的问题如下:
- style-loader:将样式加载到JS中,解决样式加载的问题。
- babel-loader:将ES6代码转化为ES5代码,解决浏览器兼容性问题。
- sass-loader:处理CSS预处理器,如Sass,能更好地编写CSS。
- postcss-loader:用于补充css样式在各种浏览器的前缀,很方便,不需要手动写了。
- eslint-loader:用于检查代码是否符合规范,是否存在语法错误。
- url-loader:处理图片类型资源,可以根据图片的大小进行不同的操作,如果图片大小大于指定大小,则将图片进行资源打包,否则将图片转换为base64格式字符串,再把这个字符串打包到JS文件里。
此外,还有一些其他的loader,例如ts-loader、file-loader等,用于处理不同类型的文件和满足不同的开发需求。
webpack常见的plugin,解决了什么问题
Webpack中常见的plugin及其解决的问题如下:
- HtmlWebpackPlugin:这个插件会创建一个html文件,并且把webpack编译的所有输出脚本自动插入到这个html文件中,解决了多个入口文件生成多个html文件的问题。
- MiniCssExtractPlugin:将CSS从主应用程序中分离,以实现更高效的加载和缓存。解决了CSS和JS分离的问题。
- CleanWebpackPlugin:在每次构建之前清除
/dist文件夹,以确保只会生成用到的文件。解决了构建过程中冗余和无效文件的问题。 - TerserPlugin:用于压缩JavaScript,通过混淆变量名和移除无用的代码来减小文件大小。这可以使得JavaScript文件加载更快,同时也可以保护源代码。
- OptimizeCSSAssetsPlugin:用于优化CSS,通过CSS压缩和最小化减少文件大小。这可以提高CSS的加载速度。
- CopyWebpackPlugin:将单个文件或整个目录复制到构建目录。解决了需要在构建后的目录中复制文件的问题。
- HotModuleReplacementPlugin:用于实现模块热替换(HMR),在运行时更新各种模块,无需进行完全刷新。这可以提高开发效率。
以上是Webpack中一些常见的plugin及其解决的问题,当然,Webpack还有许多其他的plugin,可以满足不同的开发需求。
loader和plugin的区别?编写loader和plugin的思路
Loader和Plugin是Webpack的两个重要概念,它们都可以用来扩展Webpack的功能,但它们的作用和目的有所不同。
Loader的主要作用是转换文件,它可以将特定的文件格式(如CSS、TypeScript、图片等)转换为Webpack可以处理的模块。Loader在Webpack构建流程中的编译阶段被调用,它可以将输入的源文件(可以是JS、CSS、图片等)转换为有效的模块,这些模块可以被Webpack正确地打包和处理。例如,style-loader可以将CSS代码注入到HTML文件中,而babel-loader可以将ES6代码转换为ES5代码。
编写Loader的思路:
- 首先需要确定转换文件的类型和目标格式。
- 实现loader的主要函数,接收输入的源文件,然后对其进行转换。
- 在函数中返回转换后的结果。

编写Plugin的思路:
- 创建一个新的Plugin类,并继承Webpack的Plugin基类。
- 在Plugin类中实现需要执行的任务或功能。
- 在Webpack配置文件中注册Plugin。

如何提高webpack构建速度
提高Webpack构建速度的方法有很多,以下是一些常见的优化策略:
- 使用最新的Webpack版本:每个新版本的Webpack都可能包含性能改进和优化。确保使用最新版本可以帮助你利用这些更新。
- 优化Loader配置:Loader是Webpack用来处理各种文件类型的插件。优化Loader配置可以减少不必要的处理和转换操作,从而提高构建速度。例如,你可以使用
exclude和include属性来限制Loader处理的文件类型。 - 使用
cache-loader:cache-loader可以缓存Loader的输出结果,这样在后续的构建中,相同的文件就不需要再次处理。这可以显著减少构建时间。 - 使用
HappyPack或thread-loader进行多进程构建:JavaScript是单线程的,而Webpack的构建过程大部分时间都在处理和转换文件。使用多进程工具可以并行处理文件,从而显著提高构建速度。 - 优化resolve.modules配置:通过减少模块搜索范围,可以加快Webpack解析模块的速度。例如,你可以通过配置
resolve.modules来指定Webpack只在特定的目录中查找模块。 - 使用
DllPlugin和DllReferencePlugin:这两个插件可以将不常变动的依赖库提前打包,然后在需要的时候直接引用,从而节省构建时间。 - 开启tree shaking和scope hoisting:这两个Webpack的内置优化策略可以帮助减少构建的文件大小和数量,从而提高构建速度。
- 优化开发服务器设置:如果你使用Webpack的开发服务器(devServer)功能,可以通过优化其设置来提高构建速度。例如,你可以通过设置
devServer.hot选项来启用热模块替换(HMR),这可以减少在开发过程中不必要的构建次数。 - 利用Denoising:在处理图像等大型文件时,可以采用Denoising技术,只处理需要变化的部分,以减少处理时间和空间。
以上都是一些常见的优化策略,你可以根据自己的项目需求和实际情况选择合适的优化方法。
Babel实现转码的过程
Babel是一个JavaScript编译器,它可以将ES6及更高版本的代码转译成ES5或更低版本的代码,使得它们能在当前和旧版本的浏览器或环境中运行。
Babel实现转码的过程大致如下:
- 解析:Babel首先将源代码解析成一个抽象语法树(AST)。在这个过程中,Babel会根据ECMAScript的规范,识别出源代码中的各种语法和结构,并将它们转换成对应的AST节点。
- 转换:接下来,Babel会通过遍历AST,对AST中的每个节点进行转换。这个过程通常包括将ES6的特性(比如箭头函数、模板字符串、解构赋值等)转换成ES5的等效特性。同时,Babel也会进行一些其他的转换,比如将ES6的模块导入/导出语法转换成CommonJS的require/module.exports语法。
- 生成:转换完成后,Babel会遍历AST,将转换后的AST重新生成为源代码。这个过程通常包括将AST节点重新格式化为易于阅读的源代码,并插入必要的空格和缩进。
在上述过程中,Babel还提供了丰富的插件系统,可以自定义转译过程。例如,你可以使用babel-plugin-transform-runtime插件来引入regeneratorRuntime来支持async/await语法,或者使用babel-plugin-transform-react-jsx插件来支持JSX语法。
最后,Babel的整个转译过程是异步的,它支持使用Promise和async/await来进行异步操作,使得转译过程可以更加高效。
vite编译流程
Vite的编译流程主要依赖于其预编译和按需编译的设计。
Vite的预编译阶段:
- Vite会根据项目配置和入口文件扫描并收集所有依赖。
- 使用esbuild进行预编译,将所有依赖转化为ES模块。
Vite的按需编译阶段:
- 在开发服务器启动时,Vite只需要在浏览器请求源码时进行转换并按需提供源码。
- 根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。
webpack热更新原理
hash和history
hash模式是通过监听hashChange事件来实现的,前端js会把当前hash地址对应的组件渲染到浏览器中。在hash模式下,URL中的#号后的内容不会出现在HTTP请求中,对后端完全没有影响,因此改变hash值不会重新加载页面,基本都是使用hash来实现前端路由的。
history模式是通过调用history.pushState方法(或者replaceState)并且监听popstate事件来实现的。history.pushState会追加历史记录,并更换地址栏地址信息,但是页面不会刷新,需要手动调用地址变化之后的处理函数,并在处理函数内部决定跳转逻辑;监听popstate事件是为了响应浏览器的前进后退功能。
typeScript
ts中type和interface区别
- type是类型别名,interface是接口定义
- interface 使用 extends 实现继承, type 使用交叉类型实现继承(&)
- type可以声明任何你需要的类型
- interface可以合并重复声明
ts中的keyof和typeof
keyof用于获取对象的键类型,而typeof用于获取值的类型。
tsconfig.json中有哪些配置项
include:指定需要编译的文件路径或文件夹路径。exclude:指定不需要编译的文件路径或文件夹路径。paths:路径别名files:指定需要编译的文件列表。extends:指定继承自另一个tsconfig.json文件。compileOnSave:指定是否在保存时编译文件。sourceMap:是否生成sourcemap文件。outDir:编译输出目录。rootDir:设置项目的根目录。incremental:是否启用增量编译,指再次编译时只编译增加的内容。target:指定ts编译成ES的版本。module:指定编译后代码使用的模块化规范。lib:指定项目运行时使用的库。outFile:将代码编译合并成一个文件,默认将所有全局作用域中的代码合并成一个文件。locale:指定本地化语言。typeRoots:指定类型声明文件的根目录。types:指定需要包含在编译中的类型声明文件。plugins:在编辑器中运行的语言服务插件列表。tsBuildInfoFile:指定增量编译信息文件的位置,使用该功能时,必须开启incremental选项。
性能优化
前端性能检测工具
- 谷歌浏览器的Performance
- Lighthouse(灯塔)是谷歌开发并开源的
web性能测试工具,用于改进网络应用的质量,可以将其作为一个Chrome扩展程序运行,或从命令行运行。只需要为其提供一个需要审查的地址,Lighthouse就会对页面进行一连串的测试,生成一个有关页面性能的报告。可以看到 首屏时间,可交互时间,总阻塞时间。最大绘制时间等
前端性能优化-RAIL
谷歌提出了一个关于浏览器性能优化的指导原则,叫RAIL,它提供了一套规范来评估和改善网页应用的性能。
- 响应(Response) :规范在100毫秒以内响应用户的输入,使用户感觉到立即的反馈。这包括处理点击、滚动和触摸等用户交互。(代码层面优化,减少代码复杂度、冗余度,用Web Workers优化长任务,长列表虚拟滚动)
- 动画(Animation) :保持动画流畅并以每秒60帧的速度进行渲染,以确保动画的平滑度和视觉连续性。(requestAnimationFrame制作动画,尽量使用css动画, 使用transform动画,涉及到渲染合成层)
- 空闲(Idle) :利用主线程空闲时间执行后台任务,例如预加载资源、数据获取和计算等,以避免阻塞用户交互。(prefetch、preload)
- 加载(Load) :在5秒内将页面内容加载完毕,并在此期间提供关键内容,以使用户能够尽快与页面进行交互。(减小打包产物的体积、路由懒加载、图片懒加载、骨架屏)
聊聊渲染合成层?
渲染合成层(Compositing Layer)是浏览器中的一个关键概念,用于优化页面的渲染性能和动画效果。它是浏览器渲染引擎的一部分,用于将页面元素分成多个层,并将这些层按照正确的顺序进行合成,以最终呈现在屏幕上。
以下是一些关于渲染合成层的重要信息:
- 分层渲染:渲染合成层允许浏览器将页面分解为多个独立的层。每个层都可以单独进行绘制和处理,这样可以减少不必要的重绘和重排操作。
- 硬件加速:渲染合成层利用硬件加速技术,将图形处理器(GPU)用于合成和渲染操作。这可以提高渲染性能,并实现流畅的动画和滚动效果。
- 层的创建:浏览器根据一些特定的规则将页面元素分为不同的层。具有特定属性或样式的元素,例如
position: fixed、opacity、transform等,通常会被提升为独立的层。 - 合成过程:在合成阶段,浏览器将各个层的内容按照正确的顺序进行合成。这包括层的叠加、透明度混合、裁剪等操作,最终生成最终的图像输出。
- 优化渲染性能:渲染合成层可以减少页面的重绘和重排,从而提高渲染性能。只有发生变化的层才需要重新绘制,其他层可以保持不变,减少不必要的工作量。
- 动画和滚动优化:由于渲染合成层可以利用硬件加速和部分重绘的特性,因此可以实现更平滑和流畅的动画和滚动效果。这对于提供良好的用户体验非常重要。
总体而言,渲染合成层是一种优化技术,旨在提高页面的渲染性能和动画效果。通过有效地使用渲染合成层,开发人员可以创建更流畅、响应更快的网页应用,并提供更好的用户体验。
如何极致的优化动画性能
- 使用CSS动画:CSS动画借助GPU加速,在大多数情况下具有更好的性能。使用transform和opacity属性,避免使用top、left等属性进行动画操作。
- 使用requestAnimationFrame:requestAnimationFrame是浏览器提供的优化动画的方法,可以更好地与浏览器的渲染机制同步。
- 减少重绘和回流:通过合并多个DOM修改、使用transform进行动画变换,避免频繁的DOM重绘和回流操作,以提高性能。
- 使用硬件加速:使用CSS属性translate3d、scale3d等可以启用GPU硬件加速,提高动画的性能。
- 避免使用阻塞操作:确保动画执行期间没有长时间的JavaScript计算或网络请求阻塞主线程。
为何requestAnimationFrame不会卡
重绘之前执行
setTimeout和setInterval的问题是,它们都不精确。它们的内在运行机制决定了时间间隔,参数实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。
requestAnimationFrame能够做到,精准严格的卡住显示器刷新的时间,比如普通显示器60HZ它会自动对应17ms执行一次,高级显示器120HZ,它会自动对应9ms执行一次。当然requestAnimationFrame只会执行一次,想要使其多次执行,要写成递归的形式。
所以,这就是requestAnimationFrame的好处,window.requestAnimationFrame这个api就是解决了定时器不精准的问题的。
preload和prefetch区别
- preload:这种机制会预加载相应的脚本代码,待到需要时自行调用。它主要用于预加载当前页面需要的资源。
- prefetch:这是一种利用浏览器的空闲时间加载页面将来可能用到的资源的一种机制。通常可以用于加载非首页的其他页面所需要的资源,以便加快后续页面的首屏速度。
综上所述,preload和prefetch的主要区别在于preload是预加载当前页面需要的资源,而prefetch则是利用浏览器的空闲时间加载将来可能用到的资源。
深拷贝
什么是深拷贝,什么情况下使用深拷贝
深拷贝是一种对象拷贝方式,它会创建一个新的对象,并将原始对象中的所有数据成员复制到新的对象中,包括多态分配内存。这意味着在深拷贝后,原始对象和新的对象是完全独立的,对一个对象的修改不会影响到另一个对象。 js数据类型分为基本数据类型和引用数据类型,基本数据类型存在栈内存中,引用数据类型是存储在堆内存中的,它只是在栈中存了一个指针去指向堆内存中的空间,如果只是浅拷贝,只会新增一个新的指针去指向原来的那个内存,并不会开辟一个新的内存。
需要深拷贝的情况包括:
- 当对象中包含指针或动态分配的数组时,为了避免修改原始对象导致新对象也被修改,需要进行深拷贝。
- 在涉及到动态分配内存的情况下,例如使用指针或动态分配的数组等,需要进行深拷贝以确保数据的完整性和独立性。
- 当需要将一个对象传递给多个函数或多个线程进行处理时,为了避免修改原始对象导致其他函数或线程得到错误的结果,需要进行深拷贝。
使用JSON.parse(JSON.stringify())深拷贝会出现的问题
-
时间对象会被强制转换成字符串
-
RegExp 和 Error对象会被转换成空对象
-
对象中的函数和 undefined 会被直接删除
-
对象中的 NaN,Infinity 和 -Infinity,会被转换成 null
-
由自定义构造函数生成的对象会丢失 constructor 引用
-
无法处理循环引用
实现深拷贝思路
定义一个深拷贝函数,传入两个参数,第一个参数是要拷贝的目标值,第二个参数new一个Map,这个map的作用是处理对象循环引用的问题,其实这里可以new WeakMap(),因为这个Map的键肯定是一个对象,可以用weakmap,利于垃圾回收。 然后函数体里先判断一下是不是基本数据类型,是的话直接return传入的目标值。 否则再用map.get判断一下Map里是否已经包含了传入的对象,如果包含了直接返回这个对象,如果不包含的话使用map.set把对象存到map里,然后继续遍历这个对象,递归调用自己定义的深拷贝方法,把返回值return出去。
手写深拷贝(代码)
//判断一个对象的具体类型
const mapType = '[object Map]';
const setType = '[object Set]';
const arrayType = '[object Array]';
const objectType = '[object Object]';
const deepmap = [mapType, setType, arrayType, objectType];
function isObject(target) {
const type = typeof target;
return target != null && (type === 'object' || type === 'function');
}
function getType(target) {
return Object.prototype.toString().call(target);
}
function getInit(target) {
return new target.constructor();
}
function clonedeep(target, map = new WeakMap()) {
//处理基本数据类型
if (!isObject(target)) {
return target;
}
//处理引用类型
else {
if (map.get(target)) {
return map.get(target);
}
map.set(target, result);
let result;
const type = getType(target);
//处理可继续遍历对象
if (deepmap.includes(type)) {
result = getInit(target);
// 处理 Set
if (type === setType) {
target.forEach((value) => {
result.add(deepclone(value, map));
});
return result;
}
// 处理 Map
if (type === mapType) {
target.forEach((value, key) => {
result.set(key, deepclone(value, map));
});
return result;
}
//处理对象或者数组
for (const key in target) {
result[key] = clonedeep(target[key], map);
}
return result;
} else {
//处理其他引用类型
}
}
}
Map 的特点,和WeakMap区别
- Map 默认情况下不包含任何键,所有键都是自己添加进去的。不同于 Object 原型链上有一些默认的键。
- Map 的键可以是任意类型数据,就连函数都可以。
- Map 的键值对个数可以轻易通过
size属性获取,Object 需要手动计算。 - Map 在频繁增删键值对的场景下性能要比 Object 好。
-
Map的键可以是任意类型,WeakMap只接受对象作为键(null除外),不接受其他类型的值作为键 -
Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键;WeakMap的键是弱引用,键所指向的对象可以被垃圾回收,此时键是无效的 -
Map可以被遍历,WeakMap不能被遍历
其他
聊聊常见的设计模式,追问:你在实际的工作中有用过吗?
常见的设计模式包括但不限于以下几种:
- 单例模式(Singleton Pattern):确保一个类只有一个实例,并提供全局访问点。
- 工厂模式(Factory Pattern):通过一个工厂类来创建对象,隐藏对象的创建细节。
- 观察者模式(Observer Pattern):定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其依赖者将收到通知。
- 适配器模式(Adapter Pattern):将一个类的接口转换成客户端所期望的接口。
- 策略模式(Strategy Pattern):定义一系列的算法,将它们封装起来,并使它们可以相互替换。
- 装饰者模式(Decorator Pattern):动态地给对象添加额外的职责,是继承的一种替代方案。
- MVC模式(Model-View-Controller Pattern):将应用程序分为模型、视图和控制器三个部分,实现关注点分离。
- 迭代器模式(Iterator Pattern):提供一种顺序访问聚合对象中各个元素的方法,而无需暴露其内部表示。
以上只是一些常见的设计模式,每种模式都有其独特的应用场景和解决特定问题的方式。
个人经验来说,我在工作中经常使用一些设计模式,比如单例模式、工厂模式、观察者模式等,根据具体的场景和需求来选择合适的模式。设计模式可以帮助我更好地组织和管理代码,提高开发效率和代码质量。当然,对于每个具体的项目,需要根据实际情况来评估是否需要应用设计模式。
git reset 和 git revert 的区别
Git reset和git revert是两种在Git版本控制系统中撤销更改的不同命令,它们操作的方式和应用的场景有所不同。
Git reset命令用于在本地进行操作,它会移动HEAD指针到指定的提交。根据提供的参数(如--soft、--mixed、--hard),它可以仅移动HEAD指针,也可以改变暂存区(index)或者工作目录。然而需要注意的是,使用reset可能会丢失一些更改,因此需要谨慎使用。
Git revert命令会创建一个新的提交,这个新的提交会撤销指定提交的更改。这个命令主要用于公开的历史记录,或者在与他人协作的时候。使用revert不会丢失任何更改,它是一个安全的操作。
总的来说,如果仅仅在本地进行操作,想要撤销一些更改,可以使用reset。如果正在与他人协作,想要撤销公开的提交,应该使用revert。
比较两个值是否相等
function compareArray(obj1, obj2) {
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
return false;
}
for (let props in obj1) {
if (typeof obj1[props] === 'object' && typeof obj2[props] === 'object') {
if (!compareArray(obj1[props], obj2[props])) {
return false;
}
} else if (obj1[props] !== obj2[props]) {
return false;
}
}
return true;
}
判断数据类型


深度优先和广度优先
深度优先遍历(DFS)
- DFS 的思想是从上至下,对每一个分支一直往下一层遍历直到这个分支结束,然后返回上一层,对上一层的右子树这个分支继续深搜,直到一整棵树完全遍历,因此符合栈后进先出的特点
- 深度优先遍历常用的数据结构是栈
- DFS 特性 : 不全部保留结点,占用空间少;有回溯操作(即有入栈、出栈操作),运行速度慢。
广度优先遍历(BFS)
- BFS 的思想是从左至右,对树的每一层所有结点依次遍历,当一层的结点遍历完全后,对下一层开始遍历,而下一层结点又恰好是上一层的子结点。因此符合队列先进先出的特点
- 广度优先遍历常用的数据结构为队列
- BFS 的特性 : 保留全部结点,占用空间大;无回溯操作(即无入栈、出栈操作),运行速度快。
观察者和发布订阅
**观察者模式:**观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。
**发布订阅模式:**订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。
函数科里化
// js 版本
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
// ts 版本
function curry<T extends (...args: any[]) => any>(func: T) {
return function curried(...args: Parameters<T>) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2: Parameters<T>) {
return curried.apply(this, args.concat(args2));
}
}
};
}
前端一次性渲染十万条数据
- 虚拟列表
- createDocumentFragment(创建虚拟节点) + requestAnimationFrame(执行动画帧):
const total = 100000; // 总数
const step = 20; // 每次插入多少条
const loopCount = Math.ceil(total / 20); // 需要插入多少次
const ul = document.querySelector("ul");
let renderCount = 0; // 记录渲染次数
function add() {
const fragment = document.createDocumentFragment();
for (let i = 0; i < step; i++) {
const li = document.createElement("li");
li.innerHTML = renderCount * step + i;
fragment.appendChild(li);
}
ul.appendChild(fragment);
renderCount++;
loop();
}
function loop() {
if (renderCount < loopCount) {
window.requestAnimationFrame(add);
}
}
loop();
碰到的问题
介绍项目
项目介绍: 一个国际化的健康医疗项目,给日本、印尼和泰国开发一个在线问诊加线上购药的app。系统主要有C端的问诊、商城,B端的医生工作台、商城相关的B端系统等。我主要负责的是商城业务线的C端与B端的开发工作。C端的大致有商城首页、商品列表、商品详情、购物车、订单列表、订单详情等,B端的大致有商户管理、商家管理、spu、sku维护、订单的管理等等。项目所用框架为react、webpack、typescript
项目上遇到的问题:
-
由于是新项目,需要新起的项目比较多,为了提升些效率和保证各个项目的统一性,利用node+Commander搞了一个前端初始化项目的脚手架,发到公司的npm仓库上,然后项目成员只需要install之后执行一下init命令就可以在本地创建一个初始化的项目。
-
项目首屏慢,查询搜索很慢,地图加载慢
-
- 系统较大,打包体积很大,ECharts 等没有拆分开来,导致一开始加载JS 就很慢
-
- 需要查询的数据很多,导致接口返回较慢
-
- 组件结构复杂,层级多,数据多,导致网页渲染很慢
解决方案
-
- 如何解决打包体积大的问题?—Webpack 拆包 splitChunks,路由懒加载,异步组件
-
- 如何解决数据查询慢的问题
-
- 如何解决渲染卡顿的问题——缓存,虚拟列表



