LOADING

加载过慢请开启缓存 浏览器默认开启

耀耀切克闹

前端代码规范

一、命名规范

文件命名:

  • 所有文件名统一使用小写,首页命名为index.xxx,文件名禁止特殊字符比如空格、$等。统一使用英文单词或拼音缩写,必须小写。
  • 文件名包含多个单词时,单词之间建议使用半角的连词线 ( - ) 分隔。
  • 文件目录结构嵌套层级不要过深

变量命名:

采用小驼峰式命名规范,类型+对象描述的方式,如果没有明确的类型,就可以使前缀为名词。

array a
boolean b
function fn
int i
object o
regular r
string s

函数命名:

采用小驼峰式命名(构造函数大驼峰式),前缀为动词

动词 含义
can 判断是否可执行某个动作 ( 权限 )
has 判断是否含有某个值
is 判断是否为某个值
get 获取某个值
set 设置某个值

常量命名:

全部大写命名规范,使用大写字母和下划线来组合命名,下划线用以分割单词

类命名:

  • 公共属性和方法 : 同变量命名方式
  • 私有属性和方法 : 前缀为下划线(_或#)后面跟公共属性和方法一样的命名方式

文件目录结构:

同功能放在同一个文件目录下,目录结构不要嵌套过深,文件名语义化一些,方便后续维护

  • 文件夹名称全部采用小写+”-“ 来隔开;
  • 避免多层嵌套,单个项目中的目录嵌套控制在最多三到四个层级内;

二、代码规范

JS:

JS/TS主流的大致有这几种

名称 说明
命名规范 1. 普通命名采用小驼峰式命名 2. 命名是复数的时候需要加s 3. 命名需要符合语义化,尽量减少缩写的情况发生,做到见名知意,如果函数命名,可以采用加上动词前缀
变量规范 1. 变量定义尽量使用const、let 2. 变量兜底
字符串 1. 统一使用单引号而不是双引号,配置安装eslint后,保存会自动格式化处理 2. 用字符串模板而不是’+’来拼接字符串 3. 不要使用不必要的转义字符 4. 不要在字符串中用eval(),漏洞太多
数组 1.用扩展运算符(…)做数组浅拷贝2.使用数组解构
对象 1. ES6 使用属性值缩写,将属性的缩写放在对象声明的开头 2. 对象浅拷贝时,更推荐使用扩展运算符 …使用对象解构
函数 1. 函数参数使用默认值替代使用条件语句进行赋值 2. 函数参数越少越好,如果参数超过两个,要使用 ES6 的解构语法,不用考虑参数的顺序。把默认参数赋值放在最后 3. 尽量使用箭头函数 4. 用命名函数表达式而不是函数声明,函数声明作用域会提升,降低了代码可读性和可维护性 5. 不要改参数,不要对参数重新赋值 6. 功能函数使用纯函数,输入一致,输出结果永远唯一 7. 优先使用函数式编程
for循环 使用for循环过程中,数组的长度,使用一个变量来接收, 有利于代码执行效率得到提高,而不是每走一次循环,都得重新计算数组长度

VUE:

遵循vue.js官方风格指南:https://vuejs.bootcss.com/style-guide/

组件:

名称 说明
组件命名 1. 组件名为多个单词,命名为组件用途,完整单词的组件名(倾向于完整单词而不是缩写) 2. 文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case),例如:todo-item或TodoItem 3. 基础组件名, 应用特定样式和约定的基础组件 (也就是展示类的、无逻辑的或无状态的组件) 应该全部以一个特定的前缀开头,比如Base、App或V 4. 单例组件名,只应该拥有单个活跃实例的组件应该以The前缀命名,以示其唯一性 5. 紧密耦合的组件名, 和父组件紧密耦合的子组件应该以父组件名作为前缀命名 6. 组件名中的单词顺序, 组件名应该以高级别的 (通常是一般化描述的) 单词开头,以描述性的修饰词结尾 7.模板中的组件名大小写,对于绝大多数项目来说,在单文件组件和字符串模板中组件名应该总是PascalCase的——但是在DOM模板中总是kebab-case的 8. JS/JSX中的组件名大小写,JS/JSX 中的组件名应该始终是 PascalCase 的,尽管在较为简单的应用中只使用Vue.component进行全局组件注册时,可以使用kebab-case字符串
组件数据 组件的data必须是一个函数
props定义 1. prop定义应该尽量详细 2. prop名大小写,在声明prop的时候,其命名应该始终使用camelCase,而在模板和JSX中应该始终使用kebab-case
v-for使用 1. 为v-for设置键值,尽量避免使用index作为key 2. 避免v-if和v-for用在一起
样式 为组件样式设置作用域,使用scoped属性,使用BEM约定
私有property名 1. Vue使用前缀来定义其自身的私有 property 2. 推荐使用$ ,作为一个用户定义的私有property的约定,以确保不会和Vue自身相冲突
自闭合组件 在单文件组件、字符串模板和JSX中没有内容的组件应该是自闭合的——但在DOM模板里永远不要这样做
attribute 1. 多个attribute的元素应该分多行撰写,每个attribute一行 2. 带引号的attribute值
模板中简单的表达式 组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法
简单的计算属性 应该把复杂计算属性分割为尽可能多的更简单的property,计算属性不能产生副作用
指令 指令缩写 (用:表示 v-bind:、用@表示 v-on: 和用#表示 v-slot:)
组件通信 应该优先通过prop和事件进行父子组件之间的通信,而不是this.$parent或变更prop
事件、定时器 清除定时器或者事件监听
代码文件 开发过程中单个文件不允许超过600行,特别复杂的功能,文件不允许超过1000行

CSS

stylelint插件,规范则推荐使用stylelint-config-standard

BEM命名规则

  • block:模块,名字单词间用-连接
  • element:元素,模块的子元素,以__与block连接
  • modifier:修饰,模块的变体,定义特殊模块,以–与block连接

有效使用css选择器,需遵循以下原则:

  • 保持简单,不要使用嵌套过多过于复杂的选择器,选择器嵌套应少于3级;
  • 通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用;
  • 避免使用CSS表达式;
  • 慎重选择高消耗的样式(高消耗属性在绘制前需要浏览器进行大量计算),避免重绘重排;
  • css选择器中避免使用标签名;
  • 尽量使用缩写属性;
  • 使用子选择器;
  • 0后面不带单位;
  • id和class,命名名称语义化,不要过于简单,防止模块之间样式互相影响;合理的使用id,一般情况下id不应该被用于样式,并且id的权重很高,所以不使用id解决样式的问题,而是使用class;

三、项目实战配置

Husky+eslint+ prettier规范代码 | 耀耀切克闹 (yaoyaoqiekenao.com)

阅读全文

element-plus表格合并(例如前两列合并)

项目搭建 2023/7/19

前言

做了一个后台管理日清的列表,不上线,只发布到测试环境,只供开发人员自己看的,方便了解啥时候结束联调,测试,上线等,其中有一个表格合并的功能做一下记录

核心代码

<template>
  <div>
    <el-table :data="dataList" :span-method="objectSpanMethod">
      <el-table-column label="日期" prop="createTime"></el-table-column>
      <el-table-column label="业务线" prop="projectType"></el-table-column>
      <!-- 其他列定义 -->
    </el-table>
  </div>
</template>
const  objectSpanMethod =({ row, column, rowIndex, columnIndex })=> {
const dataList = [.....] //具体的表格数据
//columnIndex列,前两列
      if (columnIndex === 0 || columnIndex === 1) {
        if (rowIndex === 0 || (rowIndex > 0 && row[column.property] !==dataList[rowIndex - 1][column.property])) {
          let rowspan = 1;
          let colspan = 1;
          let i = 1;
          while (
            rowIndex + i < dataList.length &&
            row[column.property] === dataList[rowIndex + i][column.property]
          ) {
            rowspan++;
            i++;
          }
          return { rowspan, colspan };
        } else if (row[column.property] === dataList[rowIndex - 1][column.property]) {
          return { rowspan: 0, colspan: 0 };
        }
}

具体思路逻辑

  1. 首先,我们检查当前列 columnIndex 是否为 0 或者 1,以确定我们只对前两列进行合并操作。

  2. 对于第一行(rowIndex === 0),我们返回 { rowspan: 1, colspan: 1 },表示不进行合并。

  3. 对于其他行(rowIndex > 0),我们根据合并逻辑进行判断。

    • 首先,我们检查当前单元格的值 row[column.property](例如,对于 “日期” 列,我们通过 column.property 访问 createTime 属性)是否与上一行的值相同,以及当前列是否为 “日期” 或者与上一行的 “业务线” 相同。如果满足这些条件,说明需要进行合并,我们返回 { rowspan: 0, colspan: 0 },表示该单元格不需要合并。
    • 如果不满足上述条件,我们开始计算正确的合并范围。我们初始化 rowspan 为 1,colspan 为 1,并使用变量 i 设置初始值为 1。
    • 接下来,我们使用一个循环,来寻找连续相同数据的范围。在循环中,我们依次检查当前行之后的每一行,判断它们的值是否与当前行相同,并且在 “日期” 列或者与当前行的 “业务线” 相同时。如果满足这些条件,我们将 rowspan 值增加 1,并递增 i。
    • 最后,我们返回计算出的 { rowspan, colspan } 值,表示正确的合并范围。

通过这些逻辑,我们可以准确地合并相同数据的行,并保持表格的正确展示。

效果图

17772e34eae0873ab9b82504c5657df_mosaic.png

阅读全文

WEB前端基础知识梳理-笔记

前端随笔 2023/7/11

基础部分

DOM事件流及事件委托机制

DOM事件流(DOM Event Flow): 是描述浏览器中事件传递的一种模型。当页面上发生事件时,浏览器会按照特定的顺序将事件传递给相关的元素。DOM事件流主要包括三个阶段:捕获阶段(Capture Phase)、目标阶段(Target Phase)、冒泡阶段(Bubble Phase)。

  1. 捕获阶段(Capture Phase):事件从文档的根节点(即window对象)开始,逐级向下传递到事件的目标元素之前的节点。在捕获阶段,事件是从最外层的祖先元素依次捕获到目标元素,直到达到触发事件的元素。
  2. 目标阶段(Target Phase):事件到达触发事件的元素。
  3. 冒泡阶段(Bubble Phase):事件从触发事件的元素开始,逐级向上冒泡到文档的根节点。在冒泡阶段,事件会依次冒泡到祖先元素,直到达到文档的根节点。

事件委托(Event Delegation):是一种利用事件冒泡机制的技术。通过将事件监听器绑定在祖先元素上,然后利用事件冒泡的原理,从而在事件冒泡到祖先元素时触发事件监听器。这样可以减少事件监听器的数量,提高性能和代码的可维护性。

事件委托的主要原理是通过事件冒泡,将事件处理器绑定到祖先元素上,而不是直接绑定到每个子元素上。当事件发生时,事件会冒泡到祖先元素,然后在祖先元素上触发事件处理器。通过判断事件的目标元素,可以实现根据目标元素的不同做出相应的操作。

事件委托机制的好处有:

  • 减少事件处理器的数量,节省内存和提高性能。
  • 可以动态地添加或移除子元素,而不需要重新绑定事件处理器。
  • 可以处理动态生成的元素,即使在绑定事件处理器之前。

Ajax

Ajax(Asynchronous JavaScript and XML): 是一种利用JavaScript、XML和HTTP请求的技术,用于在不重载整个页面的情况下与服务器进行异步通信。它可以实现在后台与服务器进行数据交换,并使用JavaScript更新页面的部分内容,从而提升用户体验和页面性能。

Ajax的特点和优势包括:

  1. 异步通信:Ajax使用异步方式发送和接收数据,可以在后台与服务器进行数据交换,而不需要阻塞用户界面。这允许页面在等待服务器响应时仍然保持响应,并且可以同时进行其他操作。
  2. 增量更新:Ajax可以根据需要只更新页面的一部分内容,而不是整个页面。通过使用JavaScript动态地更新DOM,可以提高页面的加载速度和响应速度。
  3. 减少带宽消耗:由于只需要传输数据而不是整个页面,Ajax可以减少网络流量,降低带宽消耗。
  4. 更好的用户体验:Ajax可以使用户与网站进行交互更加流畅,减少页面加载时间和响应延迟,提升用户体验。
  5. 支持多种数据格式:虽然名字中包含XML,但Ajax并不限于只使用XML。它可以使用各种数据格式,如JSON、HTML、纯文本等。

在使用Ajax时,常见的步骤包括:

  1. 建立XMLHttpRequest对象
  2. 设置回调函数
  3. 配置请求信息,(如open,get方法),使用open方法与服务器建立链接
  4. 向服务器发送数据
  5. 在回调函数中针对不同的响应状态进行处理;
function myAjax() {
    // 1. 建立XMLHttpRequest对象
    var xhr = new XMLHttpRequest()
    // 2. 设置回调函数
    xhr.onreadystatechange = callback
    // 3. 配置请求信息,(如open,get方法),使用open方法与服务器建立链接
    xhr.open('post', '/xxx', true)
    // 3. 设置请求头信息
    xhr.setRequestHeader('Content-type', 'application/json;charset=UTF-8')
    //4. 向服务器发送数据
    var params = { ... }
    xhr.send(params)
    
}
// 5.  在回调函数中针对不同的响应状态进行处理;
function callback() { 
// 判断异步对象的状态 
    if(xhr.readyState == 4) { 
    // 判断交互是否成功 
        if(xhr.status == 200) { 
        // 获取服务器响应的数据 
        var res = xhr.responseText 
        // 解析数据 
        res = JSON.parse(res) 
        } 
    } 
}

浏览器相关

输入 URL 回车后经过哪些过程

  1. 解析 URL,判断是否命中缓存(DNS prefetch)
  2. 访问 DNS 服务器,将域名解析获取 IP 地址
  3. 三次握手建立 TCP 连接
  4. 发送 HTTP 请求
  5. 服务器处理请求并返回 HTTP 报文
  6. 浏览器解析渲染页面
  7. 断开连接:TCP 四次挥手

跨域问题

在前端开发中,常见的前端解决跨域问题的方法包括:

  1. JSONP(JSON with Padding):JSONP利用<script>标签可以跨域加载外部资源的特性,通过动态创建<script>标签,将请求发送到其他域,并在返回的数据中包含一个回调函数调用,以实现跨域通信。但是JSONP只支持GET请求,且需要目标服务器支持返回预先定义好的回调函数。
function handleResponse(data) {
  // 处理响应数据
  console.log(data);
}

var script = document.createElement('script');
script.src = 'http://example.com/api/data?callback=handleResponse';
document.body.appendChild(script);
  1. CORS(Cross-Origin Resource Sharing):CORS是一种现代化的跨域解决方案,需要服务器设置响应头来允许跨域访问。前端代码无需特殊处理,只需发送跨域请求即可。在服务器响应中,设置Access-Control-Allow-Origin头为允许访问的域名或使用通配符*来表示允许所有域名访问。

  2. 使用代理服务器(本地工程化项目配置 Proxy 代理来跨域请求后端(Webpack、Vite),Nginx反向代理):前端代码将请求发送到同域的代理服务器,然后由代理服务器转发请求到目标服务器,在代理服务器与目标服务器之间跨域请求。前者更适合在开发环境中使用,而后者更适合在生产环境中使用。至于为什么(有性能,安全性等方面),NGINX具备一些强大的安全功能,如SSL终止、防止DDoS攻击等,安全性肯定是强些,又具有许多高级功能,如反向代理、负载均衡、缓存等。这些功能对于处理大量并发请求、提供高可用性和水平扩展方面非常有用,性能上也肯定好。

  3. WebSocket:WebSocket是一种支持在浏览器和服务器之间进行全双工通信的协议,它不受同源策略的限制,可以直接进行跨域访问。

  4. postMessage:postMessage是H5新引入的可跨源通信的API,可以通过这个api,让主页面和任意iframe页面或者windows.open打开的页面进行双向通信。

需要注意的是,为了保护用户的安全性,浏览器实施了同源策略,限制了不同源(协议、域名、端口号任一不同)之间的交互行为。因此,跨域请求在默认情况下是受到限制的。上述方法都是在特定条件下解决跨域问题的方案,不同的场景可以选择不同的方法。

postMessage的使用

window.postMessage()是一个JavaScript方法,允许在不同的window或iframe之间进行跨域通信。它允许你向其他浏览上下文(页面、iframe、子窗口等)发送消息,并且不需要同源限制。

该方法接受两个参数(实际是3个参数,但一般只用到前2个):

  1. message:要发送的数据,可以是字符串、数字、对象等。

  2. targetOrigin:目标窗口的源,即目标窗口的域名。只有在目标窗口与当前窗口具有相同的源时,目标窗口才能接收到消息。也可以使用通配符 “*” 表示允许任何源接收消息,但这样可能会存在安全风险。

使用示例:

在当前页面中发送消息:

var otherWindow = window.opener;  // 获取目标窗口的引用
var message = 'Hello, World!';    // 要发送的消息

otherWindow.postMessage(message, 'https://example.com');

在目标窗口中接收消息:

window.addEventListener('message', function(event) {
  if (event.origin === 'https://example.com') {  // 检验消息来自合法的源
    var receivedMessage = event.data;             // 接收到的消息
    console.log(receivedMessage);
  }
});

window.opener获取了打开当前窗口的窗口的引用。然后使用postMessage()方法将消息发送给目标窗口,并指定目标窗口的源。目标窗口中使用window.addEventListener()监听message事件,并通过event.origin检验消息的源。如果消息来自合法的源,就可以获取到消息内容event.data。

移动端屏幕适配

在移动端屏幕适配中,我们需要确保网页或应用程序能够在不同尺寸和分辨率的移动设备上呈现良好的用户体验。以下是一些常见的移动端屏幕适配方法:

  1. 响应式布局:使用CSS媒体查询和弹性布局等技术,根据不同的屏幕尺寸和设备方向,调整页面布局和样式。这使得页面能自适应不同的屏幕大小,并保持可读性和操作性。

  2. 流体布局:使用百分比或rem等相对单位来定义元素的尺寸和间距,而不是使用固定像素值。这允许页面元素相对于屏幕大小进行伸缩,以适应不同的设备。(通过插件postcss-pxtorem完成)

  3. 视口设置:通过设置视口的meta标签来调整页面在移动设备上的显示方式。使用<meta name="viewport">标签来设置视口宽度、缩放、禁止缩放等选项,以确保页面在不同设备上呈现正确。

  4. 移动端特定样式:通过使用CSS媒体查询和移动端特定的样式规则,为移动设备提供更好的用户体验。例如,调整字体大小、按钮大小、触摸目标的可点击区域等。

  5. 使用Flexbox或Grid布局:Flexbox和Grid布局是强大的CSS布局工具,可以帮助我们以更灵活的方式创建响应式布局。它们提供了一种简单而直观的方式来控制页面中元素的位置和尺寸。

  6. 测试和调试:在开发过程中,务必经常在不同的移动设备上测试和调试应用程序。使用浏览器开发者工具、模拟器或真实设备进行调试,以确保页面在各种设备上呈现正确且良好的用户体验。

注意在使用这些方案时需要注意一下兼容性的问题

数据存储

cookie、localStorage、sessionStorage随笔

浏览器缓存

浏览器缓存是指浏览器在访问网页时将一些静态资源(如图片、CSS文件、JavaScript文件等)保存在本地的临时存储空间中,以便在下次访问同一网页时能够更快地加载这些资源,提高网页的加载速度和用户体验。

浏览器缓存分为两种类型:强缓存和协商缓存。

强缓存是通过设置响应头中的Cache-Control和Expires字段来实现的。当浏览器发送请求时,会先检查本地缓存中是否存在该资源的副本,并根据Cache-Control和Expires字段的值判断是否可使用缓存。如果可使用缓存,则直接从本地缓存中加载资源,不会发送请求到服务器。

  • Expires(http 1.0 时期产物):设置的是具体的过期时间。
  • cache-control(http 1.1 时期产物):设置的是经过多少时间(单位秒)之后过期,与 Expires 同时存在的话优先级更高。

协商缓存是通过设置响应头中的Last-Modified和ETag字段来实现的。当浏览器发送请求时,会先发送一个带有If-Modified-Since和If-None-Match字段的请求到服务器,服务器根据这些字段的值判断资源是否有更新。如果资源没有更新,则返回304 Not Modified状态码,浏览器直接从本地缓存中加载资源。如果资源有更新,则返回新的资源,并更新本地缓存。

  • Last-modified:顾名思义,最后一次更改时间。

  • ETag:优先级更高,资源的唯一标识。优点是精度更高,因为 Last-modified 时间单位是秒,如果文件在 1 秒内被修改多次就很难侦测到。缺点是性能有一定消耗,因为获得资源的hash值需要额外计算。

浏览器缓存可以有效减少网络请求,提高网页的加载速度。但有时候也会导致问题,例如当网站更新了静态资源但用户的浏览器缓存仍然存在旧版本的资源时,用户可能无法看到最新的内容。为了解决这个问题,可以通过修改资源的文件名或者使用版本号来强制浏览器重新加载资源。

前端路由

前端路由的基础理解

垃圾回收机制

javaScript中的垃圾回收机制是自动管理内存的一种方式。JavaScript引擎使用的垃圾回收算法主要是基于标记-清除(Mark and Sweep)算法。

标记-清除算法分为两个阶段:

  • 标记阶段:垃圾回收器会从根对象开始,遍历所有的对象,并标记出那些仍然被引用的对象。在JavaScript中,全局变量、当前调用栈上的局部变量以及正在运行的函数中的变量都被视为根对象。
  • 清除阶段:垃圾回收器会对内存进行清理,释放那些没有被标记的对象所占用的内存空间。
    不管什么程序语言,内存生命周期基本是一致的:

JS 是在定义变量时就完成了内存分配,而“垃圾回收器”的工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。

除了标记-清除算法,JavaScript引擎还使用了其他优化算法来提高垃圾回收的效率,例如分代回收(Generational Collection)和增量回收(Incremental Collection)。

分代回收将对象分为新生代和老生代两个年代。新生代中的对象生命周期较短,垃圾回收频率较高;而老生代中的对象生命周期较长,垃圾回收频率较低。通过不同的回收算法和策略来处理不同年代的对象,可以提高垃圾回收的效率。

增量回收是一种在执行垃圾回收时,与程序交替执行的方式。这样可以将垃圾回收的任务分解成多个小部分,在每个小部分执行完后让程序继续执行,以减少垃圾回收对程序执行的影响,并提高响应性能。

总的来说,JavaScript的垃圾回收机制是一种自动管理内存的方式,通过使用标记-清除算法以及其他优化算法来检测和释放不再使用的内存空间,从而提高程序的性能和可靠性。

V8引擎垃圾回收机制

V8是一款用于执行JavaScript代码的高性能JavaScript引擎,它内置了独特的垃圾回收机制,与普通的垃圾回收机制存在一些不同之处。以下是V8垃圾回收机制的主要特点:

  1. 分代回收:V8的垃圾回收器采用了分代回收策略。它将内存分为新生代(young generation)和老生代(old generation)两个代。新生代中存放的是新创建的对象,生命周期较短;而老生代中存放的是存活时间较长的对象。通过将内存分代,可以根据对象的生命周期采用不同的回收策略,提高垃圾回收的效率。
  2. 增量式垃圾回收:V8的垃圾回收器采用了增量式垃圾回收,在执行JavaScript代码的过程中,将垃圾回收的任务分解为多个小部分,并与程序交替执行。这样可以将垃圾回收的负担分散到程序执行的过程中,减少了垃圾回收对应用程序的影响,提高了响应性能。
  3. Scavenge算法:新生代内存的垃圾回收使用了Scavenge算法。该算法将新生代内存空间分为From空间和To空间,对象首先分配到From空间,随着垃圾回收的进行,存活的对象会被复制到To空间,同时进行压缩操作,然后清空From空间。这种方式有效地解决了新生代内存中频繁发生的垃圾回收问题。
  4. 标记-清除算法:老生代内存的垃圾回收使用了标记-清除算法。垃圾回收器首先通过根对象遍历,标记所有可达的对象,然后清除那些未标记的对象,并回收它们占用的内存空间。V8的垃圾回收器还采用了增量标记和压缩操作,提高了清除过程的效率。

总体而言,V8的垃圾回收机制在分代回收、增量式垃圾回收、Scavenge算法和标记-清除算法等方面与普通的垃圾回收机制有一些不同。这些特点使得V8能够在执行高性能JavaScript代码的同时,有效地管理内存并提高垃圾回收的效率。

网络相关

HTTP 1.0/1.1/2.0/3.0 的特性

HTTP 1.0

  1. 确定了协议是无状态的,即同一客户端每次请求都没有任何关系
  2. 消息结构包含请求头和请求体

HTTP 1.1

  1. 持久连接:引入了持久连接(Persistent Connection),可以在一次TCP连接中发送多个HTTP请求和响应,减少了连接建立和关闭的开销。
  2. 流水线:引入了流水线(Pipelining),允许客户端同时发送多个请求,提高了并发性能。
  3. 虚拟主机:通过Host请求头字段,可以在一台服务器上运行多个网站。
  4. 缓存控制:引入了缓存控制机制,使得服务器和客户端可以更好地管理缓存和减少数据传输。
  5. 分块传输编码:支持分块传输编码(Chunked Transfer Encoding),可以在不知道内容长度的情况下逐块发送数据。

HTTP 2.0

  1. 二进制传输:HTTP 2.0使用二进制格式传输数据,替代了HTTP 1.x的文本格式,提高了传输效率和容错性。
  2. 多路复用:引入了多路复用(Multiplexing),可以通过一个连接并发发送多个请求和响应,解决了HTTP 1.x的队头阻塞问题,提高了性能。
  3. 请求优先级:可以设置请求的优先级,确保重要请求优先处理。
  4. 头部压缩:使用HPACK算法对HTTP头部进行压缩,减少了数据传输的大小。
  5. 服务器推送:服务器可以主动推送数据给客户端,减少客户端请求的延迟。

HTTP 3.0

  1. 使用QUIC协议:HTTP 3.0使用QUIC(Quick UDP Internet Connections)作为传输协议,基于UDP提供更快的连接建立和数据传输。
  2. 强化安全性:HTTP 3.0默认使用加密的传输,提供更好的安全性和隐私保护。
  3. 抗网络阻塞:引入了拥塞控制机制,可以更好地应对网络阻塞和高延迟的情况。

GET 和 POST 区别

  • Get 传输大小相对受限(不同浏览器之间不同),Post 大小不受限制
  • Get 通过 URL 编码传输数据,Post 通过 body 传输,支持多种编码格式(两者都是明文传输,都不是安全的,但 Get 参数直接暴露在 URL 上,不能用来传递敏感信息)
  • 浏览器会缓存 Get 请求,Post 则不会缓存。(在该特性下Get请求可能会出现 304 不更新,解决方法:链接加个随机参数)

网络安全

XSS(跨站脚本攻击):

利用了浏览器对于从服务器所获取的内容的信任,注入恶意脚本在受害者的浏览器中得以运行,分为反射型、储存型、DOM型。开启 CSP(内容安全策略)可以减少或消除这类攻击,副作用是 eval、Function() 等方法会失效。类似的还有 CRLF 攻击,防御此类攻击的核心就是严格控制用户提交的内容,对输入进行过滤,对输出进行转义。

CSRF(跨站伪造请求):
利用受害者的登录凭证(cookie)达到冒充该用户执行操作的目的,这在被攻击方很难完全防御,所以只能尽量减少 cookie 的使用,目前大部分网站也都是用 Token 来进行身份验证的,可以有效避免该类攻击。

Injection(注入攻击)
这种攻击主要是接口设计不当导致的,例如接口根据用户传递的内容拼接 SQL,那么就可以通过传递 SQL 语句来注入攻击;又例如根据传递的内容来拼接 Shell 命令,那么攻击者如果传了类似 && rm -rf xxx 这样的命令就会被执行,后果不言而喻。

DoS(服务拒绝)、DDoS(分布式服务拒绝):

通过构造大量特定请求,导致服务器资源被消耗过度,挤压正常的请求,进而产生雪崩效应。在 DDoS 攻击中更是利用了僵尸网络,使得追溯源头的可能性几乎为零,这类攻击主要目的在于消耗服务器带宽,非常难防御,常见的有 SYN 洪泛攻击(利用 TCP 三次握手),只能通过一些手段缓解,例如缩短超时,让服务器更快地释放掉长时间响应的连接,从而增加攻击者的成本。

中间人攻击:

通过拦截窃取手段破坏通信,信息在用户和服务之间的传递都会暴露在攻击者的视野中。使用 HTTPS 协议传输一般可以避免,还有就是敏感信息不要使用明文传输,剩下的就只能交给用户的安全意识了,比如用户连接了不安全的公共 wifi,那么他就有可能被攻击。

TCP 三次握手四次挥手的理解

TCP(Transmission Control Protocol) 是一种常用的传输层协议,用于在计算机网络中进行可靠的数据传输。为了建立和终止一个TCP连接,TCP使用了三次握手和四次挥手的过程。

三次握手(Three-way Handshake) 是在客户端和服务器之间建立TCP连接的过程。具体步骤如下:

  1. 第一次握手:客户端向服务器发送一个连接请求报文段(SYN),服务端接收了报文,此时服务端可以确定客户端发送功能正常,自己接收也正常。
  2. 第二次握手: 服务器收到请求后,确认连接请求,并发送一个带有确认标志的报文段(SYN+ACK)作为响应,但此时服务端还不确定自己的发送是否正常
  3. 客户端收到服务器的响应后,再发送一个带有确认标志的报文段(ACK),表示连接已建立,双方都确认各自收发功能正常。

这样,双方就完成了三次握手,建立了可靠的TCP连接,可以开始进行数据传输。

四次挥手(Four-way Handshake) 是在客户端和服务器之间终止TCP连接的过程。具体步骤如下:

  1. 客户端发送一个终止连接请求报文段(FIN)给服务器,表示客户端不再发送数据了。
  2. 服务器收到请求后,发送一个确认报文段(ACK),表示已收到客户端的终止请求。
  3. 服务器发送一个终止连接请求报文段(FIN)给客户端,表示服务器也不再发送数据了。
  4. 客户端收到请求后,发送一个确认报文段(ACK),表示已收到服务器的终止请求。

这样,双方就完成了四次挥手,TCP连接成功终止。

三次握手和四次挥手的目的是为了保证双方都同意建立和终止连接,并确保数据的可靠传输。通过握手和挥手过程,双方可以进行信息交换,协商参数,并确保连接的稳定性和可靠性。

域名发散和域名收敛

域名发散(Domain Name Expansion)和域名收敛(Domain Name Convergence) 是在网络中管理和分配域名的两种不同策略。

域名发散 是指将一个域名分配给多个不同的IP地址或资源。这意味着同一个域名可能会映射到不同的服务器或资源上。这种策略在分布式系统或负载均衡环境中常见。例如,一个大型网站可能会将其域名映射到多个服务器上,以实现更好的性能和可靠性。

域名收敛是指将多个域名指向同一个IP地址或资源。这意味着多个不同的域名将指向同一个服务器或资源。这种策略经常用于集中管理和简化网络架构。例如,一个公司可能会有多个域名,但它们都指向同一台服务器,这样可以方便地管理和维护这些域名和服务器。

选择采用域名发散或域名收敛策略取决于特定的网络需求和设计目标。域名发散可以提供更好的负载均衡和容错能力,但需要更复杂的配置和管理。域名收敛可以简化管理,但可能会对性能和可靠性产生风险。

CSS相关

CSS盒模型

CSS盒模型(CSS Box Model)是用于定义和布局HTML元素的一种模型。它将每个HTML元素看作一个矩形的盒子,包括内容区域、内边距、边框和外边距

  1. 内容区域(Content):这是盒子中放置实际内容的部分,如文本、图像等。它的大小由width和height属性定义。
  2. 内边距(Padding):这是内容区域与边框之间的空白区域。它的大小由padding-top、padding-right、padding-bottom和padding-left属性定义。
  3. 边框(Border):这是一个包围内容区域和内边距的线条或样式。它的大小和样式由border-width、border-style和border-color属性定义。
  4. 外边距(Margin):这是盒子与其他盒子之间的空白区域。它的大小由margin-top、margin-right、margin-bottom和margin-left属性定义。

标准盒模型:box-sizing: content-box,另外也可通过可以在HTML文档的<head>标签中添加文档类型声明(DOCTYPE)设置标准盒模型

  • 在标准盒模型中,元素的总宽度和高度包括了内容区域、内边距、边框和外边距。
  • 元素的宽度(width)和高度(height)属性指定的是内容区域的尺寸。
  • 总宽度 = width + 左内边距 + 右内边距 + 左边框 + 右边框 + 左外边距 + 右外边距。
  • 总高度 = height + 上内边距 + 下内边距 + 上边框 + 下边框 + 上外边距 + 下外边距。

IE盒模型(怪异盒子模型):box-sizing: border-box

元素宽度为内容宽度+边距+边框(content + padding + border = width)

BFC

BFC的全称是块状格式化上下文

一个块格式化上下文(block formatting context) 是Web页面的可视化CSS渲染出的一部分。它是块级盒布局出现的区域,也是浮动层元素进行交互的区域。

一个块状格式化上下文可由以下方法创建:

  1. 根元素(html元素)或其它包含它的元素
  2. 浮动元素 (元素的 float 不是 none)
  3. position 的值不为relative和static
  4. 内联块 (元素具有 display: inline-block)
  5. 表格单元格 (元素具有 display: table-cell,HTML表格单元格默认属性)
  6. 表格标题 (元素具有 display: table-caption, HTML表格标题默认属性)
  7. 具有overflow 且值不是 visible 的块元素
  8. display: flow-root
  9. column-span: all 应当总是会创建一个新的格式化上下文,即便具有 column-span: all 的元素并不被包裹在一个多列容器中。
  10. 弹性盒flex boxes(元素具有display: flex或inline-flex)

应用:

  1. 阻止 margin 重叠
  2. 阻止元素被浮动元素覆盖(以前常用于自适应两栏布局)
  3. 清除内部浮动(父级元素高度塌陷问题)

回流与重绘

回流:是指浏览器根据DOM树和CSS样式计算元素的几何属性和布局信息的过程。当页面中的元素发生了改变(如添加、删除、修改元素的位置、大小等),浏览器会重新计算元素的布局信息,以确定每个元素在文档中的准确位置。回流是一种比较昂贵的操作,因为它会影响页面的整体布局,可能需要重新计算许多元素的位置,对性能有一定的影响。

具体哪些情况会导致回流:

  1. 页面首次渲染
  2. 浏览器窗口变化
  3. 元素尺寸或位置变化(宽高、边距、边框等)
  4. 元素内容发生变化(文字数量、图片大小、字体大小变化)
  5. 添加删除可见的 DOM 节点
  6. 激活 css 伪类(hover、active等)
  7. 查询某些属性或调用某些方法(浏览器会必须回流来保证数据的准确性)

重绘:是指浏览器对页面进行重新绘制的过程,即根据元素的新的样式计算并绘制页面的外观。当元素的样式发生了改变(如颜色、背景、边框等),浏览器会重新计算元素的视觉效果,并将其重新绘制在屏幕上。相比于回流,重绘的开销较小,因为它只需要更新元素的样式,不需要重新计算布局。

回流必将引起重绘,重绘不一定引起回流。回流的性能开销更大

如何减少回流重绘(性能优化):

HTML层面:

  1. 避免使用 table 布局
  2. 在 DOM 树最末端改变 class

CSS层面:

  1. 尽量减少使用 CSS 表达式(如:calc)
  2. 避免多层内联样式
  3. 将复杂动效应用在脱离文档流的元素上(position: absolute / fixed)

JS层面:

  1. 避免用 JS 操作样式(多个样式改变尽量合并为一次操作)
  2. 如无法避免多次应用样式或操作 DOM,则可以先设置元素隐藏(先 display:none 再操作)
  3. 重复使用元素属性时赋值给变量(避免重复查询元素导致回流)
  4. 某些操作尽量采用防抖节流(如 resize、scroll)

实现水平垂直居中

flex布局(最常用)

.parent {
    display: flex;
    justify-content: center;
    align-items: center;
}

grid布局(兼容性不太好,适合内部系统,自己用)

.parent {
    display: grid;
    place-items: center;
}

translate偏移居中(绝对定位中最好用的方式,不限宽高)

.prant{
    position:relative;
}
.child{
    position:absolute;
    top:50%;
    left:50%;
    transform:translate(-50%,-50%)
}

css多行文本隐藏

单行文本溢出隐藏

overflow: hidden;

单行文本溢出隐藏显示省略号

white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

多换文本溢出隐藏显示省略号

text-overflow: -o-ellipsis-lastline;
overflow: hidden
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;//想显示几行改成几
-webkit-box-orient: vertical;

css换行

1.自动换行:

word-wrap:break-word;
word-break:normal; 

2.强制换行:

word-break:break-all;       按字符截断换行 /* 支持IE和chrome,FF不支持*/
word-wrap:break-word;    按英文单词整体截断换行  /* 以上三个浏览器均支持 */
  • 注意:单词换行需要父盒子为块级元素

3.强制不换行:

white-space:nowrap;

隐藏元素的办法

聊聊 CSS 隐藏元素的 10 种实用方法

css常见问题

解决常见的 CSS 问题 - 学习 Web 开发 | MDN (mozilla.org)

JS基础知识

数据类型

基本数据类型:undefined、null、string、number、boolean、bigint、symbol,基本数据类型存放在栈内存中,大小是可预期的。

引用数据类型有: Object、Array,引用数据类型存放在堆内存中,主要储存复杂数据。

怎样理解 堆、栈、队列 ?

【堆】:想象一个仓库,在申请到一片空间后就可以放任何东西,但是东西放多了找起来就比较麻烦,所以需要一份“清单”,通过查找清单上的索引去找你要的那堆东西,就不用每次都进仓库乱翻。

【栈】:想象一个箱子,先放进去的东西反而被压在了箱底,也就最慢才会被拿出来,所以说先进后出,后进先出。

【队列】:排队没什么好说吧,讲究一个先来后到,所以肯定先进先出。

这是我笔记主要来源的一篇文章作者的理解,感觉非常通俗易懂。

JS 处理数组常用的方法

Array.push() //末尾添加
Array.unshift()//头部添加
Array.pop() //末尾删除
Array.shift() //末尾添加
Array.reserve()//翻转数组
Array.sort() //排序
Array.slice(start, end)//切割,不改变原数组,返回新数组
Array.splice(start, length, newItem)//更改数组(删除,添加数组)
Array.indexOf()//查元素下标,查到返回元素下标,没有返回-1

数组循环

Array.forEach((item,index)=>{})//用来循环遍历数组的,无返回值 代替了for
Array.map((item,index)=>{returen ...}) //映射数组的,有返回值
Array.filter((item,index)=>{return ...}) //过滤数组,返回过滤后的数组
Array.every((item,index)=>{return ...}) //主要是用来判断数组中是不是每一个都满足条件,返回一个布尔值
Array.some((item,index)=>{return ...}) //主要是用来判断数组中有没有满足条件的,返回布尔值
Array.find((item,index)=>{return ...}) //用来获取数组中满足条件的第一个数据,没有返回undefined
Array.reduce((prev,item,index)=>{return ...}) //用来叠加的,prev :一开始就是初始值 当第一次有了结果以后;这个值就是第一次的结果

JS 处理对象常用的方法

1.对象遍历

const obj = { property1: "value1", property2: "value2", property3: "value3" };
for(let key in obj ) {
    console.log(key, obj[key]);
}

2.对象合并

const target = {property1: "value1"}
const source = {property2 "value2,property3 "value3,}
Object.assign(target,source)
console.log(target);
// 输出: { property1: "value1", property2: "value2", property3: "value3" }

3.对象中的属性检查

const obj = { property: "value" }; 
console.log("property" in obj); // 输出: true 
console.log("nonexistent" in obj); // 输出: false

4.对象拷贝

//使用 Object.assign({},Obj)
const obj = { property: "value" }; 
const clone = Object.assign({}, obj); 
console.log(clone); // 输出: { property: "value" } 
// 或者 使用解构赋值和扩展运算符(...)
const clone2 = { ...obj }; 
console.log(clone2); // 输出: { property: "value" }

上面的2种方式,如果对象的属性值为简单类型,通过这两种方式得到的新对象为深拷贝,如果属性值为【对象或者其他引用类型】,那对于这个对象实际上还是浅拷贝。

深拷贝方案

1.使用 JSON 序列化和反序列化

const obj = { property: "value", nestedObj: { nestedProperty: "nestedValue" } }; 
const clone = JSON.parse(JSON.stringify(obj)); 
console.log(clone); // 输出: { property: "value", nestedObj: { nestedProperty: "nestedValue" } } 
// 修改克隆对象的属性不会影响原始对象 
clone.property = "new value"; 
clone.nestedObj.nestedProperty = "new nested value"; 
console.log(obj); // 输出: { property: "value", nestedObj: { nestedProperty: "nestedValue" } }

需要注意的是,使用 JSON 序列化和反序列化的方法无法处理函数、循环引用等特殊类型数据。

2.递归复制对象

这种方法通过递归地遍历原始对象,并针对对象的属性逐个进行深拷贝,以创建一个原对象的独立副本。

function deepClone(obj) {
    if(obj === null || typeof obj!=='object') {
        return obj;
    }
    const clone = Array.isArray(obj)?[]:{};
    for(let key in obj) {
        if(Object.prototype.hasOwnProperty.call(obj,key)) {
            clone[key] = deepClone(obj[key]);
        }
    }
    return clone;
}
const obj = {property: "value", nestedObj: { nestedProperty: "nestedValue" }}
const clone = deepClone(obj)
// 修改克隆对象的属性不会影响原始对象 
clone.property = "new value"; 
clone.nestedObj.nestedProperty = "new nested value"; 
console.log(obj); // 输出: { property: "value", nestedObj: { nestedProperty: "nestedValue" } }

要注意处理循环引用的情况,否则可能导致无限循环

闭包

闭包就是函数里面嵌套函数,并且内部函数可以访问外包函数的变量和作用域。

闭包的主要作用:

  1. 保护数据和方法:闭包可以创建私有变量和函数,避免全局污染,只有内部函数才能访问和修改这些数据和方法。这提供了一种封装和隐藏实现细节的方式,增加了代码的安全性和可维护性。

  2. 保持状态:通过在闭包内部保存变量的状态,可以在函数调用之间保持数据的持久性。这在处理异步操作、循环事件绑定等场景中非常有用。

  3. 记忆和缓存:使用闭包可以实现记忆功能和缓存计算结果。例如,可以使用闭包缓存函数的计算结果,避免重复计算,提高性能。(在vue中计算属性中,可以使用闭包来缓存一些计算结果,当依赖的数据没有发生变化时,可以直接使用缓存结果而不进行重复计算,提高性能。)

  4. 延迟执行:闭包可以用来实现延迟执行功能,包括延迟函数调用、动画效果、事件监听等。闭包可以在特定条件满足时触发函数执行。(节流和防抖)

  5. 实现模块化和封装:通过使用闭包,可以将相关的数据和方法封装在一个独立的作用域中,形成一个模块,提供模块化开发的能力。这样可以提高代码的可读性和可维护性。(axios的封装)

  6. 共享数据和状态管理:闭包可以用于共享数据和状态管理。通过将数据存储在闭包作用域中,并提供访问和修改的方法,可以实现多个组件或函数之间的状态共享和管理。(vueX)

总的来说,闭包是一种强大的编程概念,在JavaScript和其他编程语言中都有广泛的应用。它可以用来处理数据封装、状态管理、模块化开发、函数式编程等多个方面,提供了简洁、灵活和高度可控的编程方式。

当然不恰当的使用闭包将会导致内存泄漏,以下是常见的闭包导致内存泄漏的情况:

1.保留外部变量:
闭包会引用外部函数中的变量,导致这些变量无法被垃圾回收。当闭包存在时,外部函数的作用域仍然存在,即使外部函数已经执行完毕或不再需要。如果闭包中引用的变量占用大量内存,而且没有正确释放闭包,就会导致内存泄漏。

2.不恰当的循环引用:
闭包内部的函数引用了其外部函数中的变量,而外部函数又引用了闭包内部函数,形成循环引用。如果这些循环引用无法被及时释放,就会导致内存泄漏。这种情况在使用事件监听器、定时器、异步操作等场景中比较常见。

3.未正确清理资源:
闭包中可能引用了一些需要手动清理的资源,比如事件监听器定时等。如果不正确地释放这些资源,就会造成内存泄漏。

处理方式:

  • 及时销毁闭包:在不再需要闭包时,确保将其置为null,这样可以打破对外部作用域的引用,使垃圾回收器能够回收内存。
  • 避免循环引用:确保闭包中引用的外部变量能够被及时释放,避免形成循环引用。
  • 显式清理资源:当闭包中引用了需要手动清理的资源时,确保适时释放这些资源,比如解绑事件监听器、取消定时器等。

JS 原型链

JavaScript中的原型链是一种对象之间继承属性和方法的机制。每个JavaScript对象在创建时都有一个内部属性[Prototype],它指向另一个对象,我们称之为原型对象(prototype)。

每个函数都有 prototype 属性,每个函数实例对象都有一个 __proto__ 属性,__proto__ 指向了 prototype,当访问实例对象的属性或方法,会先从自身构造函数中查找,如果找不到就通过 __proto__ 去原型中查找,直到找到该属性或方法或到达原型链的顶端(即Object.prototype)。(全篇背诵,手动狗头)

以下是原型链的一些关键概念和操作:

  1. __proto__属性:每个JavaScript对象都有一个特殊的__proto__属性,它指向该对象的原型对象。通过obj.__proto__可以访问到对象的原型对象。

  2. prototype属性:函数对象(包括构造函数和普通函数)会有一个prototype属性,它指向一个对象,成为该函数的原型对象。对于构造函数来说,它的prototype属性会被用作新创建的实例对象的原型。例如:

    function Person() {}
    Person.prototype.age = 25;
    var john = new Person();
    console.log(john.age); // 25
    
  3. instanceof操作符:用于检查一个对象是否是某个构造函数的实例。它会沿着原型链向上查找。例如:

    function Person() {}
    var john = new Person();
    console.log(john instanceof Person); // true
    
  4. 创建对象的方式:通过构造函数、对象字面量或Object.create()方法来创建对象时,会根据原型链来确定新创建对象的原型。

    function Person() {}
    var john = new Person(); // john的原型为Person.prototype
    var jane = Object.create(Person.prototype); // jane的原型为Person.prototype
    var jim = {}; // jim的原型为Object.prototype
    

call、apply、bind

call、apply和bind都是JavaScript中用于控制函数执行上下文(即函数的this值)的方法。

1.call

call方法:call方法允许你显式地指定函数的执行上下文(即函数内部的this指向哪个对象),并且可以传入多个参数。具体用法是通过函数对象调用call方法,传入一个对象作为第一个参数,这个对象将成为函数执行时的this值,之后可以跟随多个参数。

function greet() { 
    console.log("Hello, " + this.name); 
} 
var person = { name: "John" }; 
greet.call(person); // 输出:Hello, John

在上述例子中,通过调用greet.call(person),将greet函数的执行上下文绑定为person对象,函数内部的this指向person,从而得到正确的输出结果。

以下是一个手写call方法的简单版:

Function.prototype.customCall = function(context,...args) {
    context = context || window;
    const key = Symbol();//创建一个唯一的键值,避免命名冲突
    context[key] = this //将当前函数作为对象的方法
    const result = context[key](...args) //执行函数
    delete context[key];
    return result;//返回函数执行结果
}

2.apply

apply()方法与call方法类似都是,用于指定函数执行的上下文,也可以传入多个参数。不同之处在于,apply方法接受一个数组作为参数列表。数组中的元素作为实参传递给函数

function greet(greeting) { 
    console.log(greeting + ", " + this.name); 
} 
var person = { name: "John" }; 
greet.apply(person, ["Hello"]); // 输出:Hello, John

通过调用greet.apply(person, ["Hello"]),将greet函数的执行上下文绑定为person对象,并传入一个参数数组["Hello"],函数内部的this.name将获得正确的值。

以下是一个手写apply方法的简单版:

Function.prototype.customApply = function(context,args=[]){
    context = context || window;
    const key =Symbol();
    context[key] = this;
    const result = context[key](...args);
    delete context[key];
    return result;
}

3.bind

bind方法用于创建一个新的函数,并将指定的对象作为这个新函数的执行上下文,同时可以在调用新函数时传入一些参数。区别于前两者的是,bind方法不会立即执行函数,而是返回一个绑定上下文后的新函数。

function greet() { 
    console.log("Hello, " + this.name); 
} 
var person = { name: "John" }; 
var greetPerson = greet.bind(person); 
greetPerson(); // 输出:Hello, John

通过greet.bind(person)创建了一个新函数greetPerson,并将person对象作为执行上下文。当调用greetPerson(),发现页是改变这个新函数的执行上下文

以下是一个手写bind方法的简单版:

Function.prototype = function(context,...args) {
    const self = this;
    return function boundFn(...innerArgs) {
        if(this instanceof boundFn) {
            return new self(...args,...innerArgs);
        } else {
            return self.apply(context,[...args,...innerArgs])
        }
    }
}

JS进阶

ES6 新语法/特性

模板字符串

使用反引号(``)括起来的字符串,可以包含变量和表达式,并支持多行字符串。

let name = "耀耀"; 
let message = `Hello, ${name}! Welcome to the world of JavaScript `;

let、const

let zy = 'zy'; //块级作用域
const R =3.1415 //声明常量,并且不可更改,块级作用域

箭头函数(没有自己的 this,不能使用 new 命令,不能调用 call)

箭头函数提供了更简洁的函数定义语法,同时绑定了上下文的this值

let sum = (a, b) => a + b;

class 类

类是一种创建对象的模板,支持构造函数、方法和继承等特性

class Person { 
    constructor(name) { 
        this.name = name; 
    } 
    sayHello() { 
    console.log(`Hello, ${this.name}!`);
    } 
} 
let person = new Person("耀"); 
person.sayHello(); // 输出: "Hello, 耀!"

export、import 模块化(ES Module)

// math.js 
export function add(a, b) { return a + b; } 
// app.js 
import { add } from "./math.js"; 
console.log(add(2, 3)); // 输出: 5

扩展运算符(很常用,... 用于组装数组/对象)

let obj1 = { x: 1, y: 2 }; 
let obj2 = { ...obj1, z: 3 }; 
console.log(obj2); // 输出: { x: 1, y: 2, z: 3 }

解构赋值

可以将数组或对象的属性解构赋值给变量

let [x, y] = [1, 2]; 
let {firstName, lastName} = {firstName: "Alice", lastName: "Smith"};

Set(元素值是唯一的,常用于数组去重)

const arr = [1,2,2,3,3]
const uniqueArr = [...new Set(arr)] //[1,2,3]

Map(性能更好的对象)

Map在JavaScript中的作用是提供了一种存储和访问键值对的方式,可以用于数据处理、查询、遍历和优化等多种场景。根据具体的需求,你可以选择使用Map作为数据结构来更高效地处理和操作数据。

Promise、async/await(es7之后才支持)

下面的异步编程有说明

JS异步编程

JavaScript 异步队列 - 掘金 (juejin.cn)

ECMAScript 6 异步编程 阅读笔记 - 掘金 (juejin.cn)

JS实现继承的方式

  1. 原型继承
  2. 构造函数继承
  3. 组合继承(call / apply)
  4. 寄生组合继承
  5. class继承(es6)

vue相关

vue的数据双向绑定原理

采用了”发布-订阅”的设计模式,通过Object.defineProperty()劫持各个属性的getter、setter,在数据变动时调用Dep.notify发布消息给订阅者Watcher,使之更新相应的视图

vue2使用Object.defineProperty的缺陷:

1.不能监听数组的变化,所以vue2需要对数组原型链上的方法进行一些修改才能实现监听

2.只能劫持对象的属性,所以需要深度遍历对象

Vue3中将Object.defineProperty替换为Proxy解决了对象深度监听的问题,因为 Proxy 代理了整个对象。

diff算法

Vue中的diff算法也叫做虚拟DOM的周界算法,主要用来比较两个虚拟DOM节点的差异。当一个组件的数据发生变化时,Vue通过重新渲染一个虚拟DOM树,然后将新旧虚拟DOM树进行对比。通过比较新旧虚拟DOM树的差异,Vue就能够明确哪些地方需要被更新,哪些地方无需重新渲染。

Vue的diff算法是通过深度优先、先序遍历的方式进行的,它将两个虚拟DOM树进行逐层比较,当找到某一层不一样的节点时,停止下降,然后比较这些节点的子节点。当所有的子节点都完成了比较之后,算法会由下至上进行回溯,此过程被称为执行patch操作。在执行patch操作时,Vue对于不同类型的节点的更新方式也不同,对于元素节点,可以更新它的属性和子节点;对于文本节点,只能更新它的文本内容;对于每个子节点,如果key值相同,可以进行复用或者重新排序,或者将其他的节点移动到这个位置。

通过这种逐层对比,Vue的diff算法能够快速高效地计算出哪些界面需要更新,从而避免了不必要的渲染和重绘,提高了渲染性能和用户体验。

Virtual Dom(虚拟DOM)

虚拟Dom是一种用来表示真实Dom结果的javaScript对象树。它是构建在浏览器原生Dom上的一个抽象层,虚拟dom可以在内存中进行操作,而不需要直接更新真实的Dom,最终通过diff 算法比较新旧差异,才会将最终的变化应用到真实的DOM上。

其实虚拟 DOM 并不能直接提升 DOM 操作性能,它出现的理由是 JS 执行速度远比真实 DOM 操作要快,因此结合diff算法,可以批量更新dom来提高性能,减少直接操作真实DOM的开销。

vue权限管理该怎么做?

方案有很多,我就只说我常用的一种。

菜单和路由都由后端返回,前端统一定义路由组件,需要将数据处理一下,将component换为真正的组件,将处理完的路由表通过pinia存好,然后通过router.addRoute(route)动态挂载,完成菜单权限管理。


/**
 * 根据 menu 服务器返回的路由
 * @param menu
 * @returns
 */

const getRoutesByMenu = (menu: Array<IMenuModel>): Array<RouteRecordRaw> => {
    const routes: Array<RouteRecordRaw> = []
    for (const item of menu) {
        routes.push({
            path: item.path,
            component: shallowRef(Layout),
            name: item.name,
            meta: item.meta,
            children: getChildren(item.children, item)
        })
    }
    // console.log('router', routes)

    return routes
}

/**
 * 获取 menu children
 * @param children
 * @param item
 * @returns
 */
const getChildren = (children: Array<IMenuModel> | undefined, item: IMenuModel | RouteRecordRaw): Array<RouteRecordRaw> => {
    const childArray: Array<RouteRecordRaw> = []
    if (!children || children.length === 0) {
        childArray.push({
            path: item.path,
            name: item.name,
            component的处理: modules['/src/views' + item.path + '/index.vue'],//component的处理
            meta: item.meta
        })
    } else {
        for (let i = 0; i < (children as any).length; i++) {
            childArray.push(...getChildren((children as any)[i].children, (children as any)[i]))
        }
    }
    return childArray
}

使用,其中404页面,一般放在最后add到路由表中,避免出现啥未知错误。

//menu 将后端返回的菜单数据处理过了的
await permissionStore.setRoutes(menu)
    permissionStore.dynamicRoutes.forEach((route: RouteRecordRaw) => {
            router.addRoute(route)
    })
// 添加 404
router.addRoute({
    path: '/:pathMatch(.*)',
    redirect: '/404'
 })

按钮权限

将之前登录时,用户信息中返回的按钮权限的标识存在pinia中,利用自定义指令,判断该按钮是否需要展示。

/*
  需求:不同角色后台管理的按钮权限不同

  思路:
  使用:给 Dom 加上 v-permissionBtn
  <div v-permissionBtn="服务端返回的对应的权限值"></div>
*/
import type { Directive, DirectiveBinding } from 'vue'
import Storage from '@/utils/storage'
interface ElType extends HTMLElement {
    parentNode: any
}
const permissionBtn: Directive = {
    beforeMount(el: ElType, binding: DirectiveBinding) {
        const permissionList: Array<string> = Storage.getItem('session', 'btn_permission')
        if (!permissionList.includes(binding.value)) {
            el.style.display = 'none'
        }
    }
}

export default permissionBtn

前端性能优化

当考虑前端性能优化时,需要从网络层面、JavaScript 层面和 CSS 层面进行分析和优化。

网络层面优化

  1. 减小文件大小:通过压缩文件、使用图像压缩算法、删除不必要的注释和空格等方式来减小文件大小。使用适当的文件格式(如 WebP 图片格式)来提高图像的压缩效率。
  2. HTTP/2 和 HTTP/3:利用 HTTP/2 或 HTTP/3 协议提供的多路复用、头部压缩、服务器推送等性能优化特性,减少网络请求的开销。
  3. 加载资源顺序:使用预加载和按需加载的技术,按照优先级加载关键资源,避免阻塞主要内容的加载。
  4. 缓存:配置适当的缓存策略,通过设置适当的缓存头(如 Cache-Control、Expires),使浏览器可以缓存静态资源以减少重复请求。

JavaScript 层面优化

  1. 代码压缩与混淆:使用压缩工具(如 UglifyJS、Terser)来删除不必要的字符、注释和空格,并进行变量名缩减,以减小文件大小。
  2. 延迟加载和异步加载:使用延迟加载和异步加载技术来减少初始加载的 JavaScript 文件大小,只在需要时加载相关脚本。
  3. 代码优化:避免不必要的计算、循环和操作,在关键路径上进行性能分析和优化。优化算法和数据结构的选择,避免性能瓶颈。
  4. 避免频繁的 DOM 操作:减少直接操作 DOM 的次数,使用虚拟 DOM 或动态模板来优化页面渲染性能。

CSS 层面优化

  1. 压缩和合并 CSS:使用压缩工具(如 cssnano)来删除不必要的字符、注释和空格,并将多个 CSS 文件合并为一个以减少网络请求次数。
  2. 避免使用低效的选择器:避免使用通配符、属性选择器等效率较低的选择器,尽量使用 ID 选择器和类选择器。
  3. 避免过多的 CSS 动画和过渡:过多的 CSS 动画和过渡可能会导致性能下降。限制动画的数量和复杂性,并使用硬件加速技术(如使用 transform 和 opacity)来提高性能。
  4. 使用媒体查询和响应式布局:使用媒体查询来为不同屏幕尺寸和设备提供不同的样式,以实现响应式布局,减少不必要的布局更改。

博客

耀耀切克闹

笔记来源

「1.5w字总结」Web前端开发必知必会详尽知识手册

聊聊 CSS 隐藏元素的 10 种实用方法

BFC-块状格式化上下文的特性及具体应用

阅读全文

React学习笔记(四)-搭建项目

recat 2023/6/19

前面我已经把基础的东西学了,但还是缺点啥,自己得试着搭建一个react项目试试,实践出真知。

创建项目

我使用的是vite4.3+react18+zustand+antd创建的一个后台管理模板,主要也是为了巩固所学内容。

  • vite创建react项目命令:yarn create vite,然后给项目起个名,选择react,选择TypeScript

image.png

  • 安装依赖:yarn add
  • 运行:yarn dev,项目就启动起来了

    Husky+eslint+ prettier规范代码

    具体可看我另一篇文章:Husky+eslint+ prettier规范代码

    相关配置

    路径配置和vite兼容旧浏览器

    vite兼容旧浏览器:安装插件 yarn add @vitejs/plugin-legacy -D
    在vite.config.ts中
    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    import viteEslint from 'vite-plugin-eslint'
    import path from 'path'
    import legacy from '@vitejs/plugin-legacy'
    export default defineConfig({
    plugins: [
      react(),
      viteEslint({
        failOnError: false
      }),
      /** 兼容传统浏览器*/
      legacy({
        targets: ['ie >= 11'],
        additionalLegacyPolyfills: ['regenerator-runtime/runtime']
      })
    ],
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src'),
        '@components': path.resolve(__dirname, 'src/components'),
        '@utils': path.resolve(__dirname, 'src/utils')
      }
    }
    })
    

    添加.env配置文件

    根目录下添加.env.production
    VITE_APP_ENV = 'production'
    VITE_BASE_URL = '/'
    VITE_API_URL = '/'
    
    根目录下添加.env.development
