乐闻世界logo
搜索文章和话题

面试题手册

HTML 中 script 标签的位置是否会影响网站首屏的显示时间?

<script> 标签的位置确实会影响网站首屏的显示时间。当浏览器解析到 HTML 文档中的 <script> 标签时,它会暂停文档的解析,去加载和执行脚本。这意味着,如果 <script> 标签放置在文档的 head 部分,浏览器必须先加载并执行完这些脚本,才能继续解析 HTML 文档的剩余部分。如果脚本很大或者需要从慢速服务器加载,这将延迟页面内容的渲染,从而增加首屏显示的时间。例如,假设我们有一个简单的 HTML 页面,其中在 head 部分包含了一个大型的外部 JavaScript 文件:<!DOCTYPE html><html><head> <title>My Page</title> <script src="big-external-script.js"></script></head><body> <!-- 页面内容 --></body></html>在上述情况中,用户必须等待 big-external-script.js 完全加载并执行后,才能看到页面内容,即首屏的显示会受到影响。作为对比,如果我们将 <script> 标签放在 HTML 文档的 body 部分的最后:<!DOCTYPE html><html><head> <title>My Page</title></head><body> <!-- 页面内容 --> <script src="big-external-script.js"></script></body></html>在这种结构中,浏览器会先渲染页面内容,用户可以更快地看到首屏,而脚本将在页面内容加载之后异步加载和执行,从而不会阻塞首屏的显示。为了进一步提高性能,还可以使用 async 或 defer 属性,这两者都允许浏览器异步加载脚本:使用 async 时,脚本会在加载完成后尽快执行,但不保证执行顺序;使用 defer 时,脚本会在整个文档解析完成后,但在 DOMContentLoaded 事件之前按照它们在文档中出现的顺序执行。<!DOCTYPE html><html><head> <title>My Page</title> <script src="external-script.js" defer></script></head><body> <!-- 页面内容 --></body></html>在实际开发中,为了优化首屏显示时间,开发者通常会将非必须的脚本移动到文档底部,或者使用 async 或 defer 属性,以确保关键渲染路径(Critical Rendering Path)不会因脚本加载和执行而受阻。
阅读 13·2024年6月24日 16:43

浏览器中的事件循环的工作流程

事件循环 (Event Loop) 是浏览器用于处理非同步事件的一种机制。它确保了 JavaScript 的执行能够同步进行,即使 JavaScript 是单线程运行的。下面是事件循环的工作流程:执行栈(Call Stack): 当一段JavaScript代码开始执行时,它首先会进入到执行栈中。如果这段代码是一个函数,那么这个函数就会被放到栈的顶部。Web APIs: 当遇到非同步操作(如:setTimeout, XMLHttpRequest 等)时,该操作会被浏览器的Web APIs接管,执行栈会继续执行下一行代码,不会停下等待异步操作的结果。任务队列(Task Queue): 一旦异步操作完成了(比如说 setTimeout 中指定的时间已过),回调函数就会被放入任务队列中。任务队列就是一个等待执行栈清空后执行的回调函数的列表。事件循环: 事件循环的职责是监控执行栈和任务队列。如果执行栈为空,它就会检查任务队列。如果任务队列中有待执行的回调函数,事件循环就会将其从队列中取出,放到执行栈中去执行。渲染队列(Render Queue): 当浏览器准备进行渲染时(通常是每16.7毫秒,对应于60fps),它会有自己的渲染队列来处理重绘和回流事件。如果执行栈和任务队列都是空的,事件循环会从渲染队列取出任务执行,以确保用户界面能够及时更新。微任务队列(Microtask Queue): 除了常规的任务队列,还有一种叫作微任务(Microtask)的任务,比如Promise的回调。微任务队列的特点是在当前执行栈清空后,就会立即执行微任务队列中的所有任务,即使任务队列中有等待的任务。只有当微任务队列为空时,事件循环才会查看任务队列。例子假如我们有以下代码片段:console.log('脚本开始');setTimeout(function() { console.log('setTimeout');}, 0);Promise.resolve().then(function() { console.log('promise1');}).then(function() { console.log('promise2');});console.log('脚本结束');执行顺序将会是这样的:'脚本开始' 被打印到控制台,因为它是第一行同步代码。setTimeout 的回调被 Web APIs 接管,执行栈继续往下执行。Promise.resolve() 创建了一个Promise,它的回调被放入了微任务队列。'脚本结束' 被打印到控制台,因为它是同步代码。当前的同步代码已经执行结束,执行栈被清空。事件循环首先检查微任务队列,发现有Promise的回调。'promise1' 和 'promise2' 依次被打印到控制台。微任务队列清空,事件循环现在检查任务队列。setTimeout 的回调现在被事件循环从任务队列移到执行栈,打印 'setTimeout'。
阅读 23·2024年6月24日 16:43

