你真的了解位运算吗?从基础概念到前端框架中的应用

你真的了解位运算吗?

最近在研究react v18的源码,看到一些位运算很好的写法,让我对位运算产生了兴趣,所以写篇文章记录一下

阅读源码总是能发现很多很好的写法,很有意思,了解位运算当然要了解二进制

文中引用的react源码是github仓库最新的代码 main分支 ,引用的vue3源码是github仓库最新的代码 main分支

二进制基础知识补充

概念:

二进制(Binary)是一种基于2的计数系统,它只使用两个数字符号:01。与我们日常使用的十进制(Decimal,基于10)不同,二进制的每一位代表的是2的幂次方。

二进制属于进位计数制,和八进制、十进制一样

进位计数制:

  1. 如果采用的数制有R个基本符号,则称为R数制,R成为数制的基数
  2. 进位计数制的编号符号逢R进位的规则
  3. 数制中每一固定的位置对应的单位值成为权
  4. 各位的权是以R为底的幂
  5. 一个数可按权展开成为多项式
js 复制代码
十进制 13.75
按权展开式就是
1 * 10的1次方 + 3 * 10的0次方 + 7 * 10的-1次方 + 5 * 10的-2次方

二进制 1101.11
按权展开就是
1 * 2的3次方 + 1 * 2的2次方 + 0 * 2的1次方 + 1 * 2的0次方 + 1 * 2的-1次方 + 1 * 2的-2次方

为什么计算机世界不使用我们熟悉的十进制来存储数据呢?

因为二进制有以下优势:

  1. 易于物理实现

    • 电子器件的双稳态特性:计算机的基本电子元件(如晶体管)天然具有两种稳定状态:(开/关,高电压/低电压)

    两种状态可以完美对应二进制的0和1,实现简单可靠

    • 如果使用十进制,就需要电子元件区分10种不同状态,这会导致电路设计复杂度指数级上升,抗干扰能力大幅下降,制造成本增加,可靠性降低等等
  2. 运算规则简单 (R(R + 1) / 2)

    二进制与布尔代数天然对应:1对应true, 0对应false

    所有复杂运算都可以通过三种基本逻辑运算实现:与、或、非

  3. 机器可靠性高

    在第二点中提到,复杂进制会导致可靠性降低

  4. 通用性强(逻辑运算)适用于所有类型的数据

简单的使用:一些常见的逻辑运算

js 复制代码
// 逻辑与 两个操作数都为1时结果为1,否则为0
0b1100 & 0b1010 = 0b1000

// 分解:
//   1100
// & 1010
//   ----
//   1000

// 逻辑或 两个操作数中至少一个为1时结果为1,否则为0
0b1100 | 0b1010 = 0b1110

// 分解:
//   1100
// | 1010
//   ----
//   1110

// 逻辑非 对操作数取反
~0b1100 = 0b0011 // 实际结果取决于位数,这里假设4位
// 注意:在JavaScript中会得到32位取反结果

// 逻辑与非 先与在进行取反
~(a & b)

// 逻辑或非 先或再进行取反
~(a | b)

// 逻辑同或 两个操作数相同时结果为1,不同时为0
~(a ^ b)

// 逻辑异或 两个操作数不同时结果为1,相同时为0
0b1100 ^ 0b1010 = 0b0110
// 分解:
//   1100
// ^ 1010
//   ----
//   0110

计算机不是只能处理二进制

现代计算机底层确实是二进制,但通过抽象层(编程语言)可以让我们使用十进制等其他进制工作

量子计算机不限于二进制

传统计算机基于二进制,量子计算机使用量子位,可以同时表示0和1的叠加态

二进制不是唯一可能的计算机基础

历史上存在过三进制计算机(例如苏联的Setun),但是二进制最终因其简单可靠以及其他优点能够脱颖而出

位运算

概念

位运算是对整数在内存中的二进制位进行操作的一组运算符

