【Vue 3 Diff算法解析:从排队老头到最长递增子序列(LIS)】

Vue 3 Diff算法解析:从排队老头到最长递增子序列(LIS)

前言

Vue 3的diff算法是前端框架中的一颗明珠,它通过巧妙的最长递增子序列(LIS)算法,将DOM操作的复杂度从O(n²)降低到O(n log n)。但这个算法对很多开发者来说就像一本天书,充满了抽象的概念和复杂的逻辑。今天我们用通俗易懂的比喻来揭开它的神秘面纱。

一、核心理念:老头排队的智慧

1.1 问题场景

想象一下,有一群老头要按年龄从小到大重新排队。原来的队伍是:[80, 60, 70, 90, 65],现在要变成:[60, 65, 70, 80, 90]

如果我们傻乎乎地让所有人都重新排队,那就太累了。聪明的做法是:找出那些已经站对位置的老头,让他们不动,只移动其他人

1.2 Vue的智慧

Vue 3的diff算法就是这个思路:

  • 旧队伍 = 旧的虚拟DOM节点列表
  • 新队伍 = 新的虚拟DOM节点列表
  • 找出不需要移动的人 = 找出最长递增子序列
  • 只移动其他人 = 只操作需要变化的DOM节点

二、最长递增子序列(LIS)的核心概念

2.1 什么是子序列?

子序列的定义:从原序列中选出一些元素,保持它们在原序列中的相对顺序不变。

比喻理解
想象一排老头站成一队:[张三(80), 李四(60), 王五(70), 赵六(90), 钱七(65)]

子序列就像从这队人中挑选一些人出来,但必须保持他们原来的前后顺序:

  • [张三(80), 王五(70), 赵六(90)] ✅ 是子序列(保持原顺序)
  • [李四(60), 张三(80), 王五(70)] ❌ 不是子序列(顺序乱了)

2.2 什么是递增?

递增的定义:序列中后面的元素总是大于前面的元素。

严格递增 vs 非严格递增

  • 严格递增a[i] < a[i+1],如 [1, 3, 5, 7]
  • 非严格递增a[i] ≤ a[i+1],如 [1, 3, 3, 7]

老头排队比喻

  • 严格递增:每个老头的年龄都比前面的大
  • 非严格递增:允许年龄相同的老头相邻站立

2.3 最长递增子序列的完整定义

LIS定义:在给定序列中,找到最长的子序列,使得这个子序列是严格递增的。

实例分析

原序列:[80, 60, 70, 90, 65, 75, 85]
所有递增子序列:
- [60, 70, 90] 长度=3
- [60, 65, 75, 85] 长度=4 ← 这是最长的
- [60, 70, 75, 85] 长度=4 ← 这也是最长的

老头排队比喻
从一群乱站的老头中,找出最多能有几个老头已经按年龄正确排好了队。

2.4 有序 vs 无序的深度理解

2.4.1 全局有序 vs 局部有序

全局有序:整个序列都是有序的

[60, 65, 70, 75, 80] ← 完全有序

局部有序:序列中存在有序的片段

[80, 60, 70, 90, 65] 
     ↑   ↑   ↑
   [60, 70, 90] 这部分是有序的

比喻

  • 全局有序:整个队伍都按年龄排好了
  • 局部有序:队伍中有一些老头恰好站对了位置
2.4.2 为什么要找局部有序?

在Vue的diff算法中:

  • 全局有序:新旧节点列表完全一致,不需要任何操作
  • 局部有序:找出已经正确的部分,只调整错误的部分

效率对比

// 场景1:全局无序,需要移动所有节点
旧:[A, B, C, D, E]
新:[E, D, C, B, A]
LIS[] (),需要移动所有节点

// 场景2:局部有序,只需要移动部分节点  
旧:[A, B, C, D, E]
新:[B, D, E, A, C]
LIS[B, D, E],只需要移动AC
2.4.3 相对顺序的重要性

为什么必须保持相对顺序?

原序列:[张三(80), 李四(60), 王五(70), 赵六(90)]

如果不保持相对顺序:

  • 我们可能选择:[李四(60), 张三(80), 王五(70)]
  • 但这违反了"子序列"的定义,因为张三原本在李四前面

