阅读视图

发现新文章,点击刷新页面。

[blockly]自定义工具栏样式

前言

因为在网上发现blockly相关的帖子太少,或者就是因为版本太老,无法正常使用,所有在这里把这段时间遇到的问题都给整理一下分享出来 效果: 在这里插入图片描述

版本

使用的是blockly 12.1.0版本

操作

首先要创建一个ToolboxCategory 类

我们可以在里面来定义我们的相关的操作 constructor()构造函数,继承父类即可,一般不需要修改 init()类别初始化后自动调用,用于设置样式、DOM 操作 addColourBorder_()Blockly 初始化颜色时调用,控制分类背景色或边框色 setSelected()类别选中/取消选中时自动调用,控制选中样式 更多的可以参考文档

我这里是这样定义的

class CustomToolboxCategory extends Blockly.ToolboxCategory {
  constructor(categoryDef, toolbox, opt_parent) {
    super(categoryDef, toolbox, opt_parent)
  }
  // 初始化
  init() {
    super.init();

    if (this.rowDiv_) {
      this.rowDiv_.style.margin = '8px 8px';        // 上下间距
      this.rowDiv_.style.borderRadius = '6px';    // 圆角
      this.rowDiv_.style.fontSize = '14px';       // 字体大小
      this.rowDiv_.style.height = '50px'
      // 字体颜色
      var labelDom = this.rowDiv_.getElementsByClassName('blocklyToolboxCategoryLabel')[0];
      labelDom.style.color = 'white';
      labelDom.style.fontWeight = 'bold';
      this.rowDiv_.style.display = 'flex';
      this.rowDiv_.style.alignItems = 'center';
      this.rowDiv_.style.justifyContent = 'center';
    }
  }
  //Blockly 初始化颜色时调用
  addColourBorder_(colour) {
    this.rowDiv_.style.backgroundColor = colour;
  }
  /** @override */
  // 类别选中/取消选中时自动调用
  setSelected(isSelected) {
    var labelDom = this.rowDiv_.getElementsByClassName('blocklyToolboxCategoryLabel')[0];
    if (isSelected) {
      //选中状态
      // 把DIV背景设置为白色
      this.rowDiv_.style.backgroundColor = 'white';
      labelDom.style.color = this.colour_;
    } else {
      this.rowDiv_.style.backgroundColor = this.colour_;
      // 设置文本颜色为白色
      labelDom.style.color = 'white';
    }
  }
}

创建完成ToolboxCategory 类之后,我们要对这个类进行注册

// 函数介绍
Blockly.registry.register(
  type,     // 注册的类型,比如 TOOLBOX_ITEM 表示工具箱元素
  name,     // 注册的名称,用于标识,比如 'category'
  classRef, // 自定义的类,比如你要用来替代默认 Category 的类
  optOverride // 可选,是否覆盖默认注册(通常设置为 true)
)
// 我的函数
// 2. 注册到 Blockly 注册表中
Blockly.registry.register(
  Blockly.registry.Type.TOOLBOX_ITEM,
  'category', // 这里要记住
  CustomToolboxCategory,// ToolboxCategory 类名
  true
)

我们注册完成之后,就要开始引用他了 我们需要在工具箱之中使用category 要注意这一句 kind: 'category',这是我们是否可以正确加载刚刚创建的ToolboxCategory 类关键

const toolbox = {
  kind: 'categoryToolbox',
  contents: [
    {
      kind: 'category',
      name: '逻辑',
      colour: '#5CA65C',
      contents: [
        {
          kind: 'block',
          type: 'controls_if'
        },
        {
          kind: 'block',
          type: 'logic_compare'
        }
      ]
    },
    {
      kind: 'category',
      name: '数学',
      colour: '#5C81A6',
      contents: [
        {
          kind: 'block',
          type: 'math_number'
        },
        {
          kind: 'block',
          type: 'math_arithmetic'
        }
      ]
    }
  ]
}

完整代码

<template>
  <div>
    <div ref="blocklyDiv" style="height: 600px; width: 100%; border: 1px solid #ccc;"></div>
  </div>
</template>

<script setup>
import { onMounted, ref } from 'vue'
import * as Blockly from 'blockly'

// 1. 自定义 ToolboxCategory 类
class CustomToolboxCategory extends Blockly.ToolboxCategory {
  constructor(categoryDef, toolbox, opt_parent) {
    super(categoryDef, toolbox, opt_parent)
  }

  init() {
    super.init();

    if (this.rowDiv_) {
      this.rowDiv_.style.margin = '8px 8px';        // 上下间距
      this.rowDiv_.style.borderRadius = '6px';    // 圆角
      this.rowDiv_.style.fontSize = '14px';       // 字体大小
      this.rowDiv_.style.height = '50px'
      // 字体颜色
      var labelDom = this.rowDiv_.getElementsByClassName('blocklyToolboxCategoryLabel')[0];
      labelDom.style.color = 'white';
      labelDom.style.fontWeight = 'bold';
      this.rowDiv_.style.display = 'flex';
      this.rowDiv_.style.alignItems = 'center';
      this.rowDiv_.style.justifyContent = 'center';
    }
  }
  addColourBorder_(colour) {
    this.rowDiv_.style.backgroundColor = colour;
  }
  /** @override */
  setSelected(isSelected) {
    var labelDom = this.rowDiv_.getElementsByClassName('blocklyToolboxCategoryLabel')[0];
    if (isSelected) {
      //选中状态
      // 把DIV背景设置为白色
      this.rowDiv_.style.backgroundColor = 'white';
      labelDom.style.color = this.colour_;
    } else {
      this.rowDiv_.style.backgroundColor = this.colour_;
      // 设置文本颜色为白色
      labelDom.style.color = 'white';
    }
  }
}

// 2. 注册到 Blockly 注册表中
Blockly.registry.register(
  Blockly.registry.Type.TOOLBOX_ITEM,
  'category',
  CustomToolboxCategory,// 修改工具栏的类
  true
)

const blocklyDiv = ref()
const toolbox = {
  kind: 'categoryToolbox',
  contents: [
    {
      kind: 'category',
      name: '逻辑',
      colour: '#5CA65C',
      contents: [
        {
          kind: 'block',
          type: 'controls_if'
        },
        {
          kind: 'block',
          type: 'logic_compare'
        }
      ]
    },
    {
      kind: 'category',
      name: '数学',
      colour: '#5C81A6',
      contents: [
        {
          kind: 'block',
          type: 'math_number'
        },
        {
          kind: 'block',
          type: 'math_arithmetic'
        }
      ]
    }
  ]
}

// 4. 初始化 Blockly
onMounted(() => {
  Blockly.inject(blocklyDiv.value, {
    toolbox,
    renderer: 'geras',
    theme: Blockly.Themes.Classic
  })
})
</script>

CSS 样式计算与视觉格式化模型详解

前端渲染基础:CSS 样式计算与视觉格式化模型详解

一、CSS 样式计算:从规则到实际效果

CSS 样式计算是浏览器将 CSS 规则应用到 DOM 元素,计算出每个元素最终样式的过程。这个过程看似简单,实则涉及复杂的算法和优先级规则。

1. 收集样式规则

浏览器首先需要收集所有相关的 CSS 规则,这些规则来源包括:

  • 外部样式表(通过引入)

  • 内部样式表(位于标签中)

  • 行内样式(直接写在 HTML 元素的 style 属性中)

  • 浏览器默认样式(User Agent Stylesheet)

浏览器会构建一个样式表集合,并将这些规则解析为内部数据结构(通常是哈希表或树)以便快速查找。例如:

css

/* 外部样式表 */
body { font-family: Arial; }
.container { width: 960px; }

/* 内部样式表 */
<style>
  .header { background-color: #f5f5f5; }
</style>

/* 行内样式 */
<div class="container" style="margin: 0 auto;">内容</div>
2. 层叠与优先级

当多个 CSS 规则应用到同一个元素时,浏览器需要通过层叠(Cascade)机制决定最终应用哪些样式。优先级由以下因素决定(从高到低):

  1. !important 声明

  2. 行内样式

  3. ID 选择器

  4. 类选择器、属性选择器、伪类

  5. 元素选择器、伪元素

  6. 通配符选择器

  7. 继承的样式

优先级计算可以用一个简单的公式表示:

plaintext

!important > 行内样式 > (ID数量, 类数量, 元素数量)

例如:

css

/* 优先级: 0,0,1 */
p { color: red; }

/* 优先级: 0,1,0 */
.text-danger { color: blue; }

/* 优先级: 1,0,0 */
#special-paragraph { color: green; }
3. 继承与默认值

有些 CSS 属性会自动从父元素继承值,这称为继承(Inheritance)。例如:

  • color

  • font-family

  • font-size

  • text-align

而有些属性则不会继承,例如:

  • width

  • height

  • margin

  • padding

  • border

当没有显式指定值时,浏览器会使用属性的默认值。例如,display属性的默认值是inline,而position的默认值是static

二、视觉格式化模型:从样式到布局

视觉格式化模型是浏览器根据计算出的样式,将元素转换为实际屏幕上可见的盒子(Box)的过程。

1. 盒模型(Box Model)

盒模型是 CSS 布局的基础,每个元素都被视为一个矩形盒子,由内容区(content)、内边距(padding)、边框(border)和外边距(margin)组成。

plaintext

+---------------------+
|      margin         |
|  +---------------+  |
|  |    border     |  |
|  |  +---------+  |  |
|  |  | padding |  |  |
|  |  | +-----+ |  |  |
|  |  | |content| |  |  |
|  |  | +-----+ |  |  |
|  |  +---------+  |  |
|  +---------------+  |
+---------------------+

盒模型的宽度和高度计算方式:

plaintext

总宽度 = width + padding-left + padding-right + border-left + border-right + margin-left + margin-right
总高度 = height + padding-top + padding-bottom + border-top + border-bottom + margin-top + margin-bottom

可以通过box-sizing属性修改盒模型的计算方式:

css

/* 标准盒模型(默认值) */
.box { box-sizing: content-box; }

/* 怪异盒模型(宽度包含padding和border) */
.box { box-sizing: border-box; }
2. 布局模式

CSS 提供了多种布局模式,用于控制元素在页面中的排列方式:

  • 块级布局(Block Layout) :元素按垂直方向排列,每个块级元素独占一行

  • 行内布局(Inline Layout) :元素按水平方向排列,不会换行

  • 表格布局(Table Layout) :元素按照表格结构排列

  • 弹性布局(Flexbox) :用于一维布局,提供灵活的对齐和分布能力

  • 网格布局(Grid) :用于二维布局,可以同时控制行和列

  • 浮动布局(Float) :元素脱离正常流,向左或向右浮动

例如,使用 Flexbox 布局:

css

.container {
  display: flex;
  justify-content: center;
  align-items: center;
}
3. 格式化上下文(Formatting Context)

格式化上下文是一个独立的渲染区域,规定了内部元素如何布局,并且与外部元素相互隔离。常见的格式化上下文包括:

  • 块级格式化上下文(BFC) :由浮动元素、绝对定位元素、行内块元素等创建

  • 行内格式化上下文(IFC) :由行内元素创建

  • 网格格式化上下文(GFC) :由 display: grid 创建

  • 弹性格式化上下文(FFC) :由 display: flex 创建

BFC 的主要作用是清除浮动,防止 margin 重叠等。创建 BFC 的常见方式:

css

.element {
  float: left; /* 浮动元素 */
  overflow: hidden; /* 触发BFC */
  display: inline-block; /* 行内块元素 */
  position: absolute; /* 绝对定位元素 */
}
4. 层叠上下文(Stacking Context)

层叠上下文决定了元素在 Z 轴上的显示顺序,即哪些元素会覆盖在其他元素之上。层叠上下文由以下条件创建:

  • 根元素(HTML)

  • position 值为 absolute 或 relative 且 z-index 不为 auto

  • position 值为 fixed 或 sticky

  • display 值为 flex 或 grid 的子元素且 z-index 不为 auto

  • opacity 值小于 1 的元素

层叠顺序从低到高:

  1. 背景和边框(层叠上下文的背景和边框)

  2. 负 z-index 值的子元素

  3. 块级子元素

  4. 浮动子元素

  5. 行内子元素

  6. z-index: 0 的子元素

  7. 正 z-index 值的子元素

例如:

css

.container {
  position: relative;
  z-index: 1; /* 创建层叠上下文 */
}

.overlay {
  position: absolute;
  z-index: 2; /* 显示在container之上 */
}

总结

CSS 样式计算和视觉格式化模型是前端开发的核心基础知识,深入理解这些概念对于解决布局难题、优化渲染性能至关重要。通过掌握样式规则的收集、层叠优先级、盒模型、布局模式、格式化上下文和层叠上下文,开发者能够更精确地控制页面的外观和行为,避免常见的布局陷阱。

扩写一些

前端渲染基础:CSS 样式计算与视觉格式化模型详解(扩展版)

一、CSS 样式计算:从规则到实际效果

CSS 样式计算是浏览器将 CSS 规则应用到 DOM 元素,计算出每个元素最终样式的过程。这个过程看似简单,实则涉及复杂的算法和优先级规则。

1. 收集样式规则

浏览器首先需要收集所有相关的 CSS 规则,这些规则来源包括:

  • 外部样式表(通过引入)

  • 内部样式表(位于标签中)

  • 行内样式(直接写在 HTML 元素的 style 属性中)

  • 浏览器默认样式(User Agent Stylesheet)

  • 用户自定义样式(浏览器扩展或用户偏好设置)

浏览器会构建一个样式表集合,并将这些规则解析为内部数据结构(通常是哈希表或树)以便快速查找。值得注意的是,不同来源的样式表加载顺序也会影响最终的样式计算,特别是当存在相同优先级的规则时。

css

/* 外部样式表 */
body { font-family: Arial; }
.container { width: 960px; }

/* 内部样式表 */
<style>
  .header { background-color: #f5f5f5; }
</style>

/* 行内样式 */
<div class="container" style="margin: 0 auto;">内容</div>

现代浏览器在解析样式表时会进行优化,例如并行下载多个外部样式表,以及使用样式表缓存机制提高重复加载时的性能。

2. 层叠与优先级

当多个 CSS 规则应用到同一个元素时,浏览器需要通过层叠(Cascade)机制决定最终应用哪些样式。优先级由以下因素决定(从高到低):

  1. !important 声明

  2. 行内样式

  3. ID 选择器

  4. 类选择器、属性选择器、伪类

  5. 元素选择器、伪元素

  6. 通配符选择器

  7. 继承的样式

优先级计算可以用一个简单的公式表示:

plaintext

!important > 行内样式 > (ID数量, 类数量, 元素数量)

例如:

css

/* 优先级: 0,0,1 */
p { color: red; }

/* 优先级: 0,1,0 */
.text-danger { color: blue; }

/* 优先级: 1,0,0 */
#special-paragraph { color: green; }

特殊情况说明

  • 当两条规则优先级相同时,后定义的规则会覆盖先定义的规则
  • !important 声明会覆盖任何优先级规则,但应谨慎使用,过度使用会导致样式难以维护
  • 继承的样式优先级最低,即使父元素的样式优先级很高
3. 继承与默认值

有些 CSS 属性会自动从父元素继承值,这称为继承(Inheritance)。理解哪些属性会继承,哪些不会继承,对于编写高效的 CSS 代码至关重要。

常见继承属性

  • color

  • font-family

  • font-size

  • font-weight

  • text-align

  • line-height

  • letter-spacing

常见非继承属性

  • width

  • height

  • margin

  • padding

  • border

  • background

  • position

  • display

当没有显式指定值时,浏览器会使用属性的默认值。默认值由 CSS 规范定义,但不同浏览器可能存在细微差异。例如,display属性的默认值是inline,而position的默认值是static

二、视觉格式化模型:从样式到布局

视觉格式化模型是浏览器根据计算出的样式,将元素转换为实际屏幕上可见的盒子(Box)的过程。

1. 盒模型(Box Model)

盒模型是 CSS 布局的基础,每个元素都被视为一个矩形盒子,由内容区(content)、内边距(padding)、边框(border)和外边距(margin)组成。

plaintext

+---------------------+
|      margin         |
|  +---------------+  |
|  |    border     |  |
|  |  +---------+  |  |
|  |  | padding |  |  |
|  |  | +-----+ |  |  |
|  |  | |content| |  |  |
|  |  | +-----+ |  |  |
|  |  +---------+  |  |
|  +---------------+  |
+---------------------+

盒模型的宽度和高度计算方式:

plaintext

总宽度 = width + padding-left + padding-right + border-left + border-right + margin-left + margin-right
总高度 = height + padding-top + padding-bottom + border-top + border-bottom + margin-top + margin-bottom

可以通过box-sizing属性修改盒模型的计算方式:

css

/* 标准盒模型(默认值) */
.box { box-sizing: content-box; }

/* 怪异盒模型(宽度包含padding和border) */
.box { box-sizing: border-box; }

外边距折叠(Margin Collapsing)

  • 相邻的块级元素之间的垂直外边距会发生折叠,取两者中的较大值
  • 父子元素之间如果没有边框、内边距、行内内容或 clear 分隔,垂直外边距也会发生折叠
  • 浮动元素、绝对定位元素、行内块元素等不会发生外边距折叠
2. 布局模式

CSS 提供了多种布局模式,用于控制元素在页面中的排列方式:

  • 块级布局(Block Layout) :元素按垂直方向排列,每个块级元素独占一行

  • 行内布局(Inline Layout) :元素按水平方向排列,不会换行

  • 表格布局(Table Layout) :元素按照表格结构排列

  • 浮动布局(Float) :元素脱离正常流,向左或向右浮动

  • 弹性布局(Flexbox) :用于一维布局,提供灵活的对齐和分布能力

  • 网格布局(Grid) :用于二维布局,可以同时控制行和列

  • 定位布局(Positioning) :通过 position 属性精确定位元素

Flexbox 布局示例

css

.container {
  display: flex;
  flex-direction: row; /* 主轴方向 */
  justify-content: space-between; /* 主轴对齐方式 */
  align-items: center; /* 交叉轴对齐方式 */
  flex-wrap: wrap; /* 换行设置 */
}

.item {
  flex: 1 1 200px; /* 灵活增长、收缩和基准尺寸 */
}

Grid 布局示例

css

.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* 三列,每列等宽 */
  grid-template-rows: auto; /* 行高自动 */
  gap: 20px; /* 行列间距 */
}

.item {
  grid-column: span 1; /* 跨越1列 */
  grid-row: span 1; /* 跨越1行 */
}
3. 格式化上下文(Formatting Context)

格式化上下文是一个独立的渲染区域,规定了内部元素如何布局,并且与外部元素相互隔离。常见的格式化上下文包括:

  • 块级格式化上下文(BFC) :由浮动元素、绝对定位元素、行内块元素等创建

  • 行内格式化上下文(IFC) :由行内元素创建

  • 网格格式化上下文(GFC) :由 display: grid 创建

  • 弹性格式化上下文(FFC) :由 display: flex 创建

BFC 的主要作用是清除浮动,防止 margin 重叠等。创建 BFC 的常见方式:

css

.element {
  float: left; /* 浮动元素 */
  overflow: hidden; /* 触发BFC */
  display: inline-block; /* 行内块元素 */
  position: absolute; /* 绝对定位元素 */
  display: table-cell; /* 表格单元格 */
  display: flex; /* Flex容器 */
  display: grid; /* Grid容器 */
}

IFC 的特性

  • 行内元素会在一行内水平排列,直到一行排满换行
  • 行内元素的垂直对齐由 vertical-align 属性控制
  • 行内格式化上下文的高度由行高 (line-height) 决定
4. 层叠上下文(Stacking Context)

层叠上下文决定了元素在 Z 轴上的显示顺序,即哪些元素会覆盖在其他元素之上。层叠上下文由以下条件创建:

  • 根元素(HTML)

  • position 值为 absolute 或 relative 且 z-index 不为 auto

  • position 值为 fixed 或 sticky

  • display 值为 flex 或 grid 的子元素且 z-index 不为 auto

  • opacity 值小于 1 的元素

  • transform 值不为 none 的元素

  • mix-blend-mode 值不为 normal 的元素

层叠顺序从低到高:

  1. 背景和边框(层叠上下文的背景和边框)

  2. 负 z-index 值的子元素

  3. 块级子元素

  4. 浮动子元素

  5. 行内子元素

  6. z-index: 0 的子元素

  7. 正 z-index 值的子元素

示例代码

css

.container {
  position: relative;
  z-index: 1; /* 创建层叠上下文 */
  opacity: 0.9; /* 也会创建层叠上下文 */
}

.overlay {
  position: absolute;
  z-index: 2; /* 显示在container之上 */
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.5);
}