VITE_APP_ENV = 'development'
VITE_BASE_URL = '/'
VITE_API_URL = '/'

安装需要的插件和依赖

recat路由

  • 安装react路由:yarn add react-router-dom@latest
    在src下新建router/lazyLoad.tsx
import { Suspense } from 'react'

const lazyLoad = (Component: React.LazyExoticComponent<() => JSX.Element>) => {
  return (
    <Suspense>
      <Component />
    </Suspense>
  )
}
export default lazyLoad

在router/index.tsx

import { lazy } from 'react'
import { createBrowserRouter, Navigate } from 'react-router-dom'
import type { RouteObject } from 'react-router-dom'

import lazyLoad from './lazyLoad'
import Layout from '@/layouts/index'
import IsloginStatus from '@/components/auth/loginStatus'
import Login from '@/views/login'

const Home = lazy(() => import('@/views/home'))
const About = lazy(() => import('@/views/aboutMy/about'))
const Study = lazy(() => import('@/views/aboutMy/study'))
const routes: RouteObject[] = [
  {
    path: '/login',
    element: <Login />
  },
  {
    path: '/',
    element: (
      <IsloginStatus>
        <Layout />
      </IsloginStatus>
    ),
    children: [
      {
        index: true,
        element: <Navigate to="/home" replace />
      },
      {
        path: '/home',
        element: lazyLoad(Home)
      },
      {
        path: '/about',
        children: [
          {
            index: true,
            element: <Navigate to="/about/study" replace />
          },
          {
            path: 'study',
            element: lazyLoad(Study)
          },
          {
            path: 'aboutMy',
            element: lazyLoad(About)
          }
        ]
      }
    ]
  }
]

