高阶函数

高阶函数是指那些可以接受函数作为参数或者返回函数作为结果的函数。这种特性使得JavaScript的函数变得异常灵活和强大,为开发者提供了丰富的工具和手段来编写更加简洁、可重用和模块化的代码。 本文将详细介绍JavaScript高阶函数的概念、特性、常见用法以及在实际开发中的应用。 一、高阶函数的概念 高阶函数是函数式编程中的一个核心概念。在JavaScript中,函数是一等公民,这意味着函数可以像其他数据类型(如数字、字符串、对象等)一样被传递和操作。高阶函数正是利用了这一特性,使得函数可以作为参数传递给其他函数,或者作为其他函数的返回值。 二、高阶函数的特性 函数作为参数:高阶函数可以接受一个或多个函数作为参数。这使得我们可以将特定的行为(即函数)作为参数传递给高阶函数,从而实现更加灵活和可重用的代码。 函数作为返回值:高阶函数可以返回一个函数作为结果。这种特性允许我们创建可以动态生成新函数的函数,这些新函数可以根据需要执行不同的行为。 闭包:闭包是高阶函数的一个重要特性。闭包允许函数访问其外部作用域中的变量,即使该函数在其外部作用域之外执行。这使得高阶函数可以捕获和存储状态,从而创建更加复杂和强大的函数行为。 三、高阶函数的常见用法 数组方法:JavaScript中的数组方法(如map、filter、reduce等)都是高阶函数的典型例子。这些方法接受一个函数作为参数,并对数组中的每个元素执行该函数,从而返回一个新的数组或单一结果。 map方法:用于创建一个新数组,其结果是该数组中的每个元素调用一个提供的函数后返回的结果。 filter方法:创建一个新数组,其包含通过所提供函数实现的测试的所有元素。 reduce方法:对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。 函数组合:函数组合是指将多个函数组合在一起形成一个新的函数。通过高阶函数,我们可以轻松地将多个函数组合在一起,从而创建一个更加复杂和强大的函数。这种技术在实际开发中非常有用,特别是当我们需要处理一系列转换或操作时。 柯里化:柯里化是一种将接收多个参数的函数转换为一系列接收单一参数的函数的技术。虽然柯里化本身不是高阶函数,但高阶函数在实现柯里化时扮演着重要角色。通过高阶函数,我们可以创建一个接受原始函数作为参数的柯里化函数,并返回一个新的函数来处理单一参数。 创建高阶函数:我们可以编写自己的高阶函数来实现特定的行为。例如,我们可以编写一个高阶函数来接受一个函数和一个数组作为参数,并返回该数组中应用该函数后的结果。 四、高阶函数在实际开发中的应用 1. 数据转换 高阶函数非常适合用于数据的转换和映射。例如,我们可以使用Array.prototype.map方法将一个数组中的每个元素转换为另一种形式。 // 定义一个高阶函数,它接受一个转换函数和一个数组作为参数 const transformArray = (transformFn, array) => array.map(transformFn); // 定义一个简单的转换函数,将数字转换为它的平方 const square = x => x * x; // 使用高阶函数和转换函数来处理数组 const numbers = [1, 2, 3, 4, 5]; const squaredNumbers = transformArray(square, numbers); console.log(squaredNumbers); // 输出: [1, 4, 9, 16, 25] 在这个例子中,transformArray是一个高阶函数,它接受一个转换函数transformFn和一个数组array作为参数。然后,它使用Array.prototype.map方法来遍历数组,并对每个元素应用转换函数。 2. 事件处理 在Web开发中,事件处理是一个常见的任务。通过高阶函数,我们可以创建更加灵活和可重用的事件处理器。 // 定义一个高阶函数,它接受一个事件处理函数和一个事件类型作为参数 const createEventHandler = (handlerFn, eventType) => event => { if (event.type === eventType) { handlerFn(event); } }; // 定义一个简单的事件处理函数,用于打印事件信息 const logEvent = event => console.log(`Event type: ${event.type}, Target: ${event.target}`); // 创建一个点击事件处理器 const handleClick = createEventHandler(logEvent, 'click'); // 假设我们有一个DOM元素,我们可以为它添加事件监听器 // const button = document.querySelector('button'); // button.addEventListener('click', handleClick); // 为了演示,我们直接调用处理器并传递一个模拟的事件对象 const mockEvent = { type: 'click', target: document.body }; handleClick(mockEvent); // 输出: Event type: click, Target: [object HTMLBodyElement] 在这个例子中,createEventHandler是一个高阶函数,它接受一个事件处理函数handlerFn和一个事件类型eventType作为参数,并返回一个新的函数。这个新的函数会检查传入的事件对象的类型是否与指定的事件类型匹配,如果匹配,则调用事件处理函数。 ...