详细说明浏览器的缓存机制

浏览器的缓存机制主要是为了提高网页的加载速度,减少服务器的负载,并优化用户的浏览体验。浏览器缓存可以分为以下几种类型: 强缓存(HTTP Cache Control)强缓存不会向服务器发送请求,直接从缓存中读取资源。这是通过设置HTTP响应头中的 Cache-Control和 Expires实现的。Cache-Control: 这个响应头可以设置多个值,比如:max-age=xxx:表示资源在xxx秒后过期。no-store:不允许缓存,每次都要向服务器请求。no-cache:资源不会被缓存,每次都会发请求到服务器验证资源是否有更新。Expires: 这是一个绝对时间,表示资源的过期时间。协商缓存(Validation Cache)当强缓存失效后,浏览器会向服务器发送请求,询问资源是否有更新。这是通过 Last-Modified/If-Modified-Since和 ETag/If-None-Match这两对HTTP头实现的。Last-Modified/If-Modified-Since: 服务器响应时通过 Last-Modified标识资源最后修改时间,浏览器下次请求时带上 If-Modified-Since,服务器比较后如果没有变化则返回304状态码,浏览器继续使用缓存。ETag/If-None-Match: ETag是资源的一个唯一标识,类似于指纹。浏览器在请求时带上 If-None-Match,服务器对比ETag,如果没有变化则返回304状态码,浏览器继续使用缓存。Web Storage(本地存储)包括 localStorage和 sessionStorage,它们提供了在客户端存储键值对数据的能力。localStorage数据在浏览器关闭后依然存在,而 sessionStorage的数据在页面会话结束时被清除。IndexedDB是一种在浏览器中保存大量结构化数据的方式,可以创建、读取、遍历和搜索数据库中的记录。IndexedDB操作基于事件响应,与Web Storage相比,它可以提供更复杂的数据操作功能。Service WorkersService workers可以拦截请求,并可以使用缓存API来管理请求的响应。开发者可以编写自己的缓存策略,例如,当网络不可用时,可以从缓存中提供备份内容。举个例子,假设用户第一次访问一个网页,浏览器会下载所有资源,并按照HTTP头信息决定哪些资源应当被缓存。当用户再次访问这个网页时,如果相关资源具备有效的强缓存设置,浏览器会直接从缓存中加载资源,不经过服务器请求,这样可以极大提高页面加载速度。如果强缓存过期,浏览器会使用协商缓存机制与服务器通信,确认资源是否更新,从而决定是重新下载资源,还是继续使用缓存版本。
阅读 33·2024年6月24日 16:43

[Event Loop] 浏览器和nodejs事件循环有什么区别?