export default createBrowserRouter(routes, {
  basename: '/'
})

zustand

  • 安装zustand:zustand一款简洁好用的状态管理库 yarn add zustand
  1. 在 src 目录下新建 stores 文件夹,添加 counter.ts 文件:
import { create } from 'zustand'

interface CounterState {
  counter: number
  delcounter: number
  increase: (by: number) => void
  decrease: (by: number) => void
}

const useCounterStore = create<CounterState>()((set) => ({
  counter: 0,
  delcounter: 10,
  increase: (by) => set((state) => ({ counter: state.counter + by })),
  decrease: (by) => set((state) => ({ delcounter: state.delcounter - by }))
}))

export default useCounterStore

src/home/index.tsx,在home组件中使用

import useCounterStore from '@/store/counter'
import styles from './index.module.scss'
const Home = () => {
  const counter = useCounterStore((state) => state.counter)
  const delcounter = useCounterStore((state) => state.delcounter)
  const increase = useCounterStore((state) => state.increase)
  const decrease = useCounterStore((state) => state.decrease)
  return (
    <div className={styles.home_content}>
      <div>zustand状态管理小练习</div>
      <button onClick={() => increase(1)}>增加 counter:{counter}</button>
      <button onClick={() => decrease(1)}>减少 counter:{delcounter}</button>
    </div>
  )
}