October 1, 2024

适配器模式

定义 适配器模式(Adapter Pattern)是一种结构型设计模式,它将一个类的接口转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作。 适配器模式的核心思想是:通过一个中间层(适配器)来使不兼容的接口能够一起工作,而无需修改原有代码。 为什么需要适配器模式 适配器模式要解决的核心问题是:让接口不兼容的类能够一起工作,而不需要修改原有代码。 问题场景:假设我们的App原来使用的是自己封装的网络库,现在想要切换到Alamofire,但项目中有大量地方直接使用了旧接口。 // 旧的网络接口 - 项目中到处都在使用 protocol OldNetworkClient { func get(url: String, callback: @escaping (Data?, Error?) -> Void) func post(url: String, body: Data?, callback: @escaping (Data?, Error?) -> Void) } // 新引入的Alamofire有完全不同的接口 // AF.request(url).response { response in ... } 最直接的方式是修改所有调用处: // 需要修改项目中所有使用旧接口的地方 // 从: oldClient.get(url: "https://api.example.com/users") { data, error in ... } // 改为: AF.request("https://api.example.com/users").response { response in ... } 这种方式有什么问题? 工作量巨大:需要修改项目中所有使用网络请求的地方 风险高:大量修改容易引入bug 难以回退:如果新库有问题,很难切换回旧实现 违反开闭原则:修改了大量现有代码 适配器模式的解决思路: 创建一个适配器,让新库"伪装"成旧接口,现有代码无需修改: // 适配器 - 让Alamofire适配旧接口 class AlamofireAdapter: OldNetworkClient { func get(url: String, callback: @escaping (Data?, Error?) -> Void) { // 内部使用Alamofire,但对外暴露旧接口 AF.request(url).response { response in callback(response.data, response.error) } } func post(url: String, body: Data?, callback: @escaping (Data?, Error?) -> Void) { AF.request(url, method: .post, parameters: nil, encoding: URLEncoding.default) .response { response in callback(response.data, response.error) } } } // 现有代码完全不需要修改 let client: OldNetworkClient = AlamofireAdapter() client.get(url: "https://api.example.com/users") { data, error in // 原有的处理逻辑保持不变 } 适配器模式的好处: ...

June 21, 2026

Swift 宏

Swift 5.9(WWDC 2023)引入的宏(Macro)是近几年语言层最大的一次能力扩展。它让 Swift 第一次具备了编译期代码生成的官方机制——你可以在源码层面写一段"能生成代码的代码",编译器会在类型检查前把它展开成真正的 Swift 源码,再走正常的编译流程。SwiftUI 的 #Preview、Observation 框架的 @Observable、SwiftData 的 @Model、Swift Testing 的 @Test / #expect、FoundationModels 的 @Generable / @Tool,全都是宏。 一、为什么要有宏 1.1 Swift 之前能做什么,又差在哪 在宏之前,Swift 已经有一整套"元编程"手段,但每一种都有明显的边界: 机制 做什么 局限 @propertyWrapper 给属性注入行为(如 @Published、@State) 只能包属性,不能生成方法、不能生成类型、不能访问其他成员 @resultBuilder 把一段 DSL 收集成表达式(如 SwiftUI 的 ViewBuilder) 只作用于闭包里,无法改类型定义 KeyPath + Mirror 运行时反射读写属性 只能读,类型安全差,性能差(见 iOS反射) Codable 等编译器魔法 自动合成 init(from:) / encode(to:) 只有编译器内部能写,开发者写不出同等能力的东西 Sourcery / SwiftGen 文本级代码生成 游离于编译器之外,无类型信息、无法访问 AST Objective-C #define 纯文本替换 没有作用域、没有类型、没有卫生(hygiene),对调试器和 IDE 极不友好 宏就是要一次性补齐这块能力:在保留类型安全与 IDE 体验的前提下,把"只有 Swift 编译器团队能写的代码生成"开放给所有开发者。 ...