主要包括:

  1. 按位与(&):两位都为1时结果为1

    js 复制代码
    const a = 5;    // 二进制: 0101
    const b = 3;    // 二进制: 0011
    
    console.log(a & b); // 输出: 1 (二进制: 0001)
    
    // 过程
      0101 (5)
    & 0011 (3)
      ----
      0001 (1)
  2. 按位或(|):两位有一个为1时结果为1

    js 复制代码
    const a = 5;    // 0101
    const b = 3;    // 0011
    
    console.log(a | b); // 输出: 7 (二进制: 0111)
    
    // 过程
      0101 (5)
    | 0011 (3)
      ----
      0111 (7)
  3. 按位异或(^):两位不同时结果为1

    js 复制代码
    const a = 5;    // 0101
    const b = 3;    // 0011
    
    console.log(a ^ b); // 输出: 6 (二进制: 0110)
    
    // 过程
      0101 (5)
    ^ 0011 (3)
      ----
      0110 (6)
  4. 按位非(~):对每一位取反

    反转操作数的所有二进制位 , 在JavaScript中,数字使用32位表示,所以结果是32位有符号整数的补码形式

    js 复制代码
    const a = 5;    // 二进制: 0000 0101
    
    console.log(~a); // 输出: -6 (二进制: 1111 1010)
    
    // 过程
    ~ 0000 0101 (5)
      --------
      1111 1010 (-6)
  5. 左移(<<):将二进制位向左移动,右侧补0

    js 复制代码
    const a = 5;    // 二进制: 0101
    
    console.log(a << 1); // 输出: 10 (二进制: 1010)
    console.log(a << 2); // 输出: 20 (二进制: 10100)
    
    // 过程
    0101 (5) << 1 = 1010 (10)
    0101 (5) << 2 = 10100 (20)
  6. 右移(>>):将二进制位向右移动,左侧补符号位

    js 复制代码
    const a = 5;    // 二进制: 0101
    const b = -5;   // 二进制: 1111 1011 (补码表示)
    
    console.log(a >> 1); // 输出: 2 (二进制: 0010)
    console.log(b >> 1); // 输出: -3 (二进制: 1111 1101)
    
    // 过程
    0101 (5) >> 1 = 0010 (2)
    1111 1011 (-5) >> 1 = 1111 1101 (-3)
  7. 无符号右移(>>>):将二进制位向右移动,左侧补0

    js 复制代码
    const a = 5;    // 二进制: 0101
    const b = -5;   // 二进制: 1111 1011 (补码表示)
    
    console.log(a >>> 1); // 输出: 2 (二进制: 0010)
    console.log(b >>> 1); // 输出: 2147483645
    
    // 过程
    0101 (5) >>> 1 = 0010 (2)
    1111 1011 (-5) >>> 1 = 0111 1101 (2147483645)

程序中有哪些地方用到了位运算或者逻辑运算

一些简单的位运算的场景

权限管理

位运算结合逻辑运算

js 复制代码
const PERM = {
  READ: 1 << 0, // 1
  WRITE: 1 << 1, // 2
  DELETE: 1 << 2 // 4
};

let userPerm = PERM.READ | PERM.WRITE; // 赋予读写权限

// 检查是否有写权限
if (userPerm & PERM.WRITE) {
  console.log("有写权限");
}

下面有五个权限 0代表没有此权限 1代表有此权限

查看 详情 删除 编辑 创建
0 0 0 0 0
js 复制代码
// 添加权限 使用或运算即可

  00011 // 有编辑和创建的权限
  10000 // 添加查看的权限
  -----
  10011


// 删除权限 异或
  00011 // 有编辑和创建的权限
  00010 // 取消编辑的权限
  -----
  00001

// 判断是否有某一个权限  与
// 得到的值与要查看权限的值相等 就说明有这个权限
  00011 // 有编辑和创建的权限
  00010 // 查看是否有编辑权限
  -----
  00010 // 有
判断奇偶
js 复制代码
function isEven(n) {
  return (n & 1) === 0;
}

console.log(isEven(4)); // true
console.log(isEven(5)); // false
交换两个变量的值
js 复制代码
let a = 5, b = 7;

a ^= b;
b ^= a;
a ^= b;