export default Home

效果如下:

image.png

Antd

  • 安装Antd:yarn add antd

具体使用就不详述了,还是看官网去吧:Ant Design - 一套企业级 UI 设计语言和 React 组件库

axios

  • 安装Axios: yarn add axios

在 src 中添加 utils/https.ts 封装一下 axios

import { lazy } from 'react'
import { createBrowserRouter, Navigate } from 'react-router-dom'
import type { RouteObject } from 'react-router-dom'

import lazyLoad from './lazyLoad'
import Layout from '@/layouts/index'
import IsloginStatus from '@/components/auth/loginStatus'
import Login from '@/views/login'

const Home = lazy(() => import('@/views/home'))
const About = lazy(() => import('@/views/aboutMy/about'))
const Study = lazy(() => import('@/views/aboutMy/study'))
const routes: RouteObject[] = [
  {
    path: '/login',
    element: <Login />
  },
  {
    path: '/',
    element: (
      <IsloginStatus>
        <Layout />
      </IsloginStatus>
    ),
    children: [
      {
        index: true,
        element: <Navigate to="/home" replace />
      },
      {
        path: '/home',
        element: lazyLoad(Home)
      },
      {
        path: '/about',
        children: [
          {
            index: true,
            element: <Navigate to="/about/study" replace />
          },
          {
            path: 'study',
            element: lazyLoad(Study)
          },
          {
            path: 'aboutMy',
            element: lazyLoad(About)
          }
        ]
      }
    ]
  }
]