May 4, 2026

神经网络原理

想象你手写了一个数字"3",然后拿给计算机看——它是怎么"认出"这是3而不是8的?这个看似简单的问题,背后隐藏着整个深度学习的核心思想。让我们从最直觉的方式开始,一层一层揭开神经网络的面纱。 一、从像素到数字:问题的本质 1.1 手写数字识别 假设我们有一张28x28像素的灰度手写数字图片。每个像素是一个0到1之间的数字——0代表全黑,1代表全白。这意味着一张图片其实就是一个包含784个数字的列表。 图片 → [0.0, 0.0, 0.12, 0.85, 0.93, ..., 0.0] (784个数字) 我们的目标是设计一个函数,输入这784个数字,输出10个数字,分别代表这张图是0~9每个数字的概率: f(784个像素值) → [P(0), P(1), P(2), ..., P(9)] 比如输入一张手写的"3",我们期望输出类似: [0.01, 0.02, 0.05, 0.89, 0.01, 0.01, 0.00, 0.01, 0.00, 0.00] ^^^^ "3"的概率最高 这就是神经网络要解决的问题。但关键问题是:这个函数 f 长什么样? 1.2 为什么传统编程行不通 你可能会想:能不能写一堆if-else规则?比如"如果上方有一个圆弧,下方也有一个圆弧,那就是3"。 问题在于:人类写字的方式千变万化。同一个"3",有人写得圆润,有人写得尖锐,有人偏左,有人偏右。你几乎不可能用明确的规则覆盖所有情况。 我们需要的是一种从数据中自动学习规则的方法——这就是神经网络。 二、神经网络的结构 2.1 神经元:最基本的单元 让我们从最简单的单元开始。一个神经元本质上就是一个持有数字的容器。这个数字叫做激活值(activation),取值范围通常在0到1之间。 你可以把它想象成一个灯泡:0表示完全熄灭,1表示完全点亮,中间的值表示不同亮度。 2.2 层:神经元的组织方式 神经网络把神经元组织成层(layer): graph LR subgraph 输入层 [输入层 - 784个神经元] I1[像素1] I2[像素2] I3[...] I784[像素784] end subgraph 隐藏层1 [隐藏层1 - 16个神经元] H1_1[h1] H1_2[h2] H1_3[...] H1_16[h16] end subgraph 隐藏层2 [隐藏层2 - 16个神经元] H2_1[h1] H2_2[h2] H2_3[...] H2_16[h16] end subgraph 输出层 [输出层 - 10个神经元] O0[0] O1[1] O2[...] O9[9] end I1 --> H1_1 I1 --> H1_2 I784 --> H1_16 H1_1 --> H2_1 H1_16 --> H2_16 H2_1 --> O0 H2_16 --> O9 输入层:784个神经元,每个对应一个像素值 隐藏层:中间的层(这里用2层,每层16个神经元) 输出层:10个神经元,对应数字0~9 为什么叫"隐藏层"?因为在训练数据中,我们只知道输入(像素)和输出(标签),中间层在"学什么"我们并不直接指定——它们是被自动学出来的。 ...

May 2, 2026

单页面架构

传统的多页面应用构建方式: 纯服务端渲染,前后端不分离,使用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