保持相对顺序的意义:

  • DOM节点的位置关系:节点在DOM树中的位置有语义
  • 用户体验:保持稳定的视觉锚点
  • 动画连贯性:避免元素跳跃式移动

三、数据结构的巧妙运用

3.1 数组:存储排队结果

在我们的算法中,主要用到了几个关键数组:

let result = [];  // 就像一个"最佳位置记录本"
let p = [];       // 就像一个"前任记录本"

数组的作用比喻

  • result数组就像一个**“最佳位置记录本”**,记录着当前找到的最优排队方案
  • p数组就像一个**“前任记录本”**,每个人都记录着自己前面应该站谁

3.2 栈的概念:回溯机制

虽然代码中没有显式使用栈数据结构,但回溯过程体现了栈的思想:

// 回溯过程就像沿着"前任记录本"往回找
while (u-- > 0) {
    result[u] = arr[v];
    v = p[v];  // 找到前一个人
}

栈的作用比喻

  • 就像**“倒带回放”**,从最后一个正确位置开始,一步步往前追溯
  • 每次回溯都是一次"出栈"操作,直到找到完整的排队序列

3.3 为什么用索引存储?

result数组中,我们存储的不是老头的年龄,而是他们在原队伍中的位置编号

好处

  1. 节省内存:索引通常是小整数,比存储完整对象省空间
  2. 快速定位:通过索引可以快速找到原始数据
  3. 避免重复:相同值的元素可以通过索引区分

坏处

  1. 间接访问:需要通过索引再去访问真实数据
  2. 理解复杂:增加了一层抽象,不够直观

比喻说明
就像给每个老头发一个号码牌,我们记录的是号码牌序列[2, 5, 3],而不是年龄序列[70, 65, 80]。这样既节省纸张,又能快速找到对应的人。

四、贪心算法:为什么要找"最小的"?

4.1 贪心策略的核心

if (arrI < arr[result[u]]) {
    // 找到更小的值,替换掉
    result[u] = i;
}

为什么要贪心地选择最小值?

想象老头排队的场景:

  • 当前位置需要一个年龄为70的老头
  • 来了两个候选人:一个72岁,一个68岁
  • 选择68岁的老头,因为他为后面更大年龄的老头留下了更多可能性

4.2 贪心的必要性

如果不选最小的会怎样?

错误示例:
原序列:[60, 80, 70, 90]
如果在位置1选择80而不是70:
- 序列变成:[60, 80, ?, ?]
- 后面的70就无法加入了
- 最终长度只有2

正确示例:
选择70:
- 序列变成:[60, 70, ?, ?]  
- 后面的80、90都可以加入
- 最终长度为4

比喻:就像搭积木,每一层都要为上面的积木留出最大的可能性。选择最小的值就是为后续元素"让路"。

五、二分查找:快速定位的艺术

5.1 二分查找的使用条件

// 二分查找的前提:result数组必须是有序的
let left = 0, right = result.length - 1;
while (left < right) {
    let mid = (left + right) >> 1;
    if (arr[result[mid]] < arrI) {
        left = mid + 1;
    } else {
        right = mid;
    }
}

使用条件

  1. 数组必须有序result数组中存储的索引对应的值必须是递增的
  2. 查找目标明确:要找到第一个大于等于目标值的位置

5.2 二分查找的比喻

想象在图书馆找书:

  • 线性查找:从第一本书开始,一本本翻,直到找到目标书籍
  • 二分查找:每次翻到中间,比较后决定往左找还是往右找

在老头排队中:

  • 新来一个75岁的老头
  • 不需要从头开始比较,直接跳到队伍中间
  • 发现中间是70岁,75>70,所以往右半边找
  • 继续二分,直到找到合适位置

六、DOM操作的奥秘:为什么排好序就能高效修改?

6.1 DOM的双重身份

真实DOM既是树结构也是双向链表

<!-- 树结构 -->
<div>
  <span>节点1</span>  ← → <span>节点2</span>  ← → <span>节点3</span>
</div>

树结构特性

  • 父子关系明确
  • 层级结构清晰

双向链表特性

  • 每个节点都有previousSiblingnextSibling
  • 可以快速在兄弟节点间移动

6.2 递增序列的优势

当我们找到最长递增子序列后:

// 假设最长递增子序列是:[1, 3, 4](索引)
// 对应的节点:[B, D, E]
// 这些节点保持不动!

