前端面试题手册
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可靠性的核心,确保了双方都准备好接收和发送数据,并且可以处理序列号,以追踪数据包的传输顺序和确认。
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将是更合适的选择。
阅读 10·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会被调用。
如何实现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文件,并在图片标签中做一些简单的修改即可。
阅读 24·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> );}这些方法各有优劣,你可以根据具体场景和需求来选择最合适的方式来抽离和复用组件的逻辑。
React 中 setState 是如何工作的?
setState 是 React 类组件中的一个方法,用于更新组件的状态,并触发组件的重新渲染。每当状态改变时,React 会重新执行 render 方法来确定是否需要更新 DOM。当你调用 setState 方法时,你实际上是在对 React 发起一个“请求”来更新组件状态。然而,这个更新并不是立即执行的。React 会将 setState 调用放入一个队列中,稍后异步地批量处理这些更新。这种更新策略有助于优化性能,因为它避免了不必要的重复渲染和DOM操作。下面是一个 setState 的工作流程的简单描述:调用 setState: 当组件的状态需要更新时,你会调用 setState,传入一个新的状态对象或者一个函数。如果传入的是函数,该函数会接收前一个状态作为参数,并返回一个新状态。合并状态: React 会将你传入的状态对象合并到当前状态中。这个合并是浅合并,意味着只合并第一层的属性,更深层次的对象则会被整个替换。组件重新渲染: 一旦状态被更新,React 会将新的状态和当前的属性作为输入,计算出新的组件树。虚拟 DOM 比较: React 使用虚拟 DOM 来优化性能,它会比较旧的组件树和新的组件树,来确定实际DOM需要哪些更新。更新 DOM: 最后,React 会根据需要更新的部分来更新实际的 DOM,这使得渲染过程更加高效。让我们来看一个具体的例子:class MyComponent extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } handleClick = () => { this.setState(prevState => { return { count: prevState.count + 1 }; }); } render() { return ( <div> <p>You clicked {this.state.count} times</p> <button onClick={this.handleClick}> Click me </button> </div> ); }}在这个例子中,每当按钮被点击时,handleClick 方法会被调用,它又调用了 setState 来更新 count 状态。对 setState 的调用会导致 MyComponent 重新渲染,渲染方法中的 {this.state.count} 会显示新的计数值。
Webpack 有哪些优化手段
Webpack优化手段概览Webpack是一个现代JavaScript应用程序的静态模块打包器,它可以帮助开发者管理和打包他们的前端资源。以下是Webpack的一些常见优化手段:1. Tree ShakingTree Shaking是一个通过删除未使用代码来减少打包体积的过程。Webpack内置支持ES6模块的Tree Shaking,可以识别出未被引用的代码并在打包时排除它们。例子:在开发过程中,可能会引入一个库,比如Lodash,但只使用其中的几个函数。通过配置sideEffects属性为false,Webpack可以标记并移除那些未被使用的模块,减小最终的bundle体积。2. 代码分割 (Code Splitting)代码分割允许将代码分解为可按需加载的多个包,从而减少单个包的大小,提高加载速度。例子:使用import()语法实现动态导入,将特定功能模块分割成独立的chunk,只有当用户需要时才加载这些模块。3. 使用Externals当你使用一些CDN外部扩展或从外部引入库时,可以配置Webpack的externals选项,让Webpack知道这些依赖不应该打包进bundle。例子:例如,如果你的项目使用jQuery,可以从CDN引入而不是打包到bundle中,配置externals让Webpack忽略它。4. 优化解析配置resolve选项可以加快模块解析速度。例如,通过配置extensions减少文件尝试的后缀列表,设置alias提供路径别名减少查找路径的时间。例子:resolve: { extensions: ['.js', '.jsx'], alias: { Components: path.resolve(__dirname, 'src/components/') }}5. 使用缓存Webpack的cache选项可以启用持久化缓存,提高重建速度。例子:在webpack.config.js中启用cache选项,使得模块在第一次构建后将转换结果缓存起来,之后的构建会加快。6. 压缩代码利用插件如TerserPlugin压缩JavaScript代码,减少文件大小。例子:在optimization配置中使用TerserPlugin来开启代码压缩。7. 优化CSS使用如MiniCssExtractPlugin和cssnano等工具将CSS提取为单独的文件并压缩。例子:plugins: [ new MiniCssExtractPlugin({ filename: '[name].[contenthash].css', })],optimization: { minimizer: [ new CssMinimizerPlugin(), ],},8. 使用持久化缓存通过设置output.filename使用内容哈希,当文件内容未变化时,利用浏览器缓存机制避免重新下载。例子:output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist')}9. 使用高效的加载器和插件例如babel-loader的cacheDirectory选项,或者HappyPack插件来并行处理任务。例子:module: { rules: [ { test: /\.js$/, use: 'babel-loader?cacheDirectory=true', exclude: /node_modules/ } ]}10. 监控和分析使用webpack-bundle-analyzer等工具分析bundle大小,找到优化点。例子:通过安装并配置`webpack-bundle-analyzer
阅读 30·2024年6月24日 16:43
浏览器渲染页面的详细过程
当浏览器渲染页面时,会经历以下几个主要步骤:1. 处理HTML - 构建DOM树浏览器首先解析HTML文档以构建DOM(文档对象模型)树。DOM树是页面结构的表示,其中每个HTML标签都是树中的一个节点。例子:如果有一个简单的HTML文档如下:<!DOCTYPE html><html><head> <title>示例页面</title></head><body> <h1>你好,世界!</h1> <p>这是一个段落。</p></body></html>浏览器会创建一个DOM树,包含html、head、title、body、h1和p等节点。2. 处理CSS - 构建CSSOM树浏览器会解析CSS,包括外部的CSS文件和页面内部的样式。解析完成后,浏览器会创建CSSOM(CSS对象模型)树。CSSOM树反映了所有CSS规则以及它们的层叠和继承关系。例子:对于上述HTML,可能有一个CSS文件如下:body { font-family: Arial, sans-serif; }h1 { color: blue; }对应的CSSOM树会包含body和h1的样式规则。3. 结合DOM和CSSOM - 构建渲染树接下来,浏览器会结合DOM树和CSSOM树来创建渲染树。渲染树只包含需要显示的节点和它们的样式信息。这意味着例如<script>和<style>标签等不会被包含在渲染树中。4. 布局 - 计算每个节点的位置一旦渲染树构建完成,浏览器将进行布局(也被称为回流),计算出渲染树中每个节点的确切位置和大小。这个过程会考虑视口大小、元素的大小、元素之间的关系等因素。5. 绘制 - 像素化页面内容布局完成后,浏览器会进入绘制(或绘图)阶段,它会遍历渲染树,并使用UI后端层将每个节点绘制到屏幕上。这个过程涉及将样式信息转化为实际的像素。6. 合成 - 层的合成和显示某些复杂的视觉效果如3D变换或阴影,可能会使得元素被分到不同的层。浏览器会管理这些层,最后将它们合成到一起,显示在屏幕上。在整个渲染过程中,如果DOM或CSSOM被脚本更新,会触发重新布局(回流)和重绘,这可能会影响渲染性能。为了获得最佳性能,开发者应该尽量减少这些操作的频率和范围。以上就是浏览器渲染页面的详细过程。这个过程需要高效地处理和合作,才能尽快地将内容呈现给用户。
前端模块规范有哪些?模块如何异步加载?
前端模块规范在JavaScript中,模块规范主要有以下几种:CommonJS:CommonJS是Node.js采用的模块规范,它通过 require函数来同步加载依赖的其他模块,通过 module.exports或 exports对象来导出模块。例子: // math.js function add(a, b) { return a + b; } module.exports = { add }; // main.js const math = require('./math'); console.log(math.add(1, 2));AMD (Asynchronous Module Definition):AMD是RequireJS实现的规范。它支持在浏览器环境中异步加载模块,使用 define函数定义模块,使用 require函数加载模块。例子: // 定义模块math.js define(function() { return { add: function(a, b) { return a + b; } }; }); // 加载模块 require(['math'], function(math) { console.log(math.add(1, 2)); });ES Modules (ESM):ECMAScript 2015(也称为ES6)引入了官方的JavaScript模块标准,这是现代前端开发中推荐使用的模块化解决方案。它支持静态导入和导出,也可以配合特定的语法进行动态导入。例子: // math.js export function add(a, b) { return a + b; } // main.js import { add } from './math.js'; console.log(add(1, 2));UMD (Universal Module Definition):UMD旨在提供一个跨平台的模块定义方式,使得一个模块可以同时在AMD和CommonJS环境中运行。例子: (function(root, factory) { if (typeof define === 'function' && define.amd) { // AMD环境 define([], factory); } else if (typeof exports === 'object') { // CommonJS环境 module.exports = factory(); } else { // 浏览器全局变量 root.myModule = factory(); } }(this, function() { return { add: function(a, b) { return a + b; } }; }));模块的异步加载使用AMD规范(如RequireJS):AMD规范设计之初就考虑了模块的异步加载。通过 require函数,我们可以异步加载所需的模块。使用ES Modules的动态 import():ES6模块支持使用 import()函数来实现模块的动态导入,这使得我们可以在代码执行时按需加载模块。例子: // 假设我们有一个math.js模块 // 动态导入ES模块 import('./math.js').then(math => { console.log(math.add(1, 2)); }).catch(error => { console.log('Error loading the module:', error); });Webpack等构建工具的代码分割(Code Splitting)功能:现代前端构建工具像Webpack提供了代码分割功能,可以将应用程序分割成多个chunks,并在需要时异步加载。例子:在Webpack中,可以通过动态 import()语法来创建所谓的"分割点"(split point)。 // Webpack代码分割示例 import(/* webpackChunkName: "math" */ './math').then(math => { console.log(math.add(1, 2)); });
阅读 20·2024年6月24日 16:43
addEventListener 和 attachEvent 的区别是什么?
addEventListener 和 attachEvent 都是用于在 Web 开发中将事件监听器绑定到 DOM 元素上的方法,但它们之间存在一些关键差异:浏览器兼容性:addEventListener 是 DOM Level 2 Events 规范的一部分,它被所有现代浏览器(包括 IE9 及以上版本)支持。attachEvent 是微软为 IE8 及以下版本的浏览器提出的一个方法,不是标准的 DOM 事件处理方法。语法:addEventListener 采用以下语法: javascript element.addEventListener(event, function, useCapture); 其中 event 是事件名称(不带 on 前缀),function 是事件处理函数,而 useCapture 是一个布尔值,用于指定事件是在捕获阶段处理还是在冒泡阶段处理。attachEvent 采用不同的语法: javascript element.attachEvent(eventWithOn, function); 其中 eventWithOn 是带有 on 前缀的事件名称,function 是事件处理函数。事件流:addEventListener 允许你指定事件处理函数是在捕获阶段还是冒泡阶段被调用。attachEvent 只支持事件冒泡,不支持事件捕获。this 关键字:在 addEventListener 中绑定的事件处理函数里,this 指向绑定事件的元素。在 attachEvent 中绑定的事件处理函数里,this 指向全局对象 window,而不是绑定事件的元素。多个事件监听器:使用 addEventListener,你可以为同一个事件添加多个事件监听器,它们会按顺序执行。而 attachEvent 有可能会导致多个事件监听器的执行顺序不一致,而且同一个事件处理函数如果添加多次,也会被执行多次。移除事件监听器:addEventListener 配对的是 removeEventListener,可以用于移除事件监听器。attachEvent 配对的是 detachEvent,用于移除事件监听器,但需要确保传递给 detachEvent 的参数与 attachEvent 时的参数完全一致。由于 attachEvent 方法是一个非标准方法且仅在老版本的 IE 浏览器中可用,所以通常推荐使用 addEventListener,因为它是跨浏览器兼容的并遵循现代的 Web 标准。当需要支持老版本 IE 浏览器时,开发者可能需要编写额外的代码来兼容 attachEvent。
阅读 22·2024年6月24日 16:43