在浏览器和Node.js中,事件循环是实现非阻塞I/O操作的核心机制,尽管它们在高层面上非常相似,但具体实现上有几个主要区别。以下是我将回顾的几点关键差异及其例子:1. 任务源和处理方式浏览器:浏览器的事件循环主要处理来自Web API的任务,这些可以是DOM事件、Ajax回调、setTimeout等。它使用了宏任务(macro tasks)和微任务(micro tasks)的概念。宏任务包括script(整体代码)、setTimeout、setInterval和I/O,而微任务主要包括Promise.then、MutationObserver。在一个事件循环中,每次只会从宏任务队列中取出一个任务执行,然后执行所有可用的微任务。Node.js:Node.js的事件循环由libuv库实现,包括了多个阶段,如timers、I/O callbacks、poll、check、close callbacks等。Node.js中处理任务更为复杂,各个阶段几乎都有自己的队列。timers阶段处理setTimeout和setInterval回调,poll阶段负责I/O事件回调,而setImmediate的回调会在check阶段执行。例子:在浏览器中,Promise.resolve().then()会在当前宏任务完成后立即执行,因为微任务总是在宏任务之后清空。在Node.js中,由于事件循环的阶段性,可能会在执行微任务时插入其他类型的任务,例如,如果在I/O操作完成后添加了一个setImmediate,邑可能在当前阶段的微任务和下一阶段的微任务之间执行。2. 定时器的精度浏览器:浏览器的定时器(如setTimeout和setInterval)的精度相对较低,早期定时器至少有4ms的延迟(根据HTML5标准规定),而现代浏览器偶尔会有更高的延迟,以帮助减少后台标签页的能耗。Node.js:Node.js定时器的精度通常更高,因为服务器端的环境对实时性和性能有更高的要求。Node.js的事件循环可以精确到毫秒。例子:在浏览器中设置 setTimeout(fn, 1)可能实际上在4ms后才执行回调,而在Node.js中,相同的设置会尽量接近1ms执行回调。3. 默认行为和扩展性浏览器:浏览器的事件循环通常是不可见和不可控制的,由浏览器内核管理。Node.js:Node.js的事件循环可以通过C++插件和核心模块进行扩展,给开发者提供了更多控制。例如,使用libuv库,开发者能够接触到底层的事件循环机制。例子:Node.js的开发者可以编写本地插件,通过直接与libuv交互来修改或增强事件循环的行为,而这在浏览器端是做不到的。4. 性能和优化浏览器:浏览器的事件循环是为了优化用户界面和用户互动设计的,因此,许多优化都是围绕用户体验和界面响应性进行的。Node.js:Node.js的事件循环是针对I/O密集型操作进行优化的,特别是网络和文件系统操作。
阅读 48·2024年6月24日 16:43

TCP 建立连接的详细过程

TCP(传输控制协议)建立连接的过程通常被称为三次握手(Three-way handshake)。这个过程确保客户端和服务器之间建立一个可靠的会话。三次握手的基本步骤如下:SYN(同步)步骤:客户端开始连接过程,向服务器发送一个带有SYN(同步序列编号)标志的TCP段,说明客户端愿意建立连接,并且提供了自己的初始序列号(ISN),用来同步序列号。SYN-ACK(同步确认)步骤:服务器收到客户端的SYN请求后,若同意建立连接,将发送一个TCP段给客户端,这个TCP段同时设置了SYN和ACK(确认)标志。ACK标志确认了客户端的初始序列号,而服务器的SYN标志则提供了服务器的初始序列号。ACK(确认)步骤:客户端收到服务器的SYN-ACK响应后,再次发送一个TCP段给服务器,这次的TCP段只设置了ACK标志,确认了服务器的初始序列号。这样,三次握手就完成了,双方都确认了对方的初始序列号,可以开始数据传输。让我用一个简单的例子来说明这个过程:假设Alice想要通过TCP与Bob的服务器建立连接:Alice -> Bob: Alice发送一个TCP段,其中SYN标志被置为1,初始序列号设为100(假设的值)。Bob -> Alice: Bob收到了Alice的请求后,发送一个TCP段作为回应,这个段中SYN和ACK标志都被置为1,确认号设为Alice的初始序列号+1,即101,同时Bob提供自己的初始序列号,设为300。Alice -> Bob: Alice收到Bob的响应后,发送一个TCP段,其中ACK标志被置为1,确认号设为Bob的初始序列号+1,即301。完成上述步骤后,Alice和Bob之间的TCP连接就正式建立了,他们可以开始安全可靠的数据交换。这个三次握手的机制是TCP可靠性的核心,确保了双方都准备好接收和发送数据,并且可以处理序列号,以追踪数据包的传输顺序和确认。
阅读 46·2024年6月24日 16:43

nodejs如何开启多进程,进程之间如何通讯?