旧:A B C D E F
新:B D E A C F

为什么这样高效?

  1. 保持稳定的锚点:递增序列的节点就像"定海神针",不需要移动
  2. 减少DOM操作:只需要移动非递增序列的节点
  3. 利用链表特性:通过insertBefore等API快速插入

6.3 DOM操作的比喻

想象重新整理书架:

  • 传统方法:把所有书都拿下来,重新按顺序放回去
  • Vue 3方法:找出已经放对位置的书(递增序列),只移动放错位置的书
// 伪代码示例
function moveNodes(oldNodes, newNodes, lis) {
    // lis中的节点保持不动
    for (let i = 0; i < newNodes.length; i++) {
        if (!lis.includes(i)) {
            // 只移动不在递增序列中的节点
            parentNode.insertBefore(newNodes[i], targetPosition);
        }
    }
}

七、算法完整实现代码

7.1 标准版本:返回LIS长度

/**
 * 最长递增子序列 - 标准版本(返回长度)
 * @param {number[]} nums - 输入数组
 * @return {number} - 最长递增子序列的长度
 */
function lengthOfLIS(nums) {
    if (!nums || nums.length === 0) return 0;
    
    // tails[i] 表示长度为 i+1 的递增子序列的最小尾部元素
    const tails = [];
    
    for (let num of nums) {
        // 二分查找插入位置
        let left = 0, right = tails.length;
        while (left < right) {
            const mid = Math.floor((left + right) / 2);
            if (tails[mid] < num) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        
        // 如果 left === tails.length,说明 num 比所有元素都大,直接追加
        // 否则替换 tails[left]
        if (left === tails.length) {
            tails.push(num);
        } else {
            tails[left] = num;
        }
    }
    
    return tails.length;
}

7.2 Vue 3版本:返回索引序列

/**
 * Vue 3 中的 getSequence 函数
 * @param {number[]} arr - 输入数组(通常是新旧节点的索引映射)
 * @return {number[]} - 最长递增子序列的索引数组
 */
function getSequence(arr) {
    const len = arr.length;
    const result = [0]; // 存储当前最长递增子序列的索引
    const p = new Array(len); // 存储前驱索引,用于回溯
    
    let i, j, u, v, c;
    
    for (i = 0; i < len; i++) {
        const arrI = arr[i];
        
        // Vue 3 特殊处理:0 表示新增节点,跳过
        if (arrI !== 0) {
            j = result[result.length - 1];
            
            // 如果当前元素比 result 中最后一个元素大,直接追加
            if (arr[j] < arrI) {
                p[i] = j; // 记录前驱
                result.push(i);
                continue;
            }
            
            // 二分查找:找到第一个大于等于 arrI 的位置
            u = 0;
            v = result.length - 1;
            
            while (u < v) {
                c = (u + v) >> 1; // 等价于 Math.floor((u + v) / 2)
                if (arr[result[c]] < arrI) {
                    u = c + 1;
                } else {
                    v = c;
                }
            }
            
            // 如果找到更小的值,替换它
            if (arrI < arr[result[u]]) {
                if (u > 0) {
                    p[i] = result[u - 1]; // 记录前驱
                }
                result[u] = i; // 替换
            }
        }
    }
    
    // 回溯构建完整的最长递增子序列
    u = result.length;
    v = result[u - 1];
    
    while (u-- > 0) {
        result[u] = v;
        v = p[v];
    }
    
    return result;
}

7.3 通用版本:返回实际序列

/**
 * 最长递增子序列 - 通用版本(返回实际序列)
 * @param {number[]} nums - 输入数组
 * @return {number[]} - 最长递增子序列的实际值
 */
function getLISSequence(nums) {
    if (!nums || nums.length === 0) return [];
    
    const len = nums.length;
    const dp = new Array(len).fill(1); // dp[i] 表示以 nums[i] 结尾的最长递增子序列长度
    const prev = new Array(len).fill(-1); // 前驱索引
    
    let maxLength = 1;
    let maxIndex = 0;
    
    // 动态规划求解
    for (let i = 1; i < len; i++) {
        for (let j = 0; j < i; j++) {
            if (nums[j] < nums[i] && dp[j] + 1 > dp[i]) {
                dp[i] = dp[j] + 1;
                prev[i] = j;
            }
        }
        
        if (dp[i] > maxLength) {
            maxLength = dp[i];
            maxIndex = i;
        }
    }
    
    // 回溯构建序列
    const result = [];
    let current = maxIndex;
    
    while (current !== -1) {
        result.unshift(nums[current]);
        current = prev[current];
    }
    
    return result;
}

7.4 优化版本:O(n log n) 时间复杂度

/**
 * 最长递增子序列 - 优化版本
 * @param {number[]} nums - 输入数组
 * @return {number[]} - 最长递增子序列的实际值
 */
function getLISOptimized(nums) {
    if (!nums || nums.length === 0) return [];
    
    const len = nums.length;
    const tails = []; // tails[i] 存储长度为 i+1 的递增子序列的最小尾部元素
    const tailsIndex = []; // 对应的索引
    const prev = new Array(len).fill(-1); // 前驱索引
    
    for (let i = 0; i < len; i++) {
        const num = nums[i];
        
        // 二分查找插入位置
        let left = 0, right = tails.length;
        while (left < right) {
            const mid = Math.floor((left + right) / 2);
            if (tails[mid] < num) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        
        // 更新前驱关系
        if (left > 0) {
            prev[i] = tailsIndex[left - 1];
        }
        
        // 更新 tails 数组
        if (left === tails.length) {
            tails.push(num);
            tailsIndex.push(i);
        } else {
            tails[left] = num;
            tailsIndex[left] = i;
        }
    }
    
    // 回溯构建序列
    const result = [];
    let current = tailsIndex[tailsIndex.length - 1];
    
    while (current !== -1) {
        result.unshift(nums[current]);
        current = prev[current];
    }
    
    return result;
}

7.5 测试用例

// 测试函数
function testLIS() {
    const testCases = [
        [10, 9, 2, 5, 3, 7, 101, 18], // 期望: [2, 3, 7, 18] 或 [2, 3, 7, 101]
        [0, 1, 0, 3, 0, 4, 5, 7],     // 期望: [0, 1, 3, 4, 5, 7]
        [7, 7, 7, 7, 7, 7, 7],        // 期望: [7]
        [1, 3, 6, 7, 9, 4, 10, 5, 6], // 期望: [1, 3, 4, 5, 6] 或其他
        []                             // 期望: []
    ];
    
    testCases.forEach((testCase, index) => {
        console.log(`测试用例 ${index + 1}: [${testCase}]`);
        console.log(`长度: ${lengthOfLIS(testCase)}`);
        console.log(`序列: [${getLISOptimized(testCase)}]`);
        console.log(`Vue3索引: [${getSequence(testCase)}]`);
        console.log('---');
    });
}

// 运行测试
testLIS();

7.6 算法复杂度分析

版本时间复杂度空间复杂度特点
通用版本(DP)O(n²)O(n)易理解,适合小数据
优化版本O(n log n)O(n)高效,适合大数据
Vue 3版本O(n log n)O(n)专门优化,返回索引

7.7 实际应用示例

// Vue 3 diff 算法中的应用示例
function patchKeyedChildren(oldChildren, newChildren) {
    // ... 前置处理 ...
    
    // 构建新旧节点索引映射
    const newIndexToOldIndexMap = new Array(toBePatched);
    for (let i = 0; i < toBePatched; i++) {
        newIndexToOldIndexMap[i] = 0; // 0 表示新增
    }
    
    // 填充映射关系
    for (let i = oldStartIndex; i <= oldEndIndex; i++) {
        const prevChild = oldChildren[i];
        const newIndex = keyToNewIndexMap.get(prevChild.key);
        if (newIndex !== undefined) {
            newIndexToOldIndexMap[newIndex - newStartIndex] = i + 1;
        }
    }
    
    // 获取最长递增子序列
    const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap);
    
    // 根据 LIS 结果移动节点
    let j = increasingNewIndexSequence.length - 1;
    for (let i = toBePatched - 1; i >= 0; i--) {
        const nextIndex = newStartIndex + i;
        const nextChild = newChildren[nextIndex];
        
        if (j < 0 || i !== increasingNewIndexSequence[j]) {
            // 需要移动的节点
            move(nextChild, container, anchor);
        } else {
            // 在 LIS 中的节点,不需要移动
            j--;
        }
    }
}

八、算法实现详解

8.1 核心数据结构

function getSequence(arr) {
    let len = arr.length;
    let result = [0];        // 最佳位置记录本
    let p = new Array(len);  // 前任记录本
    
    // ... 算法实现
}

8.2 构建过程

for (let i = 0; i < len; i++) {
    let arrI = arr[i];
    
    if (arrI !== 0) {  // Vue 3中0表示新增节点,跳过
        let u = result.length;
        let v = result[u - 1];
        
        if (arr[v] < arrI) {
            // 直接追加:新老头年龄更大,直接站到队尾
            p[i] = v;
            result.push(i);
        } else {
            // 二分查找:找到合适位置插入
            // ... 二分查找逻辑
        }
    }
}

8.3 回溯重建

// 沿着"前任记录本"回溯,重建完整序列
let u = result.length;
let v = result[u - 1];
while (u-- > 0) {
    result[u] = arr[v];
    v = p[v];  // 找前任
}

九、Vue 3中的特殊处理

9.1 为什么跳过0?

在Vue 3的diff算法中:

  • 0表示新增的节点
  • 新增节点不参与LIS计算
  • 只有已存在的节点才需要考虑移动

9.2 索引映射的巧思

// 新旧节点的索引映射
const keyToNewIndexMap = new Map();
for (let i = 0; i < newChildren.length; i++) {
    keyToNewIndexMap.set(newChildren[i].key, i);
}

// 构建索引数组用于LIS计算
const newIndexToOldIndexMap = new Array(toBePatched);
for (let i = 0; i < toBePatched; i++) {
    newIndexToOldIndexMap[i] = 0; // 0表示新增
}

十、性能优化的考量

10.1 时间复杂度分析

  • 暴力方法:O(n²) - 每个节点都要和其他节点比较
  • LIS方法:O(n log n) - 二分查找的威力
  • 空间复杂度:O(n) - 需要额外的数组存储

10.2 实际应用场景

Vue 3的diff算法特别适合:

  • 列表渲染:大量相似节点的重排
  • 动画过渡:需要保持某些元素稳定
  • 表格操作:行列的增删改查

十一、总结

Vue 3的diff算法通过最长递增子序列,巧妙地解决了DOM更新的性能问题。它的核心思想可以总结为:

  1. 最长递增子序列的本质:在保持相对顺序的前提下,找出最长的递增片段
  2. 有序性的巧妙利用:区分全局有序和局部有序,专注于局部优化
  3. 数据结构的合理运用:数组存储结果,栈思想回溯,索引节省空间
  4. 贪心策略:每一步都选择最优解,为后续留下最大可能性
  5. 二分查找:在有序数组中快速定位,提升查找效率
  6. DOM特性利用:利用DOM的双向链表特性,减少不必要的操作
  7. 保持稳定锚点:让已经正确的节点保持不动,只移动需要调整的节点
  8. 完整的代码实现:从标准版本到Vue 3专用版本,满足不同场景需求

这就像一个经验丰富的队长,能够快速识别出队伍中已经站对位置的人,然后用最少的调整让整个队伍变得有序。这种智慧不仅体现在算法的巧妙设计上,更体现在对实际应用场景的深刻理解上。

通过这种方式,Vue 3不仅提升了性能,还为开发者提供了更好的用户体验。当我们理解了这些原理后,就能更好地编写高效的Vue应用,让我们的代码像这些排队的老头一样,井然有序且高效运行。

<think>好的,我现在需要帮助用户比较Vue2和Vue3diff算法的具体区别。首先,用户的问题很明确,他们想了解两者的差异,但作为助手,我需要确保回答准确且结构清晰。根据用户提供的引用内容,我需要结合这些信息来组织答案。 首先,我需要回忆Vue2的diff算法Vue2采用的是双端比较算法,通过四个指针(新旧节点的头尾)进行比较,尽可能复用节点。但这种方法在存在大量静态内容时效率不高,因为每次都要全量对比。另外,Vue2不支持多根节点组件,需要包裹一个根元素,这在处理复杂结构时可能不够灵活。 接下来是Vue3的改进。根据引用[1],Vue3引入了PatchFlag和静态提升,优化了动态节点的追踪。PatchFlag可以在编译阶段标记动态节点,这样在diff时只需关注这些标记的部分,减少不必要的对比。同时,Vue3支持Fragment(多根节点),提高了灵活性。另外,Vue3diff算法在处理列表时,使用最长递增子序列来最小化移动次数,这在引用[2]中提到,通过建立映射表和新旧节点的比较,找出最少的移动操作。 我需要将这些点分门别类地对比,比如对比范围、多根节点支持、事件处理、编译优化等。用户提供的引用中有提到这些方面,需要正确引用并标注。例如,Vue3的PatchFlag和Block Tree结构是在编译时优化的,而Vue2没有这些优化。 还要注意用户可能对技术细节感兴趣,比如最长递增子序列的具体应用,或者PatchFlag如何工作。因此,在回答中需要简明扼要地解释这些概念,同时保持专业性。例如,在解释最长递增子序列时,可以举一个简单的例子,说明如何通过该算法减少节点移动次数,提升性能。 另外,用户的问题可能涉及性能影响,需要指出Vue3在处理静态内容时几乎零开销,而Vue2在存在大量静态节点时效率较低。这部分可以结合引用[1]中的内容进行说明。 最后,生成的相关问题需要与主题紧密相关,帮助用户进一步探索。例如,询问PatchFlag的工作原理,或如何利用Vue3diff优化项目性能,都是合理的后续问题。 总结一下,回答的结构应该是先概述两者差异,然后分点详细对比,包括节点对比范围、多根节点支持、事件处理、编译优化、性能影响等,每个部分结合引用内容,并标注引用来源。最后生成相关问题,引导深入学习。</think>### Vue2与Vue3 Diff算法差异详解 #### 1. **节点对比范围优化** - **Vue2**:全量对比新旧虚拟DOM树,遍历所有节点,无论是否包含动态内容。例如: ```javascript // Vue2伪代码逻辑 function patch(oldVnode, newVnode) { if (oldVnode.tag === newVnode.tag) { // 递归对比所有子节点 } } ``` - **Vue3**:通过`PatchFlag`标记动态节点,仅对比动态内容。例如: ```javascript // Vue3伪代码逻辑 if (patchFlag & DYNAMIC_SLOTS) { // 仅处理动态插槽相关节点 } ``` 这种优化使得静态内容(如纯文本)完全跳过对比流程[^1]。 #### 2. **多根节点支持** - **Vue2**:强制要求组件模板必须包裹在单一根节点中,否则会报错: ```html <!-- 非法写法 --> <div>A</div> <div>B</div> ``` - **Vue3**:支持`<Fragment>`多根节点组件,减少不必要的DOM层级: ```html <!-- 合法写法 --> <template> <header>标题</header> <main>内容</main> </template> ``` 这一特性通过`Block Tree`结构实现动态节点追踪[^1]。 #### 3. **列表对比策略** - **Vue2**:使用双端对比算法(头尾指针交叉比对),时间复杂度为$O(n^2)$: ```plaintext 旧节点: A-B-C-D 新节点: D-A-B-C 比对过程需多次移动节点 ``` - **Vue3**:基于`key`建立映射表后,通过**最长递增子序列LIS)**算法优化移动次数: ```javascript // 示例映射表 const oldIndexMap = { A:0, B:1, C:2, D:3 } const newSequence = [D, A, B, C] const lis = findLIS([3,0,1,2]) // 结果为[0,1,2] ``` 此算法将移动操作复杂度降低到$O(n \log n)$[^2]。 #### 4. **编译时优化** - **Vue2**:运行时才进行动态内容分析,无预编译优化。 - **Vue3**:在编译阶段完成以下优化: - **静态提升(Hoist Static)**:将静态节点提取到渲染函数外 - **Block Tree**:将动态节点按层级划分为块(Block) - **PatchFlag**:标记动态绑定类型(如`1`表示文本变化,`2`表示class变化) #### 5. **性能对比** | 场景 | Vue2处理方式 | Vue3处理方式 | |--------------------|-------------------|--------------------| | 含100个静态节点列表 | 全量对比100次 | 仅对比1个动态节点 | | 多根节点组件 | 需包裹额外div | 直接渲染 | | 事件监听器更新 | 每次重新绑定 | 缓存并复用 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gazer_S

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值