单页面架构

传统的多页面应用构建方式: 纯服务端渲染,前后端不分离,使用jsp,jade,’ejs’,’tempalte’等技术在后台先拼接成对应的HTML结构,然后转换成字符串,在每个对应的路由返回对应的数据(文件)即可 Jade模版服务端渲染,代码实现: const express= require('express') const app =express() const jade = require('jade') const result = *** const url path = *** const html = jade.renderFile(url, { data: result, urlPath })//传入数据给模板引擎 app.get('/',(req,res)=>{ res.send(html)//直接吐渲染好的`html`文件拼接成字符串返回给客户端 }) //RestFul接口 app.listen(3000,err=>{ //do something }) 使用jQuery等传统库绘制的前端页面 传统前后端不分离,服务端渲染的优缺点: 优点: SEO友好,因为返回给前端的是渲染好的HTML结构,里面的内容都可以被爬虫抓取到。 对于一些应用性能等要求不高的项目,比如某个公司的静态网页,内容很少的情况下,直接一把梭就好,不用再搭建工程化的环境等 对于后端程序员(全干工程师)来说,不用去特意学习前端框架,公司也不用特意去招聘前端 兼容性好,传统服务端渲染多页面应用吐出来的都是字符串,HTML结构 缺点: 如果项目很大,不利于维护,据我所知,目前很多云计算公司,还有不少都是使用非单页面应用,例如一个几十万行的项目是用jQuery写的,如果注释和文档不是非常齐全,那么真的会无从下手 性能和用户体验,不能跟单页面应用相比 后期迭代,升级空间不大,目前大部分写得比较好的库,都建立vue,react等框架基础上,他们都有一套自己的运行机制,有自己的生命周期,并且不像传统的应用,还加上了一层虚拟DOM以及diff算法 现在类似Ant-Design-pro这样的开箱即用的库已经很多,单页面应用的学习和开发成本已经很低很低,如果还在使用传统的技术去开发新的应用,对于开发人员多内心来说也是一种折磨。 这里并不是说多页面应用不好,只能说各有各自的好,单页面应用如果通过大量的极致优化手段,是可以从不少方面跟原生一拼。 目前的单页面应用: 只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源 ,公共资源(js、css等)仅需加载一次,常用于PC端官网、购物等网站 其实只有一个空的DIV标签,其他都是js动态生态的内容 单页面应用实现步骤: 代码实现: 首先是一个静态模板文件 index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="root"></div> </body> <script> </script> </html> 在vue react框架的入口文件中指定对应的渲染元素: import React from 'react; import ReactDOM from 'react-dom'; ReactDOM.render( <App/>, document.querySelector("#root") ) 引入react-router或者 react-router-dom,dva等路由跳转的库 配置路由跳转 <HashRouter>//这里使用HashRouter <ErrorBoundary>//React错误边界 <Switch> <Route path="/login" component={Login} /> <Route path="/home" component={Home} /> <Route path="/" component={NotFound} />//404路由或者重定向都可以 </Switch> </ErrorBoundary> </HashRouter> 单页面应用所谓路由跳转,其实最终结果就是: 浏览器的url地址发生变化,但是其实并没有发送请求,也没有刷新整个页面 根据我们配置的路由信息,每次点击切换路由,会切换到不同的组件显示,类似于选项卡功能的实现,但是同时url地址栏会变化 分为HashRouter和BrowserRouter两种模式 自己实现一个粗略的路由跳转: 自己实现传统的Hash模式跳转: hash 就是指 url 后的 # 号以及后面的字符。例如www.baidu.com/#segmentfault,那么#segmentfault就是hash值 ...

August 7, 2025

TCP和UDP