console.log(a, b); // 7, 5
前端中也有一些包含进制转换的方法
js 复制代码
// 十进制转其他进制
const num = 255;
console.log(num.toString(2));  // "11111111" (二进制)
console.log(num.toString(8));  // "377" (八进制)
console.log(num.toString(16)); // "ff" (十六进制)

// 其他进制转十进制
console.log(parseInt('11111111', 2));  // 255
console.log(parseInt('377', 8));      // 255
console.log(parseInt('ff', 16));      // 255

// ES6二进制和八进制字面量
const binary = 0b11111111; // 255
const octal = 0o377;       // 255
const hex = 0xff;          // 255
hex和rgba互换
js 复制代码
/**
 * 颜色转换工具
 */
const ColorConverter = {
  /**
   * HEX 转 RGBA
   * @param {string} hex - HEX颜色值,支持3位或6位,可带#前缀
   * @param {number} [alpha=1] - 透明度,0-1之间
   * @returns {string} rgba颜色字符串
   */
  hexToRgba: function(hex, alpha = 1) {
    // 移除#号
    hex = hex.replace('#', '');
    
    // 处理3位缩写形式
    if (hex.length === 3) {
      hex = hex.split('').map(char => char + char).join('');
    }
    
    // 解析RGB值
    const r = parseInt(hex.substring(0, 2), 16);
    const g = parseInt(hex.substring(2, 4), 16);
    const b = parseInt(hex.substring(4, 6), 16);
    
    // 确保alpha在0-1之间
    const a = Math.min(1, Math.max(0, alpha));
    
    return `rgba(${r}, ${g}, ${b}, ${a})`;
  },
  
  /**
   * RGBA 转 HEX
   * @param {string} rgba - rgba颜色字符串
   * @returns {string} HEX颜色值,带#前缀
   */
  rgbaToHex: function(rgba) {
    // 提取rgba中的数值
    const rgbaValues = rgba.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i);
    
    if (!rgbaValues) {
      throw new Error('Invalid RGBA color format');
    }
    
    // 解析RGB值
    const r = parseInt(rgbaValues[1]);
    const g = parseInt(rgbaValues[2]);
    const b = parseInt(rgbaValues[3]);
    
    // 将RGB转换为HEX
    const toHex = (value) => {
      const hex = value.toString(16);
      return hex.length === 1 ? '0' + hex : hex;
    };
    
    return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
  }
};

// 使用
console.log(ColorConverter.hexToRgba('#ff0000')); // rgba(255, 0, 0, 1)
console.log(ColorConverter.hexToRgba('#f00', 0.5)); // rgba(255, 0, 0, 0.5)
console.log(ColorConverter.rgbaToHex('rgba(255, 0, 0, 1)')); // #ff0000
console.log(ColorConverter.rgbaToHex('rgb(0, 255, 0)')); // #00ff00

React中的位运算

fiber的flags

在 React 中,用来标记 fiber 操作的 flags,使用的就是二进制:

js 复制代码
// 源码文件路径 packages/react-reconciler/src/ReactFiberFlags.js
export const NoFlags = /*                      */ 0b0000000000000000000000000000000;
export const PerformedWork = /*                */ 0b0000000000000000000000000000001;
export const Placement = /*                    */ 0b0000000000000000000000000000010;
export const DidCapture = /*                   */ 0b0000000000000000000000010000000;
export const Hydrating = /*                    */ 0b0000000000000000001000000000000;

// You can change the rest (and add more).
export const Update = /*                       */ 0b0000000000000000000000000000100;
export const Cloned = /*                       */ 0b0000000000000000000000000001000;

export const ChildDeletion = /*                */ 0b0000000000000000000000000010000;
export const ContentReset = /*                 */ 0b0000000000000000000000000100000;
export const Callback = /*                     */ 0b0000000000000000000000001000000;
/* Used by DidCapture:                            0b0000000000000000000000010000000; */

export const ForceClientRender = /*            */ 0b0000000000000000000000100000000;
export const Ref = /*                          */ 0b0000000000000000000001000000000;
export const Snapshot = /*                     */ 0b0000000000000000000010000000000;
export const Passive = /*                      */ 0b0000000000000000000100000000000;
/* Used by Hydrating:                             0b0000000000000000001000000000000; */