在Node.js中,可以利用cluster模块开启多个进程,以此来充分地利用多核CPU的资源。cluster模块可以创建一组Node.js进程,它们共享同一个服务器端口。以下是一个使用cluster模块的基本步骤:导入cluster模块和其他必须的模块。使用cluster.isMaster判断当前是否是主进程(Master)。在主进程中,可以使用cluster.fork()来创建工作进程(Worker)。在工作进程中,执行实际的应用代码,如HTTP服务器的监听等。监听相应的事件来处理工作进程的启动、在线、退出等情况。进程之间的通信可以通过以下方式:主进程通过worker.send()发送消息到工作进程。工作进程通过process.send()发送消息到主进程。监听message事件来接收消息。下面是一个创建多个工作进程并实现主工作进程通信的简单示例:const cluster = require('cluster');const http = require('http');const numCPUs = require('os').cpus().length;if (cluster.isMaster) { console.log(`主进程 ${process.pid} 正在运行`); // 衍生工作进程。 for (let i = 0; i < numCPUs; i++) { const worker = cluster.fork(); // 主进程接收工作进程发送的消息 worker.on('message', (msg) => { console.log(`主进程收到消息 '${msg}' 来自工作进程 ${worker.process.pid}`); }); } cluster.on('exit', (worker, code, signal) => { console.log(`工作进程 ${worker.process.pid} 已退出`); });} else { // 工作进程可以共享任何TCP连接。 // 在本例中,它是一个 HTTP 服务器。 http.createServer((req, res) => { res.writeHead(200); res.end('你好世界\n'); // 工作进程向主进程发送消息 process.send(`工作进程 ${process.pid} 收到请求`); }).listen(8000); console.log(`工作进程 ${process.pid} 已启动`);}在这个例子中,主进程创建了和CPU核心数量相同的工作进程,并设置了接受消息的监听器。每当工作进程接收到HTTP请求时,它就会向主进程发送一个消息。主进程监听到消息后,在控制台中输出相关信息。这种模式让Node.js应用可以在多核CPU上以更高效的方式运行,而且主工作进程之间的消息传递机制让它们可以交换信息。
阅读 75·2024年6月24日 16:43

XML 和 JSON 的区别是什么?

XML(Extensible Markup Language)和JSON(JavaScript Object Notation)都是用于存储和传输数据的格式,但它们有一些关键的区别:语法XMLXML是一种标记语言,非常类似于HTML。它使用开始和结束标签来定义数据。例如:<user> <name>张三</name> <email>zhangsan@example.com</email></user>JSONJSON是一种轻量级的数据交换格式。它使用易于阅读的键值对。例如:{ "user": { "name": "张三", "email": "zhangsan@example.com" }}可读性XML因为XML更像是HTML,所以人类可以相对容易地阅读。然而,它的冗长特性可能使得阅读和理解大型文档变得复杂。JSONJSON的格式更简洁,通常更容易被人阅读。它的数据格式也让解析变得更简单。数据类型XMLXML不支持数据类型。所有的数据都是字符串。开发者需要在应用层面转换和验证数据类型。JSONJSON支持多种数据类型,包括字符串、数字、布尔值、数组、对象等。这使得数据可以更直接地映射到程序语言的数据结构。元数据XMLXML天然支持元数据,因为它可以包含属性,并且标签本身可以提供信息。例如,可以通过命名空间和属性来扩展XML元素。JSONJSON不包含元数据的概念。所有的数据都是明确的键值对,不支持属性或命名空间。解析XML解析XML需要使用DOM(文档对象模型)或SAX(简单API用于XML)这样的解析器。这些解析器通常比JSON的解析器更复杂和耗时。JSONJSON可以通过各种语言内置的解析器进行解析,例如JavaScript的JSON.parse()方法。解析通常更快且效率更高。互操作性XMLXML广泛用于多种不同的系统中,并且在Web服务(如SOAP)中使用得非常普遍。它的灵活性在需要严格的文档验证和命名空间支持时非常有用。JSONJSON通常用于Web应用中,特别是作为AJAX操作的一部分。它与JavaScript的自然兼容性使其在Web开发中非常流行。总结XML和JSON都可以用于数据存储和传输,但JSON更轻量级,解析起来更快,而XML更加灵活,更适合复杂的文档结构。选择哪种格式取决于应用场景和开发者的需求。例如,在一个需要执行大量网络请求并且对传输数据大小敏感的移动应用中,可能会倾向于使用JSON,因为它的简洁可以减少带宽使用。相反,如果一个企业需要与多个外部系统交换数据,并且这些系统预期使用基于XML的协议(如SOAP),那么XML将是更合适的选择。
阅读 11·2024年6月24日 16:43

React 组件渲染过程是怎么样的?

React 组件的渲染过程大致分为几个步骤:初始化阶段:当组件被引入到React应用中时,首先会进行初始化。初始化的过程包括设置组件的默认属性(defaultProps),以及组件的初始状态(state)。挂载阶段(Mounting):constructor:如果组件是一个类组件,会首先调用构造函数,进行一些如状态的初始化等操作。getDerivedStateFromProps(可选):在组件实例化后和重新渲染之前调用,可以用来根据props来更新state。render:该方法是组件渲染的核心。它会对当前组件的 props与 state进行分析,并返回一个React元素(通常是虚拟DOM节点),这个返回的元素可以是原生DOM的表现、也可以是其它组件的集合。值得注意的是,render 方法是纯函数,不应该包含任何会改变组件状态的代码。componentDidMount:组件挂载完成后调用。这是执行副作用操作的理想位置,如发起网络请求、设置定时器等操作。更新阶段(Updating):组件的props或state发生变化时,组件会重新渲染,其过程如下:getDerivedStateFromProps(可选):如上所述,这个方法用在props发生变化时根据新的props来更新state。shouldComponentUpdate(可选):通过返回值决定组件是否应当进行更新。如果返回false,则不会调用render方法,也不会进行后面的更新过程。render:重新执行render函数,与初始化阶段的render相同。getSnapshotBeforeUpdate(可选):在DOM更新前被调用,用于捕获更新前的DOM状态。componentDidUpdate:组件更新完成后被调用,可以执行例如更新DOM的操作。卸载阶段(Unmounting):componentWillUnmount:组件将要被卸载之前调用,进行必要的清理工作,如清除定时器、取消网络请求等。在这个过程中,React还会对组件树进行优化,使用虚拟DOM和Diff算法来减少实际DOM操作的次数,从而提高性能。例子:假设我们有一个简单的计数器组件 Counter,它有一个按钮用来增加计数,计数的值保存在状态 count中。当用户点击按钮时,组件的state会更新,触发更新流程:class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } componentDidMount() { console.log('Counter: componentDidMount'); } componentDidUpdate() { console.log('Counter: componentDidUpdate'); } componentWillUnmount() { console.log('Counter: componentWillUnmount'); } increment = () => { this.setState(state => ({ count: state.count + 1 })); }; render() { return ( <div> <p>Count: {this.state.count}</p> <button onClick={this.increment}>Increment</button> </div> ); }}在这个例子中:当 Counter首次加载进React树时,constructor、render 和 componentDidMount会依次被调用。当用户点击按钮时,increment方法通过 setState更新组件的state,触发组件的更新流程。因为state发生了变化,shouldComponentUpdate(如果定义了的话)和render方法会被调用,接着如果有必要,getSnapshotBeforeUpdate和componentDidUpdate也会被调用。当组件要被移除时,componentWillUnmount会被调用。
阅读 21·2024年6月24日 16:43