TCP/IP 五层模型 先看图,为网络模型分层,以及一个完整http请求在五层模型中的完整工作流程 应用层:最高层,提供特定于应用程序的协议,运行在该层的协议有HTTP、FTP、SSH、WebScoket等 传输层:为两个主机进程通信提供通用的数据传输协议,如TCP、UDP 网络层:负责寻址和路由功能,将数据包发送到特定的计算机,主要协议是IP协议,路由器就是在这一层 链路层:负责将二进制数据包和网络信号相互转换,交换机、网卡就是在这一层 物理层:主要有接收器、发送器、中继器、光纤电缆等 网络协议通过分层来明确每一层的工作职责,通过定义明确的接口来协同工作,第一层都可以使用下面各层的功能,而不用担心各层是怎么实现的。就好像我们开发封装组件一样,每一个组件各自负责各自的事,互不干扰,也提高了复用度,如上图文件基本传输过程,也就是http分层工作流程: 主机A发起请求,数据发送前会被分为许多片段,称为数据包,然后使用http协议将数据包封装,并加上请求头,传给下一层 传输层拿到数据,为每个数据包分配一个端口号,用来确定目标计算机的哪一个应用程序,然后使用TCP协议进行处理,加上TCP头或UDP头,通过TCP协议传给下一层 网络层拿到数据后为每个数据包添加目标计算机的IP地址,并决定传给什么路由或接收的主机,再封装传给下一层 链路层将数据转译成电子信号,进一步封装成数据帧,传给物理层 物理层通过电缆传送给主机B这边的链路层 主机B的链路层拿到数据后,检查每个包中的目标地址并确定将其发送到哪里,如果不是发给自己的就丢弃,然后根据数据确定协议类型,再传给网络层的IP协议模块 网络层接收到后拆开获取IP头,判断首部接收的IP地址匹配,然后根据头部协议类型,转发TCP或UDP等 传输层TCP收到后会计算校验,判断数据的完整性,然后处理数据包顺序接收的逻辑,最后根据端口确定要转发给应用层的哪个程序 最终应用层接到数据之后,根据http协议解析数据 这里只展开一下网络层的 IP 和传输层的 TCP 、UDP IP 如果是在局域网内都是用MAC地址通信,局域网之外,就得用IP了。MAC就像是身份证,IP就像是住址。所有TCP、UDP、ICMP等数据都是以IP数据报格式进行传输 IP协议本身不支持发往目的地址失败的IP数据包,也没有提供直接的方式获取诊断信息,比如发送途中经过哪些路由器,以及往返时间,而这就由ICMP协议来专门负责 ICMP并不为IP网络提供可靠性,只用于反馈各种故障和配置信息,丢包不会触发ICMP 我们常用的ping就是用ICMP查询报文。不过ping使用ICMP协议会直接跳过了传输层,所以ping程序是没有端口号的 IP协议的特点是: IP协议是不可靠的传输协议。如果 ICMP协议出现传输异常,IP都会丢弃数据包并可能会响应一个ICMP差错消息给发送端,而任何要求可靠性必须由上层如TCP协议来提供 IP协议是无连接的。就是不维护任何关于后续数据的状态信息,每个数据独立。表现在:可以不按发送顺序接收,不用维护连接状态,免去了维护复制的链路状态信息(TCP会讲到) UDP UDP的特点 无连接不需要握手和挥手就可以直接发送数据。 不可靠性:就是一个传递数据的搬运工,来一个包就发一个。不会备份,也不关心对方是否正确收到,传输顺序也无法保证。所以就只能由应用层来保证可靠,因为网络层也是不可靠的 在发送端应用层将数据传给传输层的UDP,它只加一个UDP头标识(UDP协议),就直接发给网络层了。 接收端在网络层将数据发给传输层,传输层UDP只去掉IP报文头就传给应用层了。 其他什么都不会管,不过这也减少开销和发送数据之前的延迟 支持广播:有单播,多播,广播的功能,不只支持一对一传输方式,还支持一对多,多对多的方式 首部开销小:8个字节(源端口号(非必填)、目的端口号、UDP长度(数据报的整个长度)、UDP检查和(检测UDP数据报是否有错或者目的端口找不到对应的进程,各2字节),因为它要求不高而且实现的功能没有那么多,所以首部字段不多,而TCP有20个字节。它的数据是可以为0的,所以它最少可以是8个字节 是面向报文的:适合一次性传输少量数据,因为应用层给UDP多长的报文都会照样发送,即一次发送一个完整的报文,即不合并也不拆分。如果报文太长的话,UDP完整的装进来交给网络层的话,网络层就要分片了,因为传给链路层的话它有一个MTU的要求,所以网络层就要分片,这会给网络层的效率造成影响 无拥塞控制:适合实时应用,因为它会一直以恒定的速度发送数据,即使网络条件不好,也不会对发送速率进行调整。这就导致在网络不好的情况下就有可能丢包,但优点也明显,在某些实时性要求高的场景比如说聊天、在线视频、网络语音电话等使用UDP而不是TCP,比如打微信电话出现偶尔断续不是太大问题。当然拥塞太严重也有一些补救措施比如向前纠错或者重传 UDP 为什么不可靠 传输数据之前不需要先建立连接 不保证消息交付,远程主机的传输层在接收到UDP报文后,不需要确认 不保证将会顺序,不设置包序号、不重排、不会发生队首阻塞 不进行拥塞控制,没有内置反馈机制,不重传、无超时 TCP 这是我们平时用的最多的协议,特别是前后端 TCP给应用程序提供了一种与UDP完全不同的服务 TCP是面向连接的可靠的服务,面向连接指TCP的两个应用程序必须在它们可交换数据之前,通过相互联系来建立一个TCP连接 TCP提供了一种字节流抽象概念给应用程序:不会自动插入记录标志或者消息边界,如发送端分别发10字节和30字节,接收端可能会以两个20字节的方式读入 TCP的特点 是面向连接的,通信之前双方必须要先建立连接 只支持单播,就是点对点的传输,一条TCP连接只能有两个端点 提供可靠交付的服务,有完整性校验、数据不会丢失,会丢包重传、且会按顺序到达 是面向字节流的。不像UDP那样一个个报文独立传输,而是在不保留报文边界的情况下以字节流方式进行传输 提供拥塞控制,当网络出现拥塞的情况,有流量控制,能够减少传输数据的速度和数量,缓解拥塞,保证稳定 提供全双工通信和可靠通信,指的是发送方和接收方可以同时发送数据也可以同时接收数据。因为两边都会设置有发送缓存和接收缓存 发送缓存就是发送缓存的队列里面有准备发送的数据和已经发送但是还没有收到来自接收方确认的数据,如果没有收到确认还要重发所以不能扔掉,将可能会被重传,因为TCP需要保证可靠传输 接收缓存就是按序到达但是还没有被接收应用程序读取的数据和没按序到达的数据,需要顺序排好了,接收方才能逐一接收数据 为什么说 TCP 是可靠的 因为接收方收到数据后会发送一个ACK确认应答消息,这样发送方就知道自己的数据被对方接收了,如果一直没有收到ACK一定时间后就会重发。因此就算数据没有发到接收方,或者接收方的ACK数据包丢失也有重传机制,确保双方最终可以通过重传也能正确收到消息 重传机制 由于TCP的下层网络层可能出现丢失、重复或乱序的情况,TCP协议需要提供可靠数据传输服务。 ...

July 28, 2025

性能优化

本文篇幅较长,将从 编译阶段 -> 路由阶段 -> 渲染阶段 -> 细节优化 -> 状态管理 -> 海量数据源,长列表渲染 方向分别加以探讨。 一 不能输在起跑线上,优化babel配置,webpack配置为项 1 真实项目中痛点 当我们用create-react-app或者webpack构建react工程的时候,有没有想过一个问题,我们的配置能否让我们的项目更快的构建速度,更小的项目体积,更简洁清晰的项目结构。 随着我们的项目越做越大,项目依赖越来越多,项目结构越来越来复杂,项目体积就会越来越大,构建时间越来越长,久而久之就会成了一个又大又重的项目,所以说我们要学会适当的为项目‘减负’,让项目不能输在起跑线上。 2 一个老项目 拿我们之前接触过的一个react老项目为例。我们没有用dva,umi快速搭建react,而是用react老版本脚手架构建的,这对这种老的react项目,上述的问题都会存在,下面让我们一起来看看。 我们首先看一下项目结构。 再看看构建时间。 为了方便大家看构建时间,我简单写了一个webpack,plugin ConsolePlugin ,记录了webpack在一次compilation所用的时间。 const chalk = require('chalk') /* console 颜色 */ var slog = require('single-line-log'); /* 单行打印 console */ class ConsolePlugin { constructor(options){ this.options = options } apply(compiler){ /** * Monitor file change 记录当前改动文件 */ compiler.hooks.watchRun.tap('ConsolePlugin', (watching) => { const changeFiles = watching.watchFileSystem.watcher.mtimes for(let file in changeFiles){ console.log(chalk.green('当前改动文件:'+ file)) } }) /** * before a new compilation is created. 开始 compilation 编译 。 */ compiler.hooks.compile.tap('ConsolePlugin',()=>{ this.beginCompile() }) /** * Executed when the compilation has completed. 一次 compilation 完成。 */ compiler.hooks.done.tap('ConsolePlugin',()=>{ this.timer && clearInterval( this.timer ) const endTime = new Date().getTime() const time = (endTime - this.starTime) / 1000 console.log( chalk.yellow(' 编译完成') ) console.log( chalk.yellow('编译用时:' + time + '秒' ) ) }) } beginCompile(){ const lineSlog = slog.stdout let text = '开始编译:' /* 记录开始时间 */ this.starTime = new Date().getTime() this.timer = setInterval(()=>{ text += '█' lineSlog( chalk.green(text)) },50) } } 构建时间如下: ...

December 17, 2024

表单验证

HTML 表单是用于收集用户输入数据的重要元素,通常用于提交到服务器进行处理。它包括一系列控件,如文本输入框、按钮、复选框、单选按钮、文件上传等。表单通过 form 标签定义,用户可以通过这些控件与网页进行交互。 1. 基础结构 表单通过 <form> 标签创建,表单的 action 属性定义数据提交的目标 URL,method 属性定义提交方式(如 GET 或 POST)。 <form action="/submit" method="POST"> <input type="text" name="username"> <input type="submit" value="Submit"> </form> 2. 表单控件 HTML 表单提供了多种用户输入控件: 文本输入 (<input type="text">, <textarea>) 密码输入 (<input type="password">) 单选框和复选框 (<input type="radio">, <input type="checkbox">) 下拉列表 (<select>) 按钮 (<button>, <input type="submit">) 文件上传 (<input type="file">) 隐藏输入 (<input type="hidden">) 3. 表单的属性 action:指定表单数据提交到的 URL。 method:定义提交方式,常用 GET(将数据附在 URL 后)和 POST(将数据放入 HTTP 请求体中)。 enctype:指定数据的编码类型,常用于文件上传(如 multipart/form-data)。 autocomplete:启用或禁用浏览器的自动完成功能。 4. 表单校验 HTML5 引入了许多新的表单校验功能,能够减少 JavaScript 校验代码的复杂度。校验功能包括: ...

October 14, 2024

浮动、清除浮动和BFC

浮动、清除浮动和BFC是前端开发中常见的概念,它们对于页面布局及美化至关重要。 浮动 在网页设计中,浮动是一种常见的布局技术,可以让元素脱离文档流,我们来看看它有什么作用吧! 使文字环绕 代码示例如下: html结构: <div class="box"></div> <div class="text">一段文字</div> css样式: .box{ width: 200px; height: 200px; background-color: coral; } 若不加浮动,则页面是这样的效果: 但若是在box加上float: left;(向左浮动),则能使文字环绕box这个盒子: .box{ width: 200px; height: 200px; background-color: coral; float: left; } 让块级元素同行显示 块级元素本身是占据一整行的,但浮动能让多个块级元素处于同一行,示例如下: 若不加浮动,则多个块级元素各自占据一行: html结构: <ul> <li>1</li> <li>2</li> <li>3</li> </ul> css样式: *{ margin: 0; padding: 0; } ul li{ list-style:none; width: 200px; height: 100px; font-size: 16px; } li:nth-child(1){/*子容器选择器 */ background-color: rgb(227, 149, 149); } li:nth-child(2){ background-color: rgb(205, 139, 197); } li:nth-child(3){ background-color: rgb(145, 212, 227); } 页面如图: ...

October 10, 2024

自定义指令

如何自定义指令? 其实关于这个问题官方文档上已经有了很好的示例的,我们先来温故一下。 除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。举个聚焦输入框的例子,如下: 当页面加载时,该元素将获得焦点 (注意: autofocus 在移动版 Safari 上不工作)。事实上,只要你在打开这个页面后还没点击过任何内容,这个输入框就应当还是处于聚焦状态。现在让我们用指令来实现这个功能: // 注册一个全局自定义指令 `v-focus` Vue.directive('focus', { // 当被绑定的元素插入到 DOM 中时…… inserted: function (el) { // 聚焦元素 el.focus() } }) 如果想注册局部指令,组件中也接受一个 directives 的选项: directives: { focus: { // 指令的定义 inserted: function (el) { el.focus() } } } 然后你可以在模板中任何元素上使用新的 v-focus property,如下: <input v-focus> 指令内部提供的钩子函数 一个指令定义对象可以提供如下几个钩子函数 (均为可选): bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。 inserted: 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。 update: 所在组件的 VNode 更新时调用,**但是可能发生在其子 VNode 更新之前。**指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。 componentUpdated: 指令所在组件的 VNode 及其子VNode全部更新后调用。 unbind: 只调用一次,指令与元素解绑时调用。 钩子函数参数 指令钩子函数会被传入以下参数: ...

October 5, 2024

WeakMap 和 WeakSet

1. 什么是 WeakSet WeakSet 结构与 Set 类似,也是不重复的值的集合,具备 WeakSet.prototype.add(value) :向 WeakSet 实例添加一个新成员。 WeakSet.prototype.delete(value) :清除 WeakSet 实例的指定成员。 WeakSet.prototype.has(value) :返回一个布尔值,表示某个值是否在 WeakSet 实例之中。 这三个方法。但是,它与 Set 有两个区别。 区别一:WeakSet 的成员只能是对象和Symbol类型,而不能是其他类型的值,我们用代码演示一下 从代码中可以看到,想像Set一样直接往 WeakSet 中添加原始类型化会报错,只能添加对象和Symbol 区别二(这是核心): WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。 (这句解释来自ECMAScript 6 入门,第一眼看到这句话这话会觉得有点晦涩,我们来解释一下) 弱引用:垃圾回收机制有一套自己的回收算法,我们都知道一个函数执行完成后该函数在调用栈中创建的执行上下文会被销毁,这里说的销毁,其实指的就是执行上下文中环境变量、词法变量中的数据存储所占据的内存空间被垃圾回收机制所回收,那么垃圾回收机制不考虑 WeakSet 对该对象的引用是不是就意味着垃圾回收机制不会回收 WeakSet 对象里面的数据所占据的内存呢?不!不是的!代码是最好的解释 我们用 ws 中存放一个对象,然后再将该对象置为null,(这里要说明一下,一个变量被置为null,就意味着这个变量的内存可以被回收了)看着这个打印结果有没有突然明白了点什么,对!没错!只要 WeakSet 结构中的对象不再需要被引用,那么 WeakSet 就直接为空了,这不就意味着WeakSet中的数据所占据的内存被释放了吗。好的,你或许还会有疑问,难道不用 WeakSet 存储数据,结果就不是这样的吗,来打消你的疑虑 不使用 WeakSet 存放数据,当变量obj为null时,fistName依旧是有值的,对比这段代码,我们可以清晰的看出, WeakSet中 - 垃圾回收机制会自动回收该对象所占用的内存 2. 这里有’BUG' 可能已经有小伙伴拔出了他40米长的大剑了,上图二中的代码其实有问题,真实的打印应该是 各位手中的长剑收一收,先别急,其实我们前面聊的没有问题,为什么这里真是打印能出来结果呢?这个问题在国外某技术论坛上也是一度出现在 WeakSet 话题榜首位,解释为:因为浏览器的垃圾回收机制是不受我们控制的,我们无法知道垃圾回收机制什么时候执行,之所以打印能出现内容,其实是因为打印的时候垃圾回收机制没有启动。所以我们可以认为,上图二中的打印在垃圾回收启动之后,它就是空的 我们可以这样来证明它: 如果这样你觉得依旧不够睡服你自己的话,我们把这段代码搬到node中: ...

October 2, 2024

异步编程

浏览器中的JavaScript程序是典型的事件驱动型程序,即它们会等待用户触发后才真正的执行,而基于的JavaScript的服务器通常要等待客户端通过网络发送请求,然后才能执行。这种异步编程在JavaScript是很常见的,下面就来介绍几个异步编程的重要特性,它们可以使编写异步代码更容易。 本文将按照异步编程方式的出现时间来归纳整理: 一、什么是异步 下面先来看看同步和异步的概念: 同步: 在执行某段代码时,在没有得到返回结果之前,其他代码暂时是无法执行的,但是一旦执行完成拿到返回值,即可执行其他代码。也就是说,在此段代码执行完未返回结果之前,会阻塞之后的代码执行,这样的情况称为同步。 异步: 当某一代码执行异步过程调用发出后,这段代码不会立刻得到返回结果。而是在异步调用发出之后,一般通过回调函数处理这个调用之后拿到结果。异步调用发出后,不会影响阻塞后面的代码执行,这样的情况称为异步。 下面来看一个例子: // 同步 function syncAdd(a, b) { return a + b; } syncAdd(1, 2) // 立即得到结果:3 // 异步 function asyncAdd(a, b) { setTimeout(function() { console.log(a + b); }, 1000) } asyncAdd(1, 2) // 1s后打印结果:3 这里定义了同步函数 syncAdd 和异步函数 asyncAdd,调用 syncAdd(1, 2) 函数时会等待得到结果之后再执行后面的代码。而调用 asyncAdd(1, 2) 时则会在得到结果之前继续执行,直到 1 秒后得到结果并打印。 我们知道,JavaScript 是单线程的,如果代码同步执行,就可能会造成阻塞;而如果使用异步则不会阻塞,不需要等待异步代码执行的返回结果,可以继续执行该异步任务之后的代码逻辑。因此,在 JavaScript 编程中,会大量使用异步。 那为什么单线程的JavaScript还能实现异步呢,其实也没有什么魔法,只是把一些操作交给了其他线程处理,然后采用了事件循环的机制来处理返回结果。 二、回调函数 在最基本的层面上,JavaScript的异步编程式通过回调实现的。回调的是函数,可以传给其他函数,而其他函数会在满足某个条件时调用这个函数。下面就来看看常见的不同形式的基于回调的异步编程。 1. 定时器 一种最简单的异步操作就是在一定时间之后运行某些代码。如下面代码: setTimeout(asyncAdd(1, 2), 8000) setTimeout()方法的第一个参数是一个函数,第二个参数是以毫秒为单位的时间间隔。asyncAdd()方法可能是一个回调函数,而setTimeout()方法就是注册回调函数的函数。它还代指在什么异步条件下调用回调函数。setTimeout()方法只会调用一次回调函数。 2. 事件监听 给目标 DOM 绑定一个监听函数,用的最多的是 addEventListener: document.getElementById('#myDiv').addEventListener('click', (e) => { console.log('我被点击了') }, false); 通过给 id 为 myDiv 的一个元素绑定了点击事件的监听函数,把任务的执行时机推迟到了点击这个动作发生时。此时,任务的执行顺序与代码的编写顺序无关,只与点击事件有没有被触发有关。 ...

October 1, 2024

状态管理工程化

状态管理是一个前端界老生常谈的话题了,所有前端框架的发展历程中都离不开状态管理的迭代与更替,对于react来说呢,整个状态管理的发展也随着react架构的变更和新特性的加入而不停的做调整,作为一个一起伴随react成长了快5年的开发者,经历过reflux、redux、mobx,以及其他redux衍生方案dva、mirror、rematch等等后,我觉得它们都不是我想要的状态管理的终极形态,所以为了打造一个和react结合得最优雅、使用起来最简单、运行起来最高效的状态管理方案,踏上了追梦旅途。 为何需要状态管理 为何需要在前端引用里引入状态管理,基本上大家都达成了共识,在此我总结为3点: 随着应用的规模越来越大,功能越来越复杂,组件的抽象粒度会越来越细,在视图中组合起来后层级也会越来越深,能够方便的跨组件共享状态成为迫切的需求。 状态也需要按模块切分,状态的变更逻辑背后其实就是我们的业务逻辑,将其抽离出来能够彻底解耦ui和业务,有利于逻辑复用,以及持续的维护和迭代。 状态如果能够被集中的管理起来,并合理的派发有利于组件按需更新,缩小渲染范围,从而提高渲染性能 已有状态管理方案现状 redux 遵循react不可变思路的状态管理方案,无论从git的star排名还是社区的繁荣度,首推的一定是redux这个react界状态管理一哥,约束使用唯一路径reducer纯函数去修改store的数据,从而达到整个应用的状态流转清晰、可追溯。 image.png mbox 遵循响应式的后期之秀mbox,提出了computed、reaction的概念,其官方的口号就是任何可以从应用程序状态派生的内容都应该派生出来,通过将原始的普通json对象转变为可观察对象,我们可以直接修改状态,mbox会自动驱动ui渲染更新,因其响应式的理念和vue很相近,在react里搭配mobx-react使用后,很多人戏称mobx是一个将react变成了类vue开发体验的状态管理方案。 image.png 当然因为mbox操作数据很方便,不满足大型应用里对状态流转路径清晰可追溯的诉求,为了约束用户的更新行为,配套出了一个mobx-state-tree,总而言之,mobx成为了响应式的代表。 其他 剩下的状态管理方案,主要有3类。 一类是不满足redux代码冗余啰嗦,接口不够友好等缺点,进而在redux之上做2次封装,典型的代表国外的有如rematch,国内有如dva、mirror等,我将它们称为redux衍生的家族作品,或者是解读了redux源码,整合自己的思路重新设计一个库,如final-state、retalk、hydux等,我将它们称为类redux作品。 一类是走响应式道路的方案,和mobx一样,劫持普通状态对象转变为可观察对象,如dob,我将它们称为类mobx作品。 剩下的就是利用react context api或者最新的hook特性,主打轻量,上手简单,概念少的方案,如unstated-next,reactn、smox、react-model等。 我心中的理想方案 上述相关的各种方案,都各自在一定程度上能满足我们的需求,但是对于追求完美的水瓶座程序猿,我觉得它们终究都不是我理想的方案,它们或小而美、或大而全,但还是不够强,不够友好,所以决定开始自研状态管理方案。 我知道小和 美、全、强本身是相冲突的,我能接受一定量的大,gzip后10kb到20kb都是我接受的范围,在此基础上,去逐步地实现美、全、强,以便达到以下目的,从而体现出和现有状态管理框架的差异性、优越性。 让新手使用的时候,无需了解新的特性api,无感知状态管理的存在,使其遁于无形之中,仅按照react的思路组织代码,就能享受到状态管理带来的福利。 让老手可以结合对状态管理的已有认知来使用新提供的特性api,还原各种社区公认的最佳实践,同时还能向上继续探索和提炼,挖掘状态管理带来的更多收益。 在react有了hook特性之后,让class组件和function组件都能够享有一致的思路、一致的api接入状态管理,不产生割裂感。 在保持以上3点的基础上,让用户能够使用更精简且更符合思维直觉的组织方式书写代码,同时还能够获得巨大的性能提升收益。 为了达成以上目标,立项concent,将其定义为一个可预测、零入侵、渐进式、高性能的增强型状态管理方案,期待能把他打磨成为一个真真实实让用户用起来感觉到美丽、全面、强大的框架。 说人话就是:理解起来够简单、代码写起来够优雅、工程架构起来够健壮、性能用起来够卓越…… ^_^ concent.png 可预测 react是一个基于pull based来做变化侦测的ui框架,对于用户来说,需要显式的调用setState来让react感知到状态变化,所以concent遵循react经典的不可变原则来体现可预测,不使用劫持对象将转变为可观察对象的方式来感知状态变化(要不然又成为了一个类mobx……), 也不使用时全局pub&sub的模式来驱动相关视图更新,同时还要配置各种reselect、redux-saga等中间件来解决计算缓存、异步action等等问题(如果这样,岂不是又迈向了一个redux全家桶轮子的不归路….. ) 吐槽一下:redux粗放的订阅粒度在组件越来越多,状态越来越复杂的时候,经常因为组件订阅了不需要的数据而造成冗余更新,而且各种手写mapXXXToYYY很烦啊有木有啊有木有,伤不起啊伤不起…… 零入侵 上面提到了期望新手仅按照react的思路组织代码,就能够享受到状态管理带来的福利,所以必然只能在setState之上做文章,其实我们可以把setState当做一个下达渲染指令重要入口(除此之外,还有forceUpdate)。 setState,下达更新指令 仔细看看上图,有没有发现有什么描述不太准确的地方,我们看看官方的setState函数签名描述: 代码语言:txt AI代码解释 setState<K extends keyof S>( state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null), callback?: () => void ): void; 通过签名描述,我们可以看出传递给setState的是一个部分状态(片段状态),实际上我们在调用setState也是经常这么做的,修改了谁就传递对应的stateKey和值。 ...