export default createBrowserRouter(routes, {
  basename: '/'
})

SWR

SWR 是一个 React Hooks 库,它用于管理组件的数据获取和缓存。使用 SWR 可以方便地进行数据获取和处理,同时也有良好的缓存机制,可以减少对服务器的请求。

如果你正在使用 React,那么可以考虑采用 SWR。SWR 很容易安装和使用,并且可以与其他 React 库和组件配合使用。SWR 还支持多种数据源,包括 RESTful APIs、GraphQL、WebSocket 等。

要使用 SWR,你需要先安装它。可以使用 npm 或者 yarn 进行安装:

yarn add swr

安装完成后,就可以在 React 组件中使用 SWR。例如:
在api/hotNewApi

import useSWR from 'swr'
import { httpClient } from '@/utils/https'
export const useFetchHotNews = (type: string) => {
  const { data, isValidating, error } = useSWR(
    `/new?type=${type}`,
    (url) => httpClient.get(url),
    {
      revalidateIfStale: false,
      revalidateOnFocus: false,
      revalidateOnReconnect: false
    }
  )
  return {
    hotNews: data?.data.list,
    isValidating,
    error
  }
}

使用

import { useFetchHotNews } from '@/api/hotNewsApi'
const { hotNews, isValidating, error } = useFetchHotNews('入参')

swr中需要了解的属性isValidating

isValidating 是 SWR 中一个非常有用的属性,它表示当前请求是否仍在进行中,即请求是否仍在等待服务器响应。

在使用 SWR 进行数据请求时,SWR 会首先在本地缓存中查找数据,如果缓存不存在或已经过期,则会发送一个新的请求,并标记 isValidating 为 true,表示请求正在进行中。当响应到达后,SWR 会将 data 更新为最新的响应数据,并将 isValidating 标记为 false,表示请求已经完成。

因此,我们可以利用 isValidating 属性来显示一个 loading 状态,告诉用户请求正在进行中,例如:

import useSWR from 'swr';

function Profile() {
  const { data, error, isValidating } = useSWR('/api/user', fetch);

  if (error) return <div>Failed to load user data</div>;
  if (!data && isValidating) return <div>Loading...</div>;
  if (!data) return <div>No data available.</div>;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.email}</p>
    </div>
  );
}

在这个例子中,我们根据数据、错误和 isValidating 来判断组件应该显示的内容。如果数据不存在且请求正在进行中,我们会显示 “Loading…”,否则就是显示错误信息或最终的数据。

总结

整完之后的项目源码:
Recat-vite (github.com) 该项目的账号密码随便填,没做校验。

通过一系列的学习,现在能用react完成一些普通的功能,也跟着大佬的文章熟悉了react搭建项目的过程,个人感觉和vue搭建时差不多,果然熟悉了一个框架后,再去了解另一个框架,上手也会快些,类比着学习,也能明确感受到他们的区别,也算成功入门了react,提升了自己。就是目前公司都是vue的项目,可能没真实线上的项目练手了,后面再工作之余也只能去github上找一些好的开源项目练练手了。

笔记来源

react@18 + vite@4 + react-router@6 + zustand + antd@5 + axios + swr 保姆级搭建后台管理系统 - 掘金 (juejin.cn)

阅读全文

React学习笔记(三)

recat 2023/6/13

学习步骤

  • react-事件处理
  • 受控组件和非受控组件
  • React Router

react-事件处理

注册事件

React注册事件与DOM的事件语法非常像。语法on+事件名={事件处理程序} 比如onClick={this.handleClick}。React事件采用驼峰命名法,比如onMouseEnter, onClick。

import React from 'react';

function MyComponent() {
  function handleClick() {
    console.log('Button clicked!');
  }

  return (
    <button onClick={handleClick}>Click me</button>
  );
}

export default MyComponent;

事件对象

在React中,事件处理函数的第一个参数是事件对象(event object),它提供了一些关于事件的信息。例如,在一个点击事件处理函数中,事件对象将包含关于点击的位置、按下的键和其他信息。

import React from 'react';

function handleClick(event) {
  console.log(event.target.textContent);
}

function App() {
  return (
    <div>
      <button onClick={handleClick}>Click Me!</button>
    </div>
  );
}

export default App;

react中this的指向问题

在React中,函数组件和类组件具有不同的this指向方式。

在函数组件中,this指向undefined,因为函数组件是无状态的,它们不会在实例化过程中创建this。因此,在函数组件中,你应该使用Hooks API(如useState)管理组件状态,或使用redux(或其他状态管理库)管理全局状态。而在类组件中,this指向类的实例对象。在React类组件中访问props和state时,你必须使用this关键字。否则,你将在当前作用域中引用未定义的变量。

this丢失的问题(类组件开发时估计会遇到,我现在学时一直用的函数式组件开发,没遇上网上说的this丢失的问题,但也做个记录吧)

但如果你是vue开发者,估计遇到this丢失的问题,例如:

class Demo extends Component{
    constructor(props){
        super(props)
    }
    handleClick(){
        console.log('this', this)
    }
    render(){
        return (
            <div>
                <button onClick={this.handleClick}>点我</button>
            </div>
        )
    }
}
ReactDOM.render(<Demo/>, document.getElementById('test'))

在react中,像上面这样写,在执行的时候我们就会找不到this的引用。 我们首先要了解,我们在类中定义的handleClick和render方式是定义在类的原型上了。只有类的实例对象才可以调用,且函数内部的this指向实例本身。 在ReactDOM.render()执行的时候,会帮我们new一个实例对象,并调用render方法,所以在render方法内部的this指向实例自身。但是<button onClick={this.handleClick}>点我</button>这里绑定的事件处理函数为this.handleClick方法的引用。但是当我们点击的时候,handleClick的执行上下文为Window,由于jsx经babel编译后会开启严格模式。所以this指向变为undefined。

解决方式

1.在构造函数中使用bind改变this的指向

import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState({
      count: this.state.count + 1
    });
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={this.handleClick}>Click Me</button>
      </div>
    );
  }
}
export default App;

上面实例中,我们通过将this.handleClick绑定到类实例上来保证该方法中的this指向正确。 在render方法中,使用this.state和this.handleClick访问计数器状态和点击处理程序

2.使用箭头函数绑定事件

class Demo extends Component{

    handleClick(){
        console.log('this', this)
    }
    render(){
        return (
            <div>
                <button onClick={(e) => this.handleClick(e)}>点我</button>
            </div>
        )
    }
}
ReactDOM.render(<Demo/>, document.getElementById('test'))

render函数的this指向为实例自身,所以,我们可以直接在绑定的时候使用箭头函数,此时handleClick的执行上下文为箭头函数定义时的作用域。

3.使用箭头函数定义实例方法

class Demo extends Component{

    // 该方法定义在实例对象,且执行上下文为实例自身
    handleClick = ()=>{
        console.log('this', this)
    }
    render(){
        return (
            <div>
                <button onClick={this.handleClick}>点我</button>
            </div>
        )
    }
}
ReactDOM.render(<Demo/>, document.getElementById('test'))

受控组件和非受控组件

在React中,表单元素(例如输入框、复选框、下拉框等)可以是“受控”组件或“非受控”组件。

受控组件

受控组件是指受React控制的表单元素,其值通过React state来管理。在受控组件中每次用户输入时,都会更新组件状态并重新渲染组件(或更新虚拟DOM),从而反映表单元素的当前值。要使用受控组件,你需要将表单元素的值绑定到状态(state)并在改变时通过事件监听程序更新该状态。

非受控组件

非受控组件是指值通过DOM节点本身管理的表单元素。在非受控组件中,组件自己掌控着输入框的值。非受控组件的值并不受React控制,所以React不会通过重新渲染来更新值。相反,您必须使用原生JavaScript来获取或设置组件的当前值。因此,在绝大多数情况下,我们都建议使用受控组件。

受控组件与非受控组件对比

我们来看一个使用受控组件,以及一个使用非受控组件的示例:

   import React, { useState } from 'react';

    function App() {
      const [value, setValue] = useState('');

      function handleChange(event) {
        setValue(event.target.value);
      }

      function handleSubmit(event) {
        event.preventDefault();
        console.log('You typed: ', value);
        setValue('');
      }

      return (
        <div>
          <form onSubmit={handleSubmit}>
            <label>
              Name:
              <input type="text" value={value} onChange={handleChange} />
            </label>
            <input type="submit" value="Submit" />
          </form>
        </div>
      );
    }

    export default App;