如何实现web图片懒加载功能

懒加载(Lazy Loading)是一种常见的Web性能优化技术,它可以延迟加载页面上的非关键资源,比如图片。当用户滚动页面并接近这些资源时,这些资源才会开始加载。下面是实现图片懒加载的几种方法: 1. 使用原生的 loading属性(HTML5)最新的HTML标准中为 <img>标签增加了一个 loading属性,可以设置为 lazy,这样浏览器会自动懒加载这些图片。<img src="example.jpg" loading="lazy" alt="描述文本" />这种方式是最简单、最直接的,但它依赖于浏览器的支持,老版本的浏览器可能不支持这个属性。2. 使用JavaScript实现懒加载可以用JavaScript监听滚动事件,动态地加载图片。实现的基本思路是:将图片的 src属性替换为 data-src,初次加载时不加载实际图片。监听页面的滚动事件。当图片进入可视区域时,将 data-src的值赋给 src,加载图片。示例代码如下:<img data-src="example.jpg" alt="描述文本" />document.addEventListener("DOMContentLoaded", function() { var lazyImages = [].slice.call(document.querySelectorAll("img[data-src]")); let active = false; const lazyLoad = function() { if (active === false) { active = true; setTimeout(function() { lazyImages.forEach(function(lazyImage) { if ((lazyImage.getBoundingClientRect().top <= window.innerHeight && lazyImage.getBoundingClientRect().bottom >= 0) && getComputedStyle(lazyImage).display !== "none") { lazyImage.src = lazyImage.dataset.src; lazyImage.removeAttribute("data-src"); lazyImages = lazyImages.filter(function(image) { return image !== lazyImage; }); if (lazyImages.length === 0) { document.removeEventListener("scroll", lazyLoad); window.removeEventListener("resize", lazyLoad); window.removeEventListener("orientationchange", lazyLoad); } } }); active = false; }, 200); } }; document.addEventListener("scroll", lazyLoad); window.addEventListener("resize", lazyLoad); window.addEventListener("orientationchange", lazyLoad);});3. 使用Intersection Observer API这是一个现代的API,它提供了一种异步检测目标元素与其祖先元素或顶级文档 viewport的交叉状态的方法。document.addEventListener("DOMContentLoaded", function() { const lazyImages = [].slice.call(document.querySelectorAll("img[data-src]")); const imageObserver = new IntersectionObserver(function(entries, observer) { entries.forEach(function(entry) { if (entry.isIntersecting) { const image = entry.target; image.src = image.dataset.src; imageObserver.unobserve(image); } }); }); lazyImages.forEach(function(image) { imageObserver.observe(image); });});这种方式不需要监听滚动事件,性能更好,但需要浏览器支持 Intersection Observer。4. 使用第三方库还有一些现成的第三方库可以帮助实现图片懒加载,如 lozad.js、lazysizes等。这些库通常提供了更多的功能和更好的兼容性。<script src="path_to_lazysizes.js" async=""></script><!-- 在img元素中使用class="lazyload" --><img data-src="example.jpg" class="lazyload" alt="描述文本" />在使用第三方库时,通常只需要引入相应的JavaScript文件,并在图片标签中做一些简单的修改即可。
阅读 25·2024年6月24日 16:43