August 7, 2025

网络延迟和丢包

1、什么是延迟呢? 延迟其实就是我们在网页浏览或者使用应用时,从我们点击请求到服务器返回结果给我们之间的时间差。就像你在跟朋友打电话,你说完话后,朋友听到并回应你所说话的时间差一样。 我们的最终目标是创建一个系统,让这个时间差变得尽可能短,也就是实现零延迟。但现实世界中,有各种各样的问题会导致系统出现延迟。如果系统的延迟很低,那么我们请求得到响应的时间就会很短。每次你在浏览器中输入网址或者点击一个链接,浏览器都会向服务器发出一个请求信号,然后服务器需要处理这个请求,获取需要的信息,最后把这些信息返回给你的浏览器。整个过程中就会有一些时间差,这就是延迟。所以,我们要不断努力降低延迟,提高系统的响应速度。 2、延迟是怎么回事呢? 延迟其实就是你在请求后需要等待的时间,就像等待快递送到家门一样。来看个例子,更容易理解它是怎么运作的。 想象你正在和一个电子商务网站(比如淘宝)互动,你喜欢一个商品,然后把它加入购物车。现在,当你点击“添加到购物车”按钮时,下面的事情会依次发生: 你点击了“添加到购物车”按钮,这时就像你启动了一个计时器,浏览器开始向服务器发请求。 服务器收到请求,然后开始处理它,就像你的快递订单到了快递中心一样。 服务器处理完后,回应你的请求,信息到达你的浏览器,商品成功添加到购物车中,就像你的包裹送到了家门口一样。 你可以想象在第一步按下了计时器的启动按钮,然后在最后一步停下,这段时间就是延迟。希望这个例子能让你更容易理解延迟是如何运作的。 3、延迟都是怎么来的呢? 现在,你应该已经理解了要点,但是你知道是什么造成了延迟吗?网络中的延迟受多种因素影响,它们在确定延迟的具体数值时扮演着关键角色。其中一个主要因素是出站呼叫。回到之前添加购物车的例子,当你点击浏览器上的按钮时,请求会发送到后端的某个服务器,这个服务器可能会在内部调用多个服务来进行计算(可能是同时或者按顺序),然后等待它们的响应或将它们汇总。所有这些因素都会增加呼叫的延迟。但总结起来,主要由以下几个因素引起: 传输介质: 传输介质指的是信息在起点和终点之间的物理路径。系统的延迟会取决于用于传输请求的介质类型。广域网、光纤电缆等传输介质都广泛应用,但每种介质都有自己的限制,这会影响延迟。 传播延迟: 这指的是数据包从一个源传播到另一个源所需的时间。系统的延迟很大程度上取决于通信节点之间的距离。节点距离越远,系统的延迟就会越高。 路由器: 路由器在通信中扮演着重要的角色,它们需要一些时间来分析数据包的标头信息。延迟取决于路由器处理请求的效率。每一次路由器到路由器的跳跃都会增加系统的延迟。 存储延迟: 系统的延迟还受到所使用的存储系统类型的影响,因为处理和返回数据可能需要一些时间。因此,访问存储中的数据会增加系统的延迟。 4、如何测量延迟? 要量化延迟其实很简单,我们有几种常用的方法,让我们来看看最常见的三种: Ping(网络探测): Ping是测量延迟最常用的工具之一。它的原理是向目标地址发送一个小数据包,然后查看接收到响应所需的时间。更快的Ping意味着连接更敏捷,响应更迅速。 Traceroute(路径跟踪): Traceroute是另一个用于测试延迟的工具。它也使用数据包,但不止如此,它还会逐一记录数据包从源到目的地经过的每个中间节点所需的时间。这有助于识别网络中的延迟点。 MTR(网络诊断工具): MTR是Ping和Traceroute的超级组合。MTR提供了详尽的报告,列出了从一个端点到另一个端点所需的每个网络节点的信息。这份报告通常包括了各种细节,比如丢包率、平均延迟等,非常有助于分析网络性能。 5、延迟优化 延迟是系统性能的绊脚石,所以我们需要采取一些措施来进行优化。下面是一些简单又实用的方法,可以帮助我们减少延迟: 采用HTTP/2: 使用HTTP/2协议可以显著减少延迟。它支持并行传输,最大程度地减少了数据从发送方到接收方的往返次数,这对于降低延迟非常有效。 减少外部HTTP请求: 第三方服务会增加延迟。通过减少外部HTTP请求的数量,我们可以提高系统的响应速度和质量。 使用CDN: 内容分发网络(CDN)被证明能够减少延迟。CDN会在全球多个位置缓存资源,从而减少请求和响应的传输时间。这意味着可以从更接近客户端的缓存位置获取请求,而不必每次都回到原始服务器。 浏览器缓存: 利用浏览器缓存,可以减少向服务器发送的请求次数,从而降低延迟。浏览器会在本地缓存特定资源,这对于提高页面加载速度很有帮助。 优化磁盘I/O: 为了减小磁盘I/O的影响,我们需要优化算法,尽量减少频繁的磁盘写入操作。可以考虑使用直写式缓存、内存数据库,或者在适当的情况下进行写入合并,还可以考虑使用快速存储系统,比如SSD。 作为开发人员,我们还可以在应用程序级别采取一些方法来优化延迟: 避免低效算法: 高效的算法是代码中延迟的主要来源之一。要尽量避免不必要的循环或昂贵的嵌套操作。 避免锁定的设计模式: 锁定会引入延迟,因此我们应该采用避免锁定的设计模式,特别是在多线程环境中。 采用异步编程模型: 异步编程可以更好地利用硬件资源,因为它避免了阻塞操作,从而减少等待时间。 限制无界队列深度: 限制无界队列深度并提供反压通常可以减少代码中的等待时间,从而产生更可预测的延迟。 这些方法可以帮助我们优化延迟,提高系统性能,让用户获得更好的体验。 常见考点 在前端面试中,网络延迟和丢包是评估你对网络传输、性能瓶颈及应对策略理解的重要考点。面试官会通过这些话题判断能否从网络层面分析问题、优化用户体验,特别是在弱网环境、移动端场景下。 一、网络延迟的考点 1. 网络延迟的构成 面试官可能会问:“用户输入 URL 后,请求延迟可能发生在哪些阶段?” 阶段 含义 DNS 解析 域名 → IP 地址 TCP 建立连接 三次握手耗时 TLS 握手 HTTPS 建立安全连接耗时 请求发送 客户端发送数据 首字节返回 TTFB 服务端处理 + 网络传输 内容下载 响应内容下载耗时 渲染耗时 浏览器解析和绘制页面 2. 常见影响网络延迟的因素 地域距离:客户端与服务端物理距离大(如中国访问美国服务器) DNS 缓存未命中或 DNS 配置不合理 TLS 握手过长(HTTPS 开销) 带宽瓶颈:文件过大、网络拥堵 长连接未复用(未启用 HTTP/2) 移动网络抖动高(4G/5G 网络波动) 首包延迟(TTFB 过高) 3. 如何优化延迟? 启用 CDN,部署全球节点,减少 RTT 启用 DNS 预解析:<link rel="dns-prefetch"> 启用 HTTP/2 或 HTTP/3,减少连接耗时 减少重定向跳转、合并请求 使用 preconnect、prefetch 提前连接目标源 SSR 提前输出首屏 HTML,减少白屏 缓存优化(减少服务端响应压力) 二、丢包的考点 1. 丢包的本质 网络传输中,部分数据包因链路拥堵、信号弱、硬件丢包率高等原因,未能到达目的地。 ...

July 28, 2025