在这个示例中,我们定义了一个带有一个文本框和一个提交按钮的表单。文本框的值是一个受控组件,我们绑定它的值到状态变量value,每次文本变化时触发handleChange事件处理程序,handleSubmit处理表单的提交,把表单文本内容打印到控制台,并将状态value重置为空字符串。我们还可以通过使用非受控组件来改编相似的表单,但是将输入值传递给事件处理程序需要使用表单节点的引用来实现。如下所示:

  import React, { useRef } from 'react';

    function App() {
      const inputRef = useRef(null);

      function handleSubmit(event) {
        event.preventDefault();
        console.log('You typed: ', inputRef.current.value);
        inputRef.current.value = '';
        inputRef.current.focus();
      }

      return (
        <div>
          <form onSubmit={handleSubmit}>
            <label>
              Name:
              <input type="text" ref={inputRef} />
            </label>
            <input type="submit" value="Submit" />
          </form>
        </div>
      );
    }

    export default App;

在这个示例中,我们定义了一个带有一个文本框和一个提交按钮的表单。文本框是一个非受控组件,通过使用ref来获取其当前值。在handleSubmit事件处理程序中,我们将使用该方法记录输入值,并清空文本框,并将焦点设置回文本框中。

React Router

什么是React router

React Router是React的一个第三方库,它提供了一种利用组件 <Route /> <Link />等来定义路由的方式。通过React Router,开发者可以方便地实现路由控制,将不同的组件映射到不同的URL路径上,以达到构建单页应用的目的。React Router还提供了一些高级功能,如动态路由匹配、嵌套路由和程序性导航等。

安装使用React-Router

npm install react-router-dom@6

如果在 web 上的话,你需要的是 react-router-dom 而不是 react-router 这个包。它们的区别是,后者包含了 react-native 中需要的一些组件,如果你只需要做网页应用的话,用前者就可以了。

React Router 6 是 React Router 的下一个版本,与 React 18 兼容。它具有许多新功能和改进,包括全新的 Hooks API、对导航声明语法和后台路由的支持等。在 React 18 中使用 React Router 6,需要进行以下步骤:

  1. 创建路由:在应用程序中定义路由。应用程序中可能有多种方式定义路由,例如通过 JavaScript 文件,或通过组件内部的以路径名为 key 的对象。

    Javascript 文件:

    // routes.js
    import {Routes, Route} from 'react-router-dom';
    import {Home, About} from './pages';
    
    function AppRoutes() {
      return (
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="about" element={<About />} />
        </Routes>
      );
    }
    
    export default AppRoutes;
    

    组件内部:

    // App.js
    import {Routes, Route} from 'react-router-dom';
    
    function App() {
      return (
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="about" element={<About />} />
        </Routes>
      );
    }
    
    export default App;
    
  2. 定义页面组件:在应用程序中定义页面组件,与路由结合使用。

    // pages/Home.js
    function Home() {
      return <h1>Home Page</h1>;
    }
    
    export default Home;
    
    // pages/About.js
    function About() {
      return <h1>About Page</h1>;
    }
    
    export default About;
    
  3. 定义导航:在应用程序中定义导航栏,以便在视图之间导航。

    // components/Nav.js
    import {Link} from 'react-router-dom';
    
    function Nav() {
      return (
        <ul>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/about">About</Link></li>
        </ul>
      );
    }
    
    export default Nav;
    

上述代码中,我们首先在 routes.js 文件中配置了两个路由,并通过 element 属性指定了路由所对应的组件。然后我们在组件中使用 Routes 和 Route 组件来设置路由,路径为 / 的路由对应 Home 组件,路径为 /about 的路由对应 About 组件。

最后我们在 Nav 组件中使用 Link 组件来完成页面之间的跳转。

需要注意的是,在 React Router 6 中,<Switch> 组件不再存在,取代它的是 <Routes> 组件,我们可以在 <Routes> 中定义多个路由,并在 <Route> 中设置 path 属性,React Router 6 会自动匹配路径,并渲染相应的组件。

React Router

官网的介绍:

组件名 作用 说明
<Routers> 一组路由 代替原有<Switch>,所有子路由都用基础的Router children来表示
<Router> 基础路由 Router是可以嵌套的,解决原有V5中严格模式,后面与V5区别会详细介绍
<Link> 导航组件 在实际页面中跳转使用
<Outlet/> 自适应渲染组件 根据实际路由url自动选择组件
hooks名 作用 说明
useParams 返回当前参数 根据路径读取参数
useNavigate 返回当前路由 代替原有V5中的 useHistory
useOutlet 返回根据路由生成的element
useLocation 返回当前的location 对象
useRoutes 同Routers组件一样,只不过是在js中使用
useSearchParams 用来匹配URL中?后面的搜索参数

笔记来源

react笔记(四)—— 事件处理 - 掘金 (juejin.cn)

React-Router V6 使用详解(干货) - 掘金 (juejin.cn)

阅读全文

React学习笔记(二)

recat 2023/6/6

前言

书接上文,React继续学习并记录笔记。本文有好多是问chatGPT得到的,chatGPT回答结合着例子,感觉更加通俗易懂些。

学习步骤

  • react hooks
  • 组件间的传值
  • 组件内常用的使用方式(类比vue,v-if,v-for试试例子)

react hooks

什么是react hooks

我去问了问chatGPT,回答如下:
image.png

react hooks解决了什么问题

image.png
大概就是这三点,网上的回答也大同小异:

  • 让函数组件也能做类组件的事,有自己的状态,可以处理一些副作用,能获取 ref ,也能做数据缓存。
  • 解决逻辑复用难的问题
  • 放弃面向对象编程,拥抱函数式编程

什么是自定义hooks

image.png

常用的React hooks

useState

useState 可以使函数组件像类组件一样拥有 state,函数组件通过 useState 可以让组件重新渲染,更新视图。

const [ ①state , ②dispatchAction ] = useState(③initData)

① state,目的提供给 UI ,作为渲染视图的数据源。

② dispatchAction 改变 state 的函数,可以理解为推动函数组件渲染的渲染函数。

③ initData 有两种情况,第一种情况是非函数,将作为 state 初始化的值。 第二种情况是函数,函数的返回值作为 useState 初始化的值。

import { useState } from "react"

interface myInterface{
    name:string;
}

const DemoState = (props:myInterface) => {
    console.log(props)
    /* number为此时state读取值 ,setNumber为派发更新的函数 */
    let [number, setNumber] = useState(0) /* 0为初始值 */
    return (<div>
        {/* 这里展示的又是最新的值,因为在整个事件处理结束之后再重新渲染组件,此时state已经更新好的 */}
        <span>{ number }</span>  
        <button onClick={ ()=> {
        setNumber(number+1)
      console.log(number) /* 由于 useState 是异步的,点击时state还没有更新好,所以 console.log 同步输出的是上一次更新后的值,并不是最新的值。  */
    } } ></button>
    </div>)
}

export default DemoState

useRef

useRef 可以用来获取元素,缓存状态,接受一个状态 initState 作为初始值,返回一个 ref 对象, 这个对象上有一个 current 属性就是 ref 对象需要获取的内容。

const ref = React.useRef(initState)
console.log(ref.current)

useRef常用用法:

  • useRef 获取 DOM 元素,在 React Native 中虽然没有 DOM 元素,但是也能够获取组件的节点信息,如下:
const DemoUseRef = ()=>{
  const dom= useRef(null)
  const handerSubmit = ()=>{
    /*  <div >表单组件</div>  dom 节点 */
    console.log(dom.current)
  }
  return <div>
    {/* ref 标记当前dom节点 */}
    <div ref={dom} >表单组件</div>
    <button onClick={()=>handerSubmit()} >提交</button> 
  </div>
}
  • 可以利用 useRef 返回的 ref 对象来保存状态,只要当前组件不被销毁,那么状态就会一直存在。具体如下:
  1. 定义一个ref变量
const myRef = useRef(null);
  1. 使用myRef保存状态
const [myState, setMyState] = useState(0);
useEffect(() => {
  myRef.current = myState;
}, [myState]);

上面的代码中,我们定义了一个名为myRef的ref变量,并在useEffect钩子函数中将myState保存到myRef中。这里要注意的是,因为myRef.current是一个可变的变量,所以当myState变化时,我们需要使用数组的解构赋值和useState钩子来更新myState的值。

  1. 读取保存的状态
console.log(myRef.current);

在组件的任何部分,我们都可以使用myRef.current来读取已经保存的状态。

最后需要说明的是,useRef保存的状态在每次重新渲染时都会持续存在,这意味着它可以在不同的渲染之间长期存储数据,而不像useState只会在当前的渲染周期中有效。所以如果需要保存状态并且不想在render时失去这个状态,可以用useRef钩子。

useEffect

React的useEffect钩子用于在React组件渲染完成后执行一些副作用操作,比如访问API、更新DOM等。它的作用类似于类组件中的生命周期函数。
useEffect具有三种执行方式:

初始化渲染

初始渲染在初始渲染时,useEffect会在组件挂载之后立即执行回调函数。如果指定了依赖项,React会检查每个依赖项是否发生变化,如果有,则重新执行回调函数。
三种依赖项:

  • 1.不传值 现象: useEffect 会在第一次渲染以及每次更新渲染后都执行。
  • 2.空数组作为依赖 现象:useEffect 会在第一次渲染后执行一次。
  • 3.基本类型作为依赖 现象:  useEffect 会在第一次渲染以及每次更新渲染后都执行。

具体可看:React useEffect 两个参数你用对了吗 - 掘金 (juejin.cn)

更新渲染

在组件更新时,useEffect会在所有更新完毕后执行回调函数。如果指定了依赖项,React会检查每个依赖项是否发生变化,如果有,则重新执行回调函数。

卸载组件

在组件卸载时,useEffect会执行清除副作用操作的回调函数。这种情况下不需要指定依赖项。

useEffect钩子接收两个参数,第一个参数是一个函数,这个函数包含需要执行的副作用操作,例如在钩子中访问API或者更新DOM。第二个参数是一个数组,它用来指定useEffect什么情况下需要重新运行。如果不需要重新运行,可以将数组参数留空。

例子

下面是一些useEffect的用例:

  1. 访问API并更新状态
import React, { useState, useEffect } from "react";

function App() {
  const [data, setData] = useState([]);

  useEffect(() => {
    async function fetchData() {
      const result = await axios.get("https://api.example.com/data");
      setData(result.data);
    }
    fetchData();
  }, []);

  return (
    <div>
      {data.map((item) => (
        <p key={item.id}>{item.name}</p>
      ))}
    </div>
  );
}

export default App;

上面的代码中,我们在useEffect钩子中发起了一个axios的get请求,并将获取到的数据设置到data状态中。需要注意的是,由于我们在useEffect钩子函数中调用了async函数,所以我们需要在函数前面加上async关键字。

  1. 监听props的变化并更新状态
import React, { useState, useEffect } from "react";

function App(props) {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    setCounter(props.value);
  }, [props.value]);

  const handleClick = () => setCounter(counter + 1);

  return (
    <div>
      <p>{counter}</p>
      <button onClick={handleClick}>Increase</button>
    </div>
  );
}

export default App;

上面的代码中,我们使用了useEffect钩子和useState钩子,以响应props的变化并更新状态。钩子中的函数会在props.value改变时触发,它将props.value的值设置到counter变量中。

总的来说,useEffect是React中非常重要的一个钩子,它可以让我们更加容易地进行组件的副作用操作,并且可以防止我们在组件渲染完成前执行操作。

useMemo

React的useMemo钩子用于缓存函数的计算结果,以减少不必要的计算和渲染。它的作用范围更广,可以缓存任何复杂的计算结果。

useMemo接收两个参数,第一个参数是一个函数,这个函数包含需要执行的计算操作,例如根据props和状态计算出一个值或者过滤一些数据。第二个参数是一个数组,它用来指定这个函数依赖的数据,只有当依赖数据发生变化时才会重新计算缓存结果。如果依赖数据没有变化,则直接返回上一次计算的缓存结果。

下面是一些useMemo的用例:

  1. 根据props和状态计算出一个值
import React, { useMemo, useState } from "react";

function App(props) {
  const [count, setCount] = useState(0);

  const result = useMemo(() => {
    console.log("Compute Result");
    return count * props.value;
  }, [count, props.value]);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Value: {props.value}</p>
      <p>Result: {result}</p>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
    </div>
  );
}

export default App;

上面的代码中,我们使用了useMemo钩子来缓存计算结果,这个计算结果基于state变量count和props中的值计算而来。由于这个计算结果只依赖于count和props.value,所以只有count或props.value变化时,useMemo才会重新计算这个计算结果。同时,由于这个计算结果被缓存起来了,所以即使我们反复点击Increase Count按钮,计算结果也不会重新计算和渲染。

  1. 从数组中过滤出存在某个字符的项
import React, { useState, useMemo } from "react";