三、实际应用中的常见问题与解决方案

1. 浮动元素导致的父容器高度塌陷

问题:当子元素设置为 float:left 或 float:right 时,父容器会失去高度,无法包裹子元素。

解决方案

  • 使用 clearfix 方法:

    css

    .clearfix::after {
      content: "";
      display: block;
      clear: both;
    }
    
  • 让父容器成为 BFC:

    css

    .parent {
      overflow: hidden; /* 触发BFC */
    }
    
2. 垂直居中难题

解决方案

  • Flexbox 方案

    css

    .parent {
      display: flex;
      justify-content: center; /* 水平居中 */
      align-items: center; /* 垂直居中 */
    }
    
  • Grid 方案

    css

    .parent {
      display: grid;
      place-items: center; /* 水平和垂直居中 */
    }
    
  • 绝对定位 + transform 方案

    css

    .child {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
    }
    
3. 响应式布局实现

方法

  • 使用媒体查询(Media Queries):

    css

    @media (max-width: 768px) {
      .container {
        width: 100%;
      }
    }
    
  • 使用弹性布局(Flexbox)和网格布局(Grid):

    css

    .container {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    }
    
  • 使用 viewport 单位:

    css

    .hero-title {
      font-size: 5vw; /* 相对于视口宽度的5% */
    }
    

四、性能优化考虑

  1. 减少重排(Reflow)和重绘(Repaint)

    • 批量修改 DOM 样式
    • 使用requestAnimationFrame处理动画
    • 避免频繁读取和修改布局信息
  2. 合理使用层叠上下文

    • 避免过度使用 z-index
    • 为动画元素创建独立层叠上下文
  3. 优化样式选择器

    • 避免深层嵌套选择器
    • 使用类选择器代替元素选择器组合

【精通react】(二)理解useState的实现原理---实现一个简易版的useState

在 React 中,useState 是函数式组件中管理状态的核心 Hook。它允许组件在不使用类组件的情况下,拥有可变的状态,并在状态变化时重新渲染 UI。以下是 useState 的简化实现和详细说明。


✅ useState 的核心机制

useState 的核心思想是通过 闭包 和 全局状态数组 来保存组件的状态,并在每次调用 useState 时返回当前状态和更新状态的函数。React 通过 调用顺序 来确保状态的正确性(即同一个组件中 useState 必须按相同顺序调用)。


🧩 简化版 useState 的实现(单组件场景)

以下是一个简化版的 useState 实现,适用于单个组件的状态管理:

// 状态数组和游标
let state = [];
let cursor = 0;

function useState(initialValue) {
  const currentIndex = cursor;

  // 如果当前索引超出数组长度,初始化新状态
  if (state.length <= currentIndex) {
    state.push(initialValue);
  }

  // 获取当前状态
  const currentValue = state[currentIndex];

  // 更新状态的函数
  const setState = (newValue) => {
    state[currentIndex] = newValue;
    cursor = 0; // 重置游标,确保下次渲染从头开始读取
    render();    // 触发重新渲染
  };

  // 游标递增
  cursor++;

  return [currentValue, setState];
}

// 模拟组件函数
function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("Hello");

  return (
    <div>
      <p>Count: {count}</p>
      <p>Text: {text}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setText("World")}>Change Text</button>
    </div>
  );
}

// 模拟渲染函数
function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
}

📌 实现的关键点

1. 状态数组与游标(cursor)

  • state:保存组件的状态值。
  • cursor:记录当前调用 useState 的位置。每次调用 useState 时,cursor 递增,确保状态按顺序读取。

2. 状态初始化

  • 当组件首次渲染时,state 数组为空,useState 会将初始值推入数组中。
  • 之后的渲染中,useState 会从 state 中读取对应位置的状态值。

3. 状态更新与重新渲染

  • 调用 setState 时,更新对应索引处的状态值。
  • 重置 cursor 为 0,确保下次渲染时能正确读取状态。
  • 调用 render() 重新执行组件函数,生成新的虚拟 DOM 并更新真实 DOM。

4. 组件调用顺序一致性

  • 组件中 useState 的调用顺序必须保持一致,否则会导致状态错位(例如,第一次渲染调用两次 useState,第二次渲染只调用一次,会导致状态读取错误)。
  • React 通过严格的调用顺序保证状态的一致性。

示例:运行简化版 useState


<!DOCTYPE html>
<html>
<head>
  <title>useState Demo</title>
  <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
</head>
<body>
  <div id="root"></div>

  <script>
    // 状态数组和游标
    let state = [];
    let cursor = 0;

    function useState(initialValue) {
      const currentIndex = cursor;

      if (state.length <= currentIndex) {
        state.push(initialValue);
      }

      const currentValue = state[currentIndex];

      const setState = (newValue) => {
        state[currentIndex] = newValue;
        cursor = 0;
        render();
      };

      cursor++;

      return [currentValue, setState];
    }

    function App() {
      const [count, setCount] = useState(0);
      const [text, setText] = useState("Hello");

      return React.createElement(
        "div",
        null,
        React.createElement("p", null, "Count: " + count),
        React.createElement("p", null, "Text: " + text),
        React.createElement("button", { onClick: () => setCount(count + 1) }, "Increment"),
        React.createElement("button", { onClick: () => setText("World") }, "Change Text")
      );
    }

    function render() {
      ReactDOM.render(React.createElement(App), document.getElementById("root"));
    }

    // 初始渲染
    render();
  </script>
</body>
</html>

🧠 深入理解

  • 为什么 useState 必须按顺序调用?
    因为 React 通过调用顺序来定位状态在数组中的位置,顺序不一致会导致状态错位。
  • 为什么不能在条件语句中使用 useState
    条件语句可能改变 useState 的调用顺序,导致状态错位。

页面级主题动态切换功能

less实现一套主题按钮,并可以根据需求动态切换

要使用 Less 实现一套可动态切换的主题按钮,我们可以利用 Less 的变量、混合(mixins)和嵌套功能。以下是一个实现方案:

  1. 首先,创建一个 theme.less 文件来定义主题变量:
// theme.less

// 定义主题
@themes: {
  default: {
    primary-color: #1890ff;
    secondary-color: #52c41a;
    text-color: #ffffff;
  };
  dark: {
    primary-color: #177ddc;
    secondary-color: #49aa19;
    text-color: #ffffff;
  };
  light: {
    primary-color: #40a9ff;
    secondary-color: #73d13d;
    text-color: #000000;
  };
}

// 主题混合
.theme(@rules) {
  each(@themes, {
    .theme-@{key} {
      @theme-rules: @rules();
      @{theme-rules};
    }
  });
}
  1. 然后,创建一个 button.less 文件来定义按钮样式:
// button.less

@import "theme.less";

.button {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s;

  .theme({
    background-color: @primary-color;
    color: @text-color;

    &:hover {
      background-color: lighten(@primary-color, 10%);
    }

    &.secondary {
      background-color: @secondary-color;

      &:hover {
        background-color: lighten(@secondary-color, 10%);
      }
    }
  });
}
  1. 在你的主 Less 文件中导入这些样式:
// main.less

@import "theme.less";
@import "button.less";

// 可以在这里添加其他全局样式
  1. 在 HTML 中使用这些按钮:
<body class="theme-default">
  <button class="button">Primary Button</button>
  <button class="button secondary">Secondary Button</button>
  <div>
    <button onclick="changeTheme('default')">Default Theme</button>
    <button onclick="changeTheme('dark')">Dark Theme</button>
    <button onclick="changeTheme('light')">Light Theme</button>
  </div>
</body>
  1. 添加 JavaScript 来动态切换主题:
function changeTheme(theme) {
  document.body.className = `theme-${theme}`;
}

这个方案的工作原理如下:

  • theme.less 文件定义了不同主题的颜色变量。
  • .theme() 混合在 button.less 中被调用,为每个主题生成对应的 CSS 类。
  • 按钮样式在 button.less 中定义,使用主题变量来设置颜色。
  • 通过改变 body 的类名,我们可以动态切换整个页面的主题。

要使用这个方案,你需要:

  1. 使用 Less 编译器将 Less 文件编译成 CSS。
  2. 在你的 HTML 文件中引入编译后的 CSS。
  3. 确保 JavaScript 函数 changeTheme 可以被调用(可以直接在 HTML 中内联,或在单独的 JS 文件中定义并引入)。

这种方法的优点是:

  • 易于维护:所有主题颜色都集中在一个文件中。
  • 高度可扩展:可以轻松添加新的主题或修改现有主题。
  • 性能好:所有主题的样式都在一个 CSS 文件中,切换主题只需改变一个类名,不需要加载新的样式表。

滚动太快出现白屏怎么解决

一、问题本质:渲染阻塞与资源加载延迟

白屏/卡顿的核心原因是:浏览器在短时间内无法完成视觉更新,常见场景包括:

  • 大量DOM节点动态更新导致重排/重绘耗时过长
  • 图片、字体等资源未及时加载,导致空白区域
  • 滚动事件处理函数复杂,阻塞主线程
  • 浏览器帧率(FPS)低于60,出现视觉断层

二、核心优化方案(按优先级排序)

1. 优化滚动事件处理:防抖与节流

  • 问题:滚动事件高频触发(每秒约60次),若处理函数复杂(如DOM操作、计算),会阻塞主线程。
  • 解决方案
    • 防抖(debounce):滚动停止后再执行回调,适用于非实时需求(如搜索联想)。
    • 节流(throttle):控制事件触发频率(如每秒1次),适用于需要实时反馈的场景(如滚动加载)。
// 示例:节流函数(以lodash为例)
import { throttle } from 'lodash';

// 优化前(高频触发)
window.addEventListener('scroll', () => {
  console.log('滚动中...');
  // 复杂计算或DOM操作
});

// 优化后(每秒最多执行1次)
window.addEventListener('scroll', throttle(() => {
  // 仅在节流周期内执行一次
}, 166)); // 60fps的周期约16ms,这里设为166ms(6fps)平衡性能

2. 图片与资源加载优化:懒加载与缓存

  • 问题:大量图片未加载时,容器占位不足导致白屏。
  • 解决方案
    • 懒加载(Lazy Loading)
      • 使用Intersection Observer监听元素是否进入视口,避免提前加载所有图片。
      • 现代浏览器支持loading="lazy"属性(兼容性需处理)。
    • 占位图与骨架屏
      • 图片加载前显示灰色占位或骨架屏,避免空白区域闪烁。
    • 资源缓存
      • 利用Service Worker或浏览器缓存策略,重复滚动时直接读取缓存。
// 示例:Intersection Observer实现图片懒加载
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      const realSrc = img.dataset.src;
      img.src = realSrc;
      observer.unobserve(img);
    }
  });
});

// 页面中图片使用data-src存储真实地址
document.querySelectorAll('img.lazy').forEach(img => {
  observer.observe(img);
});

3. 减少重排重绘:CSS硬件加速与分层策略

  • 问题:滚动时频繁操作DOM样式,触发浏览器重排(布局计算)和重绘(像素渲染)。
  • 解决方案
    • CSS硬件加速
      • 对滚动相关元素使用will-change: transformtransform: translateZ(0),让GPU接管渲染。
    • 分层策略(Layer Compositing)
      • 将固定元素(如导航栏)与滚动内容分层,避免互相影响。
    • 避免触发重排的操作
      • 例如,滚动时避免同时修改元素的widthheight,可先用classList批量修改样式。
/* 硬件加速示例 */
.scroll-container {
  will-change: transform;
  transform: translateZ(0);
  /* 或使用CSS过渡减少闪烁 */
  transition: transform 0.1s ease-out;
}

4. 虚拟滚动(Virtual Scrolling):仅渲染可视区域

  • 问题:长列表(如10000条数据)全部渲染导致DOM节点过多,滚动时性能极差。
  • 解决方案
    • 只渲染视口内可见的元素,通过计算偏移量模拟滚动效果。
    • 可使用成熟库:vue-virtual-scroll-list(Vue)、react-window(React)等。
// 虚拟滚动核心逻辑(简化示例)
function renderVirtualList(data, containerHeight, itemHeight) {
  const visibleStart = Math.floor(scrollTop / itemHeight);
  const visibleEnd = visibleStart + Math.ceil(containerHeight / itemHeight) + 1;
  const visibleData = data.slice(visibleStart, visibleEnd);
  
  // 渲染visibleData,并设置容器偏移
  container.style.transform = `translateY(${scrollTop}px)`;
}

5. 优化JavaScript执行:避免主线程阻塞

  • 问题:滚动事件中执行复杂计算(如大数据处理、大量API请求),导致页面无响应。
  • 解决方案
    • Web Worker:将耗时任务(如数据处理)转移到后台线程。
    • requestAnimationFrame:将滚动更新任务与浏览器渲染周期同步,避免丢帧。
// 示例:使用requestAnimationFrame优化滚动动画
let isScrolling = false;
window.addEventListener('scroll', () => {
  if (!isScrolling) {
    isScrolling = true;
    requestAnimationFrame(() => {
      // 执行滚动相关更新(如动画、位置计算)
      isScrolling = false;
    });
  }
});

三、进阶方案:性能监控与降级策略

  1. 性能监控
    • 使用PerformanceObserverrequestAnimationFrame监测帧率(FPS),定位卡顿帧。
  2. 降级策略
    • 检测到低性能设备时,自动关闭动画、减少渲染精度,保证基本功能可用。
  3. 代码分割与异步加载
    • 滚动到特定区域时再加载相关模块(如Chart.js图表),避免初始加载过重。

四、总结

“滚动白屏的核心是渲染性能不足,优化需从‘减少渲染负担’和‘资源按需加载’入手:

  1. 防抖/节流控制滚动事件频率,避免主线程阻塞;
  2. 图片用懒加载+占位图,资源用缓存策略减少重复请求;
  3. 利用CSS硬件加速虚拟滚动,将渲染压力转移到GPU或减少DOM节点;
  4. 最后通过性能监控定位瓶颈,配合降级策略保证不同设备的体验一致性。”

Vue3中的watch和wactEffect有什么区别,分别适用在什么场景?

在 Vue3 中,watchwatchEffect是用于响应式监听数据变化的两个 API,它们的主要区别和适用场景如下:

核心区别

  1. 触发时机

    • watch:只监听明确指定的数据源,且只有当这些数据源变化时才会触发回调。
    • watchEffect:会自动追踪其内部使用的所有响应式依赖,并在初始执行时立即触发一次,之后依赖变化时再次触发。
  2. 依赖声明方式

    • watch:需要显式指定要监听的数据源,可以是一个或多个响应式引用、计算属性等。
    • watchEffect:不需要显式指定依赖,自动捕获回调函数中使用的所有响应式数据。
  3. 回调参数

    • watch:回调函数接收三个参数:新值、旧值和可选的onCleanup函数(用于清理副作用)。
    • watchEffect:回调函数只接收一个onCleanup函数,不提供新旧值对比。

适用场景

  1. watch 的适用场景

    • 需要访问变化前后的值(例如计算差值、记录变更历史)。
    • 监听特定数据的变化(例如表单输入、路由参数)。
    • 需要延迟或异步执行副作用(例如 API 请求)。
    • 需要在组件销毁时手动清理副作用(例如定时器、WebSocket 连接)。
  2. watchEffect 的适用场景

    • 需要根据多个响应式依赖自动触发副作用(例如自动保存表单数据)。
    • 副作用不需要访问旧值(例如更新 DOM、同步本地存储)。
    • 简化依赖追踪(无需显式列出所有依赖)。

示例对比

以下是一个简单的示例,展示两者的差异:

import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const double = computed(() => count.value * 2)

// watch示例:监听count变化
watch(count, (newValue, oldValue) => {
  console.log(`count从${oldValue}变为${newValue}`)
  // 可以访问新旧值进行比较
})

// watchEffect示例:自动追踪所有依赖
watchEffect(() => {
  console.log(`count或double变化了:${count.value}, ${double.value}`)
  // 不需要显式声明依赖,只要内部使用的响应式数据变化就会触发
})

// 修改count值
count.value++ // 同时触发watch和watchEffect

总结

  • 使用 watch:当需要精确控制监听的数据源,并需要访问变化前后的值时。
  • 使用 watchEffect:当需要自动响应所有依赖的变化,且不需要新旧值对比时。

最后,以官方文档清晰的说明来结尾:

image.png

选择合适的 API 可以使代码更清晰、更高效,避免不必要的副作用触发。

🌐 深入理解端口:从基础概念到冲突解决

作为一前端开发工程师,了解网络通信中的端口是构建高效、稳定的Web应用的关键之一。无论是前后端交互还是与第三方服务通信,端口都扮演着至关重要的角色。本文将详细介绍什么是端口、常见的服务端口号、如何解决端口冲突以及避免冲突的最佳实践。

一、什么是端口?

在网络通信中,端口是一个抽象的概念,它用于标识特定进程或服务的逻辑地址。每个运行在网络上的应用程序都需要一个唯一的端口号来区分其他服务,以便正确地接收和发送数据包。端口号范围是从0到65535,其中:

  • 0-1023:系统或常用服务保留端口(如HTTP使用80端口,HTTPS使用443端口)。
  • 1024-49151:注册端口,可以被用户程序动态分配。
  • 49152-65535:动态或私有端口,通常用于临时连接。

端口的工作原理

当客户端发起请求时,操作系统会根据目标IP地址和端口号确定要访问的服务。例如,当你在浏览器中输入http://example.com,实际上是在向example.com服务器的80端口发起HTTP请求。服务器接收到请求后,通过监听该端口的应用程序处理请求并返回响应。

二、常见服务端口号

以下是一些常用的网络服务及其默认端口号:

端口号 对应服务 描述
80 HTTP 超文本传输协议,默认网页浏览端口
443 HTTPS 安全超文本传输协议,加密网页浏览
21 FTP 文件传输协议控制连接
22 SSH 安全外壳协议,远程登录
25 SMTP 简单邮件传输协议
53 DNS 域名系统,负责域名解析
8080 HTTP 备用端口 常用于测试环境下的HTTP服务
3306 MySQL 关系型数据库管理系统MySQL
6379 Redis 内存中的键值存储系统
27017 MongoDB 面向文档的NoSQL数据库