React 组件抽离公共逻辑代码有哪些方式

React 组件抽离公共逻辑主要有以下几种方式:1. 高阶组件(Higher-Order Components,HOCs)高阶组件是一个接收组件并返回新组件的函数。它可以用于重用组件逻辑。例子:function withUserData(WrappedComponent) { return class extends React.Component { state = { user: null }; componentDidMount() { // 假设 getUserData() 方法从某个服务获取用户数据 getUserData().then(user => this.setState({ user })); } render() { return <WrappedComponent {...this.props} user={this.state.user} />; } };}// 使用高阶组件const EnhancedComponent = withUserData(MyComponent);2. Render PropsRender Props 是指以函数为子组件的这种模式,它允许我们的组件告诉其子组件需要渲染的内容。例子:class UserData extends React.Component { state = { user: null }; componentDidMount() { // 同样假设 getUserData() 方法从某个服务获取用户数据 getUserData().then(user => this.setState({ user })); } render() { return this.props.render(this.state.user); }}// 使用 Render Props<UserData render={user => user ? <MyComponent user={user} /> : <LoadingSpinner />} />3. 自定义 Hooks自定义 Hooks 允许你将组件逻辑提取到可重用的函数中。例子:function useUserData() { const [user, setUser] = useState(null); useEffect(() => { getUserData().then(userData => setUser(userData)); }, []); return user;}// 使用自定义 Hookfunction MyComponent() { const user = useUserData(); if (!user) { return <LoadingSpinner />; } return <Profile user={user} />;}4. Context APIContext API 允许你在组件树中直接传递数据,而不必在每个层级手动传递 props。例子:const UserContext = React.createContext();// Context 提供者class UserProvider extends React.Component { state = { user: null, }; componentDidMount() { getUserData().then(user => this.setState({ user })); } render() { return ( <UserContext.Provider value={this.state.user}> {this.props.children} </UserContext.Provider> ); }}// Context 消费者function MyComponent() { return ( <UserContext.Consumer> {user => user ? <Profile user={user} /> : <LoadingSpinner />} </UserContext.Consumer> );}// 应用 Context<UserProvider> <MyComponent /></UserProvider>5. 组件组合(Component Composition)组件组合是 React 中一种基本的模式,它允许你将子组件传递给父组件,并在父组件中渲染。例子:function UserProfile({ user, children }) { return ( <div> <Profile user={user} /> {children} </div> );}function MyComponent() { const user = useUserData(); return ( <UserProfile user={user}> {/* 其他定制的子组件 */} </UserProfile> );}这些方法各有优劣,你可以根据具体场景和需求来选择最合适的方式来抽离和复用组件的逻辑。
阅读 42·2024年6月24日 16:43