function App() {
  const [list, setList] = useState(["apple", "banana", "mango", "peach"]);
  const [search, setSearch] = useState("");

  const filteredList = useMemo(() => {
    console.log("Filter List");
    return list.filter((item) => item.includes(search));
  }, [list, search]);

  const handleChange = (event) => {
    setSearch(event.target.value);
  };

  return (
    <div>
      <input value={search} onChange={handleChange} />
      <ul>
        {filteredList.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

上面的代码中,我们使用了useMemo钩子来缓存计算结果,这个计算结果被过滤离数组list中包含搜索关键词的项。由于这个计算结果只依赖于list和search变量,所以只有这两个变量发生变化时,useMemo才会重新计算。同样,由于这个过滤结果被缓存起来了,即使我们在搜索框中不断输入文字或者反复添加删除项,useMemo也只会在依赖变化时重新计算。

useLayoutEffect

React的useLayoutEffect钩子与useEffect类似,都是用于执行副作用操作,但是useLayoutEffect会在浏览器绘制之前执行,可以保证副作用操作对页面布局的影响被计算在内。

useLayoutEffect的使用方法和useEffect一样,都是接收一个函数和一个依赖数组。useLayoutEffect钩子的使用场景与useEffect相似,当需要在组件挂载或更新后立即执行一个副作用操作,以确保副作用操作对页面布局有影响时,可以使用useLayoutEffect。

下面是一个useLayoutEffect的用例:

import { useState, useLayoutEffect, useRef } from "react";

function App() {
  const [width, setWidth] = useState(0);
  const ref = useRef();

  useLayoutEffect(() => {
    setWidth(ref.current.offsetWidth);
  }, []);

  return (
    <div>
      <h1 ref={ref}>Hello, World!</h1>
      <p>The width of the h1 element is {width}px.</p>
    </div>
  );
}

export default App;

上面的代码中,我们使用了useLayoutEffect钩子来获取h1元素的宽度,并将它保存到width状态变量中。由于useLayoutEffect钩子在DOM更新之前执行,在这个例子中,我们可以保证获取的宽度值是最新的,同时,由于我们使用useLayoutEffect钩子来获取宽度值,所以这个宽度值的更新不会导致额外的页面渲染。

useContext

React的useContext钩子用于获取React上下文的值。上下文可以理解为一个全局对象,其中保存了在组件树中被共享的数据。使用useContext可以使得在组件树的任何一个组件中都能够访问到上下文的值。

useContext接收一个上下文对象作为参数,并返回这个上下文对象的当前值。上下文对象在创建时由React.createContext函数创建。

下面是一个useContext的用例:

import { createContext, useContext } from "react";

const ThemeContext = createContext("light");

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Parent />
    </ThemeContext.Provider>
  );
}

function Parent() {
  return <Child />;
}

function Child() {
  const theme = useContext(ThemeContext);

  return <div>Current theme is {theme}</div>;
}

export default App;

上面的代码中,我们创建了一个名为ThemeContext的上下文,它的默认值为”light”。在App组件中,我们使用ThemeContext.Provider提供了一个新的值”dark”,并将这个值传递给了Child组件。在Child组件中,我们使用了useContext钩子来获取当前的主题值,并将这个值显示在页面上。

需要注意的是,由于useContext只能在函数组件中使用,所以需要将上下文对象和Provider组件都放在函数组件外面或者使用React.memo包裹组件。另外,需要通过Context.Provider提供一个新值,以便在组件层次中下传下去。

useReducer

React的useReducer钩子用于管理和更新应用程序的状态,可与Redux架构相比。与useState(类似)不同之处在于,useState钩子通常用于管理状态值。useReducer则被视为管理复杂状态和操作的更好解决方案。

useReducer接收两个参数:一个是reducer函数,一个是初始状态值。reducer函数的作用是在更新状态之前处理一个操作(或动作),并且需要返回一个新的状态值。所得到的状态值将被作为useReducer钩子的返回值,同时,还将被传递到应用程序中的其他组件中。

下面是一个useReducer的用例:

  import { useReducer } from "react";

    const initialState = {
        count: 0
    };

    function reducer(state, action) {
        switch (action.type) {
            case "increment":
                return { count: state.count + 1 };
            case "reset":
                return { count: initialState.count };
            default:
                throw new Error("Invalid action type");
        }
    }

    function App() {
        const [state, dispatch] = useReducer(reducer, initialState);

        return (
            <div>
                <p>Count: {state.count}</p>
                <button onClick={() => dispatch({ type: "increment" })}>Click to increment count: {state.count}</button>
                <button onClick={() => dispatch({ type: "reset" })}>Reset count</button>
            </div>
        );
    }

    export default App;

上面的代码中,我们使用了useReducer钩子来创建一个与状态相关的reduce函数。该reduce函数管理一个名为“count”的状态计数器。在组件中,我们显示此计数器状态的当前值,并将两个按钮连接到dispatch方法。第一个按钮将用于increment操作,第二个用于reset计数器。

需要注意的是,在dispatch方法中,我们使用action对象来通知reducer函数要执行什么样的操作。这个对象是一个简单的JavaScript对象,包含一个字符串类型的属性,表示操作类型(在本例中,我们使用“increment”和“reset”作为操作类型字符串)。

总之,useReducer是一种更好的状态管理解决方案,可以轻松地管理复杂的组件状态。

useImperativeHandle

useImperativeHandle 可以配合 forwardRef 自定义暴露给父组件的实例值。使得父组件能够通过ref获取到子组件的实例,并且调用这些自定义的API。通常用于实现一些跨组件的通讯,或者是对某些组件逻辑的封装,可以将它看成是将一些子组件的方法暴露给父组件来调用。

useImperativeHandle接收两个参数:一个Ref(引用)对象和一个返回一个对象的函数。返回的对象表示要暴露给父组件的API接口。父组件可以通过ref来调用这些自定义的方法。

下面是一个useImperativeHandle的用例:

import { forwardRef, useImperativeHandle, useState } from "react";

const Child = forwardRef((props, ref) => {
  const [count, setCount] = useState(0);

  useImperativeHandle(ref, () => ({
    increment() {
      setCount(count + 1);
    },
    reset() {
      setCount(0);
    },
  }));

  return <div>Count: {count}</div>;
});

function App() {
  const childRef = useRef();

  function handleIncrement() {
    childRef.current.increment();
  }

  function handleReset() {
    childRef.current.reset();
  }

  return (
    <div>
      <Child ref={childRef} />
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleReset}>Reset</button>
    </div>
  );
}

export default App;

上面的代码中,我们创建了一个名为Child的组件,它包含了一个名为“count”的状态计数器,并将它通过useImperativeHandle钩子暴露给了父组件。然后,我们使用forwardRef钩子将Child组件的ref引用传递给了App组件。在App组件中,我们使用ref来调用Child组件中的increment和reset方法。通过useImperativeHandle,我们可以在父组件中调用Child组件的自定义方法,实现了跨组件的通讯。

需要注意的是,如果你正在使用useImperativeHandle来暴露方法,则必须将子组件包装在forwardRef中,以便可以在父组件中使用ref引用。

实现一个自定义的React的自定义Hooks

可以采用以下步骤来创建一个自定义Hooks:

  1. 创建一个以”use”开头的函数,这是React在命名约定中使用的方法,以通知其他程序员此函数是一个Hook;
  2. 在函数中编写一些逻辑,可以使用现有的Hooks,如useState、useEffect;
  3. 根据需要返回一些有用的值。

以下是一个简单的示例,它使用useState和useEffect自定义了一个计时器hooks:

import { useState, useEffect } from "react";

function useTimer(initialTime) {
  const [time, setTime] = useState(initialTime);

  useEffect(() => {
    const timer = setInterval(() => {
      setTime(time => time + 1);
    }, 1000);

    return () => {
      clearInterval(timer);
    };
  }, []);

  return time;
}

export default useTimer;

上面的代码中,我们定义了一个名为useTimer的自定义Hooks,并使用useState和useEffect在内部实现了一个计时器的逻辑,以输出时间。这个自定义Hooks可以用于任何需要计时器的组件中,并使得代码的重复度更低。

使用自定义Hooks非常简单,只需要导入并使用即可。例如:

import useTimer from "./useTimer";

function App() {
  const time = useTimer(0);

  return (
    <div>
      <p>Current Time: {time} seconds</p>
    </div>
  );
}

export default App;

在上面这个例子中,我们导入了自定义Hooks useTimer,并在App组件中调用了它。最终,我们成功地使用自定义Hooks来达到了逻辑复用的效果。

总之,自定义Hooks是非常有用的,可以让我们在函数组件之间共享逻辑,并且使得我们的代码更加简洁易懂。当多个组件需要相同的逻辑时,它可以提高代码的重用性和可维护性。

组件间的传值

父子组件传值

父传子

在React中,父组件可以向子组件传递数据(props)以及函数作为回调来处理事件。在函数式组件中,可以通过参数的形式来接收父组件传递的props。

例如,父组件可以这样传递数据和函数:

  function ParentComponent() {
      const data = { name: 'John', age: 30 };
      const handleClick = () => console.log('Button clicked');

      return (
        <ChildComponent data={data} onClick={handleClick} />
      );
    }

而在子组件中,可以使用props来接收这些参数:

 function ChildComponent(props) {
      const { data, onClick } = props;

      return (
        <div>
          <p>Name: {data.name}</p>
          <p>Age: {data.age}</p>
          <button onClick={onClick}>Click me</button>
        </div>
      );
    }

在子组件中使用props.xxx即可获取父组件传递的数据或函数。这样,在子组件中就可以使用这些数据和函数来展示内容或处理事件了。

子传父

在React中,在函数式组件中子组件向父组件传递数据的方式是通过回调函数,父组件将一个函数作为props传递给子组件,然后子组件在需要向父组件传递数据时,调用该函数并传递参数即可。具体实现方法如下:

父组件:

import React, { useState } from 'react';
import Child from './Child';

function Parent() {
  const [message, setMessage] = useState('');

  function handleMessage(message) {
    setMessage(message);
  }

  return (
    <div>
      <h2>Parent Component</h2>
      <Child onMessage={handleMessage} />
      <p>Message from child: {message}</p>
    </div>
  );
}

export default Parent;

子组件:

import React, { useState } from 'react';

function Child(props) {
  const [inputValue, setInputValue] = useState('');

  function handleInput(event) {
    setInputValue(event.target.value);
  }

  function sendMessage() {
    props.onMessage(inputValue);
  }

  return (
    <div>
      <h3>Child Component</h3>
      <input type="text" value={inputValue} onChange={handleInput} />
      <button onClick={sendMessage}>Send message to parent</button>
    </div>
  );
}

export default Child;

在上述代码中,父组件通过onMessage属性向子组件传递一个函数handleMessage,子组件触发sendMessage函数来传递参数inputValue给父组件。子组件通过调用props.onMessage(inputValue)来触发父组件中的回调函数handleMessage,实现了子组件向父组件传递数据的功能。

兄弟组件传值

在 React 中,兄弟组件之间的传值通常需要借助父组件作为中间层,通过向父组件传递数据再由父组件传递给另一个兄弟组件实现。同时,我们可以使用 context API、Redux 等方案进行数据管理。

总体思路:将状态共享,提升到最近的公共父组件中,由父组件管理状态

  • 提升公共状态
  • 提供操作共享状态的方法
import React, { useState } from 'react';

function Parent() {
  const [message, setMessage] = useState('Hello, World!');

  function handleMessageChange(message) {
    setMessage(message);
  }

  return (
    <div>
      <Child1 onMessageChange={handleMessageChange} />
      <Child2 message={message} />
    </div>
  );
}

function Child1(props) {
  function handleChange(e) {
    props.onMessageChange(e.target.value);
  }

  return (
    <div>
      <input type="text" onChange={handleChange} />
    </div>
  );
}

function Child2(props) {
  return (
    <div>
      <p>{props.message}</p>
    </div>
  );
}

我们定义了一个名为 Parent 的组件作为中间层,分别传递给了 Child1 和 Child2 两个组件。Child1 的输入框可以让用户输入任意文本,并将文本内容通过 props.onMessageChange 传递给父组件 Parent 中的 handleMessageChange 方法进行处理。父组件 Parent 的 handleMessageChange 方法通过设置 message 的状态来记录下输入框中的文本。最后,将 message 状态通过 props 传递给 Child2 组件进行渲染。

需要注意的是,在实际开发中,如果组件之间关系比较紧密,或者组件之间需要频繁地传递数据,则建议考虑使用 context API、Redux 等方案进行数据管理,这样可以使代码更加简洁和易于维护。

总之,在 React 中,兄弟组件之间的传值需要通过父组件作为中间层来实现。可以通过 props、context API、Redux 等方式实现数据的传递和管理。

祖孙组件传值

Context 跨组件传递数据 【类似vue的 provide / inject】

1、首先,在父组件中创建一个 Contex对象:

import React from 'react';
const MyContext = React.createContext();

2、然后,在需要共享数据的组件的父组件中,通过 Provider 提供数据:

import React from 'react';
import MyContext from './MyContext';

function ParentComponent() {
  const sharedData = 'Hello, world!';

  return (
    <MyContext.Provider value={sharedData}>
      <ChildComponent />
    </MyContext.Provider>
  );
}

3、最后,在需要访问共享数据的组件中,可以使用 useContext 获取数据:

import React, { useContext } from 'react';
import MyContext from './MyContext';

function ChildComponent() {
  const value = useContext(MyContext);

  return <div>{value}</div>;
}

组件内常用的使用方式(类比vue,v-if,v-for试试例子)

类比v-if,react里的实现dom元素的切换显示隐藏

在React中,你可以使用state来控制组件的显示和隐藏。下面是一个简单的代码示例来切换一个div元素的显示和隐藏:

import React, { useState } from 'react';

function App() {
  const [show, setShow] = useState(false);

  function toggleDiv() {
    setShow(!show);
  }

  return (
    <div>
      <button onClick={toggleDiv}>Toggle Div</button>
      {show && <div>Some content to show</div>}
    </div>
  );
}

export default App;

初始状态设置为不显示元素。当按钮被点击时,toggleDiv函数会被调用并更新show状态,如果show为true,则显示div元素,否则隐藏div元素。在JSX中使用{},可以将变量嵌入到标签中。如果show为false,则{show && <div>Some content to show</div>}的内容会被忽略。

类比v-if-else,react里的实现多个dom元素之间的显示隐藏

React中,并没有 v-else 指令,但可以通过JavaScript中的条件运算符 ternary operator (三元运算符)来实现类似的效果。

例如,假设有一个条件为 showComponent 的状态,当为 true 时渲染 Component1,否则渲染 Component2,可以这样编写:

{ showComponent ? <Component1 /> : <Component2 /> }

上述代码中,{} 表示JSX中的JS表达式,其中使用三目运算符检查 showComponent 的状态,根据结果渲染相应的组件。

在React中也可以使用 if-else 语句来实现条件渲染,例如:

let component;

if (showComponent) {
  component = <Component1 />;
} else {
  component = <Component2 />;
}

return (
  <div>
    {component}
  </div>
);

以上代码中,根据 showComponent 的状态来分配 component 的值,然后在JSX中渲染该组件。

类比v-for,实现列表遍历展示

在React中,你可以使用map()方法来遍历列表,并将列表项渲染到页面上。

    import React from 'react';

    function App() {
      const list = ['item1', 'item2', 'item3'];

      return (
        <div>
          <h1>My List</h1>
          <ul>
            {list.map((item, index) => (
              <li key={index}>{item}</li>
            ))}
          </ul>
        </div>
      );
    }

    export default App;

在这个示例中,list变量是一个包含三个字符串的数组。使用map()方法遍历数组,并为每个项创建一个<li>元素,并在元素中显示该项的值。key属性是必须的,并且必须是每个列表项的唯一标识符。在此示例中,使用index作为key属性,但更好的做法是使用每个项的唯一标识符。

笔记来源

React useEffect 两个参数你用对了吗 - 掘金 (juejin.cn)

Vite+React+TS基础学习,看这里就够了!(上) - 掘金 (juejin.cn)

十分钟搭建一个 Vite+React+TS+ESLint+Prettier+Husky+Commitlint 项目 - 掘金 (juejin.cn)

阅读全文

React学习笔记(一)

recat 2023/6/5
-->

前言