三、端口冲突的原因及解决方案

1. 端口冲突的原因

端口冲突通常是由于多个应用程序试图绑定到同一台机器上的相同端口而引起的。这在开发环境中尤为常见,尤其是在本地调试多个项目或服务时。例如,如果你同时启动了两个不同的Web服务器,它们都试图监听8080端口,那么其中一个将会失败。

2. 解决端口冲突的方法

a. 修改配置文件

对于大多数服务来说,可以通过修改其配置文件来更改监听端口。比如,如果你发现MySQL无法启动,可能是由于3306端口已被占用。这时可以在MySQL配置文件中指定一个新的端口:

[mysqld]
port=3307

然后重启MySQL服务,并更新所有连接到该数据库的应用程序的端口号设置。

b. 使用端口转发(Port Forwarding)

什么是端口转发?

想象一下:你家里只有一根网线插在路由器上,但你有好几台设备(比如电脑、手机、智能电视)都想上网。这时候路由器就会使用“端口转发”技术,把来自互联网的数据包正确地转给对应的设备。

在开发中,端口转发的意思是:你可以让一个端口上的请求自动被“转送”到另一个端口或另一台机器上。这样做的好处是,你可以:

  • 在同一台机器上运行多个服务,而它们都对外“看起来”像是运行在同一个端口。
  • 测试不同版本的服务而不改动客户端配置。
  • 把本地开发环境暴露给外网进行调试(例如内网穿透)。
实际应用场景举例
场景1:你在本机同时运行两个Web服务
  • A项目用的是 8080 端口
  • B项目也想用 8080 端口,但已经被占用了

这时你可以设置:

  • 让外部访问 8080 的请求转发到 8081
  • 访问 8080 就等于访问 8081
场景2:你想让别人访问你本地开发的服务
  • 你在本地运行了一个 Node.js 应用,监听 localhost:3000
  • 别人无法直接访问你电脑的 3000 端口
  • 你可以设置端口转发,将你的 4000 端口转发到 3000,然后告诉别人访问你的公网IP+4000端口

🔧 不同系统的操作方式
操作系统 命令/工具示例
Linux 使用 iptables 或 socat 工具实现转发
macOS 使用 pfctl 或 launchd 配置转发规则
Windows 使用 netsh interface ipv4 add route ... 或第三方工具如 netsh portproxy

⚠️ 注意:这些命令对新手有一定门槛,建议先从图形化工具(如 ngrok、localtunnel)入手体验端口转发的效果。


c. 动态端口分配(Dynamic Port Assignment)

什么是动态端口分配?

简单来说,就是不指定具体的端口号,让操作系统帮你选择一个当前未被占用的端口。

这在开发阶段特别有用,因为:

  • 你不需要关心哪个端口被占用了
  • 可以快速启动多个服务实例
  • 减少手动配置错误
✅ 实际应用场景举例
Node.js + Express 示例:
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello from dynamic port!');
});

const server = app.listen(0, () => {
  const port = server.address().port;
  console.log(`服务已启动,正在监听端口 ${port}`);
});

在这个例子中,我们传入了 0 作为端口号,表示让系统自动分配一个可用端口。运行后输出可能是:

服务已启动,正在监听端口 59347

每次运行程序时,系统都会给你一个不同的、未被占用的端口。

🔍 为什么这个功能很有用?
  • 多个开发人员可以并行开发,不会互相干扰。
  • 自动化测试脚本可以安全地创建多个独立的服务实例。
  • 避免因端口冲突导致程序崩溃。

四、如何避免端口冲突

为了避免端口冲突带来的麻烦,以下是几个实用的建议:


1. 规划端口使用

🤔 它到底是什么?

就像你在公司里安排工位一样:每个员工都有自己的座位编号,不能两个人坐同一个位置。在系统中,我们也要给不同的服务(比如数据库、Web服务器)分配“座位”——也就是端口号。

✅ 举个生活中的类比:

想象你开了一家奶茶店:

  • 前台接待员负责接单 → 端口 8080
  • 后厨做奶茶的师傅 → 端口 8081
  • 财务人员收钱 → 端口 8082

大家各司其职,互不干扰。

如果两个岗位用了同一个端口,就会“撞车”,程序就运行不了了。

💡 所以我们要提前规划好:

  • 哪些服务用哪些端口?
  • 避免多个服务争抢同一个“座位”
  • 把这些信息记录下来,方便以后查(比如写一个文档)

2. 监控端口状态

🤔 它又是什么?

就是定期检查一下,看看谁在用你的“座位”,有没有人偷偷坐在别人的位置上(非法占用),或者某个座位明明没人却一直空着。

✅ 举个生活中的例子:

就像每天早上保安巡逻办公室,看看有没有陌生人进来,或者有没有人乱占别人的位置。

🔧 不同系统下的命令:

操作系统 查看当前占用端口的方法
Linux netstat -tuln 或 ss -lntu
Mac `netstat -an
Windows netstat -ano

你可以把这些命令想象成“监控摄像头”,用来查看哪个程序正在用哪个端口。


3. 利用容器化技术(比如 Docker)

🤔 它到底是什么?为什么能防止冲突?

继续用奶茶店的例子:

假设你有两个朋友也开了同样的奶茶店,你们三个都想在同一栋楼里办公。但是楼层有限,工位不够,怎么办?

解决办法是:每人建一个“透明的小房子”,里面有自己的桌子、椅子、设备,虽然都在同一栋楼里,但彼此之间互不干扰。

这个“小房子”就是 Docker 容器

✅ 实际应用场景举例:

你有两个 Web 项目都希望使用 8080 端口:

  • A项目运行在 Docker 容器里,内部监听 8080
  • B项目也运行在另一个容器里,也监听 8080

只要对外映射到不同的端口,比如:

  • A项目暴露为 localhost:8081
  • B项目暴露为 localhost:8082

就不会冲突!


4. 自动化端口检测

🤔 它又是什么?有什么用?

想象你去餐厅吃饭,服务员先看看哪张桌子空着,然后带你去那张桌子。这就是“自动找空位”。

自动化端口检测就是让程序自己去找一个没被占用的端口来运行,避免手动设置出错。

✅ 举个代码例子(Node.js):

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('你好!我正在使用一个自动分配的端口!');
});

// listen(0) 表示让操作系统自动选一个空闲端口
const server = app.listen(0, () => {
  const port = server.address().port;
  console.log(`服务已启动,正在监听端口:${port}`);
});

输出可能是:

服务已启动,正在监听端口:59347

下次运行可能变成另一个数字,比如 61234,完全由系统决定。

🛠️ 更进一步:在 CI/CD 中自动检测(如 Jenkins、GitHub Actions)

你也可以写一个脚本来自动找空闲端口,例如在 Shell 脚本中:

PORT=$(python -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1])')
echo "找到一个空闲端口:$PORT"
npm start -- --port $PORT

这样每次部署都能自动找一个不冲突的端口运行服务,非常适合测试环境、多实例部署等场景。


🎯 总结一句话:

  • 规划端口使用:就像安排座位,提前定好谁坐哪。
  • 监控端口状态:像保安巡逻,确保没人乱占。
  • 利用容器化技术:像给每个人建了个独立房间,即使他们坐一样的“座位”也不冲突。
  • 自动化端口检测:像聪明的服务员,自动找空位,省心省力。

搜索数据建设系列之数据架构重构

导读

主要概述百度搜索业务数据建设的创新实践,重点围绕宽表模型设计、计算引擎优化和新一代业务服务交付模式(图灵3.0开发模式)三大方向,解决了传统数仓在搜索场景下面临的诸多挑战,实现了搜索数据建设的高效、稳定、低成本;为百度搜索业务敏捷迭代奠定夯实基础。

名词解释

TDS(Turing Data Studio): 是基于图灵(百度内部数据分析平台)的数据建设解决方案,提供 数据开发、数仓管理、监控运维、资源管理等一站式服务的数据开发平台。详情参见:百度MEG数据开发治理平台-TDS

TDA(Turing Data Analysis):是一款可视化BI产品,旨在帮助用户轻松上手及使用,进行拖拽式可视化数据分析及仪表盘建设。产品模块包括仪表盘、数据集、可视化分析及智能分析模块。详情参见:百度一站式数据自助分析平台(TDA)建设

TDE(Turing Data Engine ):是基于图灵生态的计算引擎,包含Spark计算引擎和ClickHouse。详情参见:揭秘百度数仓融合计算引擎ClickHouse在百度MEG数据中台的落地和优化

UPI(Udw-API):百度内部编程访问接口;以Map/Reduce计算框架为主,适用于计算逻辑复杂,以及多种数据源混合计算的例行离线数据挖掘业务

数仓融合计算引擎:是百度自主研发,基于spark自研的adhoc服务。提供数据查询分析,具有简单易用、超大规模支持、成本极低等特点,能实现T级数据秒级查询,也适用于例行生产的 ETL 场景。

函谷:图灵核心模块,作为图灵查询的gateway,完成图灵查询的接收、分发、提交执行、查询进展轮询、结果获取等一系列功能。

01 背景与问题

1.1 背景

在当今互联网产品发展日新月异、业务迭代迅猛的时代;跨业务分析的需求日益增长,这种变化既为企业创造了敏捷决策、精准运营的新机遇,也带来数据割裂、价值释放滞后等严峻挑战。特别是大型互联网企业,往往构建有复杂的多业务、多模块、多线条体系,每日持续产出海量的数据信息。这些数据的服务对象正逐步从数据研发人员扩展至更为广泛的产品相关人员,如何高效开展数据获取工作,打破数据孤岛现象,充分挖掘并释放数据驱动业务的潜力,已成为业界广泛关注和讨论的焦点议题。针对该问题,业界传统数仓常采用的是经典分层模型的数仓架构,从ODS(Operational Data Store)>DWD(Data Warehouse Detail)>DWS(Data Warehouse Summary)>ADS(Application Data Store)逐层建模,但我们会发现,从传统固化开发的角度来看,传统经典数仓模型是比较有优势的。然而,面对当下数据需求灵活多变的时代,其局限性也日益凸显。如下图

图片

1.2 搜索场景下的困境与挑战

搜索作为百度的核心支柱业务,涵盖通用搜索、智能搜索、阿拉丁与垂类等多元化、多模态的搜索产品,具有快速迭代、模块多元化且复杂的特性,搜索数据更是复杂多样,整体数仓规模达到数百PB以上。

随着搜索业务各个模块之间的联系日益紧密,交叉分析的需求也在不断增长。使用人员对数据获取的便捷性提出了更高的要求,其中涵盖了数据分析师、策略、业务产品经理、运营、评估等多类角色。他们的诉求期望能够跨越复杂的数据架构壁垒,以更加高效、直观、快速的方式获取到所需数据。

而传统的搜索数仓建设体系 无论从建模角度还是技术框架上,都与现阶段用户诉求背道而驰。

  1. 建模角度:多层的传统分层建模。往往会出现(大表数据量大、查询慢、存储冗余、口径不统一)等问题,影响业务分析效率,从而达不到数据驱动业务的效果。数据开发侧作为需求的被动承接方,根据业务侧提出的数据需求进行数据开发与交付,存在需求交付周期长、人力成本高等问题。

  2. 技术框架角度:搜索数仓过去大多是采用UPI框架(以C++ MR计算框架为主)进行ETL处理。由于该框架技术陈旧,往往会出现以下问题影响数仓整体时效、稳定。从而使业务部门感知需求支持迟缓、数据产出延迟及数据质量低等一系列问题。

  • 容易出现服务不稳定。

  • 处理能力薄弱:处理不了特殊字符,从而导致数据丢失或任务失败等。

  • 只能通过物理机远程执行的方式提交,有单节点风险。

  • 无法Writer将数据写到Parquet文件,需要进行特定脚本ETLServer框架进行转换。

思考:如何更好的满足用户角色需求,进一步降低数据获取的使用门槛?

破局:拥抱变化,以用户诉求为核心出发点。 探索更适合用户的 体系化建模 来进行实质、有效的数据管理。

**体系化建模:**以业务产品需求驱动模型设计,以模型设计驱动和约束开发实施,防止因模型设计与业务产品割裂、开发实施缺少约束带来的无序、“烟囱式”开发。在机制上形成模型设计与开发实施的有效协同。

切入点:以规范“基础数据建设”,消除因“烟囱式”开发给业务带来的困扰和技术上的浪费。

由此我们探索出一套新的建模体系:大宽表+数据集:其核心点就是基于宽表 将之前的"需求-交付"解耦为以数据集为中心的建设,从而提升搜索内业务数据分析效率与分析深度,更好助力业务决策。以下将从宽表建模、计算引擎架构优化、新型业务交付模式等方向为大家介绍搜索数据团队业务实践。

02 搜索建模思路与技术方案

2.1 建模模型

2.1.1 思路

基于搜索产品功能特性与差异化业务场景,我们对日志数据进行主题化的分类。在每个主题域内,结合业务运营的具体环节特征,构建具备高整合度的宽表模型。在模型构建过程中,保持 ODS(操作数据存储)层与 DWD(明细数据层)的表结构粒度一致,确保数据的一致性与连贯性。所构建的宽表不仅完整涵盖下游业务所需的全部字段,包括业务明细表中的基础数据,还整合了各层级的维度属性与计算指标。通过这种方式,形成一个全面、统一的数据底座,为上层业务的多维分析、指标监控及决策支持提供坚实的数据支撑,有效满足多样化的业务分析需求。

2.1.1.1 举例

以展点主题为例,从历史的模型表粒度和模型层级来分析:ODS与DWD、DWS表行为、检索、点击各个主题在同层模型或者跨模型之间都存在字段、口径的冗余,如下图

图片

2.1.1.2 思路分析过程

核心思想过程:展点主题下明确粒度,丰富维度&指标 建设宽表模型。

将展点主题下各层之间的事实表复杂嵌套字段打平后与各个维度表、指标等进行join生成宽表,宽表的列最终分为公共属性、展点行为属性、业务属性和指标属性。

消除:

  • 数仓层间:字段存储冗余问题

  • 数仓层内:口径不一致问题

图片

图片

图片

2.1.2 建模核心思想

基于思路分析过程,总结出一套核心建模理论,核心思想如下图

图片

构建搜索系统性数据建模:根据产品功能和业务不同,按照不同主题构建宽表。从而达到节约存储、精简表数量、口径更清晰的目标。

2.1.3 整体模型架构

基于核心建模思想理论得到整体的模型架构,如下图

图片

  • 采用Parquet列式存储,可支持宽表数百、千列,超多字段,再经过按列的高效压缩(bucket sort后,压缩率更高),降低了数仓整体存储空间,提高了IO效率,起到了降低上层应用延迟的效果。

  • 将各层之间的表复杂嵌套字段打平后与各个维度表、指标等进行join生成百列宽表,宽表的列最终分为公共属性、业务维度属性和指标属性,便于业务分析,实现快速迭代。

2.2 计算引擎

为了保证数据生产稳定、准确性。我们对计算引擎的选择做了升级,采用传统Spark结合数仓融合计算引擎对搜索数仓ETL进行重构。

图片

2.2.1 从架构&处理流程上

  • C++ MR :多进程,每个任务独立运行,必须经过Map-Shuffle-Reduce,然后中间结果写磁盘。

  • Spark :多线程,任务在Executor内以线程执行。基于DAG,可以在内存中缓存数据,减少IO。

Spark 框架 相较于 MR框架优势在于

  • 基于内存计算,处理速度快。

  • 支持多种计算模式,功能丰富,适合迭代处理数据。

  • 提供了高级的 API,开发效率高。

  • 基于平台提交,有效避免单节点计算风险。

且在有shuffle情况下计算表现更好(MR在Shuffle时默认进行排序,spark对shuffle有优化,只有在部分场景才需要排序),在具体业务实践中:同耗时的情况下,Spark计算资源相较于MR节省20%左右。

2.2.2 ETLServer到数仓融合引擎转变

图片

各主题宽表模型的计算通过数仓融合计算引擎(通过Spark Application Context常驻方式做到资源有效复用;省去了启动Driver的时间实现任务的快速启动,来提升任务执行时间)可直接Writer将数据写到Parquet文件,文件无需进行多次脚本server转换。

在具体业务实践中 各主题计算耗时由之前40min 缩短至 10min(减少了30min),实现数仓快速产出。

2.3 新数据模型及架构下的挑战与解决方案

任何数仓模型架构不会存在一个绝对完美的、涵盖所有方面的解决方案。宽表设计仅是当前环境数仓模型的最优解,它依然面临着诸多不容忽视的挑战。

图片

2.3.1 挑战1解决方案

  1. 列式存储&读取:

宽表采用了Parquet列式存储,以及ZSTD高效压缩算法。结合数仓融合引擎,达到Data Skipping(即读的越少、计算越快)的效果,仅需读取查询涉及的分区及列,减少了磁盘 I/O 和内存传输的数据量来提升查询效率,通过Sql分析服务发现热点复杂字段,主动引导业务建设物化列,命中后查询性能提升80%。

  1. 复杂嵌套字段打平

业务常用核心指标以及高频字段口径下沉宽表。虽然行数变多了,但是避免了explode,get_json_object、array、map等复杂字段获取的耗时操作,查询性能相较于之前提升了2.1倍。

2.3.2 挑战2解决方案

搜索数据升级到了湖仓一体架构,借助Iceberg Merge Into功能,实现高效回溯方式:对表数据进行行级别的更新或删除。相比insert overwrite 操作更加高效,减少了不必要的数据移动和存储浪费。

通过单一原子操作实现了复杂的数据整合需求。相比传统的先删除再插入的方式,Merge Into提供了更好的性能和一致性保证,其原理是通过重写包含需要删除和更新行数据所在的date files。Merge Into可以使用一个查询结果数据来更新目标表的数据,其语法类似join关联方式,根据指定的匹配条件对匹配的行数据进行相应的操作

Merge Into基本语法

图片

回溯原理流程如下图

图片

1. 关联匹配

  • 目标表和源表根据指定key进行join操作。

2. 条件判断

  • 若Key匹配:根据源表操作类型,对目标表中的记录执行相应的操作(更新或删除)。

  • 若Key不匹配:执行Insert操作,将源表数据插入目标表。

3. 原子性操作

  • 整个流程是事务性的,确保数据一致性。

以下是特定回溯场景下 hive与iceberg不同方式的回溯耗时对比,可以看的出来用merge into代替insert overwrite进行回溯,回溯更新效率整体可提高54%左右。

图片

2.3.3 挑战3解决方案

2.3.3.1 重排序、高效压缩

开发ATO优化器(通过任务依次执行重排序、压缩等一系列Rules,实现分区优化和数据重分布),高效率压缩,解决存储成本,存储节约20%。

图片

(1)压缩编码

数仓表字段元信息采集:通过任务对图灵宽表表进行字段元信息采集,分析数据分布情况,获取重排序字段。

具体做法:通过RLE、Delta等缩码方式来提升数据压缩效率;数据重复度越高、连续性越好(有序)的场景,压缩效率会越高,RLE、Delta编码原理如下。

图片

(2) 压缩格式

使用ZSTD压缩格式和更大的压缩level,在不影响查询性能的情况下,更大的压缩level能进一步提高压缩率,level=9时在压缩效率和耗时上最为平衡,读写耗时和压缩率对比效果如下。

图片

(3) Page Size

针对Parquet文件格式特性进行深入挖掘 ,对Parquet page size进行分页扩容,将Page Size从1MB增大至5MB,让更多相似的数据集中到同一个数据页中,充分利用编码的压缩特性,进一步减少各个数据页之间存在的相似数据。在ZSTD的基础上,能进一步提升压缩效果,效果如下

图片

2.3.3.2 历史裁剪,管理有效字段

开发了一套半自动化的通用裁剪模式,通过采集日常任务代码,sql parser模块解析出无用字段信息(尤其是大json 大map类型扩展字段的无用字段)自动化实现了裁剪。减少了 50% 的回溯任务计算资源消耗,将人力投入从5人/天降低到0.5人/天。

图片

  • 字段频率统计模块:通过对函谷 SQL 数据库和 TDS 平台 No SQL 任务的物理执行计划进行解析,实现对宽表 SQL 任务和非 SQL 任务的字段访问频率的自动化统计。

  • 裁剪字段抽取模块:基于字段访问频率,每月抽取冷温字段,形成可视化的字段访问频率报表,生成裁剪 SQL。

  • **冷温字段告警模块:**通过对比前一个月和本月冷温字段列表,生成当月新增冷温字段列表,然后向产品研发团队和数据RD团队发出告警,确认需要动态调整的裁剪字段;引入冷温字段告警模块,成功实现了裁剪字段的动态调整。最后,通过滚动裁剪模块自动裁剪395天前的数据,进一步降低人力/资源的消耗。

  • 滚动裁剪模块:自动化滚动裁剪,裁剪宽表中395天前的数据。

基于业务实践证明:宽表数仓模型数仓融合计算引擎的结合 比传统数仓模型 更适合 面向服务于快速迭代的驱动型业务,主要体现在

1. 查询性能巨大提升带来快速响应支持业务需求:

简单查询场景 :Adhoc查询场景,耗时在数十秒级别,相比于普通Spark性能提升5倍。

复杂场景:业务常用复杂字段拆分打平,避免数组、map等复杂字段耗时操作、查询性能提升2.1倍。

2.口径封装下沉:封装业务核心口径,解决业务长期数据源多、口径不一致带来的数据准确性问题,省去不必要的沟通,使用更加简便。

3.减少冗余存储:相较于经典传统数仓同主题模型下存储降低30%左右。

03 基于建模与技术框架初步整合 探讨图灵3.0生态新一代业务服务交付模式

随着搜索数仓模型&计算引擎架构的重构和技术栈统一,搜索数仓定义逐步清晰化、数仓个数大幅度降低,整体趋向更加紧凑、高效以及收敛的态势。在此基础上,为了助力数据迭代效率和分析效率进一步提升,在业务线基础数仓及应用层数据建设上,百度MEG内部开发了图灵3.0生态系统(即 数仓合理建设,数据分析需求尽可能收敛到TDA平台,配套数据集建设完善),包括Turing Data Engine(TDE)计算引擎、Turing Data Studio(TDS)数据开发治理平台和Turing Data Analysis(TDA)可视化BI产品。依托图灵3.0生态,我们进而形成了一套新的开发范式—— 图灵3.0新开发模式,用来提升搜索内业务数据分析效率与分析深度,如下图(阶段三)所示

图片

3.1 阶段一到阶段二

如之前所述:由于搜索数仓早期查询性能不理想,为了提升业务分析效率建设了大量的业务表。从而导致数据冗余、数据链路稳定性差、效率低、指标口径不一致等一系列问题。搜索数据团队通过数仓模型(将多层数据模型控制在1-2层)以及计算引擎架构升级重构、湖仓一体、高效压缩、裁剪等一系列措施解决了这些问题。数据建设更加完善规范化,搜索数仓表的数量由过去的数百张减少至20张左右,时效性大幅提升,全数据链路全流程提速4H+,数据稳定性及运维成本降低30%。

3.2 阶段二到阶段三

随着图灵3.0生态系统(包括TDA、TDS、TDE)及搜索数仓模型的日益完善,内部提出了 以数据集为核心来构建数据应用层,将数据开发侧与业务侧的依赖关系从之前的"需求-交付"解耦为以数据集为中心的建设,实现数据集<->可视化分析<->仪表盘的数据分析闭环,解决业务常用维度、指标长周期的查询分析需求 ——> 图灵3.0新开发模式。

图灵3.0新开发模式核心思想在于数据集的建设,我们将不再仅仅只是根据业务需求来定制开发数据报表,而是构建一个灵活、可扩展的数据集。使业务侧能够自主地根据需求从中提取、分析和可视化数据,以满足不断变化的业务需求。

那么,在数据集建模实践中,如何才能合理构建一个灵活、可扩展且高质量的数据集?是数据研发对数据集建模关键核心,也是最大的挑战。

3.2.1 数据集建模合理度挑战

1. 为了满足业务需求的多样性与广泛性,并支持更多的维度和指标。我们往往会倾向于在单个数据集中不断叠加新的维度和指标,这种做法虽然表面上看起来方便快捷,但实际上却导致了数据集行数的急剧增加,进而对聚合查询的性能造成了不利影响

  1. 为了确保查询的高效性,同时兼顾更多维度与指标的业务需求。我们往往的做法 就是建立更多的数据集,以空间换时间去满足查询性能。

显然,这些做法之间存在着明显的矛盾,具体如下图。

图片

3.2.2 解决方案

为了更好地找到平衡点,搜索数据团队采取了以下解决措施:

  1. 明确边界:分主题建设对应数据集,单主题内 数据集尽量做到合并统一,以达到更高的集成度与一致性。

  2. 明确粒度:从业务场景需求出发,单主题内数据集建设前明确数据集最小粒度 ,确保数据最小粒度既能满足主题分析的精度要求,又避免因过度细化或粗放导致的分析效能损耗,为后续数据集的结构化构建与高效奠定基础。

  3. 深度性能优化:充分利用了TDE-ClickHouse强大基础引擎,例如在处理高基数去重计数字段时 创新性地采用NoMerge技术来替代传统的COUNT(DISTINCT)方法,降低了聚合层的计算负担,实现了查询性能5至10倍的提升,极大地优化了数据处理速度。

3.3 新模式带来的改变

图片

△ 图灵3.0的数据开发新模式

  1. 强化主动能力,业务自助效率显著提升:相较于以往被动式的一对一需求定制化开发模式,数据研发工作已从单纯响应被动需求转变为主动规划构建数据集。图灵3.0新开发模式下,实现数据集<->可视化分析<->仪表盘的数据分析闭环(满足 90%查询;其余10%长尾交给Adhoc查询),业务人员对日常通用需求的分析工作转移到数据集自助查询与分析上(根据数据集自助创建可视化数据报表)。可视化分析占比、业务自助率提高至90%,数据研发日常需求量减少80%。

  2. 非核心常用维度指标查询性能显著提升:非核心常用维度指标由以往业务提需 查表或单独建设报表来获取数据的方式 转变为通过数据集自助下钻、拖拉拽自由组合常用维度指标,实现可视化分析的方式。借助TDE-ClickHouse 强大基础引擎能力:可视化分析效率大幅提升,从小时、分钟级的数据分析效率 提升至秒级分析。单次查询数据周期由1周内 提升至1年内(秒级完成查询,真正做到即需即查即用。

  3. 血缘管理规范化,运维效率显著提升:数据血缘更加完整流程化,数仓-数据集 血缘在TDS完成闭环,数据集内字段血缘在TDA完成闭环,以数据集为纽带串联整个数据流全过程,数据链路运维效率提升2-3倍

目前,该模式已经广泛应用于搜索各业务数据运营人员早报、周报等多种业务汇报场景。得益于该模式,搜索产品线下仪表盘周均查询(PV)高达1.7W次左右,可视化分析周均0.93W次左右 ,每周超过400多名用户参与TDA搜索数据分析工作更重要的是,需求的交付周期实现了显著缩短,由以往的单/双周缩短至按天交付;甚至在某些情况下,业务人员能够直接自助获取所需数据。在处理重点项目时,该模式也能确保业务团队在第一时间获取到P0级别的关键数据。这种方式的转变不仅能够减轻数据开发团队的工作负担——人力成本由原先的3人锐减至1人,还能提高业务侧的数据使用效率和自主性,使得团队得以从繁琐的“取数”与“跑数”任务中解放出来,将更多的精力投入到数仓模型的优化、技术框架的探索与治理等更具战略价值的工作中去。

04 总结与展望

数据建模领域正经历从“技术驱动”向“价值驱动”的深刻转型,更加强调的是敏捷性、可解释性和业务对齐。尽管当前的技术工具愈发强大,但成功的关键依旧在于跟业务的紧密协作与一个清晰明确的治理框架。

搜索业务,作为百度内部最核心且最为复杂的板块,涵盖了多个至关重要的产品线。近年来,搜索数据团队始终致力于运用前沿技术来不断优化和完善数仓体系的建设,以坚实的基础数仓架构支撑起数据质量飞跃提升,从而高效赋能业务,带来可量化、可感知的业务成效与用户体验升级。

展望未来,随着AI代理和边缘计算等技术的蓬勃发展,数据建模有望朝着自适应与嵌入式方向进一步进化。搜索数据侧还将在以下关键方向与大家进行深入探讨、交流和学习:

  • 通用数据流解决方案:构建事件规则引擎等通用数据流处理工具,简化数据处理流程,提高数据处理效率与灵活性。

  • 日志埋点技术(含无埋点):探索高效的自动化埋点机制,提升数据采集的全面性与准确性,为业务洞察提供坚实的数据基础。

  • 宽表模型框架抽象层:探索更为高效、灵活的模型统一抽象方法层。

  • AI大模型时代下的高效开发模式:探索如何通过利用大模型技术 来优化代码质量、数据链路等,打造更加高效、可靠的数据开发与运维体系。

我们期待之后再次与大家见面探讨这些议题,共同推动数据领域的创新与发展。

vite和webpack打包lib库细节

概述

常见的组件库,业务工程项目,都会用到各式各样的npm包,打包的格式也很多元,比如umd,cjs,es等,不同打包格式,适合不同环境不同导入方式,以下是关于现在主流webpack和vite这两个环境打包的配置信息,如果自己要写npm包给别人用,打包配置必不可少。

webpack配置

webpack.config.js配置如下, 依赖如下:

  • webpack
  • webpack-cli
  • vue-loader
  • clean-webpack-plugin
  • ts-loader
  • style-loader
  • css-loader
  • postcss-loader
  • sass-loader/less-loader(根据情况定)

如下配置会将所有资源输出到一份文件中,如果需要将资源分块输出,可以参考webpack分解到不同文件配置

const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');

//注意:wenbapack单入口输出多文件,需要数组形式,CleanWebpackPlugin关闭,不然会覆盖其他生成的文件
const baseConfig = {
  mode: 'production',
  entry: path.resolve(__dirname, './src/index.js'),
  resolve: {
    extensions: ['.ts', '.js', '.vue', '.json'],
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          reactivityTransform: true // 可选:启用 Vue 3 响应性语法糖
        }
      },
      {
        test: /\.ts$/,
        loader: 'ts-loader',
        options: {
          appendTsSuffixTo: [/\.vue$/],
          transpileOnly: true,
          // 使用项目中的 tsconfig.json
          configFile: path.resolve(__dirname, 'tsconfig.json')
        },
        exclude: /node_modules/
      },
      {
        test: /\.css$/,
        use: [
         'style-loader',
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.scss$/,
        use: [
         'style-loader',
          'css-loader',
          'postcss-loader',
          'sass-loader'
        ]
      },
      {
        test: /\.(png|jpe?g|gif|svg|webp)$/,
        type: 'asset/inline',
      },
      {
        test: /\.(woff2?|eot|ttf|otf)$/,
        type: 'asset/inline',
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    // new CleanWebpackPlugin(),
  ],
  optimization: {
    minimize: true,
    splitChunks: false // 禁用代码分割,因为我们打包的是库
  },
  performance: {
    hints: false,
    maxEntrypointSize: 512000,
    maxAssetSize: 512000
  }
};
module.exports = (env, argv) => {
  return [
    {
      ...baseConfig,
      output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'index.umd.js',
        library: {
          name: ['myNamespace','MyComponent'],
          type: 'umd',
        },
        umdNamedDefine: false,
  
      },
    },
    {
      ...baseConfig,
      experiments: {
        outputModule: true // 启用实验性 ESM 输出支持
          },
      output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'index.es.js',
  
        library: {
          type: 'module',
        },
        umdNamedDefine: false,
  
      },
    },
    {
      ...baseConfig,
      output: {
        path: path.resolve(__dirname, './dist'),
        filename: 'index.cjs',
        library: {
          name: ['myNamespace','MyComponent'],
          type: 'commonjs',
        },
        umdNamedDefine: false,
  
      },
    },
  ]
};

vite配置

vite.config.js 依赖如下:

  • vite-plugin-css-injected-by-js(将所有资源内聚到js中)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
// https://vite.dev/config/
export default defineConfig({
 plugins: [vue(),
   cssInjectedByJsPlugin() // 强制 CSS 内联到 JS
],
 build: {
   minify: true, 
   emptyOutDir: true ,
   cssCodeSplit: false, //禁止代码分割  
   assetsInlineLimit: 100 * 1024 * 1024, // 100MB
   lib: {
     entry: './src/index.js', // 组件入口文件
     name: `myNamespace.MyComponent`,     // 全局变量名(与平台约定)
     formats: ['umd',"es","cjs"],        // 必须使用UMD格式
     fileName: 'index' // 输出文件名
   },
   rollupOptions: {
    
     output: {
       manualChunks: undefined, // 强制内联动态导入
       inlineDynamicImports: true,
       entryFileNames:'[name].[format].js'
     }
   }
 }
})

最后执行输入如下

  • vite

image.png

  • webpack

image.png

tauri项目在windows上的c盘没有权限写入文件

在使用 Tauri 开发时,如果尝试在 C:\Program Files\ 这样的受保护系统目录写入或读取文件,Windows 会阻止操作(除非以管理员权限运行)。以下是解决方案:


1. 避免写入 Program Files(推荐)

Windows 对 Program Files 有严格的权限控制,普通应用不应在此目录写入数据。改用以下目录:

  • 用户数据目录(推荐):

    <pre>
    

    use tauri::api::path::{app_data_dir, resolve}; let app_data_path = app_data_dir(&tauri::Config::default()).unwrap(); std::fs::create_dir_all(&app_data_path).unwrap(); let icon_path = app_data_path.join("app.ico");

    <ul>
    <li>路径示例:<code>C:\Users\&lt;用户名&gt;\AppData\Roaming\&lt;你的应用名&gt;\app.ico</code></li>
    </ul>
    </li>
    <li>
    <p><strong>临时目录</strong></p>
    
    <pre>
    

    let temp_dir = std::env::temp_dir(); let icon_path = temp_dir.join("app.ico");


2. 如果必须写入 Program Files(不推荐)

方法 1:以管理员权限运行应用

  • 在 tauri.conf.json 中启用管理员权限:
    <pre>
    

    { "tauri": { "windows": [{ "webviewInstallMode": { "type": "offline" }, "runAsAdmin": true // 以管理员运行 }] } } 缺点:用户每次启动都会看到 UAC 弹窗,体验差。

方法 2:安装时修改目录权限

  • 用 NSIS 或 WiX 安装包脚本,在安装时赋予 Program Files\YourApp 可写权限(仍需谨慎)。

3. 检查文件是否存在(错误处理)

在读取文件前,先检查路径是否存在:

use std::path::Path;
if !Path::new(&icon_path).exists() {
  // 提供默认图标或报错
}

4. 开发时调试路径

在 main.rs 或事件处理中打印路径,确认是否正确:

println!("当前路径: {:?}", icon_path);

总结

  • 推荐方案:改用 %APPDATA% 或用户目录存储数据(Tauri 的 app_data_dir 已封装)。
  • 临时方案:以管理员运行(不推荐长期使用)。
  • 调试技巧:检查路径是否存在,打印日志定位问题。

如果仍有问题,可以提供更多代码片段(如 tauri.conf.json 和文件操作部分),我会进一步分析!

前端真的需要懂算法吗?聊聊感受

image.png 在公司干了几年,带个小团队,零零总总也面试了上百个前端候选人了。说实话,有时候面完一天,感觉人都是麻的。

最让我头疼的是什么?就是“算法题”这个环节。

我经常遇到两种候选人。一种是一听算法题,就两手一摊,表情痛苦,说“哥,我天天写业务,真没准备这个”。另一种呢,正好相反,题目一出,眼睛一亮,不出三十秒,就把LeetCode上背得滚瓜烂熟的最优解,一字不差地敲了出来,然后一脸期待地看着我。

说实话,这两种,都不是我最想看到的。

这就引出了一个很多候选人都想问,但不敢问的问题:“你们这些面试官,到底怎么想的?你们明知道我们前端平时工作中,99%的时间都用不上这些,为什么非要折磨我们?”

今天,我就想站在桌子对面,跟大伙掏心窝子地聊聊,我们问算法题,到底图个啥。


首先,我得承认一件事:我们知道你工作中不怎么写算法

对,你没看错。

我心里门儿清,我团队里的小伙伴们,每天的工作是跟产品经理“吵架”,是跟UI设计师对像素,是封装React/Vue组件,是处理浏览器兼容性,是调CSS。我招你进来,也不是为了让你用动态规划来给按钮加border-radius的。

我们不会天真地以为,前端开发就是算法竞赛。如果你能把一个复杂的业务表单组件写得清晰、可维护、可扩展,在我眼里,这远比你徒手写一个红黑树要来得有价值。

所以,请你先放轻松。我们不是在考察你是不是一个“算法大神”。


那我们到底在看什么?——思路远比答案重要

既然不是看你会不会背最优解,那我们花这宝贵的20分钟,到底在考察什么?

其实,算法题只是一个“载体”,一个“媒介”。通过这个载体,我想看到的是这几样东西:

1. 你是怎么“解读”问题的(沟通与理解能力)

一个靠谱的工程师,拿到需求不会立刻动手。他会先问问题,搞清楚所有的边界和约束。

我出一道题:“写个函数,找出数组中第二大的数。”

  • 普通候选人:埋头就开始写代码。
  • 我欣赏的候选人:会先问我,“这个数组里会有重复的数字吗?会是无序的吗?会有负数吗?如果数组长度小于2怎么办?”

你看,这就是差距。我能通过这些问题,看出你是否严谨,是否有处理边界情况的意识。这个能力,在你将来面对产品经理那些模糊的需求时,至关重要。

2. 你的“思路”是否清晰(逻辑思维)

我最喜欢看到的,不是你直接写出最优解,而是你告诉我你的思考过程。

比如,你可以说:“我首先想到的,是一个最笨的办法,先排序,然后取倒数第二个。这个时间复杂度是O(n log n)。但感觉可以优化,我再想想……也许我只需要遍历一遍,用两个变量来维护最大值和第二大值,这样时间复杂度就降到O(n)了。”

这个“先暴力,再优化”的思考过程,在我看来,比你直接默写出最优解要加分得多。因为它展示了你的逻辑推理能力优化意识

3. 你的代码“品味”(工程素养)

算法题的代码量不大,但足以管中窥豹,看出一个人的代码“品味”。

你的变量是怎么命名的?a, b, c 还是 max, secondMax, current?

你有没有处理我刚才提到的那些边界情况?

你的代码有没有基本的缩进和格式?

这些细节,都反映了你平时的编码习惯。一个连算法题都写得乱七八糟的人,我很难相信他在业务项目里能写出整洁的代码。

4. 当你卡住时,你会怎么办?(抗压与学习能力)

我有时候会故意出一些有点难度的题。我不是为了让你难堪,而是想看看你卡住的时候,会有什么反应。

是直接放弃,说“不会”?还是会尝试跟我沟通,说“我卡在xxx了,能不能给点提示?”

我非常乐意给提示。我更想招一个能和我一起“协作”解决问题的人,而不是一个遇到困难就“躺平”的人。你面对一道题的态度,很可能就是你未来面对一个技术难题的态度。


给求职者的一些真心话

所以,聊了这么多:

  • 别光背题,没用。 我只要稍微改动一下题目条件,或者问你为什么这么写,背题的同学马上就露馅了。
  • 多练习“说” 。刷题的时候,试着把你的思路说出来,录下来自己听听,或者讲给朋友听。面试时的口头表达,和自己闷头做题是两回事。
  • 重点理解“为什么” 。不要满足于“这道题这么解”,要去理解它为什么要用双指针,为什么要用哈希表。理解了思路,才能举一反三。
  • 面试时,心态放平。 没做出最优解,真没关系。把你思考的过程、你的尝试、你的权衡都清晰地表达出来,你已经赢了很多人了。

我知道,让前端去卷算法,这个“游戏规则”本身就不那么公平。我们想找的是一个会思考、会沟通、有工程素养的“解决问题的人”。

算法题,只是恰好成了当前最方便、成本最低的考察工具而已。

希望这些“面试官的牢骚”,能让你稍微不那么焦虑一点。 你们怎么看?

如何在React移动端项目引入UnoCSS及实现热更新

UnoCSS 是一个即时按需生成的原子化 CSS 引擎,它凭借独特的理念和设计,在前端开发中迅速流行。对于我们小型的项目能实现快速开发和优化打包体积,非常nice!

但在看官方文档的时候发现,按照文档操作并不能顺利的引入,或者引入之后没办法热更新,研究了大半天解决了这个问题。下面我们来一步一步的把unocss引入我们的项目吧~

目录结构

首先看一下项目结构: 我的项目本身是用React, TypeScript, 和Ant Design

  src
  ├── components/     # Reusable components
  ├── pages/         # Page components
  ├── store/         # Redux store configuration
  ├── utils/         # Utility functions
  ├── services/      # API services
  ├── constants/     # Constants and configuration
  ├── subpackages/   # Subpackage modules
  ├── uno.css
  └── index.tsx
  craco.config.js
  tsconfig.json
  uno.config.ts
  package.json

依赖包

然后看下package.json(省略掉非unocss部分)

{
  "name": "pos_ipad",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    ...
  },
  "scripts": {
    "start": "yarn uno:generate && cross-env NODE_ENV=development craco start",
    "build": "yarn uno:generate && cross-env NODE_ENV=production craco build"
    "uno:generate": "unocss \"src/**/*.{js,jsx,ts,tsx}\" -o src/uno.css",
    "uno:watch": "unocss \"src/**/*.{js,jsx,ts,tsx}\" -o src/uno.css --watch",
    "dev": "concurrently \"yarn start\" \"yarn uno:watch\""
  },
  "eslintConfig": {
  ...
  },
  "browserslist": {
  ...
  },
  "devDependencies": {
    "@unocss/postcss": "^66.3.2",
    "@unocss/preset-attributify": "^66.3.2",
    "@unocss/preset-icons": "^66.3.2",
    "@unocss/preset-rem-to-px": "^66.3.2",
    "@unocss/preset-uno": "^66.3.2",
    "@unocss/transformer-directives": "^66.3.2",
    "@unocss/transformer-variant-group": "^66.3.2",
    "@unocss/webpack": "^66.3.2",
    "concurrently": "^9.2.0",
    "unocss": "^66.3.2"
    ...
  },
}

按照devDependencies安装列表,就可以把用到的uno相关npm包安装好,接下来配置script。 我们想执行在运行项目的同时watch样式改动,还需要安装一个concurrently,参考scriptdev的配置。

unocss配置及引用

那么到现在为止我们的依赖和启动脚本都配置好啦,接下来看unocss配置部分。 craco.config.js

const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
  webpack: {
    configure: (webpackConfig) => {
      webpackConfig.resolve.plugins = [
        ...(webpackConfig.resolve.plugins || []),
        new TsconfigPathsPlugin({})
      ];
      return webpackConfig;
    },
  },
  //可以只参考这个引入
  style: {
    postcss: {
      plugins: [
        require('@unocss/postcss')(),
      ],
    },
  },
};

uno.config.ts 重中之重 可以根据自己项目情况调整配置,由于我要用px,所以多引入了一个presetRemToPx的npm,不用的话可以删掉

import { defineConfig, presetAttributify, presetIcons, transformerDirectives, transformerVariantGroup } from "unocss";
import { presetMini } from "@unocss/preset-mini";
import presetRemToPx from "@unocss/preset-rem-to-px";

export default defineConfig({
  // 扫描文件 - 在 webpack 模式下,这个配置会被 webpack 插件自动处理
  content: {
    filesystem: ["src/**/*.{js,jsx,ts,tsx}", "public/index.html"],
  },

  // 输出配置
  outputToCssLayers: false,

  // 预设
  presets: [
    presetMini(), // 默认预设,包含 Tailwind CSS 兼容的工具类
    presetAttributify(), // 属性化模式
    presetIcons({
      // 图标预设配置
      scale: 1.2,
      warn: true,
      collections: {
        // 可以添加你需要的图标集
        // carbon: () => import('@iconify-json/carbon/icons.json').then(i => i.default),
        // mdi: () => import('@iconify-json/mdi/icons.json').then(i => i.default),
      },
    }),
    presetRemToPx(), // rem 转 px 预设
  ],

  // 转换器
  transformers: [
    transformerDirectives(), // 支持 @apply 指令
    transformerVariantGroup(), // 支持变体组语法
  ],

  // 自定义规则
  rules: [],

  // 快捷方式
  shortcuts: [
    // 按钮样式
    ['btn', 'px-4 py-2 rounded inline-block cursor-pointer transition-colors duration-200 disabled:cursor-default disabled:opacity-50'],
    ['btn-primary', 'btn bg-blue-500 text-white hover:bg-blue-600 disabled:bg-gray-400'],
    ['btn-secondary', 'btn bg-gray-200 text-gray-800 hover:bg-gray-300 disabled:bg-gray-100'],

    // 卡片样式
    ['card', 'bg-white p-6 rounded-lg shadow-sm border border-gray-200'],
    ['card-hover', 'card hover:shadow-md transition-shadow duration-200'],

    // 布局快捷方式
    ['flex-center', 'flex items-center justify-center'],
    ['flex-col-center', 'flex flex-col items-center justify-center'],
  ],

  // 主题配置
  theme: {
    colors: {
      // 自定义颜色
      primary: "#1890ff",
      success: "#52c41a",
      warning: "#faad14",
      error: "#f5222d",
    },
    breakpoints: {
      // 响应式断点
      sm: "640px",
      md: "768px",
      lg: "1024px",
      xl: "1280px",
      "2xl": "1536px",
    },
  },

  // 安全列表 - 确保这些类不会被清除
  safelist: [
    // 'text-red-500',
    // 'bg-blue-500',
  ],
});

src/index.tsx 在入口文件中引入uno.css

import React from 'react';
import ReactDOM from 'react-dom/client';
import './uno.css';//重点只有这一行
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
reportWebVitals();

验证

好啦,到现在为止我们的引入工作就结束啦,这个时候可以运行项目试一下啦,验证的方法是:如果新增了unocss的使用,在src/uno.css则会新增对应的css样式,由于我们做了热更新,所以src/uno.css文件会随着改动慢慢增多,注意:这个文件是不要手动更新的哦!(* ̄︶ ̄) 让我们启动项目试一下吧~ 在这里插入图片描述

看到UnoCSS的watch打印就说明项目启动成功啦~撒花!!!

React 渲染全流程剖析:初次渲染与重渲染的底层逻辑

附上Vue渲染机制以作对比:Vue3渲染机制解析:编译时优化与虚拟DOM的性能跃迁

一、初次渲染

import Image from './Image.js';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'))
root.render(<Image />);

如以上代码初次渲染是调用createRoot方法转入目标DOM节点,然后调用render函数完成的渲染,以下是详细步骤:

1. JSX编译成虚拟DOM树

通过babel编译转化jsxjs格式代码,如

return (
<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>
)

转化为

return (
React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)
)

createElement接受以下参数:

  • component:元素类型标签
  • props:标签属性
  • children:子节点 编译后的元素即是虚拟DOM树节点 注意render函数在类组件中即是指render方法,如果是在函数组件中则是函数组件本身,即
function Foo() {
    return <h1> Foo </h1>;
}

2. Fiber树构建

创建根节点

function createFiberRoot(containerInfo) {
  // 创建 FiberRoot(React应用根节点)
  const root = new FiberRootNode(containerInfo);
  
  // 创建未初始化的 HostRootFiber(Fiber树的根节点)
  const uninitializedFiber = createHostRootFiber();
  root.current = uninitializedFiber;  // FiberRoot.current 指向 HostRootFiber
  uninitializedFiber.stateNode = root; // HostRootFiber.stateNode 回指 FiberRoot
  
  // 初始化更新队列
  initializeUpdateQueue(uninitializedFiber);
  return root;
}

创建更新对象

function updateContainer(element, container) {
  const current = container.current; // 这里是HostRootFiber
  const lane = requestUpdateLane(current); // 获取更新优先级
  
  // 创建更新对象
  const update = createUpdate(lane);
  update.payload = { element }; // 存储 ReactElement (<App/>)
  
  // - 将根组件 <App/> 存入 HostRootFiber 的更新队列
  enqueueUpdate(current, update);
  
  // 开始调度更新
  scheduleUpdateOnFiber(current, lane);
}

// 更新后内存结构:
HostRootFiber.updateQueue.shared.pending = {
  payload: { element: <App/> }, // 存储要渲染的组件
  next: update // 环形链表
}

3. 协调阶段:深度优先构建Fiber树

  • workInProgress 指针跟踪当前处理的 Fiber
  • 此处无alternate 指针,因为是初次渲染,无旧树进行比较
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
  
  // 创建子Fiber节点
  const next = beginWork(current, unitOfWork, renderLanes);
  
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  
  if (next === null) {
    // 完成当前节点
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next; // 继续处理子节点
  }
}

例子1:

class App extends React.Component {
  componentDidMount() {
    console.log(`App Mount`);
    console.log(`App 组件对应的fiber节点: `, this._reactInternals);
  }

  render() {
    return (
      <div className="app">
        <header>header</header>
        <Content />
      </div>
    );
  }
}

class Content extends React.Component {
  componentDidMount() {
    console.log(`Content Mount`);
    console.log(`Content 组件对应的fiber节点: `, this._reactInternals);
  }

  render() {
    return (
      <React.Fragment>
        <p>1</p>
        <p>2</p>
      </React.Fragment>
    );
  }
}

export default App;

例子1中Fiber 树构建过程: 1. 从 HostRootFiber 开始,创建 <App> Fiber 节点 2. 处理 <App> 的 render 结果,创建 <div> Fiber 节点 3. 处理 <div> 的子节点,依次创建 <header> 和 <Content> Fiber 节点 4. 处理 <Content> 的 render 结果,创建 <Fragment> 和两个 <p> Fiber 节点

4. 完成阶段:创建DOM和收集副作用

  • 所有标记了 Placement 的 Fiber 组成单向链表
  • 顺序:深度优先,子节点在前,父节点在后
function completeWork(current, workInProgress) {
  switch (workInProgress.tag) {
    case HostComponent:
      // 创建DOM实例
      const instance = createInstance(workInProgress.type, workInProgress.pendingProps);
      
      // 将子DOM附加到当前DOM
      appendAllChildren(instance, workInProgress);
      
      // 设置DOM属性
      finalizeInitialChildren(instance, workInProgress.type, workInProgress.pendingProps);
      
      workInProgress.stateNode = instance; // 关联DOM
      
      // 标记插入操作
      if (workInProgress.flags & Placement) {
        markUpdate(workInProgress);
      }
      break;
    case HostText:
      // 文本节点处理...
    // 其他类型...
  }
  
  // 收集副作用到父节点
  if (workInProgress.flags > PerformedWork) {
    appendEffectToList(workInProgress.return, workInProgress);
  }
}

例子1的副作用链表的形成:<header><p>1 → <p>2 → <div><App>HostRootFiber

5. 提交阶段:DOM操作

function commitRoot(root) {
  const finishedWork = root.finishedWork;
  let effect = finishedWork.firstEffect;
  
  // 提交所有插入操作
  while (effect !== null) {
    commitPlacement(effect); // 实际DOM插入操作
    effect = effect.nextEffect;
  }
  
  // 调用生命周期
  commitLifeCycles(finishedWork);
}

image.png

二、重渲染

组件(或者是其祖先之一)的状态发生了改变会导致重新渲染。

1. 状态变更触发更新

创建更新对象

  • 当组件状态变更(通过 setStateuseStateforceUpdate)时,React 会创建一个更新对象(update object)。这个对象包含更新内容、优先级信息等,并添加到对应 Fiber 节点的更新队列中
  • 当使用setState时:
enqueueSetState(inst, payload) {
  const fiber = getInstance(inst); // 获取组件对应fiber
  const lane = requestUpdateLane(fiber); // 确定更新优先级
  const update = createUpdate(eventTime, lane);
  update.payload = payload; // 存储更新数据
  
  // 将更新加入fiber的更新队列
  enqueueUpdate(fiber, update);
  
  // 开始调度
  scheduleUpdateOnFiber(fiber, lane);
}

标记更新路径

  • React 需要确定哪些节点会受到更新的影响,它会从触发更新的 Fiber 节点开始,向上遍历父节点,标记出整个更新路径
  • 更新路径可帮助后续遍历跳过未标记的虚拟DOM子树
function markUpdateLaneFromFiberToRoot(sourceFiber, lane) {
  // 设置当前fiber的lanes(标记当前节点需要更新)
  sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
  
  // 向上遍历父节点,设置childLanes(标记子树需要更新)
  let node = sourceFiber;
  let parent = sourceFiber.return;
  while (parent !== null) {
    parent.childLanes = mergeLanes(parent.childLanes, lane);
    node = parent;
    parent = parent.return;
  }
  
  // 返回FiberRoot
  return node.stateNode;
}

2. 准备阶段:双缓存结构初始化

  • 使用双缓存技术,在更新时创建一棵新的 workInProgress 树,与 current 树(当前显示树)交替使用
function prepareFreshStack(root, lanes) {
  // 创建workInProgress树
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  
  // 初始化workInProgress节点
  const rootWorkInProgress = createWorkInProgress(root.current, null);
  workInProgress = rootWorkInProgress;
  workInProgressRoot = root;
  workInProgressRootRenderLanes = lanes;
}

3. 协调阶段:对比更新

beginWork与节点对比

  • 在 beginWork 阶段,React 对比新旧节点,决定是否需要更新或复用子树
function beginWork(current, workInProgress, renderLanes) {
  if (current !== null) {
    // 检查props是否变化
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    
    // 判断是否需要更新
    if (oldProps === newProps && !hasLegacyContextChanged()) {
      // 检查优先级:是否在本次渲染范围内
      if (!includesSomeLane(renderLanes, workInProgress.lanes)) {
        // 无需更新,进入bailout逻辑
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
      }
    }
    didReceiveUpdate = true;
  }
  
  // 需要更新:调用具体updatexxx更新函数
  switch (workInProgress.tag) {
    case ClassComponent: 
      return updateClassComponent(current, workInProgress, ...);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    // 其他类型...
  }
}

bailout 逻辑:跳过未变更子树

  • 当节点不需要更新时,React进入bailout逻辑
  • bailout 条件:props 未变化且优先级不匹配
  • 根据更新路径可以跳过整个子树的重渲染
function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {
  // 检查子节点是否需要更新
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    // 整个子树都不需要更新,直接跳过
    return null;
  }
  
  // 克隆子节点(复用现有 Fiber)
  cloneChildFibers(current, workInProgress);
  
  // 返回第一个子节点继续处理
  return workInProgress.child;
}

4. updatexxx函数与reconcileChildren调和函数(此时需要更新)

当节点需要更新时,React 会调用调和函数,其实现的 Diff 算法会对比新旧子节点,决定复用、移动或删除节点。 传统的Diff算法是循环递归每一个节点(真实DOM节点),算法复杂度是O(n^3)。Vue和React都使用虚拟DOM节点的Diff算法,虚拟DOM是将目标所需的UI通过数据结构虚拟表现出来,保存到内存中,再将真实DOM与之保持同步。 Vue2的Diff算法使用双端对比虚拟节点,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作;Vue3在从=此基础借鉴了 ivi算法和 inferno算法。在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型。 React的Diff算法遵循以下三个层级的策略:tree Diff(只比较同层级节点,不跨层级比较)、component Diff(相同类型组件复用实例)、element Diff(使用 key 标识稳定元素)。本节不对diff算法展示细说,后面会出一篇新文章diff算法,到时候附上链接。 本节只需要了解reconcileChildren调和函数:

  • 调和函数是updateXXX(如: updateHostRoot, updateClassComponent 等)函数中的一项重要逻辑, 它的作用是向下生成子节点, 并设置fiber.flags. 与初次渲染对比
  • 初次创建时fiber节点没有比较对象, 所以在向下生成子节点的时候没有任何多余的逻辑
  • 对比更新时需要把ReactElement对象与旧fiber对象进行比较, 来判断是否需要复用旧fiber对象. 调和函数的目的
  1. 给新增,移动,和删除节点设置fiber.flags,flags标记:
    • Placement:新增或移动节点
    • Update:更新属性
    • Deletion:删除节点
  2. 如果是需要删除的fiber, 除了自身打上Deletion之外, 还要将其添加到父节点的effects链表中(正常副作用队列的处理是在completeWork函数, 但是该节点(被删除)会脱离fiber树, 不会再进入completeWork阶段, 所以在beginWork阶段提前加入副作用队列)

5. 完成阶段:completeWork与副作用收集

在 completeWork 阶段,React 完成节点处理并收集副作用(即DOM 操作)。 completeWork函数与初次更新时的completeWork函数逻辑一致,只是此时current不为null

function completeWork(current, workInProgress) {
  const newProps = workInProgress.pendingProps;
  
  switch (workInProgress.tag) {
    case HostComponent: // DOM 元素
      if (current !== null && workInProgress.stateNode != null) {
        // 对比更新:比较新旧属性
        updateHostComponent(current, workInProgress, newProps);
      } else {
        // 新增节点:创建 DOM 实例
        const instance = createInstance(
          workInProgress.type,
          newProps,
          workInProgress
        );
        // 关联 DOM 与 Fiber
        workInProgress.stateNode = instance;
        // 设置初始属性
        finalizeInitialChildren(instance, newProps);
      }
      break;
    // 其他类型处理...
  }
  
  // 收集副作用(flags)
  if (workInProgress.flags > PerformedWork) {
    // 添加到父节点的副作用链表
    if (returnFiber.firstEffect === null) {
      returnFiber.firstEffect = workInProgress;
    } else {
      returnFiber.lastEffect.nextEffect = workInProgress;
    }
    returnFiber.lastEffect = workInProgress;
  }
}

触发updateHostComponent函数更新DOM属性

function updateHostComponent(current, workInProgress, newProps) {
  const oldProps = current.memoizedProps;
  
  // 比较新旧属性差异
  if (oldProps !== newProps) {
    // 计算属性差异
    const updatePayload = prepareUpdate(
      workInProgress.stateNode,
      workInProgress.type,
      oldProps,
      newProps
    );
    
    // 存储差异到 updateQueue
    workInProgress.updateQueue = updatePayload;
    
    // 标记 Update 副作用
    if (updatePayload) {
      workInProgress.flags |= Update;
    }
  }
}

6. 提交阶段:DOM 操作与生命周期

提交阶段遍历副作用链表,执行 DOM 操作并调用生命周期方法

function commitRoot(root) {
  const finishedWork = root.finishedWork;
  let effect = finishedWork.firstEffect;
  
  // 阶段1: BeforeMutation(调用 getSnapshotBeforeUpdate 获取 DOM 快照)
  commitBeforeMutationEffects();
  
  // 阶段2: Mutation(DOM操作)
  while (effect !== null) {
    const nextEffect = effect.nextEffect;
    const flags = effect.flags;
    
    // 处理 Placement(插入/移动)
    if (flags & Placement) {
      commitPlacement(effect);
    } 
    // 处理 Update(属性更新)
    else if (flags & Update) {
      commitWork(effect);
    }
    // 处理 Deletion(删除)
    else if (flags & Deletion) {
      commitDeletion(effect);
    }
    
    effect = nextEffect;
  }
  
  // 阶段3: Layout(同步生命周期)
  effect = finishedWork.firstEffect;
  while (effect !== null) {
    const nextEffect = effect.nextEffect;
    if (effect.flags & Update) {
      // 类组件:componentDidMount/Update
      // 函数组件:useLayoutEffect
      commitLayoutEffects(effect);
    }
    effect = nextEffect;
  }
  
  // 阶段4: Passive(异步 useEffect)
  scheduleCallback(NormalPriority, () => {
    flushPassiveEffects();
  });
}

7. 清理与切换:完成更新

提交完成后,React 清理临时状态并切换 current 指针

function finishCommit() {
  // 清理临时状态
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  
  // 切换 current 树,使新树成为当前树
  root.current = finishedWork;
  
  // 调度未处理的更新
  if (root.pendingPassiveEffects !== null) {
    scheduleCallback(NormalPriority, () => {
      flushPassiveEffects();
      return null;
    });
  }
  
  // 检查是否有待处理的更新
  ensureRootIsScheduled(root);
}

image.png

三、初次缓存和重缓存的区别

  • 对于初次渲染, React 会调用根组件。
  • 对于重渲染, React 会调用内部状态更新触发了渲染的函数组件
    • 这个过程是递归的:如果更新后的组件会返回某个另外的组件,那么 React 接下来就会渲染 那个 组件,而如果那个组件又返回了某个组件,那么 React 接下来就会渲染 那个 组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。
特性 初次渲染 重渲染
缓存机制 无缓存 双缓存树(current/workInProgress)
节点创建 全量新建节点 复用节点 + 条件创建新节点
对比机制 无旧节点比较 Fiber节点Diff算法(复用决策)
副作用标记 全部标记为Placement 动态标记Update/Placement/Deletion
子树处理 全量处理 bailout机制跳过未变更子树
DOM操作 全量插入 增量更新(仅变更部分)
性能优化 无优化空间 优先级调度 + 子树跳过
alternate指针 不存在 新旧节点互相引用

Clipboard_Screenshot_1751456908.png

四、React和Vue的渲染机制的区别

1. 虚拟DOM

  • React:通过JSX编译生成虚拟DOM树,所有UI逻辑(插值、循环、条件)均用原生JavaScript实现。状态变化时生成新虚拟DOM树,通过Diff算法 计算最小变更集,再更新真实DOM
  • Vue:模板编译为虚拟DOM树,但依赖响应式系统追踪数据变化。数据变更时直接定位受影响组件,生成局部虚拟DOM并比对,减少不必要的树遍历

2. 响应式系统

  • React无自动依赖追踪。状态更新(setState/useState)默认触发当前组件及所有子组件的重渲染,需开发者手动优化(如 React.memo 或 shouldComponentUpdate
  • Vue依赖收集+发布订阅
    • Vue2:使用 Object.defineProperty 拦截数据读写
    • Vue3:换成使用Proxy实现细粒度依赖追踪,数据变更时仅触发关联组件的更新

参考资料: fiber 树构造(初次创建) fiber 树构造(对比更新) React文档-渲染和提交

[blockly] blockly块的变形器

前言

因为之前我在网上找blockly变形器的相关的内容的时候,资料少的又少,现在我也来补充一点

版本

使用的blackly版本为12.1.0,如果为过于老的版本可能不适用

效果展示

7月3日.gif

可以根据我们的需要来进行切换样式

实现解释

通过在代码块的样式中添加setMutator为打开变形器的开关,打开后

this.setMutator(new Blockly.icons.MutatorIcon(['custom_if_elseif', 'custom_if_else'], this));

与之配合的是下面两个函数: decompose用户点击变形器按钮时展示当前结构供用户编辑 compose用户完成编辑关闭窗口根据编辑内容修改 block 结构

 decompose: function (workspace) {
 // 创建一个新的workspac,并且在这个workspace上面添加一个新块,名为custom_if_container
        const containerBlock = workspace.newBlock('custom_if_container');
        containerBlock.initSvg();

        let connection = containerBlock.getInput('STACK').connection;

// 根据之前变形的结构,补充所存在的custom_if_elseif块和custom_if_else块
        for (let i = 1; i <= this.elseifCount_; i++) {
            const elseifBlock = workspace.newBlock('custom_if_elseif');
            elseifBlock.initSvg();
            connection.connect(elseifBlock.previousConnection);
            connection = elseifBlock.nextConnection;
        }
        if (this.elseCount_) {
            const elseBlock = workspace.newBlock('custom_if_else');
            elseBlock.initSvg();
            connection.connect(elseBlock.previousConnection);
        }

        return containerBlock;
    },

// 每次放置新的块的时候进行变形的操作
    compose: function (containerBlock) {
        // 移除所有 elseif 和 else
        for (let i = 1; this.getInput(`IF${i}`); i++) {
            this.removeInput(`IF${i}`);
            this.removeInput(`DO${i}`);
        }
        if (this.getInput('ELSE')) {
            this.removeInput('ELSE');
        }

// 用于记录这个块中存在的custom_if_elseif块和custom_if_else块的数量
        this.elseifCount_ = 0;
        this.elseCount_ = 0;

        // 重新插入
        let clauseBlock = containerBlock.getInputTargetBlock('STACK');
        let i = 1;
        // 根据取到的clauseBlock ,也就是打开变形器的这个工作区上面放置的块,进行添加并且记录数量
        while (clauseBlock) {
            if (clauseBlock.type === 'custom_if_elseif') {
                this.appendValueInput(`IF${i}`)
                    .setCheck("Boolean")
                    .appendField("否则如果");
                this.appendStatementInput(`DO${i}`)
                    .appendField("那么");
                i++;
                this.elseifCount_++;
            } else if (clauseBlock.type === 'custom_if_else') {
                this.appendStatementInput('ELSE')
                    .appendField("否则");
                this.elseCount_++;
            }
            clauseBlock = clauseBlock.nextConnection && clauseBlock.nextConnection.targetBlock();
        }
    }
};

完整代码

Blockly.Blocks['custom_if'] = {
    init: function () {
        this.setMutator(new Blockly.icons.MutatorIcon(['custom_if_elseif', 'custom_if_else'], this));
        this.appendValueInput("IF0")
            .setCheck("Boolean")
            .appendField("如果");
        this.appendStatementInput("DO0")
            .appendField("那么");

        this.elseifCount_ = 0;
        this.elseCount_ = 0;

        this.setPreviousStatement(true, null);
        this.setNextStatement(true, null);
        this.setColour("#1E90FF");
    },

    decompose: function (workspace) {
        const containerBlock = workspace.newBlock('custom_if_container');
        containerBlock.initSvg();

        let connection = containerBlock.getInput('STACK').connection;

        for (let i = 1; i <= this.elseifCount_; i++) {
            const elseifBlock = workspace.newBlock('custom_if_elseif');
            elseifBlock.initSvg();
            connection.connect(elseifBlock.previousConnection);
            connection = elseifBlock.nextConnection;
        }

        if (this.elseCount_) {
            const elseBlock = workspace.newBlock('custom_if_else');
            elseBlock.initSvg();
            connection.connect(elseBlock.previousConnection);
        }

        return containerBlock;
    },

    compose: function (containerBlock) {
        // 移除所有 elseif 和 else
        for (let i = 1; this.getInput(`IF${i}`); i++) {
            this.removeInput(`IF${i}`);
            this.removeInput(`DO${i}`);
        }
        if (this.getInput('ELSE')) {
            this.removeInput('ELSE');
        }

        this.elseifCount_ = 0;
        this.elseCount_ = 0;

        // 重新插入
        let clauseBlock = containerBlock.getInputTargetBlock('STACK');
        let i = 1;
        while (clauseBlock) {
            if (clauseBlock.type === 'custom_if_elseif') {
                this.appendValueInput(`IF${i}`)
                    .setCheck("Boolean")
                    .appendField("否则如果");
                this.appendStatementInput(`DO${i}`)
                    .appendField("那么");
                i++;
                this.elseifCount_++;
            } else if (clauseBlock.type === 'custom_if_else') {
                this.appendStatementInput('ELSE')
                    .appendField("否则");
                this.elseCount_++;
            }
            clauseBlock = clauseBlock.nextConnection && clauseBlock.nextConnection.targetBlock();
        }
    }
};
Blockly.Blocks['custom_if_container'] = {
    init: function () {
        this.appendDummyInput().appendField("条件分支");
        this.appendStatementInput("STACK");
        this.setColour(210);
        this.setTooltip("");
        this.contextMenu = false;
    }
};

Blockly.Blocks['custom_if_elseif'] = {
    init: function () {
        this.appendDummyInput().appendField("添加 否则如果");
        this.setPreviousStatement(true, null);
        this.setNextStatement(true, null);
        this.setColour(210);
        this.setTooltip("");
        this.contextMenu = false;
    }
};

Blockly.Blocks['custom_if_else'] = {
    init: function () {
        this.appendDummyInput().appendField("添加 否则");
        this.setPreviousStatement(true, null);
        this.setColour(210);
        this.setTooltip("");
        this.contextMenu = false;
    }
};
javascriptGenerator.forBlock['custom_if'] = function (block, generator) {
    let code = '';
    let n = 0;

    const condition = generator.valueToCode(block, 'IF0', Order.NONE) || 'false';
    const branch = generator.statementToCode(block, 'DO0');
    code += `if (${condition}) {${branch}}`;

    for (n = 1; n <= block.elseifCount_; n++) {
        const condition = generator.valueToCode(block, 'IF' + n, Order.NONE) || 'false';
        const branch = generator.statementToCode(block, 'DO' + n);
        code += ` else if (${condition}) {${branch}}`;
    }

    if (block.elseCount_) {
        const branch = generator.statementToCode(block, 'ELSE');
        code += ` else {${branch}}`;
    }



    return code + '';
};

JavaScript 数据扁平化方法大全

前言

数据扁平化是指将多维数组转换为一维数组的过程。由于嵌套数据结构增加了访问和操作数据的复杂度,所以·我们可以将嵌套数据变成一维的数据结构,下面就是我搜集到的一些方法,希望可以给你带来帮助!!

1. 使用 Array.prototype.flat()(推荐)

ES2019 引入的专门方法:

const nestedArr = [1, [2, [3, [4]], 5]];

// 默认只扁平化一层
const flattened1 = nestedArr.flat();
console.log(flattened1); // [1, 2, [3, [4]], 5]

// 指定深度为2
const flattened2 = nestedArr.flat(2);
console.log(flattened2); // [1, 2, 3, [4], 5]

// 完全扁平化
const fullyFlattened = nestedArr.flat(Infinity);
console.log(fullyFlattened); // [1, 2, 3, 4, 5]

解析

  • flat(depth) 方法创建一个新数组,所有子数组元素递归地连接到指定深度
  • 参数 depth 指定要提取嵌套数组的结构深度,可选的参数,默认为1
  • 使用 Infinity 可展开任意深度的嵌套数组,Infinity 是一个特殊的数值,表示无穷大

2. 使用 reduce() 和 concat() 递归

function flatten(arr) {
  // 使用 reduce 方法遍历数组元素
  return arr.reduce((acc, val) => {
    // 如果当前元素是数组,则递归调用 flatten 继续展开,并拼接到累积数组 acc
    if (Array.isArray(val)) {
      return acc.concat(flatten(val));
    } 
    // 如果当前元素不是数组,直接拼接到累积数组 acc
    else {
      return acc.concat(val);
    }
  }, []); // 初始累积值是一个空数组 []
}

// 测试用例
const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // 输出: [1, 2, 3, 4, 5]

解析

  1. 递归处理嵌套数组

    • 遇到子数组时,递归调用 flatten(val) 继续展开,直到所有层级都被展开为单层。
  2. reduce 方法的作用

    • 遍历数组,通过 acc(累积值)逐步拼接结果,初始值设为 [](空数组)。
  3. Array.isArray(val) 检查

    • 判断当前元素是否为数组,决定是否需要递归展开。
  4. concat 拼接结果

    • 将非数组元素或递归展开后的子数组拼接到累积数组 acc 中。

3. 使用 concat() 和扩展运算符递归

function flatten(arr) {
  // 使用扩展运算符 (...) 展开数组的第一层,并合并成一个新数组
  const flattened = [].concat(...arr);

  // 检查当前展开后的数组中是否仍然包含嵌套数组
  // 如果存在嵌套数组,则递归调用 flatten 继续展开
  // 如果所有元素都是非数组类型,则直接返回展开后的数组
  return flattened.some(item => Array.isArray(item)) 
    ? flatten(flattened) 
    : flattened;
}

// 测试用例
const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // 输出: [1, 2, 3, 4, 5]

解析

  1. [].concat(...arr) 展开一层数组

    • 使用扩展运算符 ... 展开 arr 的最外层,并通过 concat 合并成一个新数组。
    • 例如:[].concat(...[1, [2, [3]]]) → [1, 2, [3]](仅展开一层)。
  2. flattened.some(Array.isArray) 检查嵌套

    • 使用 Array.prototype.some() 检查当前数组是否仍然包含子数组。
    • 如果存在,则递归调用 flatten 继续展开。
  3. 递归终止条件

    • 当 flattened 不再包含任何子数组时,递归结束,返回最终结果。

4. 使用 toString() 方法(仅适用于数字数组)

const nestedArr = [1, [2, [3, [4]], 5]];
const flattened = nestedArr.toString().split(',').map(Number);
console.log(flattened); // [1, 2, 3, 4, 5]

解析

  1. toString() 的隐式转换

    • JavaScript 的 Array.prototype.toString() 会自动展开嵌套数组,并用逗号连接所有元素。
    • 例如:[1, [2, [3]]].toString() → "1,2,3"
  2. split(',') 分割字符串

    • 将字符串按逗号拆分成字符串数组,但所有元素会是字符串类型(如 "2")。
  3. map(Number) 类型转换

    • 通过 Number 构造函数将字符串元素转换为数字类型。
    • 注意:如果原数组包含非数字(如 ['a', [2]]),结果会变成 [NaN, 2]

优缺点

  • 优点:代码极其简洁,适合纯数字的嵌套数组。

  • 缺点

    • 仅适用于数字数组(其他类型会被强制转换,如 true → 1null → 0)。
    • 无法保留原数据类型(如字符串 '3' 会被转成数字 3)。

适用场景

  • 快速展开纯数字的嵌套数组,且不关心中间过程的性能损耗(toString 和 split 会有临时字符串操作)。

5. 使用 JSON.stringify() 和正则表达式

function flatten(arr) {
  // 1. 使用 JSON.stringify 将数组转换为字符串表示
  //    例如:[1, [2, [3]], 'a'] → "[1,[2,[3]],\"a\"]"
  const jsonString = JSON.stringify(arr);

  // 2. 使用正则表达式移除所有的 '[' 和 ']' 字符
  //    例如:"[1,[2,[3]],\"a\"]" → "1,2,3,\"a\""
  const withoutBrackets = jsonString.replace(/[\[\]]/g, '');

  // 3. 按逗号分割字符串,生成字符串数组
  //    例如:"1,2,3,\"a\"" → ["1", "2", "3", "\"a\""]
  const stringItems = withoutBrackets.split(',');

  // 4. 尝试将每个字符串解析回原始数据类型
  //    - 数字会变成 Number 类型(如 "1" → 1)
  //    - 字符串会保留(如 "\"a\"" → "a")
  //    - 其他 JSON 可解析类型也会被正确处理
  return stringItems.map(item => {
    try {
      // 尝试 JSON.parse 解析(处理字符串、数字等)
      return JSON.parse(item);
    } catch (e) {
      // 如果解析失败(如空字符串或非法 JSON),返回原始字符串
      return item;
    }
  });
}

// 测试用例
const nestedArr = [1, [2, [3, [4]], 5, 'a', { b: 6 }];
console.log(flatten(nestedArr)); 
// 输出: [1, 2, 3, 4, 5, "a", { b: 6 }]

解析

  1. JSON.stringify 的作用

    • 将整个数组(包括嵌套结构)转换为 JSON 字符串,保留所有数据类型信息。
  2. 正则替换 /[[]]/g

    • 移除所有方括号字符 [ 和 ],只保留逗号分隔的值。
  3. split(',') 分割字符串

    • 生成一个字符串数组,但每个元素可能仍是被 JSON 字符串化的(如 ""a"")。
  4. JSON.parse() 尝试恢复数据类型

    • 通过 JSON.parse 将字符串转换回原始类型(数字、字符串、对象等)。
    • 使用 try-catch 处理不合法的 JSON 字符串(如空字符串或格式错误的情况)。

优缺点

  • 优点

    • 支持任意数据类型(数字、字符串、对象等)。
    • 能正确处理嵌套对象(如 { b: 6 })。
  • 缺点

    • 性能较低(涉及 JSON 序列化、正则替换、解析等操作)。
    • 如果原始数组包含特殊字符串(如 "[1]" ,可能会被错误解析。

适用场景

  • 需要处理混合数据类型(非纯数字)的嵌套数组。
  • 对性能要求不高,但需要代码简洁的场景。

6. 使用堆栈的非递归实现

function flatten(arr) {
  // 创建栈并初始化(使用扩展运算符浅拷贝原数组)
  const stack = [...arr];
  const result = [];
  
  // 循环处理栈中的元素
  while (stack.length) {
    // 从栈顶取出一个元素
    const next = stack.pop();
    
    if (Array.isArray(next)) {
      // 如果是数组,展开后压回栈中(保持顺序)
      stack.push(...next);
    } else {
      // 非数组元素,添加到结果数组前端(保持原顺序)
      result.unshift(next);
    }
  }
  
  return result;
}

const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // [1, 2, 3, 4, 5]

解析

  1. 栈结构初始化

    • 使用扩展运算符 [...arr] 创建原数组的浅拷贝作为初始栈
    • 避免直接修改原数组
  2. 栈处理循环

    • 使用 while 循环处理栈直到为空
    • 每次从栈顶 pop() 一个元素进行处理
  3. 元素类型判断

    • 使用 Array.isArray() 检查元素是否为数组
    • 如果是数组则展开后重新压入栈
    • 非数组元素则添加到结果数组
  4. 顺序保持

    • 使用 unshift() 将元素添加到结果数组前端,当然这样比较费性能,可以改用 push() + reverse() 替代 unshift()
    • 确保最终结果的顺序与原数组一致

优缺点

  • 优点

    • 支持任意数据类型(不限于数字)
    • 可以处理深层嵌套结构(无递归深度限制)
    • 相比递归实现,不易导致栈溢出
  • 缺点

    • 使用 unshift() 导致时间复杂度较高(O(n²))
    • 需要额外空间存储栈结构
    • 相比原生 flat() 方法性能稍差
    • 无法控制扁平化深度(总是完全扁平化)

适用场景

  • 需要处理混合数据类型的深层嵌套数组
  • 需要避免递归导致的栈溢出风险

7. 使用 Array.prototype.some() 和扩展运算符

function flatten(arr) {
  // 循环检测数组中是否还包含数组元素
  while (arr.some(item => Array.isArray(item))) {
    // 使用扩展运算符展开当前层级的所有数组
    // 并通过concat合并为一层
    arr = [].concat(...arr);
  }
  return arr;
}

const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // [1, 2, 3, 4, 5]

解析

  1. 循环条件检测

    • 使用 arr.some() 方法检测数组中是否还存在数组元素
    • Array.isArray(item) 判断每个元素是否为数组
  2. 层级展开

    • 使用扩展运算符 ...arr 展开当前层级的数组
    • 通过 [].concat() 将展开的元素合并为新数组
  3. 迭代处理

    • 每次循环处理一层嵌套
    • 重复直到没有数组元素存在

性能比较

对于大多数现代应用:

  1. 优先使用 flat(Infinity)(最简洁且性能良好)
  2. 对于深度嵌套的大数组,考虑非递归的堆栈实现
  3. 递归方法在小数据集上表现良好且代码简洁
  4. 避免 toString() 方法除非确定只有数字数据

总结

JavaScript 提供了多种扁平化数组的方法,从简单的内置 flat() 方法到各种手动实现的递归、迭代方案。选择哪种方法取决于:

  • 运行环境是否支持 ES2019+
  • 数据结构的复杂程度
  • 对性能的要求
  • 代码可读性需求

在大多数现代应用中,flat(Infinity) 是最佳选择,因为它简洁、高效且语义明确。

如何丝滑使用JavaScript的装饰器?

在 JavaScript 里,装饰器(Decorators)是一种能对类、方法、属性的行为进行扩展或者修改的语法。它的核心原理是借助元编程,在不改变原有代码结构的前提下,为目标添加新功能。

基本概念

直接show code,现有如下代码,用来记录log日志:

function log(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
    const result = original.apply(this, args);
    console.log(`方法 ${name} 返回:${result}`);
    return result;
  };
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

// 使用示例
const calc = new Calculator();
calc.add(3, 4); // 控制台会输出调用信息和返回结果

装饰器函数参数解析

在 JavaScript 装饰器中,log 函数的三个参数分别代表:

  1. target:被装饰的类或原型对象。

    • 若装饰的是类方法,target 就是类的原型(prototype)。
    • 若装饰的是类,target 就是类本身。
  2. name:被装饰的方法或属性的名称(字符串类型)。

  3. descriptor:属性描述符对象(与 Object.defineProperty 中的描述符相同),包含以下属性:

    • value:被装饰的方法或属性的值(即原始函数)。
    • writable:是否可修改(布尔值)。
    • enumerable:是否可枚举(布尔值)。
    • configurable:是否可配置(布尔值)。

函数实现原理详解

log 装饰器的核心逻辑是替换原始方法,在执行前后添加日志:


    function log(target, name, descriptor) {
      // 1. 保存原始方法的引用
      const original = descriptor.value;

      // 2. 修改 descriptor.value 为新函数
      descriptor.value = function(...args) {
        // 3. 执行前置逻辑(打印入参)
        console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
        
        // 4. 执行原始方法并保存结果
        const result = original.apply(this, args);
        
        // 5. 执行后置逻辑(打印返回值)
        console.log(`方法 ${name} 返回:${result}`);
        
        // 6. 返回原始结果
        return result;
      };

      // 7. 返回修改后的描述符
      return descriptor;
    }

为什么要这样实现?

这种写法的关键点在于:

  1. 不改变原始方法的核心逻辑:通过包装原始方法,在不修改其代码的前提下添加新功能。

  2. 保留上下文(this

    • 使用 original.apply(this, args) 确保原始方法在调用时的 this 指向不变。
    • 若直接调用 original(args),可能导致 this 指向全局对象(非严格模式)或 undefined(严格模式)。
  3. 支持任意参数

    • 使用剩余参数 ...args 收集所有传入参数。
    • 使用 JSON.stringify(args) 将参数序列化为字符串(需注意无法处理函数或 undefined 类型的参数)。
  4. 遵循装饰器规范

    • 装饰器必须返回一个描述符对象(或新类)。
    • 通过修改 descriptor.value 替换原始方法。

应用示例

使用该装饰器的类方法会自动添加日志功能:


    class Calculator {
      @log
      add(a, b) {
        return a + b;
      }
    }

    const calc = new Calculator();
    calc.add(3, 4);

    // 输出:
    // 调用 add 方法,参数:[3,4]
    // 方法 add 返回:7

注意事项

  1. 参数序列化限制

    • JSON.stringify 无法处理函数或 undefined 参数,可能导致日志不完整。
    • 改进方案:使用 args.map(arg => String(arg)).join(', ') 或自定义序列化函数。
  2. 异步方法处理

    • 若原始方法返回 Promise,需使用 await 等待结果:

      
          descriptor.value = async function(...args) {
            // ...
            const result = await original.apply(this, args);
            // ...
          };
      
  3. 兼容性

    • 装饰器语法需 Babel 或 TypeScript 支持。

    • 确保项目配置中启用了装饰器(如 @babel/plugin-proposal-decorators)。

通过这种方式,装饰器实现了 ** 横切关注点(Cross-cutting Concerns)** 的分离,让日志、权限等功能与核心业务逻辑解耦。

下面介绍装饰器常见的应用场景:

1. 日志记录

装饰器能够在方法执行的前后添加日志,这样可以对函数的调用情况进行监控。

function log(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
    const result = original.apply(this, args);
    console.log(`方法 ${name} 返回:${result}`);
    return result;
  };
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

// 使用示例
const calc = new Calculator();
calc.add(3, 4); // 控制台会输出调用信息和返回结果

2. 权限验证

可以在执行方法前对用户权限进行检查,防止未授权的访问。


    function auth(requiredRole) {
      return function(target, name, descriptor) {
        const original = descriptor.value;
        descriptor.value = function(...args) {
          if (this.userRole !== requiredRole) {
            throw new Error("权限不足");
          }
          return original.apply(this, args);
        };
        return descriptor;
      };
    }

    class AdminPanel {
      userRole = "admin";

      @auth("admin")
      deleteUser() {
        return "用户已删除";
      }
    }

3. 性能分析

装饰器能够对函数的执行时间进行测量,有助于性能优化。


    function benchmark(target, name, descriptor) {
      const original = descriptor.value;
      descriptor.value = async function(...args) {
        const start = performance.now();
        const result = await original.apply(this, args);
        const end = performance.now();
        console.log(`${name} 方法执行耗时:${end - start}ms`);
        return result;
      };
      return descriptor;
    }

    class DataService {
      @benchmark
      async fetchData() {
        await new Promise(resolve => setTimeout(resolve, 1000));
        return { data: "大量数据" };
      }
    }

4. 自动绑定

在 React 等框架中,装饰器可以解决方法上下文丢失的问题。


    function autobind(target, name, descriptor) {
      const original = descriptor.value;
      return {
        configurable: true,
        get() {
          const bound = original.bind(this);
          Object.defineProperty(this, name, {
            value: bound,
            configurable: true,
            writable: true
          });
          return bound;
        }
      };
    }

    class Component {
      constructor() {
        this.state = { count: 0 };
      }

      @autobind
      increment() {
        this.state.count++;
      }
    }

5. 单例模式实现

装饰器可以确保一个类仅有一个实例。


    function singleton(constructor) {
      let instance;
      return function(...args) {
        if (!instance) {
          instance = new constructor(...args);
        }
        return instance;
      };
    }

    @singleton
    class AppState {
      constructor() {
        this.data = {};
      }
    }

    const state1 = new AppState();
    const state2 = new AppState();
    console.log(state1 === state2); // 输出 true

6. 类型检查

在运行时对函数参数的类型进行验证。


    function validateTypes(target, name, descriptor) {
      const original = descriptor.value;
      descriptor.value = function(...args) {
        const paramTypes = Reflect.getMetadata("design:paramtypes", target, name);
        args.forEach((arg, i) => {
          if (arg && paramTypes[i] && !(arg instanceof paramTypes[i])) {
            throw new TypeError(`参数 ${i} 类型错误,期望 ${paramTypes[i].name}`);
          }
        });
        return original.apply(this, args);
      };
      return descriptor;
    }

    class MathUtils {
      @validateTypes
      add(a: number, b: number) {
        return a + b;
      }
    }

7. 缓存机制

对函数的计算结果进行缓存,避免重复计算。


    function memoize(target, name, descriptor) {
      const original = descriptor.value;
      const cache = new Map();
      descriptor.value = function(...args) {
        const key = args.toString();
        if (cache.has(key)) {
          return cache.get(key);
        }
        const result = original.apply(this, args);
        cache.set(key, result);
        return result;
      };
      return descriptor;
    }

    class Fibonacci {
      @memoize
      calculate(n) {
        return n <= 1 ? n : this.calculate(n - 1) + this.calculate(n - 2);
      }
    }

装饰器使用注意要点

  • 要启用装饰器语法,需要在 Babel 或者 TypeScript 中进行配置。

  • 装饰器的执行顺序是从下往上,例如:

    
        @a
        @b
        method() {} // 先执行 b,再执行 a
    
    
  • 装饰器可以返回一个新的类或者修改原有的描述符(descriptor)。

装饰器的主要价值在于它遵循了开放 - 封闭原则,即对扩展开放,对修改封闭。它能让代码变得更加简洁,同时增强代码的可复用性。

🔥Vue3生态利器:高效开发必备工具库实战

Vue3生态利器:高效开发必备工具库实战

解锁Vue3高效开发的终极武器,掌握提升300%开发效率的工具链秘籍

一、为什么需要工具库?生态的力量

在Vue3开发中,合理使用工具库可减少70%重复代码。根据2025年开发者调查报告:

pie
    title 开发者使用工具库比例
    "VueUse" : 68
    "Vite插件" : 72
    "组件库" : 85
    "可视化库" : 55
    "调试工具" : 90

工具链带来的核心价值

  • 开发效率:减少重复工作,聚焦业务逻辑
  • 代码质量:经过社区验证的可靠解决方案
  • 性能优化:内置最佳实践避免性能陷阱
  • 维护成本:统一解决方案降低团队协作成本

二、VueUse:组合式API的瑞士军刀

1. 核心函数实战

npm install @vueuse/core
鼠标跟踪器
<script setup>
import { useMouse } from '@vueuse/core'

const { x, y } = useMouse()
</script>

<template>
  <div>鼠标位置:{{ x }}, {{ y }}</div>
</template>
本地存储同步
<script setup>
import { useStorage } from '@vueuse/core'

// 自动同步到localStorage
const user = useStorage('user', {
  name: '张三',
  age: 28,
  preferences: {
    theme: 'dark',
    notifications: true
  }
})

const changeTheme = () => {
  user.value.preferences.theme = 
    user.value.preferences.theme === 'dark' ? 'light' : 'dark'
}
</script>

2. 高级应用:设备传感器

<script setup>
import { 
  useDeviceOrientation,
  useBattery,
  useNetwork
} from '@vueuse/core'

const { alpha, beta, gamma } = useDeviceOrientation()
const { isSupported, level, charging } = useBattery()
const { isOnline, type } = useNetwork()

console.log(`电池电量:${level.value * 100}%`)
</script>

三、Vite插件宝库:开发体验飞跃

1. 必备插件清单

npm install -D unplugin-auto-import unplugin-vue-components vite-plugin-inspect

2. 自动化导入配置

// vite.config.js
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'

export default {
  plugins: [
    AutoImport({
      imports: [
        'vue',
        'vue-router',
        '@vueuse/core',
        'pinia'
      ],
      dts: 'types/auto-imports.d.ts' // 自动生成类型声明
    }),
    Components({
      dirs: ['src/components'], // 组件目录
      dts: 'types/components.d.ts' // 组件类型声明
    })
  ]
}

使用效果:

<script setup>
// 无需手动导入ref, computed等API
const count = ref(0)
const double = computed(() => count.value * 2)

// 组件自动注册
const increment = () => count.value++
</script>

<template>
  <!-- 自动引入的组件 -->
  <MyButton @click="increment">
    点击: {{ count }} ({{ double }})
  </MyButton>
</template>

3. 可视化调试插件

// vite.config.js
import Inspect from 'vite-plugin-inspect'

export default {
  plugins: [
    Inspect() // 启用Vite模块分析
  ]
}

启动后访问:http://localhost:5173/__inspect/ 查看模块关系图

四、组件库深度集成技巧

1. Element Plus按需加载

npm install element-plus @element-plus/icons-vue
// plugins/element.js
import { ElButton, ElInput } from 'element-plus'

export default (app) => {
  app.use(ElButton)
  app.use(ElInput)
}

2. Naive UI主题定制

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag.startsWith('n-')
        }
      }
    })
  ]
})

主题覆盖:

// src/naive-theme.ts
import { createTheme } from 'naive-ui'

export default createTheme({
  common: {
    primaryColor: '#FF6B6B',
    primaryColorHover: '#FF8E8E',
    borderRadius: '8px'
  }
})

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import naive from 'naive-ui'
import naiveTheme from './naive-theme'

const app = createApp(App)
app.use(naive, {
  theme: naiveTheme
})
app.mount('#app')

五、数据可视化实战:ECharts 5 + Vue3

1. 优雅集成方案

npm install echarts vue-echarts

2. 按需加载组件

<script setup>
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { PieChart } from 'echarts/charts'
import { TitleComponent, TooltipComponent } from 'echarts/components'
import VChart from 'vue-echarts'

use([CanvasRenderer, PieChart, TitleComponent, TooltipComponent])

const options = ref({
  title: { text: '产品销量分布' },
  tooltip: { trigger: 'item' },
  series: [{
    name: '销量',
    type: 'pie',
    radius: '50%',
    data: [
      { value: 1048, name: '手机' },
      { value: 735, name: '平板' },
      { value: 580, name: '笔记本' },
      { value: 300, name: '配件' }
    ]
  }]
})
</script>

<template>
  <v-chart :option="options" autoresize />
</template>

3. 性能优化技巧

// 大数据量场景使用增量渲染
const bigDataOptions = {
  dataset: {
    source: [...Array(10000).keys()].map(i => [i, Math.sin(i / 100)])
  },
  series: {
    type: 'line',
    progressive: 1000, // 每次渲染1000个点
    progressiveThreshold: 5000 // 超过5000点启用分批渲染
  }
}

六、调试利器:Vue DevTools 7.0

1. 新特性解析

graph LR
A[时间旅行调试] --> B[状态回滚]
C[组件性能分析] --> D[渲染耗时统计]
E[Pinia集成] --> F[状态快照对比]
G[路由调试] --> H[路由历史追踪]

2. 高级调试技巧

// 组件性能标记
import { markComponentPerformance } from './perf-utils'

export default {
  setup() {
    markComponentPerformance('UserProfile', 'init')
    // 组件初始化代码...
    markComponentPerformance('UserProfile', 'init-end')
  }
}

在DevTools中查看组件各阶段耗时:

阶段 耗时(ms) 占比
setup 12.4 35%
render 18.2 52%
mount 4.3 13%

七、动画引擎:GSAP专业级动画

1. 基础集成

npm install gsap

2. 滚动驱动动画

<script setup>
import { ref, onMounted } from 'vue'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'

gsap.registerPlugin(ScrollTrigger)

const cardRefs = ref([])

onMounted(() => {
  cardRefs.value.forEach((card, index) => {
    gsap.from(card, {
      opacity: 0,
      y: 50,
      duration: 0.5,
      scrollTrigger: {
        trigger: card,
        start: 'top 80%',
        toggleActions: 'play none none none'
      },
      delay: index * 0.1
    })
  })
})
</script>

<template>
  <div 
    v-for="(item, i) in 5" 
    :key="i"
    ref="el => cardRefs[i] = el"
    class="card"
  >
    内容卡片 {{ i+1 }}
  </div>
</template>

3. SVG路径动画

<script setup>
import { onMounted } from 'vue'
import gsap from 'gsap'

const path = ref(null)

onMounted(() => {
  const pathElement = path.value
  const length = pathElement.getTotalLength()
  
  // 初始化路径样式
  gsap.set(pathElement, {
    strokeDasharray: length,
    strokeDashoffset: length
  })
  
  // 创建绘制动画
  gsap.to(pathElement, {
    strokeDashoffset: 0,
    duration: 2,
    ease: 'power1.inOut'
  })
})
</script>

<template>
  <svg width="400" height="200">
    <path 
      ref="path"
      d="M10 80 Q 95 10 180 80 T 350 80" 
      stroke="#FF6B6B" 
      fill="none"
      stroke-width="3"
    />
  </svg>
</template>

八、其他实用工具

1. 表单验证:VeeValidate 4

npm install vee-validate
<script setup>
import { useForm, useField } from 'vee-validate'
import * as yup from 'yup'

// 验证规则
const schema = yup.object({
  email: yup.string().email().required(),
  password: yup.string().min(6).required()
})

const { handleSubmit } = useForm({
  validationSchema: schema
})

const { value: email, errorMessage: emailError } = useField('email')
const { value: password, errorMessage: passwordError } = useField('password')

const onSubmit = handleSubmit(values => {
  console.log('提交数据:', values)
})
</script>

<template>
  <form @submit="onSubmit">
    <input v-model="email" placeholder="邮箱">
    <span>{{ emailError }}</span>
    
    <input v-model="password" type="password" placeholder="密码">
    <span>{{ passwordError }}</span>
    
    <button type="submit">登录</button>
  </form>
</template>

2. 国际化:Vue I18n

// plugins/i18n.js
import { createI18n } from 'vue-i18n'

const messages = {
  en: {
    welcome: 'Welcome',
    user: {
      profile: 'User Profile'
    }
  },
  zh: {
    welcome: '欢迎',
    user: {
      profile: '用户资料'
    }
  }
}

export default createI18n({
  locale: navigator.language.split('-')[0] || 'en',
  fallbackLocale: 'en',
  messages
})

3. 请求封装:Axios + Vue3

// utils/http.ts
import axios from 'axios'
import type { InternalAxiosRequestConfig } from 'axios'

const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE,
  timeout: 10000
})

// 请求拦截
service.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  const token = localStorage.getItem('token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

// 响应拦截
service.interceptors.response.use(
  response => response.data,
  error => {
    if (error.response?.status === 401) {
      // 处理未授权
    }
    return Promise.reject(error)
  }
)

export default service

九、工具链最佳组合方案

场景 推荐工具 优势
状态管理 Pinia 简洁API + 完整TS支持
路由管理 Vue Router 4 动态路由 + 过渡动画
UI组件 Element Plus / Naive UI 企业级组件 + 主题定制
工具函数 VueUse 200+ 开箱即用函数
构建工具 Vite + 插件生态 闪电般开发体验
数据可视化 ECharts + vue-echarts 专业图表 + 按需加载
动画效果 GSAP 专业级动画控制
表单验证 VeeValidate + Yup 声明式验证规则
HTTP请求 Axios 拦截器 + 类型安全

十、总结:构建高效开发工作流

通过本文,我们系统掌握了:

  1. VueUse核心函数:提升日常开发效率200%
  2. Vite插件配置:实现零配置自动化开发环境
  3. 组件库深度集成:企业级UI方案定制技巧
  4. 数据可视化方案:ECharts专业图表集成
  5. 高级调试技术:DevTools深度性能分析
  6. 专业动画实现:GSAP复杂动画控制
  7. 辅助工具链:表单验证、国际化等解决方案

完整工作流示例

graph LR
A[项目创建 vite] --> B[组件库集成]
B --> C[状态管理 Pinia]
C --> D[工具函数 VueUse]
D --> E[路由配置]
E --> F[HTTP请求封装]
F --> G[开发调试]
G --> H[测试验证]
H --> I[构建优化]
I --> J[部署上线]

最终效果

  • 开发效率提升300%
  • 代码量减少70%
  • 性能提升50%
  • 维护成本降低60%

掌握这些工具库,你将拥有Vue3开发的"超能力"!如果本文对你有帮助,欢迎点赞收藏,也欢迎在评论区分享你的工具链组合方案!


附录:工具库资源大全

类别 工具 官方链接
工具函数 VueUse vueuse.org
构建工具 Vite vitejs.dev
组件库 Element Plus element-plus.org
组件库 Naive UI www.naiveui.com
数据可视化 ECharts echarts.apache.org
调试工具 Vue DevTools devtools.vuejs.org
动画库 GSAP greensock.com/gsap
表单验证 VeeValidate vee-validate.logaretm.com
国际化 Vue I18n vue-i18n.intlify.dev

贝塞尔曲线:让计算机画出丝滑曲线的魔法

想象一下,如果你让计算机画一条曲线,它可能会像个刚学画画的孩子,画出的线条要么僵硬得像铁丝,要么歪歪扭扭如同毛毛虫。但有了贝塞尔曲线,计算机突然就像掌握了绘画技巧的艺术家,能画出从字体轮廓到动画路径的各种丝滑线条。今天我们就来揭开这个让计算机变身为 "曲线大师" 的秘密。

从点到线:贝塞尔曲线的底层逻辑

贝塞尔曲线的核心原理其实很简单:用几个控制点 "拉扯" 出一条平滑曲线。就像你用手指捏住绳子的几个点,轻轻一拉就能得到自然的弧线。这背后藏着一种叫 "插值" 的数学思想 —— 通过已知的点,算出中间该有的样子。

最基础的是一次贝塞尔曲线,说穿了就是直线。取两个点,比如 (0,0) 和 (100,100),连接它们的线段就是一次贝塞尔曲线。这时候你可能会说:"这有什么了不起?" 别急,精彩的在后面。

当我们增加到三个点时,就得到了二次贝塞尔曲线。想象中间那个点是个 "磁铁",它会把直线段往自己这边吸,形成一条优美的抛物线。三个点分工明确:起点和终点固定曲线的两端,中间的控制点则决定了曲线的弯曲程度 —— 离直线越远,曲线弯得越厉害,就像有人在中间用力拽了一把。

让曲线更灵活:高阶贝塞尔曲线

三次贝塞尔曲线是应用最广泛的,它有四个控制点:起点、终点和两个中间控制点。这两个中间控制点就像两个方向舵,能让曲线做出更复杂的转弯。你可以把它想象成一条被两个人从不同方向拉扯的绳子,最终形成的形状取决于两人用力的方向和大小。

更高阶的贝塞尔曲线原理类似,只是增加了更多控制点。但有趣的是,在实际应用中,我们很少用到五阶以上的曲线。这就像做菜,加太多调料反而会破坏原本的味道,三个到四个控制点已经能满足绝大多数设计需求了。

数学背后的小秘密

贝塞尔曲线的数学表达其实是一系列多项式的组合,但我们可以用更形象的方式理解:曲线上每个点的位置,都是由所有控制点按一定比例 "混合" 而成的

以三次贝塞尔曲线为例,想象有一辆小车从起点开往终点,行驶过程中会受到两个中间控制点的 "引力" 影响。刚出发时,起点的引力最大,小车几乎直线冲向第一个控制点;随着前进,第一个控制点的引力逐渐减弱,第二个控制点的引力逐渐增强;快到终点时,终点的引力变成主导,小车会从第二个控制点的方向平滑地驶入终点。整个过程就像一场精心编排的舞蹈,每个控制点都在特定时刻发挥着恰到好处的作用。

这种 "混合" 比例遵循着类似二项式展开的规律,每个控制点的影响力随曲线位置呈现平滑的增减变化,这正是曲线能保持连续光滑的关键。

用代码画出贝塞尔曲线

让我们用 JavaScript 来实践一下,通过 Canvas 绘制一条三次贝塞尔曲线:

// 获取画布元素
const canvas = document.getElementById('bezierCanvas');
const ctx = canvas.getContext('2d');
// 设置画布尺寸
canvas.width = 600;
canvas.height = 400;
// 定义四个控制点
const startPoint = { x: 50, y: 200 };         // 起点
const controlPoint1 = { x: 200, y: 50 };      // 第一个控制点
const controlPoint2 = { x: 400, y: 350 };     // 第二个控制点
const endPoint = { x: 550, y: 200 };          // 终点
// 绘制辅助线和控制点(帮助理解)
ctx.strokeStyle = '#cccccc';
ctx.beginPath();
ctx.moveTo(startPoint.x, startPoint.y);
ctx.lineTo(controlPoint1.x, controlPoint1.y);
ctx.lineTo(controlPoint2.x, controlPoint2.y);
ctx.lineTo(endPoint.x, endPoint.y);
ctx.stroke();
// 绘制控制点标记
[startPoint, controlPoint1, controlPoint2, endPoint].forEach((point, index) => {
    ctx.fillStyle = index === 0 || index === 3 ? 'green' : 'red';
    ctx.beginPath();
    ctx.arc(point.x, point.y, 6, 0, Math.PI * 2);
    ctx.fill();
});
// 绘制贝塞尔曲线(这才是主角!)
ctx.strokeStyle = '#3366ff';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(startPoint.x, startPoint.y);
// 核心API:绘制三次贝塞尔曲线
ctx.bezierCurveTo(
    controlPoint1.x, controlPoint1.y,
    controlPoint2.x, controlPoint2.y,
    endPoint.x, endPoint.y
);
ctx.stroke();

运行这段代码,你会看到一条蓝色的平滑曲线,旁边还有灰色的辅助线连接着四个控制点。绿色的是起点和终点,红色的是中间控制点。试着修改控制点的坐标值,你会发现曲线的形状会随之发生奇妙的变化 —— 这就是贝塞尔曲线的魅力所在。

动画中的贝塞尔魔法

在动画领域,贝塞尔曲线更是不可或缺的工具。当你看到一个物体先加速后减速的自然运动,或者一个元素平滑地转弯绕行时,很可能就是贝塞尔曲线在背后默默工作。

比如下面这个简单的动画示例,让一个小球沿着贝塞尔曲线运动:

const ball = document.getElementById('ball');
let time = 0;
function updateBallPosition() {
    // 计算当前时间在动画中的比例(0到1之间)
    time += 0.01;
    if (time > 1) time = 0;
    
    const t = time;
    // 三次贝塞尔曲线的位置计算公式(简化版)
    const cx = 3 * (1 - t) * (1 - t) * t * controlPoint1.x 
             + 3 * (1 - t) * t * t * controlPoint2.x 
             + t * t * t * endPoint.x 
             + (1 - t) * (1 - t) * (1 - t) * startPoint.x;
             
    const cy = 3 * (1 - t) * (1 - t) * t * controlPoint1.y 
             + 3 * (1 - t) * t * t * controlPoint2.y 
             + t * t * t * endPoint.y 
             + (1 - t) * (1 - t) * (1 - t) * startPoint.y;
             
    // 更新小球位置
    ball.style.left = `${cx}px`;
    ball.style.top = `${cy}px`;
    
    requestAnimationFrame(updateBallPosition);
}
// 开始动画
updateBallPosition();

这段代码通过不断计算小球在贝塞尔曲线上的位置,让它看起来像是沿着一条平滑的路径运动。你可以调整控制点的位置,让小球做出各种有趣的轨迹 —— 直线、弧线、S 形曲线,甚至是看似不可能的急转弯。

无处不在的贝塞尔曲线

贝塞尔曲线的应用远不止于此:从你手机上的图标设计到汽车的流线型车身,从字体的优美轮廓到地图上的路线规划,都能看到它的身影。每当你在屏幕上画出一条平滑的线条,或者看到一个自然流畅的动画时,不妨想一想:这背后是不是有贝塞尔曲线在施展魔法?

下次当你再看到那些令人赞叹的数字设计时,或许会对它们多一份理解和欣赏 —— 因为你知道,那些看似复杂的曲线背后,其实是几个控制点和一段精妙的数学逻辑共同谱写的优雅篇章。

vue3源码解析:diff算法之processElement函数分析

上文我们分析到了 setupRenderEffect 函数的实现。至此,我们已经完成了组件级别的渲染流程的分析。setupRenderEffect 函数通过调用 patch 函数来处理组件的更新。 而 patch 函数会根据虚拟节点的类型进行不同处理。对于组件节点会递归处理其子组件,对于普通元素节点则会调用 processElement 函数进行处理。

这种递归调用形成了一个树形的渲染结构:从根组件开始,通过 patch 函数逐层向下处理,直到遇到普通元素节点。此时就从组件级别的渲染转换为 DOM 元素级别的渲染,通过 processElement 函数实现。

本文我们将深入分析 processElement 函数的实现,看看 Vue 是如何将虚拟 DOM 节点转换为真实的 DOM 元素(DOM 级别的 diff)。

函数定义

const processElement = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean,
) => {
  // 处理特殊的命名空间
  if (n2.type === 'svg') {
    namespace = 'svg'
  } else if (n2.type === 'math') {
    namespace = 'mathml'
  }

  // 根据是否存在旧节点判断是挂载还是更新
  if (n1 == null) {
    mountElement(
      n2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
      optimized,
    )
  } else {
    patchElement(
      n1,
      n2,
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
      optimized,
    )
  }
}

mountElement详细分析

让我们先看看mountElement函数的完整实现:

const mountElement = (
  vnode: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean,
) => {
  let el: RendererElement
  const { props, shapeFlag, transition, dirs } = vnode

  // 1. 创建DOM元素
  el = vnode.el = hostCreateElement(
    vnode.type as string,
    namespace,
    props && props.is,
    props,
  )

  // 2. 处理子节点
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 文本子节点直接设置文本内容
    hostSetElementText(el, vnode.children as string)
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    // 数组子节点需要递归挂载
    mountChildren(
      vnode.children as VNodeArrayChildren,
      el,
      null,
      parentComponent,
      parentSuspense,
      resolveChildrenNamespace(vnode, namespace),
      slotScopeIds,
      optimized,
    )
  }

  // 3. 处理指令
  if (dirs) {
    invokeDirectiveHook(vnode, null, parentComponent, 'created')
  }

  // 4. 设置scopeId
  setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)

  // 5. 处理props
  if (props) {
    // 先处理除value之外的属性
    for (const key in props) {
      if (key !== 'value' && !isReservedProp(key)) {
        hostPatchProp(el, key, null, props[key], namespace, parentComponent)
      }
    }
    // 特殊处理value属性
    if ('value' in props) {
      hostPatchProp(el, 'value', null, props.value, namespace)
    }
  }

  // 6. 处理过渡动画
  if (transition && !transition.persisted) {
    transition.beforeEnter(el)
  }

  // 7. 插入DOM
  hostInsert(el, container, anchor)
}

整体流程

  1. 创建DOM元素
  2. 处理子节点(文本或数组)
  3. 处理指令
  4. 设置作用域ID
  5. 处理属性
  6. 插入到DOM中

详细分析

1. 创建DOM元素
el = vnode.el = hostCreateElement(
  vnode.type as string,
  namespace,
  props && props.is,
  props,
)

这一步通过平台特定的API创建真实DOM元素。hostCreateElement是一个适配器函数,在不同平台(如浏览器、服务器等)有不同的实现。它的参数包括:

  • type: 元素类型(如'div'、'span'等)
  • namespace: 命名空间(用于svg等特殊元素)
  • is: Web Components的支持
  • props: 元素的属性集合
2. 处理子节点
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  mountChildren(
    vnode.children as VNodeArrayChildren,
    el,
    null,
    parentComponent,
    parentSuspense,
    resolveChildrenNamespace(vnode, namespace),
    slotScopeIds,
    optimized,
  )
}

子节点处理分为两种情况:

  • 文本子节点:直接设置元素的文本内容,这是最简单的情况
  • 数组子节点:需要递归处理每个子节点,这可能会触发新的渲染周期
3. 属性处理
if (props) {
  for (const key in props) {
    if (key !== 'value' && !isReservedProp(key)) {
      hostPatchProp(el, key, null, props[key], namespace, parentComponent)
    }
  }
  if ('value' in props) {
    hostPatchProp(el, 'value', null, props.value, namespace)
  }
}

属性处理的特点:

  • value属性单独处理,确保正确的设置顺序
  • 使用hostPatchProp适配不同平台的属性设置方式
  • 过滤保留属性,避免处理特殊属性

patchElement详细分析

接下来看看patchElement函数的完整实现:

const patchElement = (
  n1: VNode,
  n2: VNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean,
) => {
  // 1. 复用DOM元素
  const el = (n2.el = n1.el!)
  
  // 2. 获取新旧props和其他需要的信息
  let { patchFlag, dynamicChildren, dirs } = n2
  const oldProps = n1.props || EMPTY_OBJ
  const newProps = n2.props || EMPTY_OBJ

  // 3. 调用beforeUpdate钩子
  if (dirs) {
    invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
  }

  // 4. 处理子节点更新
  if (dynamicChildren) {
    // 优化模式:只更新动态子节点
    patchBlockChildren(
      n1.dynamicChildren!,
      dynamicChildren,
      el,
      parentComponent,
      parentSuspense,
      resolveChildrenNamespace(n2, namespace),
      slotScopeIds,
    )
  } else if (!optimized) {
    // 完整diff
    patchChildren(
      n1,
      n2,
      el,
      null,
      parentComponent,
      parentSuspense,
      resolveChildrenNamespace(n2, namespace),
      slotScopeIds,
      false,
    )
  }

  // 5. 根据patchFlag优化更新props
  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.FULL_PROPS) {
      // 完整props更新
      patchProps(el, oldProps, newProps, parentComponent, namespace)
    } else {
      // 按需更新class
      if (patchFlag & PatchFlags.CLASS) {
        if (oldProps.class !== newProps.class) {
          hostPatchProp(el, 'class', null, newProps.class, namespace)
        }
      }
      // 按需更新style
      if (patchFlag & PatchFlags.STYLE) {
        hostPatchProp(el, 'style', oldProps.style, newProps.style, namespace)
      }
      // 按需更新动态props
      if (patchFlag & PatchFlags.PROPS) {
        const propsToUpdate = n2.dynamicProps!
        for (let i = 0; i < propsToUpdate.length; i++) {
          const key = propsToUpdate[i]
          const prev = oldProps[key]
          const next = newProps[key]
          if (next !== prev || key === 'value') {
            hostPatchProp(el, key, prev, next, namespace, parentComponent)
          }
        }
      }
    }
  }

  // 6. 调用updated钩子
  if ((dirs)) {
    queuePostRenderEffect(() => {
      dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
    }, parentSuspense)
  }
}

整体流程

  1. 复用并更新DOM元素引用
  2. 处理子节点更新
  3. 处理属性更新
  4. 触发更新后的钩子函数

详细分析

1. 复用DOM元素
const el = (n2.el = n1.el!)

这是Vue更新策略的核心之一:尽可能复用已有的DOM元素。这样可以:

  • 避免不必要的DOM创建和销毁
  • 保持元素的状态(如focus、scroll位置等)
  • 提高更新性能
2. 子节点更新策略
if (dynamicChildren) {
  patchBlockChildren(
    n1.dynamicChildren!,
    dynamicChildren,
    el,
    parentComponent,
    parentSuspense,
    resolveChildrenNamespace(n2, namespace),
    slotScopeIds,
  )
} else if (!optimized) {
  patchChildren(
    n1,
    n2,
    el,
    null,
    parentComponent,
    parentSuspense,
    resolveChildrenNamespace(n2, namespace),
    slotScopeIds,
    false,
  )
}

Vue提供了两种子节点更新策略:

  1. Block树更新(优化模式)

    • 只更新动态子节点
    • 跳过静态内容
    • 性能更好,适用于编译优化的情况
  2. 完整的子节点diff

    • 对所有子节点进行比较
    • 使用传统的diff算法
    • 更通用,但性能较差
3. 属性更新优化
if (patchFlag > 0) {
  if (patchFlag & PatchFlags.FULL_PROPS) {
    patchProps(el, oldProps, newProps, parentComponent, namespace)
  } else {
    // 按需更新class
    if (patchFlag & PatchFlags.CLASS) {
      if (oldProps.class !== newProps.class) {
        hostPatchProp(el, 'class', null, newProps.class, namespace)
      }
    }
    // 按需更新style
    if (patchFlag & PatchFlags.STYLE) {
      hostPatchProp(el, 'style', oldProps.style, newProps.style, namespace)
    }
    // 按需更新动态props
    if (patchFlag & PatchFlags.PROPS) {
      const propsToUpdate = n2.dynamicProps!
      for (let i = 0; i < propsToUpdate.length; i++) {
        const key = propsToUpdate[i]
        const prev = oldProps[key]
        const next = newProps[key]
        if (next !== prev || key === 'value') {
          hostPatchProp(el, key, prev, next, namespace, parentComponent)
        }
      }
    }
  }
}

Vue的属性更新使用了标记优化(patchFlag):

  1. 完整props更新

    • 当props可能有动态键时
    • 需要完整的props比较
  2. 优化更新

    • CLASS:只更新class属性
    • STYLE:只更新style属性
    • PROPS:只更新动态绑定的属性

这种优化机制使得Vue能够:

  • 跳过静态内容的更新
  • 只关注动态绑定的属性
  • 根据不同的更新类型采用不同的处理策略

总结

通过分析processElement及其相关函数的实现,我们可以看到Vue在处理DOM元素时的几个关键特点:

  1. 分情况处理

    • 首次创建时调用mountElement
    • 更新已有元素时调用patchElement
  2. 优化策略

    • 使用patchFlag标记动态内容
    • 针对不同类型的更新采用不同的优化策略
    • 复用已有DOM节点,只更新必要的部分
  3. 特殊处理

    • 对svg和mathml提供特殊的命名空间支持
    • value属性的特殊处理
    • 指令的处理

在下一篇文章中,我们将深入分析子节点的更新过程,包括patchChildrenpatchBlockChildren的实现。

❌