export const Visibility = /*                   */ 0b0000000000000000010000000000000;
export const StoreConsistency = /*             */ 0b0000000000000000100000000000000;

这些flags是用来标记fiber状态的

之所以要专门抽离fiber状态,是因为这种操作是非常高效的。针对一个 fiber 的操作,可能有增加、删除、修改,但是我不直接进行操作,而是给这个 fiber 打上一个 flag,接下来在后面的流程中针对有 flag 的 fiber 统一进行操作。

js 复制代码
// 源码文件路径 packages/react-reconciler/src/ReactFiber.js  170行
// 初始化的时候是没有状态的
// 这里将flags以及子树状态初始化为没有状态
this.flags = NoFlags;
this.subtreeFlags = NoFlags;

// 创建wip tree node,用于fiber双缓冲
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate; // 存储旧的fiber对象 fiber双缓冲,新旧Fiber Tree的节点通过alternate相互指向
  if (workInProgress === null) {
    // 如果wip node是空,说明是初次渲染
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;
  } else {
    // 反之,则说明已经有了alternate,重置flags
    // ...
    workInProgress.flags = NoFlags
    workInProgress.subtreeFlags = NoFlags;
    workInProgress.deletions = null; // 用户diff比较是的待删除数组
	// ...
  }
    
  // ...

  return workInProgress;
}
js 复制代码
// 小例子:合并多个状态
flag = flag | PerformedWork | Update

// 要判断是否有某一个 flag,直接通过 & 来进行判断即可
// 判断是否有  PerformedWork 种类的更新
if(flag & PerformedWork){
    console.log('执行 PerformedWork')
}
js 复制代码
// 源码文件路径: packages/react-reconciler/src/ReactFiberFlags.js
// 我在文件里找到了一个变量 LifecycleEffectMask
// 用到了 按位或(|) 运算符在其中一个或两个操作数对应的二进制位为 1 时,该位的结果值为 1
// 那么LifecycleEffectMask变量值就是同时拥有了六种 flags 状态
export const LifecycleEffectMask = Passive | Update | Callback | Ref | Snapshot | StoreConsistency;


// 然后搜了一下这个用法,因为既然声明了联合的状态,那么肯定有用到判断的

// 源码路径:packages/react-reconciler/src/ReactFiberThrow.js 282行 在这个文件里面找到了用法,这个文件是用来抛出错误的
// We're going to commit this fiber even though it didn't complete.
// But we shouldn't call any lifecycle methods or callbacks. Remove
// all lifecycle effect tags.
// 大致意思:我们将要提交一个不完整的节点,但是我们不能调用任何生命周期方法或者回调函数,移除所有的生命周期副作用标签

// Incomplete的值是0b0000000000000001000000000000000  上方有提到
sourceFiber.flags &= ~(LifecycleEffectMask | Incomplete)

// 拆解
// 1. sourceFiber.flags = sourceFiber.flags & ~(LifecycleEffectMask | Incomplete)
// 2. ~(LifecycleEffectMask | Incomplete)
//    第二步可以拆解为 LifecycleEffectMask | Incomplete,这里就是在之前的状态上面又增加了一种状态,现在是七种状态 结果是 52804 
//    对得到的结果进行按位非(~)操作,将运算数的位反转  我运算了一下得到的结果是 -52805
// 3. sourceFiber.flags & ~(LifecycleEffectMask | Incomplete)  flags是number类型,可以是任意数

扩展:fiber是什么?

可以从三个方面来解释:架构、数据类型、动态的工作单元

架构:React从v16之后重构了之前的架构,新架构称之为fiber架构

数据类型:Fiber本质上也是一个对象,是在之前React元素基础上的一种升级版本。每个FiberNode对象里面会包含React元素的类型、周围连接的FiberNode以及DOM相关信息

动态的工作单元:在每个FiberNode中,保存了本次更新中该React元素变化的数据,还有就是要执行的工作以及副作用的信息