vue3的也历经了几个大项目,可以说的上是熟练运用了,本来我也在想,是继续深度研究vue,看看源码实现啥的,半年前买的霍春阳大佬写的《Vue.js的设计与实现》只看了1/2,也是惭愧,看了这么久都没看完,一直断断续续的看,哪怕是这样,我也感觉收获也颇深。先订个目标,react学完后,把这本书后面的学完看完。(可能对外面市场的焦虑,想多掌控一些技能,于是便先开始了对React的学习,之前虽然有了解,但我不怎么系统的学习),当然本文本来就是学习记录,如有雷同,肯定是我C+V过来记录的。

学习步骤

  • 官网的快速入门 => 井字棋小游戏(因为有vue基础,我先去官网,试了试提供的例子,感觉挺有趣的)
  • react组件
  • react生命周期

react组件

什么是react组件

它接收任意的输入参数(props),并返回用于描述页面展示内容的React元素,也就是说我们在javascript函数中写一些react元素,处理一些逻辑,其实这就是一个组件。 组件可以将UI拆分成一个个独立的可复用的代码片段,并且可以对每个片段进行独立构思和管理,因此组件还能够很好的实现代码的复用。

react组件的分类

React 组件可以分为两种形式:类(class)组件和函数(function)组件

函数(function)组件

  • 函数式(function)组件:函数式组件其实就是写一个JavaScript函数,接收一个props对象作为参数,并返回用于描述页面内容的React元素(JSX语法),这就构成了一个函数式组件。官网例子就是使用的这种方式。
export default function MyApp() {  
return (  
    <div>  
        <h1>Welcome to my app</h1>  
        <MyButton />  
    </div>  
    );  
}

注意:

1.组件名称必须以大写字母开头
2.return出去的React元素需要有一个外层节点包裹着

类(class)组件

  • 类(class)组件:使用ES6中的类(class)来定义React组件,通过声明一个类来实现定义一个React组件
import React from 'raeact' ​
class B extends React.Component {    
  constructor(props){        
    super(props)   
  }    
  render(){        
    return(       
      <div>hi</div>       
    )   
  } 
}
export default B

注意:

 1.定义类组件必须要继承自React的Component类或React.PureComponent
 2.类中必须要定义一个名为render的函数,函数的返回值应该是React元素(JSX语法)
 3.在类的内部默认会有个props属性(继承自Component)可以直接使用
 4.与函数组件一样,组件名称必须以大写字母开头
 5.在类的构造函数(constructor)中通过this.props访问属性值是获取不到的,因为这时props还没有挂载到this.props上,要等constructor执行完成之后才会挂载
 6.如果非要在构造函数(constructor)中使用this.props来获取,则可以把外面传进来的props传递给constructor中的父类构造函数super从而实现this.props的挂载
 7.类组件是动态组件,基于数据驱动视图渲染

react组件生命周期

什么是react组件生命周期

在 React 中,对于每一次由状态改变导致页面视图的改变,都会经历两个阶段:render 阶段、commit 阶段。只有 class 组件才有生命周期,因为 class 组件会创建对应的实例,而函数组件不会。组件实例从被创建到被销毁的过程称为组件的生命周期。红色的为react18删除,删除的我也就没深入了解。

image.png

初始化阶段(render 阶段)

constructor

constructor是JavaScript中的一个构造函数,在创建一个新对象时被调用。它通常用于初始化实例的属性或方法。继承了React.Component后,该方法只会执行一次,调用该方法会返回一个组件实例。
在初始化阶段执行,可直接对 this.state 赋值。其他生命周期函数中只能通过 this.setState 修改 state,不能直接为 this.state 赋值。

使用场景: 一般在 constructor 中做一些组件的初始化工作,例如:初始化组件的 state。

getDerivedStateFromProps

简单翻译过来就是从Props中获得State,所以该函数的功能就是从更新后的props中获取State,它让组件在 props 发生改变时更新它自身的内部 state。

shouldComponentUpdate

在组件准备更新之前调用,但是首次渲染或者使用 forceUpdate 函数时不会被调用。跟它的名字一样,它用来判断一个组件是否应该更新。

默认情况下,当组件的 props 或者 state 变化时,都会导致组件更新。它在 render 方法之前执行,如果它的返回值为 false,则不会更新组件,也不会执行后面的 render 方法。

它接收两个参数,nextProps 和 nextState,即下一次更新的 props 和下一次更新的 state。我们可以将 this.props 和 nextProps 比较,以及将 this.state 与 nextState 比较,并返回 false,让组件跳过更新。

使用场景: 这个生命周期方法通常用来做性能优化

render

render 方法是类组件中唯一必须实现的方法,它的返回值将作为页面渲染的视图。render 函数应该为纯函数,也就是对于相同的 state 和 props,它总是返回相同的渲染结果。

commit 阶段(运行中阶段)

commit 阶段在首次渲染时会执行 componentDidMount,在组件更新时会执行 getSnapshotBeforeUpdate 和 componentDidUpdate。

componentDidMount

该生命周期方法会在组件挂载之后执行,也只会执行一次,也就是将组件对应的 DOM 插入 DOM 树中之后调用。它会在浏览器更新视图之前调用,如果在 componentDidMount 中直接调用 this.setState,它会触发额外的渲染,会再一次调用 render 函数,但是浏览器中视图的更新只会执行一次。

使用场景: 依赖于 DOM 的初始化操作应该放在这里,此外,我们一般在这个生命周期方法中发送网络请求、添加订阅等。

在函数式组件中,可以使用React的钩子函数 useEffect() 来完成 componentDidMount() 的功能。针对不同的需求,可以使用不同的函数来替代 componentDidMount()。

使用hooks代替:

如果需要在组件挂载时执行一次某些操作,可以在 useEffect() 的回调函数中加入空数组作为第二个参数,该回调函数仅会在组件挂载时被调用一次,就类似于 componentDidMount():

import { useEffect } from 'react';

function MyComponent(props) {
  useEffect(() => {
    // 挂载时的操作
    console.log('Component is mounted');
  }, []); // 空数组作为第二个参数
  // ...
}

**

如果需要在组件挂载和更新时都执行某些操作,可以省略掉第二个参数,这样 useEffect() 的回调函数会在每次组件更新时都被调用,就类似于 componentDidMount() 和 componentDidUpdate() 的结合:

import { useEffect } from 'react';

function MyComponent(props) {
  useEffect(() => {
    // 挂载或更新时的操作
    console.log('Component is mounted or updated');
  });
  // ...
}

**

有了 useEffect(),我们就可以在函数式组件中获得和使用 componentDidMount() 相同的能力,以做到更好的组件控制和管理。

getSnapshotBeforeUpdate

此生命周期函数在最近一次渲染提交至 DOM 树之前执行,此时 DOM 树还未改变,我们可以在这里获取 DOM 改变前的信息,例如:更新前 DOM 的滚动位置。

它接收两个参数,分别是:prevProps、prevState,上一个状态的 props 和上一个状态的 state。它的返回值将会传递给 componentDidUpdate 生命周期钩子的第三个参数。

使用场景: 需要获取更新前 DOM 的信息时。例如:需要以特殊方式处理滚动位置的聊天线程等。

componentDidUpdate

在组件更新后立即调用,首次渲染不会调用该方法。它的执行时机和 componentDidMount 一致,只是 componentDidMount 在首次渲染时调用,而 componentDidUpdate 在后续的组件更新时调用。可以在这个生命周期中直接调用 this.setState,但是必须包裹在一个条件语句中,否则会导致死循环。

componentDidUpdate 接收三个参数,分别是 prevProps、prevState、snapshot,即:前一个状态的 props,前一个状态的 state、getSnapshotBeforeUpdate 的返回值。

如果组件实现了 getSnapshotBeforeUpdate 生命周期函数,则 getSnapshotBeforeUpdate 的返回值将作为 componentDidUpdate 的第三个参数。

使用场景:

在这个生命周期方法中,可以对 DOM 进行操作或者进行网络请求。

componentWillUnmount

这个生命周期函数会在组件卸载以及销毁之前调用。

使用场景: 通常用来执行组件的清理操作,例如:清除 timer、取消网络请求、清除订阅等。

笔记来源

React 生命周期详解 - 掘金 (juejin.cn)

【react】 手把手学习react - state 及组件的生命周期 - 掘金 (juejin.cn)

阅读全文

git裸仓库并利用post-receive自动化部署hexo博客

前言

因为最近想把自己的博客项目实现自动化部署,所以百度百科了这个相关的知识,然后总结,并记录笔记,总结并分享自己的操作!

1.git普通库

什么是普通库

就是使用git init命令创建git仓库,可以先看看其目录结构


[root@VM-0-3-centos data]# git init simple
Initialized empty Git repository in /data/simple/.git/
[root@VM-0-3-centos data]# cd simple/
[root@VM-0-3-centos simple]# touch README.md
[root@VM-0-3-centos simple]# cd ..
[root@VM-0-3-centos data]# tree -a simple/
simple/
|-- .git
|   |-- branches
|   |-- config
|   |-- description
|   |-- HEAD
|   |-- hooks
|   |   |-- applypatch-msg.sample
|   |   |-- commit-msg.sample
|   |   |-- post-update.sample
|   |   |-- pre-applypatch.sample
|   |   |-- pre-commit.sample
|   |   |-- prepare-commit-msg.sample
|   |   |-- pre-push.sample
|   |   |-- pre-rebase.sample
|   |   `-- update.sample
|   |-- info
|   |   `-- exclude
|   |-- objects
|   |   |-- info
|   |   `-- pack
|   `-- refs
|       |-- heads
|       `-- tags
`-- README.md

10 directories, 14 files

通过上述命令操作后可以看到,git init simple 操作之后,创建了一个名为 simple 的库,simple 目录下还有一个 .git 子目录,其中包含了git系统常用的文件,在 .git 目录外是我们的工作区,可以存放我们库中待更新的文件,修改之后可以通过 git add,git commit 等命令更新 .git 中的内容,简单来说普通库就是在工作目录 simple 中还包括一个 .git 目录

git裸仓库

什么是裸仓库

就是使用git init –bare 命令创建git仓库,可以先看看其目录结构

[root@VM-0-3-centos data]# git init --bare bare.git
Initialized empty Git repository in /data/bare.git/
[root@VM-0-3-centos data]# tree bare.git/ -a
bare.git/
|-- branches
|-- config
|-- description
|-- HEAD
|-- hooks
|   |-- applypatch-msg.sample
|   |-- commit-msg.sample
|   |-- post-update.sample
|   |-- pre-applypatch.sample
|   |-- pre-commit.sample
|   |-- prepare-commit-msg.sample
|   |-- pre-push.sample
|   |-- pre-rebase.sample
|   `-- update.sample
|-- info
|   `-- exclude
|-- objects
|   |-- info
|   `-- pack
`-- refs
    |-- heads
    `-- tags

9 directories, 13 files

目录结构来看裸仓库和普通库很像,甚至是一模一样,那么我们仿照普通库操作在这个目录下提交一个文件会怎样呢?

[root@VM-0-3-centos data]# cd bare.git/
[root@VM-0-3-centos bare.git]# touch README.md
[root@VM-0-3-centos bare.git]# git add README.md
fatal: This operation must be run in a work tree
[root@VM-0-3-centos bare.git]# git status
fatal: This operation must be run in a work tree
[root@VM-0-3-centos bare.git]#

通过操作发现这个裸仓库不允许增删改库内的文件,甚至连 git status 这种命令都无法使用,统一提示了 fatal: This operation must be run in a work tree 这句话,告诉用户这些命令都必须在工作区内操作,既然不能修改,那么这个裸仓库就是“只读”的,那么它还有什么用呢?
虽然裸仓库不允许直接修改,但是可以作为服务端远程仓库,在本地克隆这个远程仓库之后再进行修改,这也是最常见的应用方式,总结来说,普通库和裸仓库的区别就是:普通库拥有工作目录,并且工作目录中可以存放正常编辑和提交的文件,而裸库只存放这些文件的commit记录,不允许用户直接在上面进行各种git操作。

3.为什么使用裸仓库而不使用普通库

普通库实际上包含两份数据的,一份在 .git 目录中以object形式存在,一份在工作目录中以源文件形式存在,我们每次使用 git 命令,可以保证工作目录内文件和 .git 目录数据是一致的,但是如果将普通库作为远端时,在下游提交数据时,远端库中的 .git 目录会直接更新,但是工作区却不知道此时谁在用,不能直接更新覆盖,这就造成了数据不一致的情况。

4.自动化部署

post-receive

post-receive是服务端钩子,post-receive 挂钩在整个过程完结以后运行,可以用来更新其他系统服务或者通知用户。 它接受与 pre-receive 相同的标准输入数据。 它的用途包括给某个邮件列表发信,通知持续集成(continous integration)的服务器, 或者更新问题追踪系统(ticket-tracking system) —— 甚至可以通过分析提交信息来决定某个问题(ticket)是否应该被开启,修改或者关闭。 该脚本无法终止推送进程,不过客户端在它结束运行之前将保持连接状态, 所以如果你想做其他操作需谨慎使用它,因为它将耗费你很长的一段时间。

自动化部署原理

利用 post-receive 进行自动化部署的原理就是,我们可以修改 post-receive 脚本,在修改提交后自动部署最新内容,实现自自动化部署。

5.具体操作

需求:服务端建立裸仓库,在接收到新的提交时,自动将项目部署到/myPoject-Yao/distblog/DarknessZY.github.io 目录下

服务端远端操作

  • 建立裸仓库/myPoject-Yao/repo/myBlog.git,建立对应的部署目录/myPoject-Yao/distblog
  • 新建 /myPoject-Yao/repo/myBlog.git/hooks/post-receive脚本,内容如下:
#!/bin/sh
echo '======上传代码到服务器======'
unset GIT_DIR
DeployPath=/myPoject-Yao/distblog/DarknessZY.github.io
cd $DeployPath
git pull origin master
echo '======代码更新完成======'

本地hexo项目操作

  • 关联服务器的远程裸仓库
git remote add origin2 root@服务器公网地址:/myPoject-Yao/repo/myBlog.git
  • 修改hexo博客根目录下的_config.yml
deploy:
- type: git
  repo: git@github.com:DarknessZY/DarknessZY.github.io.git
  branch: master
- type: git
  repo: root@服务器公网地址:/myPoject-Yao/repo/myBlog.git
  branch: master

6.注意

  • 如果服务器不是直接用的root用户,而是新建了一个用户,记得给给用户高权限,用户名尽量不要起和其他命令冲突的语义化命名
  • 建立的裸仓库和脚本文件也记得给高权限
  • 如果不想输入密码,可把本地git生成的公钥id_pub复制到服务器的/root/.ssh/authorized_keys下

7.我的博客

耀耀切克闹

阅读全文
1 ... 5 6 7 ... 9
avatar
耀耀切克闹

欢迎来到耀耀切克闹的博客

不是因为是主角才成功,而是成功了才成为主角

想要的都拥有,得不到的都释怀