js 复制代码
// 源码位置:packages/react-reconciler/src/ReactFiber.js  138行
function FiberNode(
  this: $FlowFixMe,
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // Fiber
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;
  this.refCleanup = null;

  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  // Effects
  // 副作用相关
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;
      
  // 与调度优先级有关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  this.alternate = null;
      
  // ...
}

至于里面每个标签就不再进行详述了,要不然扩展的有点多,有点儿喧宾夺主的意思了,下一篇更新一下fiber双缓冲以及渲染流程,在里面再进行详述

lane模型

lane 模型是一套优先级机制,相比 Scheduler,lane 模型能够对任务进行更细粒度的控制。

js 复制代码
// 源代码文件路径:packages/react-reconciler/src/ReactFiberLanes.js 39行
export const TotalLanes = 31;

export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

export const SyncHydrationLane: Lane = /*               */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000010;
export const SyncLaneIndex: number = 1;

export const InputContinuousHydrationLane: Lane = /*    */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000001000;

export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /*                     */ 0b0000000000000000000000000100000;

export const SyncUpdateLanes: Lane =
  SyncLane | InputContinuousLane | DefaultLane;

export const GestureLane: Lane = /*                     */ 0b0000000000000000000000001000000;
// ...

获取最高级别的lanes

js 复制代码
function getHighestPriorityLanes(lanes) {
  // 在lanes组合中,得到优先级最高的lane
  switch (getHighestPriorityLane(lanes)) {
    case SyncHydrationLane:
      return SyncHydrationLane;
    case SyncLane:
      return SyncLane;
    case InputContinuousHydrationLane:
      return InputContinuousHydrationLane;
    case InputContinuousLane:
      return InputContinuousLane;
    case DefaultHydrationLane:
      return DefaultHydrationLane;
    case DefaultLane:
      return DefaultLane;
    case GestureLane:
      return GestureLane;
    case TransitionHydrationLane:
      return TransitionHydrationLane;
          
    // ...
          
    default:
      return lanes;
  }
}

// lane 在表示优先级的时候,大致是这样的:
// 0000 0001
// 0000 0010
// 0010 0000
// lanes表示一套lane的组合 上面三个lane组合到一起就变成了一个lanes 0010 0011
// getHighestPriorityLanes 这个方法要做的事情就是分离出优先级最高的
// 0010 0011 --> getHighestPriorityLanes 处理 --> 0000 0001
function getHighestPriorityLane(lanes) {
  return lanes & -lanes;
}

// 假设要对以下两个lane进行合并
const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;
const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000000100;

// 合并结果就是 lanes --- 0b0000000000000000000000000000101
// 取负值后就是 -lanes ---  0b1111111111111111111111111111011
// & 操作后就是 0b0000000000000000000000000000001

// SyncLane 优先级高于 InputContinuousLane
上下文
js 复制代码
// 源码文件路径:packages/react-reconciler/src/ReactFiberWorkLoop.js 373行
export const NoContext = /*             */ 0b000;
const BatchedContext = /*               */ 0b001;
export const RenderContext = /*         */ 0b010;
export const CommitContext = /*         */ 0b100;

// 初始化的时候没有上下文
let executionContext: ExecutionContext = NoContext;

// 到了渲染阶段的时候,改为RenderContext
executionContext |= RenderContext;

// 执行方法的时候,会先判断处于哪一个上下文中

// 函数位于 1807 行,函数大意是对于时间函数而言,是否无效的上下文
export function isInvalidExecutionContextForEventFunction(): boolean {
  return (executionContext & RenderContext) !== NoContext; // 是否处于 RenderContext 上下文中
}

// 3279行代码 commitRoot 函数中
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
    throw new Error('Should not already be working.');
}


// 如果要离开某一个上下文
executionContext &= ~RenderContext; // 从当前上下文中移除 RenderContext 上下文

Vue中的位运算

ShapeFlags 来标记组件的类型和子节点类型:

js 复制代码
// 源码文件路径:packages/shared/src/ShapeFlags.ts
export enum ShapeFlags {
  ELEMENT = 1, // 0001
  FUNCTIONAL_COMPONENT = 1 << 1, // 0010
  STATEFUL_COMPONENT = 1 << 2, // 0100
  TEXT_CHILDREN = 1 << 3, // 1000
  ARRAY_CHILDREN = 1 << 4,
  SLOTS_CHILDREN = 1 << 5,
  TELEPORT = 1 << 6,
  SUSPENSE = 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9,
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT,
}
  
  
// 使用示例:packages/runtime-core/src/renderer.ts 430行
if (shapeFlag & ShapeFlags.ELEMENT) {
    // 检查标志,处理元素节点
} else if(shapeFlag & ShapeFlags.COMPONENT) {
    // 检查标志,处理组件节点       
}
// ...

补丁标志 PatchFlags 用于优化虚拟 DOM 的 diff 算法:

是编译器生成的优化提示。

在进行差异比较(diff)过程中,当遇到带有动态子元素(dynamicChildren)的块时,算法会进入 "优化模式"。在这种模式下,我们知道虚拟 DOM(vdom)是由编译器生成的渲染函数产生的,所以算法只需要处理由这些补丁标志明确标记的更新。

补丁标志可以使用按位或运算符(|)进行组合,并使用按位与运算符(&)进行检查。

PatchFlags 确实是 Vue 3 相比 Vue 2 在性能上实现质的飞跃的核心机制之一。让diff过程从vue2的全量对比到vue3的靶向更新。

文件内有详细的英文注解,下面只介绍了两个状态的说明 CACHED、BAIL

js 复制代码
// 源码文件路径:packages/shared/src/PatchFlags.ts
export enum PatchFlags {
  TEXT = 1,
  CLASS = 1 << 1,
  STYLE = 1 << 2,
  PROPS = 1 << 3,
  FULL_PROPS = 1 << 4,
  NEED_HYDRATION = 1 << 5,
  STABLE_FRAGMENT = 1 << 6,
  KEYED_FRAGMENT = 1 << 7,
  UNKEYED_FRAGMENT = 1 << 8,
  NEED_PATCH = 1 << 9,
  DYNAMIC_SLOTS = 1 << 10,
  DEV_ROOT_FRAGMENT = 1 << 11,
      
  // 表示一个缓存的静态虚拟节点(vnode)。这也是在hydration过程中的一个提示,即跳过整个子树,因为静态内容永远无需更新。
  CACHED = -1,
      
  // 这是一种特殊标志,它指示差异对比算法应退出优化模式。例如,对于由 renderSlot() 创建的块片段 ,当遇到非编译器生成的插槽时(即手动编写的渲染函数,对   // 于这种情况应始终进行完整的差异对比),或者遇到手动克隆的虚拟节点(cloneVNodes)时,就需要使用该标志。
  BAIL = -2, 
}

// 使用示例:
// 检查是否是动态属性 处理动态 props
if (patchFlag & PatchFlags.PROPS) {}

还有一些标志信息,EffectFlags、SlotFlags等等

end!感谢大家能看完,写的不好的地方或者有什么要补充的可以评论出来,感谢!

相关推荐
LLLLYYYRRRRRTT6 分钟前
MariaDB 数据库管理与web服务器
前端·数据库·mariadb
胡gh7 分钟前
什么是瀑布流?用大白话给你讲明白!
前端·javascript·面试
universe_0113 分钟前
day22|学习前端ts语言
前端·笔记
teeeeeeemo17 分钟前
一些js数组去重的实现算法
开发语言·前端·javascript·笔记·算法
Zz_waiting.18 分钟前
Javaweb - 14.1 - 前端工程化
前端·es6
掘金安东尼20 分钟前
前端周刊第426期(2025年8月4日–8月10日)
前端·javascript·面试
Abadbeginning20 分钟前
FastSoyAdmin导出excel报错‘latin-1‘ codec can‘t encode characters in position 41-54
前端·javascript·后端
ZXT22 分钟前
WebAssembly
前端
卢叁22 分钟前
Flutter开发环境安装指南
前端·flutter
curdcv_po40 分钟前
Three.js,闲谈3D——智慧XX
前端